Skip to content

fix(runner): stop reporting context-window size as a run's token usage - #5710

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

fix(runner): stop reporting context-window size as a run's token usage#5710
mmabrouk wants to merge 1 commit into
mainfrom
fix/runner-context-size-not-usage

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Aug 3, 2026

Copy link
Copy Markdown
Member

The symptom

Agent runs report token totals that are not token totals.

Measured on one stack: 6,041 root spans shaped input 0 / output 0 / total 63369, with no cost.

The cause

When the harness's PromptResponse carries no usage, the runner fell back to the ACP stream's usage_update.used value and reported it as the run's token total.

That value is not a count of the tokens the run spent. It is how full the agent's context window is at that point in the turn.

A wrong number here is worse than no number. A reader cannot tell a genuine 63,369-token run from a context-size artifact, and any token aggregate built on that path is silently poisoned.

The fix

Fixing the usage merge alone would not have removed those rows. The tracer keeps its own usage record built straight from the stream, and handing it undefined is a no-op rather than a clear, so it kept stamping the context size on the root span anyway.

So the fix cuts at the source:

  • The usage_update handler drops used entirely and keeps only the reported cost. An update carrying neither is not recorded.
  • mergePromptAndStreamUsage derives the total solely from the harness's split. Cost-only usage is still returned, as before.
  • stampUsage skips a usage record with no tokens and no cost. All zeros is the absence of a measurement, not a measured zero, and asserting that a run cost nothing is its own wrong answer.

Nothing about what the harness reports changed, and the cost path is untouched.

Why dropping the value is safe

Every consumer of the merged usage object was traced:

Consumer Behavior with no usage
run.setUsage early returns on undefined, stamps nothing
Python SDK record_usage early returns on a falsy total
wire.py reads data.get("usage"), no presence assumption
Frontend transcriptToMessages merges field by field, each guarded on !== undefined

The other tracer in src/extensions/agenta.ts is fed by Pi's own token counts, not the ACP path, and is untouched.

No consumer wants the context size, so carrying it under a distinct field would have added a wire field to the mirrored protocol.ts and wire.py contract with zero subscribers. Absent data reading as absent is the honest option.

Verification

  • Full runner suite: 1,486 tests across 97 test files, all passing. tsc --noEmit clean.
  • Nine new cases: a context-size-only stream reports nothing; a cost-only stream still reports its cost; a half-reported split is preserved; a normal split through the harness is stamped as before; and at the tracer level, a usage_update carrying only used produces no usage event and no gen_ai.usage.* on the root span.

Related

Part of a set of three independent fixes for the same reported problem, that an agent run shows a cost in the playground and none in the trace. The other two are the Python SDK streaming fix and the API ingest fix.

@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

@mmabrouk

mmabrouk commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

This is one of three independent fixes for the same reported problem: an agent run shows a cost in the playground and nothing in the trace. They can be reviewed and merged separately, in any order.

Live end-to-end verification of all three together on a running stack is in progress, and I will post the result here.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved usage and cost reporting when token details are incomplete.
    • Preserved explicit zero costs while omitting unavailable cost data.
    • Prevented context-size and intermediate updates from being reported as run token usage.
    • Preserved stream costs without fabricating token totals.
    • Prioritized available token splits and retained partial data accurately.
  • Tests
    • Added coverage for missing, zero, partial, and conflicting usage and cost data.

Walkthrough

The change separates ACP context occupancy from run token usage. It preserves reported stream and harness costs, including zero, omits unknown costs, and skips empty OpenTelemetry usage updates. Unit tests cover sandbox usage resolution and telemetry stamping.

Changes

Usage Corrections

Layer / File(s) Summary
Sandbox usage resolution and cost contract
services/runner/src/protocol.ts, services/runner/src/engines/sandbox_agent/usage.ts, services/runner/tests/unit/sandbox-agent-usage.test.ts
AgentUsage.cost is optional. Run token totals use input/output splits instead of stream totals. Tests cover partial data, precedence, context-only usage, unknown cost, and explicit zero cost.
OpenTelemetry usage stamping
services/runner/src/tracing/otel.ts, services/runner/tests/unit/otel-usage-context-size.test.ts
Empty usage is not stamped. ACP usage_update ignores context occupancy as token usage and records only numeric costs. Tests cover stream costs, token-only harness usage, and harness token splits.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary fix: preventing context-window size from being reported as run token usage.
Description check ✅ Passed The description directly explains the reporting issue, implementation changes, safety rationale, and verification results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/runner-context-size-not-usage

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.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== PR metadata and changed files =="
gh pr view 5710 --repo Agenta-AI/agenta --json title,baseRefName,headRefName,files,body \
  --jq '{title, baseRefName, headRefName, files: [.files[].path]}'

echo
echo "== Changed-file diff summary =="
git diff --stat "origin/$(gh pr view 5710 --repo Agenta-AI/agenta --json baseRefName --jq -r .baseRefName)"...HEAD

