Add an Optimizer#
A method is one package, optimizers/<method>/, whose integration.py exposes an optimizer class registered with the run protocol. The protocol hands it a budgeted runner and the train and validation rows, and does everything else: final validation, selection, test and artifacts. Your results are then comparable with the other eight methods by construction.
What the protocol owns#
| The protocol | The method |
|---|---|
| Builds the cell's adapter, the seed bundle and the dataset metric. | Proposes candidate bundles. |
Charges usable rollouts to a BudgetLedger of B = 600 and never overshoots it. |
Decides which rows and bundles to run, within B. |
| Retries infrastructure failures with the same seed, uncharged. | Chooses its incumbent. |
| Runs the uncharged final validation and the strictly-better selection. | Nothing: it never sees the selection. |
| Runs the test split after the selection is locked. | Nothing: test rows never reach it. |
Writes job.json, optimization.json, selection.json, test.json and result.json. |
Writes optional scratch files under its run_dir. |
Evaluation Protocol defines each of these steps.
1. Create and register the package#
Put the method in optimizers/<method>/, with its optimizer class in integration.py and its knobs in one frozen settings dataclass, as the existing methods do. Register the class in METHODS in optimizers/protocol/methods/__init__.py:
METHODS: dict[str, str] = {
"gepa": "optimizers.gepa.integration:GEPAOptimizer",
# ...
"my_method": "optimizers.my_method.integration:MyOptimizer",
}
The registry is lazy: nothing is imported until a job asks for the method, and the --method choices of optimizers.protocol.run come from it. identity.py is the smallest complete method: it returns the seed bundle without a rollout.
2. Implement the interface#
class MyOptimizer:
def __init__(self, seed_bundle: PromptBundle, *, run_dir=None, reflection_lm=None): ...
def optimize(self, cell: CellSpec, runner, budget: BudgetLedger,
training: list[dict], validation: list[dict]) -> OptimizerResult: ...
- Constructor.
methods.build_optimizerpasses the seed bundle asseed_bundleorinitial_bundle, whichever the signature declares. If declared, it also passesrun_dir(a scratch folder,optimization/native/),reflection_lm(dspy_bridge.build_reflection_lm()) andtask_lm(dspy_bridge.build_task_lm()). - Bundles. A
PromptBundleholdsroles(role name to prompt text), optionaldemosandmetadata; itsdigestcovers all three. A candidate must have exactly the seed bundle's roles. - Rows. JSON-safe dicts with an
id, atask_instanceand the gold labels. Pass them to the runner unchanged. - Runner.
ProtocolRunner.run(example, bundle, request_seed) -> RunRecordandrun_batch(examples, bundle, request_seeds) -> BatchExecution, plusbudget,cell,phase,seed_bundle/initial_bundle,required_roles,validate_bundle,artifact_store,artifact_directoryandsupports_concurrent = False. A batch reserves budget before dispatch and is trimmed at B. - Records. A
RunRecordcarriesscore,status,final_output,messages,usageand the metric's feedback text inmetadata["scorer_metadata"]["feedback"]. Onlysuccessandsemantic_failurerecords are usable and charged.
The protocol package has building blocks for the common cases: seeding.request_seeds for logical request seeds, rollouts for text methods, session.RunnerSession for a method with its own adapter-driven loop, reflection.ReflectionClient for the reflection model, and methods/dspy_bridge.py for DSPy programs. Rollouts are serialized per process; run jobs in parallel as processes.
3. Return a result#
optimize returns a schema.OptimizerResult with method, cell_id, the seed and incumbent bundles, budget_snapshot == budget.snapshot() and a schema.StopReason. The protocol rejects a result whose identity or budget snapshot differs from the job, or whose incumbent the runtime cannot execute.
- Layouts. The result is published as
SEARCH_LAYOUT(the incumbent asselected_bundle; GEPA, MIPRO, MASPOB and TAVO) orJOURNAL_LAYOUT(incumbent_bundle;identityand theRunnerSessionmethods, throughjournal.LifecycleJournal). - Errors. All exceptions live in
optimizers/protocol/errors.py. An infrastructure failure (PreObservationInfrastructureFailure,NativeInfrastructureExhausted,OptimizerInfrastructureFailure) keeps the seed bundle with the fallback reasoninfrastructure_invalid_optimization; any other exception fails the job. - Masked execution. A bundle with
metadata["optimizer_control"]runs through the hook registered byrunner.register_execution_hook("optimizer_control", hook), which providesbuild_adapter(runtime, request, control)andverify(runtime, request, control, output); without a hook it is rejected.
4. Test it#
-
Unit tests go to
optimizers/<method>/tests/. Use fakes (seeoptimizers/conftest.py) so they need no endpoint:PYTHONDONTWRITEBYTECODE=1 python -m pytest -p no:cacheprovider optimizers/<method> -q -
Smoke job. A new method is outside the experiment grid, so it needs
--allow-any-cell. Methods charge rollouts before proposing anything, so pick a budget that reaches one proposal (see smoke runs):python -m optimizers.protocol.run --method my_method --dataset hotpotqa \ --topology centralized --model qwen --seed 0 --budget 60 --allow-any-cell \ --out runs/smoke/my_method -
Golden cell. Add a row to
METHOD_CELLSintests/golden/methods.py. These cells pin each method's end-to-end trace against a scripted fake server. Record it withpython -m tests.golden.record --only 'methods/my_method/*'.
Jobs outside the grid are non-conformant, so optimizers.protocol.aggregate skips them unless you pass --include-nonconformant. The grid of published cells is build_grid in optimizers/protocol/cells.py.
Checklist#
optimizers/<method>/integration.pywith the optimizer class and a frozen settings dataclass.- A
METHODSentry inoptimizers/protocol/methods/__init__.py. optimizespends only the runner's ledger, uses train and validation rows only, and returns anOptimizerResultwith the matching budget snapshot.- Seed prompts in
configs/left untouched; scratch files underrun_dir. - Unit tests with fakes, a smoke job and a golden cell.