@tanstack/ai-client 0.4.5 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,7 +47,7 @@ A powerful, type-safe AI SDK for building AI-powered applications.
47
47
  - **Enhanced integration with TanStack Start** - Share implementations between AI tools and server functions
48
48
  - **Observability events** - Structured, typed events for text, tools, image, speech, transcription, and video ([docs](./docs/guides/observability.md))
49
49
 
50
- ### <a href="https://tanstack.com/ai">Read the docs →</b></a>
50
+ ### <a href="https://tanstack.com/ai">Read the docs →</a>
51
51
 
52
52
  ## Tree-Shakeable Adapters
53
53
 
@@ -18,6 +18,7 @@ export declare class ChatClient {
18
18
  private postStreamActions;
19
19
  private pendingToolExecutions;
20
20
  private continuationPending;
21
+ private continuationSkipped;
21
22
  private callbacksRef;
22
23
  constructor(options: ChatClientOptions);
23
24
  private generateUniqueId;
@@ -78,7 +79,8 @@ export declare class ChatClient {
78
79
  */
79
80
  append(message: UIMessage | ModelMessage): Promise<void>;
80
81
  /**
81
- * Stream a response from the LLM
82
+ * Stream a response from the LLM.
83
+ * Returns true if the stream completed successfully, false on abort or error.
82
84
  */
83
85
  private streamResponse;
84
86
  /**
@@ -157,5 +159,8 @@ export declare class ChatClient {
157
159
  onChunk?: (chunk: StreamChunk) => void;
158
160
  onFinish?: (message: UIMessage) => void;
159
161
  onError?: (error: Error) => void;
162
+ onCustomEvent?: (eventType: string, data: unknown, context: {
163
+ toolCallId?: string;
164
+ }) => void;
160
165
  }): void;
161
166
  }
@@ -13,6 +13,7 @@ class ChatClient {
13
13
  this.postStreamActions = [];
14
14
  this.pendingToolExecutions = /* @__PURE__ */ new Map();
15
15
  this.continuationPending = false;
16
+ this.continuationSkipped = false;
16
17
  this.uniqueId = options.id || this.generateUniqueId("chat");
17
18
  this.body = options.body || {};
18
19
  this.connection = options.connection;
@@ -40,6 +41,8 @@ class ChatClient {
40
41
  onErrorChange: options.onErrorChange || (() => {
41
42
  }),
42
43
  onStatusChange: options.onStatusChange || (() => {
44
+ }),
45
+ onCustomEvent: options.onCustomEvent || (() => {
43
46
  })
44
47
  }
45
48
  };
@@ -133,6 +136,9 @@ class ChatClient {
133
136
  args.approvalId
134
137
  );
135
138
  }
139
+ },
140
+ onCustomEvent: (eventType, data, context) => {
141
+ this.callbacksRef.current.onCustomEvent(eventType, data, context);
136
142
  }
137
143
  }
138
144
  });
@@ -268,17 +274,20 @@ class ChatClient {
268
274
  const messages = this.processor.getMessages();
269
275
  this.processor.setMessages([...messages, uiMessage]);
270
276
  if (this.isLoading) {
271
- this.queuePostStreamAction(() => this.streamResponse());
277
+ this.queuePostStreamAction(async () => {
278
+ await this.streamResponse();
279
+ });
272
280
  return;
273
281
  }
274
282
  await this.streamResponse();
275
283
  }
276
284
  /**
277
- * Stream a response from the LLM
285
+ * Stream a response from the LLM.
286
+ * Returns true if the stream completed successfully, false on abort or error.
278
287
  */
279
288
  async streamResponse() {
280
289
  if (this.isLoading) {
281
- return;
290
+ return false;
282
291
  }
283
292
  this.setIsLoading(true);
284
293
  this.setStatus("submitted");
@@ -305,7 +314,7 @@ class ChatClient {
305
314
  } catch (err) {
306
315
  if (err instanceof Error) {
307
316
  if (err.name === "AbortError") {
308
- return;
317
+ return false;
309
318
  }
310
319
  this.setError(err);
311
320
  this.setStatus("error");
@@ -328,6 +337,7 @@ class ChatClient {
328
337
  }
329
338
  }
330
339
  }
340
+ return streamCompletedSuccessfully;
331
341
  }
332
342
  /**
333
343
  * Reload the last assistant message
@@ -433,15 +443,22 @@ class ChatClient {
433
443
  */
434
444
  async checkForContinuation() {
435
445
  if (this.continuationPending || this.isLoading) {
446
+ this.continuationSkipped = true;
436
447
  return;
437
448
  }
438
449
  if (this.shouldAutoSend()) {
439
450
  this.continuationPending = true;
451
+ this.continuationSkipped = false;
452
+ let succeeded = false;
440
453
  try {
441
- await this.streamResponse();
454
+ succeeded = await this.streamResponse();
442
455
  } finally {
443
456
  this.continuationPending = false;
444
457
  }
458
+ if (this.continuationSkipped && succeeded) {
459
+ this.continuationSkipped = false;
460
+ await this.checkForContinuation();
461
+ }
445
462
  }
446
463
  }
