@tanstack/ai-client 0.22.1 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/esm/audio-recorder.js +190 -213
- package/dist/esm/audio-recorder.js.map +1 -1
- package/dist/esm/chat-client.d.ts +172 -3
- package/dist/esm/chat-client.js +1656 -1386
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/cleared-stream-tracker.d.ts +23 -0
- package/dist/esm/cleared-stream-tracker.js +97 -0
- package/dist/esm/cleared-stream-tracker.js.map +1 -0
- package/dist/esm/client-persistor.d.ts +25 -12
- package/dist/esm/client-persistor.js +260 -235
- package/dist/esm/client-persistor.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +231 -10
- package/dist/esm/connection-adapters.js +989 -574
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/devtools-noop.d.ts +1 -0
- package/dist/esm/devtools-noop.js +79 -139
- package/dist/esm/devtools-noop.js.map +1 -1
- package/dist/esm/devtools.d.ts +31 -1
- package/dist/esm/devtools.js +977 -1127
- package/dist/esm/devtools.js.map +1 -1
- package/dist/esm/events.js +224 -226
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/generation-client.d.ts +145 -2
- package/dist/esm/generation-client.js +659 -321
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-reconstruct.d.ts +21 -0
- package/dist/esm/generation-reconstruct.js +85 -0
- package/dist/esm/generation-reconstruct.js.map +1 -0
- package/dist/esm/generation-types.d.ts +289 -3
- package/dist/esm/generation-types.js +356 -13
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/index.d.ts +9 -4
- package/dist/esm/index.js +7 -39
- package/dist/esm/interrupt-manager.d.ts +77 -0
- package/dist/esm/interrupt-manager.js +787 -0
- package/dist/esm/interrupt-manager.js.map +1 -0
- package/dist/esm/mcp-app-bridge.js +56 -64
- package/dist/esm/mcp-app-bridge.js.map +1 -1
- package/dist/esm/realtime-client.js +366 -440
- package/dist/esm/realtime-client.js.map +1 -1
- package/dist/esm/response-stream.js +19 -26
- package/dist/esm/response-stream.js.map +1 -1
- package/dist/esm/sse-parser.js +44 -47
- package/dist/esm/sse-parser.js.map +1 -1
- package/dist/esm/sse-utils.js +8 -9
- package/dist/esm/sse-utils.js.map +1 -1
- package/dist/esm/storage-adapters.d.ts +62 -0
- package/dist/esm/storage-adapters.js +174 -0
- package/dist/esm/storage-adapters.js.map +1 -0
- package/dist/esm/types.d.ts +212 -10
- package/dist/esm/types.js +38 -7
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +113 -2
- package/dist/esm/video-generation-client.js +665 -379
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +7 -7
- package/src/chat-client.ts +1079 -61
- package/src/cleared-stream-tracker.ts +151 -0
- package/src/client-persistor.ts +102 -33
- package/src/connection-adapters.ts +1185 -142
- package/src/devtools-noop.ts +4 -3
- package/src/devtools.ts +121 -3
- package/src/generation-client.ts +563 -13
- package/src/generation-reconstruct.ts +121 -0
- package/src/generation-types.ts +727 -3
- package/src/index.ts +56 -1
- package/src/interrupt-manager.ts +1440 -0
- package/src/storage-adapters.ts +242 -0
- package/src/types.ts +301 -9
- package/src/video-generation-client.ts +479 -13
- package/dist/esm/index.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-persistor.js","sources":["../../src/client-persistor.ts"],"sourcesContent":["import { getChunkRunId } from './connection-adapters'\nimport type { StreamChunk } from '@tanstack/ai/client'\nimport type { ChatClientPersistence, UIMessage } from './types'\n\n// `StreamChunk` is a discriminated union; `toolCallId` / `messageId` /\n// `parentMessageId` exist on only some members. Narrow with `in` (matching\n// `getChunkRunId`) instead of asserting a shape, so the field's real type is\n// preserved and a protocol rename can't be read past silently.\nfunction getChunkToolCallId(chunk: StreamChunk): string | undefined {\n return 'toolCallId' in chunk && typeof chunk.toolCallId === 'string'\n ? chunk.toolCallId\n : undefined\n}\n\nfunction getChunkMessageId(chunk: StreamChunk): string | undefined {\n return 'messageId' in chunk && typeof chunk.messageId === 'string'\n ? chunk.messageId\n : undefined\n}\n\nfunction getChunkParentMessageId(chunk: StreamChunk): string | undefined {\n return 'parentMessageId' in chunk && typeof chunk.parentMessageId === 'string'\n ? chunk.parentMessageId\n : undefined\n}\n\n/**\n * Encapsulates everything persistence-related for `ChatClient` so the client\n * itself stays focused on streaming and message state.\n *\n * Two responsibilities live here:\n *\n * 1. **Storage orchestration** — hydrate from `getItem(id)` on creation, save to\n * `setItem(id, messages)` on every change through an ordered write queue, and\n * `removeItem(id)` on clear. A generation counter discards stale writes when a\n * removal or a newer conversation supersedes an in-flight async operation.\n * 2. **Clear-during-stream suppression** — when a conversation is cleared while a\n * stream is still producing, late chunks for the cleared run(s) must not\n * repopulate the now-empty state. The persistor tracks the cleared ids and\n * decides, per chunk, whether the client should ignore it.\n *\n * All adapter calls are best-effort: a throwing or rejecting adapter is swallowed\n * so storage problems never break the chat.\n */\nexport class ChatPersistor {\n // --- storage queue state ---\n private skipNextPersist = false\n private generation = 0\n private queue: Promise<void> = Promise.resolve()\n private queuePending = false\n // Bumped on every message change; lets an in-flight async hydration detect\n // that the message list moved on and avoid clobbering it.\n private messagesGeneration = 0\n\n // --- clear-during-stream suppression state ---\n private readonly clearedMessageIds = new Set<string>()\n private readonly clearedRunIds = new Set<string>()\n private readonly ignoredActiveRunIds = new Set<string>()\n private readonly clearedToolCallIds = new Set<string>()\n private currentRunlessRunId: string | null = null\n\n constructor(\n private readonly adapter: ChatClientPersistence,\n private readonly id: string,\n private readonly applyMessages: (messages: Array<UIMessage>) => void,\n ) {}\n\n // ---------------------------------------------------------------------------\n // Storage orchestration\n // ---------------------------------------------------------------------------\n\n /**\n * Synchronously read the persisted messages for constructor-time hydration.\n * Returns the raw `getItem` result (which may be a promise for async stores).\n */\n readInitial():\n | Array<UIMessage>\n | null\n | undefined\n | Promise<Array<UIMessage> | null | undefined> {\n try {\n return this.adapter.getItem(this.id)\n } catch {\n return undefined\n }\n }\n\n /**\n * Apply messages from an async `getItem` once it resolves, unless the message\n * list has already changed since hydration began.\n */\n hydrateAsync(\n persistedMessages:\n | Array<UIMessage>\n | null\n | undefined\n | Promise<Array<UIMessage> | null | undefined>,\n ): void {\n if (!(persistedMessages instanceof Promise)) {\n return\n }\n\n const hydrationGeneration = this.messagesGeneration\n persistedMessages\n .then((messages) => {\n if (\n Array.isArray(messages) &&\n this.messagesGeneration === hydrationGeneration\n ) {\n this.applyMessages(messages)\n }\n })\n .catch(() => {\n // Persistence adapters are best-effort and must not break chat setup.\n })\n }\n\n /**\n * Record a message-list change and queue a `setItem` write for it. Skips a\n * single write after {@link beginClear} so the clear's empty snapshot isn't\n * persisted between `clearMessages()` and {@link remove}.\n */\n notifyMessagesChanged(messages: Array<UIMessage>): void {\n this.messagesGeneration++\n if (this.skipNextPersist) {\n this.skipNextPersist = false\n return\n }\n const generation = this.generation\n const messagesSnapshot = [...messages]\n this.runOperation(() => {\n if (generation !== this.generation) {\n return\n }\n return this.adapter.setItem(this.id, messagesSnapshot)\n })\n }\n\n /** Remove the persisted conversation. Invalidates any queued writes. */\n remove(): void {\n const generation = ++this.generation\n this.runOperation(() => {\n if (generation !== this.generation) {\n return\n }\n return this.adapter.removeItem(this.id)\n })\n }\n\n private runOperation(operation: () => void | Promise<void>): void {\n if (this.queuePending) {\n const queued = this.queue.then(operation).catch(() => {\n // Persistence adapters are best-effort and must not break chat updates.\n })\n this.queue = queued\n void queued.finally(() => {\n if (this.queue === queued) {\n this.queuePending = false\n }\n })\n return\n }\n\n try {\n const result = operation()\n if (result instanceof Promise) {\n this.queuePending = true\n const queued = result.catch(() => {\n // Persistence adapters are best-effort and must not break chat updates.\n })\n this.queue = queued\n void queued.finally(() => {\n if (this.queue === queued) {\n this.queuePending = false\n }\n })\n }\n } catch {\n // Persistence adapters are best-effort and must not break chat updates.\n }\n }\n\n // ---------------------------------------------------------------------------\n // Clear-during-stream suppression\n // ---------------------------------------------------------------------------\n\n /**\n * Capture the message/run ids that exist at the moment of a clear so chunks\n * still arriving for them can be ignored.\n */\n snapshotClear(context: {\n messages: Array<UIMessage>\n activeRunIds: Set<string>\n currentRunId: string | null\n }): void {\n for (const message of context.messages) {\n this.clearedMessageIds.add(message.id)\n }\n for (const runId of context.activeRunIds) {\n this.clearedRunIds.add(runId)\n this.ignoredActiveRunIds.add(runId)\n }\n if (context.currentRunId) {\n this.clearedRunIds.add(context.currentRunId)\n this.ignoredActiveRunIds.add(context.currentRunId)\n }\n }\n\n /** Mark that the next persisted message change (the clear itself) is skipped. */\n beginClear(): void {\n this.skipNextPersist = true\n }\n\n /** Whether a chunk belongs to cleared state and should not be processed. */\n shouldIgnoreChunk(chunk: StreamChunk): boolean {\n const runId = getChunkRunId(chunk)\n if (runId && this.clearedRunIds.has(runId)) {\n if (chunk.type === 'RUN_STARTED') {\n this.ignoredActiveRunIds.add(runId)\n this.currentRunlessRunId = runId\n }\n this.markIgnoredChunkIds(chunk)\n return true\n }\n\n if (runId && this.ignoredActiveRunIds.has(runId)) {\n this.markIgnoredChunkIds(chunk)\n return true\n }\n\n if (this.isRunlessChunkFromIgnoredRun(chunk)) {\n this.markIgnoredChunkIds(chunk)\n return true\n }\n\n const toolCallId = getChunkToolCallId(chunk)\n if (toolCallId && this.clearedToolCallIds.has(toolCallId)) {\n return true\n }\n\n const parentMessageId = getChunkParentMessageId(chunk)\n if (parentMessageId && this.clearedMessageIds.has(parentMessageId)) {\n if (toolCallId) {\n this.clearedToolCallIds.add(toolCallId)\n }\n return true\n }\n\n const messageId = getChunkMessageId(chunk)\n if (!messageId) {\n return false\n }\n if (this.clearedMessageIds.has(messageId)) {\n return true\n }\n\n return false\n }\n\n /**\n * The owning client calls this when a run starts so runless content chunks\n * (adapters that omit `runId` on content events) can be attributed to it.\n */\n onRunStarted(runId: string): void {\n this.currentRunlessRunId = runId\n }\n\n /** Forget a settled run, advancing the runless pointer to another ignored run. */\n onRunSettled(runId: string): void {\n this.ignoredActiveRunIds.delete(runId)\n this.clearedRunIds.delete(runId)\n if (this.currentRunlessRunId === runId) {\n this.currentRunlessRunId =\n this.ignoredActiveRunIds.values().next().value ?? null\n }\n }\n\n /** A session-level (runId-less) RUN_ERROR clears all ignored-run tracking. */\n onSessionRunError(): void {\n this.ignoredActiveRunIds.clear()\n this.currentRunlessRunId = null\n }\n\n /** Clear the ignored-active-run markers (mirrors a session-generating reset). */\n resetIgnored(): void {\n this.ignoredActiveRunIds.clear()\n }\n\n /**\n * Consume the current runless run id (if any), forgetting it. Used when an\n * ignored, runId-less RUN_ERROR drains the run the client is still tracking.\n */\n takeRunlessRunId(): string | null {\n const runId = this.currentRunlessRunId\n if (!runId) return null\n this.ignoredActiveRunIds.delete(runId)\n this.clearedRunIds.delete(runId)\n // Advance to another still-ignored run (mirroring `onRunSettled`) so that\n // when two cleared runs drain concurrently, draining one via a runId-less\n // RUN_ERROR doesn't stop suppressing the other's runless content.\n this.currentRunlessRunId =\n this.ignoredActiveRunIds.values().next().value ?? null\n return runId\n }\n\n private markIgnoredChunkIds(chunk: StreamChunk): void {\n const messageId = getChunkMessageId(chunk)\n if (messageId) {\n this.clearedMessageIds.add(messageId)\n }\n const toolCallId = getChunkToolCallId(chunk)\n if (toolCallId) {\n this.clearedToolCallIds.add(toolCallId)\n }\n }\n\n private isRunlessChunkFromIgnoredRun(chunk: StreamChunk): boolean {\n const runId = getChunkRunId(chunk)\n if (runId || !this.currentRunlessRunId) return false\n if (\n !this.ignoredActiveRunIds.has(this.currentRunlessRunId) &&\n !this.clearedRunIds.has(this.currentRunlessRunId)\n ) {\n return false\n }\n return (\n chunk.type === 'TEXT_MESSAGE_START' ||\n chunk.type === 'TEXT_MESSAGE_CONTENT' ||\n chunk.type === 'TOOL_CALL_START' ||\n chunk.type === 'TOOL_CALL_ARGS' ||\n chunk.type === 'TOOL_CALL_END' ||\n chunk.type === 'TOOL_CALL_RESULT' ||\n chunk.type === 'MESSAGES_SNAPSHOT' ||\n chunk.type === 'RUN_ERROR'\n )\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client-persistor.js","names":[],"sources":["../../src/client-persistor.ts"],"sourcesContent":["import { getChunkRunId } from './connection-adapters'\nimport type { StreamChunk } from '@tanstack/ai/client'\nimport type {\n ChatClientPersistence,\n ChatPersistedState,\n ChatResumeSnapshot,\n UIMessage,\n} from './types'\n\n/** Normalize a raw `getItem` result (legacy bare array or combined record). */\nfunction normalizePersistedState(\n raw: ChatPersistedState | Array<UIMessage> | null | undefined,\n): ChatPersistedState | undefined {\n if (Array.isArray(raw)) return { messages: raw }\n if (raw && Array.isArray(raw.messages)) return raw\n return undefined\n}\n\n// `StreamChunk` is a discriminated union; `toolCallId` / `messageId` /\n// `parentMessageId` exist on only some members. Narrow with `in` (matching\n// `getChunkRunId`) instead of asserting a shape, so the field's real type is\n// preserved and a protocol rename can't be read past silently.\nfunction getChunkToolCallId(chunk: StreamChunk): string | undefined {\n return 'toolCallId' in chunk && typeof chunk.toolCallId === 'string'\n ? chunk.toolCallId\n : undefined\n}\n\nfunction getChunkMessageId(chunk: StreamChunk): string | undefined {\n return 'messageId' in chunk && typeof chunk.messageId === 'string'\n ? chunk.messageId\n : undefined\n}\n\nfunction getChunkParentMessageId(chunk: StreamChunk): string | undefined {\n return 'parentMessageId' in chunk && typeof chunk.parentMessageId === 'string'\n ? chunk.parentMessageId\n : undefined\n}\n\n/**\n * Encapsulates everything persistence-related for `ChatClient` so the client\n * itself stays focused on streaming and message state.\n *\n * Two responsibilities live here:\n *\n * 1. **Storage orchestration** — hydrate from `getItem(id)` on creation, save a\n * combined `{ messages, resume? }` record via `setItem` on every change\n * through an ordered write queue, and `removeItem(id)` on clear (or when\n * both the transcript and resume pointer are empty). A generation counter\n * discards stale writes when a removal or a newer conversation supersedes\n * an in-flight async operation.\n * 2. **Clear-during-stream suppression** — when a conversation is cleared while a\n * stream is still producing, late chunks for the cleared run(s) must not\n * repopulate the now-empty state. The persistor tracks the cleared ids and\n * decides, per chunk, whether the client should ignore it.\n *\n * All adapter calls are best-effort: a throwing or rejecting adapter is swallowed\n * so storage problems never break the chat.\n */\nexport class ChatPersistor {\n // --- storage queue state ---\n private skipNextPersist = false\n private generation = 0\n private queue: Promise<void> = Promise.resolve()\n private queuePending = false\n // Bumped on every message change; lets an in-flight async hydration detect\n // that the message list moved on and avoid clobbering it.\n private messagesGeneration = 0\n // Latest messages + resume snapshot, written together as one combined record\n // so a full page reload restores both from a single adapter key.\n private lastMessages: Array<UIMessage> = []\n private lastResume: ChatResumeSnapshot | null = null\n\n // --- clear-during-stream suppression state ---\n private readonly clearedMessageIds = new Set<string>()\n private readonly clearedRunIds = new Set<string>()\n private readonly ignoredActiveRunIds = new Set<string>()\n private readonly clearedToolCallIds = new Set<string>()\n private currentRunlessRunId: string | null = null\n\n constructor(\n private readonly adapter: ChatClientPersistence,\n private readonly id: string,\n private readonly applyMessages: (messages: Array<UIMessage>) => void,\n private readonly applyResume?: (snapshot: ChatResumeSnapshot) => void,\n ) {}\n\n /** Persist the current state as one combined `{ messages, resume? }` record. */\n private writeState(): void {\n const messages = [...this.lastMessages]\n // Nothing to persist (no transcript, no resume pointer): remove the key\n // rather than writing an empty `{ messages: [] }`, so a cleared\n // conversation does not leave a stale record behind.\n if (messages.length === 0 && !this.lastResume) {\n const generation = this.generation\n this.runOperation(() => {\n if (generation !== this.generation) {\n return\n }\n return this.adapter.removeItem(this.id)\n })\n return\n }\n const generation = this.generation\n const state: ChatPersistedState = {\n messages,\n ...(this.lastResume ? { resume: this.lastResume } : {}),\n }\n this.runOperation(() => {\n if (generation !== this.generation) {\n return\n }\n return this.adapter.setItem(this.id, state)\n })\n }\n\n // ---------------------------------------------------------------------------\n // Storage orchestration\n // ---------------------------------------------------------------------------\n\n /**\n * Synchronously read the persisted state for constructor-time hydration.\n * Returns the normalized combined record, or a promise of it for async stores.\n */\n readInitial():\n | ChatPersistedState\n | undefined\n | Promise<ChatPersistedState | undefined> {\n try {\n const raw = this.adapter.getItem(this.id)\n if (raw instanceof Promise) {\n return raw.then(normalizePersistedState).catch(() => undefined)\n }\n const state = normalizePersistedState(raw)\n if (state) {\n this.lastMessages = state.messages\n this.lastResume = state.resume ?? null\n }\n return state\n } catch {\n return undefined\n }\n }\n\n /**\n * Apply state from an async `getItem` once it resolves, unless the message\n * list has already changed since hydration began.\n */\n hydrateAsync(\n persistedState:\n | ChatPersistedState\n | undefined\n | Promise<ChatPersistedState | undefined>,\n ): void {\n if (!(persistedState instanceof Promise)) {\n return\n }\n\n const hydrationGeneration = this.messagesGeneration\n persistedState\n .then((state) => {\n if (!state || this.messagesGeneration !== hydrationGeneration) {\n return\n }\n this.lastResume = state.resume ?? null\n this.lastMessages = state.messages\n this.applyMessages(state.messages)\n if (state.resume && this.applyResume) {\n this.applyResume(state.resume)\n }\n })\n .catch(() => {\n // Persistence adapters are best-effort and must not break chat setup.\n })\n }\n\n /**\n * Record a message-list change and queue a combined write for it. Skips a\n * single write after {@link beginClear} so the clear's empty snapshot isn't\n * persisted between `clearMessages()` and {@link remove}.\n */\n notifyMessagesChanged(messages: Array<UIMessage>): void {\n this.messagesGeneration++\n this.lastMessages = [...messages]\n if (this.skipNextPersist) {\n this.skipNextPersist = false\n return\n }\n this.writeState()\n }\n\n /**\n * Record the current resume snapshot (which run to rejoin / which interrupts\n * are pending) and persist it alongside the messages. Pass `null` to clear it\n * once the run reaches a non-interrupt terminal.\n */\n persistResumeSnapshot(snapshot: ChatResumeSnapshot | null): void {\n this.lastResume = snapshot\n if (this.skipNextPersist) {\n return\n }\n this.writeState()\n }\n\n /** Remove the persisted conversation. Invalidates any queued writes. */\n remove(): void {\n this.lastMessages = []\n this.lastResume = null\n const generation = ++this.generation\n this.runOperation(() => {\n if (generation !== this.generation) {\n return\n }\n return this.adapter.removeItem(this.id)\n })\n }\n\n private runOperation(operation: () => void | Promise<void>): void {\n if (this.queuePending) {\n const queued = this.queue.then(operation).catch(() => {\n // Persistence adapters are best-effort and must not break chat updates.\n })\n this.queue = queued\n void queued.finally(() => {\n if (this.queue === queued) {\n this.queuePending = false\n }\n })\n return\n }\n\n try {\n const result = operation()\n if (result instanceof Promise) {\n this.queuePending = true\n const queued = result.catch(() => {\n // Persistence adapters are best-effort and must not break chat updates.\n })\n this.queue = queued\n void queued.finally(() => {\n if (this.queue === queued) {\n this.queuePending = false\n }\n })\n }\n } catch {\n // Persistence adapters are best-effort and must not break chat updates.\n }\n }\n\n // ---------------------------------------------------------------------------\n // Clear-during-stream suppression\n // ---------------------------------------------------------------------------\n\n /**\n * Capture the message/run ids that exist at the moment of a clear so chunks\n * still arriving for them can be ignored.\n */\n snapshotClear(context: {\n messages: Array<UIMessage>\n activeRunIds: Set<string>\n currentRunId: string | null\n }): void {\n for (const message of context.messages) {\n this.clearedMessageIds.add(message.id)\n }\n for (const runId of context.activeRunIds) {\n this.clearedRunIds.add(runId)\n this.ignoredActiveRunIds.add(runId)\n }\n if (context.currentRunId) {\n this.clearedRunIds.add(context.currentRunId)\n this.ignoredActiveRunIds.add(context.currentRunId)\n }\n }\n\n /** Mark that the next persisted message change (the clear itself) is skipped. */\n beginClear(): void {\n this.skipNextPersist = true\n }\n\n /** Whether a chunk belongs to cleared state and should not be processed. */\n shouldIgnoreChunk(chunk: StreamChunk): boolean {\n const runId = getChunkRunId(chunk)\n if (runId && this.clearedRunIds.has(runId)) {\n if (chunk.type === 'RUN_STARTED') {\n this.ignoredActiveRunIds.add(runId)\n this.currentRunlessRunId = runId\n }\n this.markIgnoredChunkIds(chunk)\n return true\n }\n\n if (runId && this.ignoredActiveRunIds.has(runId)) {\n this.markIgnoredChunkIds(chunk)\n return true\n }\n\n if (this.isRunlessChunkFromIgnoredRun(chunk)) {\n this.markIgnoredChunkIds(chunk)\n return true\n }\n\n const toolCallId = getChunkToolCallId(chunk)\n if (toolCallId && this.clearedToolCallIds.has(toolCallId)) {\n return true\n }\n\n const parentMessageId = getChunkParentMessageId(chunk)\n if (parentMessageId && this.clearedMessageIds.has(parentMessageId)) {\n if (toolCallId) {\n this.clearedToolCallIds.add(toolCallId)\n }\n return true\n }\n\n const messageId = getChunkMessageId(chunk)\n if (!messageId) {\n return false\n }\n if (this.clearedMessageIds.has(messageId)) {\n return true\n }\n\n return false\n }\n\n /**\n * The owning client calls this when a run starts so runless content chunks\n * (adapters that omit `runId` on content events) can be attributed to it.\n */\n onRunStarted(runId: string): void {\n this.currentRunlessRunId = runId\n }\n\n /** Forget a settled run, advancing the runless pointer to another ignored run. */\n onRunSettled(runId: string): void {\n this.ignoredActiveRunIds.delete(runId)\n this.clearedRunIds.delete(runId)\n if (this.currentRunlessRunId === runId) {\n this.currentRunlessRunId =\n this.ignoredActiveRunIds.values().next().value ?? null\n }\n }\n\n /** A session-level (runId-less) RUN_ERROR clears all ignored-run tracking. */\n onSessionRunError(): void {\n this.ignoredActiveRunIds.clear()\n this.currentRunlessRunId = null\n }\n\n /** Clear the ignored-active-run markers (mirrors a session-generating reset). */\n resetIgnored(): void {\n this.ignoredActiveRunIds.clear()\n }\n\n /**\n * Consume the current runless run id (if any), forgetting it. Used when an\n * ignored, runId-less RUN_ERROR drains the run the client is still tracking.\n */\n takeRunlessRunId(): string | null {\n const runId = this.currentRunlessRunId\n if (!runId) return null\n this.ignoredActiveRunIds.delete(runId)\n this.clearedRunIds.delete(runId)\n // Advance to another still-ignored run (mirroring `onRunSettled`) so that\n // when two cleared runs drain concurrently, draining one via a runId-less\n // RUN_ERROR doesn't stop suppressing the other's runless content.\n this.currentRunlessRunId =\n this.ignoredActiveRunIds.values().next().value ?? null\n return runId\n }\n\n private markIgnoredChunkIds(chunk: StreamChunk): void {\n const messageId = getChunkMessageId(chunk)\n if (messageId) {\n this.clearedMessageIds.add(messageId)\n }\n const toolCallId = getChunkToolCallId(chunk)\n if (toolCallId) {\n this.clearedToolCallIds.add(toolCallId)\n }\n }\n\n private isRunlessChunkFromIgnoredRun(chunk: StreamChunk): boolean {\n const runId = getChunkRunId(chunk)\n if (runId || !this.currentRunlessRunId) return false\n if (\n !this.ignoredActiveRunIds.has(this.currentRunlessRunId) &&\n !this.clearedRunIds.has(this.currentRunlessRunId)\n ) {\n return false\n }\n return (\n chunk.type === 'TEXT_MESSAGE_START' ||\n chunk.type === 'TEXT_MESSAGE_CONTENT' ||\n chunk.type === 'TOOL_CALL_START' ||\n chunk.type === 'TOOL_CALL_ARGS' ||\n chunk.type === 'TOOL_CALL_END' ||\n chunk.type === 'TOOL_CALL_RESULT' ||\n chunk.type === 'MESSAGES_SNAPSHOT' ||\n chunk.type === 'RUN_ERROR'\n )\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ModelMessage, StreamChunk, UIMessage } from '@tanstack/ai/client';
|
|
2
|
-
import { ChatFetcher } from './types.js';
|
|
1
|
+
import { ModelMessage, RunAgentResumeItem, StreamChunk, UIMessage } from '@tanstack/ai/client';
|
|
2
|
+
import { ChatFetcher, ChatPendingInterrupt } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Resolve a chunk's run id, preferring the value on the chunk itself
|
|
5
5
|
* (RUN_STARTED / RUN_FINISHED / RUN_ERROR carry one) and falling back to the
|
|
@@ -14,6 +14,41 @@ export declare function getChunkRunId(chunk: StreamChunk): string | undefined;
|
|
|
14
14
|
export declare class StreamTruncatedError extends Error {
|
|
15
15
|
constructor();
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Thrown when a durable (id-tagged) run's stream ends with no terminal event
|
|
19
|
+
* and a reconnect makes no forward progress — the run cannot complete, so the
|
|
20
|
+
* consumer must not be left silently hanging on a stream that just stops.
|
|
21
|
+
*/
|
|
22
|
+
export declare class DurableStreamIncompleteError extends Error {
|
|
23
|
+
constructor();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Thrown when a durable run exceeds its reconnect ceiling. Bounds the
|
|
27
|
+
* otherwise-unbounded reconnect loop so a flapping producer (or a proxy that
|
|
28
|
+
* rolls the socket after every event) surfaces a failure instead of
|
|
29
|
+
* reconnecting without end.
|
|
30
|
+
*/
|
|
31
|
+
export declare class StreamReconnectLimitError extends Error {
|
|
32
|
+
constructor(attempts: number);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Reconnect bounding for resumable streams. A constant throttle delay prevents a
|
|
36
|
+
* hot loop against the origin, and the ceiling bounds a pathologically failing
|
|
37
|
+
* run — but only counts CONSECUTIVE reconnects that made no forward progress.
|
|
38
|
+
*/
|
|
39
|
+
export interface ReconnectOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Ceiling on the number of CONSECUTIVE reconnects that deliver no new events,
|
|
42
|
+
* before failing with {@link StreamReconnectLimitError}. The counter resets to
|
|
43
|
+
* zero whenever a reconnect makes forward progress, so a healthy long run —
|
|
44
|
+
* even one behind a proxy that rolls the socket after every event — never
|
|
45
|
+
* approaches it; the ceiling only fires when the run is genuinely stuck
|
|
46
|
+
* (reconnecting repeatedly without receiving anything new). Default 5.
|
|
47
|
+
*/
|
|
48
|
+
maxAttempts?: number;
|
|
49
|
+
/** Delay between reconnect attempts, in ms, to avoid hammering. Default 250. */
|
|
50
|
+
delayMs?: number;
|
|
51
|
+
}
|
|
17
52
|
/**
|
|
18
53
|
* Per-send context provided by the chat client to the connection adapter.
|
|
19
54
|
* The adapter combines this with serialized messages to build a full
|
|
@@ -23,6 +58,8 @@ export interface RunAgentInputContext {
|
|
|
23
58
|
threadId: string;
|
|
24
59
|
runId: string;
|
|
25
60
|
parentRunId?: string;
|
|
61
|
+
/** AG-UI interrupt resume entries returned to the server on a follow-up run. */
|
|
62
|
+
resume?: Array<RunAgentResumeItem>;
|
|
26
63
|
/** Client-declared tools to advertise in the request payload. */
|
|
27
64
|
clientTools?: Array<{
|
|
28
65
|
name: string;
|
|
@@ -37,6 +74,110 @@ export interface ConnectConnectionAdapter {
|
|
|
37
74
|
* Connect and return an async iterable of StreamChunks.
|
|
38
75
|
*/
|
|
39
76
|
connect: (messages: Array<UIMessage> | Array<ModelMessage>, data?: Record<string, any>, abortSignal?: AbortSignal, runContext?: RunAgentInputContext) => AsyncIterable<StreamChunk>;
|
|
77
|
+
/**
|
|
78
|
+
* Fetch server-driven hydration for a generation `threadId`: the last
|
|
79
|
+
* generation's resume snapshot, plus a cursor to a run still generating if
|
|
80
|
+
* one exists. The generation client calls this itself on mount when
|
|
81
|
+
* `persistence: true` (no loader/prop) and repaints the snapshot — it never
|
|
82
|
+
* auto-starts a run. Read-only JSON GET (`?threadId`), so it is
|
|
83
|
+
* transport-agnostic. Optional and feature-detected exactly like the chat
|
|
84
|
+
* `hydrate` handler.
|
|
85
|
+
*/
|
|
86
|
+
hydrateGeneration?: (threadId: string) => Promise<GenerationHydrationResult>;
|
|
87
|
+
/**
|
|
88
|
+
* Re-attach to a run that is still generating and replay it from the start
|
|
89
|
+
* (read-only `?offset=-1&runId` against the delivery-durability log). The
|
|
90
|
+
* generation client tails this on mount when hydration reports a run still in
|
|
91
|
+
* flight, so a dropped connection or a full reload finishes the generation in
|
|
92
|
+
* place — the same durability replay the chat client uses. Optional and
|
|
93
|
+
* feature-detected; present on `fetchServerSentEvents` / `fetchHttpStream`.
|
|
94
|
+
*/
|
|
95
|
+
joinRun?: (runId: string, abortSignal?: AbortSignal) => AsyncIterable<StreamChunk>;
|
|
96
|
+
/**
|
|
97
|
+
* Fetch server-driven hydration for a chat `threadId`: the stored transcript
|
|
98
|
+
* plus a cursor to an in-flight run and any pending interrupts. The chat
|
|
99
|
+
* client calls this itself on mount when `persistence: true` (no loader/prop)
|
|
100
|
+
* and repaints it — it never auto-sends. Read-only JSON GET (`?threadId`), so
|
|
101
|
+
* it is transport-agnostic. Optional and feature-detected; present on
|
|
102
|
+
* `fetchServerSentEvents` / `fetchHttpStream`, and on `stream()` /
|
|
103
|
+
* `rpcStream()` when supplied via {@link StreamConnectionHandlers}.
|
|
104
|
+
*/
|
|
105
|
+
hydrate?: (threadId: string) => Promise<ChatHydrationResult>;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Server-resolved hydration for a generation thread. `resumeSnapshot` is the
|
|
109
|
+
* last generation's lightweight snapshot (validated client-side before it is
|
|
110
|
+
* adopted); `activeRun` is a cursor to a run still generating for the thread
|
|
111
|
+
* (or `null`).
|
|
112
|
+
*
|
|
113
|
+
* Field-for-field compatible with `@tanstack/ai-persistence`'s
|
|
114
|
+
* `ReconstructedGeneration` (the body `reconstructGeneration` returns) — the
|
|
115
|
+
* client never imports that package, so this is a structural contract, not a
|
|
116
|
+
* shared type. Two deliberate widenings on this side: `schemaVersion` is
|
|
117
|
+
* optional (the server always writes `1`, but a hand-written fixture need not),
|
|
118
|
+
* and `status` also admits `'idle'`, which the server's mapper never emits.
|
|
119
|
+
* Only a client-local snapshot reaches it, when `stop()` retires a cancelled
|
|
120
|
+
* run.
|
|
121
|
+
*/
|
|
122
|
+
export interface GenerationHydrationResult {
|
|
123
|
+
resumeSnapshot: {
|
|
124
|
+
schemaVersion?: 1;
|
|
125
|
+
resumeState: {
|
|
126
|
+
threadId: string;
|
|
127
|
+
runId: string;
|
|
128
|
+
} | null;
|
|
129
|
+
status: 'idle' | 'running' | 'complete' | 'error';
|
|
130
|
+
result?: unknown;
|
|
131
|
+
error?: {
|
|
132
|
+
message: string;
|
|
133
|
+
code?: string;
|
|
134
|
+
};
|
|
135
|
+
activity?: string;
|
|
136
|
+
} | null;
|
|
137
|
+
activeRun: {
|
|
138
|
+
runId: string;
|
|
139
|
+
} | null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Server-resolved hydration for a thread. `messages` is the stored transcript;
|
|
143
|
+
* `activeRun` is a cursor to a run still generating for the thread (or `null`).
|
|
144
|
+
* Keyed on the STABLE thread id — the client never handles a run id, so a turn
|
|
145
|
+
* that spans several runs (interrupt/tool continuations) reconnects correctly.
|
|
146
|
+
*/
|
|
147
|
+
export interface ChatHydrationResult {
|
|
148
|
+
messages: Array<UIMessage>;
|
|
149
|
+
activeRun: {
|
|
150
|
+
runId: string;
|
|
151
|
+
} | null;
|
|
152
|
+
/**
|
|
153
|
+
* Pending human-in-the-loop interrupts for the thread and the run they paused,
|
|
154
|
+
* so a reload (or another device) re-prompts the approval from the server. The
|
|
155
|
+
* client restores them exactly as a persisted resume snapshot would.
|
|
156
|
+
*/
|
|
157
|
+
interrupts: {
|
|
158
|
+
runId: string;
|
|
159
|
+
pending: Array<ChatPendingInterrupt>;
|
|
160
|
+
} | null;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* A {@link ConnectConnectionAdapter} that also supports joining an existing run
|
|
164
|
+
* (a second tab, or re-attaching after a full reload) via `joinRun`, replaying
|
|
165
|
+
* the ordered stream from the start off the server's delivery-durability sink.
|
|
166
|
+
*/
|
|
167
|
+
export interface ResumableConnectConnectionAdapter extends ConnectConnectionAdapter {
|
|
168
|
+
/**
|
|
169
|
+
* Join an in-flight or finished run by id, replaying from the start
|
|
170
|
+
* (`?offset=-1`). Read-only — sends no messages.
|
|
171
|
+
*/
|
|
172
|
+
joinRun: (runId: string, abortSignal?: AbortSignal) => AsyncIterable<StreamChunk>;
|
|
173
|
+
/**
|
|
174
|
+
* Fetch server-authoritative hydration for `threadId`: the stored transcript,
|
|
175
|
+
* and a cursor to an in-flight run if one exists. The client calls this itself
|
|
176
|
+
* on mount (no loader/prop), then tails `activeRun` via `joinRun`. Read-only
|
|
177
|
+
* JSON GET (`?threadId`), so it is transport-agnostic regardless of how the
|
|
178
|
+
* delivery stream is served.
|
|
179
|
+
*/
|
|
180
|
+
hydrate?: (threadId: string) => Promise<ChatHydrationResult>;
|
|
40
181
|
}
|
|
41
182
|
export interface SubscribeConnectionAdapter {
|
|
42
183
|
/**
|
|
@@ -47,6 +188,19 @@ export interface SubscribeConnectionAdapter {
|
|
|
47
188
|
* Send a request; chunks arrive through subscribe().
|
|
48
189
|
*/
|
|
49
190
|
send: (messages: Array<UIMessage> | Array<ModelMessage>, data?: Record<string, any>, abortSignal?: AbortSignal, runContext?: RunAgentInputContext) => Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* Re-attach to an existing run by id, replaying its stream from the start off
|
|
193
|
+
* the server's delivery-durability sink. Present only when the underlying
|
|
194
|
+
* connection is resumable (a `ResumableConnectConnectionAdapter`). Used to
|
|
195
|
+
* rejoin an in-flight run after a full page reload.
|
|
196
|
+
*/
|
|
197
|
+
joinRun?: (runId: string, abortSignal?: AbortSignal) => AsyncIterable<StreamChunk>;
|
|
198
|
+
/**
|
|
199
|
+
* Server-authoritative hydration for a thread (transcript + in-flight-run
|
|
200
|
+
* cursor). Present only when the underlying connection supports it. The client
|
|
201
|
+
* calls it on mount to re-hydrate without any app-side loader or prop.
|
|
202
|
+
*/
|
|
203
|
+
hydrate?: (threadId: string) => Promise<ChatHydrationResult>;
|
|
50
204
|
}
|
|
51
205
|
/**
|
|
52
206
|
* Connection adapter union.
|
|
@@ -69,6 +223,8 @@ export interface FetchConnectionOptions {
|
|
|
69
223
|
signal?: AbortSignal;
|
|
70
224
|
body?: Record<string, any>;
|
|
71
225
|
fetchClient?: typeof globalThis.fetch;
|
|
226
|
+
/** Bounding for resumable-SSE reconnection (throttle delay + attempt ceiling). */
|
|
227
|
+
reconnect?: ReconnectOptions;
|
|
72
228
|
}
|
|
73
229
|
/**
|
|
74
230
|
* Options for XHR-based connection adapters.
|
|
@@ -79,6 +235,8 @@ export interface XhrConnectionOptions {
|
|
|
79
235
|
signal?: AbortSignal;
|
|
80
236
|
body?: Record<string, any>;
|
|
81
237
|
xhrFactory?: () => XMLHttpRequest;
|
|
238
|
+
/** Bounding for resumable reconnection (throttle delay + attempt ceiling). */
|
|
239
|
+
reconnect?: ReconnectOptions;
|
|
82
240
|
}
|
|
83
241
|
/**
|
|
84
242
|
* Create a Server-Sent Events connection adapter
|
|
@@ -109,12 +267,12 @@ export interface XhrConnectionOptions {
|
|
|
109
267
|
* const connection = fetchServerSentEvents('/api/chat', async () => ({
|
|
110
268
|
* body: {
|
|
111
269
|
* provider: 'openai',
|
|
112
|
-
* model: 'gpt-
|
|
270
|
+
* model: 'gpt-5.5',
|
|
113
271
|
* }
|
|
114
272
|
* }));
|
|
115
273
|
* ```
|
|
116
274
|
*/
|
|
117
|
-
export declare function fetchServerSentEvents(url: string | (() => string), options?: FetchConnectionOptions | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>)):
|
|
275
|
+
export declare function fetchServerSentEvents(url: string | (() => string), options?: FetchConnectionOptions | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>)): ResumableConnectConnectionAdapter;
|
|
118
276
|
/**
|
|
119
277
|
* Create an HTTP streaming connection adapter (for raw streaming without SSE format)
|
|
120
278
|
*
|
|
@@ -144,25 +302,67 @@ export declare function fetchServerSentEvents(url: string | (() => string), opti
|
|
|
144
302
|
* const connection = fetchHttpStream('/api/chat', async () => ({
|
|
145
303
|
* body: {
|
|
146
304
|
* provider: 'openai',
|
|
147
|
-
* model: 'gpt-
|
|
305
|
+
* model: 'gpt-5.5',
|
|
148
306
|
* }
|
|
149
307
|
* }));
|
|
150
308
|
* ```
|
|
151
309
|
*/
|
|
152
|
-
export declare function fetchHttpStream(url: string | (() => string), options?: FetchConnectionOptions | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>)):
|
|
310
|
+
export declare function fetchHttpStream(url: string | (() => string), options?: FetchConnectionOptions | (() => FetchConnectionOptions | Promise<FetchConnectionOptions>)): ResumableConnectConnectionAdapter;
|
|
153
311
|
type XhrConnectionOptionsResolver = XhrConnectionOptions | (() => XhrConnectionOptions | Promise<XhrConnectionOptions>);
|
|
154
312
|
/**
|
|
155
313
|
* Create an XMLHttpRequest-backed Server-Sent Events connection adapter.
|
|
314
|
+
*
|
|
315
|
+
* Resumable: against a durable (`id:`-tagged) server response, a dropped socket
|
|
316
|
+
* auto-reconnects with `Last-Event-ID` and de-dupes the replayed prefix, and
|
|
317
|
+
* `joinRun` attaches to an existing run from the start. A non-durable response
|
|
318
|
+
* is a single plain request, exactly as before.
|
|
156
319
|
*/
|
|
157
|
-
export declare function xhrServerSentEvents(url: string | (() => string), options?: XhrConnectionOptionsResolver):
|
|
320
|
+
export declare function xhrServerSentEvents(url: string | (() => string), options?: XhrConnectionOptionsResolver): ResumableConnectConnectionAdapter;
|
|
158
321
|
/**
|
|
159
322
|
* Create an XMLHttpRequest-backed newline-delimited JSON stream adapter.
|
|
323
|
+
*
|
|
324
|
+
* Resumable: against a durable (envelope-tagged) server response, a dropped
|
|
325
|
+
* socket auto-reconnects with `Last-Event-ID` and de-dupes the replayed prefix,
|
|
326
|
+
* and `joinRun` attaches to an existing run from the start. A non-durable
|
|
327
|
+
* (bare-line) response is a single plain request, exactly as before.
|
|
160
328
|
*/
|
|
161
|
-
export declare function xhrHttpStream(url: string | (() => string), options?: XhrConnectionOptionsResolver):
|
|
329
|
+
export declare function xhrHttpStream(url: string | (() => string), options?: XhrConnectionOptionsResolver): ResumableConnectConnectionAdapter;
|
|
330
|
+
/**
|
|
331
|
+
* Optional persistence handlers for the lightweight adapters (`stream()`,
|
|
332
|
+
* `rpcStream()`). These are one-shot, request-scoped calls with no built-in
|
|
333
|
+
* GET endpoint or second channel, so hydration and run-rejoin only exist if
|
|
334
|
+
* the app supplies them — typically thin wrappers over TanStack Start server
|
|
335
|
+
* functions backed by `@tanstack/ai-persistence` (`getGenerationHydration`)
|
|
336
|
+
* and a delivery-durability log (`memoryStream` / `replayRunStream`).
|
|
337
|
+
*
|
|
338
|
+
* Each handler is spread onto the returned adapter only when defined, so
|
|
339
|
+
* feature detection (`connection.hydrateGeneration` etc.) keeps working.
|
|
340
|
+
*/
|
|
341
|
+
export interface StreamConnectionHandlers {
|
|
342
|
+
/**
|
|
343
|
+
* Server-driven chat hydration for `persistence: true`: the stored
|
|
344
|
+
* transcript for `threadId` plus a cursor to an in-flight run.
|
|
345
|
+
*/
|
|
346
|
+
hydrate?: (threadId: string) => Promise<ChatHydrationResult>;
|
|
347
|
+
/**
|
|
348
|
+
* Server-driven generation hydration for `persistence: true`: the last
|
|
349
|
+
* generation's resume snapshot for `threadId` plus a cursor to a run still
|
|
350
|
+
* generating. See {@link ConnectConnectionAdapter.hydrateGeneration}.
|
|
351
|
+
*/
|
|
352
|
+
hydrateGeneration?: (threadId: string) => Promise<GenerationHydrationResult>;
|
|
353
|
+
/**
|
|
354
|
+
* Re-attach to a run still generating and replay it from the start. See
|
|
355
|
+
* {@link ConnectConnectionAdapter.joinRun}.
|
|
356
|
+
*/
|
|
357
|
+
joinRun?: (runId: string, abortSignal?: AbortSignal) => AsyncIterable<StreamChunk>;
|
|
358
|
+
}
|
|
162
359
|
/**
|
|
163
360
|
* Create a direct stream connection adapter (for server functions or direct streams)
|
|
164
361
|
*
|
|
165
362
|
* @param streamFactory - A function that returns an async iterable of StreamChunks
|
|
363
|
+
* @param handlers - Optional persistence handlers (`hydrate`,
|
|
364
|
+
* `hydrateGeneration`, `joinRun`) that let server-driven persistence work
|
|
365
|
+
* without an HTTP endpoint — each is usually a one-line server-function call
|
|
166
366
|
* @returns A connection adapter for direct streams
|
|
167
367
|
*
|
|
168
368
|
* @example
|
|
@@ -171,9 +371,18 @@ export declare function xhrHttpStream(url: string | (() => string), options?: Xh
|
|
|
171
371
|
* const connection = stream(() => serverFunction({ messages }));
|
|
172
372
|
*
|
|
173
373
|
* const client = new ChatClient({ connection });
|
|
374
|
+
*
|
|
375
|
+
* // With generation persistence over server functions
|
|
376
|
+
* const connection = stream(
|
|
377
|
+
* () => generateImageFn({ data: input }),
|
|
378
|
+
* {
|
|
379
|
+
* hydrateGeneration: (threadId) => getImageHydrationFn({ data: threadId }),
|
|
380
|
+
* joinRun: (runId) => joinImageRunFn({ data: runId }),
|
|
381
|
+
* },
|
|
382
|
+
* );
|
|
174
383
|
* ```
|
|
175
384
|
*/
|
|
176
|
-
export declare function stream(streamFactory: (messages: Array<UIMessage> | Array<ModelMessage>, data?: Record<string, any>, abortSignal?: AbortSignal) => AsyncIterable<StreamChunk
|
|
385
|
+
export declare function stream(streamFactory: (messages: Array<UIMessage> | Array<ModelMessage>, data?: Record<string, any>, abortSignal?: AbortSignal) => AsyncIterable<StreamChunk>, handlers?: StreamConnectionHandlers): ConnectConnectionAdapter;
|
|
177
386
|
/**
|
|
178
387
|
* Wrap a `ChatFetcher` as a `ConnectConnectionAdapter` so the chat client can
|
|
179
388
|
* consume it through the same `subscribe`/`send` plumbing used for SSE /
|
|
@@ -187,6 +396,9 @@ export declare function fetcherToConnectionAdapter(fetcher: ChatFetcher): Connec
|
|
|
187
396
|
* Create an RPC stream connection adapter (for RPC-based streaming like Cap'n Web RPC)
|
|
188
397
|
*
|
|
189
398
|
* @param rpcCall - A function that accepts messages and returns an async iterable of StreamChunks
|
|
399
|
+
* @param handlers - Optional persistence handlers (`hydrate`,
|
|
400
|
+
* `hydrateGeneration`, `joinRun`) that let server-driven persistence work
|
|
401
|
+
* without an HTTP endpoint — each is usually a one-line RPC call
|
|
190
402
|
* @returns A connection adapter for RPC streams
|
|
191
403
|
*
|
|
192
404
|
* @example
|
|
@@ -197,7 +409,16 @@ export declare function fetcherToConnectionAdapter(fetcher: ChatFetcher): Connec
|
|
|
197
409
|
* );
|
|
198
410
|
*
|
|
199
411
|
* const client = new ChatClient({ connection });
|
|
412
|
+
*
|
|
413
|
+
* // With generation persistence over RPC
|
|
414
|
+
* const connection = rpcStream(
|
|
415
|
+
* (messages, data) => api.streamMurfResponse(messages, data),
|
|
416
|
+
* {
|
|
417
|
+
* hydrateGeneration: (threadId) => api.getGenerationHydration(threadId),
|
|
418
|
+
* joinRun: (runId) => api.replayRun(runId),
|
|
419
|
+
* },
|
|
420
|
+
* );
|
|
200
421
|
* ```
|
|
201
422
|
*/
|
|
202
|
-
export declare function rpcStream(rpcCall: (messages: Array<UIMessage> | Array<ModelMessage>, data?: Record<string, any>, abortSignal?: AbortSignal) => AsyncIterable<StreamChunk
|
|
423
|
+
export declare function rpcStream(rpcCall: (messages: Array<UIMessage> | Array<ModelMessage>, data?: Record<string, any>, abortSignal?: AbortSignal) => AsyncIterable<StreamChunk>, handlers?: StreamConnectionHandlers): ConnectConnectionAdapter;
|
|
203
424
|
export {};
|