echo
echo "== Changed-file names =="
git diff --name-only "origin/$(gh pr view 5710 --repo Agenta-AI/agenta --json baseRefName --jq -r .baseRefName)"...HEAD

echo
echo "== Usage-related symbols and nearby definitions =="
rg -n -C 5 -g '*.{ts,tsx}' \
  'mergePromptAndStreamUsage|stampUsage|usage_update|usageUpdate|PromptResponse|setUsage' .

Length of output: 1717


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== PR diff from GitHub =="
gh pr diff 5710 --repo Agenta-AI/agenta --patch

echo
echo "== Modified source-file outlines =="
ast-grep outline services/runner/src/engines/sandbox_agent/usage.ts --items all
ast-grep outline services/runner/src/tracing/otel.ts --items all

echo
echo "== Modified source files =="
cat -n services/runner/src/engines/sandbox_agent/usage.ts
cat -n services/runner/src/tracing/otel.ts

echo
echo "== Modified tests =="
cat -n services/runner/tests/unit/otel-usage-context-size.test.ts
cat -n services/runner/tests/unit/sandbox-agent-usage.test.ts

echo
echo "== Direct usage-accounting call sites and ACP usage-update definitions =="
rg -n -C 4 -g '*.{ts,tsx}' \
  'mergePromptAndStreamUsage\s*\(|stampUsage\s*\(|usage_update|used\s*:' \
  services/runner/src services/runner/tests

Length of output: 139398


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AgentUsage and usage event contract =="
rg -n -C 8 -g '*.{ts,tsx}' \
  'interface AgentUsage|type AgentUsage|type: "usage"|interface .*Usage' \
  services/runner/src/protocol.ts services/runner/src

echo
echo "== Usage resolution and tracer writeback call sites =="
rg -n -C 10 -g '*.{ts,tsx}' \
  'resolveRunUsage\s*\(|\.setUsage\s*\(|otel\.usage\s*\(' \
  services/runner/src services/runner/tests

echo
echo "== Existing ACP stream-event assertions =="
cat -n services/runner/tests/unit/stream-events.test.ts

echo
echo "== Dependency declarations and ACP type definitions, if checked in =="
rg -n -C 5 -g '*.{ts,json,yaml,yml}' \
  'agent-client-protocol|`@agentclientprotocol`|usage_update|UsageUpdate' \
  services/runner/package.json services/runner/pnpm-lock.yaml package.json pnpm-lock.yaml 2>/dev/null || true
fd -HI -t f '.*\.(ts|d\.ts)$' . | rg 'agent.*protocol|acp' | head -80

Length of output: 53953


@mmabrouk I found no blocking issues in #5710.

The change removes ACP usage_update.used from both usage paths. The runner now derives token totals from the harness split. The tracer records cost without treating context-window occupancy as token usage. The new tests cover the affected paths.

I did not run the test suite. The review sandbox does not permit execution of repository code.


✏️ Learnings added
Learnt from: mmabrouk
URL: https://github.com/Agenta-AI/agenta/pull/5710

Timestamp: 2026-08-03T20:41:50.353Z
Learning: The reported agent-run cost-versus-trace issue has three independent fixes that can be reviewed and merged in any order: `#5708` preserves the workflow span during Python SDK streaming, `#5709` accepts and preserves reported cost during API ingest and span-tree construction, and `#5710` prevents runner ACP context-window occupancy (`usage_update.used`) from being reported as run token usage.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fb38a46a-63cb-4a92-8c46-01fe6b6ddd65

📥 Commits

Reviewing files that changed from the base of the PR and between 69e2165 and e3cf8b3.

📒 Files selected for processing (4)
  • services/runner/src/engines/sandbox_agent/usage.ts
  • services/runner/src/tracing/otel.ts
  • services/runner/tests/unit/otel-usage-context-size.test.ts
  • services/runner/tests/unit/sandbox-agent-usage.test.ts

Comment on lines 1457 to 1465
const cost = update.cost?.amount;
const total = update.used;
if (typeof cost !== "number") return;
usage = {
input: usage?.input ?? 0,
output: usage?.output ?? 0,
total: typeof total === "number" ? total : usage?.total ?? 0,
cost: typeof cost === "number" ? cost : usage?.cost ?? 0,
total: usage?.total ?? 0,
cost,
};
record({ type: "usage", ...usage });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Skip all-zero usage_update records.

When cost.amount is 0 and no token split exists, Line 1458 accepts the cost and Lines 1459-1465 emit an all-zero usage event. This contradicts the requirement to skip measurements with neither tokens nor cost.

Build the next usage value first. Record it only when its total tokens or cost is positive. Add a regression test for contextSizeUpdate(63369, 0).

Proposed fix
 const cost = update.cost?.amount;
 if (typeof cost !== "number") return;
