Skip to content

Self-evolution algorithms — twenty ports

AgentDescent is a general engine for parallel, merge-based evolution. To show it is faithful to the field — not a toy — twenty published skill, program and harness self-evolution algorithms run on it, each as one runnable example with a dedicated page. Eight reproduce their paper's own benchmark; twelve preserve the mechanism on a compact domain and say so. What each one follows and where it departs is recorded per port in port fidelity.

All twenty run through the AgentDescent evolution engines. No example bypasses the engine, and they reach it three ways:

  • the eight benchmark-faithful ports are each a custom strategy= and/or a custom aggregator_factory=, with their parent/gate rules extracted as named policy classes at the standard seams (selection, acceptance);
  • the eleven microports and analogues are declarative MethodPolicy definitions over one shared runner — their mechanisms plug in as Policies(...) fields, their artifacts as shared strategies, and the runtime matrix measures them under all three schedulers;
  • Genesis is neither: a custom strategy= like the first group, a mechanism_microport fidelity class like the second, and its whole algorithm installed through Policies(proposal=, acceptance=, conflict=) — the case that shows the seams are enough on their own.

All twenty are parallel — and can run async. In synchronous mode their workers run concurrently (overlapping LLM rollouts) with the aggregator merge as the barrier (synchronous data-parallelism). Add --async and the same example runs barrier-free through async_evolve() — workers never wait for the merge, and the staleness policy rebases/discards stale diffs. Their custom optimizers keep shared state thread-safe, so both modes work unchanged.

python -m examples.ace.ace_context_evolution --model claude-haiku-4-5           # synchronous DP
python -m examples.ace.ace_context_evolution --model claude-haiku-4-5 --async   # barrier-free

The eight benchmark-faithful ports

Algorithm Port author Kind Domain (faithful) evolve() plug-ins Page
ACE (Agentic Context Engineering) chendanyang skill / context FiNER-139 (XBRL tagging) strategy=ACEPlaybook; Curator = default aggregator
GEPA (Reflective Prompt Evolution) chendanyang skill / prompt HotpotQA (EM) aggregator_factory= Pareto optimizer
EvoSkill (Automated Skill Discovery) chendanyang skill library OfficeQA (Treasury), FinQA without HF access strategy + aggregator_factory= bounded top-K frontier
SkillOpt (ReflACT) chendanyang skill document SearchQA (EM/F1) strategy (edits) + aggregator_factory= strict gate
ADAS (Meta Agent Search) chendanyang harness (L1) MGSM, GPQA Diamond strategy + aggregator_factory= keep-all archive
DGM (Darwin Gödel Machine) chendanyang harness (L1) SWE-bench Verified ids; vendored bugs w/ pytest strategy + aggregator_factory= archive + selection
OpenEvolve (Program Evolution) cyanneko program (L1) Function minimization strategy + aggregator_factory= MAP-Elites islands
ERA (Empirical Research Assistance) chendanyang program (L1) Kaggle Playground S3E1 (RMSE) strategy + aggregator_factory= FUTS tree, selection.FlatPuct

The eleven microports and analogues

All eleven are MethodPolicy definitions over examples/_method_runner.py, so they share a runner, a budget contract and a command line. Port author: cyanneko.

Algorithm Fidelity class Domain Mechanism seams Page
PromptBreeder mechanism_microport GSM8K binary tournament as the population layer; FieldSlots genome
AFlow mechanism_microport GSM8K SoftMixed selection; per-parent experience
Self-Refine mechanism_microport GSM8K two-call FEEDBACK→REFINE, stop signal
Reflexion mechanism_microport GSM-Hard WindowedMemory (bounded append-only)
SICA self_edit_analogue GSM-Hard AST gate; Archive('best') selection
Gödel Agent self_edit_analogue GSM-Hard AST gate; optional --gateless acceptance
Voyager environment_analogue crafting world SkillLibrary, DifficultyWeighted, self-verify critic
SkillWeaver environment_analogue settings site SkillLibrary, DifficultyWeighted, self-verify reward model
Absolute Zero inference_analogue self-play carts frozen self-play evaluation; learnability signal
R-Zero inference_analogue self-play carts AdvantageAcceptance (GRPO shape), DifficultyWeighted
Agent0 inference_analogue self-play carts DifficultyWeighted; calculator stop-and-go

