Skip to content

feat(eval): capture the detail of every failing run, not just the winning one - #1816

Open
Tomkess wants to merge 2 commits into
masterfrom
feat/per-run-failure-capture
Open

Tomkess wants to merge 2 commits into
masterfrom
feat/per-run-failure-capture

Conversation

@Tomkess

@Tomkess Tomkess commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

The gap

best_detail describes whichever run ranked highest. On a partial pass that means every visible verdict belongs to the attempt that worked, and the runs that failed leave no trace — their detail is computed inside the run loop and then dropped.

So a 1-of-3 item is undiagnosable after the fact. The only recourse is re-running the question and hoping it fails the same way, which for a nondeterministic agent is not a given.

This isn't an edge case. On one evaluation day in our corpus, half of all lost runs sat on items whose recorded detail was entirely green — every criterion passing, the item still failing 2 of 3 times, and nothing anywhere explaining why.

The change

One new field on ItemReport, emitted beside detail in the JSON report:

"failed_runs": [
  { "run_index": 2,
    "passed": false,
    "error": null,
    "detail": { ... },              // that run's own verdict
    "conversation_id": "",         // that run's own ids
    "response_id": "",
    "stream_ended": true,
    "turn_wall_clock_sec": 41.2,
    "latency_s": 41.2,
    "reasoning_step_count": 7,
    "reasoning_steps": [""] }
]

Kind-agnostic. detail is opaque to the runner — it never inspects its shape — so this covers all test kinds and any added later, with no per-evaluator work.

Failing runs only. A fully-passing item records nothing, so the cost tracks how broken the corpus is rather than how large it is, and shrinks as quality improves.

Nothing existing changes. detail and the top-level ids keep their exact current meaning, so consumers of this report are unaffected.

It also fixes a latent mismatch

report.conversation_id = getattr(chat_result, "conversation_id", None) or report.conversation_id   # every run
...
best = evaluation          # only when this run ranks highest
best_chat_result = chat_result

The top-level conversation_id/response_id are overwritten on every iteration and end up describing the last run, while best_detail and reasoning_steps describe the best one. When those differ, the ids point at a different conversation than the detail beside them.

best_chat_result already exists precisely to keep reasoning_steps aligned with best_detail (see the comment at its declaration) — the ids were never given the same treatment. Per-run ids make the pairing correct by construction rather than adding a fourth field to keep in sync.

Why stream_ended is in there

A stalled turn leaves the evaluator's gated checks False even though none of them ran, which reads as a content failure in every downstream rate. Recording it at the source removes the need for consumers to infer stalls from the shape of the detail block.

Tests

Eight new tests. uv run pytest976 passed, 0 failed.

  • a partial pass keeps the failing run's detail while best_detail stays the winner
  • a fully-passing item records nothing
  • every failing run appears, in run order
  • a failed run carries its own conversation/response ids while the top-level pair still describes the last run
  • stream_ended and reasoning_step_count are recorded
  • an ungraded run is captured with its judge error — the only thing explaining pass_power_k: false on an item whose graded runs all passed
  • the JSON report emits failed_runs beside detail, and [] for a clean item

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Evaluation reports now include diagnostic details for each failed or ungraded run, including run metadata, timing, stream status, and evaluator information.
    • Winning-run details remain available alongside failed-run diagnostics.
    • Passing items show an empty failed-runs list.
  • Tests

    • Added coverage for failure tracking, metadata retention, run ordering, and partially passing evaluations.

…ning one

`best_detail` describes whichever run ranked highest, so a partial pass exposes
only the attempt that worked. On a 1-of-3 item every visible verdict belongs to
the run that passed, and the two that failed leave no trace at all -- their
`detail` is computed inside the run loop and then dropped on the floor.

That makes a partial pass undiagnosable after the fact. The only recourse is to
re-run the question and hope it fails the same way, which for a nondeterministic
agent is not a given. In a real corpus this is not an edge case: on one eval day
half of all lost runs sat on items whose recorded detail was entirely green.

`failed_runs` records one entry per run that did not pass, in run order:

    run_index, passed, error, detail,
    conversation_id, response_id,
    stream_ended, turn_wall_clock_sec, latency_s,
    reasoning_step_count, reasoning_steps