-usage = {
+const nextUsage = {
   input: usage?.input ?? 0,
   output: usage?.output ?? 0,
   total: usage?.total ?? 0,
   cost,
 };
+if (nextUsage.total <= 0 && nextUsage.cost <= 0) return;
+usage = nextUsage;
 record({ type: "usage", ...usage });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cost = update.cost?.amount;
const total = update.used;
if (typeof cost !== "number") return;
usage = {
input: usage?.input ?? 0,
output: usage?.output ?? 0,
total: typeof total === "number" ? total : usage?.total ?? 0,
cost: typeof cost === "number" ? cost : usage?.cost ?? 0,
total: usage?.total ?? 0,
cost,
};
record({ type: "usage", ...usage });
const cost = update.cost?.amount;
if (typeof cost !== "number") return;
const nextUsage = {
input: usage?.input ?? 0,
output: usage?.output ?? 0,
total: usage?.total ?? 0,
cost,
};
if (nextUsage.total <= 0 && nextUsage.cost <= 0) return;
usage = nextUsage;
record({ type: "usage", ...usage });

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-pr-5710.up.railway.app/w
Project agenta-oss-clone-spike
Image tag pr-5710-5b1e2fd
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-08-03T22:31:46.104Z

When the harness's PromptResponse carried no usage, the runner fell back to the ACP
stream's `usage_update.used` value and reported it as the run's token total. That
value is not a token count of the run. It is how full the agent's context window is
at that point in the turn. Measured on one stack: 6,041 root spans shaped
`input 0 / output 0 / total 63369`, with no cost.

A wrong number here is worse than no number, because a reader cannot tell a real
63,369-token run from a context-size artifact, and any token aggregate built on that
path is silently poisoned.

Fixing the usage merge alone would not have removed those rows. The tracer keeps its
own usage record built straight from the stream, and handing it nothing is a no-op
rather than a clear, so it kept stamping the context size on the root span anyway.
So the stream handler now drops `used` and keeps only the reported cost, and an
update carrying neither is not recorded at all.

The stamping function now also skips a usage record with no tokens and no cost. All
zeros is the absence of a measurement, not a measured zero, and asserting that a run
spent nothing is its own wrong answer.

Nothing consumes the context size. The only readers of the merged usage object are
the span attributes and the Python SDK's `record_usage`, and both treat every number
in it as a token count.

Tests: the full runner suite passes, 1,486 tests across 97 files, plus nine new cases
covering a context-size-only stream, a cost-only stream, a half-reported split, and a
normal split delivered through the harness.

Claude-Session: https://claude.ai/code/session_01RkWWQUNNzRbaB5jnCAdjYA

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 663f547b-687b-4634-ac15-c378b101fc5f

📥 Commits

Reviewing files that changed from the base of the PR and between e3cf8b3 and ab1460e.

📒 Files selected for processing (5)
  • services/runner/src/engines/sandbox_agent/usage.ts
  • services/runner/src/protocol.ts
  • services/runner/src/tracing/otel.ts
  • services/runner/tests/unit/otel-usage-context-size.test.ts
  • services/runner/tests/unit/sandbox-agent-usage.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • services/runner/src/engines/sandbox_agent/usage.ts
  • services/runner/tests/unit/sandbox-agent-usage.test.ts
  • services/runner/src/tracing/otel.ts

Comment on lines +430 to +436
/**
* INVARIANT: absent means the cost is UNKNOWN (the harness reported none); a present `0` is a
* measured zero — a free model or a fully cached turn. Consumers read presence as evidence of
* a measurement, so a producer must never substitute a zero for an absence: doing so records
* an unpriced run as a free one, which every downstream aggregate then believes.
*/
cost?: number;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate files"
fd -a 'protocol\.ts|otel\.ts' . | sed 's#^\./##'

echo
echo "protocol.ts cost lines"
cat -n services/runner/src/protocol.ts | sed -n '420,440p'

echo
echo "otel.ts stampUsage area"
wc -l services/runner/src/tracing/otel.ts
cat -n services/runner/src/tracing/otel.ts | sed -n '1130,1205p'

echo
echo "Search stampUsage and cost references"
rg -n "stampUsage|cost" services/runner/src || true

Repository: Agenta-AI/agenta

Length of output: 9875


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Behavioral probe of the relevant guard expression for absent cost.
node - <<'JS'
function guard(cost, total) {
  return cost <= 0 && total <= 0;
}
const cases = [
  { cost: undefined, total: 0 },
  { cost: 0, total: 0 },
  { cost: 1, total: 0 },
  { cost: undefined, total: undefined },
];
for (const c of cases) {
  console.log(JSON.stringify({
    cost: c.cost,
    total: c.total,
    originalGuard: guard(c.cost, c.total),
    normalizedCostGuard: (c.cost ?? 0) <= 0 && c.total <= 0,
  }));
}
JS

Repository: Agenta-AI/agenta

Length of output: 407


Normalize absent cost in stampUsage.

stampUsage checks u.total <= 0 && u.cost <= 0, but u.cost is optional. For { input: 0, output: 0, total: 0 } with no cost, undefined <= 0 is false, so the guard falls through and writes zero token attributes. Use const cost = u.cost ?? 0 for the empty-record guard, and keep the u.cost > 0 check when stamping the gen_ai.usage.cost attribute. Add a regression case for { input: 0, output: 0, total: 0 } with no cost.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Report 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