The twentieth: Genesis, at the standard seams

One port is a mechanism microport that is not a MethodPolicy: its algorithm is a recursion, so it installs as a proposal policy rather than as a declaration over a shared runner. It is listed on its own because putting it in either table above would make that table's opening sentence false.

Algorithm Port author Fidelity class Domain evolve() plug-ins Page
Genesis (Persistent Recursive Worlds) chendanyang mechanism_microport four compact formation runs (--domain minilang / stackvm / jqx / md), each grown from an implementation-empty repository; md is scored by a frozen test suite with no reference implementation in the loop strategy + Policies(proposal=, acceptance=, conflict=) + stop_when=

The shared command line

Every port's shared flags have one definition, in add_standard_args, and the behaviour behind each lives there too — a flag declared centrally and honoured locally is how a port grows a --yes it never reads.

flag what it does where the behaviour lives
--provider / --model pick Claude or any OpenAI-compatible endpoint completion_for
--seed the run's seed the port
--async / --async-ratio / --max-seconds barrier-free runtime and its lag budget the port
--serial the upstream algorithm's own semantics: one worker, nothing to merge worker_count
--budget-rollouts total rollouts, held fixed as workers vary budget_kwargs
--val-cap shrink the gate's split without shrinking test capped_val
--eval-concurrency held-out evaluations in flight at once; wall-clock only the port
--reflective-merge merge contradicting diffs with a model instead of ranking them merge_kwargs
--eval-cache DIR memoise held-out scores across processes; off by default eval_cache_kwargs
--no-thinking ask an Anthropic-shaped endpoint for no reasoning tokens completion_for
--dry-run print the plan, with no model call the port's early return
--yes skip the confirmation before real API calls confirm

The iteration count is deliberately not standardised: --rounds (ACE, GEPA), --generations (ADAS, DGM), --iterations (EvoSkill, OpenEvolve, ERA), --steps (SkillOpt) and --candidates (the eleven) each keep their own vocabulary, which for the eight is part of being a faithful port. --budget-rollouts maps onto whichever one a port uses, so a sweep can pin every arm with one flag.

--serial is the control every one of these ports was missing. They all parallelise an algorithm that was published as a serial loop, and until this flag existed none of them could run that loop — so every claim about parallelising them had no baseline in the repository at all. It is refused together with --async: the barrier-free runtime's concurrency is n_workers, so --serial --async is a one-worker asynchronous run whose diffs can still go stale against a moved head, and staleness in the control arm is the one thing a control must not have.

--serial on its own is still not a comparison. Six of these eight ports pass a fixed iteration count and let the worker count multiply it, so an N=8 run does eight times the rollouts of the serial one — measured on the engine at rounds=24: 192 rollouts against 24. A wall-clock read across that gap is eight times the model spend reported as parallel efficiency. --budget-rollouts N pins both arms to the same total; pass it to both, at the same value, and report result.rollouts rather than the budget, because the synchronous path checks at the round barrier and can overshoot by up to n_workers - 1.

OpenEvolve is the exception and needed no fixing: it derives rounds = iterations // workers, so its total work was already fixed and the flag simply sets --iterations.

Every example takes --dry-run, which prints its configuration and returns without a model call and without an API key. Whether it also avoids the network depends on which runner it is on: the eight return before any dataset is touched, and say so (Data: deferred). The eleven build their MethodPolicy first, so a port whose domain is a real benchmark — PromptBreeder, AFlow, Self-Refine, Reflexion, SICA, Gödel Agent — loads and caches its split during a dry run, and prints that it did.

