@tanstack/ai-client 0.4.4 → 0.5.1

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
 
@@ -157,5 +157,8 @@ export declare class ChatClient {
157
157
  onChunk?: (chunk: StreamChunk) => void;
158
158
  onFinish?: (message: UIMessage) => void;
159
159
  onError?: (error: Error) => void;
160
+ onCustomEvent?: (eventType: string, data: unknown, context: {
161
+ toolCallId?: string;
162
+ }) => void;
160
163
  }): void;
161
164
  }
@@ -40,6 +40,8 @@ class ChatClient {
40
40
  onErrorChange: options.onErrorChange || (() => {
41
41
  }),
42
42
  onStatusChange: options.onStatusChange || (() => {
43
+ }),
44
+ onCustomEvent: options.onCustomEvent || (() => {
43
45
  })
44
46
  }
45
47
  };
@@ -133,6 +135,9 @@ class ChatClient {
133
135
  args.approvalId
134
136
  );
135
137
  }
138
+ },
139
+ onCustomEvent: (eventType, data, context) => {
140
+ this.callbacksRef.current.onCustomEvent(eventType, data, context);
136
141
  }
137
142
  }
138
143
  });
@@ -508,6 +513,9 @@ class ChatClient {
508
513
  if (options.onError !== void 0) {
509
514
  this.callbacksRef.current.onError = options.onError;
510
515
  }
516
+ if (options.onCustomEvent !== void 0) {
517
+ this.callbacksRef.current.onCustomEvent = options.onCustomEvent;
518
+ }
511
519
  }
512
520
  }
513
521
  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":"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"}
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 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(() => 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 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.4",
3
+ "version": "0.5.1",
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.0"
34
+ "@tanstack/ai": "0.6.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@vitest/coverage-v8": "4.0.14",
@@ -51,6 +51,11 @@ export class ChatClient {
51
51
  onLoadingChange: (isLoading: boolean) => void
52
52
  onErrorChange: (error: Error | undefined) => void
53
53
  onStatusChange: (status: ChatClientState) => void
54
+ onCustomEvent: (
55
+ eventType: string,
56
+ data: unknown,
57
+ context: { toolCallId?: string },
58
+ ) => void
54
59
  }
55
60
  }
56
61
 
@@ -78,6 +83,7 @@ export class ChatClient {
78
83
  onLoadingChange: options.onLoadingChange || (() => {}),
79
84
  onErrorChange: options.onErrorChange || (() => {}),
80
85
  onStatusChange: options.onStatusChange || (() => {}),
86
+ onCustomEvent: options.onCustomEvent || (() => {}),
81
87
  },
82
88
  }
83
89
 
@@ -198,6 +204,13 @@ export class ChatClient {
198
204
  )
199
205
  }
200
206
  },
207
+ onCustomEvent: (
208
+ eventType: string,
209
+ data: unknown,
210
+ context: { toolCallId?: string },
211
+ ) => {
212
+ this.callbacksRef.current.onCustomEvent(eventType, data, context)
213
+ },
201
214
  },
202
215
  })
203
216
 
@@ -684,6 +697,11 @@ export class ChatClient {
684
697
  onChunk?: (chunk: StreamChunk) => void
685
698
  onFinish?: (message: UIMessage) => void
686
699
  onError?: (error: Error) => void
700
+ onCustomEvent?: (
701
+ eventType: string,
702
+ data: unknown,
703
+ context: { toolCallId?: string },
704
+ ) => void
687
705
  }): void {
688
706
  if (options.connection !== undefined) {
689
707
  this.connection = options.connection
@@ -709,5 +727,8 @@ export class ChatClient {
709
727
  if (options.onError !== undefined) {
710
728
  this.callbacksRef.current.onError = options.onError
711
729
  }
730
+ if (options.onCustomEvent !== undefined) {
731
+ this.callbacksRef.current.onCustomEvent = options.onCustomEvent
732
+ }
712
733
  }
713
734
  }
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