Run everything, and extend it¶
Install and first run covers getting set up. This page is the complete list of what you can run, the configuration reference, and how to plug your own domain into the loop.
Looking for something specific?
Module map indexes every module and its page ·
API reference has every signature ·
The evolve method is the entry point's own page.
1. Run the demos¶
The demos need a checkout, not just pip install
The examples/ directory ships with the repository, not the wheel, so
python -m examples.… requires git clone. pip install agentdescent gives
you the library only.
RQ1 — merge vs fork (synchronous DP)¶
Runs the merge-based AgentDescent loop and a DGM-style fork baseline on the same
budget, then prints the learning curve and the comparison:
round dev_acc stable commit fused stale confl oracle
0 0.828 0.000 1 1 0 0 0
3 1.000 0.000 1 0 0 1 0 ← a contradiction dropped
8 1.000 1.000 0 0 0 0 0 ← stable branch catches up
AgentDescent (merge) held-out accuracy : 1.000
Fork/archive best-fork accuracy : 0.379
merge advantage : +0.621
Async stage orchestration (FlashEvolve-style)¶
Compares the three staleness policies and sweeps the async_ratio lag budget.
RQ2 — staleness tolerance sweep¶
Evolving a directory (a skill folder a real agent reads)¶
Materialises a candidate skill directory into a throwaway workspace at
.claude/skills/csv-total/ and lets a real (sub)process agent read it. Runs
offline in seconds; --agent claude-code swaps in the real CLI agent, and
--install-to <dir> writes the evolved skill back. See
evolving a directory.
Self-evolution algorithm ports¶
Nineteen ports of the latest self-evolution algorithms (see
the catalog). Twelve load a real benchmark through
the agentdescent.dataloader data layer; the other six run
bundled deterministic domains. The eleven MethodPolicy ports
share a runner and are measured together in the
runtime matrix (python -m bench.candidate_methods).
Every port's --dry-run prints its configuration and makes no model call.
The eight return before touching data too; the eleven build their policy first,
so a dry run of one on a real benchmark loads and caches the split.
python -m examples.ace.ace_context_evolution --dry-run # ACE / FiNER-139
python -m examples.gepa.gepa_prompt_evolution --dry-run # GEPA / HotpotQA
python -m examples.dgm.dgm_self_improve # DGM / offline surrogate
python -m examples.sica.sica_self_edit --dry-run # SICA / GSM-Hard
Molecule search — porous molecular crystals¶
A domain application rather than a port: the same flat-PUCT tree ERA runs, over molecules, scored on rigidity, symmetry, directional interaction sites, an open packing that is still competitive on lattice energy, and synthetic accessibility. Every expansion must come back as a SMILES and must pass a dependency-free validity gate. See the page.
python -m examples.porous.porous_tree_search --dry-run
python -m examples.porous.porous_tree_search --offline --iterations 24 --workers 4
--offline proposes with rule-based edit operators, so the whole search runs
with no API key and no network — it is both the test path and the control arm
for a model-driven run.
Tests¶
pytest # the whole suite, no external services
pytest -q tests/test_async.py # just the async runtime
2. Programmatic use¶
The entry point — evolve()¶
Covered in full elsewhere, and not repeated here:
- Quickstart — a dataset to an evolved skill in one call, with the measured result of running it.
- The
evolvemethod — the entry point underneath, with every parameter and what it plugs into. - Connecting agents & LLMs — any
prompt -> textis a backend, including tool-using CLI agents. - Measured results — every empirical claim with its setup.
What a run cost¶
EvolutionResult carries the artifact and the bill. Every field below defaults
to zero and is read back with .get, so a result saved before they existed still
loads.
r = evolve(tasks, reward, agent=agent)
r.cost_summary() # one line: rollouts, wall-clock, model calls, ratios
r.time_to_quality(0.9) # seconds to the first round at >= 0.9, or None
r.cost_to_quality(0.9) # rollouts to that same round, or None
r.stale_rate() # discarded / considered -- the ratio needs both
r.duplicate_rate() # evaluation cache hit rate
| what | field |
|---|---|
| time | wallclock, and RoundInfo.elapsed_s per round |
| work | rollouts, rollout_seconds (a sum across workers, so it exceeds wallclock when they overlap), eval_seconds |
| model | usage.calls / .seconds / .failures |
| staleness | stale_considered, stale_discarded |
| recovery | redispatched, duplicates_dropped |
| recomputation | cache_hits, cache_misses |
| sandboxes | sandbox_wait_s, sandbox_setup_s, sandboxes_created / _reused / _failures |
Two things worth knowing before reading these numbers:
usage.callscounts actor invocations — onerunorpropose— not provider requests. Acli_agentrollout is one call here and many requests to the model.- Token counts need a shared
Usage.runis(rendered, task) -> str, so an opaque actor cannot report tokens. Pass the same object to both and they accumulate together:
from agentdescent import Usage
u = Usage()
r = evolve(tasks, reward, agent=LLMAgent(claude(usage=u)), usage=u)
print(r.usage.total_tokens, r.usage.estimated_cost(3.0, 15.0))
The sandbox fields are zero on the default path: one throwaway workspace per rollout, nothing to queue for and no image to warm. They exist so that when a pool does, "8 workers only bought 2x" can be attributed rather than guessed at.
The reference classes — AgentDescent / AsyncAgentDescent¶
The entry points the RQ1/RQ2 and efficiency experiments were published against, on the built-in synthetic router domain. They are adapters over the one engine now — see the note — kept so those measurements stay reproducible under their original names. Reach for them to reproduce the experiments, not to evolve your own artifact.
import tempfile
from agentdescent.domains.router import make_task_universe
from agentdescent import AgentDescent
universe = make_task_universe(seed=7)
with tempfile.TemporaryDirectory() as repo:
system = AgentDescent(repo, universe, n_workers=6, noise=0.15, seed=1)
history = system.run(rounds=40)
print(system.final_accuracy()) # held-out accuracy on the dev branch
import tempfile
from agentdescent import AsyncAgentDescent, AsyncConfig
from agentdescent.domains.router import make_task_universe
from agentdescent import get_policy
universe = make_task_universe(seed=7)
cfg = AsyncConfig(n_workers=6, async_ratio=4, target_accuracy=0.98, max_seconds=15.0)
with tempfile.TemporaryDirectory() as repo:
system = AsyncAgentDescent(repo, universe, config=cfg,
staleness_policy=get_policy("reflective"))
stats = system.run()
print(stats.final_dev_accuracy, stats.commits, stats.discarded_stale)
3. Configuration reference¶
AggregatorConfig¶
Tunes the shipped merge pipeline; see the aggregator.
| Field | Default | Meaning |
|---|---|---|
batch_trigger |
4 | B: fire a bucket once it holds this many cards |
max_wait_rounds |
3 | T_max: fire a cold bucket after this many sweeps |
base_delta |
0.5 | acceptance risk; threshold is 1 − δ, annealed by version |
alpha_head |
5 | staleness tolerance α for hot artifacts |
alpha_tail |
1 | staleness tolerance α for cold artifacts |
trust_region_ops |
6 | max edits per diff (trust region) |
trust_region_chars |
32 000 | max characters in one op's value — for a FileTree that is a per-file cap, and TreeSpec.max_file_bytes must stay under it |
anneal_half_life |
64 | versions over which the acceptance risk decays |
promote_after_k |
3 | dev→stable survival rounds (EMA) |
trust_region_policy |
None |
an AdaptiveTrustRegion that widens/tightens the caps |
accept_samples |
4000 | Monte-Carlo draws behind each acceptance decision |
cas_attempts |
3 | commit retries under CAS conflict (jittered backoff) |
cas_backoff |
0.05 | base backoff (seconds) for those retries |
fusion_tournament |
False |
rank the union against the singles (fusion); off by default |
AsyncConfig¶
The reference async orchestrator's own config; see async.
| Field | Default | Meaning |
|---|---|---|
n_workers |
6 | worker threads |
async_ratio |
3 | ROLL Flash lag budget: max head-drift before a worker refreshes |
noise |
0.15 | per-op probability that a noisy worker proposes a wrong label (every third worker is noisy; the rest are clean) |
target_accuracy |
0.98 | stop early when the dev branch reaches this |
max_seconds |
20.0 | wall-clock safety bound |
aggregator_interval |
0.002 | sleep between aggregator sweeps |
worker_pause |
0.001 | sleep between worker rollouts |
oracle_budget |
400 | oracle calls the AuditScheduler may spend |
stall_patience |
150 | no-commit sweeps before a backpressure sync |
AsyncStats.error is None on a clean run and carries the backend failure that ended it otherwise — check it, since a run whose workers all died otherwise returns normal-looking zeros.
Staleness policy¶
What a lagging diff is worth; see staleness.
from agentdescent import get_policy
get_policy("full") # accept stale diffs directly
get_policy("guarded") # version-gated (default)
get_policy("reflective") # always rebase + re-verify
4. Plug in your own domain¶
Most callers never need this
A strategy over a flat {key: value} state covers nearly
everything — including a whole directory. Implement
Evolvable yourself only when your artifact genuinely is not
that shape; domains/router.py is the worked example.
The whole framework is domain-agnostic: what evolves is decided by registration, not hard-coded. To evolve something new, provide four things.
4.1 An Evolvable¶
Implement the protocol from agentdescent/evolvable.py
(reference: RouterSkill):
from agentdescent import Contract, Diff, EvidenceCard
class MyArtifact:
def __init__(self, id, state, version=1, blast_radius=0.2):
self.id = id
self.version = version
self.blast_radius = blast_radius # → auto L0/L1/L2 layering
self.contract = Contract(major=1)
self.state = state
def diff(self, other) -> Diff: ... # difference to another instance
def apply(self, diff: Diff) -> "MyArtifact": # return a NEW instance, version+1
...
def evidence_eval(self, evidence: EvidenceCard) -> float:
# score on the tasks the evidence card carries (used by rebase
# re-verify); the full protocol -- and why `full_eval` no longer
# exists -- is on [the data model page](data-model.md)
...
apply must be pure
apply returns a new instance with version + 1; it never mutates
self. The aggregator relies on this to test candidates without side
effects.
4.2 Serialize / deserialize for the Ledger¶
The Ledger stores artifacts as JSON blobs in git, so give it two functions:
def serialize_mine(a) -> dict: # → JSON-friendly dict
return {"state": a.state, "blast_radius": a.blast_radius}
def deserialize_mine(artifact_id, version, state) -> MyArtifact:
return MyArtifact(artifact_id, state["state"], version, state["blast_radius"])
ledger = Ledger(repo_path, serialize_mine, deserialize_mine)
ledger.register(MyArtifact("my-art", initial_state))
4.3 An eval function for the verifier¶
Ground-truth scorer over a held-out task set:
def my_eval(artifact, tasks) -> float: # accuracy / reward in [0, 1]
...
verifier = ThreeLayerVerifier(eval_fn=my_eval, held_out=held_out_tasks)
4.4 A worker that proposes diffs¶
Workers turn observed failures into a Diff + EvidenceCard. There is no
Worker class — the role is the run/propose callables the engine takes
(domains/router.py's router_propose is the deterministic reference); in a
real system this is where an LLM reflects on a trajectory and proposes an
edit. Emit a card
with the base_version you read, the touched artifacts, and a local
before_after_delta, then aggregator.ingest(card).
Once these four pieces exist, everything else — the Ledger, the aggregator, the schedulers, the governance layers, both runtimes, and all three parallel paradigms — works unchanged.
5. Building the documentation site¶
See install — one canonical copy of the commands lives there.