feat(a2a): emit aggregated turn token usage on the terminal status update#2332
Draft
QuentinBisson wants to merge 2 commits into
Draft
feat(a2a): emit aggregated turn token usage on the terminal status update#2332QuentinBisson wants to merge 2 commits into
QuentinBisson wants to merge 2 commits into
Conversation
…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>
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
UsageMetadatamapping as the per-event usage metadata (in Go via the sametoA2AMetadataMap, in Python via the sameserialize_metadata_value), so consumers can share one parser.thoughtsTokenCountandcachedContentTokenCountare aggregated as well; zero counts are omitted like in the per-event shape.modelVersionis the last non-empty model version seen and is omitted when none was reported.Coverage:
completed,input-required, andfailed, including failed events emitted mid-loop (Go LLM-error events, Python run-loop exceptions), so tokens burned before an error are reported too.Task.Metadatavalue, makingkagent_usage_totala task-lifetime total rather than a last-segment snapshot.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-eventadk_usage_metadata/kagent_usage_metadataare 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.pymirrors the same cases, including the shape-parity assertion againstserialize_metadata_value.