info values become materialized context for a read-only Codex subprocess. Keep feedback concrete and bounded: include the failing output, expected behavior, and a focused next change — not unrelated logs.
The contract
score: float— higher is better. This is what the optimizer maximizes and selects on.info: dict— free-form feedback shown to the proposer as Actionable Side Information (ASI). This is the single biggest lever on mutation quality.- For single-task runs the signature is
evaluate(candidate). Withdatasetorvalset, it isevaluate(candidate, example). Returning a barefloatalso works — the wrapper normalizes it — but the proposer then gets no feedback. Always return the tuple.
Batched form
When evaluations batch better than they stream — a provider batch API, one job submission per stage, or fan-out over your own infrastructure — write the batched form:info applies per pair. Put diagnostics in each returned info. The per-call channels (oa.log(), capture_stdio) do not apply to the grouped call.
Feedback-rich info
The proposer writes the next candidate by reading info. Give it specifics:
info could tell you how to fix the candidate, the proposer LLM can too. If info is {"score": 0.0}, the search is blind.
Built-in diagnostic channels
oa.log().import gepa.optimize_anything as oa; oa.log("landing distance:", d)inside your evaluator. Same calling convention asprint(); output is captured per-eval (thread-safe) and auto-included in the feedback underinfo["log"]. For child threads, propagate the context viaoa.get_log_context()andoa.set_log_context().capture_stdio. SetGEPAConfig(engine=EngineConfig(capture_stdio=True), ...)and anyprint(),stdout, orstderrduring evaluation lands in the feedback under"stdout"or"stderr". This does not catch C-extension or subprocess output that bypasses Python’ssys.stdout— route that throughoa.log().
LLM-as-judge scoring
For open-ended tasks (writing quality, helpfulness, tone, rubric adherence) the evaluator can call an LLM judge and use its rating as the score, then return the written critique as feedback:Stochastic systems
The eval server calls your function once per (candidate, example) pair. There is nosamples_per_eval knob. For a temperature > 0 model, every score becomes a single-sample estimate and candidate selection then runs on noisy numbers.
Average N samples inside evaluate:
EngineConfig.max_metric_calls.
Multi-objective optimization
GEPA can keep an objective-level Pareto front. Return per-objective metrics underinfo["scores"] — the adapter forwards them as objective_scores:
EngineConfig(frontier_type="hybrid") inside GEPAConfig(engine=...). Hybrid is the default (instance-level and objective-level fronts combined). "objective", "instance", and "cartesian" are the alternatives. The scalar score still drives final selection; the per-objective scores shape the frontier that candidates are drawn from.
Reward-hacking-resistant scoring
The optimizer maximizes exactly what you write. A correctness-only score is gameable — e.g. the optimizer learns to emit a trivial wrapper that is “correct” but does nothing useful. Gate the score on validity and correctness, then increase it only for the real objective:Determinism and robustness
- Make
evaluateside-effect-free and resumable. It may run concurrently (EngineConfig.max_workers) and be retried. - Set a seed in the GEPA engine’s nested
EngineConfig(seed=0)for reproducible search order. - Log your own per-eval record (id, score, sub-metrics, candidate hash) for analysis. A configured
run_dirretains GEPA’s run log and state, andoa.log()covers in-feedback diagnostics. - Catch and return failures as low scores with
info["error_*"], rather than raising.EngineConfig.raise_on_exceptiondefaults toTrue, so an uncaught exception aborts the run. Setting it toFalseconverts exceptions to score0.0withinfo["error"].