-
-
Notifications
You must be signed in to change notification settings - Fork 87
Description
TanStack AI version
0.2.0
Framework/Library version
react 18.2.0
Describe the bug and the steps to reproduce it
Summary: Server-side tool results don't update the tool-call part's output field, causing inconsistent UI state between server and client tool execution.
Expected behavior: Both server-executed tools and client-executed tools should have their results visible on the tool-call part's output field, and the state should transition to 'complete'.
Actual behavior:
- Client tools:
tool-callpart showsstate: 'complete'withoutputpopulated - Server tools:
tool-callpart showsstate: 'input-complete'with nooutput(result only exists in a separatetool-resultpart)
Root cause: In StreamProcessor, handleToolResultChunk() only creates a tool-result part via updateToolResultPart(), but doesn't update the corresponding tool-call part's output field or state. In contrast, addToolResult() (used for client tools) does both:
// addToolResult() does this (lines 283-300):
let updatedMessages = updateToolCallWithOutput( // Updates tool-call output
this.messages,
toolCallId,
output,
error ? 'input-complete' : undefined,
error,
)
updatedMessages = updateToolResultPart(...) // Also creates tool-result part
// handleToolResultChunk() only does this (lines 691-699):
this.messages = updateToolResultPart(...) // Only creates tool-result part
// Missing: updateToolCallWithOutput() callSteps to reproduce:
- Define a tool with
toolDefinition()that has bothinputSchemaandoutputSchema - Create a server-side implementation using
.server()with an execute function - Create a client-side implementation using
.client()with an execute function - Call both tools in a chat session
- Observe the
UIMessage.parts- client tool-call parts haveoutputandstate: 'complete', server tool-call parts only havestate: 'input-complete'with nooutput
Example to reproduce
import { toolDefinition, chat } from '@tanstack/ai'
import { z } from 'zod'
// Shared tool definition
const myToolDef = toolDefinition({
name: 'myTool',
description: 'A test tool',
inputSchema: z.object({ input: z.string() }),
outputSchema: z.object({ result: z.string() }),
})
// Server tool - has execute, runs on server
const serverTool = myToolDef.server(async (args) => {
return { result: `Server processed: ${args.input}` }
})
// Client tool - has execute, runs on client
const clientTool = myToolDef.client(async (args) => {
return { result: `Client processed: ${args.input}` }
})
// When server tool executes:
// - StreamProcessor receives tool_result chunk
// - handleToolResultChunk() creates tool-result part
// - tool-call part remains at state: 'input-complete', output: undefined ❌
// When client tool executes:
// - StreamProcessor receives tool-input-available chunk
// - Client executes tool and calls addToolResult()
// - addToolResult() updates tool-call part with output AND creates tool-result part
// - tool-call part shows state: 'complete', output: {...} ✅Screenshots or Videos (Optional)
In the screenshot:
getCurrentPipelineConfig(client tool): Shows "complete" status with RESULT section visiblegetPipelineSchema(server tool): Shows "input-complete" status with no RESULT sectionvalidatePipelineConfig(server tool): Shows "input-complete" status with no RESULT sectionapplyPipelineConfig(client tool): Shows "complete" status
Suggested Fix
In processor.ts, handleToolResultChunk() should also update the tool-call part similar to addToolResult():
private handleToolResultChunk(
chunk: Extract<StreamChunk, { type: 'tool_result' }>,
): void {
const state: ToolResultState = 'complete'
// Emit legacy handler
this.handlers.onToolResultStateChange?.(
chunk.toolCallId,
chunk.content,
state,
)
// Update UIMessage if we have a current assistant message
if (this.currentAssistantMessageId) {
// NEW: Also update the tool-call part's output field
const output = this.parseToolResultContent(chunk.content)
this.messages = updateToolCallWithOutput(
this.messages,
chunk.toolCallId,
output,
)
// Existing: Create tool-result part
this.messages = updateToolResultPart(
this.messages,
this.currentAssistantMessageId,
chunk.toolCallId,
chunk.content,
state,
)
this.emitMessagesChange()
}
}Do you intend to try to help solve this bug with your own PR?
None
Terms & Code of Conduct
- I agree to follow this project's Code of Conduct
- I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.