Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fix-silent-chat-continuation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@tanstack/ai": patch
"@tanstack/ai-client": patch
---

fix: Continue conversation after client tool execution
17 changes: 17 additions & 0 deletions packages/typescript/ai-client/src/chat-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -312,6 +313,7 @@ export class ChatClient {
)

await this.processStream(stream)
streamCompletedSuccessfully = true
} catch (err) {
if (err instanceof Error) {
if (err.name === 'AbortError') {
Expand All @@ -323,6 +325,21 @@ 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 lastMessage = messages[messages.length - 1]
const lastPart = lastMessage?.parts[lastMessage.parts.length - 1]

if (lastPart?.type === 'tool-result' && this.shouldAutoSend()) {
try {
await this.continueFlow()
} catch (error) {
console.error('Failed to continue flow after tool result:', error)
}
}
}
}
}

Expand Down
12 changes: 11 additions & 1 deletion packages/typescript/ai/src/stream/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof p, { type: 'tool-result' }> =>
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),
)
}

Expand Down