447
464
  /**
@@ -508,6 +525,9 @@ class ChatClient {
508
525
  if (options.onError !== void 0) {
509
526
  this.callbacksRef.current.onError = options.onError;
510
527
  }
528
+ if (options.onCustomEvent !== void 0) {
529
+ this.callbacksRef.current.onCustomEvent = options.onCustomEvent;
530
+ }
511
531
  }
512
532
  }
513
533
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"chat-client.js","sources":["../../src/chat-client.ts"],"sourcesContent":["import {\n StreamProcessor,\n generateMessageId,\n normalizeToUIMessage,\n} from '@tanstack/ai'\nimport { DefaultChatClientEventEmitter } from './events'\nimport type {\n AnyClientTool,\n ContentPart,\n ModelMessage,\n StreamChunk,\n} from '@tanstack/ai'\nimport type { ConnectionAdapter } from './connection-adapters'\nimport type { ChatClientEventEmitter } from './events'\nimport type {\n ChatClientOptions,\n ChatClientState,\n MessagePart,\n MultimodalContent,\n ToolCallPart,\n UIMessage,\n} from './types'\n\nexport class ChatClient {\n private processor: StreamProcessor\n private connection: ConnectionAdapter\n private uniqueId: string\n private body: Record<string, any> = {}\n private pendingMessageBody: Record<string, any> | undefined = undefined\n private isLoading = false\n private error: Error | undefined = undefined\n private status: ChatClientState = 'ready'\n private abortController: AbortController | null = null\n private events: ChatClientEventEmitter\n private clientToolsRef: { current: Map<string, AnyClientTool> }\n private currentStreamId: string | null = null\n private currentMessageId: string | null = null\n private postStreamActions: Array<() => Promise<void>> = []\n // Track pending client tool executions to await them before stream finalization\n private pendingToolExecutions: Map<string, Promise<void>> = new Map()\n // Flag to deduplicate continuation checks during action draining\n private continuationPending = false\n\n private callbacksRef: {\n current: {\n onResponse: (response?: Response) => void | Promise<void>\n onChunk: (chunk: StreamChunk) => void\n onFinish: (message: UIMessage) => void\n onError: (error: Error) => void\n onMessagesChange: (messages: Array<UIMessage>) => void\n onLoadingChange: (isLoading: boolean) => void\n onErrorChange: (error: Error | undefined) => void\n onStatusChange: (status: ChatClientState) => void\n }\n }\n\n constructor(options: ChatClientOptions) {\n this.uniqueId = options.id || this.generateUniqueId('chat')\n this.body = options.body || {}\n this.connection = options.connection\n this.events = new DefaultChatClientEventEmitter(this.uniqueId)\n\n // Build client tools map\n this.clientToolsRef = { current: new Map() }\n if (options.tools) {\n for (const tool of options.tools) {\n this.clientToolsRef.current.set(tool.name, tool)\n }\n }\n\n this.callbacksRef = {\n current: {\n onResponse: options.onResponse || (() => {}),\n onChunk: options.onChunk || (() => {}),\n onFinish: options.onFinish || (() => {}),\n onError: options.onError || (() => {}),\n onMessagesChange: options.onMessagesChange || (() => {}),\n onLoadingChange: options.onLoadingChange || (() => {}),\n onErrorChange: options.onErrorChange || (() => {}),\n onStatusChange: options.onStatusChange || (() => {}),\n },\n }\n\n // Create StreamProcessor with event handlers\n this.processor = new StreamProcessor({\n chunkStrategy: options.streamProcessor?.chunkStrategy,\n initialMessages: options.initialMessages,\n events: {\n onMessagesChange: (messages: Array<UIMessage>) => {\n this.callbacksRef.current.onMessagesChange(messages)\n },\n onStreamStart: () => {\n this.setStatus('streaming')\n },\n onStreamEnd: (message: UIMessage) => {\n this.callbacksRef.current.onFinish(message)\n this.setStatus('ready')\n },\n onError: (error: Error) => {\n this.setError(error)\n this.setStatus('error')\n this.callbacksRef.current.onError(error)\n },\n onTextUpdate: (messageId: string, content: string) => {\n // Emit text update to devtools\n if (this.currentStreamId) {\n this.events.textUpdated(this.currentStreamId, messageId, content)\n }\n },\n onThinkingUpdate: (messageId: string, content: string) => {\n // Emit thinking update to devtools\n if (this.currentStreamId) {\n this.events.thinkingUpdated(\n this.currentStreamId,\n messageId,\n content,\n )\n }\n },\n onToolCallStateChange: (\n messageId: string,\n toolCallId: string,\n state: string,\n args: string,\n ) => {\n // Get the tool name from the messages\n const messages = this.processor.getMessages()\n const message = messages.find((m: UIMessage) => m.id === messageId)\n const toolCallPart = message?.parts.find(\n (p: MessagePart): p is ToolCallPart =>\n p.type === 'tool-call' && p.id === toolCallId,\n )\n const toolName = toolCallPart?.name || 'unknown'\n\n // Emit tool call state change to devtools\n if (this.currentStreamId) {\n this.events.toolCallStateChanged(\n this.currentStreamId,\n messageId,\n toolCallId,\n toolName,\n state,\n args,\n )\n }\n },\n onToolCall: (args: {\n toolCallId: string\n toolName: string\n input: any\n }) => {\n // Handle client-side tool execution automatically\n const clientTool = this.clientToolsRef.current.get(args.toolName)\n const executeFunc = clientTool?.execute\n if (executeFunc) {\n // Create and track the execution promise\n const executionPromise = (async () => {\n try {\n const output = await executeFunc(args.input)\n await this.addToolResult({\n toolCallId: args.toolCallId,\n tool: args.toolName,\n output,\n state: 'output-available',\n })\n } catch (error: any) {\n await this.addToolResult({\n toolCallId: args.toolCallId,\n tool: args.toolName,\n output: null,\n state: 'output-error',\n errorText: error.message,\n })\n } finally {\n // Remove from pending when complete\n this.pendingToolExecutions.delete(args.toolCallId)\n }\n })()\n\n // Track the pending execution\n this.pendingToolExecutions.set(args.toolCallId, executionPromise)\n }\n },\n onApprovalRequest: (args: {\n toolCallId: string\n toolName: string\n input: any\n approvalId: string\n }) => {\n if (this.currentStreamId) {\n this.events.approvalRequested(\n this.currentStreamId,\n this.currentMessageId || '',\n args.toolCallId,\n args.toolName,\n args.input,\n args.approvalId,\n )\n }\n },\n },\n })\n\n this.events.clientCreated(this.processor.getMessages().length)\n }\n\n private generateUniqueId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n }\n\n private setIsLoading(isLoading: boolean): void {\n this.isLoading = isLoading\n this.callbacksRef.current.onLoadingChange(isLoading)\n this.events.loadingChanged(isLoading)\n }\n\n private setStatus(status: ChatClientState): void {\n this.status = status\n this.callbacksRef.current.onStatusChange(status)\n }\n\n private setError(error: Error | undefined): void {\n this.error = error\n this.callbacksRef.current.onErrorChange(error)\n this.events.errorChanged(error?.message || null)\n }\n\n /**\n * Process a stream through the StreamProcessor\n */\n private async processStream(\n source: AsyncIterable<StreamChunk>,\n ): Promise<UIMessage | null> {\n // Generate a stream ID for this streaming operation\n this.currentStreamId = this.generateUniqueId('stream')\n\n // Prepare for a new assistant message (created lazily on first content)\n this.processor.prepareAssistantMessage()\n\n // Process each chunk\n for await (const chunk of source) {\n this.callbacksRef.current.onChunk(chunk)\n this.processor.processChunk(chunk)\n\n // Track the message ID once the processor lazily creates it\n if (!this.currentMessageId) {\n const newMessageId =\n this.processor.getCurrentAssistantMessageId() ?? null\n if (newMessageId) {\n this.currentMessageId = newMessageId\n // Emit message appended event now that the assistant message exists\n const assistantMessage = this.processor\n .getMessages()\n .find((m: UIMessage) => m.id === newMessageId)\n if (assistantMessage) {\n this.events.messageAppended(\n assistantMessage,\n this.currentStreamId || undefined,\n )\n }\n }\n }\n\n // Yield control back to event loop to allow UI updates\n await new Promise((resolve) => setTimeout(resolve, 0))\n }\n\n // Wait for all pending tool executions to complete before finalizing\n // This ensures client tools finish before we check for continuation\n if (this.pendingToolExecutions.size > 0) {\n await Promise.all(this.pendingToolExecutions.values())\n }\n\n // Finalize the stream\n this.processor.finalizeStream()\n\n // Get the message ID (may be null if no content arrived)\n const messageId = this.processor.getCurrentAssistantMessageId()\n\n // Clear the current stream and message IDs\n this.currentStreamId = null\n this.currentMessageId = null\n\n // Return the assistant message if one was created\n if (messageId) {\n const messages = this.processor.getMessages()\n return messages.find((m: UIMessage) => m.id === messageId) || null\n }\n\n return null\n }\n\n /**\n * Send a message and stream the response.\n * Supports both simple string content and multimodal content (images, audio, video, documents).\n *\n * @param content - The message content. Can be:\n * - A simple string for text-only messages\n * - A MultimodalContent object with content array and optional custom ID\n * @param body - Optional body parameters to merge with the client's base body for this request.\n * Uses shallow merge with per-message body taking priority.\n *\n * @example\n * ```ts\n * // Simple text message\n * await client.sendMessage('Hello!')\n *\n * // Text message with custom body params\n * await client.sendMessage('Hello!', { temperature: 0.7 })\n *\n * // Multimodal message with image\n * await client.sendMessage({\n * content: [\n * { type: 'text', content: 'What is in this image?' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }\n * ]\n * })\n *\n * // Multimodal message with custom ID and body params\n * await client.sendMessage(\n * {\n * content: [\n * { type: 'text', content: 'Describe this audio' },\n * { type: 'audio', source: { type: 'data', value: 'base64...' } }\n * ],\n * id: 'custom-message-id'\n * },\n * { model: 'gpt-4-audio' }\n * )\n * ```\n */\n async sendMessage(\n content: string | MultimodalContent,\n body?: Record<string, any>,\n ): Promise<void> {\n const emptyMessage = typeof content === 'string' && !content.trim()\n if (emptyMessage || this.isLoading) {\n return\n }\n // Normalize input to extract content, id, and validate\n const normalizedContent = this.normalizeMessageInput(content)\n\n // Store the per-message body for use in streamResponse\n this.pendingMessageBody = body\n\n // Add user message via processor\n const userMessage = this.processor.addUserMessage(\n normalizedContent.content,\n normalizedContent.id,\n )\n this.events.messageSent(userMessage.id, normalizedContent.content)\n\n await this.streamResponse()\n }\n\n /**\n * Normalize the message input to extract content and optional id.\n * Trims string content automatically.\n */\n private normalizeMessageInput(input: string | MultimodalContent): {\n content: string | Array<ContentPart>\n id?: string\n } {\n if (typeof input === 'string') {\n return { content: input.trim() }\n }\n return { content: input.content, id: input.id }\n }\n\n /**\n * Append a message and stream the response\n */\n async append(message: UIMessage | ModelMessage): Promise<void> {\n // Normalize the message to ensure it has id and createdAt\n const normalizedMessage = normalizeToUIMessage(message, generateMessageId)\n\n // Skip system messages - they're handled via systemPrompts, not UIMessages\n if (normalizedMessage.role === 'system') {\n return\n }\n\n // Type assertion: after checking for system, we know it's user or assistant\n const uiMessage = normalizedMessage as UIMessage\n\n // Emit message appended event\n this.events.messageAppended(uiMessage)\n\n // Add to messages\n const messages = this.processor.getMessages()\n this.processor.setMessages([...messages, uiMessage])\n\n // If stream is in progress, queue the response for after it ends\n if (this.isLoading) {\n this.queuePostStreamAction(() => this.streamResponse())\n return\n }\n\n await this.streamResponse()\n }\n\n /**\n * Stream a response from the LLM\n */\n private async streamResponse(): Promise<void> {\n // Guard against concurrent streams - if already loading, skip\n if (this.isLoading) {\n return\n }\n\n this.setIsLoading(true)\n this.setStatus('submitted')\n this.setError(undefined)\n this.abortController = new AbortController()\n // Reset pending tool executions for the new stream\n this.pendingToolExecutions.clear()\n let streamCompletedSuccessfully = false\n\n try {\n // Get UIMessages with parts (preserves approval state and client tool results)\n const messages = this.processor.getMessages()\n\n // Call onResponse callback\n await this.callbacksRef.current.onResponse()\n\n // Merge body: base body + per-message body (per-message takes priority)\n // Include conversationId for server-side event correlation\n const mergedBody = {\n ...this.body,\n ...this.pendingMessageBody,\n conversationId: this.uniqueId,\n }\n\n // Clear the pending message body after use\n this.pendingMessageBody = undefined\n\n // Connect and stream\n const stream = this.connection.connect(\n messages,\n mergedBody,\n this.abortController.signal,\n )\n\n await this.processStream(stream)\n streamCompletedSuccessfully = true\n } catch (err) {\n if (err instanceof Error) {\n if (err.name === 'AbortError') {\n return\n }\n this.setError(err)\n this.setStatus('error')\n this.callbacksRef.current.onError(err)\n }\n } finally {\n this.abortController = null\n this.setIsLoading(false)\n this.pendingMessageBody = undefined // Ensure it's cleared even on error\n\n // Drain any actions that were queued while the stream was in progress\n await this.drainPostStreamActions()\n\n // Continue conversation if the stream ended with a tool result (server tool completed)\n if (streamCompletedSuccessfully) {\n const messages = this.processor.getMessages()\n const lastPart = messages.at(-1)?.parts.at(-1)\n\n if (lastPart?.type === 'tool-result' && this.shouldAutoSend()) {\n try {\n await this.checkForContinuation()\n } catch (error) {\n console.error('Failed to continue flow after tool result:', error)\n }\n }\n }\n }\n }\n\n /**\n * Reload the last assistant message\n */\n async reload(): Promise<void> {\n const messages = this.processor.getMessages()\n if (messages.length === 0) return\n\n // Find the last user message\n const lastUserMessageIndex = messages.findLastIndex(\n (m: UIMessage) => m.role === 'user',\n )\n\n if (lastUserMessageIndex === -1) return\n\n this.events.reloaded(lastUserMessageIndex)\n\n // Remove all messages after the last user message\n this.processor.removeMessagesAfter(lastUserMessageIndex)\n\n // Resend\n await this.streamResponse()\n }\n\n /**\n * Stop the current stream\n */\n stop(): void {\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n this.setStatus('ready')\n this.events.stopped()\n }\n\n /**\n * Clear all messages\n */\n clear(): void {\n this.processor.clearMessages()\n this.setError(undefined)\n this.events.messagesCleared()\n }\n\n /**\n * Add the result of a client-side tool execution\n */\n async addToolResult(result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }): Promise<void> {\n this.events.toolResultAdded(\n result.toolCallId,\n result.tool,\n result.output,\n result.state || 'output-available',\n )\n\n // Add result via processor\n this.processor.addToolResult(\n result.toolCallId,\n result.output,\n result.errorText,\n )\n\n // If stream is in progress, queue continuation check for after it ends\n if (this.isLoading) {\n this.queuePostStreamAction(() => this.checkForContinuation())\n return\n }\n\n await this.checkForContinuation()\n }\n\n /**\n * Respond to a tool approval request\n */\n async addToolApprovalResponse(response: {\n id: string // approval.id, not toolCallId\n approved: boolean\n }): Promise<void> {\n // Find the tool call ID from the approval ID\n const messages = this.processor.getMessages()\n let foundToolCallId: string | undefined\n\n for (const msg of messages) {\n const toolCallPart = msg.parts.find(\n (p: MessagePart): p is ToolCallPart =>\n p.type === 'tool-call' && p.approval?.id === response.id,\n )\n if (toolCallPart) {\n foundToolCallId = toolCallPart.id\n break\n }\n }\n\n if (foundToolCallId) {\n this.events.toolApprovalResponded(\n response.id,\n foundToolCallId,\n response.approved,\n )\n }\n\n // Add response via processor\n this.processor.addToolApprovalResponse(response.id, response.approved)\n\n // If stream is in progress, queue continuation check for after it ends\n if (this.isLoading) {\n this.queuePostStreamAction(() => this.checkForContinuation())\n return\n }\n\n await this.checkForContinuation()\n }\n\n /**\n * Queue an action to be executed after the current stream ends\n */\n private queuePostStreamAction(action: () => Promise<void>): void {\n this.postStreamActions.push(action)\n }\n\n /**\n * Drain and execute all queued post-stream actions\n */\n private async drainPostStreamActions(): Promise<void> {\n while (this.postStreamActions.length > 0) {\n const action = this.postStreamActions.shift()!\n await action()\n }\n }\n\n /**\n * Check if we should continue the flow and do so if needed\n */\n private async checkForContinuation(): Promise<void> {\n // Prevent duplicate continuation attempts\n if (this.continuationPending || this.isLoading) {\n return\n }\n\n if (this.shouldAutoSend()) {\n this.continuationPending = true\n try {\n await this.streamResponse()\n } finally {\n this.continuationPending = false\n }\n }\n }\n\n /**\n * Check if all tool calls are complete and we should auto-send\n */\n private shouldAutoSend(): boolean {\n return this.processor.areAllToolsComplete()\n }\n\n /**\n * Get current messages\n */\n getMessages(): Array<UIMessage> {\n return this.processor.getMessages()\n }\n\n /**\n * Get loading state\n */\n getIsLoading(): boolean {\n return this.isLoading\n }\n\n /**\n * Get current status\n */\n getStatus(): ChatClientState {\n return this.status\n }\n\n /**\n * Get current error\n */\n getError(): Error | undefined {\n return this.error\n }\n\n /**\n * Manually set messages\n */\n setMessagesManually(messages: Array<UIMessage>): void {\n this.processor.setMessages(messages)\n }\n\n /**\n * Update options refs (for use in React hooks to avoid recreating client)\n */\n updateOptions(options: {\n connection?: ConnectionAdapter\n body?: Record<string, any>\n tools?: ReadonlyArray<AnyClientTool>\n onResponse?: (response?: Response) => void | Promise<void>\n onChunk?: (chunk: StreamChunk) => void\n onFinish?: (message: UIMessage) => void\n onError?: (error: Error) => void\n }): void {\n if (options.connection !== undefined) {\n this.connection = options.connection\n }\n if (options.body !== undefined) {\n this.body = options.body\n }\n if (options.tools !== undefined) {\n this.clientToolsRef.current = new Map()\n for (const tool of options.tools) {\n this.clientToolsRef.current.set(tool.name, tool)\n }\n }\n if (options.onResponse !== undefined) {\n this.callbacksRef.current.onResponse = options.onResponse\n }\n if (options.onChunk !== undefined) {\n this.callbacksRef.current.onChunk = options.onChunk\n }\n if (options.onFinish !== undefined) {\n this.callbacksRef.current.onFinish = options.onFinish\n }\n if (options.onError !== undefined) {\n this.callbacksRef.current.onError = options.onError\n }\n }\n}\n"],"names":[],"mappings":";;AAuBO,MAAM,WAAW;AAAA,EAiCtB,YAAY,SAA4B;AA7BxC,SAAQ,OAA4B,CAAA;AACpC,SAAQ,qBAAsD;AAC9D,SAAQ,YAAY;AACpB,SAAQ,QAA2B;AACnC,SAAQ,SAA0B;AAClC,SAAQ,kBAA0C;AAGlD,SAAQ,kBAAiC;AACzC,SAAQ,mBAAkC;AAC1C,SAAQ,oBAAgD,CAAA;AAExD,SAAQ,4CAAwD,IAAA;AAEhE,SAAQ,sBAAsB;AAgB5B,SAAK,WAAW,QAAQ,MAAM,KAAK,iBAAiB,MAAM;AAC1D,SAAK,OAAO,QAAQ,QAAQ,CAAA;AAC5B,SAAK,aAAa,QAAQ;AAC1B,SAAK,SAAS,IAAI,8BAA8B,KAAK,QAAQ;AAG7D,SAAK,iBAAiB,EAAE,SAAS,oBAAI,MAAI;AACzC,QAAI,QAAQ,OAAO;AACjB,iBAAW,QAAQ,QAAQ,OAAO;AAChC,aAAK,eAAe,QAAQ,IAAI,KAAK,MAAM,IAAI;AAAA,MACjD;AAAA,IACF;AAEA,SAAK,eAAe;AAAA,MAClB,SAAS;AAAA,QACP,YAAY,QAAQ,eAAe,MAAM;AAAA,QAAC;AAAA,QAC1C,SAAS,QAAQ,YAAY,MAAM;AAAA,QAAC;AAAA,QACpC,UAAU,QAAQ,aAAa,MAAM;AAAA,QAAC;AAAA,QACtC,SAAS,QAAQ,YAAY,MAAM;AAAA,QAAC;AAAA,QACpC,kBAAkB,QAAQ,qBAAqB,MAAM;AAAA,QAAC;AAAA,QACtD,iBAAiB,QAAQ,oBAAoB,MAAM;AAAA,QAAC;AAAA,QACpD,eAAe,QAAQ,kBAAkB,MAAM;AAAA,QAAC;AAAA,QAChD,gBAAgB,QAAQ,mBAAmB,MAAM;AAAA,QAAC;AAAA,MAAA;AAAA,IACpD;AAIF,SAAK,YAAY,IAAI,gBAAgB;AAAA,MACnC,eAAe,QAAQ,iBAAiB;AAAA,MACxC,iBAAiB,QAAQ;AAAA,MACzB,QAAQ;AAAA,QACN,kBAAkB,CAAC,aAA+B;AAChD,eAAK,aAAa,QAAQ,iBAAiB,QAAQ;AAAA,QACrD;AAAA,QACA,eAAe,MAAM;AACnB,eAAK,UAAU,WAAW;AAAA,QAC5B;AAAA,QACA,aAAa,CAAC,YAAuB;AACnC,eAAK,aAAa,QAAQ,SAAS,OAAO;AAC1C,eAAK,UAAU,OAAO;AAAA,QACxB;AAAA,QACA,SAAS,CAAC,UAAiB;AACzB,eAAK,SAAS,KAAK;AACnB,eAAK,UAAU,OAAO;AACtB,eAAK,aAAa,QAAQ,QAAQ,KAAK;AAAA,QACzC;AAAA,QACA,cAAc,CAAC,WAAmB,YAAoB;AAEpD,cAAI,KAAK,iBAAiB;AACxB,iBAAK,OAAO,YAAY,KAAK,iBAAiB,WAAW,OAAO;AAAA,UAClE;AAAA,QACF;AAAA,QACA,kBAAkB,CAAC,WAAmB,YAAoB;AAExD,cAAI,KAAK,iBAAiB;AACxB,iBAAK,OAAO;AAAA,cACV,KAAK;AAAA,cACL;AAAA,cACA;AAAA,YAAA;AAAA,UAEJ;AAAA,QACF;AAAA,QACA,uBAAuB,CACrB,WACA,YACA,OACA,SACG;AAEH,gBAAM,WAAW,KAAK,UAAU,YAAA;AAChC,gBAAM,UAAU,SAAS,KAAK,CAAC,MAAiB,EAAE,OAAO,SAAS;AAClE,gBAAM,eAAe,SAAS,MAAM;AAAA,YAClC,CAAC,MACC,EAAE,SAAS,eAAe,EAAE,OAAO;AAAA,UAAA;AAEvC,gBAAM,WAAW,cAAc,QAAQ;AAGvC,cAAI,KAAK,iBAAiB;AACxB,iBAAK,OAAO;AAAA,cACV,KAAK;AAAA,cACL;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,cACA;AAAA,YAAA;AAAA,UAEJ;AAAA,QACF;AAAA,QACA,YAAY,CAAC,SAIP;AAEJ,gBAAM,aAAa,KAAK,eAAe,QAAQ,IAAI,KAAK,QAAQ;AAChE,gBAAM,cAAc,YAAY;AAChC,cAAI,aAAa;AAEf,kBAAM,oBAAoB,YAAY;AACpC,kBAAI;AACF,sBAAM,SAAS,MAAM,YAAY,KAAK,KAAK;AAC3C,sBAAM,KAAK,cAAc;AAAA,kBACvB,YAAY,KAAK;AAAA,kBACjB,MAAM,KAAK;AAAA,kBACX;AAAA,kBACA,OAAO;AAAA,gBAAA,CACR;AAAA,cACH,SAAS,OAAY;AACnB,sBAAM,KAAK,cAAc;AAAA,kBACvB,YAAY,KAAK;AAAA,kBACjB,MAAM,KAAK;AAAA,kBACX,QAAQ;AAAA,kBACR,OAAO;AAAA,kBACP,WAAW,MAAM;AAAA,gBAAA,CAClB;AAAA,cACH,UAAA;AAEE,qBAAK,sBAAsB,OAAO,KAAK,UAAU;AAAA,cACnD;AAAA,YACF,GAAA;AAGA,iBAAK,sBAAsB,IAAI,KAAK,YAAY,gBAAgB;AAAA,UAClE;AAAA,QACF;AAAA,QACA,mBAAmB,CAAC,SAKd;AACJ,cAAI,KAAK,iBAAiB;AACxB,iBAAK,OAAO;AAAA,cACV,KAAK;AAAA,cACL,KAAK,oBAAoB;AAAA,cACzB,KAAK;AAAA,cACL,KAAK;AAAA,cACL,KAAK;AAAA,cACL,KAAK;AAAA,YAAA;AAAA,UAET;AAAA,QACF;AAAA,MAAA;AAAA,IACF,CACD;AAED,SAAK,OAAO,cAAc,KAAK,UAAU,YAAA,EAAc,MAAM;AAAA,EAC/D;AAAA,EAEQ,iBAAiB,QAAwB;AAC/C,WAAO,GAAG,MAAM,IAAI,KAAK,KAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,UAAU,CAAC,CAAC;AAAA,EAC3E;AAAA,EAEQ,aAAa,WAA0B;AAC7C,SAAK,YAAY;AACjB,SAAK,aAAa,QAAQ,gBAAgB,SAAS;AACnD,SAAK,OAAO,eAAe,SAAS;AAAA,EACtC;AAAA,EAEQ,UAAU,QAA+B;AAC/C,SAAK,SAAS;AACd,SAAK,aAAa,QAAQ,eAAe,MAAM;AAAA,EACjD;AAAA,EAEQ,SAAS,OAAgC;AAC/C,SAAK,QAAQ;AACb,SAAK,aAAa,QAAQ,cAAc,KAAK;AAC7C,SAAK,OAAO,aAAa,OAAO,WAAW,IAAI;AAAA,EACjD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,cACZ,QAC2B;AAE3B,SAAK,kBAAkB,KAAK,iBAAiB,QAAQ;AAGrD,SAAK,UAAU,wBAAA;AAGf,qBAAiB,SAAS,QAAQ;AAChC,WAAK,aAAa,QAAQ,QAAQ,KAAK;AACvC,WAAK,UAAU,aAAa,KAAK;AAGjC,UAAI,CAAC,KAAK,kBAAkB;AAC1B,cAAM,eACJ,KAAK,UAAU,6BAAA,KAAkC;AACnD,YAAI,cAAc;AAChB,eAAK,mBAAmB;AAExB,gBAAM,mBAAmB,KAAK,UAC3B,YAAA,EACA,KAAK,CAAC,MAAiB,EAAE,OAAO,YAAY;AAC/C,cAAI,kBAAkB;AACpB,iBAAK,OAAO;AAAA,cACV;AAAA,cACA,KAAK,mBAAmB;AAAA,YAAA;AAAA,UAE5B;AAAA,QACF;AAAA,MACF;AAGA,YAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,CAAC,CAAC;AAAA,IACvD;AAIA,QAAI,KAAK,sBAAsB,OAAO,GAAG;AACvC,YAAM,QAAQ,IAAI,KAAK,sBAAsB,QAAQ;AAAA,IACvD;AAGA,SAAK,UAAU,eAAA;AAGf,UAAM,YAAY,KAAK,UAAU,6BAAA;AAGjC,SAAK,kBAAkB;AACvB,SAAK,mBAAmB;AAGxB,QAAI,WAAW;AACb,YAAM,WAAW,KAAK,UAAU,YAAA;AAChC,aAAO,SAAS,KAAK,CAAC,MAAiB,EAAE,OAAO,SAAS,KAAK;AAAA,IAChE;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyCA,MAAM,YACJ,SACA,MACe;AACf,UAAM,eAAe,OAAO,YAAY,YAAY,CAAC,QAAQ,KAAA;AAC7D,QAAI,gBAAgB,KAAK,WAAW;AAClC;AAAA,IACF;AAEA,UAAM,oBAAoB,KAAK,sBAAsB,OAAO;AAG5D,SAAK,qBAAqB;AAG1B,UAAM,cAAc,KAAK,UAAU;AAAA,MACjC,kBAAkB;AAAA,MAClB,kBAAkB;AAAA,IAAA;AAEpB,SAAK,OAAO,YAAY,YAAY,IAAI,kBAAkB,OAAO;AAEjE,UAAM,KAAK,eAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,sBAAsB,OAG5B;AACA,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO,EAAE,SAAS,MAAM,OAAK;AAAA,IAC/B;AACA,WAAO,EAAE,SAAS,MAAM,SAAS,IAAI,MAAM,GAAA;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAO,SAAkD;AAE7D,UAAM,oBAAoB,qBAAqB,SAAS,iBAAiB;AAGzE,QAAI,kBAAkB,SAAS,UAAU;AACvC;AAAA,IACF;AAGA,UAAM,YAAY;AAGlB,SAAK,OAAO,gBAAgB,SAAS;AAGrC,UAAM,WAAW,KAAK,UAAU,YAAA;AAChC,SAAK,UAAU,YAAY,CAAC,GAAG,UAAU,SAAS,CAAC;AAGnD,QAAI,KAAK,WAAW;AAClB,WAAK,sBAAsB,MAAM,KAAK,eAAA,CAAgB;AACtD;AAAA,IACF;AAEA,UAAM,KAAK,eAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,iBAAgC;AAE5C,QAAI,KAAK,WAAW;AAClB;AAAA,IACF;AAEA,SAAK,aAAa,IAAI;AACtB,SAAK,UAAU,WAAW;AAC1B,SAAK,SAAS,MAAS;AACvB,SAAK,kBAAkB,IAAI,gBAAA;AAE3B,SAAK,sBAAsB,MAAA;AAC3B,QAAI,8BAA8B;AAElC,QAAI;AAEF,YAAM,WAAW,KAAK,UAAU,YAAA;AAGhC,YAAM,KAAK,aAAa,QAAQ,WAAA;AAIhC,YAAM,aAAa;AAAA,QACjB,GAAG,KAAK;AAAA,QACR,GAAG,KAAK;AAAA,QACR,gBAAgB,KAAK;AAAA,MAAA;AAIvB,WAAK,qBAAqB;AAG1B,YAAM,SAAS,KAAK,WAAW;AAAA,QAC7B;AAAA,QACA;AAAA,QACA,KAAK,gBAAgB;AAAA,MAAA;AAGvB,YAAM,KAAK,cAAc,MAAM;AAC/B,oCAA8B;AAAA,IAChC,SAAS,KAAK;AACZ,UAAI,eAAe,OAAO;AACxB,YAAI,IAAI,SAAS,cAAc;AAC7B;AAAA,QACF;AACA,aAAK,SAAS,GAAG;AACjB,aAAK,UAAU,OAAO;AACtB,aAAK,aAAa,QAAQ,QAAQ,GAAG;AAAA,MACvC;AAAA,IACF,UAAA;AACE,WAAK,kBAAkB;AACvB,WAAK,aAAa,KAAK;AACvB,WAAK,qBAAqB;AAG1B,YAAM,KAAK,uBAAA;AAGX,UAAI,6BAA6B;AAC/B,cAAM,WAAW,KAAK,UAAU,YAAA;AAChC,cAAM,WAAW,SAAS,GAAG,EAAE,GAAG,MAAM,GAAG,EAAE;AAE7C,YAAI,UAAU,SAAS,iBAAiB,KAAK,kBAAkB;AAC7D,cAAI;AACF,kBAAM,KAAK,qBAAA;AAAA,UACb,SAAS,OAAO;AACd,oBAAQ,MAAM,8CAA8C,KAAK;AAAA,UACnE;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAwB;AAC5B,UAAM,WAAW,KAAK,UAAU,YAAA;AAChC,QAAI,SAAS,WAAW,EAAG;AAG3B,UAAM,uBAAuB,SAAS;AAAA,MACpC,CAAC,MAAiB,EAAE,SAAS;AAAA,IAAA;AAG/B,QAAI,yBAAyB,GAAI;AAEjC,SAAK,OAAO,SAAS,oBAAoB;AAGzC,SAAK,UAAU,oBAAoB,oBAAoB;AAGvD,UAAM,KAAK,eAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACX,QAAI,KAAK,iBAAiB;AACxB,WAAK,gBAAgB,MAAA;AACrB,WAAK,kBAAkB;AAAA,IACzB;AACA,SAAK,aAAa,KAAK;AACvB,SAAK,UAAU,OAAO;AACtB,SAAK,OAAO,QAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,UAAU,cAAA;AACf,SAAK,SAAS,MAAS;AACvB,SAAK,OAAO,gBAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,cAAc,QAMF;AAChB,SAAK,OAAO;AAAA,MACV,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO,SAAS;AAAA,IAAA;AAIlB,SAAK,UAAU;AAAA,MACb,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,IAAA;AAIT,QAAI,KAAK,WAAW;AAClB,WAAK,sBAAsB,MAAM,KAAK,qBAAA,CAAsB;AAC5D;AAAA,IACF;AAEA,UAAM,KAAK,qBAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,wBAAwB,UAGZ;AAEhB,UAAM,WAAW,KAAK,UAAU,YAAA;AAChC,QAAI;AAEJ,eAAW,OAAO,UAAU;AAC1B,YAAM,eAAe,IAAI,MAAM;AAAA,QAC7B,CAAC,MACC,EAAE,SAAS,eAAe,EAAE,UAAU,OAAO,SAAS;AAAA,MAAA;AAE1D,UAAI,cAAc;AAChB,0BAAkB,aAAa;AAC/B;AAAA,MACF;AAAA,IACF;AAEA,QAAI,iBAAiB;AACnB,WAAK,OAAO;AAAA,QACV,SAAS;AAAA,QACT;AAAA,QACA,SAAS;AAAA,MAAA;AAAA,IAEb;AAGA,SAAK,UAAU,wBAAwB,SAAS,IAAI,SAAS,QAAQ;AAGrE,QAAI,KAAK,WAAW;AAClB,WAAK,sBAAsB,MAAM,KAAK,qBAAA,CAAsB;AAC5D;AAAA,IACF;AAEA,UAAM,KAAK,qBAAA;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKQ,sBAAsB,QAAmC;AAC/D,SAAK,kBAAkB,KAAK,MAAM;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,yBAAwC;AACpD,WAAO,KAAK,kBAAkB,SAAS,GAAG;AACxC,YAAM,SAAS,KAAK,kBAAkB,MAAA;AACtC,YAAM,OAAA;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,uBAAsC;AAElD,QAAI,KAAK,uBAAuB,KAAK,WAAW;AAC9C;AAAA,IACF;AAEA,QAAI,KAAK,kBAAkB;AACzB,WAAK,sBAAsB;AAC3B,UAAI;AACF,cAAM,KAAK,eAAA;AAAA,MACb,UAAA;AACE,aAAK,sBAAsB;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,iBAA0B;AAChC,WAAO,KAAK,UAAU,oBAAA;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,cAAgC;AAC9B,WAAO,KAAK,UAAU,YAAA;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA,EAKA,eAAwB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,YAA6B;AAC3B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,WAA8B;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,oBAAoB,UAAkC;AACpD,SAAK,UAAU,YAAY,QAAQ;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA,EAKA,cAAc,SAQL;AACP,QAAI,QAAQ,eAAe,QAAW;AACpC,WAAK,aAAa,QAAQ;AAAA,IAC5B;AACA,QAAI,QAAQ,SAAS,QAAW;AAC9B,WAAK,OAAO,QAAQ;AAAA,IACtB;AACA,QAAI,QAAQ,UAAU,QAAW;AAC/B,WAAK,eAAe,UAAU,oBAAI,IAAA;AAClC,iBAAW,QAAQ,QAAQ,OAAO;AAChC,aAAK,eAAe,QAAQ,IAAI,KAAK,MAAM,IAAI;AAAA,MACjD;AAAA,IACF;AACA,QAAI,QAAQ,eAAe,QAAW;AACpC,WAAK,aAAa,QAAQ,aAAa,QAAQ;AAAA,IACjD;AACA,QAAI,QAAQ,YAAY,QAAW;AACjC,WAAK,aAAa,QAAQ,UAAU,QAAQ;AAAA,IAC9C;AACA,QAAI,QAAQ,aAAa,QAAW;AAClC,WAAK,aAAa,QAAQ,WAAW,QAAQ;AAAA,IAC/C;AACA,QAAI,QAAQ,YAAY,QAAW;AACjC,WAAK,aAAa,QAAQ,UAAU,QAAQ;AAAA,IAC9C;AAAA,EACF;AACF;"}
1
+ {"version":3,"file":"chat-client.js","sources":["../../src/chat-client.ts"],"sourcesContent":["import {\n StreamProcessor,\n generateMessageId,\n normalizeToUIMessage,\n} from '@tanstack/ai'\nimport { DefaultChatClientEventEmitter } from './events'\nimport type {\n AnyClientTool,\n ContentPart,\n ModelMessage,\n StreamChunk,\n} from '@tanstack/ai'\nimport type { ConnectionAdapter } from './connection-adapters'\nimport type { ChatClientEventEmitter } from './events'\nimport type {\n ChatClientOptions,\n ChatClientState,\n MessagePart,\n MultimodalContent,\n ToolCallPart,\n UIMessage,\n} from './types'\n\nexport class ChatClient {\n private processor: StreamProcessor\n private connection: ConnectionAdapter\n private uniqueId: string\n private body: Record<string, any> = {}\n private pendingMessageBody: Record<string, any> | undefined = undefined\n private isLoading = false\n private error: Error | undefined = undefined\n private status: ChatClientState = 'ready'\n private abortController: AbortController | null = null\n private events: ChatClientEventEmitter\n private clientToolsRef: { current: Map<string, AnyClientTool> }\n private currentStreamId: string | null = null\n private currentMessageId: string | null = null\n private postStreamActions: Array<() => Promise<void>> = []\n // Track pending client tool executions to await them before stream finalization\n private pendingToolExecutions: Map<string, Promise<void>> = new Map()\n // Flag to deduplicate continuation checks during action draining\n private continuationPending = false\n // Tracks whether a queued checkForContinuation was skipped because\n // continuationPending was true (chained approval scenario)\n private continuationSkipped = false\n\n private callbacksRef: {\n current: {\n onResponse: (response?: Response) => void | Promise<void>\n onChunk: (chunk: StreamChunk) => void\n onFinish: (message: UIMessage) => void\n onError: (error: Error) => void\n onMessagesChange: (messages: Array<UIMessage>) => void\n onLoadingChange: (isLoading: boolean) => void\n onErrorChange: (error: Error | undefined) => void\n onStatusChange: (status: ChatClientState) => void\n onCustomEvent: (\n eventType: string,\n data: unknown,\n context: { toolCallId?: string },\n ) => void\n }\n }\n\n constructor(options: ChatClientOptions) {\n this.uniqueId = options.id || this.generateUniqueId('chat')\n this.body = options.body || {}\n this.connection = options.connection\n this.events = new DefaultChatClientEventEmitter(this.uniqueId)\n\n // Build client tools map\n this.clientToolsRef = { current: new Map() }\n if (options.tools) {\n for (const tool of options.tools) {\n this.clientToolsRef.current.set(tool.name, tool)\n }\n }\n\n this.callbacksRef = {\n current: {\n onResponse: options.onResponse || (() => {}),\n onChunk: options.onChunk || (() => {}),\n onFinish: options.onFinish || (() => {}),\n onError: options.onError || (() => {}),\n onMessagesChange: options.onMessagesChange || (() => {}),\n onLoadingChange: options.onLoadingChange || (() => {}),\n onErrorChange: options.onErrorChange || (() => {}),\n onStatusChange: options.onStatusChange || (() => {}),\n onCustomEvent: options.onCustomEvent || (() => {}),\n },\n }\n\n // Create StreamProcessor with event handlers\n this.processor = new StreamProcessor({\n chunkStrategy: options.streamProcessor?.chunkStrategy,\n initialMessages: options.initialMessages,\n events: {\n onMessagesChange: (messages: Array<UIMessage>) => {\n this.callbacksRef.current.onMessagesChange(messages)\n },\n onStreamStart: () => {\n this.setStatus('streaming')\n },\n onStreamEnd: (message: UIMessage) => {\n this.callbacksRef.current.onFinish(message)\n this.setStatus('ready')\n },\n onError: (error: Error) => {\n this.setError(error)\n this.setStatus('error')\n this.callbacksRef.current.onError(error)\n },\n onTextUpdate: (messageId: string, content: string) => {\n // Emit text update to devtools\n if (this.currentStreamId) {\n this.events.textUpdated(this.currentStreamId, messageId, content)\n }\n },\n onThinkingUpdate: (messageId: string, content: string) => {\n // Emit thinking update to devtools\n if (this.currentStreamId) {\n this.events.thinkingUpdated(\n this.currentStreamId,\n messageId,\n content,\n )\n }\n },\n onToolCallStateChange: (\n messageId: string,\n toolCallId: string,\n state: string,\n args: string,\n ) => {\n // Get the tool name from the messages\n const messages = this.processor.getMessages()\n const message = messages.find((m: UIMessage) => m.id === messageId)\n const toolCallPart = message?.parts.find(\n (p: MessagePart): p is ToolCallPart =>\n p.type === 'tool-call' && p.id === toolCallId,\n )\n const toolName = toolCallPart?.name || 'unknown'\n\n // Emit tool call state change to devtools\n if (this.currentStreamId) {\n this.events.toolCallStateChanged(\n this.currentStreamId,\n messageId,\n toolCallId,\n toolName,\n state,\n args,\n )\n }\n },\n onToolCall: (args: {\n toolCallId: string\n toolName: string\n input: any\n }) => {\n // Handle client-side tool execution automatically\n const clientTool = this.clientToolsRef.current.get(args.toolName)\n const executeFunc = clientTool?.execute\n if (executeFunc) {\n // Create and track the execution promise\n const executionPromise = (async () => {\n try {\n const output = await executeFunc(args.input)\n await this.addToolResult({\n toolCallId: args.toolCallId,\n tool: args.toolName,\n output,\n state: 'output-available',\n })\n } catch (error: any) {\n await this.addToolResult({\n toolCallId: args.toolCallId,\n tool: args.toolName,\n output: null,\n state: 'output-error',\n errorText: error.message,\n })\n } finally {\n // Remove from pending when complete\n this.pendingToolExecutions.delete(args.toolCallId)\n }\n })()\n\n // Track the pending execution\n this.pendingToolExecutions.set(args.toolCallId, executionPromise)\n }\n },\n onApprovalRequest: (args: {\n toolCallId: string\n toolName: string\n input: any\n approvalId: string\n }) => {\n if (this.currentStreamId) {\n this.events.approvalRequested(\n this.currentStreamId,\n this.currentMessageId || '',\n args.toolCallId,\n args.toolName,\n args.input,\n args.approvalId,\n )\n }\n },\n onCustomEvent: (\n eventType: string,\n data: unknown,\n context: { toolCallId?: string },\n ) => {\n this.callbacksRef.current.onCustomEvent(eventType, data, context)\n },\n },\n })\n\n this.events.clientCreated(this.processor.getMessages().length)\n }\n\n private generateUniqueId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n }\n\n private setIsLoading(isLoading: boolean): void {\n this.isLoading = isLoading\n this.callbacksRef.current.onLoadingChange(isLoading)\n this.events.loadingChanged(isLoading)\n }\n\n private setStatus(status: ChatClientState): void {\n this.status = status\n this.callbacksRef.current.onStatusChange(status)\n }\n\n private setError(error: Error | undefined): void {\n this.error = error\n this.callbacksRef.current.onErrorChange(error)\n this.events.errorChanged(error?.message || null)\n }\n\n /**\n * Process a stream through the StreamProcessor\n */\n private async processStream(\n source: AsyncIterable<StreamChunk>,\n ): Promise<UIMessage | null> {\n // Generate a stream ID for this streaming operation\n this.currentStreamId = this.generateUniqueId('stream')\n\n // Prepare for a new assistant message (created lazily on first content)\n this.processor.prepareAssistantMessage()\n\n // Process each chunk\n for await (const chunk of source) {\n this.callbacksRef.current.onChunk(chunk)\n this.processor.processChunk(chunk)\n\n // Track the message ID once the processor lazily creates it\n if (!this.currentMessageId) {\n const newMessageId =\n this.processor.getCurrentAssistantMessageId() ?? null\n if (newMessageId) {\n this.currentMessageId = newMessageId\n // Emit message appended event now that the assistant message exists\n const assistantMessage = this.processor\n .getMessages()\n .find((m: UIMessage) => m.id === newMessageId)\n if (assistantMessage) {\n this.events.messageAppended(\n assistantMessage,\n this.currentStreamId || undefined,\n )\n }\n }\n }\n\n // Yield control back to event loop to allow UI updates\n await new Promise((resolve) => setTimeout(resolve, 0))\n }\n\n // Wait for all pending tool executions to complete before finalizing\n // This ensures client tools finish before we check for continuation\n if (this.pendingToolExecutions.size > 0) {\n await Promise.all(this.pendingToolExecutions.values())\n }\n\n // Finalize the stream\n this.processor.finalizeStream()\n\n // Get the message ID (may be null if no content arrived)\n const messageId = this.processor.getCurrentAssistantMessageId()\n\n // Clear the current stream and message IDs\n this.currentStreamId = null\n this.currentMessageId = null\n\n // Return the assistant message if one was created\n if (messageId) {\n const messages = this.processor.getMessages()\n return messages.find((m: UIMessage) => m.id === messageId) || null\n }\n\n return null\n }\n\n /**\n * Send a message and stream the response.\n * Supports both simple string content and multimodal content (images, audio, video, documents).\n *\n * @param content - The message content. Can be:\n * - A simple string for text-only messages\n * - A MultimodalContent object with content array and optional custom ID\n * @param body - Optional body parameters to merge with the client's base body for this request.\n * Uses shallow merge with per-message body taking priority.\n *\n * @example\n * ```ts\n * // Simple text message\n * await client.sendMessage('Hello!')\n *\n * // Text message with custom body params\n * await client.sendMessage('Hello!', { temperature: 0.7 })\n *\n * // Multimodal message with image\n * await client.sendMessage({\n * content: [\n * { type: 'text', content: 'What is in this image?' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }\n * ]\n * })\n *\n * // Multimodal message with custom ID and body params\n * await client.sendMessage(\n * {\n * content: [\n * { type: 'text', content: 'Describe this audio' },\n * { type: 'audio', source: { type: 'data', value: 'base64...' } }\n * ],\n * id: 'custom-message-id'\n * },\n * { model: 'gpt-4-audio' }\n * )\n * ```\n */\n async sendMessage(\n content: string | MultimodalContent,\n body?: Record<string, any>,\n ): Promise<void> {\n const emptyMessage = typeof content === 'string' && !content.trim()\n if (emptyMessage || this.isLoading) {\n return\n }\n // Normalize input to extract content, id, and validate\n const normalizedContent = this.normalizeMessageInput(content)\n\n // Store the per-message body for use in streamResponse\n this.pendingMessageBody = body\n\n // Add user message via processor\n const userMessage = this.processor.addUserMessage(\n normalizedContent.content,\n normalizedContent.id,\n )\n this.events.messageSent(userMessage.id, normalizedContent.content)\n\n await this.streamResponse()\n }\n\n /**\n * Normalize the message input to extract content and optional id.\n * Trims string content automatically.\n */\n private normalizeMessageInput(input: string | MultimodalContent): {\n content: string | Array<ContentPart>\n id?: string\n } {\n if (typeof input === 'string') {\n return { content: input.trim() }\n }\n return { content: input.content, id: input.id }\n }\n\n /**\n * Append a message and stream the response\n */\n async append(message: UIMessage | ModelMessage): Promise<void> {\n // Normalize the message to ensure it has id and createdAt\n const normalizedMessage = normalizeToUIMessage(message, generateMessageId)\n\n // Skip system messages - they're handled via systemPrompts, not UIMessages\n if (normalizedMessage.role === 'system') {\n return\n }\n\n // Type assertion: after checking for system, we know it's user or assistant\n const uiMessage = normalizedMessage as UIMessage\n\n // Emit message appended event\n this.events.messageAppended(uiMessage)\n\n // Add to messages\n const messages = this.processor.getMessages()\n this.processor.setMessages([...messages, uiMessage])\n\n // If stream is in progress, queue the response for after it ends\n if (this.isLoading) {\n this.queuePostStreamAction(async () => {\n await this.streamResponse()\n })\n return\n }\n\n await this.streamResponse()\n }\n\n /**\n * Stream a response from the LLM.\n * Returns true if the stream completed successfully, false on abort or error.\n */\n private async streamResponse(): Promise<boolean> {\n // Guard against concurrent streams - if already loading, skip\n if (this.isLoading) {\n return false\n }\n\n this.setIsLoading(true)\n this.setStatus('submitted')\n this.setError(undefined)\n this.abortController = new AbortController()\n // Reset pending tool executions for the new stream\n this.pendingToolExecutions.clear()\n let streamCompletedSuccessfully = false\n\n try {\n // Get UIMessages with parts (preserves approval state and client tool results)\n const messages = this.processor.getMessages()\n\n // Call onResponse callback\n await this.callbacksRef.current.onResponse()\n\n // Merge body: base body + per-message body (per-message takes priority)\n // Include conversationId for server-side event correlation\n const mergedBody = {\n ...this.body,\n ...this.pendingMessageBody,\n conversationId: this.uniqueId,\n }\n\n // Clear the pending message body after use\n this.pendingMessageBody = undefined\n\n // Connect and stream\n const stream = this.connection.connect(\n messages,\n mergedBody,\n this.abortController.signal,\n )\n\n await this.processStream(stream)\n streamCompletedSuccessfully = true\n } catch (err) {\n if (err instanceof Error) {\n if (err.name === 'AbortError') {\n return false\n }\n this.setError(err)\n this.setStatus('error')\n this.callbacksRef.current.onError(err)\n }\n } finally {\n this.abortController = null\n this.setIsLoading(false)\n this.pendingMessageBody = undefined // Ensure it's cleared even on error\n\n // Drain any actions that were queued while the stream was in progress\n await this.drainPostStreamActions()\n\n // Continue conversation if the stream ended with a tool result (server tool completed)\n if (streamCompletedSuccessfully) {\n const messages = this.processor.getMessages()\n const lastPart = messages.at(-1)?.parts.at(-1)\n\n if (lastPart?.type === 'tool-result' && this.shouldAutoSend()) {\n try {\n await this.checkForContinuation()\n } catch (error) {\n console.error('Failed to continue flow after tool result:', error)\n }\n }\n }\n }\n\n return streamCompletedSuccessfully\n }\n\n /**\n * Reload the last assistant message\n */\n async reload(): Promise<void> {\n const messages = this.processor.getMessages()\n if (messages.length === 0) return\n\n // Find the last user message\n const lastUserMessageIndex = messages.findLastIndex(\n (m: UIMessage) => m.role === 'user',\n )\n\n if (lastUserMessageIndex === -1) return\n\n this.events.reloaded(lastUserMessageIndex)\n\n // Remove all messages after the last user message\n this.processor.removeMessagesAfter(lastUserMessageIndex)\n\n // Resend\n await this.streamResponse()\n }\n\n /**\n * Stop the current stream\n */\n stop(): void {\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n this.setStatus('ready')\n this.events.stopped()\n }\n\n /**\n * Clear all messages\n */\n clear(): void {\n this.processor.clearMessages()\n this.setError(undefined)\n this.events.messagesCleared()\n }\n\n /**\n * Add the result of a client-side tool execution\n */\n async addToolResult(result: {\n toolCallId: string\n tool: string\n output: any\n state?: 'output-available' | 'output-error'\n errorText?: string\n }): Promise<void> {\n this.events.toolResultAdded(\n result.toolCallId,\n result.tool,\n result.output,\n result.state || 'output-available',\n )\n\n // Add result via processor\n this.processor.addToolResult(\n result.toolCallId,\n result.output,\n result.errorText,\n )\n\n // If stream is in progress, queue continuation check for after it ends\n if (this.isLoading) {\n this.queuePostStreamAction(() => this.checkForContinuation())\n return\n }\n\n await this.checkForContinuation()\n }\n\n /**\n * Respond to a tool approval request\n */\n async addToolApprovalResponse(response: {\n id: string // approval.id, not toolCallId\n approved: boolean\n }): Promise<void> {\n // Find the tool call ID from the approval ID\n const messages = this.processor.getMessages()\n let foundToolCallId: string | undefined\n\n for (const msg of messages) {\n const toolCallPart = msg.parts.find(\n (p: MessagePart): p is ToolCallPart =>\n p.type === 'tool-call' && p.approval?.id === response.id,\n )\n if (toolCallPart) {\n foundToolCallId = toolCallPart.id\n break\n }\n }\n\n if (foundToolCallId) {\n this.events.toolApprovalResponded(\n response.id,\n foundToolCallId,\n response.approved,\n )\n }\n\n // Add response via processor\n this.processor.addToolApprovalResponse(response.id, response.approved)\n\n // If stream is in progress, queue continuation check for after it ends\n if (this.isLoading) {\n this.queuePostStreamAction(() => this.checkForContinuation())\n return\n }\n\n await this.checkForContinuation()\n }\n\n /**\n * Queue an action to be executed after the current stream ends\n */\n private queuePostStreamAction(action: () => Promise<void>): void {\n this.postStreamActions.push(action)\n }\n\n /**\n * Drain and execute all queued post-stream actions\n */\n private async drainPostStreamActions(): Promise<void> {\n while (this.postStreamActions.length > 0) {\n const action = this.postStreamActions.shift()!\n await action()\n }\n }\n\n /**\n * Check if we should continue the flow and do so if needed\n */\n private async checkForContinuation(): Promise<void> {\n // Prevent duplicate continuation attempts\n if (this.continuationPending || this.isLoading) {\n this.continuationSkipped = true\n return\n }\n\n if (this.shouldAutoSend()) {\n this.continuationPending = true\n this.continuationSkipped = false\n let succeeded = false\n try {\n succeeded = await this.streamResponse()\n } finally {\n this.continuationPending = false\n }\n // If a queued check was skipped while continuationPending was true\n // (e.g. a chained approval responded to during the stream), re-evaluate\n // now that the flag is cleared. Only replay after a successful stream —\n // aborted or errored streams should not trigger further continuation.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- mutated asynchronously during await\n if (this.continuationSkipped && succeeded) {\n this.continuationSkipped = false\n await this.checkForContinuation()\n }\n }\n }\n\n /**\n * Check if all tool calls are complete and we should auto-send\n */\n private shouldAutoSend(): boolean {\n return this.processor.areAllToolsComplete()\n }\n\n /**\n * Get current messages\n */\n getMessages(): Array<UIMessage> {\n return this.processor.getMessages()\n }\n\n /**\n * Get loading state\n */\n getIsLoading(): boolean {\n return this.isLoading\n }\n\n /**\n * Get current status\n */\n getStatus(): ChatClientState {\n return this.status\n }\n\n /**\n * Get current error\n */\n getError(): Error | undefined {\n return this.error\n }\n\n /**\n * Manually set messages\n */\n setMessagesManually(messages: Array<UIMessage>): void {\n this.processor.setMessages(messages)\n }\n\n /**\n * Update options refs (for use in React hooks to avoid recreating client)\n */\n updateOptions(options: {\n connection?: ConnectionAdapter\n body?: Record<string, any>\n tools?: ReadonlyArray<AnyClientTool>\n onResponse?: (response?: Response) => void | Promise<void>\n onChunk?: (chunk: StreamChunk) => void\n onFinish?: (message: UIMessage) => void\n onError?: (error: Error) => void\n onCustomEvent?: (\n eventType: string,\n data: unknown,\n context: { toolCallId?: string },\n ) => void\n }): void {\n if (options.connection !== undefined) {\n this.connection = options.connection\n }\n if (options.body !== undefined) {\n this.body = options.body\n }\n if (options.tools !== undefined) {\n this.clientToolsRef.current = new Map()\n for (const tool of options.tools) {\n this.clientToolsRef.current.set(tool.name, tool)\n }\n }\n if (options.onResponse !== undefined) {\n this.callbacksRef.current.onResponse = options.onResponse\n }\n if (options.onChunk !== undefined) {\n this.callbacksRef.current.onChunk = options.onChunk\n }\n if (options.onFinish !== undefined) {\n this.callbacksRef.current.onFinish = options.onFinish\n }\n if (options.onError !== undefined) {\n this.callbacksRef.current.onError = options.onError\n }\n if (options.onCustomEvent !== undefined) {\n this.callbacksRef.current.onCustomEvent = options.onCustomEvent\n }\n }\n}\n"],"names":[],"mappings":"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"}
@@ -174,6 +174,17 @@ export interface ChatClientOptions<TTools extends ReadonlyArray<AnyClientTool> =
174
174
  * Callback when chat status changes
175
175
  */
176
176
  onStatusChange?: (status: ChatClientState) => void;
177
+ /**
178
+ * Callback when a custom event is received from a server-side tool.
179
+ * Custom events are emitted by tools using `context.emitCustomEvent()` during execution.
180
+ *
181
+ * @param eventType - The name of the custom event
182
+ * @param data - The event payload data
183
+ * @param context - Additional context including the toolCallId that emitted the event
184
+ */
185
+ onCustomEvent?: (eventType: string, data: unknown, context: {
186
+ toolCallId?: string;
187
+ }) => void;
177
188
  /**
178
189
  * Client-side tools with execution logic
179
190
  * When provided, tools with execute functions will be called automatically
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sources":["../../src/types.ts"],"sourcesContent":["import type {\n AnyClientTool,\n AudioPart,\n ChunkStrategy,\n ContentPart,\n DocumentPart,\n ImagePart,\n InferToolInput,\n InferToolOutput,\n ModelMessage,\n StreamChunk,\n VideoPart,\n} from '@tanstack/ai'\nimport type { ConnectionAdapter } from './connection-adapters'\n\n/**\n * Tool call states - track the lifecycle of a tool call\n */\nexport type ToolCallState =\n | 'awaiting-input' // Received start but no arguments yet\n | 'input-streaming' // Partial arguments received\n | 'input-complete' // All arguments received\n | 'approval-requested' // Waiting for user approval\n | 'approval-responded' // User has approved/denied\n\n/**\n * Tool result states - track the lifecycle of a tool result\n */\nexport type ToolResultState =\n | 'streaming' // Placeholder for future streamed output\n | 'complete' // Result is complete\n | 'error' // Error occurred\n\n/**\n * ChatClient state - track the lifecycle of a chat\n */\nexport type ChatClientState = 'ready' | 'submitted' | 'streaming' | 'error'\n\n/**\n * Multimodal content input for sending messages with rich media.\n * Allows sending text, images, audio, video, and documents to the LLM.\n *\n * @example\n * ```ts\n * // Send an image with a question\n * client.sendMessage({\n * content: [\n * { type: 'text', content: 'What is in this image?' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }\n * ],\n * id: 'custom-message-id' // optional\n * })\n * ```\n */\nexport interface MultimodalContent {\n /**\n * The content of the message.\n * Can be a simple string or an array of content parts for multimodal messages.\n */\n content: string | Array<ContentPart>\n /**\n * Optional custom ID for the message.\n * If not provided, a unique ID will be generated.\n */\n id?: string\n}\n\n/**\n * Message parts - building blocks of UIMessage\n */\nexport interface TextPart {\n type: 'text'\n content: string\n}\n\n/**\n * Helper type that creates a tool-call part for a specific tool.\n * This is a conditional type to enable proper distribution over union types,\n * creating a discriminated union where `name` is the discriminant.\n */\ntype ToolCallPartForTool<T> = T extends AnyClientTool\n ? {\n type: 'tool-call'\n id: string\n name: T['name']\n arguments: string // JSON string (may be incomplete)\n /** Parsed tool input (typed from inputSchema) */\n input?: InferToolInput<T>\n state: ToolCallState\n /** Approval metadata if tool requires user approval */\n approval?: {\n id: string // Unique approval ID\n needsApproval: boolean // Always true if present\n approved?: boolean // User's decision (undefined until responded)\n }\n /** Tool execution output (for client tools or after approval) */\n output?: InferToolOutput<T>\n }\n : never\n\n/**\n * Fallback tool-call part type when tools are not typed\n */\ntype UntypedToolCallPart = {\n type: 'tool-call'\n id: string\n name: string\n arguments: string\n input?: any\n state: ToolCallState\n approval?: {\n id: string\n needsApproval: boolean\n approved?: boolean\n }\n output?: any\n}\n\n/**\n * Tool call part that creates a proper discriminated union.\n * When TTools is typed, checking `part.name === 'toolName'` will narrow\n * `part.output` to the correct type for that tool.\n *\n * The discriminant is `name`, so code like:\n * ```ts\n * if (part.name === 'recommendGuitar') {\n * // part.output is now typed to the recommendGuitar tool's output\n * }\n * ```\n */\nexport type ToolCallPart<TTools extends ReadonlyArray<AnyClientTool> = any> =\n // Check if we have a concrete tools array (not 'any' or 'never')\n [TTools] extends [never]\n ? UntypedToolCallPart\n : unknown extends TTools\n ? UntypedToolCallPart\n : TTools extends ReadonlyArray<infer Tool>\n ? Tool extends AnyClientTool\n ? ToolCallPartForTool<Tool>\n : UntypedToolCallPart\n : UntypedToolCallPart\n\nexport interface ToolResultPart {\n type: 'tool-result'\n toolCallId: string\n content: string\n state: ToolResultState\n error?: string // Error message if state is \"error\"\n}\n\nexport interface ThinkingPart {\n type: 'thinking'\n content: string\n}\n\nexport type MessagePart<TTools extends ReadonlyArray<AnyClientTool> = any> =\n | TextPart\n | ImagePart\n | AudioPart\n | VideoPart\n | DocumentPart\n | ToolCallPart<TTools>\n | ToolResultPart\n | ThinkingPart\n\n/**\n * UIMessage - Domain-specific message format optimized for building chat UIs\n * Contains parts that can be text, tool calls, or tool results\n */\nexport interface UIMessage<TTools extends ReadonlyArray<AnyClientTool> = any> {\n id: string\n role: 'system' | 'user' | 'assistant'\n parts: Array<MessagePart<TTools>>\n createdAt?: Date\n}\n\nexport interface ChatClientOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> {\n /**\n * Connection adapter for streaming\n * Use fetchServerSentEvents(), fetchHttpStream(), or stream() to create adapters\n */\n connection: ConnectionAdapter\n\n /**\n * Initial messages to populate the chat\n */\n initialMessages?: Array<UIMessage<TTools>>\n\n /**\n * Unique identifier for this chat instance\n * Used for managing multiple chats\n */\n id?: string\n\n /**\n * Additional body parameters to send\n */\n body?: Record<string, any>\n\n /**\n * Callback when a response is received\n */\n onResponse?: (response?: Response) => void | Promise<void>\n\n /**\n * Callback when a stream chunk is received\n */\n onChunk?: (chunk: StreamChunk) => void\n\n /**\n * Callback when the response is finished\n */\n onFinish?: (message: UIMessage<TTools>) => void\n\n /**\n * Callback when an error occurs\n */\n onError?: (error: Error) => void\n\n /**\n * Callback when messages change\n */\n onMessagesChange?: (messages: Array<UIMessage<TTools>>) => void\n\n /**\n * Callback when loading state changes\n */\n onLoadingChange?: (isLoading: boolean) => void\n\n /**\n * Callback when error state changes\n */\n onErrorChange?: (error: Error | undefined) => void\n\n /**\n * Callback when chat status changes\n */\n onStatusChange?: (status: ChatClientState) => void\n\n /**\n * Client-side tools with execution logic\n * When provided, tools with execute functions will be called automatically\n */\n tools?: TTools\n\n /**\n * Stream processing options (optional)\n * Configure chunking strategy\n */\n streamProcessor?: {\n /**\n * Strategy for when to emit text updates\n * Defaults to ImmediateStrategy (every chunk)\n */\n chunkStrategy?: ChunkStrategy\n }\n}\n\nexport interface ChatRequestBody {\n messages: Array<ModelMessage>\n data?: Record<string, any>\n}\n\n/**\n * Create a typed array of client tools with proper type inference.\n * This eliminates the need for `as const` when defining tool arrays.\n *\n * @example\n * ```ts\n * const tools = clientTools(\n * myTool1.client(() => result1),\n * myTool2.client(() => result2),\n * )\n *\n * // tools is now properly typed as a tuple with literal tool names\n * // This enables type narrowing when checking part.name === 'toolName'\n * ```\n */\nexport function clientTools<const T extends Array<AnyClientTool>>(\n ...tools: T\n): T {\n return tools\n}\n\n/**\n * Helper to create typed chat client options\n * Use this to get proper type inference for messages\n *\n * @example\n * ```ts\n * const tools = clientTools(myTool1, myTool2)\n *\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools,\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * ```\n */\nexport function createChatClientOptions<\n const TTools extends ReadonlyArray<AnyClientTool>,\n>(options: ChatClientOptions<TTools>): ChatClientOptions<TTools> {\n return options\n}\n\n/**\n * Extract the message type from chat options\n *\n * @example\n * ```ts\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools: [myTool1, myTool2],\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * // MyMessages is now Array<UIMessage<[typeof myTool1, typeof myTool2]>>\n * ```\n */\nexport type InferChatMessages<T> =\n T extends ChatClientOptions<infer TTools> ? Array<UIMessage<TTools>> : never\n"],"names":[],"mappings":"AAwRO,SAAS,eACX,OACA;AACH,SAAO;AACT;AAkBO,SAAS,wBAEd,SAA+D;AAC/D,SAAO;AACT;"}
1
+ {"version":3,"file":"types.js","sources":["../../src/types.ts"],"sourcesContent":["import type {\n AnyClientTool,\n AudioPart,\n ChunkStrategy,\n ContentPart,\n DocumentPart,\n ImagePart,\n InferToolInput,\n InferToolOutput,\n ModelMessage,\n StreamChunk,\n VideoPart,\n} from '@tanstack/ai'\nimport type { ConnectionAdapter } from './connection-adapters'\n\n/**\n * Tool call states - track the lifecycle of a tool call\n */\nexport type ToolCallState =\n | 'awaiting-input' // Received start but no arguments yet\n | 'input-streaming' // Partial arguments received\n | 'input-complete' // All arguments received\n | 'approval-requested' // Waiting for user approval\n | 'approval-responded' // User has approved/denied\n\n/**\n * Tool result states - track the lifecycle of a tool result\n */\nexport type ToolResultState =\n | 'streaming' // Placeholder for future streamed output\n | 'complete' // Result is complete\n | 'error' // Error occurred\n\n/**\n * ChatClient state - track the lifecycle of a chat\n */\nexport type ChatClientState = 'ready' | 'submitted' | 'streaming' | 'error'\n\n/**\n * Multimodal content input for sending messages with rich media.\n * Allows sending text, images, audio, video, and documents to the LLM.\n *\n * @example\n * ```ts\n * // Send an image with a question\n * client.sendMessage({\n * content: [\n * { type: 'text', content: 'What is in this image?' },\n * { type: 'image', source: { type: 'url', value: 'https://example.com/photo.jpg' } }\n * ],\n * id: 'custom-message-id' // optional\n * })\n * ```\n */\nexport interface MultimodalContent {\n /**\n * The content of the message.\n * Can be a simple string or an array of content parts for multimodal messages.\n */\n content: string | Array<ContentPart>\n /**\n * Optional custom ID for the message.\n * If not provided, a unique ID will be generated.\n */\n id?: string\n}\n\n/**\n * Message parts - building blocks of UIMessage\n */\nexport interface TextPart {\n type: 'text'\n content: string\n}\n\n/**\n * Helper type that creates a tool-call part for a specific tool.\n * This is a conditional type to enable proper distribution over union types,\n * creating a discriminated union where `name` is the discriminant.\n */\ntype ToolCallPartForTool<T> = T extends AnyClientTool\n ? {\n type: 'tool-call'\n id: string\n name: T['name']\n arguments: string // JSON string (may be incomplete)\n /** Parsed tool input (typed from inputSchema) */\n input?: InferToolInput<T>\n state: ToolCallState\n /** Approval metadata if tool requires user approval */\n approval?: {\n id: string // Unique approval ID\n needsApproval: boolean // Always true if present\n approved?: boolean // User's decision (undefined until responded)\n }\n /** Tool execution output (for client tools or after approval) */\n output?: InferToolOutput<T>\n }\n : never\n\n/**\n * Fallback tool-call part type when tools are not typed\n */\ntype UntypedToolCallPart = {\n type: 'tool-call'\n id: string\n name: string\n arguments: string\n input?: any\n state: ToolCallState\n approval?: {\n id: string\n needsApproval: boolean\n approved?: boolean\n }\n output?: any\n}\n\n/**\n * Tool call part that creates a proper discriminated union.\n * When TTools is typed, checking `part.name === 'toolName'` will narrow\n * `part.output` to the correct type for that tool.\n *\n * The discriminant is `name`, so code like:\n * ```ts\n * if (part.name === 'recommendGuitar') {\n * // part.output is now typed to the recommendGuitar tool's output\n * }\n * ```\n */\nexport type ToolCallPart<TTools extends ReadonlyArray<AnyClientTool> = any> =\n // Check if we have a concrete tools array (not 'any' or 'never')\n [TTools] extends [never]\n ? UntypedToolCallPart\n : unknown extends TTools\n ? UntypedToolCallPart\n : TTools extends ReadonlyArray<infer Tool>\n ? Tool extends AnyClientTool\n ? ToolCallPartForTool<Tool>\n : UntypedToolCallPart\n : UntypedToolCallPart\n\nexport interface ToolResultPart {\n type: 'tool-result'\n toolCallId: string\n content: string\n state: ToolResultState\n error?: string // Error message if state is \"error\"\n}\n\nexport interface ThinkingPart {\n type: 'thinking'\n content: string\n}\n\nexport type MessagePart<TTools extends ReadonlyArray<AnyClientTool> = any> =\n | TextPart\n | ImagePart\n | AudioPart\n | VideoPart\n | DocumentPart\n | ToolCallPart<TTools>\n | ToolResultPart\n | ThinkingPart\n\n/**\n * UIMessage - Domain-specific message format optimized for building chat UIs\n * Contains parts that can be text, tool calls, or tool results\n */\nexport interface UIMessage<TTools extends ReadonlyArray<AnyClientTool> = any> {\n id: string\n role: 'system' | 'user' | 'assistant'\n parts: Array<MessagePart<TTools>>\n createdAt?: Date\n}\n\nexport interface ChatClientOptions<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n> {\n /**\n * Connection adapter for streaming\n * Use fetchServerSentEvents(), fetchHttpStream(), or stream() to create adapters\n */\n connection: ConnectionAdapter\n\n /**\n * Initial messages to populate the chat\n */\n initialMessages?: Array<UIMessage<TTools>>\n\n /**\n * Unique identifier for this chat instance\n * Used for managing multiple chats\n */\n id?: string\n\n /**\n * Additional body parameters to send\n */\n body?: Record<string, any>\n\n /**\n * Callback when a response is received\n */\n onResponse?: (response?: Response) => void | Promise<void>\n\n /**\n * Callback when a stream chunk is received\n */\n onChunk?: (chunk: StreamChunk) => void\n\n /**\n * Callback when the response is finished\n */\n onFinish?: (message: UIMessage<TTools>) => void\n\n /**\n * Callback when an error occurs\n */\n onError?: (error: Error) => void\n\n /**\n * Callback when messages change\n */\n onMessagesChange?: (messages: Array<UIMessage<TTools>>) => void\n\n /**\n * Callback when loading state changes\n */\n onLoadingChange?: (isLoading: boolean) => void\n\n /**\n * Callback when error state changes\n */\n onErrorChange?: (error: Error | undefined) => void\n\n /**\n * Callback when chat status changes\n */\n onStatusChange?: (status: ChatClientState) => void\n\n /**\n * Callback when a custom event is received from a server-side tool.\n * Custom events are emitted by tools using `context.emitCustomEvent()` during execution.\n *\n * @param eventType - The name of the custom event\n * @param data - The event payload data\n * @param context - Additional context including the toolCallId that emitted the event\n */\n onCustomEvent?: (\n eventType: string,\n data: unknown,\n context: { toolCallId?: string },\n ) => void\n\n /**\n * Client-side tools with execution logic\n * When provided, tools with execute functions will be called automatically\n */\n tools?: TTools\n\n /**\n * Stream processing options (optional)\n * Configure chunking strategy\n */\n streamProcessor?: {\n /**\n * Strategy for when to emit text updates\n * Defaults to ImmediateStrategy (every chunk)\n */\n chunkStrategy?: ChunkStrategy\n }\n}\n\nexport interface ChatRequestBody {\n messages: Array<ModelMessage>\n data?: Record<string, any>\n}\n\n/**\n * Create a typed array of client tools with proper type inference.\n * This eliminates the need for `as const` when defining tool arrays.\n *\n * @example\n * ```ts\n * const tools = clientTools(\n * myTool1.client(() => result1),\n * myTool2.client(() => result2),\n * )\n *\n * // tools is now properly typed as a tuple with literal tool names\n * // This enables type narrowing when checking part.name === 'toolName'\n * ```\n */\nexport function clientTools<const T extends Array<AnyClientTool>>(\n ...tools: T\n): T {\n return tools\n}\n\n/**\n * Helper to create typed chat client options\n * Use this to get proper type inference for messages\n *\n * @example\n * ```ts\n * const tools = clientTools(myTool1, myTool2)\n *\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools,\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * ```\n */\nexport function createChatClientOptions<\n const TTools extends ReadonlyArray<AnyClientTool>,\n>(options: ChatClientOptions<TTools>): ChatClientOptions<TTools> {\n return options\n}\n\n/**\n * Extract the message type from chat options\n *\n * @example\n * ```ts\n * const chatOptions = createChatClientOptions({\n * connection: fetchServerSentEvents('/api/chat'),\n * tools: [myTool1, myTool2],\n * })\n *\n * type MyMessages = InferChatMessages<typeof chatOptions>\n * // MyMessages is now Array<UIMessage<[typeof myTool1, typeof myTool2]>>\n * ```\n */\nexport type InferChatMessages<T> =\n T extends ChatClientOptions<infer TTools> ? Array<UIMessage<TTools>> : never\n"],"names":[],"mappings":"AAsSO,SAAS,eACX,OACA;AACH,SAAO;AACT;AAkBO,SAAS,wBAEd,SAA+D;AAC/D,SAAO;AACT;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-client",
3
- "version": "0.4.5",
3
+ "version": "0.5.2",
4
4
  "description": "Framework-agnostic headless client for TanStack AI",
5
5
  "author": "",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "src"
32
32
  ],
33
33
  "dependencies": {
34
- "@tanstack/ai": "0.5.1"
34
+ "@tanstack/ai": "0.6.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@vitest/coverage-v8": "4.0.14",
@@ -40,6 +40,9 @@ export class ChatClient {
40
40
  private pendingToolExecutions: Map<string, Promise<void>> = new Map()
41
41
  // Flag to deduplicate continuation checks during action draining
42
42
  private continuationPending = false
43
+ // Tracks whether a queued checkForContinuation was skipped because
44
+ // continuationPending was true (chained approval scenario)
45
+ private continuationSkipped = false
43
46
 
44
47
  private callbacksRef: {
45
48
  current: {
@@ -51,6 +54,11 @@ export class ChatClient {
51
54
  onLoadingChange: (isLoading: boolean) => void
52
55
  onErrorChange: (error: Error | undefined) => void
53
56
  onStatusChange: (status: ChatClientState) => void
57
+ onCustomEvent: (
58
+ eventType: string,
59
+ data: unknown,
60
+ context: { toolCallId?: string },
61
+ ) => void
54
62
  }
55
63
  }
56
64
 
@@ -78,6 +86,7 @@ export class ChatClient {
78
86
  onLoadingChange: options.onLoadingChange || (() => {}),
79
87
  onErrorChange: options.onErrorChange || (() => {}),
80
88
  onStatusChange: options.onStatusChange || (() => {}),
89
+ onCustomEvent: options.onCustomEvent || (() => {}),
81
90
  },
82
91
  }
83
92
 
@@ -198,6 +207,13 @@ export class ChatClient {
198
207
  )
199
208
  }
200
209
  },
210
+ onCustomEvent: (
211
+ eventType: string,
212
+ data: unknown,
213
+ context: { toolCallId?: string },
214
+ ) => {
215
+ this.callbacksRef.current.onCustomEvent(eventType, data, context)
216
+ },
201
217
  },
