@tanstack/ai-client 0.9.2 → 0.11.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/README.md +0 -4
- 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/index.d.ts +1 -1
- package/dist/esm/types.d.ts +34 -7
- package/dist/esm/types.js.map +1 -1
- package/package.json +4 -4
- package/src/chat-client.ts +69 -14
- package/src/connection-adapters.ts +108 -19
- package/src/index.ts +1 -0
- package/src/types.ts +44 -5
|
@@ -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":"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"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { ChatClient } from './chat-client.js';
|
|
|
2
2
|
export { RealtimeClient } from './realtime-client.js';
|
|
3
3
|
export { GenerationClient } from './generation-client.js';
|
|
4
4
|
export { VideoGenerationClient } from './video-generation-client.js';
|
|
5
|
-
export type { UIMessage, MessagePart, TextPart, ToolCallPart, ToolResultPart, ThinkingPart, ChatClientOptions, ChatRequestBody, InferChatMessages, ChatClientState, ConnectionStatus, MultimodalContent, } from './types.js';
|
|
5
|
+
export type { UIMessage, MessagePart, TextPart, ToolCallPart, ToolResultPart, ThinkingPart, StructuredOutputPart, ChatClientOptions, ChatRequestBody, InferChatMessages, ChatClientState, ConnectionStatus, MultimodalContent, } from './types.js';
|
|
6
6
|
export type { InferGenerationOutput, GenerationClientState, GenerationClientOptions, GenerationFetcher, GenerationFetcherOptions, GenerationTransport, VideoGenerationClientOptions, VideoStatusInfo, VideoGenerateResult, ImageGenerateInput, AudioGenerateInput, SpeechGenerateInput, TranscriptionGenerateInput, SummarizeGenerateInput, VideoGenerateInput, } from './generation-types.js';
|
|
7
7
|
export { GENERATION_EVENTS } from './generation-types.js';
|
|
8
8
|
export { clientTools, createChatClientOptions } from './types.js';
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { AnyClientTool, AudioPart, ChunkStrategy, ContentPart, DocumentPart, ImagePart, InferToolInput, InferToolOutput, ModelMessage, StreamChunk, VideoPart } from '@tanstack/ai';
|
|
1
|
+
import { AnyClientTool, AudioPart, ChunkStrategy, ContentPart, DocumentPart, ImagePart, InferToolInput, InferToolOutput, ModelMessage, StreamChunk, StructuredOutputPart, VideoPart } from '@tanstack/ai';
|
|
2
2
|
import { ConnectionAdapter } from './connection-adapters.js';
|
|
3
|
+
export type { StructuredOutputPart } from '@tanstack/ai';
|
|
3
4
|
/**
|
|
4
5
|
* Tool call states - track the lifecycle of a tool call
|
|
5
6
|
*/
|
|
@@ -116,15 +117,23 @@ export interface ThinkingPart {
|
|
|
116
117
|
type: 'thinking';
|
|
117
118
|
content: string;
|
|
118
119
|
}
|
|
119
|
-
export type MessagePart<TTools extends ReadonlyArray<AnyClientTool> = any> = TextPart | ImagePart | AudioPart | VideoPart | DocumentPart | ToolCallPart<TTools> | ToolResultPart | ThinkingPart
|
|
120
|
+
export type MessagePart<TTools extends ReadonlyArray<AnyClientTool> = any, TData = unknown> = TextPart | ImagePart | AudioPart | VideoPart | DocumentPart | ToolCallPart<TTools> | ToolResultPart | ThinkingPart | StructuredOutputPart<TData>;
|
|
120
121
|
/**
|
|
121
122
|
* UIMessage - Domain-specific message format optimized for building chat UIs
|
|
122
|
-
* Contains parts that can be text, tool calls, or tool results
|
|
123
|
+
* Contains parts that can be text, tool calls, or tool results.
|
|
124
|
+
*
|
|
125
|
+
* `TTools` narrows the tool-call/result part types based on the registered
|
|
126
|
+
* tools. `TData` is the schema-inferred type for any `structured-output` part
|
|
127
|
+
* on the message — defaulted to `unknown` so untyped consumers (the core
|
|
128
|
+
* stream processor, the wire converter) don't need to thread a schema generic
|
|
129
|
+
* everywhere; the hook layer (`useChat({ outputSchema })`) substitutes it on
|
|
130
|
+
* the public return so `m.parts.find(p => p.type === 'structured-output').data`
|
|
131
|
+
* is typed without manual casts.
|
|
123
132
|
*/
|
|
124
|
-
export interface UIMessage<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
133
|
+
export interface UIMessage<TTools extends ReadonlyArray<AnyClientTool> = any, TData = unknown> {
|
|
125
134
|
id: string;
|
|
126
135
|
role: 'system' | 'user' | 'assistant';
|
|
127
|
-
parts: Array<MessagePart<TTools>>;
|
|
136
|
+
parts: Array<MessagePart<TTools, TData>>;
|
|
128
137
|
createdAt?: Date;
|
|
129
138
|
}
|
|
130
139
|
export interface ChatClientOptions<TTools extends ReadonlyArray<AnyClientTool> = any> {
|
|
@@ -144,7 +153,26 @@ export interface ChatClientOptions<TTools extends ReadonlyArray<AnyClientTool> =
|
|
|
144
153
|
*/
|
|
145
154
|
id?: string;
|
|
146
155
|
/**
|
|
147
|
-
*
|
|
156
|
+
* Thread ID to use for this chat session. Persists across sends within
|
|
157
|
+
* the session. If omitted, a unique thread ID is generated.
|
|
158
|
+
*/
|
|
159
|
+
threadId?: string;
|
|
160
|
+
/**
|
|
161
|
+
* Arbitrary client-controlled JSON forwarded to the server in the
|
|
162
|
+
* AG-UI `RunAgentInput.forwardedProps` field. Use this for per-session
|
|
163
|
+
* options like provider/model selection or feature flags that the
|
|
164
|
+
* server endpoint should read.
|
|
165
|
+
*
|
|
166
|
+
* Replaces the legacy `body` option. If both are provided,
|
|
167
|
+
* `forwardedProps` wins on key collision.
|
|
168
|
+
*/
|
|
169
|
+
forwardedProps?: Record<string, any>;
|
|
170
|
+
/**
|
|
171
|
+
* @deprecated Use `forwardedProps` instead. `body` continues to work
|
|
172
|
+
* unchanged — its values are merged into the AG-UI
|
|
173
|
+
* `RunAgentInput.forwardedProps` field on the wire and are also
|
|
174
|
+
* mirrored under the legacy `data` field for servers that have not
|
|
175
|
+
* migrated yet. Will be removed in a future major release.
|
|
148
176
|
*/
|
|
149
177
|
body?: Record<string, any>;
|
|
150
178
|
/**
|
|
@@ -275,4 +303,3 @@ export declare function createChatClientOptions<const TTools extends ReadonlyArr
|
|
|
275
303
|
* ```
|
|
276
304
|
*/
|
|
277
305
|
export type InferChatMessages<T> = T extends ChatClientOptions<infer TTools> ? Array<UIMessage<TTools>> : never;
|
|
278
|
-
export {};
|
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 StructuredOutputPart,\n VideoPart,\n} from '@tanstack/ai'\nimport type { ConnectionAdapter } from './connection-adapters'\n\nexport type { StructuredOutputPart } from '@tanstack/ai'\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<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TData = unknown,\n> =\n | TextPart\n | ImagePart\n | AudioPart\n | VideoPart\n | DocumentPart\n | ToolCallPart<TTools>\n | ToolResultPart\n | ThinkingPart\n | StructuredOutputPart<TData>\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 *\n * `TTools` narrows the tool-call/result part types based on the registered\n * tools. `TData` is the schema-inferred type for any `structured-output` part\n * on the message — defaulted to `unknown` so untyped consumers (the core\n * stream processor, the wire converter) don't need to thread a schema generic\n * everywhere; the hook layer (`useChat({ outputSchema })`) substitutes it on\n * the public return so `m.parts.find(p => p.type === 'structured-output').data`\n * is typed without manual casts.\n */\nexport interface UIMessage<\n TTools extends ReadonlyArray<AnyClientTool> = any,\n TData = unknown,\n> {\n id: string\n role: 'system' | 'user' | 'assistant'\n parts: Array<MessagePart<TTools, TData>>\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":"AA0WO,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.11.0",
|
|
4
4
|
"description": "Framework-agnostic headless client for TanStack AI",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
"src"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@tanstack/ai": "0.
|
|
35
|
-
"@tanstack/ai-event-client": "0.3.
|
|
34
|
+
"@tanstack/ai": "0.19.0",
|
|
35
|
+
"@tanstack/ai-event-client": "0.3.3"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@standard-schema/spec": "^1.1.0",
|
|
39
39
|
"@vitest/coverage-v8": "4.0.14",
|
|
40
|
-
"vite": "^7.
|
|
40
|
+
"vite": "^7.3.3",
|
|
41
41
|
"zod": "^4.2.0"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
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)
|