Skip to content

Repository files navigation

AgentDescent

Gradient descent — but the parameters are agents. A parallel, asynchronous framework for self-evolving agents (skills, prompts, harnesses) where diffs are the gradients and the aggregator is the optimizer.

DOI PyPI tests docs python license

N workers propose edits to a shared artifact in parallel; a barrier-free aggregator merges them into one version-controlled library. Serial self-improvement is bounded at one accepted change per iteration, and merging concurrent edits is the attempt to lift that bound.

The place the analogy has to break is the whole design: gradients add, diffs do not. So aggregation is not averaging but conflict resolution, fusion, statistical acceptance and a transactional commit.

What it measures

Every row names the setting that produced it, and the full results page also reports the runs where there was nothing left to learn.

measured setting
An artifact held in one key, where a keyed union can never fuse keyed union fuses 0 of 48 merges · reflective merge 42 of 48 BBH dyck_languages, GLM-5.2, N=4, 4 seeds
What that costs, at a pinned rollout budget 40% fewer model calls (95% CI 27–54%, same direction on every seed) the same runs
Wall-clock against a faithful serial control median 6.8× (three seeds, 3.1–9.5×) GEPA on HotpotQA, N=4, 16 rollouts pinned
The one-call path, end to end held-out exact match 0.167 → 0.583 40 HotpotQA items, 12 held out

Quality is claimed only where the design can support it. The paper reports intervals rather than p-values where a comparison cannot reach significance, and says so.

How it works

AgentDescent architecture: N workers roll out against ledger snapshots and emit diffs with evidence cards into a buffer; a single aggregator thread runs the five-stage merge pipeline and commits to a git-backed ledger; four pluggable seams sit under the components they select; the L0 governance layer gates the audit step.

Solid, top: the fixed data path. N workers roll out tasks against ledger snapshots and emit diffs with evidence cards; one aggregator thread runs the five-stage merge and commits winners; the green edge is the only feedback path, bounded by the lag budget.

Dashed, bottom: the seams, each selected by one keyword argument of evolve(). Red: the governance layer, deliberately not a seam — a self-modifying system must not be able to replace its own evaluator.

Stages 1–5 are one optimizer step over a discrete space. Whether that step has anything to do at all is decided upstream, by the key space your strategy writes — edits on disjoint keys fuse, edits on the same key conflict. That is why the table above starts with a one-key artifact.

The figure is the paper's, rendered from its TikZ source by tools/gen_architecture_figure.py so it cannot drift from what the paper shows.

Install and run something in 30 seconds

pip install agentdescent

The core engine has zero required dependencies and needs only Python ≥ 3.9. The examples are research artifacts kept outside the installed package — they would otherwise squat the top-level examples name — so clone the repo to run them:

git clone https://github.com/Birfy/agentdescent && cd agentdescent
pip install -e ".[dev]"
python -m examples.run_demo      # no API key, no network

A terminal recording of python -m examples.run_demo: the evolution loop runs to completion in under half a second, printing a per-round table of held-out accuracy and the aggregator's commit, fused, stale and conflict counters.

That is the whole run — no API key, no network, under half a second. Three rounds commit, then the gate stops accepting because there is nothing left to improve; commit, fused, stale and confl are the aggregator's own counters, and every run prints them.

Quickstart — a dataset to an evolved skill

One entry point, evolve(), and three building blocks that turn a dataset into its arguments. The decisions that are actually yours — your data, how to score it, which model — are the ones you still make.

Set your key first. Credentials are read from the environment at call time — never from code, arguments, or a file the repo owns — and the base URL is what picks the provider:

export OPENAI_BASE_URL=https://api.deepseek.com   # or GLM, a local vLLM, OpenAI itself
export OPENAI_API_KEY=sk-...                      # your key for *that* endpoint

agentdescent doctor reports what a machine has; the demo above and pytest -q need none of it. Every provider, with its base URL →

