Skip to content

fix: complete tool calls with server results#596

Merged
AlemTuzlak merged 9 commits into
mainfrom
issue-176-server-tool-results
May 22, 2026
Merged

fix: complete tool calls with server results#596
AlemTuzlak merged 9 commits into
mainfrom
issue-176-server-tool-results

Conversation

@AlemTuzlak
Copy link
Copy Markdown
Contributor

@AlemTuzlak AlemTuzlak commented May 20, 2026

Summary

  • mark successful tool-call parts as complete when output is attached
  • add complete to mirrored ToolCallState exports
  • add unit assertions and an e2e regression for server/client tool result output

Closes #176.

Verification

  • pnpm.cmd --filter @tanstack/ai exec vitest run tests/stream-processor.test.ts tests/message-updaters.test.ts
  • git diff --check

Notes

  • Focused Playwright e2e was attempted, but the existing tools-test route currently returns HTTP 400 before tool calls; the pre-existing server/client e2e test fails the same way locally.

Summary by CodeRabbit

  • New Features

    • Tool calls now transition to a complete state when results are available
    • Server-executed tool results are automatically populated to matching tool-call entries
  • Tests

    • Added comprehensive tests validating tool result handling and state transitions
    • Added E2E tests for server/client tool sequencing and history hydration
  • Documentation

    • Added example demonstrating server tool-result hydration

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements the fix for Issue #176 by adding a terminal 'complete' state to ToolCallState, updating stream processors to populate tool-call outputs from server-side tool results, and extending test coverage to validate the new behavior across unit and E2E scenarios.

Changes

Tool call complete state lifecycle

Layer / File(s) Summary
Tool call state type definitions
packages/typescript/ai/src/types.ts, packages/typescript/ai-client/src/types.ts, packages/typescript/ai-event-client/src/index.ts
ToolCallState union extended with 'complete' terminal state across all packages.
Stream processing and message merging
packages/typescript/ai/src/activities/chat/stream/message-updaters.ts, packages/typescript/ai/src/activities/chat/stream/processor.ts, packages/typescript/ai/src/activities/chat/messages.ts
updateToolCallWithOutput defaults to 'complete' when no error; areAllToolsComplete recognizes 'complete' as terminal; message merge logic parses tool-result content as JSON and populates tool-call.output, marks parts 'complete', includes complete tool-calls in ModelMessage.toolCalls.
Unit test assertions
packages/typescript/ai/tests/message-updaters.test.ts, packages/typescript/ai/tests/stream-processor.test.ts, packages/typescript/ai/tests/message-converters.test.ts
Assertions updated to expect 'complete' state with populated output for successful tool execution; 'input-complete' for error-only results; merged tool-call parts reflect output shapes in round-trip tests.
E2E helpers and route fixture support
testing/e2e/tests/tools-test/helpers.ts, testing/e2e/src/routes/tools-test.tsx
getMessages helper parses DOM messages JSON; route now accepts historyFixture parameter to seed chat with predefined tool-call/tool-result history.
E2E test specifications
testing/e2e/tests/tools-test/server-client-sequence.spec.ts, testing/e2e/tests/tools-test/server-tool-history.spec.ts
New specs validate mixed server/client tool sequencing and server tool history hydration; assert tool-call parts reach 'complete' with defined outputs and matching tool-result parts.
Example app repro route
examples/ts-react-chat/src/routes/issue-176-tool-result.tsx, examples/ts-react-chat/src/routeTree.gen.ts
/issue-176-tool-result route runs dual useChat instances comparing fixture vs. live tool execution; renders states, outputs, and JSON views for visual validation; route integrated into generated route tree.
Changeset
.changeset/server-tool-results-complete.md
Documents patch updates for ai, ai-client, and ai-event-client with behavior: server tool results populate tool-call.output and mark state 'complete'.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • TanStack/ai#511: Modifies tool-message handling in convertMessagesToModelMessages around tool-result/tool-call merging in the same file.

Suggested reviewers

  • tombeckenham
  • crutchcorn

Poem

🐰 A tool result once lost in the stream,
Now flows to complete, a developer's dream!
Where server and client both shine the same way,
With outputs and states in perfect display.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR successfully addresses all requirements from issue #176: tool-call parts now have output populated and state set to 'complete' when server results are attached; updates to ToolCallState across packages; unit tests (stream-processor, message-updaters) and e2e tests (server-tool-history) added.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #176 objectives: completing tool-call state transitions and adding comprehensive test coverage; no extraneous modifications detected.
Title check ✅ Passed The title 'fix: complete tool calls with server results' clearly summarizes the main change: marking successful tool-call parts as complete when server-executed tool results are attached.
Description check ✅ Passed The PR description covers the key changes (marking tool calls complete, adding state exports, unit and e2e tests), includes verification steps, and closes issue #176. A changeset was generated for the patch updates.

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

