gepa — sample-efficient reflective Pareto search
algorithms/hill-climb owns the mechanics every algorithm shares: parent →
proposal → val gate → commit, specified once in
algorithms/hill-climb/references/run-step.md. Read that first. This page states
only what GEPA (Agrawal et al., 2025) does differently, and why those
differences are the paper's actual contribution rather than decoration. A thin
wrapper over cap_evolve.gepa.gepa_loop.
The two mechanisms, and why removing either turns GEPA back into hill-climb
1. The parent is sampled from per-instance winners, not from the global best.
A mean is a lossy summary. A candidate that fixes one genuinely hard task while
regressing three easy ones has a worse mean than the incumbent, so a
best-parent rule discards it — and with it the only text in the pool that has
ever solved that task. GEPA instead scores per val instance and samples
frequency-weighted over candidates that (co-)win at least one, so specialists and
stepping-stones stay reachable as parents while their mean is still behind. That
is the quality-diversity argument (MAP-Elites): keep the set that covers the
task distribution, not the single champion. Sampling is stochastic and seeded, so
the exploration is reproducible.
2. A cheap train minibatch pre-gates the expensive val evaluation. Rollouts
dominate cost and a full-val eval costs |val| · n_trials of them. Most
proposals are bad; paying full price to find that out is what makes naive
reflective search unaffordable, and GEPA's headline "~35× fewer rollouts" comes
almost entirely from not paying it. So parent and child are evaluated on the
same small train minibatch (2 · minibatch-size rollouts, eval-cached) and
the child is dropped unless sum(child) > sum(parent). The minibatch never
decides acceptance — it decides whether acceptance is worth measuring.
A side benefit of (2): reflection reads train traces, so the proposer never
sees the split its gate is computed on.
What differs from hill-climb, step by step
- Parent — frequency-weighted sample over per-instance (co-)winners
(
--selection-strategy, default pareto_per_instance), not the current best.
- Signal — a minibatch of
--minibatch-size (default 4) train ids,
evaluated with traces, instead of the whole train focus set.
- Reflective dataset —
REFLECTION.md in the optimizer workdir, over the
parent's failing minibatch tasks (phases/diagnose owns what one is and what
shape it takes). "Failing" is the hard threshold reward < 1.0, so with a
graded scorer that never reaches 1.0 every sampled task is listed and the
header always reads 0/N pass — read it as "sampled tasks, worst first". Each
entry is truncated to ~800 chars and at most 12 tasks are written: a summary,
not an archive; untruncated rollouts stay in rollouts/train/. The prompt also
carries the run's cross-iteration files (LEDGER.md, JOURNAL.md, PROCESS.md,
RUNMAP.md + prior_iterations/) so a proposal builds on prior work. All five are
real here since #396: harness.record_iteration writes the step event those
files are built from and folds the optimizer's appended JOURNAL.md entry back
into the run-level handover, so the history accumulates across iterations.
- Local gate — child on the same minibatch,
sum(child) > sum(parent), else
dropped with no val spend. This is the extra stage; everything after it is
hill-climb's.
- Merge — every
--merge-cadence accepts, find two strict-frontier
dominators sharing a common ancestor both beat and recombine them
component-by-component (each component from whichever descendant changed it),
then minibatch-gate and val-gate the result like any other child.
Note the word "frontier" covers two different sets here: the sampling pool in
step 1 is every candidate with ≥1 instance win, which can include dominated
candidates; the strict per-task Pareto frontier (selection.pareto_frontier) is
a subset of it and is what the merge and the reported frontier_size use.
Component selection
A component is one editable file of the candidate. (Unrelated to
hill-climb's --focus, which selects tasks; this selects files.)
--component-selector round_robin (default): one component per iteration,
cycled, written to FOCUS.md. Small attributable changes are exactly the unit
the merge can later recombine — a sprawling multi-file rewrite cannot be.
--component-selector all: list every component; the optimizer may edit
anywhere. Use for monolithic capabilities or genuinely cross-cutting changes.
For a single-file capability the two coincide and the merge skips gracefully
(gepa_merge_skip) rather than emitting a degenerate child.
Key hyperparameters
--max-metric-calls (default 0 = unlimited): PRIMARY budget, checked
between iterations. An in-flight iteration runs to completion, so actual
spend can exceed it by up to 2·minibatch-size + |val|·n-trials — and a merge
fires inside an accepting iteration, adding 3·minibatch-size (the merge and
BOTH parents, on a freshly sampled minibatch) plus a second |val|·n-trials,
for a worst case of 5·minibatch-size + 2·|val|·n-trials. Set it below your
hard ceiling.
--max-iterations (default 50): secondary cap on propose→gate iterations.
--minibatch-size (default 4): train ids per cheap local gate.
--n-trials (default 1): rollouts/task on the full-val eval (raise under noise
so the significance gate is trustworthy). Minibatch evals are always 1 trial.
--max-merges (default 2): cap on merge attempts that built a candidate —
a merge rejected at either gate consumes one. A skip (no eligible pair) is free.
--merge-cadence (default 3): accepts between merge attempts.
--protected-paths (empty = off; default = the built-in globs): seals the
eval surface (scorer/gold/tasks/tests).
A child that edits one is INDECISIVE — no reward recorded, not remembered
as rejected, stall counter untouched — because scoring a gold-hacking edit at
all would teach the optimizer that it worked. It still charges
spent.iterations (record_iteration(..., indecisive=True)): the rollouts and
the optimizer call were really spent, so the spend meter counts it and only the
evidence meter does not.
--workers (default 1): pools the minibatch rollouts. Only safe when the
adapter's run_target is thread-safe.
--store / --store-commit-cmd (default git): where accepted candidates are
committed.
--gate-mode / --k-se, --no-regression, --seed: as hill-climb.
--resume: rebuild pool/lineage/frontier from gepa_state.json + each
accepted candidate's rollouts and continue the search. Preserved spend keeps
the budget honest; the parent-sampling RNG stream restarts, so a resumed run is
not byte-identical.
Known gaps (present tense — the shipped loop, not the paper)
Two. Re-derived against current main, because most of what this section used to
list has since been fixed in core: the hollow eval-cache reflection by #387, the
missing step record and the non-accumulating JOURNAL.md by #396
(harness.record_iteration is now the one place every algorithm ends an iteration,
and GEPA calls it), and the dirty snapshots plus the un-excluded optimizer-agent
dotfiles by #350 and #386. Check the two below before trusting them.
- The reflective dataset does not carry the task input, though
_write_reflection's own docstring says each failing task "contributes its
input" (gepa.py:230). It writes Agent output / Trajectory / Feedback and
no input field (:263-267), so the optimizer sees a bad answer to a question it
cannot read. Since #387 the output and trace survive an eval-cache hit, so the
entry is no longer hollow — just anonymous.
- If
splits.train is empty the minibatch silently falls back to val ids
(gepa.py:530), putting the gate split in front of the proposer, with no
warning. Do not run GEPA with a zero-size train split.
How to run
python scripts/check.py # behavioral, offline (mock optimizer + synthetic adapter)
python scripts/run.py --run-dir .capevolve/run_X --project .capevolve/project \
--optimizer 'python .../run-optimizer/scripts/run.py --name mock --workdir {workdir} --prompt {prompt}' \
--max-metric-calls 400 --minibatch-size 4 --component-selector round_robin
Requires baseline first (reads the seed's full-val result from baseline.json).
Reports the pool, frontier_size, best candidate, accepts, merges, and
metric-calls spent.
Agent-mode loop
When orchestration_mode: agent, follow orchestrate/orchestrate §Agent-mode
loop for the shared rules, and make each round GEPA-shaped: pick the parent by
per-instance win count; sample minibatch-size train ids; evaluate parent
then child on that same minibatch and drop the child unless sum(child) > sum(parent); only then pay for a full val eval and its gate. Reflect on the
train minibatch, never on val — val is the judge, not the teacher.
References
references/concepts.md — the paper's thesis (language as a richer learning
medium than a scalar), the frequency-weighted per-instance frontier, the
system-aware merge and its tie-breaking, the metric-call/eval-cache accounting,
and how the pieces relate to the hill-climb / skillopt siblings. Load when you
need the reasoning behind a knob rather than its value. Cites arXiv:2507.19457.
1---2name: gepa3description: Runs the GEPA optimization loop (arXiv:2507.19457) — sample-efficient reflective Pareto search. A cheap train-minibatch pre-gate decides whether a proposal is worth an expensive val evaluation, and parents are sampled from a per-instance frontier so specialists survive instead of being averaged away. Use when rollouts are expensive and the scorer returns informative per-task feedback, and you want the most quality per evaluation. Use hill-climb instead for a first baseline run or for feedback-poor binary pass/fail tasks.4---56# gepa — sample-efficient reflective Pareto search78`algorithms/hill-climb` owns the mechanics every algorithm shares: parent →9proposal → val gate → commit, specified once in10`algorithms/hill-climb/references/run-step.md`. Read that first. This page states11only what GEPA (Agrawal et al., 2025) does **differently**, and why those12differences are the paper's actual contribution rather than decoration. A thin13wrapper over `cap_evolve.gepa.gepa_loop`.1415## The two mechanisms, and why removing either turns GEPA back into hill-climb1617**1. The parent is sampled from per-instance winners, not from the global best.**18A mean is a lossy summary. A candidate that fixes one genuinely hard task while19regressing three easy ones has a *worse* mean than the incumbent, so a20best-parent rule discards it — and with it the only text in the pool that has21ever solved that task. GEPA instead scores per val instance and samples22frequency-weighted over candidates that (co-)win at least one, so specialists and23stepping-stones stay reachable as parents while their mean is still behind. That24is the quality-diversity argument (MAP-Elites): keep the *set* that covers the25task distribution, not the single champion. Sampling is stochastic and seeded, so26the exploration is reproducible.2728**2. A cheap train minibatch pre-gates the expensive val evaluation.** Rollouts29dominate cost and a full-val eval costs `|val| · n_trials` of them. Most30proposals are bad; paying full price to find that out is what makes naive31reflective search unaffordable, and GEPA's headline "~35× fewer rollouts" comes32almost entirely from *not* paying it. So parent and child are evaluated on the33**same** small train minibatch (`2 · minibatch-size` rollouts, eval-cached) and34the child is dropped unless `sum(child) > sum(parent)`. The minibatch never35*decides* acceptance — it decides whether acceptance is worth measuring.3637A side benefit of (2): reflection reads **train** traces, so the proposer never38sees the split its gate is computed on.3940## What differs from hill-climb, step by step41421. **Parent** — frequency-weighted sample over per-instance (co-)winners43 (`--selection-strategy`, default `pareto_per_instance`), not the current best.442. **Signal** — a minibatch of `--minibatch-size` (default 4) **train** ids,45 evaluated with traces, instead of the whole train focus set.463. **Reflective dataset** — `REFLECTION.md` in the optimizer workdir, over the47 parent's failing minibatch tasks (`phases/diagnose` owns what one is and what48 shape it takes). "Failing" is the hard threshold `reward < 1.0`, so with a49 graded scorer that never reaches 1.0 every sampled task is listed and the50 header always reads `0/N pass` — read it as "sampled tasks, worst first". Each51 entry is truncated to ~800 chars and at most 12 tasks are written: a summary,52 not an archive; untruncated rollouts stay in `rollouts/train/`. The prompt also53 carries the run's cross-iteration files (`LEDGER.md`, `JOURNAL.md`, `PROCESS.md`,54 `RUNMAP.md` + `prior_iterations/`) so a proposal builds on prior work. All five are55 real here since #396: `harness.record_iteration` writes the `step` event those56 files are built from and folds the optimizer's appended `JOURNAL.md` entry back57 into the run-level handover, so the history accumulates across iterations.584. **Local gate** — child on the same minibatch, `sum(child) > sum(parent)`, else59 dropped with no val spend. This is the extra stage; everything after it is60 hill-climb's.615. **Merge** — every `--merge-cadence` accepts, find two strict-frontier62 dominators sharing a common ancestor both beat and recombine them63 component-by-component (each component from whichever descendant changed it),64 then minibatch-gate and val-gate the result like any other child.6566Note the word "frontier" covers two different sets here: the *sampling pool* in67step 1 is every candidate with ≥1 instance win, which can include dominated68candidates; the strict per-task Pareto frontier (`selection.pareto_frontier`) is69a subset of it and is what the merge and the reported `frontier_size` use.7071## Component selection7273A **component** is one editable file of the candidate. (Unrelated to74hill-climb's `--focus`, which selects *tasks*; this selects *files*.)7576- **`--component-selector round_robin`** (default): one component per iteration,77 cycled, written to `FOCUS.md`. Small attributable changes are exactly the unit78 the merge can later recombine — a sprawling multi-file rewrite cannot be.79- **`--component-selector all`**: list every component; the optimizer may edit80 anywhere. Use for monolithic capabilities or genuinely cross-cutting changes.8182For a single-file capability the two coincide and the merge skips gracefully83(`gepa_merge_skip`) rather than emitting a degenerate child.8485## Key hyperparameters8687- `--max-metric-calls` (default 0 = unlimited): PRIMARY budget, checked88 **between** iterations. An in-flight iteration runs to completion, so actual89 spend can exceed it by up to `2·minibatch-size + |val|·n-trials` — and a merge90 fires *inside* an accepting iteration, adding `3·minibatch-size` (the merge and91 BOTH parents, on a freshly sampled minibatch) plus a second `|val|·n-trials`,92 for a worst case of `5·minibatch-size + 2·|val|·n-trials`. Set it below your93 hard ceiling.94- `--max-iterations` (default 50): secondary cap on propose→gate iterations.95- `--minibatch-size` (default 4): train ids per cheap local gate.96- `--n-trials` (default 1): rollouts/task on the full-val eval (raise under noise97 so the significance gate is trustworthy). Minibatch evals are always 1 trial.98- `--max-merges` (default 2): cap on merge **attempts that built a candidate** —99 a merge rejected at either gate consumes one. A skip (no eligible pair) is free.100- `--merge-cadence` (default 3): accepts between merge attempts.101- `--protected-paths` (empty = off; `default` = the built-in globs): seals the102 eval surface (scorer/gold/tasks/tests).103 A child that edits one is **INDECISIVE** — no reward recorded, not remembered104 as rejected, stall counter untouched — because scoring a gold-hacking edit at105 all would teach the optimizer that it worked. It still **charges106 `spent.iterations`** (`record_iteration(..., indecisive=True)`): the rollouts and107 the optimizer call were really spent, so the spend meter counts it and only the108 evidence meter does not.109- `--workers` (default 1): pools the minibatch rollouts. Only safe when the110 adapter's `run_target` is thread-safe.111- `--store` / `--store-commit-cmd` (default `git`): where accepted candidates are112 committed.113- `--gate-mode` / `--k-se`, `--no-regression`, `--seed`: as hill-climb.114- `--resume`: rebuild pool/lineage/frontier from `gepa_state.json` + each115 accepted candidate's rollouts and continue the search. Preserved spend keeps116 the budget honest; the parent-sampling RNG stream restarts, so a resumed run is117 not byte-identical.118119## Known gaps (present tense — the shipped loop, not the paper)120121Two. Re-derived against current `main`, because most of what this section used to122list has since been fixed in core: the hollow eval-cache reflection by #387, the123missing `step` record and the non-accumulating `JOURNAL.md` by #396124(`harness.record_iteration` is now the one place every algorithm ends an iteration,125and GEPA calls it), and the dirty snapshots plus the un-excluded optimizer-agent126dotfiles by #350 and #386. Check the two below before trusting them.127128- The reflective dataset does **not** carry the task input, though129 `_write_reflection`'s own docstring says each failing task "contributes its130 input" (`gepa.py:230`). It writes `Agent output` / `Trajectory` / `Feedback` and131 no input field (`:263-267`), so the optimizer sees a bad answer to a question it132 cannot read. Since #387 the output and trace survive an eval-cache hit, so the133 entry is no longer *hollow* — just anonymous.134- If `splits.train` is empty the minibatch silently falls back to **val** ids135 (`gepa.py:530`), putting the gate split in front of the proposer, with no136 warning. Do not run GEPA with a zero-size train split.137138## How to run139140```bash141python scripts/check.py # behavioral, offline (mock optimizer + synthetic adapter)142python scripts/run.py --run-dir .capevolve/run_X --project .capevolve/project \143 --optimizer 'python .../run-optimizer/scripts/run.py --name mock --workdir {workdir} --prompt {prompt}' \144 --max-metric-calls 400 --minibatch-size 4 --component-selector round_robin145```146147Requires `baseline` first (reads the seed's full-val result from `baseline.json`).148Reports the pool, `frontier_size`, best candidate, accepts, merges, and149metric-calls spent.150151## Agent-mode loop152153When `orchestration_mode: agent`, follow `orchestrate/orchestrate` §Agent-mode154loop for the shared rules, and make each round GEPA-shaped: pick the parent by155per-instance win count; sample `minibatch-size` **train** ids; evaluate parent156then child on that same minibatch and drop the child unless `sum(child) >157sum(parent)`; only then pay for a full **val** eval and its gate. Reflect on the158train minibatch, never on val — val is the judge, not the teacher.159160## References161162- `references/concepts.md` — the paper's thesis (language as a richer learning163 medium than a scalar), the frequency-weighted per-instance frontier, the164 system-aware merge and its tie-breaking, the metric-call/eval-cache accounting,165 and how the pieces relate to the hill-climb / skillopt siblings. Load when you166 need the reasoning behind a knob rather than its value. Cites arXiv:2507.19457.