fix(runner): let one span own each token observation - #5717
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Railway Preview Environment
|
The runner published the same token count twice inside one OTLP batch: on the
per-call `chat` leaf spans, and again as the run total on their `invoke_agent`
ancestor. `gen_ai.usage.*_tokens` is the incremental bucket by contract, so ingest
read both as separate observations.
Nothing showed it, because the roll-up never ran on the runner's batch at all. The
span tree was seeded only from spans with no parent, and the runner's batch has none.
Once the roll-up runs, it adds the parent's own incremental value on top of the
children it just summed, and the agent span reports exactly twice the real count.
Measured live before this change, on a four-turn Pi run:
invoke_agent incremental 7776 cumulative 15552
turn 0 -> chat 1850
turn 1 -> chat 1926
turn 2 -> chat 1983
turn 3 -> chat 2017
The four leaves sum to 7776, and the parent reported 15552.
The producer owns this, not the roll-up. Adding a span's own incremental value to its
children's cumulative is the correct general definition, and making the API skip a
span by name would trade one bug for a wrong rule that breaks any producer whose
parent span legitimately makes its own model call.
So the token attributes come off `invoke_agent` in both tracers. `gen_ai.usage.cost`
stays, because ingest maps it to the cumulative bucket, which makes it the explicit
summary a parent should carry rather than a repeated incremental one.
Both harnesses had the same shape. Pi gave each turn's `chat` span that turn's usage
and the agent span their sum, so the error scaled with turn count. Claude ACP creates
one `chat` leaf per prompt in `start()` and stamped the same run total on both it and
the agent span. Moving ownership to the leaf loses nothing in either case. Turn spans
were already clean; they never carried usage.
Live after this change: a one-turn run reports 1739 against a leaf sum of 1739, a
three-turn run reports 5413 against 1777 plus 1809 plus 1827, and a four-turn run
reports 7414 against a leaf sum of 7414. Cost survives on every run.
Tests: the full runner suite passes, 1,490 tests across 98 files, and `tsc --noEmit`
is clean. Four new tests assert, for both tracers, that no span with children carries
token attributes and that the leaf sum equals the run total.
Claude-Session: https://claude.ai/code/session_01RkWWQUNNzRbaB5jnCAdjYA
e3cf8b3 to
ab1460e
Compare
10ed84c to
6477d28
Compare
Stacked on #5710. Set the base to that branch, so this diff shows only its own change.
This must land with or before #5709. That PR makes the roll-up run on the runner's OTLP batch for the first time, which is what turns the latent duplication described below into a visible wrong number.
The symptom
The
invoke_agentspan reports exactly twice the real token count.Measured live on a four-turn Pi run, before this change:
The four leaves sum to 7,776. The parent reported 15,552. A reader sees a child span carrying twice its parent's tokens in the trace tree, and the doubled value reaches analytics and evaluations, because
ag.metrics.tokens.cumulative.totalis a registered analytics metric.The cause
The runner publishes the same token observation twice inside one OTLP batch: on the per-call
chatleaves, and again as the run total on theirinvoke_agentancestor.gen_ai.usage.*_tokensis the incremental bucket by contract, so ingest reads both as separate observations.Nothing showed it until now, because the roll-up never ran on the runner's batch at all. The span tree was seeded only from spans with no parent, and the runner's batch has none. #5709 fixes that seeding, the roll-up starts running, and it adds the parent's own incremental value on top of the children it just summed.
Cost escaped the same fate only by accident: it maps to the cumulative bucket, where a guard stops the overwrite.
Why the fix belongs on the producer
Adding a span's own incremental value to its children's cumulative is the correct general definition. A parent span can legitimately make its own model call. Making the API skip a span by name would hide this instance and give the wrong answer for any honest producer.
So the token attributes come off
invoke_agentin both tracers.gen_ai.usage.coststays, because ingest maps it to the cumulative bucket, which makes it the explicit summary a parent should carry rather than a repeated incremental one.This follows the rule from an independent review of this area: exactly one span owns each incremental observation, and a parent's total is either rolled up from its children or carried as an explicitly cumulative summary.
Both harnesses, and the turn level
Pi gave each turn's
chatspan that turn's usage and the agent span their sum, so the error scaled with turn count. Claude ACP creates onechatleaf per prompt instart()and stamped the same run total on both it and the agent span. Moving ownership to the leaf loses nothing in either case, since that leaf always exists.Turn spans were already clean. Neither tracer stamps usage on a turn span; its cumulative was always a pure roll-up. The cumulative cost visible on the root, the agent span, the turn and the leaf is one value propagating up, not four additions. There is a test pinning that.
Verification
Live, on an EE dev stack, after the change:
invoke_agentcumulativeCost survives on every run.
Unit tests: the full runner suite passes, 1,490 tests across 98 files, and
tsc --noEmitis clean. Four new tests assert, for both tracers, that no span with children carries token attributes and that the leaf sum equals the run total.Notes for the reviewer