✨ 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 issue-176-server-tool-results

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 and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

🚀 Changeset Version Preview

3 package(s) bumped directly, 16 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/ai 0.21.2 → 0.21.3 Changeset
@tanstack/ai-client 0.11.6 → 0.11.7 Changeset
@tanstack/ai-event-client 0.3.9 → 0.3.10 Changeset
@tanstack/ai-code-mode 0.1.19 → 0.1.20 Dependent
@tanstack/ai-code-mode-skills 0.1.19 → 0.1.20 Dependent
@tanstack/ai-devtools-core 0.3.36 → 0.3.37 Dependent
@tanstack/ai-fal 0.7.12 → 0.7.13 Dependent
@tanstack/ai-isolate-cloudflare 0.2.10 → 0.2.11 Dependent
@tanstack/ai-isolate-node 0.1.19 → 0.1.20 Dependent
@tanstack/ai-isolate-quickjs 0.1.19 → 0.1.20 Dependent
@tanstack/ai-preact 0.6.31 → 0.6.32 Dependent
@tanstack/ai-react 0.11.6 → 0.11.7 Dependent
@tanstack/ai-solid 0.10.6 → 0.10.7 Dependent
@tanstack/ai-svelte 0.10.6 → 0.10.7 Dependent
@tanstack/ai-vue 0.10.7 → 0.10.8 Dependent
@tanstack/ai-vue-ui 0.2.2 → 0.2.3 Dependent
@tanstack/preact-ai-devtools 0.1.40 → 0.1.41 Dependent
@tanstack/react-ai-devtools 0.2.40 → 0.2.41 Dependent
@tanstack/solid-ai-devtools 0.2.40 → 0.2.41 Dependent

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 20, 2026

View your CI Pipeline Execution ↗ for commit 1774aae

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-22 16:11:17 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 20, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/@tanstack/ai@596

@tanstack/ai-anthropic

npm i https://pkg.pr.new/@tanstack/ai-anthropic@596

@tanstack/ai-client

npm i https://pkg.pr.new/@tanstack/ai-client@596

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@596

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@596

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/@tanstack/ai-devtools-core@596

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@596

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@596

@tanstack/ai-fal

npm i https://pkg.pr.new/@tanstack/ai-fal@596

@tanstack/ai-gemini

npm i https://pkg.pr.new/@tanstack/ai-gemini@596

@tanstack/ai-grok

npm i https://pkg.pr.new/@tanstack/ai-grok@596

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@596

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@596

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@596

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@596

@tanstack/ai-ollama

npm i https://pkg.pr.new/@tanstack/ai-ollama@596

@tanstack/ai-openai

npm i https://pkg.pr.new/@tanstack/ai-openai@596

@tanstack/ai-openrouter

npm i https://pkg.pr.new/@tanstack/ai-openrouter@596

@tanstack/ai-preact

npm i https://pkg.pr.new/@tanstack/ai-preact@596

@tanstack/ai-react

npm i https://pkg.pr.new/@tanstack/ai-react@596

@tanstack/ai-react-ui

npm i https://pkg.pr.new/@tanstack/ai-react-ui@596

@tanstack/ai-solid

npm i https://pkg.pr.new/@tanstack/ai-solid@596

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/@tanstack/ai-solid-ui@596

@tanstack/ai-svelte

npm i https://pkg.pr.new/@tanstack/ai-svelte@596

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@596

@tanstack/ai-vue

npm i https://pkg.pr.new/@tanstack/ai-vue@596

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/@tanstack/ai-vue-ui@596

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@596

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/@tanstack/preact-ai-devtools@596

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/@tanstack/react-ai-devtools@596

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/@tanstack/solid-ai-devtools@596

commit: 2aa7244

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 @.changeset/server-tool-results-complete.md:
- Around line 2-4: The changeset currently uses patch bumps for packages
'`@tanstack/ai`', '`@tanstack/ai-client`', and '`@tanstack/ai-event-client`' but the
addition of 'complete' to the exported ToolCallState type is a public type-shape
change and must be released as a minor bump per the repo's pre-1.0 convention;
update the changeset entry to use "minor" for those three packages (and ensure
the changeset message references the ToolCallState/complete change) so the
release tooling will produce a minor version rather than a patch.

