Skip to content

Run a scaffold

For model-driven search, running a discovery method is four objects — a model, a config, a scaffold, and a compatible runnable task — and one call. Coding-agent modes construct their agent instead of an API model.

import galapagos as gx

model    = gx.GalapagosModel.from_card(name="openai/gpt-5.5", host="openrouter")
config   = gx.GalapagosConfig.from_config(scaffold_name="openevolve")
scaffold = gx.GalapagosScaffold.from_card(name="openevolve", config=config, model=model)
task     = gx.GalapagosTask.from_card(name="circle_packing")

result   = scaffold.run(task=task)
print(result.best_score)        # best combined_score found
print(result.best.content)      # the winning program (a string)

scaffold.run(task=...) drives the six-component loop: select parents from the Population → build a prompt → propose a candidate → evaluate it → add the scored Genome back → repeat until the budget is spent.

The four objects

Model

GalapagosModel.from_card(name=..., host=...) resolves a hosted, OpenAI-compatible endpoint. The supported hosts are openai, openrouter (the default), anthropic, gemini, azure, vllm, and litellm — or pass an explicit OpenAI-compatible base URL as the host. The API key is read from the environment per host — OPENAI_API_KEY for openai, OPENROUTER_API_KEY for openrouter, ANTHROPIC_API_KEY for anthropic, GEMINI_API_KEY (or GOOGLE_API_KEY) for gemini, and AZURE_OPENAI_API_KEY for azure. OpenRouter requires OPENROUTER_API_KEY (or an explicitly passed api_key) so an OpenAI credential is never sent to OpenRouter. vllm and litellm need no key (see the host/env-var tables in Models).

model = gx.GalapagosModel.from_card(name="openai/gpt-4o-mini", host="openrouter")

Every API-model run calls a live LLM, so set an OpenRouter key first: Galapagos reads it from OPENROUTER_API_KEY — see Installation.

Config

GalapagosConfig.from_config(scaffold_name=...) loads the scaffold's bundled default config; or pass path="cfg.yaml" for your own. Read and override tunables with dotted paths:

config = gx.GalapagosConfig.from_config(scaffold_name="openevolve")
config.set("population.num_islands", 8)
config.set("general.max_iterations", 200)
config.get("general.max_iterations")     # -> 200

The general section holds run-level settings such as the iteration budget, the checkpoint interval, the mutation approach, and the inner-retry policy.

Scaffold

Three ways to construct one:

# 1. by name via the registry (the base class dispatches)
scaffold = gx.GalapagosScaffold.from_card(name="openevolve", config=config, model=model)

# 2. a concrete subclass loads its own card + defaults (config/model optional)
scaffold = gx.OpenEvolveScaffold.from_card(model=model)

# 3. build-your-own from components (see "Write your own scaffold")
scaffold = gx.GalapagosScaffold.from_card(population=..., selection_policy=..., proposer=...)

List the runnable scaffolds at any time:

gx.available_scaffolds()    # every bundled card: ['adaevolve', 'ale_agent', 'algotune_agent', 'beam_search', 'best_of_n', 'best_of_n_attempts', 'claude_code', 'codex', 'evox', 'gepa', 'meta_harness', 'openevolve', 'random', 'topk']
gx.registered_scaffolds()   # the runnable subset — the same fourteen

All bundled scaffolds are runnable

The catalog ships 14 cards — adaevolve, ale_agent, algotune_agent, beam_search, best_of_n, best_of_n_attempts, claude_code, codex, evox, gepa, meta_harness, openevolve, random, and topk — and every one has a runnable Python controller. GalapagosScaffold.from_card("nope", ...) raises a clear KeyError listing the runnable set.

Task

GalapagosTask.from_card(name=...) loads the problem statement, the seed program, and the Evaluator. The Evaluator is supplied by the task, not the scaffold. A run still requires a runnable task and compatible language, dependencies, hardware, and execution mode.

task = gx.GalapagosTask.from_card(name="circle_packing")
task.context            # the problem text injected into prompts
task.runnable           # True iff it ships a seed + evaluator.py
task.status             # 'stable'
task.initial_genome()   # the seed Genome

The catalog bundles 1,274 task cards; 1,269 are runnable. circle_packing and function_minimization are the canonical quickstart examples. See the task catalog.

Meta-Harness

Meta-Harness requires persistent archive storage and does not support checkpoint resume:

galapagos run --scaffold meta_harness --task circle_packing \
    --output-dir outputs/meta-circle-packing

