The scripts are in the repository, not in the installed package. Clone it,
then
cd recipes/02-measure/eval-your-agent before running the commands below. Browse this recipe on GitHub.--gap), the agent(message) -> {steps, final_text}
contract, why the judge reads the trajectory and not the prose, what a
hollow run looks like and how the SDK flags it, and how to turn a pass
rate into an exit code. You need nothing; this recipe is offline. Seconds.
Run it
--k stays at 4 or above: pass^k and pass@k are None below four
repeats (min_k), and the note field says so. Rows are the budget and
asks are the situations, so a run needs budget >= situations * repeats
or the later asks never get rolled out.
Find what is untested
Before writing the eval:wai.coverage_gap takes the asks a suite
already sends and says which parts of the policy they never reach. The
axes are the ones simulate covers, so the answer comes back in the
engine’s own words: which tool, which policy rule, what stance the person
takes, what the world looks like, what condition the tool is in.
OLD_TESTS in run.py is the three-ask suite this recipe replaces.
escalates_over_limit fires on 0 rows until a seed
reaches it). Rule matching is word overlap, so a branch that only the
fixture data selects reads as untested even when an ask lands on it: pass
rows= from a graded run and the report also names the rules whose every
row ended in the same tool fault, which is a missing fixture, not a
missing ask.
wai.coverage_gap("tests/test_refunds.py", ...) reads the string
literals that look like asks (passed to a call or sitting in a list,
over fifteen characters, with a space in them), and
wai.coverage_gap("asks.jsonl", ...) reads the prompt of every row.
What you get
"refund" in reply.lower()) would have passed it. The branch table is
the number that matters; the overall pass@1 hides it.
The four pieces
The wrapper. The engine hands your function one ask and wants the tool calls it made and what it said. A callable agent runs its own real tools and is played single-turn.run.py reads the raw rollouts as
data.trajectories; data.rows is the same rollouts exported, and both
data.rows and data.rows() work.
lookup_order description, because a writer that does not know which
ids exist invents ones that do not, every rollout is “not found”, and
the run is hollow.
The judge. The refund policy as a program (refundable()), shared
with the careful bot so it lives in one place. It reads row["steps"]:
which tools ran, in what order, with what. Markers are named so 1.0 is
always the good outcome; a marker that does not apply to a row is None
and its rate counts only the rows it measured.
The gate. --gate 0.9 exits 1 under the floor and 2 when the SDK’s
coverage warnings say the run is hollow (no rollout called a tool, a
declared tool no rollout touched, a marker that fired on no row). A
pass@1 from a hollow run is not reported.
Swap in your agent
Keeprun.py, replace AGENTS. For a model-backed bot that records
calls through a shared list, make the recorder thread-local: concurrency
defaults to 32, so 32 rollouts call your function at once and one shared
list mixes their calls together.
TOOLS (a bare
{"name", "description", "parameters"} dict works too, and so does the
Anthropic {"name", "description", "input_schema"} shape), put your real
ids in the descriptions or seeds, and keep refundable() as the policy
your bot is supposed to follow. Then pass simulator="hosted" instead of
simulator=False to let the hosted writer produce more varied asks, and
raise --k. The hosted writer needs a key: whileai login, or
WHILEAI_API_KEY in the environment. A hosted run is minutes, and
it says where it is on the whileai.simulations logger
(12/64 rollouts, 3 situations written, 1m40s elapsed, ~5m left) once
logging.basicConfig(level=logging.INFO) is on.
Next
- Fast lane: a pytest file that runs
refund_judgeon eight hand-written rows, no model calls, one second. Judge edits cannot drift unnoticed. - Judge trust:
wai.attach_labels(rows, labels, kind="human")thenwai.judge_trust(rows, refund_judge). FAIL on eight labels means label more (about thirty clears the Wilson bound), not that the judge is wrong. - Every failure is a training example:
wai.simulate(traces=scored.failures())aims the next round at what broke (scored.failed_traces()andscored.tracesare the same list under other names).evaluaterows are stamped so the selectors refuse to use them as the reward. scored.push("refund-evals", purpose="eval")keeps the set out of training on the platform.