Skip to content

feat(web): agent session detail page - #546

Open
JeremyFunk wants to merge 7 commits into
mainfrom
feat/agent-session-detail
Open

feat(web): agent session detail page#546
JeremyFunk wants to merge 7 commits into
mainfrom
feat/agent-session-detail

Conversation

@JeremyFunk

@JeremyFunkJeremyFunk commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Adds the session detail page under Explore > Agent Sessions (still behind the agent_tracing flag), consuming the read layer from #540. List rows now link to it.

What's in it

API — one new internal endpoint, POST /internal/ai-sessions/spans: the existing aiSessionSpansQuery (bounded by rows and bytes via compiledQueryBounded), mapped through mapAiSpans server-side so the response carries the decoded span shape instead of the raw attribute maps. Truncation at the 2,000-span cap is reported as truncated: true; a byte-limit abort surfaces as a 413.

Session anatomy header — title from the first captured user message (falls back to agent name + start time), derived status (active / completed / failed / abandoned), wall-clock vs active time, an occupancy bar (idle · TTFT · inference · tool · unaccounted, computed as wall-clock occupancy so parallel tool calls can't push it past 100%), five token buckets, work and failure counts, and a model-spend estimate.

Trace view — the turn-grouped waterfall: trace rules band the turns, idle gaps are collapsed out of the axis (expandable per gap), agent-spans-only and text filtering, virtualized rows, and navigation from any span into the trace detail page.

Flow view — one lane per turn, nodes per agent/inference/tool span with plain SVG edges (no graph library), optional merging of repeated tool calls.

All header/turn/axis computation is pure client-side code in apps/web/src/lib/agent-sessions/ with colocated tests (turn segmentation, occupancy sweep, status derivation, axis compression, pricing).

Notes for review

  • Model spend is an estimate against a hand-maintained public list-price table dated in model-pricing.ts. Unpriced models are counted and surfaced ("N models unpriced") rather than silently priced at zero.
  • Turn boundaries: gen_ai.conversation.id when a vendor provides it (eve does), otherwise root agent-invocation spans, otherwise one turn per trace.
  • The window bounds both levels of the spans query, so the detail route carries t/end hints from the list row and pads by an hour; deep links without hints fall back to the last 7 days.
  • Known rough edge: a vendor whose first "user message" is injected context (eve prepends a <current_time> block) gets that as the session title verbatim.

Browser-verified locally against replayed captures from six frameworks: multi-trace banding, subagent nesting, 796-span virtualized session, token buckets/spend, filtering, flow lanes, deep-link fallback.

🤖 Generated with Claude Code


View with [code]smithAutofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

JeremyFunkand others added 7 commits August 20, 2026 09:36
Adds POST /internal/ai-sessions/spans: the two-stage aiSessionSpansQuery
bounded by rows and bytes, mapped through the gen_ai integration layer
server-side so the response carries the decoded span shape instead of the
raw attribute maps. Truncation at the 2,000-span cap is reported rather
than silent, and a byte-limit abort surfaces as a 413 AiSessionTooLargeError.
The web adapter and atom follow the existing list/facets plumbing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Derives the shape of an agent session from its spans: turn boundaries, the
occupancy of the wall clock, token and failure counts, the collapsed-idle time
axis, and an estimate of model spend against a dated table of list prices. No
framework records where one turn ends, so each rule is a named heuristic with
its fallbacks — conversation id, then root agent invocations, then trace
boundaries — and the tests cover the paths that pick each one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opens a session from the list into its own page: a header stating how long it
ran, where the time went, what it spent and how it failed, then the same spans
as either a turn-grouped waterfall or a lane-per-turn flow. The waterfall keeps
three things adjustable because each is occasionally the thing you need — turns,
collapsed idle, and hiding the app's own spans — and both views link every span
back to its trace.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pan filter
A trace rule was emitted before its turn's visibility was known, so filtering
out a whole turn left the rule dangling above nothing; the rule now opens at
the trace's first surviving turn. Collapsed turn headers likewise counted
spans the active filter was hiding.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lowers the response byte ceiling to 10MB — the bounded read accumulates
parsed rows before the counter can trip, so 20MB of attribute-heavy rows
risked the isolate instead of producing the designed 413. The 413 now
carries fix_request recovery (a narrower window genuinely helps), the
handler annotates session id, span count and truncation on its span, a
span-level key-drift assert joins the genAi ones, and the truncation and
limit-error paths get a handler test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review against production spans found the turn rules failing off the
mockup's idealized shapes: six vendors derive the session id from
gen_ai.conversation.id, so a single distinct id must not count as a turn
partition; agent roots must be found by walking for an AI ancestor, since
the app's own spans parent every agent span; and same-millisecond anchors
produced phantom empty turns. Also: llm-call counting now agrees with
span classification for unknown operation names, titles and turn labels
reject injected pseudo-XML context blocks, failure/retry counts and token
totals count only the deepest span carrying a signal (frameworks stamp
parents and children alike), cache buckets no longer double the headline
token count, model pricing refuses to price unknown models off a
neighbouring row, and a session that errored recently reads failed, not
active.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stats band now steps through 2/3/5 columns instead of collapsing to a
560px stack that clipped the waterfall out of reach at 1280x800. Filtering
is consistent end to end: fully filtered turns take their trace rules and
idle rows with them, the count strip reads "N of M spans", and the flow
view respects the same filter. Single-turn traces put their trace link in
the turn header instead of a dedicated rule row, the model/target column
no longer echoes the span name, SUBAGENT marks only real delegations,
retried calls get their pill, view state survives tab switches, zero-value
stats give way to empty states, and the axis uses nice-number ticks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JeremyFunk

Copy link
Copy Markdown
CollaboratorAuthor

Post-open review cycle (an effect-review-v4 multi-lane pass plus a live frontend design review) landed as the three fix commits:

Correctness against real vendor data — the turn rules were rebuilt after the review verified them against production spans: a single distinct gen_ai.conversation.id no longer counts as a turn partition (six vendors derive the session id from it, which collapsed their sessions to one turn), turn anchors are found by walking for an AI ancestor rather than requiring a parentless span (the app's own HTTP/workflow spans are the real trace roots, which made completed sessions read ABANDONED), and same-millisecond anchors no longer mint phantom turns. Failure/retry/token counting now takes only the deepest span carrying a signal, the headline token total no longer doubles when cache buckets overlap input, and model pricing refuses to price an unlisted model off a neighbouring row (gpt-4o-mini was billing as gpt-4o, 16.7× off) — those land in unpriced instead.

Endpoint hardening — byte ceiling 20MB→10MB (parsed-row accumulation means the cap must sit well under the isolate heap), 413 with fix_request recovery, span annotations for session id/span count/truncation, and a handler test covering the truncation and limit-error paths.

Design/UX — the stats band no longer collapses into a stack that pushed the waterfall off-screen at 1280×800; titles and turn labels reject injected pseudo-XML context blocks (real eve sessions titled themselves <current_time> …); the model/target column stopped echoing span names; filtering is consistent across trace rules, idle rows, counts, and the flow view; single-turn traces inline their trace link in the turn header; view state survives tab switches; zero-value stats render empty states instead of $0.00.

Known follow-ups deliberately not in this PR: TTFT is dark for orgs whose SDKs emit gen_ai.client.operation.time_to_first_chunk (catalog maps only the response.-prefixed key — needs a unit check before aliasing); the wire ships captured content fields the page doesn't render yet; the hand-mirrored genAi wire schema could move to the catalog package to delete the drift asserts; no live refresh for ACTIVE sessions.

🤖 Generated with Claude Code

Sign up for freeto 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

@JeremyFunk