Reading Results
AgentEvaluator().run(...) returns an AgentEvalResult and writes nothing. Call result.persist() to
store it as a run bundle on disk. The object and the bundle hold the same data — use the object for
programmatic follow-up, and the bundle (especially report.html) to inspect or share a run.
The result object
The summary
result.summary (an AgentEvalSummary):
-
summary.scores.scores— the aggregates. Each is named<metric.type>.<output>(andview.<name>for a view), withmean,min,max, andstd_dev. This is what the guides print: -
summary.metric_coverage— per metric output, how many trials weretotal/scored/ failed / missing, so you can tell a low mean from low coverage. -
summary.task_metric_values— per task, the individual trial values behind those means, keyed<metric.type>.<output>. Each record carries thetrial_idthat produced it and its metricvalue, so you can answer “which tasks were flaky, and on which trial?” without regroupingresult.scoresyourself:A
valueofNoneis a trial that died before scoring — a trial that did not pass. A trial whose metric failed is absent entirely, because that leaves it unmeasured rather than unsuccessful. Look up bytrial_idrather than by position: the two rules above mean lists for different outputs of one task need not be the same length.Values keep the type the metric produced them in — a count stays an
int, a flag stays abool, and a judge’s verdict stays astr. Each record’svalue_typesays which it is (number,labelormissing), which is what tells a realNaNapart from a label that reads"NaN", since strict JSON has no NaN literal and both travel as strings. Before doing arithmetic, project withnumeric_metric_values, which drops labels and keeps a dead trial’sNone: -
summary.task_outcomes(metric_name=None)— the same data as models rather than nested dicts, sorted by task then metric, each naming its owntask_idandmetric_name. Pass a"<metric.type>.<output>"to narrow to one metric, which is what a report over a single metric wants:- When you narrow, a task the metric never measured is dropped — it was scored by a different metric, so listing it would invent missing coverage.
- A task that declared the metric but produced no usable value keeps its entry with an empty
trialslist, because there the coverage really is missing. - Unfiltered, every task is returned.
-
summary.task_count,summary.trial_count,summary.score_count.
Per-metric scores
Each entry in result.scores carries: id, run_id, task_id, trial_id, metric_type, status
(e.g. completed / failed), outputs (the metric’s named outputs), diagnostics, and metadata.
Use these to drill from an aggregate down to the individual (task, metric) that produced it.
Trials
Each entry in result.trials carries: id, task_id, status (completed / partial / failed),
output (the agent’s final answer), evidence (trajectory, final state, logs), and metadata. Trials
are the durable, scorer-agnostic record — they can be re-scored offline later.
The run bundle
Call result.persist() and it writes these files (the same data, on disk):
persist() returns a BundleLocation telling you where the bundle landed:
report.html is the fastest way to eyeball a run or hand it to someone else; the .jsonl files are
convenient for loading scores and trials into your own tooling.
persist() defaults to work_dir, which is where the run’s trial evidence already lives — that keeps
the bundle self-contained, so it survives being moved or copied. Passing an explicit
persist("./elsewhere") is supported, but the bundle’s evidence references still point back at the
original directory and only resolve while it exists.
A run with no work_dir and no explicit target raises rather than inventing a directory.
report.html is written unless you pass persist(write_dashboard=False), which emits just the
JSON/JSONL artifacts and skips the HTML. The .json and .jsonl files are always written.