feat(viewer): reviewer-grade trajectory pages, multi-run browsing, and hf:// dataset sources - #1034
Conversation
|
Really enjoyed this one! I tested the main paths pretty hard and a lot of it held up well: traversal attempts all 404'd, the single-run path stayed compatible with main, the confirm flow worked end to end, and the A few things are worth fixing before merge. One test-coverage thing I’d tighten too: both security mitigations work today, but their regression tests don’t actually pin the enforcement. Removing the Happy to re-test after any updates 🙂 |
All five findings from the review pass: - `bench eval view --help` no longer exits 1: the help text carried "[/subpath]", which Rich parsed as a closing markup tag; rephrased without brackets. - `ruff format --check src tests tools` passes (the new viewer test file was unformatted). - The browse-mode run cap is no longer silent: the sidebar heading says "first N runs (capped)" when truncation happened (detected by scanning cap+1, not by len==cap), a `?run=` pointing at an undiscovered id shows an explicit load error instead of silently rendering the first run, and BENCHFLOW_VIEWER_MAX_RUNS overrides the 500 default. - The two security regression tests now pin enforcement (mutation-killing): the script-breakout test asserts the raw `</script><script>` sequence appears nowhere in the emitted page (removing the escape fails it), and a new whitelist test resolves ids through the extracted `_resolve_browse_rollout` helper against a real rollout placed outside the served base (removing the membership check fails it). - Diagnostic banner keys derive from `DIAGNOSTIC_REGISTRY` instead of a hand-copied list (static fallback kept for lenient imports), so new diagnostics like benchflow-ai#1025's chat-only flag appear without drift — and diagnostics on an otherwise-clean rollout (no error/verifier_error) render as neutral info banners rather than red error banners. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Thanks for the thorough pass — reproduced every finding, all five addressed in d26931b:
Would appreciate a re-test whenever you have time 🙏 |
|
@ljr145733 — thanks for the substantial first contribution. I reviewed commit Review — changes requestedThe viewer direction is valuable, but I don’t think this implementation is maintainable or yet matches the intended product design. Please address the following before merge. 1. Decompose the viewer architecture
One module now owns:
Please separate responsibilities, for example: Preserve Also replace the loosely coupled 2. Match SkillsBench’s actual visual systemThe current CSS approximates SkillsBench but does not implement it. Declaring Please use the live SkillsBench site as the visual source of truth:
Extract these into one shared theme rather than maintaining separate approximations in 3. Adopt PostTrainBench’s information architecture, not its visual stylingWith the PR’s example dataset, the viewer renders 225 runs as one flat 280px sidebar. There is no run-level filtering, grouping, sorting or pagination. This will not scale. Use the PostTrainBench trace browser and its index interaction logic as the layout/behavior reference: Browse screen
Run-detail screen
The PostTrainBench run-detail logic is a useful behavioral reference. Please copy the information architecture, not its cream/monospace visual identity—the BenchFlow viewer should use SkillsBench styling. 4. Tighten the
|
943aca2 to
a490d48
Compare
|
All six review points should be addressed now:
After that I did another adversarial pass over the catalog code and fixed what it turned up in 7969512. The two worst: a failed Take another look when you have time — happy to adjust anything. |
ir_to_view.py converts a trace into the step list a viewer page renders. It produces steps and deliberately not a whole payload: of a payload's five fields, only the steps are a function of the trace. rollout_name is a directory name, verifier is four sidecar files, and meta comes from result.json and timing.json - the IR has no slot for task_name, skill_mode, reward, partial_trajectory or trajectory_source at all. Returning a payload here would mean synthesizing run metadata to fill a shape rather than converting a trace, so the trace-level fields the IR does carry are declared UNSUPPORTED: outside this edge's codomain, not lost by it. Assembly belongs to the wiring slice, which has the directory. The wire shape is read from the viewer package proposed in benchflow-ai#1034, at the commit recorded in VIEW_SCHEMA_ORIGIN. Nothing is imported, vendored or fetched from that branch - it is unmerged, and the family rule is the one ir_to_atif already follows for ATIF: read the target format as data, pin what matters by test, never reach into the module that handles it. That pin protects this edge from drifting; it cannot notice benchflow-ai#1034 changing. tool.name_semantics is a seventh key on an object benchflow-ai#1034 defines with six, and it is here because the alternative is inference. benchflow-ai#1034 derives a tool's display category with tool_hue(kind, title), which scans both strings for needles: an ATIF function_name of 'execute' becomes the execute category, and an OTel gen_ai.tool.name of 'read_file' becomes 'read' because the word appears in it. This edge emits a hue only when the semantics say acp_kind and the value is already a member of the display vocabulary - membership, never inference - and otherwise the neutral 'other', which the renderer maps to the secondary tokens and which therefore asserts nothing. On the corpora that means 'execute' is spelled identically in the ACP and ATIF rows and gets a category in one only. Unlike the ACP edge this one never raises. A viewer is a display: an event it cannot type must still reach the page. Every event becomes exactly one step, UNKNOWN and ORACLE included, and what the shape cannot hold goes to the loss report. ORACLE has no member in the step vocabulary, so it lands on 'unknown' with its identity in the type slot, and both untyped kinds carry a serialization of the canonical IR event - not a raw source record, since the IR does not hold one, and the loss record says so. Sentinels are declared per slot and only where the target has no null to write. An absent title and an observed empty one both render as "", so the report is the only place that difference survives; a block the IR holds with no text contributes no string rather than having its raw form rendered as JSON. Still unwired. ir_to_view joins the IR family in the isolation test, nothing on a run path imports it, no page is rendered and no artifact changes. ir.py, ir_round_trip.py and the existing viewer.py are untouched. tests/trajectories: 752 passed (699 + 53). Mutation harness: 13/13 applied and caught. Human verification V1-V10 recorded in docs/trace-interop.md section 8.14, along with what it does not cover.
`ir_to_view_html` closes the chain `ir_to_view` opened: a viewer step list becomes one HTML page, built from the card builders `viewer.py` already renders its ACP page with. The dependency runs one way — this module imports the viewer, the viewer imports no part of the IR family — so what crosses the boundary is a plain step document and the renderer stays a renderer. It is written against the viewer on main. benchflow-ai#1034 stays a design reference for the step vocabulary and nothing else: no code from that branch is imported, vendored or fetched, and this edge works whether or not it lands. It does not rebuild steps into ACP capture events. The IR holds records ACP has no type for, a status it cannot spell, and tool names whose semantics are the point; forging capture events would launder all three back into the assumption the hub exists to remove. Measured against `_render_acp_events` on the two captured rollouts: H2's four events render three cards there and the word "timeout" appears nowhere, an unrecognized record reaches no card, and a tool card carries kind/title/status but not the tool's output. Those are that renderer's four branches, not bugs; this edge has six step kinds to place, places them, and declares the difference. `test_viewer_primitives.py` pins the legacy behaviour as a fact so the day a branch is added, this edge is revisited with it. Classification is a table over the eight display hues, never a substring: `viewer._tool_accent_class` is not imported and an AST test asserts the name never appears here. The same `execute` gets `acc-bash` as an ACP kind and stays neutral as an ATIF `function_name` in the same run; `read_file` from OTel stays neutral too. `think` resolves to the neutral accent because the stylesheet has none for it, and that one is declared DROPPED rather than hidden. `name_semantics` rides in the metrics line and in `data-name-semantics`; a step with no typed slot is labelled `Canonical IR representation` so the page never passes a reconstructed event off as a source record; cuts carry an explicit marker and a NORMALIZED record. `render_trace` returns both reports without merging them, and this edge never addresses the hub — it has the IR on neither side. Unwired: nothing under src/benchflow calls it. `python -m benchflow.trajectories.ir_to_view_html <path> [out.html]` is how a person looks at a page. Docs in §8.15, family list and status in §8.7.
V1-V12 and V4b, all PASS, 2026-08-19, in a browser against the four corpora. The table says what a person confirmed, and the paragraph after it says what the sign-off does not extend to: four corpora on one machine in one browser, the OTLP payload's content still a construction, oracle events and non-text content blocks still test-only. The verification found the reasoning loss fixed in the previous commit, which is the third time in this work that a green suite was not evidence a document was right. Recorded as such. Also states, in one place, what this slice depends on and what it does not: benchflow-ai#984 does not depend on benchflow-ai#1034 (design reference for the step vocabulary, no code imported, every commit targets viewer.py as it stands on upstream/main); the wiring is opt-in and the switch off is byte-identical; IR -> OTel stays deferred on the terms of section 8.12; and the ATIF document's two identical user steps are the production exporter's, documented in section 5.2, shown faithfully rather than repaired here. A BrokenPipeError seen from the stdlib server during V10 is recorded as an incidental observation about serve(), not as a finding about this edge.
….7.4 Port of the viewer-v2 prototype (interactive payload/template renderer, multi-run browse mode, semantic color system) onto the 0.7.4 viewer stack (benchflow-ai#1019 restyle, benchflow-ai#1020 accents, benchflow-ai#1021 --confirm, benchflow-ai#1022 redaction summary, benchflow-ai#1023 badge rules, JSONL session files). Merge shape: - serve() becomes a dispatcher: single trajectories (rollout dir or session JSONL file) go through _serve_single — byte-identical to upstream serve()'s body, preserving the confirm/DECISION contract, sidecar ordering, and exit semantics — while a directory of rollouts serves browse mode (run sidebar, /api/rollouts, /api/rollout?id=… with exact-membership whitelist ids, ?run= deep links). --confirm on a multi-run directory errors out: a confirmation needs exactly one trajectory. - ACP rollout directories render through viewer_template.html (full-fidelity collapsible stream, harness/model/skills identity row, error-diagnostic banners, Verifier/Metrics tabs, focus/filters/search/anchors, chunked rendering, per-kind hues); raw session files keep _render_acp_events, whose server-rendered output the jsonl-session tests pin. - Upstream renderers and helpers are byte-equivalent except unicode hardening (errors="replace" on trajectory/sidecar reads, extended to render_jsonl_file and the stream-json paths) and _load_result_json refactored onto the shared lenient loader. - Template gains confirm-bar compatibility: CSS variable aliases for the injected bar plus a theme-aware override for its hardcoded light-palette hover; kind-hue inference falls back to kind+title needles, mirroring _tool_accent_class. Verified: 5685-test suite green, ruff format/check + ty clean, confirm flow E2E on a template page (bar + redaction note injected, POST /decision, single DECISION line, sidecar stays clean), browse API + traversal guard live-tested. Two-agent parity review confirmed no upstream behavior and no v2 feature lost. Supersedes the pre-rebase commits cf3e5a4c + 325e4ec5 (kept on branch viewer-v2-prototype for reference). Co-Authored-By: Claude Code <noreply@anthropic.com>
Adopt the benchflow.ai visual system from the shared _VIEWER_CSS (benchflow-ai#1019/benchflow-ai#1020) in viewer_template.html: light monochrome base (near-white page, white cards, hairline borders, subtle shadows), Satoshi/Google Sans Code stacks, the BenchFlow wordmark header, pill labels (black PROMPT pill, mono uppercase labels), and the exact muted pastel accent palette for tool kinds (execute=amber, edit=blue, read=teal, think/skill=purple, fetch/search=cyan). The dark #141414 code treatment is now reserved for terminal (execute) output, matching the site renderer's .term rule. Pass/fail/warn semantics extend the same pastel grammar (pale bg + darker ink + soft strip) since the site palette defines no failure colors. Template tokens now use _VIEWER_CSS variable names directly, so the injected --confirm bar styles itself with zero adaptation — the alias bridge and the dark-theme hover override are gone along with the dark theme (single light theme, like every other viewer page). Co-Authored-By: Claude Code <noreply@anthropic.com>
…anchors Two refinements from design review of the aligned template: - Tool cards get a full pale frame in their kind hue (border-color: --k-line) on top of the saturated left strip — the same two-layer grammar the failed/timeout cards already used, removing the one card type that still wore a neutral frame. - The all-pastel palette read washed-out at reviewer information density, so verdict moments now carry ink-strength color while everything else stays muted: sidebar status edges 2px→4px in ok/bad ink, PASS/FAIL header badges become solid pills (white on ink — the black PROMPT pill's green/red siblings), failed/cancelled status chips go solid, and error-banner strips deepen to bad ink. Three deliberate intensity tiers: verdicts (solid, rare) → kind annotations (pastel, frequent) → monochrome base. Co-Authored-By: Claude Code <noreply@anthropic.com>
`bench eval view hf://<org>/<dataset>[@revision][/subpath]` fetches the viewer-relevant slice of a HF trajectory dataset — trajectories plus result/timing/prompts/verifier sidecars, skipping the large llm_trajectory/trainer files — into the shared huggingface_hub cache (repeat views are incremental) and serves it through the existing browse mode. This is the local rehearsal of the hosted-viewer story: the ground truth uploads (e.g. benchflow/skillsbench-trajectories-apr2026) become browsable with one command and no manual downloads. - spec parsing tolerates the hf:/ collapse from typer's Path conversion and supports @revision pinning - _discover_rollouts default depth 3→4: dataset roots nest one level deeper than local job dirs (jobs/<run>/<timestamp>/<rollout>) - lazy huggingface_hub import with a clear error when the extra is absent Validated live against benchflow/skillsbench-trajectories-apr2026 (jobs/opus47-with-skills-t1): 225 runs discovered and served, payloads render end to end. Co-Authored-By: Claude Code <noreply@anthropic.com>
Render event timestamps whenever the capture provides them, invisibly degrading when it does not — which is every capture today: a scan of 140 non-empty trajectories (5,698 events) across the HF ground-truth uploads and recent local experiment batches found zero timestamp fields. - _normalize_steps passes through the proposed capture fields (benchflow#1033): tool_call started_at/finished_at → step t + dur, ts on other events → step t; lenient parsing (ISO-8601, the space-separated str(datetime) form, numeric epochs), unparseable values ignored; steps carry no t/dur keys when the capture has none. - Template shows a "+m:ss" offset chip per timestamped step (origin = first timestamped step, so mixed clock sources cannot skew the base) and a duration chip on tool calls. The day the capture change lands, existing viewers gain the timeline with no further work — and reviewers can finally answer "where did 85 of the 90 minutes go" per event. Co-Authored-By: Claude Code <noreply@anthropic.com>
CHANGELOG entries plus the bench eval view reference section for the new ACP review page, multi-run browsing, hf:// dataset sources, and the forward-compatible timeline. Co-Authored-By: Claude Code <noreply@anthropic.com>
All five findings from the review pass: - `bench eval view --help` no longer exits 1: the help text carried "[/subpath]", which Rich parsed as a closing markup tag; rephrased without brackets. - `ruff format --check src tests tools` passes (the new viewer test file was unformatted). - The browse-mode run cap is no longer silent: the sidebar heading says "first N runs (capped)" when truncation happened (detected by scanning cap+1, not by len==cap), a `?run=` pointing at an undiscovered id shows an explicit load error instead of silently rendering the first run, and BENCHFLOW_VIEWER_MAX_RUNS overrides the 500 default. - The two security regression tests now pin enforcement (mutation-killing): the script-breakout test asserts the raw `</script><script>` sequence appears nowhere in the emitted page (removing the escape fails it), and a new whitelist test resolves ids through the extracted `_resolve_browse_rollout` helper against a real rollout placed outside the served base (removing the membership check fails it). - Diagnostic banner keys derive from `DIAGNOSTIC_REGISTRY` instead of a hand-copied list (static fallback kept for lenient imports), so new diagnostics like benchflow-ai#1025's chat-only flag appear without drift — and diagnostics on an otherwise-clean rollout (no error/verifier_error) render as neutral info banners rather than red error banners. Co-Authored-By: Claude Code <noreply@anthropic.com>
Independent re-review of the review-feedback commit surfaced four leftover gaps around the run cap plus one weak assertion; all closed: - the browse startup line no longer under-reports silently — it prints "first N runs (capped — raise BENCHFLOW_VIEWER_MAX_RUNS)" when truncated (same cap+1 detection as the sidebar), and the --confirm-on-directory error says "N+ runs" - /api/rollouts signals truncation with an X-BenchFlow-Capped: 1 response header (body stays a bare list for backward compatibility) - BENCHFLOW_VIEWER_MAX_RUNS and the truncation signals are documented in the bench eval view reference - has_error includes export_error, so diagnostics riding along the third error channel render as error banners, not neutral flags - the cap test pins the exact discovered-id prefix (the ids[:cap] slice and the whitelist membership set both rely on prefix determinism), not just counts; _discover_rollouts documents that directory symlinks under the served base are followed Co-Authored-By: Claude Code <noreply@anthropic.com>
…ange)
First step of the in-PR construction plan from review: split the
1,500-line viewer.py + 900-line inline template into a package with one
responsibility per module, exactly as suggested:
trajectories/viewer/
payload.py ACP normalization, diagnostics, sidecar loading
render.py payload + packaged assets -> one self-contained page
legacy.py stream-json / Codex / raw-ACP inline renderers (verbatim)
catalog.py run discovery, cap, summaries
sources.py hf:// spec parsing and resolution
server.py single-run serving (confirm contract) + browse mode
assets/ theme.css / viewer.css / viewer.js / template.html
__main__.py python -m entrypoint
The split is mechanical (functions moved verbatim by line range); the only
rewritten body is _load_template(), which now assembles the page from the
split assets. benchflow.trajectories.viewer keeps its full import surface
via __init__ re-exports — all 394 viewer-battery tests pass unmodified,
and the wheel ships the assets subtree.
Next steps per the plan: typed payload contract (models.py), the real
SkillsBench theme, then the catalog information architecture.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Second construction step: the Python → JavaScript boundary is now an explicit typed contract (viewer/models.py) instead of dict[str, Any]: - Step / ToolCall / TimeoutInfo / ErrorBanner / Meta / VerifierArtifacts / ViewerPayload / RunSummary dataclasses; to_payload() is the single serialization site and preserves the exact wire shape, including which optional keys are omitted (not nulled) when absent - tool-kind display classification moves fully server-side: models.tool_hue is the one classification site, the payload ships each tool's hue, and the template's duplicated KINDS/INFER tables are gone (the JS keeps only a class-attribute whitelist against crafted payloads) - no more silent truncation (review point 6): unknown-event payloads and diagnostic banners ship complete; the template collapses long content (banners now use the same expandable body as everything else) Co-Authored-By: Claude Code <noreply@anthropic.com>
…theme Third construction step — the theme now IS the SkillsBench system rather than an approximation of it: - assets/theme.css carries the canonical tokens from www.skillsbench.ai verbatim: the hex fallback :root plus the lab() override :root (the site's own progressive enhancement), the semantic palette (success/warning/info/destructive + foregrounds), the five chart colors, --radius: .625rem, and the site's exact font stacks - Google Sans Code (OFL 1.1, license file alongside) is inlined as @font-face data URIs (400/600 latin, ~24 KB) so exported pages stay self-contained and offline; Satoshi is declared in the stack but not redistributed — Fontshare's EULA does not permit shipping its files in this repo, so it falls back to system sans unless the host has it (flagged for maintainers in the PR) - one theme, no drifting copies: the inline legacy renderers' _VIEWER_CSS now prepends the same theme.css instead of defining its own :root, and both renderers color tool kinds from the same --kind-* component tokens; the verdict palette derives from the canonical semantic colors via color-mix, and metrics phase bars use the canonical chart palette - the interactive template migrates to canonical vocabulary (--foreground/--muted-foreground/...), with the small set of viewer-specific aliases documented in the theme Co-Authored-By: Claude Code <noreply@anthropic.com>
Fourth construction step (review point 4): - The hf:// download allowlist is now exact paths only. verifier/* pulled 418 unconsumed files (~31 MB of OBJ/MP4/PDF/NPZ run artifacts) on the example dataset; the verifier portion now derives from payload.VERIFIER_SIDECARS — the same four files _load_verifier reads — so the fetch surface cannot drift wider than what the viewer consumes, and a regression test rejects any widening (no wildcard entries, and the verifier set must equal the sidecar list). - Sources are typed: parse_source() turns the CLI argument into LocalPathSource | HfDatasetSource, validating repo components and dataset subpaths (no "..", no backslashes, no empty revision). The CLI argument is a str end to end — never pathlib.Path, whose normalization mangles hf:// into hf:/ — and the mangled spelling now gets a pointed error instead of silent acceptance. - Verified against a fresh HF cache: resolving a dataset rollout fetches only allowlisted files, zero junk. Co-Authored-By: Claude Code <noreply@anthropic.com>
…hitecture Fifth construction step (review point 3, scoped with the maintainer: the PostTrainBench index interaction model, minus the overview matrix for now). The browse-mode sidebar becomes a full-width run catalog: - corpus line (total + pass/fail/unscored, cap note when truncated) - grouping by task or by model + harness (or none), with per-group aggregates — run count, pass/fail/unscored, pass rate — and collapsible group cards; small group sets open by default, open-state lives in the URL - sorting (name/reward/duration/cost), text filtering, and incremental "show more" pagination inside large groups (100 rows first, +200 per click) - run rows carry the row-level stats reviewers scan for: verdict, duration, total tokens, cost (RunSummary gains duration_sec/cost_usd/ total_tokens/n_tool_calls) - selecting a run opens the detail page (scrolled to top, "← runs" back control); back — button or browser — restores the exact catalog view: group mode, sort, filter text, open groups, and scroll position all round-trip through the URL - ?run= deep links still work and unknown ids still get the explicit error instead of a silent fallback The overview/performance matrix and corpus statistics table from the reference design are intentionally deferred. Co-Authored-By: Claude Code <noreply@anthropic.com>
Final construction step (review point 5): the ~850 lines of template JavaScript now have behavior tests that run them in real headless Chromium via playwright (dev extra; the suite self-skips when playwright or its chromium binary is absent, so plain `pytest tests/` runs anywhere, and carries a `browser` marker for explicit deselection). Coverage, per the review's list: catalog grouping / filtering / sorting with URL-state restoration across reloads; group collapsing (regression: with ≤2 groups the default-open state used to short-circuit the toggle — caught by a user click, now pinned); run selection and back navigation preserving the exact catalog view, including browser back re-entering the detail; Trace/Verifier/Metrics tabs; Focus/Full switching; in-trace search and #eN event anchors plus timeline chips; the unknown-run error state; hostile trajectory content (<img onerror>, </script><script>) staying literal text — no elements created, no dialogs fired; and representative desktop + narrow-viewport screenshots. The playwright lifecycle is module-scoped on purpose: its sync API keeps an asyncio loop running on the calling thread, and a session-scoped instance leaks that loop into other modules' pytest-asyncio tests. Co-Authored-By: Claude Code <noreply@anthropic.com>
Findings from an adversarial review of the catalog + browser-test commits, each pinned by a browser-level regression test: - selectRun failure now clears the previous run's header/tabs/panes, resets pane visibility, and syncs ?run= into the URL before the fetch — a failed load used to silently show the prior run's data with the error hidden inside a collapsed trace pane. - Group keys are component-encoded in the toggled URL param: task names containing commas no longer split into phantom keys on reload. - defaultOpen derives from the whole corpus, not the filtered subset, so narrowing the filter to ≤2 groups no longer inverts explicit toggles; active filters force matches visible instead. - ?group=/?sort= are validated with own-property checks — prototype keys like "constructor" fall back to defaults instead of crashing. - Filter keystrokes re-render only the stats and run list; the input is never rebuilt, preserving caret position and IME composition. - popstate skips refetching when only the hash changed (in-trace #eN anchor back-navigation no longer re-renders the whole run). - fmtDuration rounds once before splitting — no more "1m 60s". - Both viewer HTTP handlers override do_HEAD: the inherited handler served filesystem paths from the process cwd, bypassing the rollout-id whitelist. - CI installs Playwright Chromium so the browser suite actually runs instead of silently self-skipping in every lane. Test corpus grows a third task group with a comma in its name and a 119.6s duration, covering the default-collapsed branch, the flat-list mode, and pagination past PAGE_SIZE via a 105-run corpus. Co-Authored-By: Claude Code <noreply@anthropic.com>
7969512 to
a101265
Compare
|
Maintainer merge pass complete on b88d6f4: rebased onto current main; hardened the payload, source, server, confirmation, and browser boundaries; split the frontend state machine; and added mutation, security, accessibility, and real-Chromium regressions. Local focused suite: 126 passed; browser suite: 30 passed. Native GitHub test, pip-audit, integration-scope, and integration-light all pass. manifest-parity is the pre-existing external benchflow-ai/agents drift (codex-acp 0.0.45 there versus 1.6.0 on main) and has also failed the last three main runs. Version remains 0.7.6.dev0; no release requested. |
Summary
Reviewers auditing trajectories (health checks, failure attribution, reward
hacking, no-skill leakage — the weekly-sync "[8pt - rollout] viewer" ticket)
need more than the current ACP page gives them: today it truncates every
message at 500 chars, shows tool calls without their output, and has no
stats, filters, or verifier view. This PR upgrades
bench eval view's ACPpath into an interactive review page, adds a multi-run browser, and makes
HuggingFace trajectory datasets directly viewable — while keeping every
existing behavior (
--confirmcontract, session JSONL files, legacyturn*.txtrenderer) byte-for-byte intact.Built on and styled after the 0.7.x viewer stack (#1019 site restyle, #1020
accents, #1021
--confirm, #1022 redaction summary, #1023 badge rules):same light monochrome language, same
_VIEWER_CSStoken names (the confirmbar injects with zero adaptation), same pastel accent palette.
Usage
1. Local folders. A single rollout renders the interactive page; a
directory of rollouts (a job dir, a whole
jobs/tree) serves a browserwith a run sidebar — task, pass/fail, harness, model, skill mode — traces
loaded dynamically without restarting, and
?run=<id>deep links:2. HuggingFace datasets. The community ground-truth uploads become
reviewable with one command. Only the viewer-relevant files are fetched
(trajectories + result/timing/prompts/verifier sidecars — never the large
llm_trajectory/trainerexports) into the sharedhuggingface_hubcache, so repeat views are incremental:
bench eval view hf://benchflow/skillsbench-trajectories-apr2026/jobs/opus47-with-skills-t1Validated live: 225 runs from that dataset (and 25 local FrontierPhysics
rollouts, including old-schema/partial/timeout cases) browse and render end
to end.
What the page gives reviewers
collapse with an expand control; ACP diff content blocks render as
readable pseudo-diffs;
agent_timeoutevents render as cards (previouslysilently dropped); unknown/future event types degrade to generic cards.
error_category,verifier_error, and the seven*_infodiagnostic blocks fromresult.json as banners, plus harness/model/skills identity tiles and
token/cost/duration stats.
tool kind, dark terminal treatment reserved for
executeoutput),failed-only, text search with jump, per-event
#e42anchors for citingsteps in review threads; chunked rendering stays responsive on large
traces.
phase-timing bars, token breakdown.
+m:ssoffsets and tooldurations whenever events carry
ts/started_at/finished_at(Subscription-auth runs (agent_native_acp) have no event-level timing — serialize timestamps in acp_trajectory.jsonl #1033);a scan of 140 non-empty uploaded trajectories (5,698 events) found zero
timestamp fields today, so nothing changes visually until capture writes
them.
Compatibility
serve()becomes a thin dispatcher; single trajectories go through_serve_single, which is byte-identical to the previousserve()body —confirm/
DECISION:contract, sidecar write-before-inject ordering, exitsemantics, JSONL session files all preserved.
--confirmon a multi-rundirectory errors out (a confirmation needs exactly one trajectory).
_render_acp_eventsrenderer whoseoutput the jsonl-session tests pin; the legacy
turn*.txtrenderer isuntouched apart from
errors="replace"unicode hardening on reads.Security
Trajectory content is untrusted input end to end: it travels as JSON data
(
</escaped against script-tag breakout) and the template rendersexclusively via
textContent; browse-mode ids resolve only by exactmembership in a fresh directory scan, so crafted ids (
../traversal)cannot reach the filesystem. An adversarial review pass produced five
verified findings (lone-surrogate encode crash, renderer-halting type
confusion, non-UTF-8 sidecar crash, null-timing crash, diff-block
rendering) — all fixed with regression tests.
Testing
tests/trajectories/test_viewer_acp_renderer.py(new, 19 tests): payloadcontract, escaping/hostile-input cases, degradation, browse discovery +
anti-traversal, hf:// spec parsing with a mocked download (asserting the
large files are never requested), timestamp passthrough.
pytest tests/: 5685 passed),ruff check,ruff format --check,ty checkclean.POST
/decision, singleDECISION:line, clean sidecar), browse API +traversal guard, HF dataset end to end.
Screenshots
(attached below: single-run page, browse mode over a real experiment batch,
HF dataset browsing,
--confirmbar on the interactive page)Related: #1033 (capture-side timestamps — the timeline above renders the




moment it lands).