Skip to content

feat: support streaming tool outputs by returning an AsyncIterable fr…#949

Merged
omeraplak merged 2 commits into
mainfrom
feat/support-streaming-tool
Jan 15, 2026
Merged

feat: support streaming tool outputs by returning an AsyncIterable fr…#949
omeraplak merged 2 commits into
mainfrom
feat/support-streaming-tool

Conversation

@omeraplak

@omeraplak omeraplak commented Jan 15, 2026

Copy link
Copy Markdown
Member

…om execute

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

What is the new behavior?

fixes (issue)

Notes for reviewers


Summary by cubic

Added support for streaming tool outputs. Tools can now return an AsyncIterable from execute to emit progress updates before the final result.

  • New Features

    • Tool API: execute may return an AsyncIterable via the new ToolExecutionResult type.
    • Agent: detects async generators/iterables, validates each yield, and treats the last value as final.
    • Observability: spans and onToolStart/onToolEnd hooks handle streaming lifecycle and errors.
    • Examples and docs: weather tool updated; new docs section on streaming tool results.
  • Migration

    • No breaking changes; tools returning promises or plain values still work.
    • To stream, yield intermediate values and use an output schema that covers preliminary and final states (e.g., a discriminated union).

Written for commit 0319ac7. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Tools now support streaming intermediate results during execution, enabling progressive/loading updates before a final result.
  • Documentation

    • Added docs and examples demonstrating how to emit status-based (loading → success) updates from tools and how clients should interpret preliminary vs final yields.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot

changeset-bot Bot commented Jan 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0319ac7

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 Patch

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 Jan 15, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds streaming tool execution: tools' execute can now return AsyncIterable (yielding intermediate results) in addition to Promise/sync values; agent and tooling adapted to iterate, validate, and surface progress before final result.

Changes

