Skip to content

Repository files navigation

NanoEvolve

The smallest useful evolutionary programming loop.

Preserve AlphaEvolve's generate–verify–select feedback loop, remove OpenEvolve's research infrastructure, and rebuild it around Pi's primitives-first, file-based, fully observable philosophy.

简体中文 · Design specification · Changelog · Contributing

Python Runtime dependencies Tests Status State

Archive --select--> Prompt --> Model --> Candidate
   ^                                      |
   |                                      v
   +------------- Evaluation <-- Evaluator

NanoEvolve keeps the useful core of AlphaEvolve-style systems—select → mutate → evaluate → archive—without turning it into an agent framework. There is no tool loop, planner, sub-agent system, hidden memory, plugin registry, or database.

Why NanoEvolve?

  • Small control surface: one evolve() function and four CLI commands.
  • Visible state: exact prompts, raw responses, candidates, scores, and errors stay on disk.
  • Recoverable runs: append-only JSONL records rebuild the archive after interruption.
  • Deterministic selection: each generation derives its own stable random seed.
  • Evaluator-owned truth: domain logic stays in an ordinary evaluate.py function.
  • Zero runtime dependencies: the core uses only the Python standard library.

Three-Minute Experience

The deterministic demo requires no API key and makes no network request.

python -m venv .venv
source .venv/bin/activate
python -m pip install -e .
python examples/hello_evolve/demo.py

Expected ending:

NanoEvolve deterministic demo completed.
Best score: 8.0
Inspect: nanoevolve inspect /tmp/nanoevolve-hello-... <record-id>

The printed temporary project remains available for the CLI:

nanoevolve best /tmp/nanoevolve-hello-...
nanoevolve best /tmp/nanoevolve-hello-... --summary
nanoevolve inspect /tmp/nanoevolve-hello-... <record-id>

Reproducible Benchmark

Run a deterministic eight-point packing trajectory without an API key or network access:

python examples/circle_packing/demo.py

The run improves the minimum pairwise distance from 0.4000000000 to 0.5176380902 across three inspectable generations. It is a compact benchmark of the actual generate–evaluate–select–archive loop rather than a synthetic score counter.

Run a Real Model

Create or reuse a project with three explicit files:

my-experiment/
├── TASK.md
├── seed.py
└── evaluate.py

Configure any OpenAI-compatible, non-streaming endpoint:

export NANOEVOLVE_MODEL="your-model"
export NANOEVOLVE_BASE_URL="https://your-endpoint.example/v1"
export NANOEVOLVE_API_KEY="..."

nanoevolve run my-experiment \
  --iterations 100 \
  --random-seed 42 \
  --target-score 0.95 \
  --patience 12

Resume to a total generation target:

nanoevolve resume my-experiment --iterations 200

resume --iterations 200 means “reach generation 200,” not “run 200 more attempts.” Repeating it after generation 200 performs no additional model calls.

--target-score stops before the next generation batch once any successful candidate reaches the requested score. The target is stored in run.json and reused by resume. With workers > 1, already-started candidates in the current batch are still evaluated and committed before the run stops.

--patience N stops when N consecutive generations produce no new objective-best record. Failed generations count toward patience because they still consume an attempt. The setting is persisted for resume, and parallel runs decide only after the current batch is committed.

best --summary reports attempts, successful/failed records, completed generation, configured stopping options, and the derived stop reason. Add --json for a machine-readable object containing both best and summary.

Minimal Python API

from nanoevolve import OpenAICompatibleModel, evolve
from evaluate import evaluate

model = OpenAICompatibleModel(
    model="your-model",
    base_url="https://your-endpoint.example/v1",
    api_key="...",
)

best = evolve(
    seed="seed.py",
    evaluate=evaluate,
    model=model,
    task="TASK.md",
    iterations=100,
    target_score=0.95,
    patience=12,
)

print(best.evaluation.score)
print(best.source_path)

The evaluator is a top-level importable function:

from nanoevolve import Evaluation


