{"slug": "a-cli-that-shows-which-sub-agent-burned-your-tokens-in-one-run", "title": "A CLI that shows which sub-agent burned your tokens in one run", "summary": "A new command-line tool called token-trace-viewer lets developers trace token and dollar costs across sub-agents in a single multi-agent run by reading OpenInference-style JSONL trace files. The Python 3.10+ tool, which requires no server, dashboard, account, or pip install, outputs plain tables showing which sub-agent burned the most tokens, which step cost the most money, and where the tool failed. It includes sample traces and supports flags for JSON output, agent/model filtering, top-N rows, and custom pricing files.", "body_md": "**Explain where the tokens (and dollars) went in a single agent run.**\n\n`token-trace-viewer`\n\nreads an [OpenInference](https://arize-ai.github.io/openinference/spec/semantic_conventions.html)-style JSONL trace produced by one multi-agent run and answers, from the command line, in plain tables:\n\n- Which sub-agent burned the most tokens, and on which step?\n- Which single step cost the most money?\n- How much of the file did the tool actually understand, and where did it fail?\n\nNo server, no dashboard, no account. Point it at a log file, get a table back.\n\n``` bash\n$ python3 cost.py sample.jsonl\n #  AGENT       STEP                     IN    OUT     COST  % OF TOTAL\n 1  planner     verify_output        15,240    430  $0.0869       63.0%\n 2  planner     decompose_goal        2,610    388  $0.0227       16.5%\n 3  planner     plan_task             1,840    512  $0.0220       15.9%\n 4  planner     route_to_researcher     940     64  $0.0063        4.6%\n 5  researcher  search_web            4,200    310      n/a         n/a\n 6  researcher  web_search_api            0      0      n/a         n/a\n ...\n\nnote: 8 step(s) have no price for their model (or no llm.model_name at all)\nand are shown with cost \"n/a\"; they are excluded from % of run total.\n\ncovered 12 of 12 lines; 0 unreadable, 0 with no agent name, 0 duplicate span(s) dropped\n```\n\n- Python\n**3.10+**(uses`X | None`\n\nunion type syntax in dataclass fields) - Standard library only — no\n`pip install`\n\nneeded\n\n```\npython3 report.py <path-to-log.jsonl>    # token totals by agent, then by step\npython3 cost.py <path-to-log.jsonl>      # every step ranked by dollar cost\npython3 repeats.py <path-to-log.jsonl>   # context blocks resent across steps\npython3 parser.py <path-to-log.jsonl>    # quick parse summary (lines parsed/skipped)\n\n# or, one entry point for the first three:\npython3 tokentrace.py report <path-to-log.jsonl>\npython3 tokentrace.py cost <path-to-log.jsonl>\npython3 tokentrace.py repeats <path-to-log.jsonl>\n```\n\nUseful flags on `report.py`\n\n/ `cost.py`\n\n/ `repeats.py`\n\n(and their `tokentrace.py`\n\nequivalents):\n\n| Flag | Effect |\n|---|---|\n`--json` |\nPrint machine-readable JSON instead of a table |\n`--agent NAME` |\nOnly include steps from this agent (`report.py` , `cost.py` ) |\n`--model NAME` |\nOnly include steps from this model (`report.py` , `cost.py` ) |\n`--top N` |\nOnly show the top N rows |\n`--prices FILE` |\nUse a pricing file other than `prices.json` (`cost.py` , `repeats.py` ) |\n\nTwo sample traces are included:\n\n`sample.jsonl`\n\n— a clean 12-step, 3-agent (planner → researcher → writer) research pipeline`sample-broken.jsonl`\n\n— the same trace with real-world damage injected (a truncated line, a stray unescaped quote, a bare blank line, a span with no`agent.name`\n\n) to demonstrate the tool's error handling\n\n```\npython3 report.py sample.jsonl\npython3 cost.py sample-broken.jsonl\nparser.py             shared JSONL → Step parsing (stdlib only, no cost/ranking logic)\nreport.py             token totals by agent → step\ncost.py               cost ranking by step\nrepeats.py            context blocks resent more than once across steps\ntokentrace.py          single CLI entry point over report/cost/repeats/parse\nprices.json.example    sample pricing-overrides file (copy to prices.json to use)\ntests/                 stdlib unittest suite for all of the above\nsample.jsonl           clean 3-agent example trace\nsample-broken.jsonl    the same trace with real-world corruption injected\nscope.md               what this tool does and deliberately does not do, and why\nexistence-check.md     survey of existing tools that informed the scope\npython3 -m unittest discover -s tests -v\n```\n\nStdlib `unittest`\n\n, no extra dependencies. Covers `parser.py`\n\n(malformed lines, negative/null token counts, duplicate `span_id`\n\nhandling, timestamp parsing), `cost.py`\n\n(pricing lookup, sorting, `prices.json`\n\noverrides), `report.py`\n\n(aggregation and rendering), and `repeats.py`\n\n(repeat detection and the wasted-token estimate) — including full runs against both `sample.jsonl`\n\nand `sample-broken.jsonl`\n\n.\n\nTrace dashboards like LangSmith, Langfuse, Helicone, and Arize Phoenix all answer *cross-run* questions well — cost trends over a week, spend per project, per model. None of them make it easy to answer a much narrower question an engineer actually asks after a spike: **\"why did this one run cost $4.20, and which sub-agent was responsible?\"**\n\nA short survey of five tools (`existence-check.md`\n\n) found:\n\n| Tool | Per-sub-agent cost ranking for one run? | Note |\n|---|---|---|\n| LangSmith | Dashboard-level only | Custom widget, aggregates across runs, no single-run breakdown |\n| Langfuse | Dashboard-level only | Same shape as LangSmith |\n| Helicone | No (tags-only) | Requires you to tag every call and write your own SQL |\n| Phoenix / Arize | No | Cost shown per trace/span/project; no grouping or ranking |\n| OpenInference spec | N/A | Defines the fields (`agent.name` , `llm.token_count.*` , ...) but ships no UI |\n\n`token-trace-viewer`\n\nreads exactly those OpenInference fields and turns one trace file into a per-run, per-agent, per-step breakdown — nothing more. See `scope.md`\n\nfor the full rationale and out-of-scope list.\n\n| Script | Answers | Sorted by |\n|---|---|---|\n`report.py` |\nHow many input/output tokens did each agent — and each step within that agent — use? | Token volume, biggest agent first, biggest step within each agent |\n`cost.py` |\nWhat did each individual step cost in dollars, across the whole run? | Dollar cost, highest first |\n`repeats.py` |\nWhich context blocks (system/user/assistant messages) were sent more than once across steps, and roughly how much did that waste? | Estimated wasted tokens, highest first |\n`parser.py` |\n(library + CLI smoke test) Parse the log and report how many lines were understood | — |\n`tokentrace.py` |\nOne entry point for the three commands above (`tokentrace.py report|cost|repeats <file>` ) |\n— |\n\n`report.py`\n\n, `cost.py`\n\n, and `repeats.py`\n\nare all built on the same shared parser (`parser.py`\n\n) and all print a coverage line at the end so you always know how much of the file the tool actually read. Each also accepts `--json`\n\nfor machine-readable output, and `--agent`\n\n/`--model`\n\n/`--top`\n\n(where applicable) to filter or limit a large trace.\n\n**No dashboard, no server.** One command, text output, nothing hosted.**No trends across runs.** It explains a single trace file, not a fleet of runs over time — that's what LangSmith/Langfuse are for.**No optimization advice.** It tells you a step cost $0.09 and ranks it; it doesn't tell you how to make it cheaper.**No instrumentation.** It doesn't wrap your LLM calls or generate traces — you bring a log, it reads it.**One provider's prices, hardcoded.** See[Pricing](#pricing)below.\n\n```\nwriter                   37,810 in  5,365 out\n    emit_answer          16,010 in  1,120 out\n    revise_draft         12,680 in  1,905 out\n    draft_section         9,120 in  2,340 out\nresearcher               34,380 in  2,428 out\n    summarize_findings   18,430 in  1,276 out\n    read_results         11,750 in    842 out\n    search_web            4,200 in    310 out\n    web_search_api            0 in      0 out\n    fetch_page                0 in      0 out\nplanner                  20,630 in  1,394 out\n    verify_output        15,240 in    430 out\n    decompose_goal        2,610 in    388 out\n    plan_task             1,840 in    512 out\n    route_to_researcher     940 in     64 out\nTOTAL                    92,820 in  9,187 out\ncovered 12 of 12 lines; 0 unreadable, 0 with no agent name, 0 duplicate span(s) dropped\n```\n\nAgents are sorted by total tokens (input + output), highest first. Steps within each agent are sorted the same way. A step name that repeats within an agent (e.g. a retry loop) is folded into one row and annotated with `(x<count>)`\n\n.\n\nPriced rows sort to the top by cost, highest first; unpriced rows (no known price for the model, or no `llm.model_name`\n\nat all) sink to the bottom, marked `n/a`\n\n, and are excluded from the `% OF TOTAL`\n\ncolumn so the percentages still add up to 100% of *priced* spend.\n\nThe gap noted in `scope.md`\n\n: no surveyed tool (see `existence-check.md`\n\n) flags identical context resent across steps in a single run. `repeats.py`\n\nhashes every system/user/assistant message body in the trace and flags any hash that shows up under more than one step:\n\n``` bash\n$ python3 repeats.py sample.jsonl\nHASH      ROLE    SENDS  CHARS  EST WASTED TOKENS  EST WASTED $  SEEN IN\nd8c5cd4f  system      9  3,066              6,128           n/a  planner/plan_task, planner/decompose_goal, researcher/search_web, +6 more\n\nnote: token/cost figures are estimated at ~4 chars per token (traces carry per-step totals, not\nper-message token counts), and only cover blocks priced in the model's rate table; 'n/a' means at\nleast one resend used an unpriced model.\n\ncovered 12 of 12 lines; 0 unreadable, 0 with no agent name, 0 duplicate span(s) dropped\n```\n\nHere, the same ~3KB system prompt was sent unchanged on 9 of the run's 9 LLM calls — 8 of those sends were pure repeats of the first, at an estimated ~6,100 wasted input tokens.\n\nTwo things this deliberately does **not** do, to stay honest about what a trace actually contains:\n\n- It hashes message content rather than storing it, so a trace with megabytes of prompt text doesn't need to be held in memory just to compare it. You get a hash prefix and a length, not a diff.\n- OpenInference spans carry a\n*per-step*token total, not a*per-message*one, so the token cost of one resent block can't be read off the trace directly — it's estimated from character length at roughly 4 characters per token, and the output says so every time.\n\n``` bash\n$ python3 parser.py sample.jsonl\nparsed 12 steps, skipped 0 bad lines, 0 step(s) had no agent name, 0 duplicate span(s) dropped\n```\n\n`report.py`\n\n, `cost.py`\n\n, and `repeats.py`\n\nall import `parse_log()`\n\nfrom this module rather than re-implementing parsing.\n\nReal traces come from streaming exporters: processes get killed mid-write, strings arrive unescaped, lines show up empty. `parser.py`\n\nreads the file **one line at a time** — never `json.load()`\n\ns the whole thing — so:\n\n- a truncated or malformed line is\n**skipped and counted**, not fatal to the run - a syntactically valid JSON value that isn't an object (a bare string, number, or list) is treated the same way\n- a line that parses fine but is missing\n`agent.name`\n\nis counted separately as**\"unknown agent\"**— a*quiet*failure mode where the run looks healthy but the tool actually learned nothing from that line, so it's surfaced instead of hidden - token counts that arrive as\n`null`\n\n, missing, a numeric string, or**negative**(exporter corruption, not a real count) are coerced to`0`\n\ninstead of crashing the run or skewing totals - a span whose\n`span_id`\n\nhas already been seen is a**duplicate**, not a new step — a common artifact of a streaming exporter that crashed and re-emitted a span on restart. It is dropped and counted separately rather than silently double-counted into the totals\n\nEvery run ends with a coverage line — `covered X of Y lines; N unreadable, M with no agent name, K duplicate span(s) dropped`\n\n— so you always know how much of the file the numbers above actually represent.\n\nRunning `report.py`\n\nagainst the intentionally-damaged `sample-broken.jsonl`\n\nshows this in action:\n\n``` bash\n$ python3 report.py sample-broken.jsonl\n...\nunknown                       0 in      0 out\n    route_to_researcher       0 in      0 out\nTOTAL                    73,260 in  7,615 out\ncovered 8 of 12 lines; 4 unreadable, 1 with no agent name, 0 duplicate span(s) dropped\n```\n\nIf a span carries `start_time`\n\n/`end_time`\n\ntimestamps, `report.py`\n\nand `cost.py`\n\nalso print a `run duration: N.Ns`\n\nline — the span from the earliest start to the latest end seen in the trace. It's omitted when the trace has fewer than two distinct timestamps to measure a duration from (as in the two sample files above, which only carry one incidental timestamp between them).\n\n`cost.py`\n\nships one hardcoded default price table (`DEFAULT_PRICES`\n\nin `cost.py`\n\n), sourced from the Anthropic pricing page and checked on the date noted in the file:\n\n| Model | Input $ / 1M tokens | Output $ / 1M tokens |\n|---|---|---|\n`claude-opus-5-0` |\n$5.00 | $25.00 |\n\nAny step whose `llm.model_name`\n\nisn't in that table — or has no model name at all (e.g. some `CHAIN`\n\n/`TOOL`\n\nspans) — is priced as `n/a`\n\nrather than guessed at, and excluded from the percentage-of-total column.\n\nTo price additional models or providers **without editing code**, drop a `prices.json`\n\nnext to `cost.py`\n\n:\n\n```\n{\n  \"claude-opus-5-0\": { \"input\": 5.0, \"output\": 25.0 },\n  \"some-other-model\": { \"input\": 1.5, \"output\": 6.0 }\n}\n```\n\nEntries there are overlaid on top of the built-in defaults (same model name overrides the price; new model names are added). `repeats.py`\n\nuses the same table, since it estimates the dollar cost of a resent block, too. Use `--prices <file>`\n\nto point at a pricing file somewhere other than the default location.\n\nEach line of the input file is one JSON object (a span), following the [OpenInference semantic conventions](https://arize-ai.github.io/openinference/spec/semantic_conventions.html). The fields this tool reads:\n\n| Field | Used for |\n|---|---|\n`attributes[\"agent.name\"]` |\nGrouping (defaults to `\"unknown\"` if absent) |\n`attributes[\"graph.node.name\"]` (falls back to top-level `\"name\"` ) |\nStep label |\n`attributes[\"llm.model_name\"]` |\nCost lookup |\n`attributes[\"llm.token_count.prompt\"]` |\nInput tokens |\n`attributes[\"llm.token_count.completion\"]` |\nOutput tokens |\n`attributes[\"llm.input_messages.<i>.message.role\"]` / `...content\"]` |\nRepeated-context detection (`repeats.py` ); content is hashed, not stored |\ntop-level `\"span_id\"` |\nDe-duplicating spans re-emitted by a restarted exporter |\ntop-level `\"start_time\"` / `\"end_time\"` |\nThe `run duration` line in `report.py` / `cost.py` , when present |\n\nTool names and any other span fields not listed above are still ignored.\n\nPer `scope.md`\n\n:\n\n~~Repeated-context detection~~— shipped as`repeats.py`\n\n.- Prices for additional model providers beyond the single hardcoded entry — the\n`prices.json`\n\noverride file (see[Pricing](#pricing)) makes this possible without code changes, but no additional providers are pre-populated. - Framework-specific log formats (only OpenInference JSONL is read today).\n\nNo license file is currently included in this repository.", "url": "https://wpnews.pro/news/a-cli-that-shows-which-sub-agent-burned-your-tokens-in-one-run", "canonical_source": "https://github.com/rrkher059/token-trace-viewer", "published_at": "2026-07-29 04:16:32+00:00", "updated_at": "2026-07-29 04:52:23.410884+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-infrastructure"], "entities": ["token-trace-viewer", "OpenInference", "Python"], "alternates": {"html": "https://wpnews.pro/news/a-cli-that-shows-which-sub-agent-burned-your-tokens-in-one-run", "markdown": "https://wpnews.pro/news/a-cli-that-shows-which-sub-agent-burned-your-tokens-in-one-run.md", "text": "https://wpnews.pro/news/a-cli-that-shows-which-sub-agent-burned-your-tokens-in-one-run.txt", "jsonld": "https://wpnews.pro/news/a-cli-that-shows-which-sub-agent-burned-your-tokens-in-one-run.jsonld"}}