Quickstart — evolve an agent's code¶
A complete, measured case: one evolve() call that takes a deliberately
minimal running agent — a bare model call and a one-line instruction file —
and evolves it on GSM-Hard against a hosted thinking model
(deepseek-v4-flash behind an Anthropic-shaped endpoint). One run, 83
minutes: test 0.692 → 0.757 on a 107-problem split the engine never saw.
The case makes one point above the others: the winning configuration was not a cleverer architecture. It was a lower starting point and a larger budget, with the engine free to decide what the agent should become.
What evolves¶
The artifact is a two-file tree, executed for real on every rollout:
agent.py— initially ~40 lines: readskills/, send one prompt, print the reply. No tools, no retries, no output parsing.skills/strategy.md— initially the single lineSolve the problem.
Both are editable (FileTree, at most 2 files per diff). A rollout
materialises the candidate into a workspace and runs
python3 agent.py "<question>" through code_runner — process isolation,
trimmed environment, hard timeout. Reward is exact-match on the last number
of stdout, normalised the way GSM-Hard's float targets require
(-9867630.0 ≡ -9867630).
Data: 320 GSM-Hard problems (the offline sample shipped in
examples/_gsmhard_sample.json), split 106 train / 107 held-out / 107 test
by a seeded shuffle. The engine trains and gates on the first two; test is
measured once before and once after.
The call¶
result = evolve(
train + held_out, reward, run=run, propose=propose,
strategy=FileTree(initial_files=INITIAL, max_files_per_diff=2),
artifact_id="dgm-agent",
blast_radius=0.6, # agent code is a harness: L1, oracle-gated
n_workers=6, asynchronous=True, async_ratio=3,
eval_concurrency=16,
rounds=30, max_rollouts=180, patience=8, target_reward=0.95,
self_verify=False, cheap_eval_tasks=6,
agg_config=AggregatorConfig(bounded_gate=True, base_delta=0.8,
anneal_half_life=256, batch_trigger=4),
policies=Policies(**reflective_merge(fusion_model, max_proposals=4)),
staleness_policy=ReflectiveStaleness(),
held_out_frac=0.5, shuffle=False, seed=0,
)
Knobs that are the lessons of this series, not defaults:
max_rollouts=180— budget is what lets the engine climb in steps. Runs of 60–72 rollouts on the same task plateaued after one commit; this run committed three times, each on top of the last.base_delta=0.8, anneal_half_life=256— a relaxed, flat acceptance threshold. Real fixes on this workload are worth 1–4 held-out tasks each; under the default schedule the Beta posterior wants more lift than that and correct small fixes die at the gate.max_proposals=4/batch_trigger=4— reflective merge synthesises a few competing proposals per model call rather than summarising many.- No
selection=— single head. With an archive (Archive("sigmoid_novelty")) commits land on divergent lineages that never recombine; single-head stacks every accepted fix on one lineage. - Reflector and fusion completions run
thinking={"type": "disabled"}. On the edit-protocol prompt this model's reasoning runs away (measured: 32,768 output tokens, zero visible text). Thinking-off returns a valid<EDITS>block in ~12s. The agent's own solve calls keep thinking on.
The reflector template asks for a diagnosis before the edit — classify the failure as output formatting / arithmetic / comprehension, then make the smallest generalising fix — and it states the grader's comparison rule outright, because a reflector that has to guess the grader fixes the wrong layer.
What one run did¶
83 minutes wall clock: 185 rollouts, 988 reflector/fusion calls, 1 transient
call failure. Eight merger sweeps; every accepted commit was a
ReflectiveFusion synthesis of 7–9 concurrent proposals, taken through the
statistical gate and the L1 oracle re-check:
| sweep | held-out | event |
|---|---|---|
| 0 | 0.766 | — |
| 1 | 0.766 | 1 oracle-rejected |
| 2 | 0.794 | commit — synth of 7 proposals |
| 3 | 0.794 | 1 oracle-rejected |
| 4 | 0.804 | commit — synth of 8 proposals |
| 5 | 0.804 | 1 oracle-rejected |
| 6 | 0.822 | commit — synth of 9 proposals |
| 7 | 0.822 | budget exhausted |
merge synth(w0:6a7fd1d6 + w1:6c55d989 + w2:8d9c6a01 + w3:0c549fbd + …) -> dgm-agent (sweep 2)
merge synth(w0:d2944aa8 + w1:88cac018 + w1:d78106da + w2:41380c5e + …) -> dgm-agent (sweep 4)
merge synth(w0:61ca8769 + w1:53943c35 + w2:3c1c1e47 + w2:ef0695da + …) -> dgm-agent (sweep 6)
What it evolved into¶
The one-line skills/strategy.md grew into a comprehension rulebook —
the engine's own diagnosis of where this model actually loses points on
GSM-Hard. Excerpts, verbatim:
For phrases like … 'three times more than X', interpret it as 'three times as many as X' (i.e., 3 × X), not 'X + 3×X'.
The new value after applying a percentage rate that repeats every T units of time is computed by compounding: new_value = base_value * (1 + (percentage/100))^(number_of_periods).
Do not multiply a price by a count unless the phrase explicitly says 'each' or 'per item'.
When a problem asks 'How many will not be used' … compute the remainder as initial_total - (used_per_recipient * number_of_recipients). This remainder may be negative … and that negative value is the correct answer.
Output the number as a plain decimal with full precision (do not round, do not add trailing zeros, do not add commas…).
…plus a restate-then-verify protocol (state the interpretation before
computing, re-read the question after). agent.py gained a retry loop
around the model call and — a caveat worth naming — one narrow hard-coded
rate-problem heuristic that slipped through the relaxed gate because its
held-out footprint happened to be positive. A looser gate admits more real
fixes and the occasional stowaway; the test number below includes both.
What it was worth¶
| held-out (gates on it) | test (never seen) | |
|---|---|---|
| before | 0.766 | 0.692 |
| after | 0.822 (+6 tasks) | 0.757 (+7 tasks, +6.5pp) |
For calibration on this same split: evolving only a prompt-policy function (the Gödel-agent port) reached 0.720, and a hand-designed write-a-program-and-exec agent also reached 0.720. The evolved rulebook beats both — and a per-item audit puts the honest ceiling near 0.879, because 13 of the 107 test items are corrupted upstream (the question's substituted numbers disagree with the numbers the gold-computing reference code used, so no faithful solver can match the gold).
Two conclusions to carry out of this case:
- Give evolution room instead of architecture. Every designed variant — prescribed tool contracts, dual-path solvers with arbitration — measured at or below what the engine reached on its own from a bare agent. The headroom was in comprehension rules, and the engine found that without being told.
- Budget converts to staircase commits. Three synthesis commits, each over the last, each clearing the gate and the L1 oracle — the shape that short runs on this workload never reached.
Reproducing¶
All public surface: evolve() with FileTree + code_runner +
tree_reflector (custom diagnose-first template), reflective_merge,
ReflectiveStaleness, and the offline GSM-Hard sample
(AGENTDESCENT_GSMHARD_SAMPLE=1). Two environment notes that cost this
series real hours: construct the Anthropic client with max_retries=0 (the
SDK's internal retries multiply with with_retries into ~45-minute stalls
on a slow endpoint), and give thinking models a generous max_tokens — at
1024 a thinking model returns empty text for every reflection and the
run silently proposes nothing.