Skip to content

report(bench): single results table with per-cell color + baked-in delta#194

Closed
hfurkanbozkurt wants to merge 23 commits into
mainfrom
report/single-table-bench
Closed

report(bench): single results table with per-cell color + baked-in delta#194
hfurkanbozkurt wants to merge 23 commits into
mainfrom
report/single-table-bench

Conversation

@hfurkanbozkurt

@hfurkanbozkurt hfurkanbozkurt commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

Redesign the bench report into one results table, replacing the previous two-table layout (a colors-only Overview + a numbers-only Detailed table, plus a separate Δ vs base column).

Each metric cell now shows the current value on top and the signed delta vs the main baseline underneath (two lines via <br>), colored by the significance + direction of the change:

Task Template Tests Judge Cost Tokens (in/out) Score Stop reason
auth-notes demo 🟡 11/11
(0)
🟡 8.8
(0)
🔴 $12.48
(+$10.11)
🔴 2387.6K/21.6K
(+1968.3K)
🔴 7.6
(-32.6)
endTurn
email-digest demo ⚪ 12/12
(new)
⚪ 9.2
(new)
⚪ $3.27
(new)
⚪ 615.8K/7.6K
(new)
⚪ 29.6
(new)
endTurn

Why

The report should always show the current numbers — the old Overview was glyphs-only and hid values, and no-baseline cells rendered as a bare 🆕 with the value suppressed. Now the current value is always shown, tagged ⚪ (new) when there's no baseline value for that field.

Details

  • Single tablerenderOverview and the separate Δ vs base column are removed; renderDetailed is the one table.
  • Value always shown⚪ <value><br>(new) when the baseline has no value for that field (never hidden).
  • Per-field baseline — drops the whole-row schema-2 gate (baselineHasMetrics/perMetricBaseline): a field the baseline lacks degrades to ⚪ (new) for that cell only instead of forcing the entire row to 🆕. Self-heals per-metric as baselines gain fields.
  • Colors = change, not absolute quality — 🟢 meaningful improvement · 🟡 within the noise band · 🔴 meaningful regression · ⚪ no baseline value. No arrows — color only. Direction per metric: tests/judge/score higher-is-better; cost/tokens lower-is-better.
  • Thresholds centralized in DELTA_THRESHOLDS (one place, commented): composite/score ±5 · judge ±0.3 · tests ±1 pass · cost ±10% · tokens ±10% (in+out combined).
  • 8-column invariant — a crashed/null-metric cell renders a placeholder in every column and can never drop a cell / misalign the row (structurally guaranteed + regression-tested).

Scope

Presentational only. Changes are confined to the render layer — steps/lib/overview.mjs, steps/summary.mjs, and their tests (steps/lib/overview.test.mjs). No scoring / aggregate / baseline-data changes: buildAggregate and diffAgainstBaseline keep the same data shape (current/delta), so analyze.mjs is unaffected.