202
218
  })
203
219
 
@@ -391,7 +407,9 @@ export class ChatClient {
391
407
 
392
408
  // If stream is in progress, queue the response for after it ends
393
409
  if (this.isLoading) {
394
- this.queuePostStreamAction(() => this.streamResponse())
410
+ this.queuePostStreamAction(async () => {
411
+ await this.streamResponse()
412
+ })
395
413
  return
396
414
  }
397
415
 
@@ -399,12 +417,13 @@ export class ChatClient {
399
417
  }
400
418
 
401
419
  /**
402
- * Stream a response from the LLM
420
+ * Stream a response from the LLM.
421
+ * Returns true if the stream completed successfully, false on abort or error.
403
422
  */
404
- private async streamResponse(): Promise<void> {
423
+ private async streamResponse(): Promise<boolean> {
405
424
  // Guard against concurrent streams - if already loading, skip
406
425
  if (this.isLoading) {
407
- return
426
+ return false
408
427
  }
409
428
 
410
429
  this.setIsLoading(true)
@@ -445,7 +464,7 @@ export class ChatClient {
445
464
  } catch (err) {
446
465
  if (err instanceof Error) {
447
466
  if (err.name === 'AbortError') {
448
- return
467
+ return false
449
468
  }
450
469
  this.setError(err)
451
470
  this.setStatus('error')
@@ -473,6 +492,8 @@ export class ChatClient {
473
492
  }
474
493
  }
475
494
  }
495
+
496
+ return streamCompletedSuccessfully
476
497
  }
477
498
 
478
499
  /**
@@ -618,16 +639,28 @@ export class ChatClient {
618
639
  private async checkForContinuation(): Promise<void> {
619
640
  // Prevent duplicate continuation attempts
620
641
  if (this.continuationPending || this.isLoading) {
642
+ this.continuationSkipped = true
621
643
  return
622
644
  }
623
645
 
624
646
  if (this.shouldAutoSend()) {
625
647
  this.continuationPending = true
648
+ this.continuationSkipped = false
649
+ let succeeded = false
626
650
  try {
627
- await this.streamResponse()
651
+ succeeded = await this.streamResponse()
628
652
  } finally {
629
653
  this.continuationPending = false
630
654
  }
655
+ // If a queued check was skipped while continuationPending was true
656
+ // (e.g. a chained approval responded to during the stream), re-evaluate
657
+ // now that the flag is cleared. Only replay after a successful stream —
658
+ // aborted or errored streams should not trigger further continuation.
659
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- mutated asynchronously during await
660
+ if (this.continuationSkipped && succeeded) {
661
+ this.continuationSkipped = false
662
+ await this.checkForContinuation()
663
+ }
631
664
  }
632
665
  }
633
666
 
@@ -684,6 +717,11 @@ export class ChatClient {
684
717
  onChunk?: (chunk: StreamChunk) => void
685
718
  onFinish?: (message: UIMessage) => void
686
719
  onError?: (error: Error) => void
720
+ onCustomEvent?: (
721
+ eventType: string,
722
+ data: unknown,
723
+ context: { toolCallId?: string },
724
+ ) => void
687
725
  }): void {
688
726
  if (options.connection !== undefined) {
689
727
  this.connection = options.connection
@@ -709,5 +747,8 @@ export class ChatClient {
709
747
  if (options.onError !== undefined) {
710
748
  this.callbacksRef.current.onError = options.onError
711
749
  }
750
+ if (options.onCustomEvent !== undefined) {
751
+ this.callbacksRef.current.onCustomEvent = options.onCustomEvent
752
+ }
712
753
  }
713
754
  }
package/src/types.ts CHANGED
@@ -239,6 +239,20 @@ export interface ChatClientOptions<
239
239
  */
240
240
  onStatusChange?: (status: ChatClientState) => void
241
241
 
242
+ /**
243
+ * Callback when a custom event is received from a server-side tool.
244
+ * Custom events are emitted by tools using `context.emitCustomEvent()` during execution.
245
+ *
246
+ * @param eventType - The name of the custom event
247
+ * @param data - The event payload data
248
+ * @param context - Additional context including the toolCallId that emitted the event
249
+ */
250
+ onCustomEvent?: (
251
+ eventType: string,
252
+ data: unknown,
253
+ context: { toolCallId?: string },
254
+ ) => void
255
+
242
256
  /**
243
257
  * Client-side tools with execution logic
244
258
  * When provided, tools with execute functions will be called automatically