from agentdescent import SingleSlot, evolve, openai_compatible, reflector, scorer, tasks_from
from agentdescent.dataloader import hf_rows

rows = hf_rows("hotpotqa/hotpot_qa", "validation", config="distractor", limit=40)
model = openai_compatible(model="deepseek-v4-flash")

tasks = tasks_from(rows, prompt="question", gold="answer")     # rows -> Task objects
run = lambda skill, task: model(f"{skill}\n\n{task.prompt}")   # the skill meets the question

result = evolve(tasks, scorer("exact"), run=run, propose=reflector(model),
                strategy=SingleSlot(initial_value="You are a helpful assistant."),
                rounds=8, n_workers=8, max_concurrency=8, held_out_frac=0.3,
                patience=3, target_reward=0.98)

print(result.rendered)        # the skill it learned
print(result.final_reward)    # held-out reward
print(result.outcomes())      # why it went that way

That run is the last row of the table above. It learned "Respond with only the requested answer, omitting any extra explanation or restatement."

The same thing without a dataset. Runnable as-is — no API key, no dependencies.
from agentdescent import Task, evolve

tasks = [Task(id=f"t{i}", prompt=f"item {i}") for i in range(12)]

def reward(task, output):                  # must return [0, 1]
    return 1.0 if "2026" in output else 0.0

def run(rendered, task):                   # your solver
    return "answer" + (" 2026" if "year" in rendered else "")

def propose(rendered, task, output, reward):   # what to add on a failure
    return "always state the year"

result = evolve(tasks, reward, run=run, propose=propose,
                rounds=6, n_workers=3, max_concurrency=3)
print(result.rendered, result.final_reward, result.error)

Swap in a real model or agent by passing agent= instead of run/propose:

from agentdescent import LLMAgent, claude, openai_compatible, claude_code

evolve(tasks, reward, agent=LLMAgent(claude(model="claude-haiku-4-5")))
evolve(tasks, reward, agent=LLMAgent(openai_compatible(model="deepseek-v4-flash")))
evolve(tasks, reward, agent=LLMAgent(claude_code()))     # Claude Code CLI
# ...or run barrier-free: evolve(..., asynchronous=True, async_ratio=3)

Use it from your agent — Claude Code, Codex, OpenCode, DeepSeek Harness

The same engine as a plugin. A shared skill teaches the host when to reach for it, an MCP server exposes doctor / plan / start / status / show / apply / cancel / resume, and the CLI mirrors them, so a run started from an agent can be inspected from a shell.

bash scripts/setup-hosts.sh   # installs it and wires up whichever agent CLIs you have
agentdescent demo             # a complete evolution, offline, no key, ~10s
agentdescent doctor           # which CLIs and keys this machine has

Say "improve this prompt against these examples" inside the agent. It runs doctor, writes a spec, quotes the call count before starting, waits for your yes, runs detached, and asks again before it writes anything back.

No cases yet is not a blocker. Drafting them is step one of the procedure, not a prerequisite: point it at the file and it writes 8–20 for you to check first.

kind is text, skill_dir, agent_dir, agent_code — or plugin: the host plugins themselves are evolvable, each rollout loaded into an isolated copy of the host, hooks and permission config frozen, with a recursion guard so a plugin evolving itself cannot start a nested run.

The plugin in three commands → · set it up and test it →

What you can replace

Two seams carry the algorithm, and both are typing.Protocols — nothing to inherit from, and the contracts are re-derived from the engine's own call sites by a test, so the published interface cannot drift from what runs.

The strategy — what evolves. Three methods over a flat {key: value} state. Its key space is what decides whether concurrent proposals can fuse at all.

strategy the artifact is concurrent proposals
AppendRules a deduped list of lessons, keyed by content almost always fuse
KeyedRules(categories) one entry per named category contradict within, fuse across
FileTree(files) a directory, one key per path contradict per file
SingleSlot one value — a prompt, an instruction always contradict

