Skip to content

feat(go-sdk): add token usage to RUN_FINISHED and RUN_ERROR - #2685

Merged
mattsp1290 merged 1 commit into
ag-ui-protocol:mainfrom
newtonnthiga:feat/go-token-usage
Sep 8, 2026
Merged

mattsp1290 merged 1 commit into
ag-ui-protocol:mainfrom
newtonnthiga:feat/go-token-usage

Conversation

@newtonnthiga

Copy link
Copy Markdown
Contributor

Fixes #2684

Ports the TokenUsage type and the usage field that TypeScript, Python and .NET already ship. The Go SDK had zero occurrences of either, so a Go agent could not report what a run cost and usage a peer SDK sent was dropped on decode.

Third in the same series, after #2678 (metadata and subagent events) and #2683 (RunFinished outcome guards). Rebased onto current main, so it applies cleanly on top of both.

What lands

  • TokenUsage — provider, model, inputTokens, outputTokens, totalTokens, reasoningTokens, cachedInputTokens. Numeric counts and labels only, with no content-bearing or identifying fields, matching the peer SDKs' stated reason: usage feeds anonymous telemetry.
  • usage []TokenUsage on RunFinishedEvent and RunErrorEvent, with WithUsage and WithErrorUsage options. A list, so a run invoking several models keeps one entry per (provider, model); RUN_ERROR carries the partial usage of a run that failed after one or more model calls completed.
  • Validate() rejects a negative count, naming the offending entry and field.

Decisions worth a reviewer's eye

  • Counts are *int64, not plain integers. Zero is meaningful data: a call that produced no output tokens is not the same as a call that did not report the figure, and omitempty on a plain int erases that distinction. int64 matches the protobuf int64 and .NET long? bindings. There is a TokenCount(int64) *int64 helper so populating one does not need a temporary variable at every call site.
  • Negative counts are rejected in Validate() rather than clamped or ignored. TypeScript declares every count z.number().int().nonnegative(), and the protobuf transport writes them through an int64 writer that throws on a bad value — so catching it at the source beats a rejected event, or a mid-stream crash, at the receiver. This follows the same reasoning as fix(go-sdk): stop RunFinished emitting an outcome the peer SDKs reject #2683's interrupt-outcome check.
  • validateUsage is shared between the two events rather than duplicated, and names the event in the error so messages match the rest of the package (RunFinishedEvent validation failed: usage[1]: ...).
  • No aggregation or vendor mappers. Python has a whole token_usage.py of those; this PR ports only the protocol surface. Mapping helpers can follow if there is appetite for them.

Testing

go test ./... passes, and gofmt/go vet are clean on every touched file. (events.go and reasoning_events.go carry pre-existing gofmt drift in a const block, untouched here rather than mixing an unrelated reformat into the diff.)

Beyond the unit tests, wire compatibility was verified against the real peer parsers rather than by inspection — the TypeScript zod schemas in sdks/typescript/packages/core/src and the Python pydantic models in ag_ui.core, run over the exact bytes Go emits:

shape TypeScript Python
RUN_FINISHED with two full usage entries accepted, nothing stripped accepted, preserved
RUN_ERROR with partial usage accepted, nothing stripped accepted, preserved
RUN_FINISHED with no usage accepted, no usage key accepted, no usage key

"Nothing stripped" is the check that matters here: TokenUsageSchema drops unrecognized keys silently, so a near-miss on field names would lose the counts with no error. Comparing what zod returns against what Go sent confirms every field survives.

No breaking changes: usage is optional and omitted when absent, so every existing event serializes exactly as before.

Ports the TokenUsage type and the usage field that TypeScript, Python
and .NET already ship. Usage is a list, one entry per (provider, model),
so a run that invokes several models keeps them separate; consumers that
only need totals sum across entries. It lives on the terminal event
rather than per-event, and RUN_ERROR carries the partial usage of a run
that failed after one or more model calls completed.

Counts are pointers because zero is meaningful data: a call that
produced no output tokens is not the same as a call that did not report
the figure. int64 matches the protobuf int64 and .NET long? bindings.

The type deliberately carries no content-bearing or identifying fields,
only provider and model labels and numeric counts, because usage feeds
anonymous telemetry.

Validate rejects a negative count. The peer SDKs constrain every count
to a non-negative integer, and the protobuf transport writes them
through an int64 writer, so catching it at the source beats a rejected
event or a mid-stream crash at the receiver.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1788888863 published to TestPyPI.

Warning: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production.

Install with uv

Add the TestPyPI index to your pyproject.toml:

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = true

Then install the packages you need:

# Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1788888863' --index testpypi

# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1788888863' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1788888863' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1788888863' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1788888863' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1788888863' --index testpypi

Install with pip

pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  ag-ui-protocol==0.0.0.dev1788888863

Use --extra-index-url https://pypi.org/simple/ so pip can resolve
transitive dependencies (pydantic, fastapi, etc.) from real PyPI.


Commit: 2d87c6b

@pkg-pr-new

pkg-pr-new Bot commented Sep 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@2685

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@2685

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@2685

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@2685

@ag-ui/mcp-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-middleware@2685

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@2685

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@2685

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@2685

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@2685

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@2685

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@2685

@ag-ui/claude-managed-agents

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-managed-agents@2685

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@2685

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@2685

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@2685

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@2685

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@2685

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@2685

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@2685

@ag-ui/watsonx

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/watsonx@2685

@ag-ui/a2ui-toolkit

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-toolkit@2685

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@2685

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@2685

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@2685

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@2685

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@2685

commit: 6be23d3

@mattsp1290 mattsp1290 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lgtm!

@mattsp1290
mattsp1290 merged commit 393c20c into ag-ui-protocol:main Sep 8, 2026
11 checks passed
@newtonnthiga
newtonnthiga deleted the feat/go-token-usage branch September 8, 2026 17:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Go SDK has no token usage — add TokenUsage and the usage field

2 participants