def evaluate(source_path: str) -> Evaluation:
    score = run_benchmark(source_path)
    return Evaluation(
        score=score,
        feedback="Benchmark completed.",
        metrics={"runtime_ms": 12.4},
    )

score remains the default optimization target. Named metrics can also drive lexicographic objectives and MAP-Elites feature coordinates.

CLI

Command Purpose
nanoevolve run <project> Start a new run and refuse existing state.
nanoevolve resume <project> Continue to a total generation target.
nanoevolve best <project> Show the highest-scoring successful record.
nanoevolve inspect <project> <record-id> Trace lineage, evaluation, errors, and artifacts.

best and inspect support --json for scripts.

run and resume support a line-oriented event stream on stderr while keeping the final best-record summary on stdout:

nanoevolve run my-experiment --iterations 100 --json-events 2>events.jsonl

Each line in events.jsonl is one JSON object with type, generation, record_id, and data. The stream includes parent selection, model completion, candidate extraction, archive commits, failures, and new-best events.

inspect can also print one persisted artifact exactly as stored, without labels or formatting:

nanoevolve inspect my-experiment <record-id> --artifact prompt > prompt.txt

Use the artifact names shown by ordinary inspect output. --artifact and --json are mutually exclusive.

Roadmap Features

All advanced behavior is opt-in and still enters through evolve() or the existing run/resume commands:

nanoevolve run my-experiment \
  --mutation-mode search_replace \
  --inspiration-count 2 \
  --artifact-feedback stdout \
  --workers 4 \
  --archive-backend sqlite \
  --objective score:max \
  --objective runtime_ms:min \
  --feature size \
  --feature-bin size=100 \
  --islands 4 \
  --migration-interval 20 \
  --target-score 0.95 \
  --patience 12
  • Use a seed/ directory instead of seed.py for multi-file workspaces; evaluators receive the workspace directory path.
  • full, search_replace, and evolve_blocks mutation modes support complete snapshots, exact patches, and named editable regions.
  • --sandbox-command "..." wraps evaluator workers with an external isolation command; credentials must come from its environment, not command arguments.
  • workers > 1 generates a deterministic batch sequentially, evaluates it concurrently, and commits records in generation order.
  • SQLite changes only the record index. Prompts, responses, workspaces, outputs, and evaluations remain ordinary hashed files.
  • Feature metrics plus bins activate simplified MAP-Elites. Islands select locally and widen the pool on migration generations.
  • target_score / --target-score stops model calls at the next batch boundary and emits a visible target_reached event.
  • patience / --patience stops stagnant runs at the next batch boundary and emits a visible patience_exhausted event.

Run the deterministic combined showcase without an API key or network access:

python examples/roadmap_showcase/demo.py

It exercises the multi-file, SEARCH/REPLACE, inspiration, artifact feedback, parallel evaluator, SQLite, multi-objective, MAP-Elites, island, and migration paths in one inspectable run.

Version and Release Verification

The installed console script and module entry point share the same parser and version source:

nanoevolve --version
python -m nanoevolve --version

Before publishing or handing off a source snapshot, run:

python scripts/release_check.py

The release checker runs tests and compilation, verifies bilingual README structure, enforces the core line budget, checks package metadata, and rejects release-facing placeholders.

Release infrastructure is intentionally separate from runtime dependencies. Wheel and source-distribution builds use the development-only build package; installed NanoEvolve still declares zero runtime dependencies.

See CONTRIBUTING.md for development conventions, SECURITY.md for the generated-code trust boundary, and CHANGELOG.md for validated milestones.

The public repository is BokaiGuo-Lincoln/nanoevolve. Package author and license fields remain unset until the project owner provides authoritative values.

Transparent State

Each project gets one inspectable state directory:

.nanoevolve/
├── run.json
├── records.jsonl or records.sqlite3
└── candidates/
    └── <record-id>/
        ├── source.py or workspace/...
        ├── prompt.txt
        ├── response.txt
        ├── evaluation.json
        ├── stdout.txt
        └── stderr.txt

run.json is immutable initial metadata. records.jsonl is the default dynamic state truth; records.sqlite3 is an opt-in equivalent index. Candidate artifacts are written before their record becomes visible, and hashes are verified when the archive reopens.