In `@testing/e2e/tests/tools-test/server-client-sequence.spec.ts`:
- Around line 72-76: The inline function passed to page.waitForFunction can
throw when JSON.parse reads transient DOM content; update the anonymous function
used in waitForFunction (the one querying 'messages-json-content' and assigning
messages) to guard JSON.parse with a try/catch: if parsing fails return false so
the wait continues, otherwise proceed to compute toolCalls and return the
intended truthy value; in short, wrap JSON.parse(...) in try/catch and return
false on parse error to avoid flaky polling.
🪄 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: 284da935-0046-4678-a880-828337b71771

📥 Commits

Reviewing files that changed from the base of the PR and between 2ad137b and a73bdc2.

📒 Files selected for processing (11)
  • .changeset/server-tool-results-complete.md
  • packages/typescript/ai-client/src/types.ts
  • packages/typescript/ai-event-client/src/index.ts
  • packages/typescript/ai/src/activities/chat/messages.ts
  • packages/typescript/ai/src/activities/chat/stream/message-updaters.ts
  • packages/typescript/ai/src/activities/chat/stream/processor.ts
  • packages/typescript/ai/src/types.ts
  • packages/typescript/ai/tests/message-updaters.test.ts
  • packages/typescript/ai/tests/stream-processor.test.ts
  • testing/e2e/tests/tools-test/helpers.ts
  • testing/e2e/tests/tools-test/server-client-sequence.spec.ts

Comment on lines +2 to +4
'@tanstack/ai': patch
'@tanstack/ai-client': patch
'@tanstack/ai-event-client': patch
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use minor bumps for this exported type-shape change.

Adding 'complete' to ToolCallState is a public shape change and should be released as minor for this repo’s pre-1.0 convention.

Suggested fix
-'`@tanstack/ai`': patch
-'`@tanstack/ai-client`': patch
-'`@tanstack/ai-event-client`': patch
+'`@tanstack/ai`': minor
+'`@tanstack/ai-client`': minor
+'`@tanstack/ai-event-client`': minor
Based on learnings: "In the TanStack/ai repository (pre-1.0), follow the repo’s versioning convention: breaking changes and breaking/shape changes documented in Changesets must use a `minor` version bump (not `major`)."
📝 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
'@tanstack/ai': patch
'@tanstack/ai-client': patch
'@tanstack/ai-event-client': patch
'`@tanstack/ai`': minor
'`@tanstack/ai-client`': minor
'`@tanstack/ai-event-client`': 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/server-tool-results-complete.md around lines 2 - 4, The changeset
currently uses patch bumps for packages '`@tanstack/ai`', '`@tanstack/ai-client`',
and '`@tanstack/ai-event-client`' but the addition of 'complete' to the exported
ToolCallState type is a public type-shape change and must be released as a minor
bump per the repo's pre-1.0 convention; update the changeset entry to use
"minor" for those three packages (and ensure the changeset message references
the ToolCallState/complete change) so the release tooling will produce a minor
version rather than a patch.

