Skip to content

feat(core): expose AI SDK usage contract in agent hooks#1372

Merged
omeraplak merged 2 commits into
mainfrom
feat/ai-sdk-usage-contract
Jul 8, 2026
Merged

feat(core): expose AI SDK usage contract in agent hooks#1372
omeraplak merged 2 commits into
mainfrom
feat/ai-sdk-usage-contract

Conversation

@omeraplak

@omeraplak omeraplak commented Jul 8, 2026

Copy link
Copy Markdown
Member

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

Agent onEnd output exposes output.usage as the aggregate run usage after #1366. That keeps provider semantics consistent, but it hides the AI SDK final-step usage contract from hook consumers. Consumers that need context-window accounting for the most recent step have to infer or reconstruct it themselves.

What is the new behavior?

Agent onEnd outputs now expose AI SDK usage semantics directly:

  • output.usage is the final-step AI SDK result.usage.
  • output.totalUsage is the aggregate AI SDK result.totalUsage.
  • output.steps carries AI SDK step results when available, including steps[].usage.

Internal observability, persistence, middleware, and guardrail metadata still use the aggregate usage path so trace/root usage remains cumulative and provider-consistent.

fixes #1368

Notes for reviewers

This intentionally changes the public onEnd.output.usage meaning to match AI SDK v6 semantics. A regression test covers a 3-step run where final-step usage is 222, aggregate usage is 508, and steps[].usage exposes each individual step.

Validation run:

  • pnpm --filter @voltagent/core typecheck
  • pnpm --filter @voltagent/core test:single src/agent/hooks/index.spec.ts src/agent/agent.spec.ts src/agent/subagent/index.spec.ts src/agent/subagent/bail.spec.ts
  • pnpm --filter @voltagent/core lint
  • pnpm exec prettier --check website/docs/agents/hooks.md website/docs/agents/overview.md website/docs/agents/context.md
  • pnpm --dir website build

The core lint command exits successfully but reports pre-existing complexity warnings in agent.ts, workflow/steps/and-agent.ts, and workspace/filesystem/backends/filesystem.ts. Docs were added under website/docs/agents/hooks.md, with related examples updated in website/docs/agents/overview.md and website/docs/agents/context.md. pnpm --dir website lint was also checked and currently fails on unrelated existing website issues (website/src/data/tweets.json formatting, PricingCalculatorModal.tsx, Testimonials.tsx, and supervisor-agent/flow-version.tsx exhaustive deps).


Summary by cubic

Expose AI SDK v6 usage semantics in agent hooks and streaming results so onEnd reports final-step usage, with aggregate and per-step details when available. Docs now show how to read final-step vs total usage in hooks and streams.

  • New Features

    • In @voltagent/core hooks, output.usage is the final-step AI SDK result.usage.
    • output.totalUsage exposes the aggregate AI SDK result.totalUsage.
    • output.steps includes AI SDK step results (with steps[].usage) when available.
    • Streaming results now expose totalUsage and steps alongside usage.
    • Updated docs to demonstrate finalStepUsage vs totalUsage in hooks and streaming examples.
  • Migration

    • If you relied on cumulative output.usage, switch to output.totalUsage.
    • Update type expectations: usage fields now use the AI SDK LanguageModelUsage shape.

Written for commit ef12f66. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Agent onEnd callbacks and stream results now include richer token-usage reporting: final-step usage, aggregate totalUsage, and steps with per-step usage when available.
    • Streaming and non-streaming text/object results now expose these fields consistently.
  • Bug Fixes
    • Updated usage reporting to follow the newer AI SDK usage contract for more accurate and consistent returned results.
  • Documentation
    • Refreshed agent and hook examples (including audit logging) to prefer output.totalUsage and detailed per-step usage.

@changeset-bot

changeset-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ef12f66

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@voltagent/core Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR updates agent onEnd and stream result usage data to expose final-step usage, aggregate totalUsage, and per-step steps. Public result types, implementations, tests, mocks, and docs are aligned to the new usage contract.