Every mutation attempt consumes one generation—including invalid model responses, evaluator errors, and timeouts. Failed attempts remain inspectable but never enter the parent pool.

Architecture

flowchart LR
    Task["TASK.md"] --> Mutation["mutation.py<br/>prompt + model + parser"]
    Archive["archive.py<br/>JSONL / SQLite + selection"] --> Engine["engine.py<br/>deterministic batches"]
    Engine --> Mutation
    Mutation --> Runner["runner.py<br/>fresh evaluator process"]
    Runner --> Engine
    Engine --> Archive
    CLI["cli.py<br/>run / resume / best / inspect"] --> Engine
Loading

The six semantic modules remain concrete. NanoEvolve has no policy hierarchy, provider registry, hidden memory, or observer framework.

Selection

The default parent policy is deliberately small:

  • With probability 0.8, sample from the top five successful records using rank weights.
  • With probability 0.2, explore uniformly across all successful records.
  • Derive randomness from SHA-256(run_seed, generation).
  • Resolve score ties by record ID.

Optional selection layers are explicit: lexicographic score/metric objectives, metric-binned MAP-Elites, and deterministic islands with periodic migration. Leaving their flags unset preserves the original top-k behavior.

Security Boundary

Important: SubprocessRunner provides fault isolation, not a security sandbox.

Generated code and evaluators may still access the network, user-readable files, system programs, and same-user processes. Run untrusted evolution inside Docker, Podman, a virtual machine, or another external sandbox.

The default evaluator subprocess removes environment variables whose names contain API_KEY, ACCESS_TOKEN, AUTH_TOKEN, SECRET, or PASSWORD.

Roadmap

v0.1 — Nano core

  • Full-source mutation format
  • Sequential evolution loop
  • Append-only JSONL archive
  • Deterministic top-k plus exploration
  • Subprocess evaluator with explicit failure states
  • run, resume, best, and inspect
  • Deterministic no-network demo

v0.1.1 — Release hardening

  • python -m nanoevolve and shared --version support
  • Typed wheel and explicit source-distribution contents
  • Cross-platform CI matrix and package-build job
  • Standard-library release checker
  • Contributor guide, security policy, and changelog
  • Clean-wheel install and installed CLI/demo verification

v0.2 — Stronger mutation context

  • SEARCH/REPLACE diffs
  • EVOLVE-BLOCK regions
  • Inspiration candidates
  • Artifact feedback

v0.3 — Mini workspace

  • Multi-file workspaces
  • External sandbox command integration
  • Parallel evaluator workers
  • Optional SQLite archive
  • Multiple selection metrics

v0.4 — Quality diversity

  • Simplified MAP-Elites
  • User-provided feature coordinates
  • Optional islands and migration

v0.5 — Target-aware stopping

  • Persisted target score for Python and CLI runs
  • Seed and resume pre-checks with zero unnecessary model calls
  • Explicit target_reached events
  • Deterministic parallel batch-boundary semantics

v0.6 — Stagnation-aware stopping

  • Persisted patience for Python and CLI runs
  • Failed generations count as non-improving attempts
  • Resume pre-checks with zero unnecessary model calls
  • Parallel batch-boundary decisions without premature stopping

v0.7 — Run evidence summary

  • Optional best --summary human-readable report
  • JSON summary for scripts and dashboards
  • Attempt, success, failure, generation, and stop-reason accounting

The published v0.2-v0.7 roadmap is implemented. Future features still require concrete evidence from real runs; parity with a larger framework is not a goal.

Development

python -m unittest discover -s tests -v
python -m compileall nanoevolve examples
python scripts/release_check.py
python -m nanoevolve --version

The detailed design and implementation checklist live in:

Project Boundary

NanoEvolve is not a complete AlphaEvolve reproduction and is not an autonomous coding agent. It is a transparent evolutionary programming kernel designed to be read, embedded, inspected, and extended with ordinary Python.

About

A transparent, recoverable, zero-runtime-dependency LLM program evolution kernel.

Topics

Resources

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages