Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 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,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)
}
}
}
}
}

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