Concepts
The mental model for benchflow. Read once, then refer back from the how-tos.The five primitives
Rollout lifecycle
ARollout is decomposable: each phase is a callable method, you can either run them in sequence or invoke Rollout.run() to execute all six in order. Multi-agent flows reuse phases (e.g. connect + execute + disconnect repeats per role).
Rollout.run() is the convenience that calls them in order.
Scenes, Roles, Turns
A Scene is authoring sugar for Step metadata. Inside a Scene:- Roles are the agents that participate (one or more).
- Turns are the prompt sequence — which Role acts when, and what they’re told.
- All Roles share the same sandbox filesystem.
The User abstraction (multi-round, single-agent)
Sometimes you want the agent to take multiple turns guided not by another LLM but by a Python callback that watches what happened and decides what to say next. That’s a User. A User is aBaseUser subclass (or FunctionUser wrapping a function) with two methods:
setup(instruction, solution)— once, before round 0run(round, instruction, round_result) → str | None— per round; returnNoneto stop the loop
soft_verify() (verifier without the destructive parts of full hardening), gives the user the round’s RoundResult (trajectory, rewards, verifier output, tool count), and lets the user decide round N+1’s prompt.
Use BaseUser when the loop logic is rule-based (compress instruction → show test failures as hints → stop on pass). See progressive-disclosure.md for the full guide.
Verifier, sandbox, hardening
Once the agent stops, the verifier runs. Its entry point is the task’stest.sh script — uploaded to /verifier for native packages (/tests for
legacy ones) — executed against the workspace the agent left behind. benchflow
runs test.sh as a script (it chmod +x’s the file and executes it
directly; a native script strategy runs cd /verifier && <command>). It
never hands test.sh to pytest — pytest cannot collect a shell script as a
test target.
Most test.sh scripts invoke pytest internally. For those invocations,
benchflow applies hardening through PYTEST_ADDOPTS in the verifier
environment — every pytest run inside test.sh inherits roughly:
<verifier-dir> is /verifier for native packages (/tests for
legacy), and <workspace> is the agent workspace (/app for Harbor/SWE-bench
conventions, /root for SkillsBench — injected dynamically). -c /dev/null
blocks pyproject.toml/pytest.ini discovery and --confcutdir blocks
conftest.py walk-up beyond the verifier dir. Tasks that do not use pytest
(e.g. a test.sh that diffs files and writes reward.txt directly) are
scored the same way — pytest is just the most common tool, not a requirement.
Between agent and verifier, benchflow hardens the sandbox to prevent the agent from gaming the score:
- Kill any lingering agent processes
- Restore build-config files (setup.py, pyproject.toml, …) to their pre-agent snapshots
- Delete agent-injected
conftest.py,sitecustomize.py,.pthfiles - Lock the workspace to root, set restrictive PYTHONPATH/PATH for the verifier process
- Run pytest with plugin auto-discovery off, only allowing plugins declared in the task config (
[verifier] pytest_pluginsintask.toml, or auto-discovered root-owned plugins)
docs/labs/benchjack-sandbox-hardening/ and docs/labs/reward-hack-matrix/.
When a task ships a legitimate conftest.py (e.g. qutebrowser uses one to break a real circular import), the task opts out via task.toml:
progressive-disclosure.md for the full opt-out list.
Verifier file map
Nativetask.md packages and the legacy split layout name their verifier
files differently. The runtime resolves native files first and falls back to
the legacy names, so a task ships one of each row, not both:
A plain
test.sh is a complete verifier on its own: with no verifier.md
strategy declared, the runtime just executes it. verifier/verifier.md
declares how a task is scored (script / llm-judge / reward-kit / agent-judge
/ ors-episode) and is the native equivalent of the legacy [verifier] section
in task.toml. The native LLM-judge rubric lives under verifier/rubrics/
(both a human-readable verifier.md and a machine-readable verifier.toml),
not in a single top-level rubric.toml. For the native verifier document and
its strategy table see Native task.md authoring;
for the legacy [verifier.judge] rubric path see LLM-as-judge.
Multi-turn vs multi-round vs multi-scene
Three different axes — easy to confuse, worth pinning down:
Single-agent simple runs use none of these. Pick the axis based on what state needs to persist (memory? sandbox? both?).
Trajectories and rewards
Every agent action is captured as an event in the trajectory — tool calls, agent messages, agent thoughts. ARolloutResult (aliased as RunResult) has the full trajectory plus tool count, plus rewards from the verifier and any error.
rewards is a dict produced by the task’s verifier. Convention: {"reward": float} where 1.0 = pass, 0.0 = fail. Tasks may add additional metrics (e.g. exact_match, partial_credit).
Trajectories are written to <jobs_dir>/<job_name>/<rollout_name>/trajectory/acp_trajectory.jsonl (the --jobs-dir directory, default jobs/). Use them for replay, debugging, or training data.
Where to go next
- Getting started — install, run your first eval.
- Task authoring (native task.md) — write a task as a single
task.mddocument plusenvironment/andverifier/. - Migrating a legacy task — convert an existing
task.toml+instruction.mdsplit package totask.md(the split layout is no longer a first-class authoring path). - LLM-as-judge — use an LLM to score subjective tasks against a rubric (see the verifier file map for native vs legacy rubric paths).
- Progressive disclosure — the User abstraction; SWE-bench Pro case study.
- Use cases — multi-agent patterns (coder/reviewer, simulated user, BYOS, stateful environments).
- CLI reference, Python API reference.
- Skill evaluation — when the artifact is a skill, not a workspace.