Changes

AI SDK usage contract update

Layer / File(s) Summary
Public usage types
packages/core/src/agent/types.ts, .changeset/ai-sdk-usage-contract.md
Result types switch usage to LanguageModelUsage and add optional totalUsage and steps fields; the changeset documents the new onEnd contract.
Agent usage reporting
packages/core/src/agent/agent.ts
generateText, streamText, generateObject, and streamObject now emit final-step usage, aggregate totalUsage, and steps in onEnd; streamText result wrappers expose totalUsage and steps.
Tests and mocks
packages/core/src/agent/agent.spec-d.ts, packages/core/src/agent/agent.spec.ts, packages/core/src/agent/hooks/index.spec.ts, packages/core/src/agent/subagent/test-utils.ts
Type fixtures, stream mocks, and runtime assertions were updated to match the new usage shape.
Docs and examples
website/docs/agents/context.md, website/docs/agents/hooks.md, website/docs/agents/overview.md
Examples now show finalStepUsage, totalUsage, and per-step usage fields in logging and inspection snippets.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR exposes the last step's usage separately while preserving aggregate usage, satisfying #1368's core requirement.
Out of Scope Changes check ✅ Passed The code, tests, docs, and changeset all support the usage-contract change and no unrelated scope stands out.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly summarizes the main change: exposing the AI SDK usage contract in agent hooks.
Description check ✅ Passed The description follows the template and includes checklist items, behavior changes, issue link, tests, docs, changeset, and reviewer notes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ai-sdk-usage-contract

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 8, 2026

Copy link
Copy Markdown

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: ef12f66
Status: ✅  Deploy successful!
Preview URL: https://36da4ffe.voltagent.pages.dev
Branch Preview URL: https://feat-ai-sdk-usage-contract.voltagent.pages.dev

View logs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 7 files

Re-trigger cubic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/core/src/agent/agent.ts (1)

1698-1713: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Bail-out onEnd payload omits steps, unlike every other path.

The normal-completion onEnd payloads for generateText/streamText/generateObject/streamObject all include steps, but this bail-out path does not — even though oc.systemContext.get("conversationSteps") holds the steps that occurred before the bail (used just below at recordStepResults(undefined, oc)). Consumers relying on onEnd.output.steps for context-window/usage bookkeeping in a bailed multi-step run will silently get undefined.

