Skip to main content

awevolve — an agent as the variation operator

Point it at a file and a command that scores that file, and watch an agent improve it — keeping every version and the score it earned.

pip install awevolve

Python 3.10+. No dependencies. The agent is a command you configure, so this works with whatever coding agent you already run instead of pinning you to one SDK.

What it is

Classical evolutionary search confines a model to a single generation call inside a fixed pipeline: sample some parents, ask for a child, score it, repeat. That confinement is the ceiling. awevolve hands the agent the lineage of what has already been tried, the knowledge the harness names, and the scorer itself — and expects it to have read them before proposing anything.

from awevolve import Harness, Lineage, AgentProposer, evolve

harness = Harness(
    name="kernel",
    mutable_file="src/kernel.py",
    eval_command="python -m bench --strict",
    metric_regex=r"METRIC latency_ms=([0-9.]+)",
    metric_name="latency_ms",
    minimize=True,
    confirm_repeats=3,          # this score moves with machine load
)

result = evolve(harness, AgentProposer(), Lineage("kernel.jsonl"), max_rounds=20)
awevolve check   --harness kernel.json          # validate; changes nothing
awevolve run     --harness kernel.json --rounds 20
awevolve lineage --lineage kernel.jsonl         # what was tried, and what it scored
awevolve status  --lineage kernel.jsonl
awevolve --self-test                            # prove the invariants still hold

Instead of trusting

that your optimisation loop is finding anything.

You check

every version it kept, the score that version earned, and the edit that produced it — including the ones that lost, which are the rows that stop the next proposal repeating a known failure.

The agent contract

One command. It reads a prompt on stdin and writes the complete new file to stdout:

export AWEVOLVE_AGENT_CMD="my-agent --print"

To decline a round it outputs exactly AWEVOLVE_NO_CHANGE. That is a sentinel rather than empty output on purpose: an empty response and a crashed agent are indistinguishable, and they must not lead to the same decision.

Any callable works too — AgentProposer is just the default:

def my_proposer(context: dict) -> str | None:
    ...  # context carries current_content, lineage, baseline, harness

The failures this is built around

Every rule below exists because its absence is invisible. A run that mutated nothing and a run that explored honestly and found nothing produce identical logs, identical records and identical stop reasons. There is no exception to catch and nothing to alert on. So the loop refuses to proceed in the states where the two become indistinguishable:

rule why
A change that leaves the file byte-identical is refused, never scored otherwise a no-op is recorded as a trial that "found no improvement"
A losing trial is restored from a snapshot taken before the write a revert that only logs keeps every regression it ever measured
A non-zero exit from the scorer refuses the trial that is the scorer saying the candidate is wrong, not slower; a broken build still prints an old-looking number
An apparent win is re-measured before it is banked a ratchet cannot walk back a win it has already kept, so one lucky sample is permanent
The baseline is measured, not assumed a run that starts from an unverified number reports improvements against fiction
The proposer is handed the lineage a proposer that cannot see what failed is sampling, not searching

Losses are not re-measured — that would multiply the cost of every unsuccessful trial, which is most of them. Only a trial that claims a win pays for confirmation, and the median of the samples decides so one outlier in either direction cannot.

The one rule that matters most

A harness may not be its own scorer. This is checked at construction and there is deliberately no override:

Harness(
    mutable_file="train.py",
    eval_command="python train.py",   # SelfScoringError
    ...
)

If the file the optimiser may change is also the file that reports the score, the cheapest available strategy is not to improve the system — it is to improve the report. And every downstream signal will agree enthusiastically that things got better.

It is not that an agent cheats out of malice. It is that you have defined a search space whose shortest path to a better number runs straight through the measuring instrument, and then asked something very good at finding shortest paths to search it.

The fix takes minutes: split the scorer out of the mutable file. An escape hatch here would be used once in a hurry and then forever.

Supervision

Long autonomous runs stall in two ways that look identical from outside — the agent exhausts a line of thinking and proposes variations on something already rejected, or it cycles through proposals that are never scored at all. Both produce a steady stream of trials and no improvement.

from awevolve import SupervisedProposer, Supervisor

proposer = SupervisedProposer(AgentProposer(), lineage, Supervisor(stall_after=3))

The supervisor adds a directive to the next prompt. It never changes the objective, relaxes the scorer, or widens what may be edited — a supervisor that can move the goalposts turns "the search stalled" into "the search found a way to look successful".

The lineage

Append-only JSONL, one line per trial, written with an atomic replace so a crashed run cannot leave a half-written record the next run reads as truth. Pass git_root= and each kept version is committed too — staging only the mutable file, because committing a whole working tree sweeps in whatever else happened to be uncommitted.

Licence

Apache-2.0.

Metadata

Release files for awevolve 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for awevolve 0.1.0
File Size Uploaded
awevolve-0.1.0.tar.gz 36.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for awevolve 0.1.0
File Interpreter ABI Platform
awevolve-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 72.7 kB

Release files / awevolve-0.1.0.tar.gz

Download URL awevolve-0.1.0.tar.gz
Size 36.7 kB
Tags Source
SHA-256 checksum
How to use checksums
83d182a823b6066c625c80b73977b381fe39efb142d6f2ef990f68ce8e2fca04
BLAKE2b-256 checksum
How to use checksums
7f9ba5f915ace47de89910298bcf9380cabbe72ee4475e6dd0eb61cf0ca9ca81
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release files / awevolve-0.1.0-py3-none-any.whl

Download URL awevolve-0.1.0-py3-none-any.whl
Size 36.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
20de8360ae5a7bd58afa99c837d1b275076044b1f662a14307a0a1fa6998a4a5
BLAKE2b-256 checksum
How to use checksums
ce18d44bc40a7824f40a50cdbe8b603e815ddd0f827cb72c5ec9bf8eba034f93
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.3

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page