diff --git a/.changeset/fix-silent-chat-continuation.md b/.changeset/fix-silent-chat-continuation.md new file mode 100644 index 00000000..a36ce6b7 --- /dev/null +++ b/.changeset/fix-silent-chat-continuation.md @@ -0,0 +1,6 @@ +--- +"@tanstack/ai": patch +"@tanstack/ai-client": patch +--- + +fix: Continue conversation after client tool execution diff --git a/packages/typescript/ai-client/src/chat-client.ts b/packages/typescript/ai-client/src/chat-client.ts index 3b9e1787..5276b009 100644 --- a/packages/typescript/ai-client/src/chat-client.ts +++ b/packages/typescript/ai-client/src/chat-client.ts @@ -290,6 +290,7 @@ export class ChatClient { this.setIsLoading(true) this.setError(undefined) this.abortController = new AbortController() + let streamCompletedSuccessfully = false try { // Get model messages for the LLM @@ -312,6 +313,7 @@ export class ChatClient { ) await this.processStream(stream) + streamCompletedSuccessfully = true } catch (err) { if (err instanceof Error) { if (err.name === 'AbortError') { @@ -323,6 +325,20 @@ export class ChatClient { } finally { this.abortController = null this.setIsLoading(false) + + // Continue conversation if the stream ended with a tool result + if (streamCompletedSuccessfully) { + const messages = this.processor.getMessages() + const lastPart = messages.at(-1)?.parts?.at(-1) + + if (lastPart?.type === 'tool-result' && this.shouldAutoSend()) { + try { + await this.continueFlow() + } catch (error) { + console.error('Failed to continue flow after tool result:', error) + } + } + } } } diff --git a/packages/typescript/ai/src/stream/processor.ts b/packages/typescript/ai/src/stream/processor.ts index d8441a33..dd3429f6 100644 --- a/packages/typescript/ai/src/stream/processor.ts +++ b/packages/typescript/ai/src/stream/processor.ts @@ -355,11 +355,21 @@ export class StreamProcessor { if (toolParts.length === 0) return true + // Check for server tool completions via tool-result parts + const toolResultParts = lastAssistant.parts.filter( + (p): p is Extract => + p.type === 'tool-result', + ) + const completedToolCallIds = new Set( + toolResultParts.map((p) => p.toolCallId), + ) + // All tool calls must be in a terminal state return toolParts.every( (part) => part.state === 'approval-responded' || - (part.output !== undefined && !part.approval), + (part.output !== undefined && !part.approval) || + completedToolCallIds.has(part.id), ) }