Datasets go through the shared agentdescent.dataloader layer — dependency-free (urllib only), cached under ~/.cache/agentdescent/, from each benchmark's canonical source. Every port has an offline test suite exercising its pure logic, named on its page. Where a paper's full setup needs heavy infrastructure, the boundary is documented in the example's module docstring — never hidden.

The MethodPolicy command line

The eleven declarative ports share build_parser, so their command line is the same on all eleven: the shared flags above plus --workers, --candidates (a synonym for --budget-rollouts), --no-reflective-merge, --staleness, --temperature, --max-tokens and --timeout. Two ports add one switch of their own — Reflexion's --per-instance and Gödel Agent's --gateless, both controls for a declared departure.

flag what it does here
--async-ratio N the lag budget, passed to async_evolve. Defaults to 1, not the shared 3 — see below
--eval-concurrency N held-out evaluations in flight at once; wall-clock only. Left off, the runner's own rule applies: 1 under --serial, --workers otherwise
--eval-cache DIR memoise the gate to a directory two processes can share, merged onto the method's own Policies bundle. Off by default: a cache that outlives the run makes a rerun return the first run's numbers
--val-cap not offered. These ports freeze train/held-out/test in build(), before the parser is consulted, so it now fails as an unrecognised argument rather than parsing and moving nothing

The first three used to be declared by add_standard_args and never passed to run_port, so on these eleven a run that set all three was byte-identical to one that set none — the same defect as Gödel Agent's --gateless, which five documents described while the parser rejected it. All four are now wired or withdrawn, and tests/test_method_runner_flags.py enumerates the parser and fails on a flag with nowhere recorded that reads it, so the next one added here cannot be decorative. run_port also records all three in framework, and --dry-run prints them: a flag absent from the plan is a flag nobody checks.

Why --async-ratio defaults to 1 here and 3 for the eight above

Two entry points reach run_port — this command line and bench.candidate_methods — and a lag budget on which they disagree is a trap, because the same nominal configuration would then run two different searches depending on which one launched it. run_port's signature says 1 and bench.candidate_methods defaults to 1, so this parser says 1 too; add_standard_args' 3 stays the default for the eight ports above, which is where it was measured. It is one argument away and it is a real change, not a label: the budget bounds both how far a worker's snapshot may drift behind head and how many cards may sit un-merged ahead of the merger.

Fifteen recorded rows said async_ratio: 2 and ran at 1

They were attributed to bench.candidate_methods, which passes the flag through. They did not come from there. bench.candidate_methods has no --staleness and never passes staleness= to run_port, so every run through it is guarded — and all fifteen blocks record full.

What they did come from is the line standard_main prints, and that is checkable rather than argued. It formats qualities at .3f, seconds at .1f and calls as an int, and across all 45 cells in those files not one value carries more precision than that; its fields are test/validation pairs, accepted, invalid, wall_s, engine_s, calls, which is the cell schema exactly. bench.candidate_methods JSON-dumps MethodRunResult.compact() unrounded — in the one file it produced, 198 of 198 wall/engine values are full floats. That command line dropped --async-ratio, so the runs took the runner's default of 1.

Those files now record 1, with a note. The Run it commands pass --async-ratio 1 explicitly for the same reason a config block should never have been typed from memory: the value matters and a default can move.

The underlying defect was not the flag. It was that a run printed what it reached and nothing about how it was set up, so the config block beside those numbers had to be remembered. A live run now prints its resolved configuration as one JSON line, in these key names, to be copied:

config: {"arm": "async_pipeline", "seed": 0, "budget_rollouts": 80, "workers": 8,
         "async_ratio": 1, "staleness": "full", "reflective_merge": true, ...}

Measured results — all twenty

Every port has been run and every number below is linked to the run that produced it. The two halves of this page are not comparable with each other, and within each half only rows on the same domain are: a gain is bounded by the headroom its baseline leaves, and the ports sit on benchmarks whose baselines run from 0.000 to 0.641.

