@tanstack/ai-client 0.9.2 → 0.10.0
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/dist/esm/chat-client.d.ts +5 -1
- package/dist/esm/chat-client.js +28 -10
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +20 -2
- package/dist/esm/connection-adapters.js +71 -15
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/types.d.ts +20 -1
- package/dist/esm/types.js.map +1 -1
- package/package.json +3 -3
- package/src/chat-client.ts +69 -14
- package/src/connection-adapters.ts +108 -19
- package/src/types.ts +22 -1
|
@@ -5,7 +5,9 @@ export declare class ChatClient {
|
|
|
5
5
|
private processor;
|
|
6
6
|
private connection;
|
|
7
7
|
private uniqueId;
|
|
8
|
-
private
|
|
8
|
+
private threadId;
|
|
9
|
+
private bodyOption;
|
|
10
|
+
private forwardedPropsOption;
|
|
9
11
|
private pendingMessageBody;
|
|
10
12
|
private isLoading;
|
|
11
13
|
private isSubscribed;
|
|
@@ -212,7 +214,9 @@ export declare class ChatClient {
|
|
|
212
214
|
*/
|
|
213
215
|
updateOptions(options: {
|
|
214
216
|
connection?: ConnectionAdapter;
|
|
217
|
+
/** @deprecated Use `forwardedProps` instead. */
|
|
215
218
|
body?: Record<string, any>;
|
|
219
|
+
forwardedProps?: Record<string, any>;
|
|
216
220
|
tools?: ReadonlyArray<AnyClientTool>;
|
|
217
221
|
onResponse?: (response?: Response) => void | Promise<void>;
|
|
218
222
|
onChunk?: (chunk: StreamChunk) => void;
|
package/dist/esm/chat-client.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { StreamProcessor, normalizeToUIMessage, generateMessageId } from "@tanstack/ai";
|
|
1
|
+
import { StreamProcessor, normalizeToUIMessage, generateMessageId, convertSchemaToJsonSchema } from "@tanstack/ai";
|
|
2
2
|
import { DefaultChatClientEventEmitter } from "./events.js";
|
|
3
3
|
import { normalizeConnectionAdapter } from "./connection-adapters.js";
|
|
4
4
|
class ChatClient {
|
|
5
5
|
constructor(options) {
|
|
6
|
-
this.
|
|
6
|
+
this.bodyOption = {};
|
|
7
|
+
this.forwardedPropsOption = {};
|
|
7
8
|
this.pendingMessageBody = void 0;
|
|
8
9
|
this.isLoading = false;
|
|
9
10
|
this.isSubscribed = false;
|
|
@@ -25,7 +26,9 @@ class ChatClient {
|
|
|
25
26
|
this.sessionGenerating = false;
|
|
26
27
|
this.activeRunIds = /* @__PURE__ */ new Set();
|
|
27
28
|
this.uniqueId = options.id || this.generateUniqueId("chat");
|
|
28
|
-
this.
|
|
29
|
+
this.threadId = options.threadId || this.generateUniqueId("thread");
|
|
30
|
+
this.bodyOption = options.body || {};
|
|
31
|
+
this.forwardedPropsOption = options.forwardedProps || {};
|
|
29
32
|
this.connection = normalizeConnectionAdapter(options.connection);
|
|
30
33
|
this.events = new DefaultChatClientEventEmitter(this.uniqueId);
|
|
31
34
|
this.clientToolsRef = { current: /* @__PURE__ */ new Map() };
|
|
@@ -100,7 +103,7 @@ class ChatClient {
|
|
|
100
103
|
this.events.textUpdated(this.currentStreamId, messageId, content);
|
|
101
104
|
}
|
|
102
105
|
},
|
|
103
|
-
onThinkingUpdate: (messageId,
|
|
106
|
+
onThinkingUpdate: (messageId, content) => {
|
|
104
107
|
if (this.currentStreamId) {
|
|
105
108
|
this.events.thinkingUpdated(
|
|
106
109
|
this.currentStreamId,
|
|
@@ -283,7 +286,7 @@ class ChatClient {
|
|
|
283
286
|
this.setSessionGenerating(true);
|
|
284
287
|
}
|
|
285
288
|
if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") {
|
|
286
|
-
const runId = chunk.type === "RUN_FINISHED" ? chunk.runId :
|
|
289
|
+
const runId = chunk.type === "RUN_FINISHED" ? chunk.runId : chunk.runId;
|
|
287
290
|
if (runId) {
|
|
288
291
|
this.activeRunIds.delete(runId);
|
|
289
292
|
} else if (chunk.type === "RUN_ERROR") {
|
|
@@ -424,9 +427,9 @@ class ChatClient {
|
|
|
424
427
|
return false;
|
|
425
428
|
}
|
|
426
429
|
const mergedBody = {
|
|
427
|
-
...this.
|
|
428
|
-
...this.
|
|
429
|
-
|
|
430
|
+
...this.bodyOption,
|
|
431
|
+
...this.forwardedPropsOption,
|
|
432
|
+
...this.pendingMessageBody
|
|
430
433
|
};
|
|
431
434
|
this.pendingMessageBody = void 0;
|
|
432
435
|
this.currentStreamId = this.generateUniqueId("stream");
|
|
@@ -434,7 +437,19 @@ class ChatClient {
|
|
|
434
437
|
this.processor.prepareAssistantMessage();
|
|
435
438
|
this.ensureSubscription();
|
|
436
439
|
const processingComplete = this.waitForProcessing();
|
|
437
|
-
|
|
440
|
+
const runContext = {
|
|
441
|
+
threadId: this.threadId,
|
|
442
|
+
runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
443
|
+
clientTools: Array.from(this.clientToolsRef.current.values()).map(
|
|
444
|
+
(t) => ({
|
|
445
|
+
name: t.name,
|
|
446
|
+
description: t.description,
|
|
447
|
+
parameters: t.inputSchema ? convertSchemaToJsonSchema(t.inputSchema) : { type: "object" }
|
|
448
|
+
})
|
|
449
|
+
),
|
|
450
|
+
forwardedProps: { ...mergedBody }
|
|
451
|
+
};
|
|
452
|
+
await this.connection.send(messages, mergedBody, signal, runContext);
|
|
438
453
|
await processingComplete;
|
|
439
454
|
if (generation !== this.streamGeneration) {
|
|
440
455
|
return false;
|
|
@@ -722,7 +737,10 @@ class ChatClient {
|
|
|
722
737
|
}
|
|
723
738
|
}
|
|
724
739
|
if (options.body !== void 0) {
|
|
725
|
-
this.
|
|
740
|
+
this.bodyOption = options.body;
|
|
741
|
+
}
|
|
742
|
+
if (options.forwardedProps !== void 0) {
|
|
743
|
+
this.forwardedPropsOption = options.forwardedProps;
|
|
726
744
|
}
|
|
727
745
|
if (options.tools !== void 0) {
|
|
728
746
|
this.clientToolsRef.current = /* @__PURE__ */ new Map();
|
|
@@ -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 { normalizeConnectionAdapter } from './connection-adapters'\nimport type {\n AnyClientTool,\n ContentPart,\n ModelMessage,\n StreamChunk,\n} from '@tanstack/ai'\nimport type {\n ConnectionAdapter,\n SubscribeConnectionAdapter,\n} from './connection-adapters'\nimport type { ChatClientEventEmitter } from './events'\nimport type {\n ChatClientOptions,\n ChatClientState,\n ConnectionStatus,\n MessagePart,\n MultimodalContent,\n ToolCallPart,\n UIMessage,\n} from './types'\n\nexport class ChatClient {\n private processor: StreamProcessor\n private connection: SubscribeConnectionAdapter\n private uniqueId: string\n private body: Record<string, any> = {}\n private pendingMessageBody: Record<string, any> | undefined = undefined\n private isLoading = false\n private isSubscribed = false\n private error: Error | undefined = undefined\n private status: ChatClientState = 'ready'\n private connectionStatus: ConnectionStatus = 'disconnected'\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 private subscriptionAbortController: AbortController | null = null\n private processingResolve: (() => void) | null = null\n private errorReportedGeneration: number | null = null\n private streamGeneration = 0\n // Tracks whether a queued checkForContinuation was skipped because\n // continuationPending was true (chained approval scenario)\n private continuationSkipped = false\n private draining = false\n private sessionGenerating = false\n private activeRunIds = new Set<string>()\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 onSubscriptionChange: (isSubscribed: boolean) => void\n onConnectionStatusChange: (status: ConnectionStatus) => void\n onSessionGeneratingChange: (isGenerating: boolean) => 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 = normalizeConnectionAdapter(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 onSubscriptionChange: options.onSubscriptionChange || (() => {}),\n onConnectionStatusChange:\n options.onConnectionStatusChange || (() => {}),\n onSessionGeneratingChange:\n options.onSessionGeneratingChange || (() => {}),\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 const assistantMessageId =\n this.processor.getCurrentAssistantMessageId()\n if (!assistantMessageId) {\n return\n }\n const messages = this.processor.getMessages()\n const assistantMessage = messages.find(\n (m: UIMessage) => m.id === assistantMessageId,\n )\n if (assistantMessage) {\n this.currentMessageId = assistantMessage.id\n this.events.messageAppended(\n assistantMessage,\n this.currentStreamId || undefined,\n )\n }\n },\n onStreamEnd: (message: UIMessage) => {\n this.callbacksRef.current.onFinish(message)\n this.setStatus('ready')\n // Resolve the processing-complete promise so streamResponse can continue\n this.resolveProcessing()\n },\n onError: (error: Error) => {\n this.reportStreamError(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: (\n messageId: string,\n _stepId: string,\n content: string,\n ) => {\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 setIsSubscribed(isSubscribed: boolean): void {\n this.isSubscribed = isSubscribed\n this.callbacksRef.current.onSubscriptionChange(isSubscribed)\n }\n\n private setConnectionStatus(status: ConnectionStatus): void {\n this.connectionStatus = status\n this.callbacksRef.current.onConnectionStatusChange(status)\n }\n\n private setSessionGenerating(isGenerating: boolean): void {\n if (this.sessionGenerating === isGenerating) return\n this.sessionGenerating = isGenerating\n this.callbacksRef.current.onSessionGeneratingChange(isGenerating)\n }\n\n private resetSessionGenerating(): void {\n this.activeRunIds.clear()\n this.setSessionGenerating(false)\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 private abortSubscriptionLoop(): void {\n this.subscriptionAbortController?.abort()\n this.subscriptionAbortController = null\n }\n\n private resolveProcessing(): void {\n this.processingResolve?.()\n this.processingResolve = null\n }\n\n private cancelInFlightStream(options?: {\n setReadyStatus?: boolean\n abortSubscription?: boolean\n }): void {\n this.abortController?.abort()\n this.abortController = null\n if (options?.abortSubscription) {\n this.abortSubscriptionLoop()\n }\n this.resolveProcessing()\n this.setIsLoading(false)\n if (options?.setReadyStatus) {\n this.setStatus('ready')\n }\n }\n\n private reportStreamError(error: Error): void {\n const alreadyReported =\n this.errorReportedGeneration === this.streamGeneration\n this.setError(error)\n // Preserve request-level error semantics even if a RUN_ERROR arrives\n // slightly after loading flips false during stream teardown.\n if (\n this.isLoading ||\n this.status === 'submitted' ||\n this.status === 'streaming'\n ) {\n this.setStatus('error')\n }\n if (!alreadyReported) {\n this.errorReportedGeneration = this.streamGeneration\n this.callbacksRef.current.onError(error)\n }\n }\n\n /**\n * Start the background subscription loop.\n */\n private startSubscription(): void {\n this.subscriptionAbortController = new AbortController()\n const signal = this.subscriptionAbortController.signal\n\n this.consumeSubscription(signal)\n .catch((err) => {\n if (err instanceof Error && err.name !== 'AbortError') {\n this.setConnectionStatus('error')\n this.resetSessionGenerating()\n this.setIsSubscribed(false)\n this.reportStreamError(err)\n }\n // Resolve pending processing so streamResponse doesn't hang\n this.resolveProcessing()\n })\n .finally(() => {\n // Ignore stale loops that were superseded by a restart.\n if (this.subscriptionAbortController?.signal !== signal) {\n return\n }\n this.subscriptionAbortController = null\n if (!signal.aborted && this.isSubscribed) {\n this.setIsSubscribed(false)\n if (this.connectionStatus !== 'error') {\n this.setConnectionStatus('disconnected')\n }\n }\n })\n }\n\n /**\n * Consume chunks from the connection subscription.\n */\n private async consumeSubscription(signal: AbortSignal): Promise<void> {\n const stream = this.connection.subscribe(signal)\n for await (const chunk of stream) {\n if (signal.aborted) break\n if (this.connectionStatus === 'connecting') {\n this.setConnectionStatus('connected')\n }\n this.callbacksRef.current.onChunk(chunk)\n this.processor.processChunk(chunk)\n if (chunk.type === 'RUN_STARTED') {\n this.activeRunIds.add(chunk.runId)\n this.setSessionGenerating(true)\n }\n // RUN_FINISHED / RUN_ERROR signal run completion — resolve processing\n // (redundant if onStreamEnd already resolved it, harmless)\n if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {\n const runId = chunk.type === 'RUN_FINISHED' ? chunk.runId : undefined\n if (runId) {\n this.activeRunIds.delete(runId)\n } else if (chunk.type === 'RUN_ERROR') {\n // RUN_ERROR without runId is a session-level error; clear all runs\n this.activeRunIds.clear()\n }\n this.setSessionGenerating(this.activeRunIds.size > 0)\n this.resolveProcessing()\n }\n // Yield control back to event loop for UI updates\n await new Promise((resolve) => setTimeout(resolve, 0))\n }\n }\n\n /**\n * Ensure subscription loop is running, starting it if needed.\n */\n private ensureSubscription(): void {\n if (!this.isSubscribed) {\n this.subscribe()\n return\n }\n if (\n !this.subscriptionAbortController ||\n this.subscriptionAbortController.signal.aborted\n ) {\n this.subscribe({ restart: true })\n }\n }\n\n /**\n * Create a promise that resolves when onStreamEnd fires.\n * Used by streamResponse to await processing completion.\n */\n private waitForProcessing(): Promise<void> {\n // Resolve any stale promise (e.g., from a previous aborted request)\n this.resolveProcessing()\n return new Promise<void>((resolve) => {\n this.processingResolve = resolve\n })\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 // Track generation so a superseded stream's cleanup doesn't clobber the new one\n const generation = ++this.streamGeneration\n\n this.setIsLoading(true)\n this.setStatus('submitted')\n this.setError(undefined)\n this.errorReportedGeneration = null\n this.abortController = new AbortController()\n // Capture the signal immediately so that a concurrent stop() or\n // sendMessage() that reassigns this.abortController cannot cause\n // connect() to receive a stale or null signal.\n const signal = this.abortController.signal\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 // If the stream was cancelled during the onResponse await (e.g. stop()\n // from a callback or unmount, or reload() superseding this stream),\n // bail out before allocating waitForProcessing() — otherwise the\n // resolveProcessing() that ran during cancellation is a no-op and the\n // await processingComplete below would deadlock.\n if (signal.aborted) {\n return false\n }\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 // Generate stream ID — assistant message will be created by stream events\n this.currentStreamId = this.generateUniqueId('stream')\n this.currentMessageId = null\n\n // Reset processor stream state for new response — prevents stale\n // messageStates entries (from a previous stream) from blocking\n // creation of a new assistant message (e.g. after reload).\n this.processor.prepareAssistantMessage()\n\n // Ensure subscription loop is running\n this.ensureSubscription()\n\n // Set up promise that resolves when onStreamEnd fires\n const processingComplete = this.waitForProcessing()\n\n // Send through normalized connection (pushes chunks to subscription queue)\n await this.connection.send(messages, mergedBody, signal)\n\n // Wait for subscription loop to finish processing all chunks\n await processingComplete\n\n // If this stream was superseded (e.g. by reload()), bail out —\n // the new stream owns the processor and processingResolve now.\n if (generation !== this.streamGeneration) {\n return false\n }\n\n // A RUN_ERROR from the stream transitions status to error.\n // Do not treat this stream as a successful completion.\n if (this.status === 'error') {\n return false\n }\n\n // Wait for pending client tool executions\n if (this.pendingToolExecutions.size > 0) {\n await Promise.all(this.pendingToolExecutions.values())\n }\n\n // Finalize (idempotent — may already be done by RUN_FINISHED handler)\n this.processor.finalizeStream()\n streamCompletedSuccessfully = true\n } catch (err) {\n if (err instanceof Error) {\n if (err.name === 'AbortError') {\n return false\n }\n if (generation === this.streamGeneration) {\n this.reportStreamError(err)\n }\n }\n } finally {\n // Only clean up if this is still the active stream.\n // A superseded stream (e.g. reload() started a new one) must not\n // clobber the new stream's abortController or isLoading state.\n if (generation === this.streamGeneration) {\n this.currentStreamId = null\n this.currentMessageId = null\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 // but ONLY if the model indicated it wants to continue (finishReason !== 'stop').\n // When finishReason is 'stop', the model is done — don't re-send.\n if (streamCompletedSuccessfully) {\n const messages = this.processor.getMessages()\n const lastPart = messages.at(-1)?.parts.at(-1)\n const { finishReason } = this.processor.getState()\n\n if (\n lastPart?.type === 'tool-result' &&\n finishReason !== 'stop' &&\n this.shouldAutoSend()\n ) {\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 return streamCompletedSuccessfully\n }\n\n /**\n * Start the client subscription loop.\n * This controls the connection lifecycle independently from request lifecycle.\n */\n subscribe(options?: { restart?: boolean }): void {\n const restart = options?.restart === true\n if (this.isSubscribed && !restart) {\n return\n }\n\n if (this.isSubscribed && restart) {\n this.abortSubscriptionLoop()\n }\n\n this.setIsSubscribed(true)\n this.setConnectionStatus('connecting')\n this.startSubscription()\n }\n\n /**\n * Unsubscribe and fully tear down live behavior.\n * This aborts an in-flight request and the subscription loop.\n */\n unsubscribe(): void {\n this.cancelInFlightStream({\n setReadyStatus: true,\n abortSubscription: true,\n })\n this.resetSessionGenerating()\n this.setIsSubscribed(false)\n this.setConnectionStatus('disconnected')\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 // Cancel any active stream before reloading\n if (this.isLoading) {\n this.cancelInFlightStream()\n }\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 this.cancelInFlightStream({ setReadyStatus: true })\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 if (this.draining) return\n this.draining = true\n try {\n while (this.postStreamActions.length > 0) {\n const action = this.postStreamActions.shift()!\n await action()\n }\n } finally {\n this.draining = false\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 * Requires that there is at least one tool call in the last assistant message;\n * a text-only response has nothing to auto-send.\n */\n private shouldAutoSend(): boolean {\n const messages = this.processor.getMessages()\n const lastAssistant = messages.findLast((m) => m.role === 'assistant')\n if (!lastAssistant) return false\n const hasToolCalls = lastAssistant.parts.some((p) => p.type === 'tool-call')\n if (!hasToolCalls) return false\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 whether the subscription loop is active\n */\n getIsSubscribed(): boolean {\n return this.isSubscribed\n }\n\n /**\n * Get current connection lifecycle status\n */\n getConnectionStatus(): ConnectionStatus {\n return this.connectionStatus\n }\n\n /**\n * Whether the shared session is actively generating.\n * Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).\n * Unlike `isLoading` (request-local), this reflects shared generation\n * activity visible to all subscribers (e.g. across tabs/devices).\n */\n getSessionGenerating(): boolean {\n return this.sessionGenerating\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 onSubscriptionChange?: (isSubscribed: boolean) => void\n onConnectionStatusChange?: (status: ConnectionStatus) => void\n onSessionGeneratingChange?: (isGenerating: boolean) => void\n onCustomEvent?: (\n eventType: string,\n data: unknown,\n context: { toolCallId?: string },\n ) => void\n }): void {\n if (options.connection !== undefined) {\n const wasSubscribed = this.isSubscribed\n\n if (this.isLoading) {\n this.cancelInFlightStream({\n setReadyStatus: true,\n abortSubscription: true,\n })\n } else if (wasSubscribed) {\n this.abortSubscriptionLoop()\n }\n\n this.resetSessionGenerating()\n this.setIsSubscribed(false)\n this.setConnectionStatus('disconnected')\n this.connection = normalizeConnectionAdapter(options.connection)\n\n if (wasSubscribed) {\n this.subscribe()\n }\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.onSubscriptionChange !== undefined) {\n this.callbacksRef.current.onSubscriptionChange =\n options.onSubscriptionChange\n }\n if (options.onConnectionStatusChange !== undefined) {\n this.callbacksRef.current.onConnectionStatusChange =\n options.onConnectionStatusChange\n }\n if (options.onSessionGeneratingChange !== undefined) {\n this.callbacksRef.current.onSessionGeneratingChange =\n options.onSessionGeneratingChange\n }\n if (options.onCustomEvent !== undefined) {\n this.callbacksRef.current.onCustomEvent = options.onCustomEvent\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 convertSchemaToJsonSchema,\n generateMessageId,\n normalizeToUIMessage,\n} from '@tanstack/ai'\nimport { DefaultChatClientEventEmitter } from './events'\nimport { normalizeConnectionAdapter } from './connection-adapters'\nimport type {\n AnyClientTool,\n ContentPart,\n ModelMessage,\n StreamChunk,\n} from '@tanstack/ai'\nimport type {\n ConnectionAdapter,\n SubscribeConnectionAdapter,\n} from './connection-adapters'\nimport type { ChatClientEventEmitter } from './events'\nimport type {\n ChatClientOptions,\n ChatClientState,\n ConnectionStatus,\n MessagePart,\n MultimodalContent,\n ToolCallPart,\n UIMessage,\n} from './types'\n\nexport class ChatClient {\n private processor: StreamProcessor\n private connection: SubscribeConnectionAdapter\n private uniqueId: string\n private threadId: string\n // Track the legacy `body` option and the canonical `forwardedProps`\n // option as separate slots so that `updateOptions({ forwardedProps })`\n // doesn't wipe a previously-set `body` (and vice versa). They are\n // merged on every send, with `forwardedProps` winning on key collision.\n private bodyOption: Record<string, any> = {}\n private forwardedPropsOption: Record<string, any> = {}\n private pendingMessageBody: Record<string, any> | undefined = undefined\n private isLoading = false\n private isSubscribed = false\n private error: Error | undefined = undefined\n private status: ChatClientState = 'ready'\n private connectionStatus: ConnectionStatus = 'disconnected'\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 private subscriptionAbortController: AbortController | null = null\n private processingResolve: (() => void) | null = null\n private errorReportedGeneration: number | null = null\n private streamGeneration = 0\n // Tracks whether a queued checkForContinuation was skipped because\n // continuationPending was true (chained approval scenario)\n private continuationSkipped = false\n private draining = false\n private sessionGenerating = false\n private activeRunIds = new Set<string>()\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 onSubscriptionChange: (isSubscribed: boolean) => void\n onConnectionStatusChange: (status: ConnectionStatus) => void\n onSessionGeneratingChange: (isGenerating: boolean) => 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.threadId = options.threadId || this.generateUniqueId('thread')\n // Both `body` (deprecated) and `forwardedProps` populate the AG-UI\n // `RunAgentInput.forwardedProps` wire field. They are stored\n // separately so `updateOptions` can replace one without touching the\n // other; the merge happens at send time, with `forwardedProps`\n // winning on key collision.\n this.bodyOption = options.body || {}\n this.forwardedPropsOption = options.forwardedProps || {}\n this.connection = normalizeConnectionAdapter(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 onSubscriptionChange: options.onSubscriptionChange || (() => {}),\n onConnectionStatusChange:\n options.onConnectionStatusChange || (() => {}),\n onSessionGeneratingChange:\n options.onSessionGeneratingChange || (() => {}),\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 const assistantMessageId =\n this.processor.getCurrentAssistantMessageId()\n if (!assistantMessageId) {\n return\n }\n const messages = this.processor.getMessages()\n const assistantMessage = messages.find(\n (m: UIMessage) => m.id === assistantMessageId,\n )\n if (assistantMessage) {\n this.currentMessageId = assistantMessage.id\n this.events.messageAppended(\n assistantMessage,\n this.currentStreamId || undefined,\n )\n }\n },\n onStreamEnd: (message: UIMessage) => {\n this.callbacksRef.current.onFinish(message)\n this.setStatus('ready')\n // Resolve the processing-complete promise so streamResponse can continue\n this.resolveProcessing()\n },\n onError: (error: Error) => {\n this.reportStreamError(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 setIsSubscribed(isSubscribed: boolean): void {\n this.isSubscribed = isSubscribed\n this.callbacksRef.current.onSubscriptionChange(isSubscribed)\n }\n\n private setConnectionStatus(status: ConnectionStatus): void {\n this.connectionStatus = status\n this.callbacksRef.current.onConnectionStatusChange(status)\n }\n\n private setSessionGenerating(isGenerating: boolean): void {\n if (this.sessionGenerating === isGenerating) return\n this.sessionGenerating = isGenerating\n this.callbacksRef.current.onSessionGeneratingChange(isGenerating)\n }\n\n private resetSessionGenerating(): void {\n this.activeRunIds.clear()\n this.setSessionGenerating(false)\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 private abortSubscriptionLoop(): void {\n this.subscriptionAbortController?.abort()\n this.subscriptionAbortController = null\n }\n\n private resolveProcessing(): void {\n this.processingResolve?.()\n this.processingResolve = null\n }\n\n private cancelInFlightStream(options?: {\n setReadyStatus?: boolean\n abortSubscription?: boolean\n }): void {\n this.abortController?.abort()\n this.abortController = null\n if (options?.abortSubscription) {\n this.abortSubscriptionLoop()\n }\n this.resolveProcessing()\n this.setIsLoading(false)\n if (options?.setReadyStatus) {\n this.setStatus('ready')\n }\n }\n\n private reportStreamError(error: Error): void {\n const alreadyReported =\n this.errorReportedGeneration === this.streamGeneration\n this.setError(error)\n // Preserve request-level error semantics even if a RUN_ERROR arrives\n // slightly after loading flips false during stream teardown.\n if (\n this.isLoading ||\n this.status === 'submitted' ||\n this.status === 'streaming'\n ) {\n this.setStatus('error')\n }\n if (!alreadyReported) {\n this.errorReportedGeneration = this.streamGeneration\n this.callbacksRef.current.onError(error)\n }\n }\n\n /**\n * Start the background subscription loop.\n */\n private startSubscription(): void {\n this.subscriptionAbortController = new AbortController()\n const signal = this.subscriptionAbortController.signal\n\n this.consumeSubscription(signal)\n .catch((err) => {\n if (err instanceof Error && err.name !== 'AbortError') {\n this.setConnectionStatus('error')\n this.resetSessionGenerating()\n this.setIsSubscribed(false)\n this.reportStreamError(err)\n }\n // Resolve pending processing so streamResponse doesn't hang\n this.resolveProcessing()\n })\n .finally(() => {\n // Ignore stale loops that were superseded by a restart.\n if (this.subscriptionAbortController?.signal !== signal) {\n return\n }\n this.subscriptionAbortController = null\n if (!signal.aborted && this.isSubscribed) {\n this.setIsSubscribed(false)\n if (this.connectionStatus !== 'error') {\n this.setConnectionStatus('disconnected')\n }\n }\n })\n }\n\n /**\n * Consume chunks from the connection subscription.\n */\n private async consumeSubscription(signal: AbortSignal): Promise<void> {\n const stream = this.connection.subscribe(signal)\n for await (const chunk of stream) {\n if (signal.aborted) break\n if (this.connectionStatus === 'connecting') {\n this.setConnectionStatus('connected')\n }\n this.callbacksRef.current.onChunk(chunk)\n this.processor.processChunk(chunk)\n if (chunk.type === 'RUN_STARTED') {\n this.activeRunIds.add(chunk.runId)\n this.setSessionGenerating(true)\n }\n // RUN_FINISHED / RUN_ERROR signal run completion — resolve processing\n // (redundant if onStreamEnd already resolved it, harmless)\n if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {\n // RUN_FINISHED has runId in its schema; RUN_ERROR carries it via the\n // AG-UI passthrough so adapters can correlate per-run errors. Extract\n // both so a RUN_ERROR with a runId only clears that run, not every\n // active run in the session.\n const runId =\n chunk.type === 'RUN_FINISHED'\n ? chunk.runId\n : (chunk as { runId?: string }).runId\n if (runId) {\n this.activeRunIds.delete(runId)\n } else if (chunk.type === 'RUN_ERROR') {\n // RUN_ERROR without runId is a session-level error; clear all runs\n this.activeRunIds.clear()\n }\n this.setSessionGenerating(this.activeRunIds.size > 0)\n this.resolveProcessing()\n }\n // Yield control back to event loop for UI updates\n await new Promise((resolve) => setTimeout(resolve, 0))\n }\n }\n\n /**\n * Ensure subscription loop is running, starting it if needed.\n */\n private ensureSubscription(): void {\n if (!this.isSubscribed) {\n this.subscribe()\n return\n }\n if (\n !this.subscriptionAbortController ||\n this.subscriptionAbortController.signal.aborted\n ) {\n this.subscribe({ restart: true })\n }\n }\n\n /**\n * Create a promise that resolves when onStreamEnd fires.\n * Used by streamResponse to await processing completion.\n */\n private waitForProcessing(): Promise<void> {\n // Resolve any stale promise (e.g., from a previous aborted request)\n this.resolveProcessing()\n return new Promise<void>((resolve) => {\n this.processingResolve = resolve\n })\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 // Track generation so a superseded stream's cleanup doesn't clobber the new one\n const generation = ++this.streamGeneration\n\n this.setIsLoading(true)\n this.setStatus('submitted')\n this.setError(undefined)\n this.errorReportedGeneration = null\n this.abortController = new AbortController()\n // Capture the signal immediately so that a concurrent stop() or\n // sendMessage() that reassigns this.abortController cannot cause\n // connect() to receive a stale or null signal.\n const signal = this.abortController.signal\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 // If the stream was cancelled during the onResponse await (e.g. stop()\n // from a callback or unmount, or reload() superseding this stream),\n // bail out before allocating waitForProcessing() — otherwise the\n // resolveProcessing() that ran during cancellation is a no-op and the\n // await processingComplete below would deadlock.\n if (signal.aborted) {\n return false\n }\n\n // Merge sources for the wire `forwardedProps` field, in priority\n // order (later spreads win):\n // 1. Legacy `body` option (deprecated).\n // 2. Canonical `forwardedProps` option (wins over `body`).\n // 3. Per-message `body` arg passed to `sendMessage` (highest).\n // The AG-UI standard `threadId` is sent at the wire's top level for\n // run/conversation correlation, so we no longer auto-emit a separate\n // `conversationId` here — `chat({ threadId })` server-side covers the\n // same role for devtools/observability.\n const mergedBody = {\n ...this.bodyOption,\n ...this.forwardedPropsOption,\n ...this.pendingMessageBody,\n }\n\n // Clear the pending message body after use\n this.pendingMessageBody = undefined\n\n // Generate stream ID — assistant message will be created by stream events\n this.currentStreamId = this.generateUniqueId('stream')\n this.currentMessageId = null\n\n // Reset processor stream state for new response — prevents stale\n // messageStates entries (from a previous stream) from blocking\n // creation of a new assistant message (e.g. after reload).\n this.processor.prepareAssistantMessage()\n\n // Ensure subscription loop is running\n this.ensureSubscription()\n\n // Set up promise that resolves when onStreamEnd fires\n const processingComplete = this.waitForProcessing()\n\n // Build per-send run context for AG-UI compliance\n // Note: mergedBody already contains the merged this.body + pendingMessageBody\n // (pendingMessageBody was cleared above, so we use mergedBody as forwardedProps)\n // Convert each client tool's `inputSchema` (a Standard Schema:\n // Zod, ArkType, Valibot, etc.) to JSON Schema for the wire. Foreign\n // AG-UI servers consuming `RunAgentInput.tools[].parameters` expect\n // JSON Schema; sending a Standard Schema instance directly would\n // serialize to an unusable shape.\n const runContext = {\n threadId: this.threadId,\n runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,\n clientTools: Array.from(this.clientToolsRef.current.values()).map(\n (t) => ({\n name: t.name,\n description: t.description,\n parameters: t.inputSchema\n ? convertSchemaToJsonSchema(t.inputSchema)\n : { type: 'object' },\n }),\n ),\n forwardedProps: { ...mergedBody },\n }\n\n // Send through normalized connection (pushes chunks to subscription queue)\n await this.connection.send(messages, mergedBody, signal, runContext)\n\n // Wait for subscription loop to finish processing all chunks\n await processingComplete\n\n // If this stream was superseded (e.g. by reload()), bail out —\n // the new stream owns the processor and processingResolve now.\n if (generation !== this.streamGeneration) {\n return false\n }\n\n // A RUN_ERROR from the stream transitions status to error.\n // Do not treat this stream as a successful completion.\n if (this.status === 'error') {\n return false\n }\n\n // Wait for pending client tool executions\n if (this.pendingToolExecutions.size > 0) {\n await Promise.all(this.pendingToolExecutions.values())\n }\n\n // Finalize (idempotent — may already be done by RUN_FINISHED handler)\n this.processor.finalizeStream()\n streamCompletedSuccessfully = true\n } catch (err) {\n if (err instanceof Error) {\n if (err.name === 'AbortError') {\n return false\n }\n if (generation === this.streamGeneration) {\n this.reportStreamError(err)\n }\n }\n } finally {\n // Only clean up if this is still the active stream.\n // A superseded stream (e.g. reload() started a new one) must not\n // clobber the new stream's abortController or isLoading state.\n if (generation === this.streamGeneration) {\n this.currentStreamId = null\n this.currentMessageId = null\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 // but ONLY if the model indicated it wants to continue (finishReason !== 'stop').\n // When finishReason is 'stop', the model is done — don't re-send.\n if (streamCompletedSuccessfully) {\n const messages = this.processor.getMessages()\n const lastPart = messages.at(-1)?.parts.at(-1)\n const { finishReason } = this.processor.getState()\n\n if (\n lastPart?.type === 'tool-result' &&\n finishReason !== 'stop' &&\n this.shouldAutoSend()\n ) {\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 return streamCompletedSuccessfully\n }\n\n /**\n * Start the client subscription loop.\n * This controls the connection lifecycle independently from request lifecycle.\n */\n subscribe(options?: { restart?: boolean }): void {\n const restart = options?.restart === true\n if (this.isSubscribed && !restart) {\n return\n }\n\n if (this.isSubscribed && restart) {\n this.abortSubscriptionLoop()\n }\n\n this.setIsSubscribed(true)\n this.setConnectionStatus('connecting')\n this.startSubscription()\n }\n\n /**\n * Unsubscribe and fully tear down live behavior.\n * This aborts an in-flight request and the subscription loop.\n */\n unsubscribe(): void {\n this.cancelInFlightStream({\n setReadyStatus: true,\n abortSubscription: true,\n })\n this.resetSessionGenerating()\n this.setIsSubscribed(false)\n this.setConnectionStatus('disconnected')\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 // Cancel any active stream before reloading\n if (this.isLoading) {\n this.cancelInFlightStream()\n }\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 this.cancelInFlightStream({ setReadyStatus: true })\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 if (this.draining) return\n this.draining = true\n try {\n while (this.postStreamActions.length > 0) {\n const action = this.postStreamActions.shift()!\n await action()\n }\n } finally {\n this.draining = false\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 * Requires that there is at least one tool call in the last assistant message;\n * a text-only response has nothing to auto-send.\n */\n private shouldAutoSend(): boolean {\n const messages = this.processor.getMessages()\n const lastAssistant = messages.findLast((m) => m.role === 'assistant')\n if (!lastAssistant) return false\n const hasToolCalls = lastAssistant.parts.some((p) => p.type === 'tool-call')\n if (!hasToolCalls) return false\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 whether the subscription loop is active\n */\n getIsSubscribed(): boolean {\n return this.isSubscribed\n }\n\n /**\n * Get current connection lifecycle status\n */\n getConnectionStatus(): ConnectionStatus {\n return this.connectionStatus\n }\n\n /**\n * Whether the shared session is actively generating.\n * Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).\n * Unlike `isLoading` (request-local), this reflects shared generation\n * activity visible to all subscribers (e.g. across tabs/devices).\n */\n getSessionGenerating(): boolean {\n return this.sessionGenerating\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 /** @deprecated Use `forwardedProps` instead. */\n body?: Record<string, any>\n forwardedProps?: 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 onSubscriptionChange?: (isSubscribed: boolean) => void\n onConnectionStatusChange?: (status: ConnectionStatus) => void\n onSessionGeneratingChange?: (isGenerating: boolean) => void\n onCustomEvent?: (\n eventType: string,\n data: unknown,\n context: { toolCallId?: string },\n ) => void\n }): void {\n if (options.connection !== undefined) {\n const wasSubscribed = this.isSubscribed\n\n if (this.isLoading) {\n this.cancelInFlightStream({\n setReadyStatus: true,\n abortSubscription: true,\n })\n } else if (wasSubscribed) {\n this.abortSubscriptionLoop()\n }\n\n this.resetSessionGenerating()\n this.setIsSubscribed(false)\n this.setConnectionStatus('disconnected')\n this.connection = normalizeConnectionAdapter(options.connection)\n\n if (wasSubscribed) {\n this.subscribe()\n }\n }\n // Replace each slot independently so callers can update one without\n // wiping the other. (Passing `undefined` for either field is a \"leave\n // unchanged\" signal — to clear a slot, pass an empty object `{}`.)\n if (options.body !== undefined) {\n this.bodyOption = options.body\n }\n if (options.forwardedProps !== undefined) {\n this.forwardedPropsOption = options.forwardedProps\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.onSubscriptionChange !== undefined) {\n this.callbacksRef.current.onSubscriptionChange =\n options.onSubscriptionChange\n }\n if (options.onConnectionStatusChange !== undefined) {\n this.callbacksRef.current.onConnectionStatusChange =\n options.onConnectionStatusChange\n }\n if (options.onSessionGeneratingChange !== undefined) {\n this.callbacksRef.current.onSessionGeneratingChange =\n options.onSessionGeneratingChange\n }\n if (options.onCustomEvent !== undefined) {\n this.callbacksRef.current.onCustomEvent = options.onCustomEvent\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import { ModelMessage, StreamChunk, UIMessage } from '@tanstack/ai';
|
|
2
|
+
/**
|
|
3
|
+
* Per-send context provided by the chat client to the connection adapter.
|
|
4
|
+
* The adapter combines this with serialized messages to build a full
|
|
5
|
+
* AG-UI `RunAgentInput` payload.
|
|
6
|
+
*/
|
|
7
|
+
export interface RunAgentInputContext {
|
|
8
|
+
threadId: string;
|
|
9
|
+
runId: string;
|
|
10
|
+
parentRunId?: string;
|
|
11
|
+
/** Client-declared tools to advertise in the request payload. */
|
|
12
|
+
clientTools?: Array<{
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
parameters: unknown;
|
|
16
|
+
}>;
|
|
17
|
+
/** Arbitrary user-controlled passthrough data. */
|
|
18
|
+
forwardedProps?: Record<string, unknown>;
|
|
19
|
+
}
|
|
2
20
|
export interface ConnectConnectionAdapter {
|
|
3
21
|
/**
|
|
4
22
|
* Connect and return an async iterable of StreamChunks.
|
|
5
23
|
*/
|
|
6
|
-
connect: (messages: Array<UIMessage> | Array<ModelMessage>, data?: Record<string, any>, abortSignal?: AbortSignal) => AsyncIterable<StreamChunk>;
|
|
24
|
+
connect: (messages: Array<UIMessage> | Array<ModelMessage>, data?: Record<string, any>, abortSignal?: AbortSignal, runContext?: RunAgentInputContext) => AsyncIterable<StreamChunk>;
|
|
7
25
|
}
|
|
8
26
|
export interface SubscribeConnectionAdapter {
|
|
9
27
|
/**
|
|
@@ -13,7 +31,7 @@ export interface SubscribeConnectionAdapter {
|
|
|
13
31
|
/**
|
|
14
32
|
* Send a request; chunks arrive through subscribe().
|
|
15
33
|
*/
|
|
16
|
-
send: (messages: Array<UIMessage> | Array<ModelMessage>, data?: Record<string, any>, abortSignal?: AbortSignal) => Promise<void>;
|
|
34
|
+
send: (messages: Array<UIMessage> | Array<ModelMessage>, data?: Record<string, any>, abortSignal?: AbortSignal, runContext?: RunAgentInputContext) => Promise<void>;
|
|
17
35
|
}
|
|
18
36
|
/**
|
|
19
37
|
* Connection adapter union.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { EventType } from "@tanstack/ai";
|
|
1
|
+
import { uiMessagesToWire, EventType } from "@tanstack/ai";
|
|
2
|
+
function generateRunId(prefix) {
|
|
3
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4
|
+
}
|
|
2
5
|
function mergeHeaders(customHeaders) {
|
|
3
6
|
if (!customHeaders) {
|
|
4
7
|
return {};
|
|
@@ -96,10 +99,15 @@ function normalizeConnectionAdapter(connection) {
|
|
|
96
99
|
}
|
|
97
100
|
})();
|
|
98
101
|
},
|
|
99
|
-
async send(messages, data, abortSignal) {
|
|
102
|
+
async send(messages, data, abortSignal, runContext) {
|
|
100
103
|
let hasTerminalEvent = false;
|
|
101
104
|
try {
|
|
102
|
-
const stream2 = connection.connect(
|
|
105
|
+
const stream2 = connection.connect(
|
|
106
|
+
messages,
|
|
107
|
+
data,
|
|
108
|
+
abortSignal,
|
|
109
|
+
runContext
|
|
110
|
+
);
|
|
103
111
|
for await (const chunk of stream2) {
|
|
104
112
|
if (chunk.type === "RUN_FINISHED" || chunk.type === "RUN_ERROR") {
|
|
105
113
|
hasTerminalEvent = true;
|
|
@@ -109,8 +117,8 @@ function normalizeConnectionAdapter(connection) {
|
|
|
109
117
|
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
110
118
|
push({
|
|
111
119
|
type: EventType.RUN_FINISHED,
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
threadId: runContext?.threadId ?? `thread-${Date.now()}`,
|
|
121
|
+
runId: runContext?.runId ?? `run-${Date.now()}`,
|
|
114
122
|
model: "connect-wrapper",
|
|
115
123
|
timestamp: Date.now(),
|
|
116
124
|
finishReason: "stop"
|
|
@@ -120,6 +128,8 @@ function normalizeConnectionAdapter(connection) {
|
|
|
120
128
|
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
121
129
|
push({
|
|
122
130
|
type: EventType.RUN_ERROR,
|
|
131
|
+
threadId: runContext?.threadId ?? `thread-${Date.now()}`,
|
|
132
|
+
runId: runContext?.runId ?? `run-${Date.now()}`,
|
|
123
133
|
timestamp: Date.now(),
|
|
124
134
|
message: err instanceof Error ? err.message : "Unknown error in connect()",
|
|
125
135
|
error: {
|
|
@@ -134,17 +144,40 @@ function normalizeConnectionAdapter(connection) {
|
|
|
134
144
|
}
|
|
135
145
|
function fetchServerSentEvents(url, options = {}) {
|
|
136
146
|
return {
|
|
137
|
-
async *connect(messages, data, abortSignal) {
|
|
147
|
+
async *connect(messages, data, abortSignal, runContext) {
|
|
138
148
|
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
139
149
|
const resolvedOptions = typeof options === "function" ? await options() : options;
|
|
140
150
|
const requestHeaders = {
|
|
141
151
|
"Content-Type": "application/json",
|
|
142
152
|
...mergeHeaders(resolvedOptions.headers)
|
|
143
153
|
};
|
|
154
|
+
const wireMessages = uiMessagesToWire(messages);
|
|
155
|
+
const forwardedProps = {
|
|
156
|
+
...resolvedOptions.body,
|
|
157
|
+
...runContext?.forwardedProps ?? {},
|
|
158
|
+
...data
|
|
159
|
+
};
|
|
144
160
|
const requestBody = {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
...
|
|
161
|
+
threadId: runContext?.threadId ?? generateRunId("thread"),
|
|
162
|
+
runId: runContext?.runId ?? generateRunId("run"),
|
|
163
|
+
...runContext?.parentRunId !== void 0 && {
|
|
164
|
+
parentRunId: runContext.parentRunId
|
|
165
|
+
},
|
|
166
|
+
state: {},
|
|
167
|
+
messages: wireMessages,
|
|
168
|
+
tools: runContext?.clientTools ?? [],
|
|
169
|
+
context: [],
|
|
170
|
+
forwardedProps,
|
|
171
|
+
// Backward-compat mirror of `forwardedProps` under the legacy
|
|
172
|
+
// field name `data`. Server endpoints that have not migrated
|
|
173
|
+
// off the pre-AG-UI shape (`{ messages, data }`) keep working.
|
|
174
|
+
// AG-UI strict consumers strip this via `RunAgentInputSchema`
|
|
175
|
+
// (see `chatParamsFromRequestBody`). Will be removed when the
|
|
176
|
+
// legacy `body` client option is dropped.
|
|
177
|
+
// Shallow-cloned so that downstream mutation of `data` (e.g.
|
|
178
|
+
// by a logging interceptor or fetch wrapper) cannot corrupt
|
|
179
|
+
// `forwardedProps` and vice versa.
|
|
180
|
+
data: { ...forwardedProps }
|
|
148
181
|
};
|
|
149
182
|
const fetchClient = resolvedOptions.fetchClient ?? fetch;
|
|
150
183
|
const response = await fetchClient(resolvedUrl, {
|
|
@@ -183,17 +216,40 @@ function fetchServerSentEvents(url, options = {}) {
|
|
|
183
216
|
}
|
|
184
217
|
function fetchHttpStream(url, options = {}) {
|
|
185
218
|
return {
|
|
186
|
-
async *connect(messages, data, abortSignal) {
|
|
219
|
+
async *connect(messages, data, abortSignal, runContext) {
|
|
187
220
|
const resolvedUrl = typeof url === "function" ? url() : url;
|
|
188
221
|
const resolvedOptions = typeof options === "function" ? await options() : options;
|
|
189
222
|
const requestHeaders = {
|
|
190
223
|
"Content-Type": "application/json",
|
|
191
224
|
...mergeHeaders(resolvedOptions.headers)
|
|
192
225
|
};
|
|
226
|
+
const wireMessages = uiMessagesToWire(messages);
|
|
227
|
+
const forwardedProps = {
|
|
228
|
+
...resolvedOptions.body,
|
|
229
|
+
...runContext?.forwardedProps ?? {},
|
|
230
|
+
...data
|
|
231
|
+
};
|
|
193
232
|
const requestBody = {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
...
|
|
233
|
+
threadId: runContext?.threadId ?? generateRunId("thread"),
|
|
234
|
+
runId: runContext?.runId ?? generateRunId("run"),
|
|
235
|
+
...runContext?.parentRunId !== void 0 && {
|
|
236
|
+
parentRunId: runContext.parentRunId
|
|
237
|
+
},
|
|
238
|
+
state: {},
|
|
239
|
+
messages: wireMessages,
|
|
240
|
+
tools: runContext?.clientTools ?? [],
|
|
241
|
+
context: [],
|
|
242
|
+
forwardedProps,
|
|
243
|
+
// Backward-compat mirror of `forwardedProps` under the legacy
|
|
244
|
+
// field name `data`. Server endpoints that have not migrated
|
|
245
|
+
// off the pre-AG-UI shape (`{ messages, data }`) keep working.
|
|
246
|
+
// AG-UI strict consumers strip this via `RunAgentInputSchema`
|
|
247
|
+
// (see `chatParamsFromRequestBody`). Will be removed when the
|
|
248
|
+
// legacy `body` client option is dropped.
|
|
249
|
+
// Shallow-cloned so that downstream mutation of `data` (e.g.
|
|
250
|
+
// by a logging interceptor or fetch wrapper) cannot corrupt
|
|
251
|
+
// `forwardedProps` and vice versa.
|
|
252
|
+
data: { ...forwardedProps }
|
|
197
253
|
};
|
|
198
254
|
const fetchClient = resolvedOptions.fetchClient ?? fetch;
|
|
199
255
|
const response = await fetchClient(resolvedUrl, {
|
|
@@ -225,14 +281,14 @@ function fetchHttpStream(url, options = {}) {
|
|
|
225
281
|
}
|
|
226
282
|
function stream(streamFactory) {
|
|
227
283
|
return {
|
|
228
|
-
async *connect(messages, data) {
|
|
284
|
+
async *connect(messages, data, _abortSignal, _runContext) {
|
|
229
285
|
yield* streamFactory(messages, data);
|
|
230
286
|
}
|
|
231
287
|
};
|
|
232
288
|
}
|
|
233
289
|
function rpcStream(rpcCall) {
|
|
234
290
|
return {
|
|
235
|
-
async *connect(messages, data) {
|
|
291
|
+
async *connect(messages, data, _abortSignal, _runContext) {
|
|
236
292
|
yield* rpcCall(messages, data);
|
|
237
293
|
}
|
|
238
294
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection-adapters.js","sources":["../../src/connection-adapters.ts"],"sourcesContent":["import { EventType } from '@tanstack/ai'\nimport type { ModelMessage, StreamChunk, UIMessage } from '@tanstack/ai'\n\n/**\n * Merge custom headers into request headers\n */\nfunction mergeHeaders(\n customHeaders?: Record<string, string> | Headers,\n): Record<string, string> {\n if (!customHeaders) {\n return {}\n }\n if (customHeaders instanceof Headers) {\n const result: Record<string, string> = {}\n customHeaders.forEach((value, key) => {\n result[key] = value\n })\n return result\n }\n return customHeaders\n}\n\n/**\n * Read lines from a stream (newline-delimited)\n */\nasync function* readStreamLines(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n abortSignal?: AbortSignal,\n): AsyncGenerator<string> {\n try {\n const decoder = new TextDecoder()\n let buffer = ''\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n // Check if aborted before reading\n if (abortSignal?.aborted) {\n break\n }\n\n const { done, value } = await reader.read()\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() || ''\n\n for (const line of lines) {\n if (line.trim()) {\n yield line\n }\n }\n }\n\n // Process any remaining data in the buffer\n if (buffer.trim()) {\n yield buffer\n }\n } finally {\n reader.releaseLock()\n }\n}\n\nexport interface ConnectConnectionAdapter {\n /**\n * Connect and return an async iterable of StreamChunks.\n */\n connect: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n ) => AsyncIterable<StreamChunk>\n}\n\nexport interface SubscribeConnectionAdapter {\n /**\n * Subscribe to stream chunks.\n */\n subscribe: (abortSignal?: AbortSignal) => AsyncIterable<StreamChunk>\n /**\n * Send a request; chunks arrive through subscribe().\n */\n send: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n ) => Promise<void>\n}\n\n/**\n * Connection adapter union.\n * Provide either `connect`, or `subscribe` + `send`.\n */\nexport type ConnectionAdapter =\n | ConnectConnectionAdapter\n | SubscribeConnectionAdapter\n\n/**\n * Normalize a ConnectionAdapter to subscribe/send operations.\n *\n * If a connection provides native subscribe/send, that mode is used.\n * Otherwise, connect() is wrapped using an async queue.\n */\nexport function normalizeConnectionAdapter(\n connection: ConnectionAdapter | undefined,\n): SubscribeConnectionAdapter {\n if (!connection) {\n throw new Error('Connection adapter is required')\n }\n\n const hasConnect = 'connect' in connection\n const hasSubscribe = 'subscribe' in connection\n const hasSend = 'send' in connection\n\n if (hasConnect && (hasSubscribe || hasSend)) {\n throw new Error(\n 'Connection adapter must provide either connect or both subscribe and send, not both modes',\n )\n }\n\n if (hasSubscribe && hasSend) {\n return {\n subscribe: connection.subscribe.bind(connection),\n send: connection.send.bind(connection),\n }\n }\n\n if (!hasConnect) {\n throw new Error(\n 'Connection adapter must provide either connect or both subscribe and send',\n )\n }\n\n // Legacy connect() wrapper\n let activeBuffer: Array<StreamChunk> = []\n let activeWaiters: Array<(chunk: StreamChunk | null) => void> = []\n\n function push(chunk: StreamChunk): void {\n const waiter = activeWaiters.shift()\n if (waiter) {\n waiter(chunk)\n } else {\n activeBuffer.push(chunk)\n }\n }\n\n return {\n subscribe(abortSignal?: AbortSignal): AsyncIterable<StreamChunk> {\n // Transfer ownership to the latest subscriber so only one active\n // subscribe() call receives chunks from the shared connect-wrapper queue.\n const myBuffer: Array<StreamChunk> = activeBuffer.splice(0)\n const myWaiters: Array<(chunk: StreamChunk | null) => void> = []\n activeBuffer = myBuffer\n activeWaiters = myWaiters\n\n return (async function* () {\n while (!abortSignal?.aborted) {\n let chunk: StreamChunk | null\n if (myBuffer.length > 0) {\n chunk = myBuffer.shift()!\n } else {\n chunk = await new Promise<StreamChunk | null>((resolve) => {\n const onAbort = () => resolve(null)\n myWaiters.push((c) => {\n abortSignal?.removeEventListener('abort', onAbort)\n resolve(c)\n })\n abortSignal?.addEventListener('abort', onAbort, { once: true })\n })\n }\n if (chunk !== null) yield chunk\n }\n })()\n },\n async send(messages, data, abortSignal) {\n let hasTerminalEvent = false\n try {\n const stream = connection.connect(messages, data, abortSignal)\n for await (const chunk of stream) {\n if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {\n hasTerminalEvent = true\n }\n push(chunk)\n }\n\n // If the connect stream ended cleanly without a terminal event,\n // synthesize RUN_FINISHED so request-scoped consumers can complete.\n if (!abortSignal?.aborted && !hasTerminalEvent) {\n push({\n type: EventType.RUN_FINISHED,\n runId: `run-${Date.now()}`,\n threadId: `thread-${Date.now()}`,\n model: 'connect-wrapper',\n timestamp: Date.now(),\n finishReason: 'stop',\n })\n }\n } catch (err) {\n if (!abortSignal?.aborted && !hasTerminalEvent) {\n push({\n type: EventType.RUN_ERROR,\n timestamp: Date.now(),\n message:\n err instanceof Error ? err.message : 'Unknown error in connect()',\n error: {\n message:\n err instanceof Error\n ? err.message\n : 'Unknown error in connect()',\n },\n })\n }\n throw err\n }\n },\n }\n}\n\n/**\n * Options for fetch-based connection adapters\n */\nexport interface FetchConnectionOptions {\n headers?: Record<string, string> | Headers\n credentials?: RequestCredentials\n signal?: AbortSignal\n body?: Record<string, any>\n fetchClient?: typeof globalThis.fetch\n}\n\n/**\n * Create a Server-Sent Events connection adapter\n *\n * @param url - The API endpoint URL (or a function that returns the URL)\n * @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)\n * @returns A connection adapter for SSE streams\n *\n * @example\n * ```typescript\n * // Static URL\n * const connection = fetchServerSentEvents('/api/chat');\n *\n * // Dynamic URL\n * const connection = fetchServerSentEvents(() => `/api/chat?user=${userId}`);\n *\n * // With options\n * const connection = fetchServerSentEvents('/api/chat', {\n * headers: { 'Authorization': 'Bearer token' }\n * });\n *\n * // With dynamic options\n * const connection = fetchServerSentEvents('/api/chat', () => ({\n * headers: { 'Authorization': `Bearer ${getToken()}` }\n * }));\n *\n * // With additional body data\n * const connection = fetchServerSentEvents('/api/chat', async () => ({\n * body: {\n * provider: 'openai',\n * model: 'gpt-4o',\n * }\n * }));\n * ```\n */\nexport function fetchServerSentEvents(\n url: string | (() => string),\n options:\n | FetchConnectionOptions\n | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal) {\n // Resolve URL and options if they are functions\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(resolvedOptions.headers),\n }\n\n // Send messages as-is (UIMessages with parts preserved)\n // Server-side TextEngine handles conversion to ModelMessages\n const requestBody = {\n messages,\n data,\n ...resolvedOptions.body,\n }\n\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n const response = await fetchClient(resolvedUrl, {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(requestBody),\n credentials: resolvedOptions.credentials || 'same-origin',\n signal: abortSignal || resolvedOptions.signal,\n })\n\n if (!response.ok) {\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n }\n\n // Parse Server-Sent Events format\n const reader = response.body?.getReader()\n if (!reader) {\n throw new Error('Response body is not readable')\n }\n\n for await (const line of readStreamLines(reader, abortSignal)) {\n // Handle Server-Sent Events format\n const data = line.startsWith('data: ') ? line.slice(6) : line\n\n if (data === '[DONE]') {\n console.warn(\n '[@tanstack/ai-client] Received [DONE] sentinel. This is deprecated — upgrade your @tanstack/ai server package. RUN_FINISHED is the stream terminator.',\n )\n continue\n }\n\n try {\n const parsed: StreamChunk = JSON.parse(data)\n yield parsed\n } catch (parseError) {\n // Skip non-JSON lines or malformed chunks\n console.warn('Failed to parse SSE chunk:', data)\n }\n }\n },\n }\n}\n\n/**\n * Create an HTTP streaming connection adapter (for raw streaming without SSE format)\n *\n * @param url - The API endpoint URL (or a function that returns the URL)\n * @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)\n * @returns A connection adapter for HTTP streams\n *\n * @example\n * ```typescript\n * // Static URL\n * const connection = fetchHttpStream('/api/chat');\n *\n * // Dynamic URL\n * const connection = fetchHttpStream(() => `/api/chat?user=${userId}`);\n *\n * // With options\n * const connection = fetchHttpStream('/api/chat', {\n * headers: { 'Authorization': 'Bearer token' }\n * });\n *\n * // With dynamic options\n * const connection = fetchHttpStream('/api/chat', () => ({\n * headers: { 'Authorization': `Bearer ${getToken()}` }\n * }));\n *\n * // With additional body data\n * const connection = fetchHttpStream('/api/chat', async () => ({\n * body: {\n * provider: 'openai',\n * model: 'gpt-4o',\n * }\n * }));\n * ```\n */\nexport function fetchHttpStream(\n url: string | (() => string),\n options:\n | FetchConnectionOptions\n | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal) {\n // Resolve URL and options if they are functions\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(resolvedOptions.headers),\n }\n\n // Send messages as-is (UIMessages with parts preserved)\n // Server-side TextEngine handles conversion to ModelMessages\n const requestBody = {\n messages,\n data,\n ...resolvedOptions.body,\n }\n\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n const response = await fetchClient(resolvedUrl, {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(requestBody),\n credentials: resolvedOptions.credentials || 'same-origin',\n signal: abortSignal || resolvedOptions.signal,\n })\n\n if (!response.ok) {\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n }\n\n // Parse raw HTTP stream (newline-delimited JSON)\n const reader = response.body?.getReader()\n if (!reader) {\n throw new Error('Response body is not readable')\n }\n\n for await (const line of readStreamLines(reader, abortSignal)) {\n try {\n const parsed: StreamChunk = JSON.parse(line)\n yield parsed\n } catch (parseError) {\n console.warn('Failed to parse HTTP stream chunk:', line)\n }\n }\n },\n }\n}\n\n/**\n * Create a direct stream connection adapter (for server functions or direct streams)\n *\n * @param streamFactory - A function that returns an async iterable of StreamChunks\n * @returns A connection adapter for direct streams\n *\n * @example\n * ```typescript\n * // With TanStack Start server function\n * const connection = stream(() => serverFunction({ messages }));\n *\n * const client = new ChatClient({ connection });\n * ```\n */\nexport function stream(\n streamFactory: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n ) => AsyncIterable<StreamChunk>,\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data) {\n // Pass messages as-is (UIMessages with parts preserved)\n // Server-side chat() handles conversion to ModelMessages\n yield* streamFactory(messages, data)\n },\n }\n}\n\n/**\n * Create an RPC stream connection adapter (for RPC-based streaming like Cap'n Web RPC)\n *\n * @param rpcCall - A function that accepts messages and returns an async iterable of StreamChunks\n * @returns A connection adapter for RPC streams\n *\n * @example\n * ```typescript\n * // With Cap'n Web RPC\n * const connection = rpcStream((messages, data) =>\n * api.streamMurfResponse(messages, data)\n * );\n *\n * const client = new ChatClient({ connection });\n * ```\n */\nexport function rpcStream(\n rpcCall: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n ) => AsyncIterable<StreamChunk>,\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data) {\n // Pass messages as-is (UIMessages with parts preserved)\n // Server-side chat() handles conversion to ModelMessages\n yield* rpcCall(messages, data)\n },\n }\n}\n"],"names":["stream","data"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"connection-adapters.js","sources":["../../src/connection-adapters.ts"],"sourcesContent":["import { EventType, uiMessagesToWire } from '@tanstack/ai'\nimport type { ModelMessage, StreamChunk, UIMessage } from '@tanstack/ai'\n\nfunction generateRunId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`\n}\n\n/**\n * Merge custom headers into request headers\n */\nfunction mergeHeaders(\n customHeaders?: Record<string, string> | Headers,\n): Record<string, string> {\n if (!customHeaders) {\n return {}\n }\n if (customHeaders instanceof Headers) {\n const result: Record<string, string> = {}\n customHeaders.forEach((value, key) => {\n result[key] = value\n })\n return result\n }\n return customHeaders\n}\n\n/**\n * Read lines from a stream (newline-delimited)\n */\nasync function* readStreamLines(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n abortSignal?: AbortSignal,\n): AsyncGenerator<string> {\n try {\n const decoder = new TextDecoder()\n let buffer = ''\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n while (true) {\n // Check if aborted before reading\n if (abortSignal?.aborted) {\n break\n }\n\n const { done, value } = await reader.read()\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() || ''\n\n for (const line of lines) {\n if (line.trim()) {\n yield line\n }\n }\n }\n\n // Process any remaining data in the buffer\n if (buffer.trim()) {\n yield buffer\n }\n } finally {\n reader.releaseLock()\n }\n}\n\n/**\n * Per-send context provided by the chat client to the connection adapter.\n * The adapter combines this with serialized messages to build a full\n * AG-UI `RunAgentInput` payload.\n */\nexport interface RunAgentInputContext {\n threadId: string\n runId: string\n parentRunId?: string\n /** Client-declared tools to advertise in the request payload. */\n clientTools?: Array<{\n name: string\n description: string\n parameters: unknown\n }>\n /** Arbitrary user-controlled passthrough data. */\n forwardedProps?: Record<string, unknown>\n}\n\nexport interface ConnectConnectionAdapter {\n /**\n * Connect and return an async iterable of StreamChunks.\n */\n connect: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n runContext?: RunAgentInputContext,\n ) => AsyncIterable<StreamChunk>\n}\n\nexport interface SubscribeConnectionAdapter {\n /**\n * Subscribe to stream chunks.\n */\n subscribe: (abortSignal?: AbortSignal) => AsyncIterable<StreamChunk>\n /**\n * Send a request; chunks arrive through subscribe().\n */\n send: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n abortSignal?: AbortSignal,\n runContext?: RunAgentInputContext,\n ) => Promise<void>\n}\n\n/**\n * Connection adapter union.\n * Provide either `connect`, or `subscribe` + `send`.\n */\nexport type ConnectionAdapter =\n | ConnectConnectionAdapter\n | SubscribeConnectionAdapter\n\n/**\n * Normalize a ConnectionAdapter to subscribe/send operations.\n *\n * If a connection provides native subscribe/send, that mode is used.\n * Otherwise, connect() is wrapped using an async queue.\n */\nexport function normalizeConnectionAdapter(\n connection: ConnectionAdapter | undefined,\n): SubscribeConnectionAdapter {\n if (!connection) {\n throw new Error('Connection adapter is required')\n }\n\n const hasConnect = 'connect' in connection\n const hasSubscribe = 'subscribe' in connection\n const hasSend = 'send' in connection\n\n if (hasConnect && (hasSubscribe || hasSend)) {\n throw new Error(\n 'Connection adapter must provide either connect or both subscribe and send, not both modes',\n )\n }\n\n if (hasSubscribe && hasSend) {\n return {\n subscribe: connection.subscribe.bind(connection),\n send: connection.send.bind(connection),\n }\n }\n\n if (!hasConnect) {\n throw new Error(\n 'Connection adapter must provide either connect or both subscribe and send',\n )\n }\n\n // Legacy connect() wrapper\n let activeBuffer: Array<StreamChunk> = []\n let activeWaiters: Array<(chunk: StreamChunk | null) => void> = []\n\n function push(chunk: StreamChunk): void {\n const waiter = activeWaiters.shift()\n if (waiter) {\n waiter(chunk)\n } else {\n activeBuffer.push(chunk)\n }\n }\n\n return {\n subscribe(abortSignal?: AbortSignal): AsyncIterable<StreamChunk> {\n // Transfer ownership to the latest subscriber so only one active\n // subscribe() call receives chunks from the shared connect-wrapper queue.\n const myBuffer: Array<StreamChunk> = activeBuffer.splice(0)\n const myWaiters: Array<(chunk: StreamChunk | null) => void> = []\n activeBuffer = myBuffer\n activeWaiters = myWaiters\n\n return (async function* () {\n while (!abortSignal?.aborted) {\n let chunk: StreamChunk | null\n if (myBuffer.length > 0) {\n chunk = myBuffer.shift()!\n } else {\n chunk = await new Promise<StreamChunk | null>((resolve) => {\n const onAbort = () => resolve(null)\n myWaiters.push((c) => {\n abortSignal?.removeEventListener('abort', onAbort)\n resolve(c)\n })\n abortSignal?.addEventListener('abort', onAbort, { once: true })\n })\n }\n if (chunk !== null) yield chunk\n }\n })()\n },\n async send(messages, data, abortSignal, runContext) {\n let hasTerminalEvent = false\n try {\n const stream = connection.connect(\n messages,\n data,\n abortSignal,\n runContext,\n )\n for await (const chunk of stream) {\n if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {\n hasTerminalEvent = true\n }\n push(chunk)\n }\n\n // If the connect stream ended cleanly without a terminal event,\n // synthesize RUN_FINISHED so request-scoped consumers can complete.\n // Reuse the caller's threadId/runId so client-side activeRunIds tracking matches.\n if (!abortSignal?.aborted && !hasTerminalEvent) {\n push({\n type: EventType.RUN_FINISHED,\n threadId: runContext?.threadId ?? `thread-${Date.now()}`,\n runId: runContext?.runId ?? `run-${Date.now()}`,\n model: 'connect-wrapper',\n timestamp: Date.now(),\n finishReason: 'stop',\n })\n }\n } catch (err) {\n if (!abortSignal?.aborted && !hasTerminalEvent) {\n push({\n type: EventType.RUN_ERROR,\n threadId: runContext?.threadId ?? `thread-${Date.now()}`,\n runId: runContext?.runId ?? `run-${Date.now()}`,\n timestamp: Date.now(),\n message:\n err instanceof Error ? err.message : 'Unknown error in connect()',\n error: {\n message:\n err instanceof Error\n ? err.message\n : 'Unknown error in connect()',\n },\n })\n }\n throw err\n }\n },\n }\n}\n\n/**\n * Options for fetch-based connection adapters\n */\nexport interface FetchConnectionOptions {\n headers?: Record<string, string> | Headers\n credentials?: RequestCredentials\n signal?: AbortSignal\n body?: Record<string, any>\n fetchClient?: typeof globalThis.fetch\n}\n\n/**\n * Create a Server-Sent Events connection adapter\n *\n * @param url - The API endpoint URL (or a function that returns the URL)\n * @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)\n * @returns A connection adapter for SSE streams\n *\n * @example\n * ```typescript\n * // Static URL\n * const connection = fetchServerSentEvents('/api/chat');\n *\n * // Dynamic URL\n * const connection = fetchServerSentEvents(() => `/api/chat?user=${userId}`);\n *\n * // With options\n * const connection = fetchServerSentEvents('/api/chat', {\n * headers: { 'Authorization': 'Bearer token' }\n * });\n *\n * // With dynamic options\n * const connection = fetchServerSentEvents('/api/chat', () => ({\n * headers: { 'Authorization': `Bearer ${getToken()}` }\n * }));\n *\n * // With additional body data\n * const connection = fetchServerSentEvents('/api/chat', async () => ({\n * body: {\n * provider: 'openai',\n * model: 'gpt-4o',\n * }\n * }));\n * ```\n */\nexport function fetchServerSentEvents(\n url: string | (() => string),\n options:\n | FetchConnectionOptions\n | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal, runContext) {\n // Resolve URL and options if they are functions\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(resolvedOptions.headers),\n }\n\n // Build AG-UI RunAgentInput payload.\n //\n // Precedence (later spreads win): static adapter `body` is the base,\n // overridden by `runContext.forwardedProps` (constructor body /\n // forwardedProps options), overridden by per-message `data` passed\n // to `connection.send`. Runtime values win over static config —\n // this matches the documented \"forwardedProps wins\" semantic.\n const wireMessages = uiMessagesToWire(messages as Array<UIMessage>)\n const forwardedProps = {\n ...resolvedOptions.body,\n ...(runContext?.forwardedProps ?? {}),\n ...data,\n }\n const requestBody = {\n threadId: runContext?.threadId ?? generateRunId('thread'),\n runId: runContext?.runId ?? generateRunId('run'),\n ...(runContext?.parentRunId !== undefined && {\n parentRunId: runContext.parentRunId,\n }),\n state: {},\n messages: wireMessages,\n tools: runContext?.clientTools ?? [],\n context: [],\n forwardedProps,\n // Backward-compat mirror of `forwardedProps` under the legacy\n // field name `data`. Server endpoints that have not migrated\n // off the pre-AG-UI shape (`{ messages, data }`) keep working.\n // AG-UI strict consumers strip this via `RunAgentInputSchema`\n // (see `chatParamsFromRequestBody`). Will be removed when the\n // legacy `body` client option is dropped.\n // Shallow-cloned so that downstream mutation of `data` (e.g.\n // by a logging interceptor or fetch wrapper) cannot corrupt\n // `forwardedProps` and vice versa.\n data: { ...forwardedProps },\n }\n\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n const response = await fetchClient(resolvedUrl, {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(requestBody),\n credentials: resolvedOptions.credentials || 'same-origin',\n signal: abortSignal || resolvedOptions.signal,\n })\n\n if (!response.ok) {\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n }\n\n // Parse Server-Sent Events format\n const reader = response.body?.getReader()\n if (!reader) {\n throw new Error('Response body is not readable')\n }\n\n for await (const line of readStreamLines(reader, abortSignal)) {\n // Handle Server-Sent Events format\n const data = line.startsWith('data: ') ? line.slice(6) : line\n\n if (data === '[DONE]') {\n console.warn(\n '[@tanstack/ai-client] Received [DONE] sentinel. This is deprecated — upgrade your @tanstack/ai server package. RUN_FINISHED is the stream terminator.',\n )\n continue\n }\n\n try {\n const parsed: StreamChunk = JSON.parse(data)\n yield parsed\n } catch (parseError) {\n // Skip non-JSON lines or malformed chunks\n console.warn('Failed to parse SSE chunk:', data)\n }\n }\n },\n }\n}\n\n/**\n * Create an HTTP streaming connection adapter (for raw streaming without SSE format)\n *\n * @param url - The API endpoint URL (or a function that returns the URL)\n * @param options - Fetch options (headers, credentials, body, etc.) or a function that returns options (can be async)\n * @returns A connection adapter for HTTP streams\n *\n * @example\n * ```typescript\n * // Static URL\n * const connection = fetchHttpStream('/api/chat');\n *\n * // Dynamic URL\n * const connection = fetchHttpStream(() => `/api/chat?user=${userId}`);\n *\n * // With options\n * const connection = fetchHttpStream('/api/chat', {\n * headers: { 'Authorization': 'Bearer token' }\n * });\n *\n * // With dynamic options\n * const connection = fetchHttpStream('/api/chat', () => ({\n * headers: { 'Authorization': `Bearer ${getToken()}` }\n * }));\n *\n * // With additional body data\n * const connection = fetchHttpStream('/api/chat', async () => ({\n * body: {\n * provider: 'openai',\n * model: 'gpt-4o',\n * }\n * }));\n * ```\n */\nexport function fetchHttpStream(\n url: string | (() => string),\n options:\n | FetchConnectionOptions\n | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data, abortSignal, runContext) {\n // Resolve URL and options if they are functions\n const resolvedUrl = typeof url === 'function' ? url() : url\n const resolvedOptions =\n typeof options === 'function' ? await options() : options\n\n const requestHeaders: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...mergeHeaders(resolvedOptions.headers),\n }\n\n // Build AG-UI RunAgentInput payload.\n //\n // Precedence (later spreads win): static adapter `body` is the base,\n // overridden by `runContext.forwardedProps` (constructor body /\n // forwardedProps options), overridden by per-message `data` passed\n // to `connection.send`. Runtime values win over static config —\n // this matches the documented \"forwardedProps wins\" semantic.\n const wireMessages = uiMessagesToWire(messages as Array<UIMessage>)\n const forwardedProps = {\n ...resolvedOptions.body,\n ...(runContext?.forwardedProps ?? {}),\n ...data,\n }\n const requestBody = {\n threadId: runContext?.threadId ?? generateRunId('thread'),\n runId: runContext?.runId ?? generateRunId('run'),\n ...(runContext?.parentRunId !== undefined && {\n parentRunId: runContext.parentRunId,\n }),\n state: {},\n messages: wireMessages,\n tools: runContext?.clientTools ?? [],\n context: [],\n forwardedProps,\n // Backward-compat mirror of `forwardedProps` under the legacy\n // field name `data`. Server endpoints that have not migrated\n // off the pre-AG-UI shape (`{ messages, data }`) keep working.\n // AG-UI strict consumers strip this via `RunAgentInputSchema`\n // (see `chatParamsFromRequestBody`). Will be removed when the\n // legacy `body` client option is dropped.\n // Shallow-cloned so that downstream mutation of `data` (e.g.\n // by a logging interceptor or fetch wrapper) cannot corrupt\n // `forwardedProps` and vice versa.\n data: { ...forwardedProps },\n }\n\n const fetchClient = resolvedOptions.fetchClient ?? fetch\n const response = await fetchClient(resolvedUrl, {\n method: 'POST',\n headers: requestHeaders,\n body: JSON.stringify(requestBody),\n credentials: resolvedOptions.credentials || 'same-origin',\n signal: abortSignal || resolvedOptions.signal,\n })\n\n if (!response.ok) {\n throw new Error(\n `HTTP error! status: ${response.status} ${response.statusText}`,\n )\n }\n\n // Parse raw HTTP stream (newline-delimited JSON)\n const reader = response.body?.getReader()\n if (!reader) {\n throw new Error('Response body is not readable')\n }\n\n for await (const line of readStreamLines(reader, abortSignal)) {\n try {\n const parsed: StreamChunk = JSON.parse(line)\n yield parsed\n } catch (parseError) {\n console.warn('Failed to parse HTTP stream chunk:', line)\n }\n }\n },\n }\n}\n\n/**\n * Create a direct stream connection adapter (for server functions or direct streams)\n *\n * @param streamFactory - A function that returns an async iterable of StreamChunks\n * @returns A connection adapter for direct streams\n *\n * @example\n * ```typescript\n * // With TanStack Start server function\n * const connection = stream(() => serverFunction({ messages }));\n *\n * const client = new ChatClient({ connection });\n * ```\n */\nexport function stream(\n streamFactory: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n ) => AsyncIterable<StreamChunk>,\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data, _abortSignal, _runContext) {\n // Pass messages as-is (UIMessages with parts preserved)\n // Server-side chat() handles conversion to ModelMessages\n yield* streamFactory(messages, data)\n },\n }\n}\n\n/**\n * Create an RPC stream connection adapter (for RPC-based streaming like Cap'n Web RPC)\n *\n * @param rpcCall - A function that accepts messages and returns an async iterable of StreamChunks\n * @returns A connection adapter for RPC streams\n *\n * @example\n * ```typescript\n * // With Cap'n Web RPC\n * const connection = rpcStream((messages, data) =>\n * api.streamMurfResponse(messages, data)\n * );\n *\n * const client = new ChatClient({ connection });\n * ```\n */\nexport function rpcStream(\n rpcCall: (\n messages: Array<UIMessage> | Array<ModelMessage>,\n data?: Record<string, any>,\n ) => AsyncIterable<StreamChunk>,\n): ConnectConnectionAdapter {\n return {\n async *connect(messages, data, _abortSignal, _runContext) {\n // Pass messages as-is (UIMessages with parts preserved)\n // Server-side chat() handles conversion to ModelMessages\n yield* rpcCall(messages, data)\n },\n }\n}\n"],"names":["stream","data"],"mappings":";AAGA,SAAS,cAAc,QAAwB;AAC7C,SAAO,GAAG,MAAM,IAAI,KAAK,IAAA,CAAK,IAAI,KAAK,OAAA,EAAS,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AAC1E;AAKA,SAAS,aACP,eACwB;AACxB,MAAI,CAAC,eAAe;AAClB,WAAO,CAAA;AAAA,EACT;AACA,MAAI,yBAAyB,SAAS;AACpC,UAAM,SAAiC,CAAA;AACvC,kBAAc,QAAQ,CAAC,OAAO,QAAQ;AACpC,aAAO,GAAG,IAAI;AAAA,IAChB,CAAC;AACD,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAKA,gBAAgB,gBACd,QACA,aACwB;AACxB,MAAI;AACF,UAAM,UAAU,IAAI,YAAA;AACpB,QAAI,SAAS;AAGb,WAAO,MAAM;AAEX,UAAI,aAAa,SAAS;AACxB;AAAA,MACF;AAEA,YAAM,EAAE,MAAM,MAAA,IAAU,MAAM,OAAO,KAAA;AACrC,UAAI,KAAM;AAEV,gBAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM;AAChD,YAAM,QAAQ,OAAO,MAAM,IAAI;AAG/B,eAAS,MAAM,SAAS;AAExB,iBAAW,QAAQ,OAAO;AACxB,YAAI,KAAK,QAAQ;AACf,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAGA,QAAI,OAAO,QAAQ;AACjB,YAAM;AAAA,IACR;AAAA,EACF,UAAA;AACE,WAAO,YAAA;AAAA,EACT;AACF;AA+DO,SAAS,2BACd,YAC4B;AAC5B,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,MAAM,gCAAgC;AAAA,EAClD;AAEA,QAAM,aAAa,aAAa;AAChC,QAAM,eAAe,eAAe;AACpC,QAAM,UAAU,UAAU;AAE1B,MAAI,eAAe,gBAAgB,UAAU;AAC3C,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAEA,MAAI,gBAAgB,SAAS;AAC3B,WAAO;AAAA,MACL,WAAW,WAAW,UAAU,KAAK,UAAU;AAAA,MAC/C,MAAM,WAAW,KAAK,KAAK,UAAU;AAAA,IAAA;AAAA,EAEzC;AAEA,MAAI,CAAC,YAAY;AACf,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAGA,MAAI,eAAmC,CAAA;AACvC,MAAI,gBAA4D,CAAA;AAEhE,WAAS,KAAK,OAA0B;AACtC,UAAM,SAAS,cAAc,MAAA;AAC7B,QAAI,QAAQ;AACV,aAAO,KAAK;AAAA,IACd,OAAO;AACL,mBAAa,KAAK,KAAK;AAAA,IACzB;AAAA,EACF;AAEA,SAAO;AAAA,IACL,UAAU,aAAuD;AAG/D,YAAM,WAA+B,aAAa,OAAO,CAAC;AAC1D,YAAM,YAAwD,CAAA;AAC9D,qBAAe;AACf,sBAAgB;AAEhB,cAAQ,mBAAmB;AACzB,eAAO,CAAC,aAAa,SAAS;AAC5B,cAAI;AACJ,cAAI,SAAS,SAAS,GAAG;AACvB,oBAAQ,SAAS,MAAA;AAAA,UACnB,OAAO;AACL,oBAAQ,MAAM,IAAI,QAA4B,CAAC,YAAY;AACzD,oBAAM,UAAU,MAAM,QAAQ,IAAI;AAClC,wBAAU,KAAK,CAAC,MAAM;AACpB,6BAAa,oBAAoB,SAAS,OAAO;AACjD,wBAAQ,CAAC;AAAA,cACX,CAAC;AACD,2BAAa,iBAAiB,SAAS,SAAS,EAAE,MAAM,MAAM;AAAA,YAChE,CAAC;AAAA,UACH;AACA,cAAI,UAAU,KAAM,OAAM;AAAA,QAC5B;AAAA,MACF,GAAA;AAAA,IACF;AAAA,IACA,MAAM,KAAK,UAAU,MAAM,aAAa,YAAY;AAClD,UAAI,mBAAmB;AACvB,UAAI;AACF,cAAMA,UAAS,WAAW;AAAA,UACxB;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QAAA;AAEF,yBAAiB,SAASA,SAAQ;AAChC,cAAI,MAAM,SAAS,kBAAkB,MAAM,SAAS,aAAa;AAC/D,+BAAmB;AAAA,UACrB;AACA,eAAK,KAAK;AAAA,QACZ;AAKA,YAAI,CAAC,aAAa,WAAW,CAAC,kBAAkB;AAC9C,eAAK;AAAA,YACH,MAAM,UAAU;AAAA,YAChB,UAAU,YAAY,YAAY,UAAU,KAAK,KAAK;AAAA,YACtD,OAAO,YAAY,SAAS,OAAO,KAAK,KAAK;AAAA,YAC7C,OAAO;AAAA,YACP,WAAW,KAAK,IAAA;AAAA,YAChB,cAAc;AAAA,UAAA,CACf;AAAA,QACH;AAAA,MACF,SAAS,KAAK;AACZ,YAAI,CAAC,aAAa,WAAW,CAAC,kBAAkB;AAC9C,eAAK;AAAA,YACH,MAAM,UAAU;AAAA,YAChB,UAAU,YAAY,YAAY,UAAU,KAAK,KAAK;AAAA,YACtD,OAAO,YAAY,SAAS,OAAO,KAAK,KAAK;AAAA,YAC7C,WAAW,KAAK,IAAA;AAAA,YAChB,SACE,eAAe,QAAQ,IAAI,UAAU;AAAA,YACvC,OAAO;AAAA,cACL,SACE,eAAe,QACX,IAAI,UACJ;AAAA,YAAA;AAAA,UACR,CACD;AAAA,QACH;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EAAA;AAEJ;AA+CO,SAAS,sBACd,KACA,UAEuE,IAC7C;AAC1B,SAAO;AAAA,IACL,OAAO,QAAQ,UAAU,MAAM,aAAa,YAAY;AAEtD,YAAM,cAAc,OAAO,QAAQ,aAAa,QAAQ;AACxD,YAAM,kBACJ,OAAO,YAAY,aAAa,MAAM,YAAY;AAEpD,YAAM,iBAAyC;AAAA,QAC7C,gBAAgB;AAAA,QAChB,GAAG,aAAa,gBAAgB,OAAO;AAAA,MAAA;AAUzC,YAAM,eAAe,iBAAiB,QAA4B;AAClE,YAAM,iBAAiB;AAAA,QACrB,GAAG,gBAAgB;AAAA,QACnB,GAAI,YAAY,kBAAkB,CAAA;AAAA,QAClC,GAAG;AAAA,MAAA;AAEL,YAAM,cAAc;AAAA,QAClB,UAAU,YAAY,YAAY,cAAc,QAAQ;AAAA,QACxD,OAAO,YAAY,SAAS,cAAc,KAAK;AAAA,QAC/C,GAAI,YAAY,gBAAgB,UAAa;AAAA,UAC3C,aAAa,WAAW;AAAA,QAAA;AAAA,QAE1B,OAAO,CAAA;AAAA,QACP,UAAU;AAAA,QACV,OAAO,YAAY,eAAe,CAAA;AAAA,QAClC,SAAS,CAAA;AAAA,QACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAUA,MAAM,EAAE,GAAG,eAAA;AAAA,MAAe;AAG5B,YAAM,cAAc,gBAAgB,eAAe;AACnD,YAAM,WAAW,MAAM,YAAY,aAAa;AAAA,QAC9C,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,MAAM,KAAK,UAAU,WAAW;AAAA,QAChC,aAAa,gBAAgB,eAAe;AAAA,QAC5C,QAAQ,eAAe,gBAAgB;AAAA,MAAA,CACxC;AAED,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,IAAI;AAAA,UACR,uBAAuB,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,QAAA;AAAA,MAEjE;AAGA,YAAM,SAAS,SAAS,MAAM,UAAA;AAC9B,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI,MAAM,+BAA+B;AAAA,MACjD;AAEA,uBAAiB,QAAQ,gBAAgB,QAAQ,WAAW,GAAG;AAE7D,cAAMC,QAAO,KAAK,WAAW,QAAQ,IAAI,KAAK,MAAM,CAAC,IAAI;AAEzD,YAAIA,UAAS,UAAU;AACrB,kBAAQ;AAAA,YACN;AAAA,UAAA;AAEF;AAAA,QACF;AAEA,YAAI;AACF,gBAAM,SAAsB,KAAK,MAAMA,KAAI;AAC3C,gBAAM;AAAA,QACR,SAAS,YAAY;AAEnB,kBAAQ,KAAK,8BAA8BA,KAAI;AAAA,QACjD;AAAA,MACF;AAAA,IACF;AAAA,EAAA;AAEJ;AAoCO,SAAS,gBACd,KACA,UAEuE,IAC7C;AAC1B,SAAO;AAAA,IACL,OAAO,QAAQ,UAAU,MAAM,aAAa,YAAY;AAEtD,YAAM,cAAc,OAAO,QAAQ,aAAa,QAAQ;AACxD,YAAM,kBACJ,OAAO,YAAY,aAAa,MAAM,YAAY;AAEpD,YAAM,iBAAyC;AAAA,QAC7C,gBAAgB;AAAA,QAChB,GAAG,aAAa,gBAAgB,OAAO;AAAA,MAAA;AAUzC,YAAM,eAAe,iBAAiB,QAA4B;AAClE,YAAM,iBAAiB;AAAA,QACrB,GAAG,gBAAgB;AAAA,QACnB,GAAI,YAAY,kBAAkB,CAAA;AAAA,QAClC,GAAG;AAAA,MAAA;AAEL,YAAM,cAAc;AAAA,QAClB,UAAU,YAAY,YAAY,cAAc,QAAQ;AAAA,QACxD,OAAO,YAAY,SAAS,cAAc,KAAK;AAAA,QAC/C,GAAI,YAAY,gBAAgB,UAAa;AAAA,UAC3C,aAAa,WAAW;AAAA,QAAA;AAAA,QAE1B,OAAO,CAAA;AAAA,QACP,UAAU;AAAA,QACV,OAAO,YAAY,eAAe,CAAA;AAAA,QAClC,SAAS,CAAA;AAAA,QACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAUA,MAAM,EAAE,GAAG,eAAA;AAAA,MAAe;AAG5B,YAAM,cAAc,gBAAgB,eAAe;AACnD,YAAM,WAAW,MAAM,YAAY,aAAa;AAAA,QAC9C,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,MAAM,KAAK,UAAU,WAAW;AAAA,QAChC,aAAa,gBAAgB,eAAe;AAAA,QAC5C,QAAQ,eAAe,gBAAgB;AAAA,MAAA,CACxC;AAED,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,IAAI;AAAA,UACR,uBAAuB,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,QAAA;AAAA,MAEjE;AAGA,YAAM,SAAS,SAAS,MAAM,UAAA;AAC9B,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI,MAAM,+BAA+B;AAAA,MACjD;AAEA,uBAAiB,QAAQ,gBAAgB,QAAQ,WAAW,GAAG;AAC7D,YAAI;AACF,gBAAM,SAAsB,KAAK,MAAM,IAAI;AAC3C,gBAAM;AAAA,QACR,SAAS,YAAY;AACnB,kBAAQ,KAAK,sCAAsC,IAAI;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAAA,EAAA;AAEJ;AAgBO,SAAS,OACd,eAI0B;AAC1B,SAAO;AAAA,IACL,OAAO,QAAQ,UAAU,MAAM,cAAc,aAAa;AAGxD,aAAO,cAAc,UAAU,IAAI;AAAA,IACrC;AAAA,EAAA;AAEJ;AAkBO,SAAS,UACd,SAI0B;AAC1B,SAAO;AAAA,IACL,OAAO,QAAQ,UAAU,MAAM,cAAc,aAAa;AAGxD,aAAO,QAAQ,UAAU,IAAI;AAAA,IAC/B;AAAA,EAAA;AAEJ;"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -144,7 +144,26 @@ export interface ChatClientOptions<TTools extends ReadonlyArray<AnyClientTool> =
|
|
|
144
144
|
*/
|
|
145
145
|
id?: string;
|
|
146
146
|
/**
|
|
147
|
-
*
|
|
147
|
+
* Thread ID to use for this chat session. Persists across sends within
|
|
148
|
+
* the session. If omitted, a unique thread ID is generated.
|
|
149
|
+
*/
|
|
150
|
+
threadId?: string;
|
|
151
|
+
/**
|
|
152
|
+
* Arbitrary client-controlled JSON forwarded to the server in the
|
|
153
|
+
* AG-UI `RunAgentInput.forwardedProps` field. Use this for per-session
|
|
154
|
+
* options like provider/model selection or feature flags that the
|
|
155
|
+
* server endpoint should read.
|
|
156
|
+
*
|
|
157
|
+
* Replaces the legacy `body` option. If both are provided,
|
|
158
|
+
* `forwardedProps` wins on key collision.
|
|
159
|
+
*/
|
|
160
|
+
forwardedProps?: Record<string, any>;
|
|
161
|
+
/**
|
|
162
|
+
* @deprecated Use `forwardedProps` instead. `body` continues to work
|
|
163
|
+
* unchanged — its values are merged into the AG-UI
|
|
164
|
+
* `RunAgentInput.forwardedProps` field on the wire and are also
|
|
165
|
+
* mirrored under the legacy `data` field for servers that have not
|
|
166
|
+
* migrated yet. Will be removed in a future major release.
|
|
148
167
|
*/
|
|
149
168
|
body?: Record<string, any>;
|
|
150
169
|
/**
|
package/dist/esm/types.js.map
CHANGED
|
@@ -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 * Connection lifecycle state for the subscription loop.\n */\nexport type ConnectionStatus =\n | 'disconnected'\n | 'connecting'\n | 'connected'\n | '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 * Supports mutually exclusive modes: request-response via `connect()`, or\n * subscribe/send mode via `subscribe()` + `send()`.\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 subscription lifecycle changes.\n * This is independent from request lifecycle (`isLoading`, `status`).\n */\n onSubscriptionChange?: (isSubscribed: boolean) => void\n\n /**\n * Callback when connection lifecycle changes.\n */\n onConnectionStatusChange?: (status: ConnectionStatus) => void\n\n /**\n * Callback when session generation activity changes.\n * Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).\n * Unlike `onLoadingChange` (request-local), this reflects shared generation\n * activity visible to all subscribers (e.g. across tabs/devices).\n */\n onSessionGeneratingChange?: (isGenerating: boolean) => 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":"AAmUO,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 * Connection lifecycle state for the subscription loop.\n */\nexport type ConnectionStatus =\n | 'disconnected'\n | 'connecting'\n | 'connected'\n | '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 * Supports mutually exclusive modes: request-response via `connect()`, or\n * subscribe/send mode via `subscribe()` + `send()`.\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 * Thread ID to use for this chat session. Persists across sends within\n * the session. If omitted, a unique thread ID is generated.\n */\n threadId?: string\n\n /**\n * Arbitrary client-controlled JSON forwarded to the server in the\n * AG-UI `RunAgentInput.forwardedProps` field. Use this for per-session\n * options like provider/model selection or feature flags that the\n * server endpoint should read.\n *\n * Replaces the legacy `body` option. If both are provided,\n * `forwardedProps` wins on key collision.\n */\n forwardedProps?: Record<string, any>\n\n /**\n * @deprecated Use `forwardedProps` instead. `body` continues to work\n * unchanged — its values are merged into the AG-UI\n * `RunAgentInput.forwardedProps` field on the wire and are also\n * mirrored under the legacy `data` field for servers that have not\n * migrated yet. Will be removed in a future major release.\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 subscription lifecycle changes.\n * This is independent from request lifecycle (`isLoading`, `status`).\n */\n onSubscriptionChange?: (isSubscribed: boolean) => void\n\n /**\n * Callback when connection lifecycle changes.\n */\n onConnectionStatusChange?: (status: ConnectionStatus) => void\n\n /**\n * Callback when session generation activity changes.\n * Derived from stream run events (RUN_STARTED / RUN_FINISHED / RUN_ERROR).\n * Unlike `onLoadingChange` (request-local), this reflects shared generation\n * activity visible to all subscribers (e.g. across tabs/devices).\n */\n onSessionGeneratingChange?: (isGenerating: boolean) => 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":"AAwVO,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.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Framework-agnostic headless client for TanStack AI",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"src"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@tanstack/ai": "0.
|
|
35
|
-
"@tanstack/ai-event-client": "0.3.
|
|
34
|
+
"@tanstack/ai": "0.18.0",
|
|
35
|
+
"@tanstack/ai-event-client": "0.3.2"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@standard-schema/spec": "^1.1.0",
|
package/src/chat-client.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
StreamProcessor,
|
|
3
|
+
convertSchemaToJsonSchema,
|
|
3
4
|
generateMessageId,
|
|
4
5
|
normalizeToUIMessage,
|
|
5
6
|
} from '@tanstack/ai'
|
|
@@ -30,7 +31,13 @@ export class ChatClient {
|
|
|
30
31
|
private processor: StreamProcessor
|
|
31
32
|
private connection: SubscribeConnectionAdapter
|
|
32
33
|
private uniqueId: string
|
|
33
|
-
private
|
|
34
|
+
private threadId: string
|
|
35
|
+
// Track the legacy `body` option and the canonical `forwardedProps`
|
|
36
|
+
// option as separate slots so that `updateOptions({ forwardedProps })`
|
|
37
|
+
// doesn't wipe a previously-set `body` (and vice versa). They are
|
|
38
|
+
// merged on every send, with `forwardedProps` winning on key collision.
|
|
39
|
+
private bodyOption: Record<string, any> = {}
|
|
40
|
+
private forwardedPropsOption: Record<string, any> = {}
|
|
34
41
|
private pendingMessageBody: Record<string, any> | undefined = undefined
|
|
35
42
|
private isLoading = false
|
|
36
43
|
private isSubscribed = false
|
|
@@ -81,7 +88,14 @@ export class ChatClient {
|
|
|
81
88
|
|
|
82
89
|
constructor(options: ChatClientOptions) {
|
|
83
90
|
this.uniqueId = options.id || this.generateUniqueId('chat')
|
|
84
|
-
this.
|
|
91
|
+
this.threadId = options.threadId || this.generateUniqueId('thread')
|
|
92
|
+
// Both `body` (deprecated) and `forwardedProps` populate the AG-UI
|
|
93
|
+
// `RunAgentInput.forwardedProps` wire field. They are stored
|
|
94
|
+
// separately so `updateOptions` can replace one without touching the
|
|
95
|
+
// other; the merge happens at send time, with `forwardedProps`
|
|
96
|
+
// winning on key collision.
|
|
97
|
+
this.bodyOption = options.body || {}
|
|
98
|
+
this.forwardedPropsOption = options.forwardedProps || {}
|
|
85
99
|
this.connection = normalizeConnectionAdapter(options.connection)
|
|
86
100
|
this.events = new DefaultChatClientEventEmitter(this.uniqueId)
|
|
87
101
|
|
|
@@ -154,11 +168,7 @@ export class ChatClient {
|
|
|
154
168
|
this.events.textUpdated(this.currentStreamId, messageId, content)
|
|
155
169
|
}
|
|
156
170
|
},
|
|
157
|
-
onThinkingUpdate: (
|
|
158
|
-
messageId: string,
|
|
159
|
-
_stepId: string,
|
|
160
|
-
content: string,
|
|
161
|
-
) => {
|
|
171
|
+
onThinkingUpdate: (messageId: string, content: string) => {
|
|
162
172
|
// Emit thinking update to devtools
|
|
163
173
|
if (this.currentStreamId) {
|
|
164
174
|
this.events.thinkingUpdated(
|
|
@@ -401,7 +411,14 @@ export class ChatClient {
|
|
|
401
411
|
// RUN_FINISHED / RUN_ERROR signal run completion — resolve processing
|
|
402
412
|
// (redundant if onStreamEnd already resolved it, harmless)
|
|
403
413
|
if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {
|
|
404
|
-
|
|
414
|
+
// RUN_FINISHED has runId in its schema; RUN_ERROR carries it via the
|
|
415
|
+
// AG-UI passthrough so adapters can correlate per-run errors. Extract
|
|
416
|
+
// both so a RUN_ERROR with a runId only clears that run, not every
|
|
417
|
+
// active run in the session.
|
|
418
|
+
const runId =
|
|
419
|
+
chunk.type === 'RUN_FINISHED'
|
|
420
|
+
? chunk.runId
|
|
421
|
+
: (chunk as { runId?: string }).runId
|
|
405
422
|
if (runId) {
|
|
406
423
|
this.activeRunIds.delete(runId)
|
|
407
424
|
} else if (chunk.type === 'RUN_ERROR') {
|
|
@@ -596,12 +613,19 @@ export class ChatClient {
|
|
|
596
613
|
return false
|
|
597
614
|
}
|
|
598
615
|
|
|
599
|
-
// Merge
|
|
600
|
-
//
|
|
616
|
+
// Merge sources for the wire `forwardedProps` field, in priority
|
|
617
|
+
// order (later spreads win):
|
|
618
|
+
// 1. Legacy `body` option (deprecated).
|
|
619
|
+
// 2. Canonical `forwardedProps` option (wins over `body`).
|
|
620
|
+
// 3. Per-message `body` arg passed to `sendMessage` (highest).
|
|
621
|
+
// The AG-UI standard `threadId` is sent at the wire's top level for
|
|
622
|
+
// run/conversation correlation, so we no longer auto-emit a separate
|
|
623
|
+
// `conversationId` here — `chat({ threadId })` server-side covers the
|
|
624
|
+
// same role for devtools/observability.
|
|
601
625
|
const mergedBody = {
|
|
602
|
-
...this.
|
|
626
|
+
...this.bodyOption,
|
|
627
|
+
...this.forwardedPropsOption,
|
|
603
628
|
...this.pendingMessageBody,
|
|
604
|
-
conversationId: this.uniqueId,
|
|
605
629
|
}
|
|
606
630
|
|
|
607
631
|
// Clear the pending message body after use
|
|
@@ -622,8 +646,31 @@ export class ChatClient {
|
|
|
622
646
|
// Set up promise that resolves when onStreamEnd fires
|
|
623
647
|
const processingComplete = this.waitForProcessing()
|
|
624
648
|
|
|
649
|
+
// Build per-send run context for AG-UI compliance
|
|
650
|
+
// Note: mergedBody already contains the merged this.body + pendingMessageBody
|
|
651
|
+
// (pendingMessageBody was cleared above, so we use mergedBody as forwardedProps)
|
|
652
|
+
// Convert each client tool's `inputSchema` (a Standard Schema:
|
|
653
|
+
// Zod, ArkType, Valibot, etc.) to JSON Schema for the wire. Foreign
|
|
654
|
+
// AG-UI servers consuming `RunAgentInput.tools[].parameters` expect
|
|
655
|
+
// JSON Schema; sending a Standard Schema instance directly would
|
|
656
|
+
// serialize to an unusable shape.
|
|
657
|
+
const runContext = {
|
|
658
|
+
threadId: this.threadId,
|
|
659
|
+
runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
660
|
+
clientTools: Array.from(this.clientToolsRef.current.values()).map(
|
|
661
|
+
(t) => ({
|
|
662
|
+
name: t.name,
|
|
663
|
+
description: t.description,
|
|
664
|
+
parameters: t.inputSchema
|
|
665
|
+
? convertSchemaToJsonSchema(t.inputSchema)
|
|
666
|
+
: { type: 'object' },
|
|
667
|
+
}),
|
|
668
|
+
),
|
|
669
|
+
forwardedProps: { ...mergedBody },
|
|
670
|
+
}
|
|
671
|
+
|
|
625
672
|
// Send through normalized connection (pushes chunks to subscription queue)
|
|
626
|
-
await this.connection.send(messages, mergedBody, signal)
|
|
673
|
+
await this.connection.send(messages, mergedBody, signal, runContext)
|
|
627
674
|
|
|
628
675
|
// Wait for subscription loop to finish processing all chunks
|
|
629
676
|
await processingComplete
|
|
@@ -982,7 +1029,9 @@ export class ChatClient {
|
|
|
982
1029
|
*/
|
|
983
1030
|
updateOptions(options: {
|
|
984
1031
|
connection?: ConnectionAdapter
|
|
1032
|
+
/** @deprecated Use `forwardedProps` instead. */
|
|
985
1033
|
body?: Record<string, any>
|
|
1034
|
+
forwardedProps?: Record<string, any>
|
|
986
1035
|
tools?: ReadonlyArray<AnyClientTool>
|
|
987
1036
|
onResponse?: (response?: Response) => void | Promise<void>
|
|
988
1037
|
onChunk?: (chunk: StreamChunk) => void
|
|
@@ -1018,8 +1067,14 @@ export class ChatClient {
|
|
|
1018
1067
|
this.subscribe()
|
|
1019
1068
|
}
|
|
1020
1069
|
}
|
|
1070
|
+
// Replace each slot independently so callers can update one without
|
|
1071
|
+
// wiping the other. (Passing `undefined` for either field is a "leave
|
|
1072
|
+
// unchanged" signal — to clear a slot, pass an empty object `{}`.)
|
|
1021
1073
|
if (options.body !== undefined) {
|
|
1022
|
-
this.
|
|
1074
|
+
this.bodyOption = options.body
|
|
1075
|
+
}
|
|
1076
|
+
if (options.forwardedProps !== undefined) {
|
|
1077
|
+
this.forwardedPropsOption = options.forwardedProps
|
|
1023
1078
|
}
|
|
1024
1079
|
if (options.tools !== undefined) {
|
|
1025
1080
|
this.clientToolsRef.current = new Map()
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { EventType } from '@tanstack/ai'
|
|
1
|
+
import { EventType, uiMessagesToWire } from '@tanstack/ai'
|
|
2
2
|
import type { ModelMessage, StreamChunk, UIMessage } from '@tanstack/ai'
|
|
3
3
|
|
|
4
|
+
function generateRunId(prefix: string): string {
|
|
5
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
/**
|
|
5
9
|
* Merge custom headers into request headers
|
|
6
10
|
*/
|
|
@@ -63,6 +67,25 @@ async function* readStreamLines(
|
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Per-send context provided by the chat client to the connection adapter.
|
|
72
|
+
* The adapter combines this with serialized messages to build a full
|
|
73
|
+
* AG-UI `RunAgentInput` payload.
|
|
74
|
+
*/
|
|
75
|
+
export interface RunAgentInputContext {
|
|
76
|
+
threadId: string
|
|
77
|
+
runId: string
|
|
78
|
+
parentRunId?: string
|
|
79
|
+
/** Client-declared tools to advertise in the request payload. */
|
|
80
|
+
clientTools?: Array<{
|
|
81
|
+
name: string
|
|
82
|
+
description: string
|
|
83
|
+
parameters: unknown
|
|
84
|
+
}>
|
|
85
|
+
/** Arbitrary user-controlled passthrough data. */
|
|
86
|
+
forwardedProps?: Record<string, unknown>
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
export interface ConnectConnectionAdapter {
|
|
67
90
|
/**
|
|
68
91
|
* Connect and return an async iterable of StreamChunks.
|
|
@@ -71,6 +94,7 @@ export interface ConnectConnectionAdapter {
|
|
|
71
94
|
messages: Array<UIMessage> | Array<ModelMessage>,
|
|
72
95
|
data?: Record<string, any>,
|
|
73
96
|
abortSignal?: AbortSignal,
|
|
97
|
+
runContext?: RunAgentInputContext,
|
|
74
98
|
) => AsyncIterable<StreamChunk>
|
|
75
99
|
}
|
|
76
100
|
|
|
@@ -86,6 +110,7 @@ export interface SubscribeConnectionAdapter {
|
|
|
86
110
|
messages: Array<UIMessage> | Array<ModelMessage>,
|
|
87
111
|
data?: Record<string, any>,
|
|
88
112
|
abortSignal?: AbortSignal,
|
|
113
|
+
runContext?: RunAgentInputContext,
|
|
89
114
|
) => Promise<void>
|
|
90
115
|
}
|
|
91
116
|
|
|
@@ -174,10 +199,15 @@ export function normalizeConnectionAdapter(
|
|
|
174
199
|
}
|
|
175
200
|
})()
|
|
176
201
|
},
|
|
177
|
-
async send(messages, data, abortSignal) {
|
|
202
|
+
async send(messages, data, abortSignal, runContext) {
|
|
178
203
|
let hasTerminalEvent = false
|
|
179
204
|
try {
|
|
180
|
-
const stream = connection.connect(
|
|
205
|
+
const stream = connection.connect(
|
|
206
|
+
messages,
|
|
207
|
+
data,
|
|
208
|
+
abortSignal,
|
|
209
|
+
runContext,
|
|
210
|
+
)
|
|
181
211
|
for await (const chunk of stream) {
|
|
182
212
|
if (chunk.type === 'RUN_FINISHED' || chunk.type === 'RUN_ERROR') {
|
|
183
213
|
hasTerminalEvent = true
|
|
@@ -187,11 +217,12 @@ export function normalizeConnectionAdapter(
|
|
|
187
217
|
|
|
188
218
|
// If the connect stream ended cleanly without a terminal event,
|
|
189
219
|
// synthesize RUN_FINISHED so request-scoped consumers can complete.
|
|
220
|
+
// Reuse the caller's threadId/runId so client-side activeRunIds tracking matches.
|
|
190
221
|
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
191
222
|
push({
|
|
192
223
|
type: EventType.RUN_FINISHED,
|
|
193
|
-
|
|
194
|
-
|
|
224
|
+
threadId: runContext?.threadId ?? `thread-${Date.now()}`,
|
|
225
|
+
runId: runContext?.runId ?? `run-${Date.now()}`,
|
|
195
226
|
model: 'connect-wrapper',
|
|
196
227
|
timestamp: Date.now(),
|
|
197
228
|
finishReason: 'stop',
|
|
@@ -201,6 +232,8 @@ export function normalizeConnectionAdapter(
|
|
|
201
232
|
if (!abortSignal?.aborted && !hasTerminalEvent) {
|
|
202
233
|
push({
|
|
203
234
|
type: EventType.RUN_ERROR,
|
|
235
|
+
threadId: runContext?.threadId ?? `thread-${Date.now()}`,
|
|
236
|
+
runId: runContext?.runId ?? `run-${Date.now()}`,
|
|
204
237
|
timestamp: Date.now(),
|
|
205
238
|
message:
|
|
206
239
|
err instanceof Error ? err.message : 'Unknown error in connect()',
|
|
@@ -270,7 +303,7 @@ export function fetchServerSentEvents(
|
|
|
270
303
|
| (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},
|
|
271
304
|
): ConnectConnectionAdapter {
|
|
272
305
|
return {
|
|
273
|
-
async *connect(messages, data, abortSignal) {
|
|
306
|
+
async *connect(messages, data, abortSignal, runContext) {
|
|
274
307
|
// Resolve URL and options if they are functions
|
|
275
308
|
const resolvedUrl = typeof url === 'function' ? url() : url
|
|
276
309
|
const resolvedOptions =
|
|
@@ -281,12 +314,40 @@ export function fetchServerSentEvents(
|
|
|
281
314
|
...mergeHeaders(resolvedOptions.headers),
|
|
282
315
|
}
|
|
283
316
|
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
// Build AG-UI RunAgentInput payload.
|
|
318
|
+
//
|
|
319
|
+
// Precedence (later spreads win): static adapter `body` is the base,
|
|
320
|
+
// overridden by `runContext.forwardedProps` (constructor body /
|
|
321
|
+
// forwardedProps options), overridden by per-message `data` passed
|
|
322
|
+
// to `connection.send`. Runtime values win over static config —
|
|
323
|
+
// this matches the documented "forwardedProps wins" semantic.
|
|
324
|
+
const wireMessages = uiMessagesToWire(messages as Array<UIMessage>)
|
|
325
|
+
const forwardedProps = {
|
|
289
326
|
...resolvedOptions.body,
|
|
327
|
+
...(runContext?.forwardedProps ?? {}),
|
|
328
|
+
...data,
|
|
329
|
+
}
|
|
330
|
+
const requestBody = {
|
|
331
|
+
threadId: runContext?.threadId ?? generateRunId('thread'),
|
|
332
|
+
runId: runContext?.runId ?? generateRunId('run'),
|
|
333
|
+
...(runContext?.parentRunId !== undefined && {
|
|
334
|
+
parentRunId: runContext.parentRunId,
|
|
335
|
+
}),
|
|
336
|
+
state: {},
|
|
337
|
+
messages: wireMessages,
|
|
338
|
+
tools: runContext?.clientTools ?? [],
|
|
339
|
+
context: [],
|
|
340
|
+
forwardedProps,
|
|
341
|
+
// Backward-compat mirror of `forwardedProps` under the legacy
|
|
342
|
+
// field name `data`. Server endpoints that have not migrated
|
|
343
|
+
// off the pre-AG-UI shape (`{ messages, data }`) keep working.
|
|
344
|
+
// AG-UI strict consumers strip this via `RunAgentInputSchema`
|
|
345
|
+
// (see `chatParamsFromRequestBody`). Will be removed when the
|
|
346
|
+
// legacy `body` client option is dropped.
|
|
347
|
+
// Shallow-cloned so that downstream mutation of `data` (e.g.
|
|
348
|
+
// by a logging interceptor or fetch wrapper) cannot corrupt
|
|
349
|
+
// `forwardedProps` and vice versa.
|
|
350
|
+
data: { ...forwardedProps },
|
|
290
351
|
}
|
|
291
352
|
|
|
292
353
|
const fetchClient = resolvedOptions.fetchClient ?? fetch
|
|
@@ -374,7 +435,7 @@ export function fetchHttpStream(
|
|
|
374
435
|
| (() => FetchConnectionOptions | Promise<FetchConnectionOptions>) = {},
|
|
375
436
|
): ConnectConnectionAdapter {
|
|
376
437
|
return {
|
|
377
|
-
async *connect(messages, data, abortSignal) {
|
|
438
|
+
async *connect(messages, data, abortSignal, runContext) {
|
|
378
439
|
// Resolve URL and options if they are functions
|
|
379
440
|
const resolvedUrl = typeof url === 'function' ? url() : url
|
|
380
441
|
const resolvedOptions =
|
|
@@ -385,12 +446,40 @@ export function fetchHttpStream(
|
|
|
385
446
|
...mergeHeaders(resolvedOptions.headers),
|
|
386
447
|
}
|
|
387
448
|
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
449
|
+
// Build AG-UI RunAgentInput payload.
|
|
450
|
+
//
|
|
451
|
+
// Precedence (later spreads win): static adapter `body` is the base,
|
|
452
|
+
// overridden by `runContext.forwardedProps` (constructor body /
|
|
453
|
+
// forwardedProps options), overridden by per-message `data` passed
|
|
454
|
+
// to `connection.send`. Runtime values win over static config —
|
|
455
|
+
// this matches the documented "forwardedProps wins" semantic.
|
|
456
|
+
const wireMessages = uiMessagesToWire(messages as Array<UIMessage>)
|
|
457
|
+
const forwardedProps = {
|
|
393
458
|
...resolvedOptions.body,
|
|
459
|
+
...(runContext?.forwardedProps ?? {}),
|
|
460
|
+
...data,
|
|
461
|
+
}
|
|
462
|
+
const requestBody = {
|
|
463
|
+
threadId: runContext?.threadId ?? generateRunId('thread'),
|
|
464
|
+
runId: runContext?.runId ?? generateRunId('run'),
|
|
465
|
+
...(runContext?.parentRunId !== undefined && {
|
|
466
|
+
parentRunId: runContext.parentRunId,
|
|
467
|
+
}),
|
|
468
|
+
state: {},
|
|
469
|
+
messages: wireMessages,
|
|
470
|
+
tools: runContext?.clientTools ?? [],
|
|
471
|
+
context: [],
|
|
472
|
+
forwardedProps,
|
|
473
|
+
// Backward-compat mirror of `forwardedProps` under the legacy
|
|
474
|
+
// field name `data`. Server endpoints that have not migrated
|
|
475
|
+
// off the pre-AG-UI shape (`{ messages, data }`) keep working.
|
|
476
|
+
// AG-UI strict consumers strip this via `RunAgentInputSchema`
|
|
477
|
+
// (see `chatParamsFromRequestBody`). Will be removed when the
|
|
478
|
+
// legacy `body` client option is dropped.
|
|
479
|
+
// Shallow-cloned so that downstream mutation of `data` (e.g.
|
|
480
|
+
// by a logging interceptor or fetch wrapper) cannot corrupt
|
|
481
|
+
// `forwardedProps` and vice versa.
|
|
482
|
+
data: { ...forwardedProps },
|
|
394
483
|
}
|
|
395
484
|
|
|
396
485
|
const fetchClient = resolvedOptions.fetchClient ?? fetch
|
|
@@ -447,7 +536,7 @@ export function stream(
|
|
|
447
536
|
) => AsyncIterable<StreamChunk>,
|
|
448
537
|
): ConnectConnectionAdapter {
|
|
449
538
|
return {
|
|
450
|
-
async *connect(messages, data) {
|
|
539
|
+
async *connect(messages, data, _abortSignal, _runContext) {
|
|
451
540
|
// Pass messages as-is (UIMessages with parts preserved)
|
|
452
541
|
// Server-side chat() handles conversion to ModelMessages
|
|
453
542
|
yield* streamFactory(messages, data)
|
|
@@ -478,7 +567,7 @@ export function rpcStream(
|
|
|
478
567
|
) => AsyncIterable<StreamChunk>,
|
|
479
568
|
): ConnectConnectionAdapter {
|
|
480
569
|
return {
|
|
481
|
-
async *connect(messages, data) {
|
|
570
|
+
async *connect(messages, data, _abortSignal, _runContext) {
|
|
482
571
|
// Pass messages as-is (UIMessages with parts preserved)
|
|
483
572
|
// Server-side chat() handles conversion to ModelMessages
|
|
484
573
|
yield* rpcCall(messages, data)
|
package/src/types.ts
CHANGED
|
@@ -205,7 +205,28 @@ export interface ChatClientOptions<
|
|
|
205
205
|
id?: string
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
|
-
*
|
|
208
|
+
* Thread ID to use for this chat session. Persists across sends within
|
|
209
|
+
* the session. If omitted, a unique thread ID is generated.
|
|
210
|
+
*/
|
|
211
|
+
threadId?: string
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Arbitrary client-controlled JSON forwarded to the server in the
|
|
215
|
+
* AG-UI `RunAgentInput.forwardedProps` field. Use this for per-session
|
|
216
|
+
* options like provider/model selection or feature flags that the
|
|
217
|
+
* server endpoint should read.
|
|
218
|
+
*
|
|
219
|
+
* Replaces the legacy `body` option. If both are provided,
|
|
220
|
+
* `forwardedProps` wins on key collision.
|
|
221
|
+
*/
|
|
222
|
+
forwardedProps?: Record<string, any>
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @deprecated Use `forwardedProps` instead. `body` continues to work
|
|
226
|
+
* unchanged — its values are merged into the AG-UI
|
|
227
|
+
* `RunAgentInput.forwardedProps` field on the wire and are also
|
|
228
|
+
* mirrored under the legacy `data` field for servers that have not
|
|
229
|
+
* migrated yet. Will be removed in a future major release.
|
|
209
230
|
*/
|
|
210
231
|
body?: Record<string, any>
|
|
211
232
|
|