Cohort / File(s) Summary
Core Types
packages/core/src/tool/index.ts
Added ToolExecutionResult<T> = `PromiseLike
Tool Manager Types
packages/core/src/tool/manager/ToolManager.ts, packages/core/src/tool/manager/...
Updated createToolExecuteFunction return type and ManagedTool.execute to use ToolExecutionResult<any>; imported ToolExecutionResult.
Agent Implementation
packages/core/src/agent/agent.ts
Refactored tool execution factory to support async generators and async iterables; added asyncGeneratorFunction, isAsyncGeneratorFunction, isAsyncIterable; introduced handleToolSuccess/handleToolError; iterates yields, validates each, and invokes hooks/span lifecycle for progress and final result.
Test Utilities
packages/core/src/agent/test-utils.ts
createMockTool execute parameter broadened to accept `PromiseLike
Tests / Specs
packages/core/src/tool/manager/index.spec.ts
Tests/types updated to reference new ToolExecutionResult return type for prepared tool execution shapes.
Example Tool
examples/with-tools/src/tools/weather.ts
Converted execute to an async generator yielding a loading state then success; changed outputSchema to a discriminated union (`status: "loading"
Docs
website/docs/agents/tools.md
Added "Streaming Tool Results (Preliminary)" guidance and TypeScript example demonstrating AsyncIterable yields and discriminated output schema.
Changeset
.changeset/upset-shrimps-guess.md
Documented public API change: ToolDefinition/Tool.execute now supports streaming/AsyncIterable outputs.

Sequence Diagram(s)

sequenceDiagram
    participant Agent
    participant Tool
    participant Validation
    participant Hooks

    Agent->>Tool: execute(params)
    alt Tool returns AsyncIterable / async generator
        Tool-->>Agent: yield {status: "loading", ...}
        Agent->>Validation: validate(intermediate)
        Validation-->>Agent: valid
        Agent->>Hooks: onToolProgress(intermediate)
        Tool-->>Agent: yield {status: "success", data}
        Agent->>Validation: validate(final)
        Validation-->>Agent: valid
    else Tool returns PromiseLike / value
        Tool-->>Agent: resolve result
        Agent->>Validation: validate(result)
        Validation-->>Agent: valid
    end
    Agent->>Hooks: onToolEnd(finalResult or error)
    Agent->>Agent: span.end()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I hopped along the streaming trail,
Yielding glimpses, small and pale;
From "loading..." to the final cheer,
Little updates draw us near.
A rabbit's wink — progress is here!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description includes required sections (PR Checklist, Bugs/Features, current/new behavior) but all checklist items are unchecked and the 'What is the current behavior?' and 'What is the new behavior?' sections are incomplete. However, an auto-generated summary by cubic provides substantial context about the feature, implementation, migration path, and updates to examples/docs. Complete the PR checklist by checking applicable items and fill in the 'What is the current behavior?' and 'What is the new behavior?' sections with explicit details to clarify the change.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main feature: support for streaming tool outputs via AsyncIterable return from execute. It is concise, specific, and directly reflects the primary change.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@ghost

This comment has been minimized.

@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

🤖 Fix all issues with AI agents
In `@packages/core/src/agent/agent.ts`:
- Around line 3688-3747: The finally block in the async generator branch of the
execute wrapper redundantly calls oc.traceContext.endChildSpan(toolSpan,
"completed", {}) even though handleToolSuccess and handleToolError already end
the span; remove the endChildSpan call from the finally of the async-generator
path (the function created where execute && isAsyncGeneratorFunction(execute) is
handled) so the span is ended only in handleToolSuccess/handleToolError, or if
the tracing API is guaranteed idempotent leave the call but add a clear comment
next to oc.traceContext.endChildSpan referencing handleToolSuccess and
handleToolError to document why duplicate end calls are safe.
🧹 Nitpick comments (3)
packages/core/src/agent/test-utils.ts (1)

363-371: Consider importing ToolExecutionResult for type consistency.

The inline type PromiseLike<any> | AsyncIterable<any> | any duplicates the ToolExecutionResult<any> type. Using the canonical type would maintain consistency and reduce drift.

♻️ Suggested change
+import type { ToolExecutionResult } from "../tool";
 // ... existing imports ...

 export function createMockTool(
   name: string,
-  execute?: (params: any, options?: any) => PromiseLike<any> | AsyncIterable<any> | any,
+  execute?: (params: any, options?: any) => ToolExecutionResult<any>,
   options?: {
examples/with-tools/src/tools/weather.ts (1)

4-22: Well-designed discriminated union for streaming states.

The schema effectively models the loading/success states for streaming tool output. Minor note: on line 9, z.undefined().optional() could be simplified to just z.undefined() since the field is already typed as undefined.

♻️ Optional simplification
   z.object({
     status: z.literal("loading"),
     text: z.string(),
-    weather: z.undefined().optional(),
+    weather: z.undefined(),
   }),
packages/core/src/tool/manager/index.spec.ts (1)

234-257: Consider adding tests for AsyncIterable execution path.

The type annotations have been updated to support ToolExecutionResult<any>, but the tests only exercise the Promise path via mockResolvedValue. Consider adding a test case that verifies the manager correctly handles an async generator function to ensure the new streaming capability is covered.

Run the following script to check if async generator tests exist elsewhere:

#!/bin/bash
# Search for async generator tests related to tool execution
rg -n --type=ts -C3 'async\s*\*|AsyncIterable|yield.*status' packages/core/src
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64f9c8c and 1ebe40b.

📒 Files selected for processing (8)
  • .changeset/upset-shrimps-guess.md
  • examples/with-tools/src/tools/weather.ts
  • packages/core/src/agent/agent.ts
  • packages/core/src/agent/test-utils.ts
  • packages/core/src/tool/index.ts
  • packages/core/src/tool/manager/ToolManager.ts
  • packages/core/src/tool/manager/index.spec.ts
  • website/docs/agents/tools.md
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: Maintain type safety in TypeScript-first codebase
Never use JSON.stringify; use the safeStringify function instead, imported from @voltagent/internal

Files:

  • packages/core/src/agent/test-utils.ts
  • packages/core/src/tool/index.ts
  • packages/core/src/tool/manager/ToolManager.ts
  • packages/core/src/agent/agent.ts
  • packages/core/src/tool/manager/index.spec.ts
  • examples/with-tools/src/tools/weather.ts
🧬 Code graph analysis (4)
packages/core/src/agent/test-utils.ts (1)
examples/with-tools/src/tools/weather.ts (1)
  • execute (37-63)
packages/core/src/tool/index.ts (1)
packages/core/src/agent/providers/base/types.ts (2)
  • ToolExecuteOptions (266-287)
  • ToolSchema (245-245)
packages/core/src/tool/manager/ToolManager.ts (1)
packages/core/src/tool/index.ts (2)
  • ToolExecutionResult (12-12)
  • AgentTool (41-41)
packages/core/src/tool/manager/index.spec.ts (2)
packages/core/src/agent/providers/base/types.ts (1)
  • ToolExecuteOptions (266-287)
packages/core/src/tool/index.ts (1)
  • ToolExecutionResult (12-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Test core
  • GitHub Check: Test voice
  • GitHub Check: Build (Node 20)
  • GitHub Check: Build (Node 24)
  • GitHub Check: Build (Node 22)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (15)
packages/core/src/tool/index.ts (3)

12-13: Well-designed type for streaming support.

The ToolExecutionResult<T> union type elegantly supports all three execution patterns: synchronous return, promise-based, and async iterable streaming. This provides flexibility for tool implementations while maintaining type safety.


139-144: LGTM - Consistent signature update for ToolOptions.execute.

The return type and JSDoc are properly updated to reflect the new streaming capability.


211-216: LGTM - Consistent signature update for Tool.execute.

The Tool class's execute method signature mirrors the ToolOptions type, maintaining API consistency.

packages/core/src/tool/manager/ToolManager.ts (2)

5-5: LGTM - Correct import of ToolExecutionResult.


48-61: LGTM - Type signatures properly updated for streaming support.

The createToolExecuteFunction and ManagedTool.execute types are correctly updated to use ToolExecutionResult<any>, maintaining consistency with the core type definition.

packages/core/src/agent/agent.ts (4)

323-337: LGTM - Utility functions for async generator detection.

The isAsyncGeneratorFunction and isAsyncIterable type guards are correctly implemented for detecting streaming tools.


3640-3686: Well-structured helper functions for tool lifecycle management.

The handleToolSuccess and handleToolError helpers centralize span management and hook invocation, improving code organization and ensuring consistent behavior across both generator and non-generator paths.


3766-3772: Good handling of AsyncIterable results in non-generator path.

This correctly drains an async iterable to extract the final value when a tool returns an AsyncIterable but wasn't detected as an async generator function.


3749-3785: Verify span ending consistency in non-generator path.

Similar to the generator path, the finally block at line 3783 calls endChildSpan after handleToolSuccess or handleToolError have already ended the span. Ensure this is intentional and safe.

#!/bin/bash
# Description: Check if endChildSpan is idempotent or if double-ending causes issues
# Search for the endChildSpan implementation to understand its behavior

ast-grep --pattern $'endChildSpan($_, $_, $_) {
  $$$
}'

# Also search for how spans are ended
rg -n "endChildSpan|toolSpan.end" --type=ts -C2
examples/with-tools/src/tools/weather.ts (1)

37-63: Good example of async generator tool implementation.

This effectively demonstrates the streaming tool pattern - yielding a loading state immediately, then yielding the final success state after the async operation completes. This provides a clear reference implementation for users.

website/docs/agents/tools.md (1)

49-99: Well-documented streaming tool results section.

The documentation clearly explains the streaming pattern with:

  • Clear explanation that each yield emits a preliminary result and the last yield is the final result
  • Proper use of discriminated union for type-safe loading/success states
  • Realistic example showing the async generator pattern
.changeset/upset-shrimps-guess.md (1)

1-29: LGTM - Clear changeset description.

The changeset correctly describes the new streaming feature with a minimal working example. The simplified example (without outputSchema) is appropriate for a changeset since it demonstrates the core async generator pattern concisely.

packages/core/src/tool/manager/index.spec.ts (3)

3-10: Import updated correctly to include ToolExecutionResult.

The import statement properly includes the new ToolExecutionResult type alongside existing exports.


241-257: Type annotation correctly updated for ToolExecutionResult.

The execute wrapper type is properly updated to ToolExecutionResult<any>, aligning with the new union type that supports PromiseLike<T> | AsyncIterable<T> | T.


269-279: Client-side tool type annotation consistent with server-side.

The type annotation update is correctly applied to the client-side tool entry as well, maintaining consistency.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread packages/core/src/agent/agent.ts
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jan 15, 2026

Copy link
Copy Markdown

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0319ac7
Status: ✅  Deploy successful!
Preview URL: https://59eeea2f.voltagent.pages.dev
Branch Preview URL: https://feat-support-streaming-tool.voltagent.pages.dev

View logs

@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: 0

Caution

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

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

3747-3784: Duplicate span ending persists in non-generator path.

The finally block at line 3781 calls endChildSpan(toolSpan, "completed", {}), but the span is already ended within handleToolSuccess (line 3643) or handleToolError (line 3670). This causes double span ending for the non-generator execution path.

The PR's "fix: duplicate endSpan" commit addressed the async generator path by removing its finally block, but the same pattern remains in this path.

🐛 Suggested fix - remove redundant span ending
         return result;
       } catch (e) {
         return await handleToolError(e);
-      } finally {
-        // End the span if it was created
-        oc.traceContext.endChildSpan(toolSpan, "completed", {});
       }
     });
   };
♻️ Duplicate comments (1)
packages/core/src/agent/agent.ts (1)

3688-3745: Duplicate span ending issue has been fixed.

The finally block that was causing duplicate endChildSpan calls (flagged in previous review) has been removed. The span is now correctly ended only within handleToolSuccess or handleToolError.

🧹 Nitpick comments (1)
packages/core/src/agent/agent.ts (1)

323-337: Async generator detection may miss transpiled generators.

The isAsyncGeneratorFunction check relies on constructor identity, which works for native async generators but may fail for transpiled code from older TypeScript targets or third-party libraries. However, the code has defensive fallback logic: if the check fails, it executes as a regular async function and then checks isAsyncIterable(result), which correctly handles transpiled async generators. The optimization of the async function* wrapper would be bypassed, but functionality remains intact.

Since this codebase targets ES2022, transpiled async generators are unlikely to be encountered. Consider this refactor only if external tools from legacy builds become a common pattern.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ebe40b and 0319ac7.

📒 Files selected for processing (1)
  • packages/core/src/agent/agent.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: Maintain type safety in TypeScript-first codebase
Never use JSON.stringify; use the safeStringify function instead, imported from @voltagent/internal

Files:

  • packages/core/src/agent/agent.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Test cli
  • GitHub Check: Test server-core
  • GitHub Check: Test core
  • GitHub Check: Build (Node 24)
  • GitHub Check: Build (Node 20)
  • GitHub Check: Build (Node 22)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (3)
packages/core/src/agent/agent.ts (3)

53-53: LGTM!

The ToolExecutionResult import is correctly added to support the new streaming tool execution return type.


3599-3639: LGTM!

The signature update from Promise<any> to ToolExecutionResult<any> correctly reflects that tool execution can now return either a Promise or an AsyncGenerator for streaming results.


3640-3686: LGTM on helper extraction.

The handleToolSuccess and handleToolError helpers are well-structured and properly encapsulate span lifecycle management and hook invocation.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

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.

1 participant