The archive is <output-dir>/meta_harness_D. Its default proposer is Claude Code, authenticated by CLAUDE_CODE_OAUTH_TOKEN or an existing Claude login. With the default Docker evaluation mode the controller, proposer, and evaluator all execute inside the task container environment. Set proposer.mode=llm only when you intentionally want the API-model fallback.

The Iteration Budget

The base loop stops when state.iteration >= general.max_iterations. Set it on the config, or override it inline on run:

config.set("general.max_iterations", 100)  # cap on iterations

result = scaffold.run(task=task, max_iterations=50)   # inline override of max_iterations

Reading RunResult

run returns a RunResult:

result = scaffold.run(task=task)

result.best          # the best Genome (or None)
result.best_score    # result.best.fitness, or -inf
result.history       # list[Genome] — the seed + every evaluated child, in order
result.run_dir       # run directory (if the scaffold set one)
result.summary       # a dict, e.g.:
{
    "scaffold": "openevolve",
    "task": "circle_packing",
    "iterations": 100,        # loop steps taken
    "evaluations": 94,        # genomes evaluated = 1 (seed) + iterations - no_diff
    "best_score": 2.61,       # best combined_score
    "best_metrics": {"combined_score": 2.61, "validity": 1.0},  # the best Genome's full metric dict
    "cost_usd": 0.42,         # accumulated model spend
    "no_diff": 7,             # wasted steps where the Proposer returned a no-op
    "rejected_too_long": 2,   # candidates dropped for exceeding the length cap
    "language": "python",     # the task's program language
    "population_size": 40,    # genomes currently in the Population
}

The winning artifact is result.best.content (a string of source code). Its metric dict is result.best.scores and the headline number is result.best.fitness (== result.best.scores["combined_score"]).

Record the complete trajectory

Set an output directory when you need the complete record—not only the winning program. It automatically enables both raw JSONL streams; no separate trajectory path is needed. Add --emit-etif only when you also want the normalized, scaffold-neutral JSON document:

galapagos run --scaffold openevolve --task circle_packing \
    --output-dir outputs/demo \
    --emit-etif

The run writes:

outputs/demo/
├── evolutionary_trajectory.jsonl
│                             # candidates and every evolutionary lifecycle event
├── tool_calling_trajectory.jsonl
│                             # one validated event per tool execution attempt
├── config.yaml                # latest secret-free effective config
├── run.json                   # terminal summary and artifact index
├── best/                      # canonical winning candidate
├── checkpoints/
│   └── checkpoint_<i>/
│       ├── state.json
│       └── tool_calling_trajectory.jsonl
│                             # cumulative tool calls through checkpoint i
└── etif.json                 # optional normalized export added by --emit-etif

evolutionary_trajectory.jsonl includes candidate creation as well as behavior without a solution row: evaluator or model failures, migration, crossover, eviction, adaptation, guidance/strategy evolution, agents, tools, memory, checkpoints, and final evaluation. See Trajectories (ETIF) for the event envelope and extension rules.

From Python, run_dir enables the raw streams in the same way; export them afterward when needed:

from galapagos.trajectory import convert_run_dir

result = scaffold.run(
    task=task,
    run_dir="outputs/demo",
)
convert_run_dir(result.run_dir)

A short, cheap run

Every model-driven run calls a live LLM and spends budget. Keep an exploratory run small by starting on the tiny function_minimization task and capping the iteration count:

import galapagos as gx

model    = gx.load_model("openai/gpt-4o-mini", host="openrouter")
scaffold = gx.OpenEvolveScaffold.from_card(model=model)
task     = gx.load_task("function_minimization")    # the fastest task

result   = scaffold.run(task=task, max_iterations=20)
print(result.best_score)                         # > the seed score

The CLI

The galapagos console script wraps the same flow. Use repeatable --set flags for model and config overrides.

# a short run on the smallest task
galapagos run --scaffold openevolve --task function_minimization \
    --set proposer.model_name=openai/gpt-4o-mini \
    --set proposer.api_base=openrouter \
    --set general.max_iterations=20

# a longer run via OpenRouter
galapagos run --scaffold openevolve --task circle_packing \
    --set proposer.model_name=openai/gpt-5.5 \
    --set proposer.api_base=openrouter \
    --set general.max_iterations=100

# point at a custom config YAML, set the seed
galapagos run --scaffold adaevolve --task function_minimization \
    --config my_config.yaml \
    --set proposer.model_name=openai/gpt-4o-mini \
    --set proposer.api_base=openrouter \
    --set seed=7

# inspect the catalogs
galapagos scaffold list
galapagos task list

galapagos run prints the final best_score and the summary JSON. Use galapagos submit to validate a card.