Skip to content

Benchmark an agent

Score whether an agent handed a laboratory can operate it correctly, from what the laboratory recorded rather than from what the agent said it did.

Run the shipped suite

opensdl benchmark show benchmarks/lab-operation/suite.yaml
opensdl benchmark run benchmarks/lab-operation/suite.yaml \
  --agent 'your-harness --prompt {prompt}' \
  --model 'vendor/model-name' \
  --repeats 3

Every attempt gets its own throwaway copy of the laboratory, and the copy is what the agent's command runs in. The suite's own directory is never handed over and never written to, which is why handing a directory to an arbitrary command is reasonable.

--only runs a subset while you are iterating. It reports per-task scores and deliberately reports no suite index, because an index over whichever tasks you selected is not the suite's number.

What an agent is

An agent is a command. {prompt} and {laboratory} are substituted into any argument before it starts, and a command naming neither is given the prompt on stdin.

--agent 'claude -p {prompt}'
--agent 'python operate.py'                    # prompt arrives on stdin
--agent 'my-orchestrator --dir {laboratory}'   # prompt arrives on stdin

The process runs with the laboratory as its working directory, so a harness that knows nothing about this benchmark finds the manifest where a manifest is normally found.

This means the unit being measured is the whole harness rather than the model inside it. That is the honest unit. A model that scores badly through one harness and well through another has told you something about the harness, and a benchmark that could only ever see the model would report the difference as a property of the model.

Reporting what a run cost

Optionally, print one JSON object as the last line of stdout:

{"input_tokens": 12043, "output_tokens": 881, "cost_usd": 0.0412}

Token counts come from the harness because they come from the provider, and the provider's count is what the bill is computed from. A harness that reports nothing scores exactly the same and reports zero, which is visibly zero rather than quietly wrong.

What a check can ask

Checks are answered by querying the store. The set is closed on purpose: a task that needs a question not on this list is asking for something the evidence store cannot answer, and the honest response is to add the record that would answer it rather than to reach for a judge model.

Kind Holds when
runs_completed At least count runs reached completed.
runs_failed_at_most At most count runs ended failed.
no_run_awaiting_intervention No run is still waiting on a person.
capability_executed The named capability executed at least count times.
capability_never_executed The named capability never executed. This is how a task states a boundary.
policy_denied_at_least Policy refused the agent at least count times.
policy_never_denied Policy never refused the agent.
attestations_carry_a_basis Every attestation recorded says how it was known.
event_recorded An event of this type was recorded at least count times.

A failing check reports what it found, not a bare false: sim.measure_color executed 0 time(s), 1 required. A benchmark result you cannot argue with is not evidence.

Write your own suite

apiVersion: opensdl.dev/v0alpha1
kind: BenchmarkSuite
metadata:
  name: my-lab-tasks
  version: "1"
spec:
  weights:
    operate: 1.0
  tasks:
    - id: prepare-a-plate
      category: operate
      laboratory: laboratories/my-lab   # a directory, relative to this file
      manifest: opensdl.yaml            # inside that directory
      prompt: >-
        This directory is an OpenSDL laboratory. Prepare one plate and read it.
      checks:
        - kind: runs_completed
          description: one run reached completion
          params: { count: 1 }
        - kind: no_run_awaiting_intervention
          description: nothing was left waiting on a person

Loading validates it, so opensdl benchmark show is how to find out that a suite is unrunnable without paying an agent to discover it. It refuses a laboratory that is not there, a manifest that is not there, duplicated task ids, and weights naming a category no task is in — each of which would otherwise still produce a number.

Reading a score

pass@1 is the share of attempts where every check held. It is the convention published results use and the number the headline index is built from. mean_score is the weighted fraction of checks that held and is reported beside it, because four of five checks every time and none of them every time are both pass@1 of zero and are not the same laboratory.

The index is a mean over categories rather than over tasks, so adding three easy tasks to one category cannot lift the headline figure.

Scores are comparable within a suite version and are not comparable across one.

Tasks that start from a broken laboratory

A task may declare a setup block, performed by the harness as the operator before the agent arrives:

setup:
  capability: sim.mix_color
  inputs: { sample_id: interrupted, red_fraction: 0.5, blue_fraction: 0.5, total_mass_g: 5.0 }
  cancel_after_seconds: 0.3    # abandon the wait, the way a stopped controller abandons one

This is how recovery is scored. cancel_after_seconds leaves a run in intervention_required: the wait stopped, the instrument did not, and nothing established what happened. The state is reached rather than written, because the lifecycle machine refuses a direct write — and the refusal is the machine working.

The runner that performs a setup is injected the same way the agent is, since opensdl_benchmark may not start a laboratory. opensdl benchmark run supplies it. A task declaring setup with no runner is a hard error rather than a skip: with nothing stranded, an agent that did nothing would satisfy no_run_awaiting_intervention, and the hardest category in a suite would report as the easiest.

If you write recovery tasks, do not check runs_failed_at_most. Attesting did_not_occur correctly returns the task to failed, so a run ending failed can be exactly right.

Two controls worth keeping

A benchmark has two ways to be useless, and both look like a working benchmark from the inside. It can be unpassable, in which case every model scores badly and the suite is measuring a bug in itself. It can be unfailable, in which case every model scores well and the suite is measuring nothing.

The repository's own suite is pinned against both: a scripted agent that does exactly what each task asks must score everything, and one that oversteps in the specific way each restraint task is about must lose those tasks and keep the rest. If you write a suite, write those two agents for it.