fix(v1): eval writes summary.json beside traces.jsonl - #2520
Conversation
A finished run dir held traces.jsonl, its resolved config and logs, and nothing that said how the run went: the dashboard computed mean reward and error share for display only, and every consumer re-derived pass rate, mean reward and failures from traces.jsonl with its own reading of Episode.ok, Trace.reward and trainable. The constraint: one reading of the record, owned by vf. `summarize` in cli/output.py is that reading. run_eval writes it as summary.json when the run completes (a resume drops the stale one and rewrites it on completion) and logs one summary line; the dashboard footer shows the same numbers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 502cc78. Configure here.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR adds a new persistent You can add or adjust custom eligibility rules. Learn more. |
An ok trace keeps the errors its own per-agent retry recovered from (Agent.run prepends that history), so reading any trace's last error could name a sibling's recovered ProviderError as what failed the episode. Read failed traces only, then the episode's own last error, the live errors episode_should_retry reads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| """Write the completed run's `summary.json` beside its traces; return its path. Nulls | ||
| included: an unscored reward reads as `null`, never as a missing key.""" | ||
| path = results_dir / SUMMARY_FILE | ||
| path.write_text(summary.model_dump_json(indent=2)) |
There was a problem hiding this comment.
summary.json is the run-completion artifact, but Path.write_text() creates or truncates the final path before the write completes. An interruption or full-disk write can leave malformed JSON that a watcher observes as completed, and --resume will not repair it when no rollouts are owed. Please serialize first, write a sibling temporary file, and atomically replace the final path.
| plan = [(task, n) for task, n in zip(tasks, counts) if n] | ||
| # The kept rows are a new run state: the summary described the old one, and | ||
| # the run rewrites it on completion. | ||
| (out / SUMMARY_FILE).unlink(missing_ok=True) |
There was a problem hiding this comment.
resume.load() above atomically rewrites traces.jsonl to the kept rows before this removes the old summary. If the process stops between those operations, a stale summary.json remains beside a changed trace set and still looks like a completion marker. Please invalidate the summary before calling resume.load(), while preserving or republishing a valid summary for the no-owed path.

What the run dir lacked
A finished
evalrun dir heldtraces.jsonl,configs/resolved/eval.jsonandlogs/, and nothing that said how the run went. The dashboard computed a mean reward and an error share for display only, so anyone reading a run afterwards (a script, a pipeline, a person withjq) re-derived pass rate, mean reward and failures fromtraces.jsonlwith their own reading ofEpisode.ok,Trace.rewardandagent.trainable. Those readings drift. The constraint here is one reading of the record, owned by verifiers.What changes
cli/output.pygainssummarize(episodes) -> Summaryandwrite_summary.run_evalwritessummary.jsonbesidetraces.jsonlwhen the run completes and logs onesummary: episodes=… failed=… reward=…line (on the console under--no-rich, inlogs/latest/eval.logalways). A--resumedrops the previous summary before re-running the owed rollouts, sinceresume.loadrewritestraces.jsonlunderneath it, and writes a fresh one on completion. The file's presence therefore means the run completed.reward … · err …readssummarizeinstead of its own formula; the per-signal breakdown rows keepformat_mean.docs/v1/evaluation.mdand theevaluate-environmentsskill name the file.What
summary.jsonholds{ "episodes": 6, "failed": 2, "errors": {"ProviderError": 2}, "reward": 0.5, "tasks": { "echo:hello world": {"name": null, "rollouts": 2, "failed": 0, "reward": 1.0}, "echo:ping": {"name": null, "rollouts": 2, "failed": 2, "reward": null}, "echo:verifiers": {"name": null, "rollouts": 2, "failed": 0, "reward": 0.0} } }episodes: finished episodes, one per rollout (on a resume, the kept ones plus the re-run ones).failed: episodes that ended not ok (Episode.okis false).errors: failed episodes by the type of the error that failed them: a failed trace's last error, else the episode's own last error (a hook's). These are the live errorsepisode_should_retryreads; an ok trace's errors are history its per-agent retry recovered from, and are never the cause. A failed episode that recorded no error is counted nowhere here, so the counts sum to at mostfailed.reward: mean over scored rollouts. A rollout's reward is the meanTrace.rewardover its policy traces (agent.trainable, or every trace when no seat is trainable) that were scored; a trace whose rewards are allNone(or empty) was never scored, and a rollout with no scored trace is unscored and outside every mean.nullwhen nothing was scored.tasks: the same per task, keyed bytask.key(task.hashwhen unset; rows written before either was recorded hash their data, as resume does), withTaskData.namewhen the task has one.utils/platform.run_metrics(the--pushmetadata) keeps its v0-shapedavg_reward/avg_error, where an errored trace counts as 0. That is a platform contract and is untouched here.How it was checked
tests/v1/test_e2e.py::test_summaryrunsecho-v1on the null harness (subprocess runtime, in-process) against a local stub endpoint that answers every request with "hello world" and fails thefailphrase upstream, so it needs no key and no network. It assertssummary.jsonverbatim: 3 episodes, 1 failed,{"ProviderError": 1}, reward 0.5, per task 1.0 / 0.0 / null.test_summary_rulespins the rules that run can't reach over hand-built episodes: a frozen seat's reward is not counted, every trace counts when no seat is trainable, an unscored trace leaves its rollout out of the mean, and a failure is attributed to the failed trace's error rather than to history a recovered sibling kept.uv run eval echo-v1 --env.agent.harness.id null --env.agent.runtime.type subprocess --no-rich -n 3 -r 2, same stub): the run dir gains thesummary.jsonabove, the log ends withsummary: episodes=6 failed=2 reward=0.500, and a--resumeof that dir re-runs the two failed rollouts and rewrites the summary.uv run ruff check --fix .,uv run ruff format .,uv run pytest tests/v1 -n auto -m "not e2e"(85 passed),uv run pre-commit run --all-filesandty check verifiersare green.🤖 Generated with Claude Code
Note
Low Risk
Additive run output and aligned dashboard display; resume behavior only removes a stale summary file before recomputation.
Overview
Finished eval runs now emit
summary.jsonnext totraces.jsonl, giving pipelines and humans a single canonical rollup instead of re-parsing episodes with ad hoc reward/error rules.summarize(episodes)incli/output.pydefines that rollup: episode and failure counts, failures grouped by error type, mean reward over scored rollouts (policy/trainabletraces only, with documented fallbacks for multi-agent and unscored cases), plus the same stats per task key.run_evalwrites the file on completion, logs a one-line summary, and deletes the oldsummary.jsonon--resumebefore rewriting it when the resumed run finishes. The live dashboard reward · err footer now usessummarizeso on-screen numbers match the file.Docs/skills mention
summary.json. Tests add a localstub_modelHTTP stub (no API key/network), an e2e check of the echo run’s summary shape, and unit tests for aggregation edge cases.Reviewed by Cursor Bugbot for commit 2331dcd. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Write
summary.jsonbesidetraces.jsonlon eval completionSummaryandTaskSummaryPydantic models plussummarizeandwrite_summaryhelpers in output.py to aggregate episode counts, failure counts by error type, scored reward means, and per-task resultsrun_evalwritessummary.jsonafter all episodes finish and deletes any stale summary before a resumed run's owed rolloutsSummaryaggregation rules (reward mean excludes unscored rollouts; error ratio uses failed/total episodes)\n- Adds a local HTTP stub-model fixture and end-to-end/aggregation tests in test_e2e.py and conftest.py, plus docs in evaluation.md and SKILL.mdsummary.json; reward means prefer trainable-agent traces and exclude unscored rollouts, which may change displayed dashboard values versus prior direct calculationMacroscope summarized 2331dcd.