feat(a2a): emit aggregated turn token usage on the terminal status update - #2461
Open
QuentinBisson wants to merge 2 commits into
Open
feat(a2a): emit aggregated turn token usage on the terminal status update#2461QuentinBisson wants to merge 2 commits into
QuentinBisson wants to merge 2 commits into
Conversation
QuentinBisson
force-pushed
the
feat/a2a-usage-total-v2
branch
from
August 17, 2026 13:40
90d57f0 to
c8981c7
Compare
QuentinBisson
marked this pull request as ready for review
August 17, 2026 14:00
Copilot stopped reviewing on behalf of
QuentinBisson due to an error
August 17, 2026 14:01
…date Both runtimes accumulate non-partial per-call token usage across an execution and stamp it on the terminal status update (completed, input-required, failed) under kagent_usage_total. Resumed tasks seed the accumulator from the task metadata, so the total covers the whole task rather than the last segment. The value uses the same genai UsageMetadata serialization as the per-event adk_usage_metadata / kagent_usage_metadata entries, plus modelVersion. Signed-off-by: QuentinBisson <quentin@giantswarm.io>
QuentinBisson
force-pushed
the
feat/a2a-usage-total-v2
branch
from
August 17, 2026 19:01
c8981c7 to
a8d0c31
Compare
Contributor
Author
|
Rebased onto The three remaining red checks are not from this branch, they reproduce identically on
|
The Python accumulator ran from the A2A after-event interceptor, which only fires for ADK events the converter turns into at least one A2A event. The event carrying the usage of the call that pauses a task for input has its long-running function call stripped before conversion and produces no A2A event, so its tokens were never counted. Accumulate from an ADK runner plugin instead, matching the Go vantage point. Failures raised outside the upstream executor publish their own terminal event, which now carries the total like the other terminal states. Derive totalTokenCount when no provider reported one, so Anthropic agents stop reporting a zero total next to non-zero counts.
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.
Supersedes #2332, which the stale bot closed and GitHub refuses to reopen. This is a reimplementation, not a rebase: both executors were rewritten to delegate to the ADK library's own A2A executor, so the manual event loop the original PR hooked into no longer exists.
What this adds
Every terminal status update (
completed,input-required,failed) carries akagent_usage_totalmetadata entry with the token usage summed across the task:{ "promptTokenCount": 300, "candidatesTokenCount": 50, "totalTokenCount": 350, "modelVersion": "gpt-4o-2024-11-20" }Partial (streaming chunk) events are skipped, since each LLM call reports its usage on the final non-partial event. Resumed tasks seed the accumulator from
Task.metadata, so a task that spans several executions (HITL input-required cycles, follow-up messages) reports a task-lifetime total instead of the last segment only. The value is a running total re-emitted on each terminal update, so consumers take the latest value rather than summing across executions.The value is serialized with the same genai
UsageMetadatamapping as the existing per-eventadk_usage_metadata/kagent_usage_metadataentries, plusmodelVersion, so consumers can share one parser (with the caveat thatmodelVersionis not a field of the genai type). Tests in both languages assert that equality directly.totalTokenCountis derived fromprompt + candidates + thoughtswhen no provider reported one. The Anthropic models report per-call input and output counts without a total, and a zero total next to non-zero counts reads as "no tokens were used".a2a-goanda2a-pythonboth merge terminal status-update metadata into the stored task, which is how the total is persisted and how the next execution reads it back.How it differs from #2332
RunnerProviderthat wraps the ADK runner instead ofAfterEventCallback. The callback only fires for ADK events that convert to an A2A artifact, and the input-required processor consumes the paused tool-call event, which is exactly the event carrying the usage of the call that paused. Wrapping the runner keeps the original vantage point: every ADK event, before conversion. The provider mirrors the ADK default provider (it has to, sinceRunnerProviderreplacesRunnerConfig), and a test reflects overadka2a.RunnerConfigso a field added upstream fails the build instead of being silently dropped. Stamping uses the newAfterExecuteCallbackhook, which covers all three terminal states in one place.on_event_callback), for the same reason. The A2A after-event interceptor only runs for ADK events the converter turns into at least one A2A event, and the event whose long-running function call is stripped before conversion produces none. The plugin sees every ADK event the runner produces, matching the Go vantage point.Known limits
toolUsePromptTokenCountis not aggregated. It is insidetotalTokenCount, so the total stays correct, but the breakdown fields do not sum to it on Vertex.modelVersionis last-writer-wins over summed counts, so a task that uses more than one model reports a total that cannot be split back into per-model cost.Verification
go build ./adk/... ./core/...,go test ./adk/...,go vet,gofmtuv run pytest packages/kagent-adk/tests/unittests(390 passed) - the 9 failures inmodels/test_tls_e2e.pyare pre-existing in this environment (verified against a clean checkout) and unrelated