Writing a benchmark
A benchmark is a Task: a dataset of scenes plus scorer(s).
It is embodiment-agnostic: it describes what to evaluate, not how the
robot is built.
from inspect_robots.scene import Scene, Target
from inspect_robots.scorer import success_at_end
from inspect_robots.task import Epochs, Task
task = Task(
name="cubepick-reach",
scenes=[
Scene(
id=f"layout-{i}",
instruction="reach the cube",
target=Target(kind="reach_object", spec={"object": "cube"}),
init_seed=i,
)
for i in range(50)
],
scorer=success_at_end(),
max_steps=200,
epochs=Epochs(count=3, reducer="mean"),
)
Scenes
Each Scene is one initial condition (the Inspect Sample
analog):
id: unique within the task.instruction: the language goal handed to the policy.target: an optionalTargetthe scorer reads; itskindis resolved in the embodiment's namespace (compatibility checking verifies the embodiment can realize it).init_seed: combined with the eval seed and epoch index to seed each trial deterministically. (An eval run withseed=Nonedraws a fresh OS seed and records it in the log, so even "unseeded" runs are reproducible after the fact, and distinct fromseed=0.)
Automatic task generation
generate_scene can ask a
vision-capable model to design one task from an embodiment's initial camera
frames. The result has two consumers: scene.instruction is handed to the
policy, while scene.metadata["rubric"] is handed to the grader. The scene
metadata, including the generated rubric and generation provenance, is copied
into the per-scene eval-log result.
By default, the model is told to inspect the visible tabletop, choose one
concrete manipulation task that a single arm could plausibly attempt, use only
visible items, and write observable success conditions that are meaningful,
fair, and achievable. Pass instructions="..." to replace those design
instructions, or instructions_file="prompt.txt" to read them from a UTF-8
file. The required TASK: and RUBRIC: reply format is always appended and
cannot be overridden.
Generation performs a peek reset before evaluation. Pass the same integer
seed to generate_scene() and eval() so a seedable simulator presents the
same epoch-zero layout to the task designer and the policy:
from inspect_robots import Task, eval, generate_scene
from inspect_robots.scorer import operator_scorer
seed = 0
scene = generate_scene(
embodiment,
model="claude-sonnet-4-5",
seed=seed,
)
task = Task(
name="generated-tabletop-task",
scenes=[scene],
scorer=operator_scorer(),
max_steps=300,
)
logs = eval(task, policy, embodiment, seed=seed)
The caller owns the embodiment throughout this sequence. Generation leaves it open for the subsequent evaluation.
Epochs and reducers
Repeat each scene epochs times to measure stochastic policies. The
Epochs reducer collapses the per-epoch scores of a scene
before metrics aggregate across scenes. Builtin reducers: mean, median,
max, min, mode, and pass_at_<k> (an unbiased pass@k estimator).
Multiple scorers
Pass a list to score several dimensions at once:
from inspect_robots.scorer import episode_length, min_distance_to_goal, success_at_end
task = Task(
name="cubepick-reach",
scenes=[...],
scorer=[success_at_end(), episode_length(), min_distance_to_goal()],
max_steps=200,
)
Horizons
A task declares exactly one rollout horizon. Use max_steps when the protocol
is inherently discrete, as in the examples above. Use max_seconds when every
embodiment should receive the same physical-time budget:
task = Task(
name="two-minute-reach",
scenes=[...],
scorer=success_at_end(),
max_seconds=120.0,
)
At evaluation time, Inspect Robots resolves the budget as
ceil(max_seconds * embodiment.info.control_hz). A 120-second task therefore
runs for 1,200 steps at 10 Hz and 1,800 steps at 15 Hz. The eval log records
both the declared seconds and the resolved integer step limit.
A seconds-based task is incompatible with an embodiment whose control_hz is
missing, non-finite, zero, or negative. Event-driven embodiments should use
max_steps. Resolution changes the step budget only: rollout() does not add
wall-clock pacing, so real-time cadence remains the embodiment's responsibility.
Registering for discovery
Wrap a task factory with task so it resolves by name in
eval("my-bench", ...) and appears in inspect-robots list:
from inspect_robots.registry import task
@task("my-bench")
def my_bench(num_scenes: int = 50) -> Task:
return Task(name="my-bench", scenes=[...], scorer=success_at_end(), max_steps=200)
See Plugins to ship a benchmark from a separate package.