Quick Start
1
Run K attempts per case
Build an agent, wrap your cases in an
EvalPackage, and call run_trials.2
Async variant
Use
arun_trials inside an existing event loop.3
Save and reload
Persist the full report — including per-attempt trajectories — and load it back.
4
Gate CI on the pass rate
Read
summary() for a stable, machine-parseable dict and fail loudly below your bar.How It Works
Each case runs K times on a fresh, isolated copy of the agent; every completed attempt is scored, then results are assembled into aTrialReport.
Scoring — one contract, four fallbacks
Scoring runs only onstop_reason="completed" attempts and resolves in this exact order.
EvalCase.verifycallable — if present, it wins. A raisingverifyis caught and recorded as a failedTrialScorewithreason="verify error: ..."— one bad metric never aborts the report.- Tool assertions — if
case.metadata["expected_tools"]is set, the engine reusesReliabilityEvaluator._extract_tool_calls. Score is1 − (missing / expected); passes only when every expected tool was called. - Criteria / expected via Judge — falls through to the existing
Judge. The judge’s 0–10 score is normalised to[0, 1]. - No scorer configured — a non-empty completion counts as a pass (
reason="no scorer configured"); an empty completion is a fail.
Configuration Options
run_trials() and arun_trials() share the same signature.
TrialScore
TrialScore.to_dict() returns {"value", "passed", "reason"}.
TrialAttempt
TrialAttempt also has to_dict(), from_dict(), and to_eval_result() (adapts to the existing EvalResult shape).
TrialReport methods
New EvalCase / EvalResult fields
verify return values are coerced: bool → TrialScore(1.0/0.0), int/float → TrialScore(value=x, passed=x >= 0.5), TrialScore → returned as-is, anything else → truthiness fallback.
Isolation guarantees
Every attempt runs on an independent copy of the agent so K attempts are K real samples and your live agent is never mutated.- Fresh
agent_idand_session_idper attempt. chat_historyrebound to a new list (not shared with the caller).- Independent system-prompt cache.
memoryandknowledgewrites severed (set toNonefor the attempt).
Timeouts & wedged agents
Common Patterns
Frontier-first iteration
Runk=8, look at report.frontier(), and focus improvement work there — mastered and zero-pass cases are lower-value to iterate on.
CI gating with summary()
Assert a threshold on the machine-parseable dict inside a pytest job.
Save trajectories → SFT dataset
Persist the report, then export it to a trainer-ready dataset.Which scorer should I use?
Pick the strongest signal your case can provide.Best Practices
Start with a small k (2–4)
Start with a small k (2–4)
Gauge cost with a small
k first, then scale up once the pipeline is trustworthy.Size concurrency to your rate limit
Size concurrency to your rate limit
concurrency sizes the thread pool — set it to your API rate limit divided by 2, not higher.Prefer verify=fn for deterministic metrics
Prefer verify=fn for deterministic metrics
A
verify callable has no LLM cost and no judge bias. Use it whenever you have a deterministic metric.Treat frontier() as your backlog
Treat frontier() as your backlog
Iterate on
frontier() cases, not every case where pass_rates() < 1.0 — mastered and zero-pass cases are lower-value.Drop records on very large runs
Drop records on very large runs
Set
capture_record=False on very large runs (>100 cases × k>16) to keep the report JSON compact.Related
Evaluation Loop
Iteratively run, judge, and keep the best output
Judge
LLM-as-judge — the fallback scorer for criteria and expected
Prompt Optimizer
The other consumer of eval packages
Extensibility
run_trials is the concrete EvalRunnerProtocol
