Skip to content

Track sub-agent type per message; add "by agent" breakdown and filter#6

Open
ludwigehlert-gif wants to merge 5 commits into
JedIV:mainfrom
ludwigehlert-gif:feat/by-agent-breakdown
Open

Track sub-agent type per message; add "by agent" breakdown and filter#6
ludwigehlert-gif wants to merge 5 commits into
JedIV:mainfrom
ludwigehlert-gif:feat/by-agent-breakdown

Conversation

@ludwigehlert-gif
Copy link
Copy Markdown
Contributor

Summary

Each Claude Code sub-agent JSONL (<project>/<sessionId>/subagents/agent-*.jsonl) has a sibling agent-*.meta.json with the agentType (Explore, Plan, general-purpose, claude-code-guide, …) and a per-invocation description. After #4 those files are ingested, but every row showed up as just "the parent session" with no way to tell whose turns cost what.

This PR makes that visible:

  • Schema — adds nullable agent_type + agent_desc columns to messages, with a lightweight ALTER TABLE migration in init() and a one-shot backfill in ingest.run() that tags rows ingested before this change.
  • Parser — reads the .meta.json sidecar when parsing a sub-agent file and stamps every message row.
  • API — every filtered endpoint accepts a new agent query parameter (agent=main selects the top-level conversation only); /api/stats returns a by_agent aggregate; /api/breakdown_series supports group=agent; MCP views filter via the assistant turn that issued the call, matching the existing model filter shape.
  • UI — new agent filter dropdown, new by agent toggle in the breakdown row, and the session timeline now shows an "agent" column so it's clear which turns came from which sub-agent invocation (with the per-invocation description rendered to disambiguate multiple invocations of the same agent type).

Example output on my data:

main session          msgs=21,646   cost=$4,747.54
general-purpose       msgs= 1,158   cost=  $209.76
Explore               msgs= 1,722   cost=   $17.79
Plan                  msgs=    25   cost=    $1.76
claude-code-guide     msgs=    16   cost=    $0.25

Test plan

  • Fresh checkout: uv run python -m tracker.ingest — schema migration adds columns; sub-agent rows ingested as before.
  • Existing DB: the same command performs the one-shot backfill; SELECT COUNT(*) FROM messages WHERE agent_type IS NOT NULL is nonzero.
  • Dashboard: switch to "by agent" — main session vs each sub-agent type listed with cost/tokens.
  • Set the agent filter to e.g. "Explore" — all other panels (totals, charts, by-model, by-project, MCP views, session list, session detail) re-scope correctly.
  • Open a session that spawned sub-agents — timeline shows the agent column with agentType · description per row.

🤖 Generated with Claude Code

ludwigehlert-gif and others added 2 commits May 20, 2026 14:33
Adds totals.cost_breakdown to /api/stats with per-bucket cost (input,
output, cache_read, cache_write_5m, cache_write_1h), grouped per
(tool, model) so the correct rates are applied. The UI surfaces four
new cards — cache read, cache write, output, fresh input — each with
its share of the total cost.

For Claude Code sessions this makes it obvious that cache reads
(re-sending the conversation each turn at 10% input rate) and 1h cache
writes dominate the bill, not fresh input or output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each Claude Code sub-agent JSONL has a sibling agent-<id>.meta.json
sidecar that names the agentType (Explore, Plan, general-purpose, ...)
and a per-invocation description. The parser now reads it and tags
every message row with agent_type / agent_desc. A lightweight ALTER
TABLE migration adds the columns to existing DBs, and a one-shot
backfill on ingest tags rows that were already imported before this
change.

New UI surface:
- "agent" filter dropdown (with a "main" pseudo-value to isolate
  top-level conversation turns).
- "by agent" breakdown toggle that groups cost / tokens / msgs by
  agent_type (NULL bucketed as "main session").
- Session timeline now shows the agent column so it's clear which
  turns came from which sub-agent invocation, with the description
  to disambiguate multiple invocations of the same agent type.

All existing filtered endpoints (/api/stats, /api/sessions,
/api/session/{id}, /api/mcp, /api/breakdown_series) now accept an
`agent` query parameter; MCP views match it via the assistant turn
that issued the call, consistent with the existing model filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ludwigehlert-gif ludwigehlert-gif marked this pull request as draft May 20, 2026 13:16
ludwigehlert-gif and others added 3 commits May 20, 2026 15:43
The dashboard is a local dev tool, so reload-on-edit beats having to
remember to kill+restart after every code change. Watches only
tracker/ and web/ to avoid noisy restarts. Set RELOAD=0 to opt out
(useful if/when running under launchd).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Track agent_id (the unique hash from agent-<id>.jsonl, what shows up
in the dashboard as the sub-agent "PID") on each message row, with an
ALTER TABLE migration and a one-shot backfill from the existing
sub-agent filenames.

The "by agent" breakdown now groups by (agent_type, agent_id,
agent_desc) so each Task invocation is its own row — e.g. you can
finally see that one specific "End-to-end SAP→Dataiku migration V3"
general-purpose run was $93.71 instead of just "general-purpose:
$209.76 across 24 runs". The chart for group=agent plots top-5
invocations over time instead of top-5 agent types.

UI tightening:
- Cards split into two locked-width groups: "overview" (6 tiles) and
  "where the $ goes" (4 cost-bucket tiles). Cards never wrap to a
  second row — they shrink uniformly to fit, with text-overflow
  ellipsis on labels/values and a full-text tooltip on hover.
- Session timeline shows agent_type, an 8-char agent_id slug, and
  the per-invocation description for sub-agent turns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…kdown

Every Claude Code JSONL record carries an "entrypoint" field —
"cli" for the interactive REPL, "sdk-cli" for SDK-spawned runs,
"claude-desktop" for the desktop app. Codex sessions are tagged
"codex" by convention. Without it, an SDK-spawned migration is
indistinguishable from an interactive session: both bucket into
"main session" with no way to scope cost to one or the other.

- sessions.entrypoint column + ALTER TABLE migration in init().
- Claude parser captures the field from any record that has it
  (first non-meta lines do); codex parser stamps "codex".
- ingest.run() one-shot backfill: walks each session's first ~30
  JSONL lines to recover entrypoint for rows ingested before this
  change. Sessions whose source JSONL has been purged by Claude
  Code's housekeeping stay (unknown) — there's no source to read.
- API: new entrypoint query param on every filtered endpoint;
  /api/filters returns distinct entrypoints; /api/stats adds
  by_entrypoint; /api/breakdown_series supports group=entrypoint.
- UI: entrypoint filter dropdown; "by entrypoint" breakdown
  toggle; entrypoint column on the by-session table so
  sdk-cli rows are visible at a glance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ludwigehlert-gif ludwigehlert-gif marked this pull request as ready for review May 20, 2026 15:26
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