Skip to content

feat(a2a): emit aggregated turn token usage on the terminal status update#2332

Draft
QuentinBisson wants to merge 2 commits into
kagent-dev:mainfrom
QuentinBisson:feat/a2a-usage-total
Draft

feat(a2a): emit aggregated turn token usage on the terminal status update#2332
QuentinBisson wants to merge 2 commits into
kagent-dev:mainfrom
QuentinBisson:feat/a2a-usage-total

Conversation

@QuentinBisson

@QuentinBisson QuentinBisson commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #2303.

What

Today per-call usage only appears as adk_usage_metadata (Go) / kagent_usage_metadata (Python) on streaming events, so every consumer wanting "tokens used" must replicate ADK's partial-event filtering and sum Gemini-shaped fields client-side.

Both executors now aggregate usage across the run's non-partial ADK events (partials are skipped; each LLM call reports its usage on its final non-partial event) and attach the total to terminal status updates under kagent_usage_total:

{"promptTokenCount": 300, "candidatesTokenCount": 50, "totalTokenCount": 350, "modelVersion": "claude-sonnet-4-5"}

The value is serialized through the exact same genai UsageMetadata mapping as the per-event usage metadata (in Go via the same toA2AMetadataMap, in Python via the same serialize_metadata_value), so consumers can share one parser. thoughtsTokenCount and cachedContentTokenCount are aggregated as well; zero counts are omitted like in the per-event shape. modelVersion is the last non-empty model version seen and is omitted when none was reported.

Coverage:

  • All terminal states carry the total: completed, input-required, and failed, including failed events emitted mid-loop (Go LLM-error events, Python run-loop exceptions), so tokens burned before an error are reported too.
  • Resumed tasks (HITL input-required cycles, follow-up messages on the same task) seed the accumulator from the previously persisted Task.Metadata value, making kagent_usage_total a task-lifetime total rather than a last-segment snapshot.
  • Since the task update managers merge status-update metadata into Task.Metadata, the total is available both on the stream and on the persisted task (covers both variants proposed in the issue).

The key uses the kagent_ prefix (kagent-defined contract, not an ADK convention mirror); per-event adk_usage_metadata / kagent_usage_metadata are unchanged. Normalizing the field names of both keys away from the Gemini casing is deliberately left to a coordinated follow-up so the two keys never disagree on shape in the meantime.

Tests

Go: TestTurnUsageAggregatesNonPartialEvents, TestTurnUsageSkipsPartialAndEmptyEvents, TestTurnUsageShapeMatchesPerEventUsageMetadata, TestTurnUsageSeedFromTaskAccumulatesAcrossExecutions, TestTurnUsageSeedFromTaskIgnoresMissingOrMalformed.
Python: tests/unittests/test_turn_usage.py mirrors the same cases, including the shape-parity assertion against serialize_metadata_value.

…date

The Go executor now sums usage across the turn's non-partial ADK events and attaches the total (prompt/completion/total tokens, model id) as kagent_usage_total metadata on the terminal status update. a2a-go merges status-update metadata into Task.Metadata, so the total is available both on the stream and on the persisted task.

Fixes kagent-dev#2303

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
@github-actions github-actions Bot added the enhancement New feature or request label Jul 25, 2026
… resumed tasks

kagent_usage_total now serializes through the same genai UsageMetadata
mapping as the per-event usage metadata (camelCase, omitempty), so
consumers can share one parser; modelVersion rides alongside the counts.
Thoughts and cached-content token counts are aggregated too.

Failed terminal status updates emitted mid-loop (Go LLM-error events,
Python run-loop exceptions) now carry the tokens burned before the
error. Resumed tasks (HITL input-required cycles, follow-up messages)
seed the accumulator from the persisted task metadata, making the key a
task-lifetime total instead of a last-segment snapshot.

The Python executor now emits the same aggregate, mirroring the Go
implementation.

Signed-off-by: QuentinBisson <quentin@giantswarm.io>
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A2A: report turn-total token usage on the terminal status update

1 participant