Harness Modules
June 3, 2026 · View on GitHub
The harness lives in src/amplifier_evaluation/harness/ as a flat set of bricks plus one assembled entry point. Each brick has a single responsibility and a small typed contract, so consumers can either call the assembled run() to do everything end-to-end, or import individual modules and compose their own flow.
The deterministic per-trial sequence wired by these modules:
launching -> installing -> seeding -> running_agent -> extracting -> grading -> cleaning_up
schema
schema.py defines the harness's stable surface area. Loaders produce these types, the scheduler consumes them, results land on disk as JSON serializations of them.
Key types:
AgentSpec— one agent loaded fromagents/<id>/. Carriesinstall(parsedinstall.yaml),meta, andinvocation_md.TaskSpec— one task loaded fromtasks/<id>/. Exposestimeout_s.TrialSpec—(agent, task, trial_number)plus optionallaunch_variablesforwarded to the DTU launch. The atomic unit of work.TrialState— Enum:pending,launching,installing,seeding,running_agent,extracting,grading,cleaning_up,completed,failed,cancelled. Theis_terminalproperty flags the last three.TrialResult— the final outcome handed back to callers. Mirrors the finalstate.json.RunSpec/RunResult— top-level inputs and outputs.
loaders
loaders.py turns benchmark directories into typed specs. Pure I/O + schema validation, no DTU, no LLM.
from amplifier_evaluation.harness import loaders
agents = loaders.discover_agents("amplifier-benchmark/agents")
tasks = loaders.discover_tasks("amplifier-benchmark/tasks")
agent = loaders.load_agent("amplifier-benchmark/agents/amplifier-foundation")
task = loaders.load_task("amplifier-benchmark/tasks/cpsc_recall_monitor")
Agents may omit data.yaml (extractor stage will be skipped for them). Tasks may omit workspace/ and grader-data/.
dtu
dtu.py is an async wrapper around the amplifier-digital-twin CLI. The harness shells out to the CLI rather than importing the engine, which keeps the dependency surface tiny and lets the backend be swapped by replacing this one file.
from amplifier_evaluation.harness.dtu import DTU, cli_available
assert cli_available()
dtu = await DTU.launch("amplifier-benchmark/tasks/cpsc_recall_monitor/profile.yaml")
await dtu.file_push("local/file.txt", "/workspace/file.txt")
result = await dtu.exec_cmd(["bash", "-lc", "ls /workspace"], timeout_s=30)
await dtu.file_pull("/workspace/output.csv", "host/output.csv")
await dtu.destroy()
All operations are async so many trials can run concurrently without blocking each other.
install
install.py installs an agent into a running DTU.
Agents declare setup_cmds: a list of bash -lc commands run inside the task's DTU after launch. There is no parallel "agent profile" path; agents always compose onto the task's profile.
from amplifier_evaluation.harness.install import (
install_agent, compose_launch_profile, verify_env,
)
missing = verify_env(agent) # checks install.yaml requires.env[]
profile_path = compose_launch_profile(
agent, task.profile_path, trial_dir / "launch_profile.yaml"
)
# Adds passthrough.services entries for agent.install.requires.env so the
# agent's required env vars reach the DTU without the task profile having
# to enumerate every possible API key. Task entries always win on conflict.
await install_agent(agent, dtu, log_to=Path("install.log"))
state
state.py is the on-disk state persistence layer. Every trial has one state.json written atomically (tempfile + rename) at every stage transition.
from amplifier_evaluation.harness import state as state_io
record = state_io.load_state(trial_dir) # None if absent
state_io.save_state(trial_dir, record)
state_io.transition(trial_dir, record, TrialState.RUNNING_AGENT)
state_io.append_log(trial_dir, "free-form note")
cancelled = state_io.check_cancel_requested(trial_dir)
The state.json schema is the harness's external observation surface. An operator (human or agent) can read it at any time to see exactly where a trial is, and can set two flags on it to course-correct a long-running batch:
cancel_requested: true— the trial stops at the next stage boundary, destroys its DTU, and transitions tocancelled.retry_requested: true— a terminal trial (failedorcancelled) resets topendingon the next harness invocation and re-runs through the full lifecycle.
trial
trial.py runs one trial end to end. This is the only place that knows the full stage sequence.
from amplifier_evaluation.harness.trial import run_trial
result = await run_trial(
spec=trial_spec,
trial_dir=Path("results/run-1/trials/<id>"),
ai_user=ai_user,
grader=grader,
extractor=extractor,
)
Properties of run_trial:
- Idempotent: writes the final state to disk on completion, failure, or cancellation.
- DTU always destroyed in a
finallyblock, even on exceptions. - Reloads any existing
state.jsonon entry. Terminal trials are skipped unlessretry_requestedis set. - Honours
cancel_requestedat every stage boundary. meta.yaml.timeoutis enforced around the AI User call viaasyncio.wait_for. Timeout transitions the trial tofailedand still destroys the DTU.- Per-stage outcomes are recorded in
state.jsonas small JSON-safe summaries (ai_user,extractor,grader), with full artifacts on disk next to it. - Extractor failure or grader failure inside a trial does not fail the trial. Both record
{"status": "failed", "error": ...}and the trial proceeds.
scheduler
scheduler.py is intentionally tiny: an asyncio.Semaphore caps concurrency, each trial runs in its own task, results return in input order.
from amplifier_evaluation.harness.scheduler import run_trials
results = await run_trials(
specs,
trials_root=Path("results/run-1/trials"),
ai_user=ai_user,
grader=grader,
extractor=extractor,
max_parallel=2,
)
Failures inside one trial never propagate to others. The scheduler also catches anything that escapes run_trial itself and converts it into a synthetic failed TrialResult, so a multi-day batch survives individual trial crashes.
The scheduler emits no events. Progress observation is via the state.json files instead, which means external observers see exactly the same data the built-in UI does.
events
events.py polls each trial's state.json and prints a flat event log to stdout. It runs as a separate asyncio.Task alongside the scheduler, so headless runs can skip it without changing anything else. No third-party UI dependency: it is plain print() lines.
import asyncio
from amplifier_evaluation.harness.events import render_events
stop_event = asyncio.Event()
ui_task = asyncio.create_task(render_events(specs, trials_root, stop_event))
# ...run the scheduler...
stop_event.set()
await ui_task
One line is emitted on the first transition into each interesting state:
PENDING -> LAUNCHING "trial started"
LAUNCHING -> INSTALLING "dtu launched dtu_id=<id>"
* -> RUNNING_AGENT "agent running timeout=<n>s"
* -> GRADING "-> grading verdict=<v>"
* -> COMPLETED "finished completed score=<x> <elapsed>"
* -> FAILED "finished failed error=<head> <elapsed>"
* -> CANCELLED "finished cancelled <elapsed>"
SEEDING, EXTRACTING, CLEANING_UP, and PENDING are intentionally silent on the console; full detail still lands in harness.log. Each line is prefixed with a [HH:MM:SS] timestamp and a fixed-width agent x task label so output from concurrent trials stays readable.
run
run.py is the assembled entry point and the "example" wiring of every other module: load, build trial specs, share one AIUser/Grader/Extractor across all trials (expensive setup() called once), schedule, optionally render the event log, write run.json + summary.json. It also configures logging so detail goes to <output_dir>/harness.log while the console stays quiet enough for the event log to own stdout.
from amplifier_evaluation.harness.run import run
result = await run(
agents_dir="amplifier-benchmark/agents",
tasks_dir="amplifier-benchmark/tasks",
selection=[("amplifier-foundation", "cpsc_recall_monitor")],
output_dir="results/my-run",
trials_per_pair=1,
max_parallel=2,
launch_variables={"SWE_REPO": "owner/name"}, # optional, forwarded to launch --var
run_id=None, # optional; defaults to a timestamped id
)
The only command-line front end is the installable amplifier-evaluation CLI (cli.py), available on PATH after uv tool install and as python -m amplifier_evaluation run from any environment where the package is importable. It wraps this same run():
amplifier-evaluation run \
--agents-dir amplifier-benchmark/agents \
--tasks-dir amplifier-benchmark/tasks \
--output-dir results \
--agent amplifier-foundation \
--task cpsc_recall_monitor \
--max-parallel 2 \
--trials-per-pair 1
Selection is either the cartesian product of --agent × --task (both repeatable; default: everything discovered) or explicit --pair agent:task (repeatable; mutually exclusive with --agent/--task). --launch-var KEY=VALUE (repeatable) is how examples 03 and 04 inject SWE_REPO/SWE_COMMIT; --run-id pins the output subdirectory name (the form the example run.sh scripts use); --dry-run previews the selection. See amplifier-evaluation run --help for the full list.
Consumers that need different behaviour can either pass overrides to run() or copy run.py and edit it directly. Bricks are stable; the example is meant to be replaced.
On-disk layout
One harness invocation produces:
<output_dir>/
run.json plan: agents, tasks, selection, started_at
summary.json final counts + per-trial summaries (written at end)
trials/<trial-id>/
state.json state machine + history + per-stage summaries
trial.log human-readable transition log
launch_profile.yaml task profile merged with the agent's required env (compose_launch_profile)
install.log agent install output (setup_cmds)
instructions.txt task instructions pushed into the DTU
ai_user.json AI User result (conclude verdict, session id, full text)
extraction/ Extractor output: extraction_report.md + manifest.json + pulled artifacts
grader/<eval>/ Grader output: initial_report.md + rubric.json per evaluation
grader/grader_result.json weighted overall + per-evaluation scores
Everything needed to analyze a trial is in its trial directory. Re-runs reuse the same layout: terminal trials are skipped, in-flight ones restart cleanly because the DTU is gone.