Comment on lines +72 to +76
await page.waitForFunction(
() => {
const messagesEl = document.getElementById('messages-json-content')
const messages = JSON.parse(messagesEl?.textContent || '[]')
const toolCalls = messages.flatMap((msg: any) =>
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard JSON parsing inside waitForFunction to avoid flaky polling failures.

JSON.parse(messagesEl?.textContent || '[]') can throw during transient DOM updates and fail the wait early. Return false on parse failure so polling continues.

Suggested fix
     await page.waitForFunction(
       () => {
         const messagesEl = document.getElementById('messages-json-content')
-        const messages = JSON.parse(messagesEl?.textContent || '[]')
+        let messages: Array<any> = []
+        try {
+          messages = JSON.parse(messagesEl?.textContent || '[]')
+        } catch {
+          return false
+        }
         const toolCalls = messages.flatMap((msg: any) =>
           (msg.parts || []).filter((part: any) => part.type === 'tool-call'),
         )
📝 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
await page.waitForFunction(
() => {
const messagesEl = document.getElementById('messages-json-content')
const messages = JSON.parse(messagesEl?.textContent || '[]')
const toolCalls = messages.flatMap((msg: any) =>
await page.waitForFunction(
() => {
const messagesEl = document.getElementById('messages-json-content')
let messages: Array<any> = []
try {
messages = JSON.parse(messagesEl?.textContent || '[]')
} catch {
return false
}
const toolCalls = messages.flatMap((msg: any) =>
(msg.parts || []).filter((part: any) => part.type === 'tool-call'),
)
🤖 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 `@testing/e2e/tests/tools-test/server-client-sequence.spec.ts` around lines 72
- 76, The inline function passed to page.waitForFunction can throw when
JSON.parse reads transient DOM content; update the anonymous function used in
waitForFunction (the one querying 'messages-json-content' and assigning
messages) to guard JSON.parse with a try/catch: if parsing fails return false so
the wait continues, otherwise proceed to compute toolCalls and return the
intended truthy value; in short, wrap JSON.parse(...) in try/catch and return
false on parse error to avoid flaky polling.

@AlemTuzlak
Copy link
Copy Markdown
Contributor Author

Added follow-up commit 4b36015 for the server-result history/conversion path: modelMessagesToUIMessages now backfills the matching tool-call part output and marks it complete when merging a tool result. Verified with pnpm.cmd --filter @tanstack/ai exec vitest run tests/message-converters.test.ts tests/stream-processor.test.ts tests/message-updaters.test.ts.

@AlemTuzlak
Copy link
Copy Markdown
Contributor Author

Added e2e coverage for the edge case from the follow-up: tests/tools-test/server-tool-history.spec.ts loads /tools-test with model-message history containing a server tool-call plus matching tool result, then asserts UIMessage.parts has the original tool-call populated with output and state: complete. Verified with pnpm.cmd --filter @tanstack/ai run build, pnpm.cmd --filter @tanstack/ai-e2e build, and pnpm.cmd --filter @tanstack/ai-e2e exec playwright test tests/tools-test/server-tool-history.spec.ts --project=chromium.

@AlemTuzlak
Copy link
Copy Markdown
Contributor Author

Added a manual ts-react-chat repro page in commit 18790cf. Visit /issue-176-tool-result to inspect the model-message history fixture and hydrated UIMessage.parts; the page marks the tool-call state/output as fixed when the original server tool-call has state: complete and output populated. I also added it to the example navigation as " Issue

@AlemTuzlak
Copy link
Copy Markdown
Contributor Author

Updated the manual repro page with a live LLM flow in commit ca4db20. The page now has a Live LLM repro section: send the default guitar recommendation prompt through /api/tanchat, then inspect the live getGuitars server tool-call part next to its matching tool-result. The deterministic history fixture remains below it for the exact modelMessagesToUIMessages hydration edge case.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 (1)
packages/typescript/ai/src/activities/chat/stream/processor.ts (1)

1144-1144: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle empty-string tool results as valid results.

The truthy guard drops valid empty results (''), so the tool-call never gets output/complete and no tool-result part is emitted in that case.

Proposed fix
-    if (chunk.result) {
+    if (chunk.result !== undefined) {
🤖 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/typescript/ai/src/activities/chat/stream/processor.ts` at line 1144,
The guard currently uses a truthy check on chunk.result ("if (chunk.result) {")
which drops valid empty-string results; change it to an explicit null/undefined
check (e.g., "if (chunk.result !== undefined && chunk.result !== null)" or "if
(chunk.result != null)") in the processor where chunk.result is handled so empty
strings are treated as valid results and the tool-call receives output/complete
and the tool-result part is emitted.
🤖 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 `@packages/typescript/ai/src/activities/chat/stream/processor.ts`:
- Line 1144: The guard currently uses a truthy check on chunk.result ("if
(chunk.result) {") which drops valid empty-string results; change it to an
explicit null/undefined check (e.g., "if (chunk.result !== undefined &&
chunk.result !== null)" or "if (chunk.result != null)") in the processor where
chunk.result is handled so empty strings are treated as valid results and the
tool-call receives output/complete and the tool-result part is emitted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8191dea7-2548-4b10-83ba-c55b8b182963

📥 Commits

Reviewing files that changed from the base of the PR and between ca4db20 and 2aa7244.

📒 Files selected for processing (2)
  • packages/typescript/ai/src/activities/chat/messages.ts
  • packages/typescript/ai/src/activities/chat/stream/processor.ts

@AlemTuzlak AlemTuzlak changed the title fix: complete tool calls with server results feat(ai): add typed runtime context May 22, 2026
@AlemTuzlak AlemTuzlak force-pushed the issue-176-server-tool-results branch from 66fd559 to 2aa7244 Compare May 22, 2026 16:10
@AlemTuzlak AlemTuzlak changed the title feat(ai): add typed runtime context fix: complete tool calls with server results May 22, 2026
@AlemTuzlak AlemTuzlak merged commit e144a53 into main May 22, 2026
16 checks passed
@AlemTuzlak AlemTuzlak deleted the issue-176-server-tool-results branch May 22, 2026 16:34
@github-actions github-actions Bot mentioned this pull request May 22, 2026
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.

Server tool results missing from tool-call output field in UIMessage

1 participant