🐛 Suggested fix
             await this.getMergedHooks(options).onEnd?.({
               conversationId: oc.conversationId || "",
               agent: this,
               output: {
                 text: finalText,
                 usage: providerUsage,
                 totalUsage: providerUsage,
+                steps: oc.systemContext.get("conversationSteps") as
+                  | ReadonlyArray<StepResult<ToolSet>>
+                  | undefined,
                 providerResponse: undefined as any,
                 finishReason: "bail" as any,
                 warnings: undefined,
                 context: oc.context,
               },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/agent/agent.ts` around lines 1698 - 1713, The bail-out
onEnd payload in Agent’s end-of-run flow is missing steps, unlike the normal
completion paths. Update the onEnd call in agent.ts to include the conversation
steps from oc.systemContext.get("conversationSteps") in output.steps, matching
the payload shape used by generateText/streamText/generateObject/streamObject.
Use the surrounding oc, recordStepResults(undefined, oc), and
this.getMergedHooks(options).onEnd?.() code to locate the bail-out branch and
ensure the steps are passed through consistently.
.changeset/ai-sdk-usage-contract.md (1)

1-6: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Bump @voltagent/core to major

This is a breaking API change: usage changes semantics and shape, so the changeset should not be minor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/ai-sdk-usage-contract.md around lines 1 - 6, Update the changeset
for `@voltagent/core` from minor to major because the agent onEnd output contract
is changing in a breaking way: usage now means final-step usage, totalUsage is
the aggregate, and steps may be included. Adjust the changeset entry in
ai-sdk-usage-contract.md so the version bump matches this API-breaking semantics
change.
🧹 Nitpick comments (2)
packages/core/src/agent/agent.ts (2)

3666-3687: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

streamObject's returned result wrapper doesn't expose totalUsage/steps, unlike streamText's.

streamText's resultWithContext (line 2819-2824) adds totalUsage/steps getters, but streamObject's equivalent wrapper here doesn't, so callers only get these fields via the onEnd hook, not on the returned object. Given streamObject is already @deprecated, this is likely low priority to backfill.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/agent/agent.ts` around lines 3666 - 3687, The streamObject
result wrapper in agent.ts is missing the same totalUsage and steps exposure
that streamText’s resultWithContext provides. Update the streamObject wrapper so
its returned object delegates these fields via getters, alongside the existing
partialObjectStream/textStream/warnings/usage/finishReason properties, using the
same pattern as the streamText wrapper and keeping the implementation anchored
around resultWithContext and StreamObjectResultWithContext.

3125-3127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Repeated unsafe as {...} casts for totalUsage/steps — consider a shared type.

generateObject/streamObject results are repeatedly cast inline ((result as { totalUsage?: LanguageModelUsage }).totalUsage, (finalResult as { steps?: ReadonlyArray<StepResult<ToolSet>> }).steps) because the underlying AI SDK result types don't natively expose these fields. This bypasses structural type-checking in 4+ places and duplicates the same ad-hoc shape.

As per coding guidelines, "Maintain type safety in TypeScript-first codebase."

♻️ Suggested consolidation
+type ResultWithUsageMeta<T> = T & {
+  totalUsage?: LanguageModelUsage;
+  steps?: ReadonlyArray<StepResult<ToolSet>>;
+};

Then reuse ResultWithUsageMeta<typeof result> / ResultWithUsageMeta<typeof finalResult> at each of the 4 call sites instead of repeating the inline cast shape.

Also applies to: 3560-3562

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/agent/agent.ts` around lines 3125 - 3127, The repeated
inline unsafe casts for `totalUsage` and `steps` in `agent.ts` should be
replaced with a shared type to preserve TypeScript safety and avoid duplicating
ad-hoc shapes. Introduce and reuse a helper like `ResultWithUsageMeta<typeof
result>` / `ResultWithUsageMeta<typeof finalResult>` at the `generateObject` and
`streamObject` result handling sites, including the `result` and `finalResult`
assignments, so the same metadata fields are accessed through a consistent typed
shape rather than repeated `(result as {...})` casts.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In @.changeset/ai-sdk-usage-contract.md:
- Around line 1-6: Update the changeset for `@voltagent/core` from minor to major
because the agent onEnd output contract is changing in a breaking way: usage now
means final-step usage, totalUsage is the aggregate, and steps may be included.
Adjust the changeset entry in ai-sdk-usage-contract.md so the version bump
matches this API-breaking semantics change.

In `@packages/core/src/agent/agent.ts`:
- Around line 1698-1713: The bail-out onEnd payload in Agent’s end-of-run flow
is missing steps, unlike the normal completion paths. Update the onEnd call in
agent.ts to include the conversation steps from
oc.systemContext.get("conversationSteps") in output.steps, matching the payload
shape used by generateText/streamText/generateObject/streamObject. Use the
surrounding oc, recordStepResults(undefined, oc), and
this.getMergedHooks(options).onEnd?.() code to locate the bail-out branch and
ensure the steps are passed through consistently.

---

Nitpick comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 3666-3687: The streamObject result wrapper in agent.ts is missing
the same totalUsage and steps exposure that streamText’s resultWithContext
provides. Update the streamObject wrapper so its returned object delegates these
fields via getters, alongside the existing
partialObjectStream/textStream/warnings/usage/finishReason properties, using the
same pattern as the streamText wrapper and keeping the implementation anchored
around resultWithContext and StreamObjectResultWithContext.
- Around line 3125-3127: The repeated inline unsafe casts for `totalUsage` and
`steps` in `agent.ts` should be replaced with a shared type to preserve
TypeScript safety and avoid duplicating ad-hoc shapes. Introduce and reuse a
helper like `ResultWithUsageMeta<typeof result>` / `ResultWithUsageMeta<typeof
finalResult>` at the `generateObject` and `streamObject` result handling sites,
including the `result` and `finalResult` assignments, so the same metadata
fields are accessed through a consistent typed shape rather than repeated
`(result as {...})` casts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 626b0c92-b0dc-4648-ba61-b395b4c02523

📥 Commits

Reviewing files that changed from the base of the PR and between 9a241f0 and 3da9798.

📒 Files selected for processing (7)
  • .changeset/ai-sdk-usage-contract.md
  • packages/core/src/agent/agent.spec-d.ts
  • packages/core/src/agent/agent.spec.ts
  • packages/core/src/agent/agent.ts
  • packages/core/src/agent/hooks/index.spec.ts
  • packages/core/src/agent/subagent/test-utils.ts
  • packages/core/src/agent/types.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@website/docs/agents/hooks.md`:
- Around line 625-632: The per-step usage example in the hooks docs assumes
every entry from output.steps has usage, which can throw when a step omits it.
Update the example around finalStepUsage, aggregateUsage, and perStepUsage so it
safely handles optional step usage by filtering or guarding before reading
usage.totalTokens, and keep the logging example aligned with the
output.steps?.map usage contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6f48b1d2-2acf-4fe1-9046-ff07367009e1

📥 Commits

Reviewing files that changed from the base of the PR and between 3da9798 and ef12f66.

📒 Files selected for processing (3)
  • website/docs/agents/context.md
  • website/docs/agents/hooks.md
  • website/docs/agents/overview.md
✅ Files skipped from review due to trivial changes (1)
  • website/docs/agents/context.md

Comment on lines +625 to +632
const finalStepUsage = output.usage;
const aggregateUsage = output.totalUsage;
const perStepUsage = output.steps?.map((step) => step.usage);

console.log({
finalStepTokens: finalStepUsage?.totalTokens,
aggregateTokens: aggregateUsage?.totalTokens,
stepTokens: perStepUsage?.map((usage) => usage.totalTokens),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the per-step usage example against missing data.

output.steps is optional, and the new contract only guarantees steps[].usage when it is available. As written, usage.totalTokens will throw if any step omits usage.

Proposed fix
-    const perStepUsage = output.steps?.map((step) => step.usage);
+    const perStepUsage = output.steps?.flatMap((step) => (step.usage ? [step.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 finalStepUsage = output.usage;
const aggregateUsage = output.totalUsage;
const perStepUsage = output.steps?.map((step) => step.usage);
console.log({
finalStepTokens: finalStepUsage?.totalTokens,
aggregateTokens: aggregateUsage?.totalTokens,
stepTokens: perStepUsage?.map((usage) => usage.totalTokens),
const finalStepUsage = output.usage;
const aggregateUsage = output.totalUsage;
const perStepUsage = output.steps?.flatMap((step) => (step.usage ? [step.usage] : []));
console.log({
finalStepTokens: finalStepUsage?.totalTokens,
aggregateTokens: aggregateUsage?.totalTokens,
stepTokens: perStepUsage?.map((usage) => usage.totalTokens),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/docs/agents/hooks.md` around lines 625 - 632, The per-step usage
example in the hooks docs assumes every entry from output.steps has usage, which
can throw when a step omits it. Update the example around finalStepUsage,
aggregateUsage, and perStepUsage so it safely handles optional step usage by
filtering or guarding before reading usage.totalTokens, and keep the logging
example aligned with the output.steps?.map usage contract.

@omeraplak omeraplak merged commit 4a3100b into main Jul 8, 2026
23 checks passed
@omeraplak omeraplak deleted the feat/ai-sdk-usage-contract branch July 8, 2026 13:25
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: Expose lastStepUsage in onEnd hook for context window calculation

1 participant