The eleven, on one runner and one budget

Three seeds each, async_pipeline, 80 rollouts, 8 workers, --staleness full, deepseek-v4-flash at temperature 0.7 with thinking disabled. Test columns are the mean over the three seeds; moved counts seeds whose held-out test score rose at all.

Method Domain test, before → after gain moved accepted calls / seed
PromptBreeder GSM8K 0.474 → 0.969 +0.495 3/3 8/240 1444
AFlow GSM8K 0.510 → 0.969 +0.458 3/3 10/240 2069
Self-Refine GSM8K 0.552 → 0.943 +0.391 3/3 5/240 1050
Reflexion GSM-Hard 0.583 → 0.599 +0.016 2/3 3/240 465
SICA GSM-Hard 0.641 → 0.667 +0.026 2/3 5/240 899
Gödel Agent GSM-Hard 0.625 → 0.687 +0.062 2/3 9/240 985
Voyager crafting world 0.000 → 0.667 +0.667 2/3 3/240 1234
SkillWeaver settings site 0.000 → 0.771 +0.771 3/3 8/240 1170
Absolute Zero self-play carts 0.042 → 0.354 +0.313 3/3 9/240 624
R-Zero self-play carts 0.062 → 0.292 +0.230 3/3 6/240 937
Agent0 self-play carts 0.042 → 0.542 +0.500 3/3 6/240 1626

Read the columns against each other, not down the gain column. The three GSM8K rows converge on 0.94–0.97 from a real model baseline and separate on cost: AFlow spends twice Self-Refine's calls for 0.026 more. The three GSM-Hard rows move a fraction as far because their headroom is a fraction as wide — and the noise floor there is ±0.02 against GSM8K's ±0.09, which is why a +0.026 on GSM-Hard is a result and a +0.05 on GSM8K would not be. The two environment analogues start at 0.000 because a seed agent that has discovered no skill solves none of their goals; the three self-play rows do not, because a trusted renderer generates their evaluation carts and a fresh solver already gets some of them — which is also why neither of those groups has a 1.000 ceiling to read a final score against.

accepted is the shape of the mechanism, not a yield. Between 3 and 10 of 240 proposals commit across every row, because the gate scores each candidate on the full held-out split and admits only a strict improvement. One accepted skill is all Voyager's run needs.

The eight, each on its own benchmark

Different budgets, different datasets, one seed each — these are single runs that establish the port works end to end, not a comparison.

Method Domain measured budget
ACE FiNER-139 val 0.719 → 0.766, test 0.786, 10 bullets curated async N=4, 120 rollouts
GEPA HotpotQA 1.85× / 3.22× concurrency (sync / async) against a true serial arm; test EM 0.600 → 0.850 on 20 items 16 rollouts pinned on all three arms
EvoSkill FinQA val 0.527 → 0.707, test 0.633, the frontier filled 5/5 async N=4, 120 rollouts
SkillOpt SearchQA (--hard) val 0.053 → 0.211, test 0.316, 3 edits accepted of 43 async N=4, 60 rollouts
ADAS GPQA Diamond no lift number — MGSM is saturated and GPQA costs 49 s / 5,116 tokens per call, and the two constraints are opposed
DGM vendored bugs, real pytest seed agent 0.844 → best archived child 0.906 held out; its own solve.py 18 → 79 lines async N=2, 16 rollouts
OpenEvolve function minimization combined score 0.9638 → 1.4995 against a 1.5 ceiling, held-out seeds async N=4, 24 rollouts
ERA Kaggle Playground S3E1 test RMSE 0.7297 → 0.5913 (−19.0%) on 2,476 unseen rows; 7-node tree, all valid async N=3, 6 expansions

The twentieth, on two formation runs