Eight policy slots — the rules of evolution. Each is one field of a Policies bundle, each defaults to the shipped rule, and filling one leaves the other seven alone. A driver refuses a field it cannot honour rather than ignoring it — a policy that installs but never runs is the one failure a caller cannot detect from a completed run.

selection task_sampler proposal staleness
conflict fusion acceptance promotion

Mechanisms that need state the pipeline does not keep — an archive, per-instance score rows, an island pool — take the aggregator_factory exit instead. How to fill a slot →

Twenty algorithm ports

Published self-evolution algorithms run as plug-ins rather than forks, under serial, synchronous or barrier-free scheduling without touching the engine — which is what makes the scheduler a controlled variable instead of a property of each paper's own loop.

Benchmark-faithful (8): ACE · GEPA · EvoSkill · SkillOpt · ADAS · DGM · OpenEvolve · ERA Microports and analogues (12): PromptBreeder · AFlow · Self-Refine · Reflexion · SICA · Gödel Agent · Voyager · SkillWeaver · Absolute Zero · R-Zero · Agent0 · Genesis

Genesis is the one whose whole algorithm — a recursion over (version, path) worlds, a three-way merge and a parent's verdict — installs through Policies(proposal=, acceptance=, conflict=) and a Strategy, with no engine change at all.

Fidelity is declared per port, follows each project's released code where it diverges from its paper, and analogues are not to be cited as benchmark reproductions. Every port has a --dry-run mode that needs no API key. All twenty, with their measured results →

Documentation

Full docs render at birfy.github.io/agentdescent.

Start here
Quickstart — dataset to skill One call: your data, how to score it, which model
Measured results Every empirical claim with the setup that produced it
Architecture Components, data flow, the two runtimes
Concepts The training↔RSI analogy, staleness, governance
Going further
Evolving anything · Strategies Evolve any artifact by writing its Strategy
Choosing policies · Using the slots The decision plane, and how to write for it
Agents & LLMs · Datasets The provider layer and the data layer
Parallelism · Execution · Sandboxes Where rollouts run, and how they are isolated
Use it from your agent · Testing it The plugin surface, and how to verify each host
Efficiency · Runtime matrix Measured scaling and the scheduler comparison
Molecule search — porous crystals A domain application: flat-PUCT tree search over molecules, with a validity gate and a real P(s,a)

Citing

The paper — AgentDescent: Asynchronous Parallel Self-Evolution of LLM Agents by Merging Conflicting Edits — gives the design, a closed-form condition for when merging can fire at all, and a live-model evaluation with every generating script named.

@article{chen2026agentdescent,
  title  = {{AgentDescent}: Asynchronous Parallel Self-Evolution of {LLM} Agents
            by Merging Conflicting Edits},
  author = {Chen, Danyang},
  year   = {2026},
  doi    = {10.5281/zenodo.22348027}
}

LaTeX source and the built PDF live on the paper branch, not here: the write-up and the code change on different clocks. Per-result raw data is in bench/results/ for most results; the merge-versus-selection sweeps are re-measurable from the command the paper names rather than recomputable, because their per-run data was not retained — the paper says so too.

Scope

A research reference implementation, not a production system. The novelty is a narrow engineering synthesis — concurrent, staleness-bounded, conflict-resolved diff-level merge over a git-backed versioned ledger — and its throughput premise is a testable hypothesis rather than community consensus. Contributions welcome: CONTRIBUTING.md. The suite is offline and deterministic, so pytest -q needs no API key.

About

Gradient descent, but the parameters are agents — a parallel, asynchronous framework for self-evolving agents (skills, prompts, harnesses). Diffs are the gradients; the aggregator is the optimizer.

Topics

Resources

Contributing

Stars

233 stars

Watchers

8 watching

Forks

Releases

Packages

Contributors

Languages