@tanstack/ai-client 0.22.1 → 0.23.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.
Files changed (72) hide show
  1. package/README.md +15 -1
  2. package/dist/esm/audio-recorder.js +190 -213
  3. package/dist/esm/audio-recorder.js.map +1 -1
  4. package/dist/esm/chat-client.d.ts +172 -3
  5. package/dist/esm/chat-client.js +1656 -1386
  6. package/dist/esm/chat-client.js.map +1 -1
  7. package/dist/esm/cleared-stream-tracker.d.ts +23 -0
  8. package/dist/esm/cleared-stream-tracker.js +97 -0
  9. package/dist/esm/cleared-stream-tracker.js.map +1 -0
  10. package/dist/esm/client-persistor.d.ts +25 -12
  11. package/dist/esm/client-persistor.js +260 -235
  12. package/dist/esm/client-persistor.js.map +1 -1
  13. package/dist/esm/connection-adapters.d.ts +231 -10
  14. package/dist/esm/connection-adapters.js +989 -574
  15. package/dist/esm/connection-adapters.js.map +1 -1
  16. package/dist/esm/devtools-noop.d.ts +1 -0
  17. package/dist/esm/devtools-noop.js +79 -139
  18. package/dist/esm/devtools-noop.js.map +1 -1
  19. package/dist/esm/devtools.d.ts +31 -1
  20. package/dist/esm/devtools.js +977 -1127
  21. package/dist/esm/devtools.js.map +1 -1
  22. package/dist/esm/events.js +224 -226
  23. package/dist/esm/events.js.map +1 -1
  24. package/dist/esm/generation-client.d.ts +145 -2
  25. package/dist/esm/generation-client.js +659 -321
  26. package/dist/esm/generation-client.js.map +1 -1
  27. package/dist/esm/generation-reconstruct.d.ts +21 -0
  28. package/dist/esm/generation-reconstruct.js +85 -0
  29. package/dist/esm/generation-reconstruct.js.map +1 -0
  30. package/dist/esm/generation-types.d.ts +289 -3
  31. package/dist/esm/generation-types.js +356 -13
  32. package/dist/esm/generation-types.js.map +1 -1
  33. package/dist/esm/index.d.ts +9 -4
  34. package/dist/esm/index.js +7 -39
  35. package/dist/esm/interrupt-manager.d.ts +77 -0
  36. package/dist/esm/interrupt-manager.js +787 -0
  37. package/dist/esm/interrupt-manager.js.map +1 -0
  38. package/dist/esm/mcp-app-bridge.js +56 -64
  39. package/dist/esm/mcp-app-bridge.js.map +1 -1
  40. package/dist/esm/realtime-client.js +366 -440
  41. package/dist/esm/realtime-client.js.map +1 -1
  42. package/dist/esm/response-stream.js +19 -26
  43. package/dist/esm/response-stream.js.map +1 -1
  44. package/dist/esm/sse-parser.js +44 -47
  45. package/dist/esm/sse-parser.js.map +1 -1
  46. package/dist/esm/sse-utils.js +8 -9
  47. package/dist/esm/sse-utils.js.map +1 -1
  48. package/dist/esm/storage-adapters.d.ts +62 -0
  49. package/dist/esm/storage-adapters.js +174 -0
  50. package/dist/esm/storage-adapters.js.map +1 -0
  51. package/dist/esm/types.d.ts +212 -10
  52. package/dist/esm/types.js +38 -7
  53. package/dist/esm/types.js.map +1 -1
  54. package/dist/esm/video-generation-client.d.ts +113 -2
  55. package/dist/esm/video-generation-client.js +665 -379
  56. package/dist/esm/video-generation-client.js.map +1 -1
  57. package/package.json +7 -7
  58. package/src/chat-client.ts +1079 -61
  59. package/src/cleared-stream-tracker.ts +151 -0
  60. package/src/client-persistor.ts +102 -33
  61. package/src/connection-adapters.ts +1185 -142
  62. package/src/devtools-noop.ts +4 -3
  63. package/src/devtools.ts +121 -3
  64. package/src/generation-client.ts +563 -13
  65. package/src/generation-reconstruct.ts +121 -0
  66. package/src/generation-types.ts +727 -3
  67. package/src/index.ts +56 -1
  68. package/src/interrupt-manager.ts +1440 -0
  69. package/src/storage-adapters.ts +242 -0
  70. package/src/types.ts +301 -9
  71. package/src/video-generation-client.ts +479 -13
  72. 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-4o',
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>)): ConnectConnectionAdapter;
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-4o',
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>)): ConnectConnectionAdapter;
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): ConnectConnectionAdapter;
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): ConnectConnectionAdapter;
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>): ConnectConnectionAdapter;
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>): ConnectConnectionAdapter;
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 {};