The repository starts with no implementation in it, so "before" is 0.000 by construction rather than by a weak baseline. Model rows are three seeds at --episodes 60 --workers 4; offline rows are --episodes 96 (minilang) and 160 (stackvm), deterministic across seeds.

Method Domain held-out, before → after accepted events notes
Genesis · minilang, deepseek-v4-flash compact formation run, 2 nodes deep 0.000 → 1.000 ×3 5 the model writes every line; seed 0 re-scored independently at 30/30, observed depth 3, 284 calls in 135 s
Genesis · minilang, offline actors the same 0.000 → 1.000 ×3 5 serial / 4 at N=4 / 3 at N=8 deterministic on every seed; --keyed-union takes 5 at every width, so the three-way merge is what parallelism buys here
Genesis · md, deepseek-v4-flash compact formation run, scored by a frozen test suite with no oracle 0.000 → 1.000 48 ended itself at 244 of 4 000 episodes when the root agent called it done; 65/65 re-scored with pytest, 67/67 in one interpreter, and a 108-particle run agrees with an independent reference line for line
Genesis · stackvm, deepseek-v4-flash compact formation run, 4 nodes deep 0.000 → 1.000 12 re-scored independently at 30/30; it invented its own decomposition and left two pieces of dead code the suite cannot see — see the page
Genesis · stackvm, offline actors the same 0.000 → 1.000 ×3 6 at N=8 ten files the agents write, observed depth 3; --keyed-union takes 8

One run per seed, and one seed on the bottom table

Nothing here is a paper-scale result. The eleven and Genesis carry three seeds and report the spread on their own pages; the eight carry one, and a single run does not pin a number on a sampled model. The quality columns are evidence the mechanism runs and moves the metric it should, not evidence about how much. Genesis carries both kinds: its model rows are three seeds on one model and one endpoint, and its offline rows are deterministic, which makes their spread a statement about scheduling rather than about a model.


Mechanism coverage

Every mechanism family in the original backlog now has at least one implemented port:

Mechanism Benchmark-faithful Microports / analogues
Evolution / program search OpenEvolve PromptBreeder, AFlow
Reflection / refinement GEPA (reflective) Reflexion, Self-Refine
Skills / lifelong learning EvoSkill, SkillOpt, ACE Voyager, SkillWeaver
Self-play / unlabeled data Absolute Zero, R-Zero, Agent0
Self-modifying code / harness DGM, ADAS SICA, Gödel Agent

The label-free path now exists — the three self-play ports derive reward from a grounded local verifier with no gold labels — but only as inference analogues: a benchmark-faithful label-free port (real RL updates, real task domains) remains open. TextGrad remains unimplemented in the reflection family.

Deferred pending released code

  • CoEvoSkills (arXiv:2604.01687, "Self-Evolving Agent Skills via Co-Evolutionary Verification") — the Skill Generator + co-evolving Surrogate Verifier + opaque pass/fail oracle is a compelling fit for AgentDescent's aggregator, but the official code is unreleased ("coming soon") and its benchmark (SkillsBench) requires a Claude Code / Codex agent harness. With no original repo to be faithful to, it is intentionally deferred until the authors release code, rather than reconstructed and mislabelled as faithful.

Fidelity principles

Use the short porting checklist before adding a row here, and record the result in port fidelity after — the checklist is the standard, that page is what each port was actually found to do.

  1. Faithful to the repo, not just the paper. Where the released code diverges from the paper's claims (e.g. EvoSkill's frontier is top-K aggregate, not per-instance Pareto), the example follows the code and says so.
  2. Declared dataset identity. A benchmark-faithful port uses the paper's own benchmark from its canonical source; a microport or analogue uses a compact or substituted domain and carries the fidelity class that says which it is.
  3. Documented boundaries. Where the full setup needs heavy infra (AppWorld, SWE-bench Docker, gated data), the example states the boundary and, when needed, substitutes a clearly-labelled surrogate — the algorithm stays faithful and runnable; nothing is passed off as a benchmark result it is not.