Validation

  • node --test: full agent-bench suite 162 pass / 0 fail (overview suite 32/32).
  • tsc --noEmit: no new errors (only the pre-existing @strands-agents type mismatches).
  • Rendered a real sample (PR feat(bench): reshape tasks to api-first framework-focus grading #191 run 29283998687 vs baseline run 29283582822) through GitHub's GFM renderer (gh api /markdown) and confirmed the <br> two-line cells render inside the table cells and every row has exactly 8 <td>s.

➕ Now also includes: deep root-cause analysis on failing cells (folded in from #195)

This branch was merged with bench/deep-failure-analysis (PR #195, now closed as superseded) so the two bench improvements ship as one PR. Both features coexist — the merge was conflict-free (disjoint files) and the combined suite is green.

What the folded-in change adds — a deeper analysis pass that fires only on failing cells (passing / clean-partial cells stay on the existing cheap per-cell path, no extra model call). Motivated by auth-notes/demo (0/11) being misdiagnosed as a "scoring/harness mismatch" because the analysis never saw the failing test output.

  • steps/lib/analysis.mjsisFailureCell (trigger predicate), extractFailingTests (walks the nested Playwright JSON, dedups identical errors → grouped counts, caps 8 groups × 300 chars), FAILURE_SYSTEM (strict-JSON prompt), buildFailureUserText (failing tests + dev/build log tails ≤1500 + judge ≤1500 + trace tail), parseFailureAnalysis (tolerant bare/fenced/prose-wrapped JSON → object, malformed → null, never throws), FAILURE_MAX_TOKENS=600.
  • steps/analyze-cell.mjs — after the cheap analysis, if (isFailureCell(result)) reads the staged evidence, calls the same Bedrock converse path (reuses BENCH_JUDGE_MODEL), writes result.failure_analysis. Fully inside the never-throw / always-exit-0 isolation.
  • steps/3-build-and-test.sh — copies pw-results.json/dev.log/build.log to stable /tmp paths (per-step CELL_TMP uses $$; mirrors how /tmp/trace.json is staged). Best-effort, never fails the build step.
  • .github/workflows/agent-bench.yml — passes PW_RESULTS/DEV_LOG/BUILD_LOG env into the "4b. Analyze cell" step.
  • steps/analyze.mjs — renders a ## 🔴 Failure root-cause section (category badge, owner, root cause, quoted-evidence blockquote, likely fix) and folds category/owner counts into the exec summary. Renders alongside the single Results table + preword above.
  • steps/lib/analysis.test.mjs — unit tests for all four pure helpers (dedup, cap, nested suites, build-log gating, prose/fence parsing, null-safety). No live Bedrock.

Combined validation (post-merge): tsc --noEmit clean · node --test 211 tests / 207 pass / 0 fail / 4 skipped (both suites) · bash -n OK · agent-bench.yml YAML parses · no conflict markers.


➕ Now also includes: quality-audit fixes (folded in from #201)

This branch was merged with bench/quality-audit-fixes (PR #201, now closed as folded-in) so the bench quality-audit fixes ship in this PR. The merge preserved both intents — this branch's dead_server→fail classification + the audit fixes above, and the #201 fixes below — and the combined scripts/agent-bench suite is green.

Fixes from the deep-dive audit of the prior bench run (4 cells — C1 sql-kb-catalog, C2 observability-api, C3 oidc-dsql-notes, C4 cognito-profile — plus the auth + kb-chat harness reviews):

Included (8):

  • #1 · console gate — all 10 tasks/*/test.spec.ts: the shared watchErrors() gate now also fails on genuine console.error (scoped to error-type; excludes benign Failed to load resource / net::ERR / favicon / WebSocket / React Warning: noise). Every PROMPT promises "no errors in the browser console"; the gate only checked pageerror before.
  • #2 · AWC-1tasks/async-word-counter/PROMPT.md: fixed the arithmetically-wrong worked example (hello,world foo.bar-baz! is 2 whitespace tokens) → hello,world foo.bar baz (genuinely 3; naive \W+ split = 5), preserving the "3 not 5" contrast. The spec's own punctuation test was verified consistent and left unchanged.
  • #3 · C3-F2tasks/oidc-dsql-notes/test.spec.ts: unauthenticated JSON-RPC probe (no session → no note data; robust to unknown-method, so never spuriously fails).
  • Test #4 · C2-F3tasks/observability-api/test.spec.ts: api.echo round-trips falsy-but-present args (0, false, null, "") — catches truthiness-based "missing arg" footguns.
  • Test2 #5 · C1-F1tasks/sql-kb-catalog/test.spec.ts: FAQ tests now assert the returned kb-result content (/refund/i), not just element presence.
  • ci: publish to npm via changesets #9 · ED-4tasks/email-digest/PROMPT.md: scope-hygiene instruction to remove unneeded scaffold/demo code.
  • fix(test): add comprehensive test app aws-blocks folder #10 · harness scoringsteps/lib/scoring.mjs (+test): renamed agent_harness_teardownwall_clock_timeout (a checkpoint-survived timeout under active isolation can't be an agent pkill-storm — cross-uid EPERM; exclusion behavior unchanged). Coexists with this branch's dead_server→fail classification.
  • fix email batch retry on transient SES failures #11 · harness no-trace analysissteps/lib/analysis.mjs + analyze-cell.mjs (+test): when no agent trace exists (timeout path) and no failing tests, emit a deterministic NO_TRACE_ANALYSIS ("no trace — undetermined", owner/category null) instead of confabulating owner=agent / category=build.

Skipped (3, noted): #6 C3-F1 oidc per-user isolation (stub IdP exposes one fixed subject — needs a harness/stub change); #7 C4-F1 cognito getLastCode (mock-only, not observable outside local/mock); #8 C2-F1 observability instrumentation-fires (no telemetry readback path — not faked). Framework defects (packages/**) were drafted, not filed.

Combined validation (post-merge): npm run test:ci -w scripts/agent-bench (tsc --noEmit + node --test) green — 243 tests / 239 pass / 0 fail / 4 skipped (union of both suites) · all 10 specs tsc --noEmit clean · no conflict markers · dead_server→fail + wall_clock_timeout + NO_TRACE_ANALYSIS + console-gate all verified to coexist.

H. Furkan Bozkurt added 2 commits July 13, 2026 23:19
… Overview/Δ column

Collapse the two-table report (Overview glyphs + Detailed numbers) into ONE
results table. Each metric cell now shows the current value on line 1 and the
signed delta vs the main baseline on line 2 (via <br>), colored by the
significance+direction of the change:
- green/red beyond a per-metric threshold, yellow within the noise band, ⚪ when
  the baseline has no value for that field (value still shown, tagged (new)).
- thresholds centralized in DELTA_THRESHOLDS: composite/score ±5, judge ±0.3,
  tests ±1, cost ±10%, tokens ±10%; direction per metric (tests/judge/score up,
  cost/tokens down). No arrows — color only.

Drops the whole-row schema-2 gate (baselineHasMetrics/perMetricBaseline): the
baseline is diffed PER FIELD, so a field the baseline lacks degrades to ⚪ (new)
for that cell only instead of forcing the entire row to 🆕. Removes renderOverview
and the separate Δ-vs-base column. Presentational only — buildAggregate/
diffAgainstBaseline data shape (current/delta) unchanged, so analyze.mjs is
unaffected.
Harden renderDetailed so a crashed/errored cell (0 tests, no tokens → null
cost/score, no judge) can never drop a metric column and misalign the row:
build the 5 metric cells into an array and coerce any empty string to the NONE
placeholder before joining, guaranteeing the 8-column invariant structurally
even if a future cell helper returns ''. Add a crashed-cell regression test
that asserts every rendered row has exactly 8 columns with no empty cell.
Presentational only.
…judge_dimensions is aggregate-only

Sync the docs to the shipped single-table render:
- README: replace the two-table (Overview colors-only + Detailed baseline->pr)
  description with the one results table (renderDetailed) — each metric cell is
  current value + colored signed delta (two lines via <br>), ⚪ (new) when the
  baseline lacks that field, value always shown. Swap MARGIN_PCT/metricColor/
  renderOverview for DELTA_THRESHOLDS + deltaColor and list the per-metric bands
  and directions. Drop the baseline->pr / multi-line per-dimension Judge-cell
  wording from Re-derivability.
- overview.mjs: one-line comment that cellMetrics.judge_dimensions is carried for
  the persisted S3 aggregate / re-derivability only and is NOT consumed by the
  rendered table (behavior unchanged).
- changeset: refresh the body to describe the single table + baked-in per-cell
  signed delta + DELTA_THRESHOLDS coloring; still non-releasing.

Docs + one comment only — no functional code change.
…C/files cols, inline delta, preword

Render (overview.mjs, summary.mjs):
- Scalar cells (tests/cost/turns/score) now inline: <ball> <value> (<Δ>), dropping the two-line <br> form.
- JUDGE column stacks one <br>-joined line per rubric dimension (functional/selectors/persistence/code/blocks) with per-dim baseline deltas; overall judge folded into the preword.
- TOKENS column stacks in / out / cached in / cached out; cache lines render ⚪ '(new)' until a baseline carries them.
- New columns: Turns (cycle_count, lower-better), LOC (created/edited), Files (created/edited).
- LOC & Files are NEUTRAL — always ⚪, value + signed delta, never green/red (no inherent good/bad direction). Cache tokens are DISPLAYED only, not deducted from cost/SCORE.
- New DELTA_THRESHOLDS bands: turns ±3, cache read/write ±20%. humanTokens now compacts ≥1M to 'M'.
- renderPreword: bulleted run summary (mean composite + Δ vs main, verdict counts, totals, biggest gains/drops, config). Replaces the old headline.
- N-column guard extended to 11 columns; every row (incl. crashed/removed) emits all 11.

Aggregate (overview.mjs buildAggregate/cellMetrics):
- Persist cycle_count, cache_read_tokens, cache_write_tokens, loc_created/edited, files_created/edited so future baselines carry them.

Instrumentation (2-agent-run.ts, partial-envelope.mjs):
- Accumulate usage.cacheReadInputTokens/cacheWriteInputTokens in the ModelStreamUpdate hook; persist cache_read/write_tokens on every envelope path (checkpoint, partial, error, success), success taking max(hook, winnerUsage).
- workspace-diff.mjs: snapshot the scaffolded workspace before the agent runs via a throwaway external GIT_DIR (never touches the workspace's own .git), diff after for LOC/files churn on the success path. Degrades to null on any failure.

Tests: overview.test.mjs updated for the 11-column layout (per-dim judge, 4-line tokens, turns/LOC/files, ⚪ '(new)', 11-column guard, M formatting); workspace-diff.test.mjs added (real git against a real temp workspace + pure parseChurn). tsc clean; 182 pass / 4 skipped (isolation tests need unshare).
H. Furkan Bozkurt added 5 commits July 15, 2026 04:57
…ale logs, never abort under set -e (fixes false build-fail from 2d53028)
…ace analysis confabulation

- scoring: rename AGENT_HARNESS_TEARDOWN_REASON -> AGENT_HARNESS_TIMEOUT_REASON
  (value 'agent_harness_teardown' -> 'wall_clock_timeout'). Under active
  isolation an ungraceful step-2 death cannot be an agent pkill-storm
  (cross-uid EPERM), so it is an infra wall-clock timeout, not a teardown.
  Exclusion behavior unchanged (still harness_error, still excluded).
- analysis: add pure deterministicCellAnalysis + NO_TRACE_ANALYSIS; when a cell
  has no agent trace (timeout path writes none) emit a deterministic
  'undetermined' note instead of letting the model confabulate an owner/cause.
- analyze-cell: route through deterministicCellAnalysis.
- tests: scoring.test + analysis.test cover both changes.
…tic and add email-digest scope-hygiene note
…ability falsy-echo and oidc unauthenticated probe
H. Furkan Bozkurt added 3 commits July 15, 2026 11:36
…h (PR #194)

# Conflicts:
#	scripts/agent-bench/steps/analyze-cell.mjs
#	scripts/agent-bench/steps/lib/analysis.test.mjs
# Conflicts:
#	.changeset/bench-report-redesign.md
…staging (EACCES), judge rubric recognizes real auth APIs / #185 footgun
hfurkanbozkurt and others added 7 commits July 16, 2026 02:57
… compact Judge to one line

The single-table agent-bench comparison report was too verbose. Reduce it:
- Remove the Tokens, LOC, and Files columns (8 cols now: Task|Template|Tests|
  Judge|Cost|Turns|Score|Stop reason). buildAggregate still persists the
  token/cache/LOC/file counts to the S3 baseline for offline analysis — only the
  rendered columns are dropped.
- Compact Judge to ONE line: overall score + per-dimension shorthand
  (F/S/P/C/B) with per-dim signed deltas, instead of one line per dimension.
- Update the glossary (summary.mjs), the README table description, the changeset,
  and the overview.test.mjs snapshot expectations to match.
- Fix a latent $-escaping bug in signedCost surfaced while removing signedTokens.
Fixes 3 harness/analysis bugs that let the bench silently score, exclude, or
misattribute cells for the harness's own failures (cognito-profile MaxTokensError
mislabeled a timeout; kb-chat ungraceful kill given a fabricated "build failed").

- 2-agent-run.ts: persist trace.json on EVERY exit path via a per-turn
  MessageAddedEvent accumulator (captures toolResult blocks, not just toolUse),
  so a non-returning invoke() (MaxTokensError / wall-clock kill) is still
  analyzable. Factored pure buildTraceArtifact() into partial-envelope.mjs.
- 2-agent-run.ts: self-imposed internal deadline (AGENT_DEADLINE_SEC, ~33.5min,
  < the 35min GH step timeout) proactively flushes a graceful partial envelope +
  trace; agent-bench.yml execs tsx so signals reach the process.
- scoring.mjs: MaxTokensError -> klass_reason='max_tokens' (kept agent_fail /
  composite 0 / INCLUDED), no longer mislabeled 'agent_timeout'.
- analysis.mjs + analyze-cell.mjs: deterministicFailureAnalysis() short-circuits
  the model for harness_error / no-trace / failed_at==='2-agent' (no
  confabulation); buildEvidenceObserved() stops reporting seeded pessimistic
  build defaults from a skipped step-3 as observed build failures.
- maxTokens left at the SDK default (64000); tests added for classify + honesty
  guard + trace-artifact.
@changeset-bot

changeset-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ab85029

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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