Three properties worth calling out.

**Kind-agnostic.** `detail` is opaque to the runner -- it never inspects its
shape -- so this covers every test kind and any kind added later, with no
per-evaluator work.

**Failing runs only.** A fully-passing item records nothing, so the cost tracks
how broken the corpus is rather than how large it is, and shrinks as quality
improves. `detail` and the top-level conversation/response ids keep their exact
current meaning, so existing consumers of the report are unaffected.

**Each entry carries its own conversation ids.** This also fixes a latent
mismatch: the report's top-level `conversation_id`/`response_id` are overwritten
on every iteration and end up describing the LAST run, while `best_detail` and
`reasoning_steps` describe the BEST one. When those differ, the ids point at a
different conversation than the detail beside them. `best_chat_result` already
exists to keep reasoning_steps aligned with best_detail; the ids were never
given the same treatment. Per-run ids make the pairing correct by construction
instead of adding a fourth field to keep in sync.

`stream_ended` is included because a stalled turn leaves the evaluator's gated
checks False even though none of them ran, which reads as a content failure in
every downstream rate. Recording it at the source removes the need for consumers
to infer stalls from the shape of the detail block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Warning

Review limit reached

  • Run on-demand review

This review includes 1 billable file and costs up to $0.25.

Or wait 45 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 81d3a116-d0eb-4403-b490-a16471936a9c

📥 Commits

Reviewing files that changed from the base of the PR and between 4378519 and 4d0e0ac.

📒 Files selected for processing (1)
  • packages/gooddata-eval/src/gooddata_eval/core/runner.py

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: a30afa79-9baa-4fec-ab88-7753f63fe9ca

📥 Commits

Reviewing files that changed from the base of the PR and between b145e1a and 4378519.

📒 Files selected for processing (4)
  • packages/gooddata-eval/src/gooddata_eval/core/reporting/json_report.py
  • packages/gooddata-eval/src/gooddata_eval/core/runner.py
  • packages/gooddata-eval/tests/test_reporting.py
  • packages/gooddata-eval/tests/test_runner.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The runner now records diagnostics for each failed or ungraded run. JSON reports expose these records while preserving the winning run’s existing detail and identifiers.

Changes

Failed Run Reporting

Layer / File(s) Summary
Capture failed-run diagnostics
packages/gooddata-eval/src/gooddata_eval/core/runner.py, packages/gooddata-eval/tests/test_runner.py
ItemReport stores failed-run records. The runner captures evaluator errors, run metadata, timing, stream status, and reasoning steps. Tests cover ordering, metadata retention, and ungraded runs.
Serialize failed runs in JSON
packages/gooddata-eval/src/gooddata_eval/core/reporting/json_report.py, packages/gooddata-eval/tests/test_reporting.py
JSON output includes failed_runs and preserves the winning run’s detail and identifiers. Tests cover failed and empty lists.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant _run_one_item
  participant _failed_run_record
  participant ItemReport
  participant _build_run_dict
  participant JSONReport
  _run_one_item->>_failed_run_record: Build diagnostic for a non-passing run
  _failed_run_record->>ItemReport: Append failed-run record
  ItemReport->>_build_run_dict: Provide winning detail and failed_runs
  _build_run_dict->>JSONReport: Emit both winning and failed-run data
Loading

Merge Risk: ⚪ Minimal · up to 43785

The failed-run diagnostics are emitted alongside existing report fields without a demonstrated regression, so the change is ready to merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: capturing details for every failing evaluation run instead of only the winning run.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

A rabbit logs each failed hop
With IDs and timing, neat and crisp
The winning run keeps its crown
While lost runs leave their clues
JSON carries every trace
And clean runs show an empty list

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.50%. Comparing base (b145e1a) to head (4d0e0ac).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1816   +/-   ##
=======================================
  Coverage   82.49%   82.50%           
=======================================
  Files         283      283           
  Lines       20448    20452    +4     
=======================================
+ Hits        16869    16873    +4     
  Misses       3579     3579           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…y ruff format

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant