Skip to content

fix(runner): let one span own each token observation - #5717

Open
mmabrouk wants to merge 1 commit into
fix/runner-context-size-not-usagefrom
fix/runner-usage-ownership
Open

fix(runner): let one span own each token observation#5717
mmabrouk wants to merge 1 commit into
fix/runner-context-size-not-usagefrom
fix/runner-usage-ownership

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Aug 3, 2026

Copy link
Copy Markdown
Member

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_agent span reports exactly twice the real token count.

Measured live on a four-turn Pi run, before this change:

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 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.total is a registered analytics metric.

The cause

The runner publishes the same token observation twice inside one OTLP batch: on the per-call chat leaves, and again as the run total on their invoke_agent ancestor. gen_ai.usage.*_tokens is 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_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.

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 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, 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:

Run invoke_agent cumulative Leaf sum Cost
1 turn 1,739 1,739 $0.0022015
3 turns 5,413 1,777 + 1,809 + 1,827 $0.0028950
4 turns 7,414 7,414 $0.0034015

Cost survives on every run.

Unit 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.

Notes for the reviewer

  • Claude ACP was not verified live on this stack. The run failed before emitting any span, with a connection reset to the ACP transport, which is an environment gap unrelated to this change. That path is covered by unit tests only.
  • Version skew is not handled, on purpose. An old runner image posting to a new API will still double count. A parent-side override would be wrong for producers whose parent spans legitimately make their own model call. A companion change adds a warning that names the offending span when it happens.
  • The Python SDK still stamps token attributes on the workflow root. That is safe only because the root ships in its own OTLP request with no token-bearing children. If that batch ever gains model spans it would double count the same way.

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Aug 3, 2026
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview Aug 3, 2026 10:29pm

Request Review

@dosubot dosubot Bot added the bug Something isn't working label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bb357185-5b1e-493b-a920-bacaec0dee1c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-5717.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-5717-47c7b81
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-08-03T22:36:13.979Z

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant