@tanstack/ai-client 0.22.0 → 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":"generation-client.js","sources":["../../src/generation-client.ts"],"sourcesContent":["import { GENERATION_EVENTS } from './generation-types'\nimport { createNoOpGenerationDevtoolsBridge } from './devtools-noop'\nimport { parseSSEResponse } from './sse-parser'\nimport type { StreamChunk } from '@tanstack/ai/client'\nimport type {\n ConnectConnectionAdapter,\n RunAgentInputContext,\n} from './connection-adapters'\nimport type {\n AIDevtoolsClientMetadata,\n AIDevtoolsGenerationProgress,\n GenerationDevtoolsBridge,\n GenerationDevtoolsBridgeOptions,\n} from './devtools'\nimport type {\n GenerationClientOptions,\n GenerationClientState,\n GenerationFetcher,\n} from './generation-types'\n\n/**\n * Callbacks stored in a ref so hooks can update them without recreating the client.\n */\n// All optional fields explicitly allow `| undefined` so callers can spread\n// option bags (where each callback may be `undefined`) into the callbacks\n// ref under `exactOptionalPropertyTypes`.\ninterface GenerationCallbacks<TResult, TOutput> {\n onResult?: ((result: TResult) => TOutput | null | void) | undefined\n onError?: ((error: Error) => void) | undefined\n onProgress?: ((progress: number, message?: string) => void) | undefined\n onChunk?: ((chunk: StreamChunk) => void) | undefined\n onResultChange?: ((result: TOutput | null) => void) | undefined\n onLoadingChange?: ((isLoading: boolean) => void) | undefined\n onErrorChange?: ((error: Error | undefined) => void) | undefined\n onStatusChange?: ((status: GenerationClientState) => void) | undefined\n}\n\n/**\n * A lightweight, generic client for one-shot generation tasks\n * (image, speech, transcription, summarize).\n *\n * Supports two transport modes:\n * - **ConnectConnectionAdapter** — Streaming transport (SSE, HTTP stream, custom).\n * Server wraps results in StreamChunk events with CUSTOM event names.\n * - **Fetcher** — Direct async function call. No streaming protocol needed.\n *\n * @template TInput - The input type for the generation request\n * @template TResult - The result type returned by the generation\n *\n * @example\n * ```typescript\n * // With streaming connection adapter\n * const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({\n * connection: fetchServerSentEvents('/api/generate/image'),\n * onResultChange: setResult,\n * onLoadingChange: setIsLoading,\n * })\n *\n * // With fetcher (direct)\n * const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({\n * fetcher: async (input) => {\n * const res = await fetch('/api/generate/image', {\n * method: 'POST',\n * body: JSON.stringify(input),\n * })\n * return res.json()\n * },\n * })\n *\n * await client.generate({ prompt: 'A sunset over mountains' })\n * ```\n */\nexport class GenerationClient<\n TInput extends Record<string, any>,\n TResult,\n TOutput = TResult,\n> {\n private readonly connection: ConnectConnectionAdapter | undefined\n private readonly fetcher: GenerationFetcher<TInput, TResult> | undefined\n private readonly uniqueId: string\n private readonly devtoolsMetadata: AIDevtoolsClientMetadata\n private readonly devtoolsBridge: GenerationDevtoolsBridge<TOutput>\n private readonly threadId: string\n private body: Record<string, any>\n private result: TOutput | null = null\n private input: TInput | null = null\n private progress: AIDevtoolsGenerationProgress | null = null\n private isLoading = false\n private error: Error | undefined = undefined\n private status: GenerationClientState = 'idle'\n private abortController: AbortController | null = null\n private readonly callbacksRef: GenerationCallbacks<TResult, TOutput>\n private devtoolsMounted = false\n\n constructor(\n options: GenerationClientOptions<TInput, TResult, TOutput> &\n (\n | { connection: ConnectConnectionAdapter; fetcher?: never }\n | {\n fetcher: GenerationFetcher<TInput, TResult>\n connection?: never\n }\n ),\n ) {\n this.uniqueId = options.id ?? this.generateUniqueId('generation')\n this.threadId = this.uniqueId\n this.connection = options.connection\n this.fetcher = options.fetcher\n this.body = options.body ?? {}\n\n this.callbacksRef = {\n onResult: options.onResult,\n onError: options.onError,\n onProgress: options.onProgress,\n onChunk: options.onChunk,\n onResultChange: options.onResultChange,\n onLoadingChange: options.onLoadingChange,\n onErrorChange: options.onErrorChange,\n onStatusChange: options.onStatusChange,\n }\n\n this.devtoolsMetadata = this.createDevtoolsMetadata(options.devtools)\n this.devtoolsBridge = (\n options.devtoolsBridgeFactory ?? createNoOpGenerationDevtoolsBridge\n )<TOutput>(this.buildDevtoolsBridgeOptions())\n }\n\n private buildDevtoolsBridgeOptions(): GenerationDevtoolsBridgeOptions<TOutput> {\n return {\n hookId: this.uniqueId,\n clientId: this.uniqueId,\n threadId: this.threadId,\n metadata: this.devtoolsMetadata,\n getCoreState: () => ({\n input: this.input,\n result: this.result,\n progress: this.progress,\n status: this.status,\n isLoading: this.isLoading,\n ...(this.error ? { error: this.error.message } : {}),\n }),\n }\n }\n\n mountDevtools(): void {\n if (this.devtoolsMounted) {\n return\n }\n\n this.devtoolsMounted = true\n this.devtoolsBridge.emitRegistered()\n this.devtoolsBridge.emitSnapshot()\n }\n\n /**\n * Trigger a generation request.\n * Only one generation can be in-flight at a time; calling generate()\n * while already generating will be a no-op.\n */\n async generate(input: TInput): Promise<void> {\n this.mountDevtools()\n if (this.isLoading) return\n\n this.input = input\n this.progress = null\n const runId = this.devtoolsBridge.beginRun(input)\n this.setIsLoading(true)\n this.setStatus('generating')\n this.setError(undefined)\n\n const abortController = new AbortController()\n this.abortController = abortController\n const { signal } = abortController\n\n try {\n if (this.fetcher) {\n // Direct fetch path\n const result = await this.fetcher(input, { signal })\n if (signal.aborted) return\n if (result instanceof Response) {\n // Server function returned SSE Response — parse stream\n await this.processStream(parseSSEResponse(result, signal), runId)\n } else {\n this.devtoolsBridge.ensureRunStarted(runId)\n this.setResult(result)\n this.setStatus('success')\n }\n } else if (this.connection) {\n // Streaming adapter path\n const mergedData = { ...this.body, ...input }\n const stream = this.connection.connect(\n [],\n mergedData,\n signal,\n this.createRunContext(runId),\n )\n await this.processStream(stream, runId)\n } else {\n throw new Error(\n 'GenerationClient requires either a connection or fetcher option',\n )\n }\n if (!signal.aborted && this.status === 'success') {\n // Bump progress to 100 on successful completion so devtools\n // snapshots reflect the final state. The bridge mirrors this in\n // the run's recorded progress, but the snapshot reads `progress`\n // from the client's core state.\n this.progress = completeProgressValue(this.progress)\n this.devtoolsBridge.finishRun(\n this.devtoolsBridge.getActiveRunId() ?? runId,\n 'run:completed',\n 'completed',\n )\n }\n } catch (err: unknown) {\n if (signal.aborted) return\n const error = err instanceof Error ? err : new Error(String(err))\n this.setError(error)\n this.setStatus('error')\n this.devtoolsBridge.finishRun(\n this.devtoolsBridge.getActiveRunId() ?? runId,\n 'run:errored',\n 'errored',\n error.message,\n )\n this.callbacksRef.onError?.(error)\n } finally {\n this.abortController = null\n this.setIsLoading(false)\n }\n }\n\n /**\n * Process a stream of AG-UI events from the streaming connection adapter.\n */\n private async processStream(\n source: AsyncIterable<StreamChunk>,\n fallbackRunId: string,\n ): Promise<void> {\n let streamRunId: string | undefined\n\n for await (const chunk of source) {\n if (this.abortController?.signal.aborted) break\n\n this.callbacksRef.onChunk?.(chunk)\n const chunkRunId =\n 'runId' in chunk && typeof chunk.runId === 'string'\n ? chunk.runId\n : undefined\n\n // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType has ~22 variants; this consumer only handles the subset relevant to generation lifecycle.\n switch (chunk.type) {\n case 'RUN_STARTED': {\n streamRunId = chunk.runId\n this.devtoolsBridge.ensureRunStarted(chunk.runId)\n break\n }\n case 'CUSTOM': {\n this.devtoolsBridge.ensureRunStarted(streamRunId ?? fallbackRunId)\n if (chunk.name === GENERATION_EVENTS.RESULT) {\n this.setResult(chunk.value as TResult)\n } else if (chunk.name === GENERATION_EVENTS.PROGRESS) {\n const { progress, message } = chunk.value as {\n progress: number\n message?: string\n }\n this.setProgress(progress, message)\n }\n break\n }\n case 'RUN_FINISHED': {\n streamRunId = chunk.runId\n this.devtoolsBridge.ensureRunStarted(chunk.runId)\n this.setStatus('success')\n break\n }\n case 'RUN_ERROR': {\n this.devtoolsBridge.ensureRunStarted(\n chunkRunId ?? streamRunId ?? fallbackRunId,\n )\n // Prefer spec `message`; fall back to deprecated `error.message`\n const msg =\n (chunk.message as string | undefined) ||\n chunk.error?.message ||\n 'An error occurred'\n throw new Error(msg)\n }\n default:\n break\n }\n }\n }\n\n /**\n * Abort any in-flight generation request.\n */\n stop(): void {\n const runId = this.devtoolsBridge.getActiveRunId()\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n if (this.status === 'generating') {\n this.setStatus('idle')\n if (runId) {\n this.devtoolsBridge.finishRun(runId, 'run:cancelled', 'cancelled')\n }\n }\n }\n\n /**\n * Clear the result, error, and return to idle state.\n */\n reset(): void {\n this.stop()\n this.setResult(null)\n this.input = null\n this.progress = null\n this.devtoolsBridge.resetRuns()\n this.setError(undefined)\n this.setStatus('idle')\n this.devtoolsBridge.emitState()\n }\n\n /**\n * Update options without recreating the client.\n */\n updateOptions(\n options: Partial<\n Pick<\n GenerationClientOptions<TInput, TResult, TOutput>,\n 'body' | 'onResult' | 'onError' | 'onProgress' | 'onChunk'\n >\n >,\n ): void {\n if (options.body !== undefined) {\n this.body = options.body ?? {}\n }\n if (options.onResult !== undefined) {\n this.callbacksRef.onResult = options.onResult\n }\n if (options.onError !== undefined) {\n this.callbacksRef.onError = options.onError\n }\n if (options.onProgress !== undefined) {\n this.callbacksRef.onProgress = options.onProgress\n }\n if (options.onChunk !== undefined) {\n this.callbacksRef.onChunk = options.onChunk\n }\n }\n\n dispose(): void {\n this.stop()\n this.devtoolsBridge.dispose()\n this.devtoolsMounted = false\n }\n\n // ===========================\n // Getters\n // ===========================\n\n getResult(): TOutput | null {\n return this.result\n }\n\n getIsLoading(): boolean {\n return this.isLoading\n }\n\n getError(): Error | undefined {\n return this.error\n }\n\n getStatus(): GenerationClientState {\n return this.status\n }\n\n // ===========================\n // Private state setters\n // ===========================\n\n private setResult(rawResult: TResult | null): void {\n if (rawResult === null) {\n this.result = null\n this.callbacksRef.onResultChange?.(null)\n this.devtoolsBridge.recordResultChange()\n return\n }\n\n if (this.callbacksRef.onResult) {\n const transformed = this.callbacksRef.onResult(rawResult)\n if (transformed === null) {\n // null return → keep previous result unchanged, just re-emit\n this.devtoolsBridge.emitState()\n return\n }\n if (transformed !== undefined) {\n // Non-null, non-undefined → use transformed value\n this.result = transformed\n this.callbacksRef.onResultChange?.(this.result)\n this.devtoolsBridge.recordResultChange()\n return\n }\n }\n\n // No onResult callback, or callback returned void → use raw value as\n // TOutput. When the caller did not supply an onResult transform,\n // `TOutput` defaults to `TResult`, so the runtime cast is sound.\n // eslint-disable-next-line no-restricted-syntax -- TOutput defaults to TResult when no onResult transform is supplied\n this.result = rawResult as unknown as TOutput\n this.callbacksRef.onResultChange?.(this.result)\n this.devtoolsBridge.recordResultChange()\n }\n\n private setIsLoading(isLoading: boolean): void {\n this.isLoading = isLoading\n this.callbacksRef.onLoadingChange?.(isLoading)\n this.devtoolsBridge.recordLoadingChange()\n }\n\n private setError(error: Error | undefined): void {\n this.error = error\n this.callbacksRef.onErrorChange?.(error)\n this.devtoolsBridge.recordErrorChange(error)\n }\n\n private setStatus(status: GenerationClientState): void {\n this.status = status\n this.callbacksRef.onStatusChange?.(status)\n this.devtoolsBridge.recordStatusChange(status)\n }\n\n private setProgress(value: number, message?: string): void {\n this.progress = {\n value,\n ...(message ? { message } : {}),\n }\n if (message === undefined) {\n this.callbacksRef.onProgress?.(value)\n } else {\n this.callbacksRef.onProgress?.(value, message)\n }\n this.devtoolsBridge.recordProgressChange()\n }\n\n private createDevtoolsMetadata(\n metadata?: Partial<AIDevtoolsClientMetadata>,\n ): AIDevtoolsClientMetadata {\n return {\n hookName: metadata?.hookName ?? 'useGeneration',\n ...(metadata?.framework ? { framework: metadata.framework } : {}),\n ...(metadata?.outputKind ? { outputKind: metadata.outputKind } : {}),\n ...(metadata?.name ? { name: metadata.name } : {}),\n }\n }\n\n private generateUniqueId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n }\n\n private createRunContext(runId: string): RunAgentInputContext {\n return {\n threadId: this.threadId,\n runId,\n }\n }\n}\n\nfunction completeProgressValue(\n progress: AIDevtoolsGenerationProgress | null,\n): AIDevtoolsGenerationProgress | null {\n if (!progress) return null\n const message = progress.message\n return {\n value: 100,\n ...(message ? { message } : {}),\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"generation-client.js","names":[],"sources":["../../src/generation-client.ts"],"sourcesContent":["import {\n GENERATION_EVENTS,\n GENERATION_STREAM_TRUNCATED_MESSAGE,\n GENERATION_UNRESTORABLE_RESULT_MESSAGE,\n clientStateFromResumeStatus,\n createGenerationHydrationError,\n createGenerationResultSnapshot,\n parseGenerationResumeSnapshot,\n updateGenerationResumeSnapshot,\n} from './generation-types'\nimport { createNoOpGenerationDevtoolsBridge } from './devtools-noop'\nimport { parseSSEResponse } from './sse-parser'\nimport type { StreamChunk } from '@tanstack/ai/client'\nimport type {\n ConnectConnectionAdapter,\n GenerationHydrationResult,\n RunAgentInputContext,\n} from './connection-adapters'\nimport type {\n AIDevtoolsClientMetadata,\n AIDevtoolsGenerationProgress,\n GenerationDevtoolsBridge,\n GenerationDevtoolsBridgeOptions,\n} from './devtools'\nimport type {\n GenerationClientOptions,\n GenerationClientState,\n GenerationFetcher,\n GenerationRestoredResult,\n GenerationResumeSnapshot,\n GenerationResumeState,\n} from './generation-types'\n\n/**\n * Callbacks stored in a ref so hooks can update them without recreating the client.\n */\n// All optional fields explicitly allow `| undefined` so callers can spread\n// option bags (where each callback may be `undefined`) into the callbacks\n// ref under `exactOptionalPropertyTypes`.\ninterface GenerationCallbacks<TResult, TOutput> {\n onResult?: ((result: TResult) => TOutput | null | void) | undefined\n onError?: ((error: Error) => void) | undefined\n onProgress?: ((progress: number, message?: string) => void) | undefined\n onChunk?: ((chunk: StreamChunk) => void) | undefined\n onResultChange?: ((result: TOutput | null) => void) | undefined\n onLoadingChange?: ((isLoading: boolean) => void) | undefined\n onErrorChange?: ((error: Error | undefined) => void) | undefined\n onStatusChange?: ((status: GenerationClientState) => void) | undefined\n onResumeSnapshotChange?:\n | ((snapshot: GenerationResumeSnapshot | undefined) => void)\n | undefined\n onResumeStateChange?:\n | ((resumeState: GenerationResumeState | null) => void)\n | undefined\n reconstructResult?:\n | ((restored: GenerationRestoredResult) => TResult | null)\n | undefined\n}\n\n/**\n * A lightweight, generic client for one-shot generation tasks\n * (image, speech, transcription, summarize).\n *\n * Supports two transport modes:\n * - **ConnectConnectionAdapter** — Streaming transport (SSE, HTTP stream, custom).\n * Server wraps results in StreamChunk events with CUSTOM event names.\n * - **Fetcher** — Direct async function call. No streaming protocol needed.\n *\n * @template TInput - The input type for the generation request\n * @template TResult - The result type returned by the generation\n *\n * @example\n * ```typescript\n * // With streaming connection adapter\n * const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({\n * connection: fetchServerSentEvents('/api/generate/image'),\n * onResultChange: setResult,\n * onLoadingChange: setIsLoading,\n * })\n *\n * // With fetcher (direct)\n * const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({\n * fetcher: async (input) => {\n * const res = await fetch('/api/generate/image', {\n * method: 'POST',\n * body: JSON.stringify(input),\n * })\n * return res.json()\n * },\n * })\n *\n * await client.generate({ prompt: 'A sunset over mountains' })\n * ```\n */\nexport class GenerationClient<\n TInput extends Record<string, any>,\n TResult,\n TOutput = TResult,\n> {\n private readonly connection: ConnectConnectionAdapter | undefined\n private readonly fetcher: GenerationFetcher<TInput, TResult> | undefined\n // Persistence handlers supplied as options (e.g. alongside a `fetcher`), used\n // when the connection doesn't carry its own — the connection's handlers take\n // precedence when both exist.\n private readonly hydrateGenerationHandler:\n | ConnectConnectionAdapter['hydrateGeneration']\n | undefined\n private readonly joinRunHandler:\n | ConnectConnectionAdapter['joinRun']\n | undefined\n private readonly uniqueId: string\n private readonly devtoolsMetadata: AIDevtoolsClientMetadata\n private readonly devtoolsBridge: GenerationDevtoolsBridge<TOutput>\n private readonly threadId: string\n private readonly persistenceScope: string | undefined\n // Server-driven mode (`persistence: true`): no local snapshot store; on mount\n // the client hydrates the last generation for `threadId` from the server.\n private readonly serverDriven: boolean = false\n private body: Record<string, any>\n private result: TOutput | null = null\n private input: TInput | null = null\n private progress: AIDevtoolsGenerationProgress | null = null\n private isLoading = false\n private error: Error | undefined = undefined\n private status: GenerationClientState = 'idle'\n private resumeSnapshot: GenerationResumeSnapshot | undefined\n private lastEmittedResumeState: string | undefined\n private abortController: AbortController | null = null\n private rejoinedRunId: string | undefined\n private readonly callbacksRef: GenerationCallbacks<TResult, TOutput>\n private devtoolsMounted = false\n private disposed = false\n private serverHydrationStarted = false\n\n constructor(\n options: GenerationClientOptions<TInput, TResult, TOutput> &\n (\n | { connection: ConnectConnectionAdapter; fetcher?: never }\n | {\n fetcher: GenerationFetcher<TInput, TResult>\n connection?: never\n }\n ),\n ) {\n // `threadId` is the single identity. Deprecated `id` is only a fallback\n // when no threadId is given (ephemeral runs / legacy call sites).\n this.uniqueId =\n options.threadId ?? options.id ?? this.generateUniqueId('generation')\n // AG-UI requires a thread id on every run, so fall back to uniqueId. The\n // generated fallback is for the WIRE ONLY: it is not stable across reloads,\n // so persistence must never key on it — see `persistenceScope` below.\n this.threadId = options.threadId ?? this.uniqueId\n // The persistence scope: the explicit `threadId` and nothing else. The\n // types require it whenever `persistence` is set; this field keeps the\n // fallback from silently becoming a storage key for JS callers.\n this.persistenceScope = options.threadId\n this.connection = options.connection\n this.fetcher = options.fetcher\n this.hydrateGenerationHandler = options.hydrateGeneration\n this.joinRunHandler = options.joinRun\n this.body = options.body ?? {}\n // `persistence` is `false`/omitted (ephemeral) or `true` (server-driven:\n // hydrate the last generation for `threadId` from the server on mount).\n this.serverDriven = options.persistence === true\n // The types require `threadId` alongside `persistence`, so this only fires\n // for JS callers. Warn rather than fall back silently: keying on the\n // generated wire id would write a different slot every reload, restoring\n // nothing while accumulating orphaned records.\n if (options.persistence && !this.persistenceScope) {\n console.warn(\n '[TanStack AI] `persistence` needs a stable `threadId` to key on. Without one nothing will be restored after a reload. Pass a `threadId` derived from your own domain (e.g. `product-123-hero`).',\n )\n }\n this.callbacksRef = {\n onResult: options.onResult,\n onError: options.onError,\n onProgress: options.onProgress,\n onChunk: options.onChunk,\n onResultChange: options.onResultChange,\n onLoadingChange: options.onLoadingChange,\n onErrorChange: options.onErrorChange,\n onStatusChange: options.onStatusChange,\n onResumeSnapshotChange: options.onResumeSnapshotChange,\n onResumeStateChange: options.onResumeStateChange,\n reconstructResult: options.reconstructResult,\n }\n\n this.devtoolsMetadata = this.createDevtoolsMetadata(options.devtools)\n this.devtoolsBridge = (\n options.devtoolsBridgeFactory ?? createNoOpGenerationDevtoolsBridge\n )<TOutput>(this.buildDevtoolsBridgeOptions())\n\n // Mount hydration (`maybeHydrateFromServer`) is deliberately NOT run here. The framework\n // hooks build this client inside `useMemo`, so the constructor executes in\n // React's render phase; hydrating here would re-fire the hydrate GET on\n // every discarded/speculative render, flooding the connection pool when\n // several clients mount together. It is kicked off once from\n // `mountDevtools`, which the hooks call from a commit-phase mount effect.\n }\n\n private buildDevtoolsBridgeOptions(): GenerationDevtoolsBridgeOptions<TOutput> {\n return {\n hookId: this.uniqueId,\n clientId: this.uniqueId,\n threadId: this.threadId,\n metadata: this.devtoolsMetadata,\n getCoreState: () => ({\n input: this.input,\n result: this.result,\n progress: this.progress,\n status: this.status,\n isLoading: this.isLoading,\n ...(this.error ? { error: this.error.message } : {}),\n }),\n }\n }\n\n mountDevtools(): void {\n // Mounting revives a disposed client. Framework hooks call this from\n // their mount effect, so a dispose → remount cycle (e.g. React\n // StrictMode's mount → cleanup → mount replay against the same memoized\n // client) leaves the client usable again.\n this.disposed = false\n this.maybeHydrateFromServer()\n // Re-attach to an in-flight run whose snapshot is already loaded — the\n // remount case. On the FIRST mount the snapshot loads asynchronously and\n // `repaintRestoredSnapshot` starts the rejoin; on a StrictMode remount the\n // snapshot is already present but the prior rejoin was aborted by\n // `dispose()`, so retrigger it here. Guarded by `rejoinInFlight`'s own\n // dedupe/in-flight checks, so this never double-joins.\n this.maybeResumeInFlight()\n if (this.devtoolsMounted) {\n return\n }\n\n this.devtoolsMounted = true\n this.devtoolsBridge.emitRegistered()\n this.devtoolsBridge.emitSnapshot()\n }\n\n /**\n * Trigger a generation request.\n * Only one generation can be in-flight at a time; calling generate()\n * while already generating will be a no-op.\n */\n async generate(input: TInput): Promise<void> {\n if (this.disposed) return\n if (this.isLoading) return\n this.mountDevtools()\n\n this.input = input\n this.progress = null\n const runId = this.devtoolsBridge.beginRun(input)\n this.setIsLoading(true)\n this.setStatus('generating')\n this.setError(undefined)\n\n const abortController = new AbortController()\n this.abortController = abortController\n const { signal } = abortController\n\n try {\n if (this.fetcher) {\n // Direct fetch path\n const result = await this.fetcher(input, { signal })\n if (signal.aborted) return\n if (result instanceof Response) {\n // Server function returned SSE Response — parse stream\n await this.processStream(\n parseSSEResponse(result, signal),\n runId,\n signal,\n )\n } else {\n this.devtoolsBridge.ensureRunStarted(runId)\n this.setResult(result)\n this.setStatus('success')\n this.completePlainFetcherResumeSnapshot(result)\n }\n } else if (this.connection) {\n // Streaming adapter path\n const mergedData = { ...this.body, ...input }\n const stream = this.connection.connect(\n [],\n mergedData,\n signal,\n this.createRunContext(runId),\n )\n await this.processStream(stream, runId, signal)\n } else {\n throw new Error(\n 'GenerationClient requires either a connection or fetcher option',\n )\n }\n if (!signal.aborted && this.status === 'success') {\n // Bump progress to 100 on successful completion so devtools\n // snapshots reflect the final state. The bridge mirrors this in\n // the run's recorded progress, but the snapshot reads `progress`\n // from the client's core state.\n this.progress = completeProgressValue(this.progress)\n this.devtoolsBridge.finishRun(\n this.devtoolsBridge.getActiveRunId() ?? runId,\n 'run:completed',\n 'completed',\n )\n }\n } catch (err: unknown) {\n if (signal.aborted) return\n const error = err instanceof Error ? err : new Error(String(err))\n this.setError(error)\n this.setStatus('error')\n this.recordResumeSnapshotError(error)\n this.devtoolsBridge.finishRun(\n this.devtoolsBridge.getActiveRunId() ?? runId,\n 'run:errored',\n 'errored',\n error.message,\n )\n this.callbacksRef.onError?.(error)\n } finally {\n if (this.abortController === abortController) {\n this.abortController = null\n this.setIsLoading(false)\n }\n }\n }\n\n /**\n * Process a stream of AG-UI events from the streaming connection adapter.\n *\n * Throws {@link GENERATION_STREAM_TRUNCATED_MESSAGE} when the iteration ends\n * without a terminal chunk. A `for await` over a stream that simply stops —\n * proxy idle timeout, server restart, a durable log missing its terminal\n * append — returns normally and would otherwise leave the caller's `status`\n * on `generating` forever, with the persisted snapshot still `running` so\n * every reload rejoins the same dead run. Throwing routes it through the\n * caller's error path instead, which settles the status and rewrites the\n * snapshot so nothing chases it again.\n */\n private async processStream(\n source: AsyncIterable<StreamChunk>,\n fallbackRunId: string,\n signal: AbortSignal,\n ): Promise<void> {\n let streamRunId: string | undefined\n let sawTerminalChunk = false\n\n for await (const chunk of source) {\n if (signal.aborted) break\n\n this.callbacksRef.onChunk?.(chunk)\n this.observeResumeSnapshot(chunk)\n const chunkRunId =\n 'runId' in chunk && typeof chunk.runId === 'string'\n ? chunk.runId\n : undefined\n\n // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- AG-UI EventType has ~22 variants; this consumer only handles the subset relevant to generation lifecycle.\n switch (chunk.type) {\n case 'RUN_STARTED': {\n streamRunId = chunk.runId\n this.devtoolsBridge.ensureRunStarted(chunk.runId)\n break\n }\n case 'CUSTOM': {\n this.devtoolsBridge.ensureRunStarted(streamRunId ?? fallbackRunId)\n if (chunk.name === GENERATION_EVENTS.RESULT) {\n this.setResult(chunk.value as TResult)\n } else if (chunk.name === GENERATION_EVENTS.PROGRESS) {\n const { progress, message } = chunk.value as {\n progress: number\n message?: string\n }\n this.setProgress(progress, message)\n }\n break\n }\n case 'RUN_FINISHED': {\n streamRunId = chunk.runId\n sawTerminalChunk = true\n this.devtoolsBridge.ensureRunStarted(chunk.runId)\n this.setStatus('success')\n break\n }\n case 'RUN_ERROR': {\n this.devtoolsBridge.ensureRunStarted(\n chunkRunId ?? streamRunId ?? fallbackRunId,\n )\n // Prefer spec `message`; fall back to deprecated `error.message`\n const msg =\n (chunk.message as string | undefined) ||\n chunk.error?.message ||\n 'An error occurred'\n throw new Error(msg)\n }\n default:\n break\n }\n }\n\n // An aborted read is a deliberate stop/dispose, not a truncation.\n if (!sawTerminalChunk && !signal.aborted) {\n throw new Error(GENERATION_STREAM_TRUNCATED_MESSAGE)\n }\n }\n\n /**\n * Abort any in-flight generation request.\n */\n stop(): void {\n const runId = this.devtoolsBridge.getActiveRunId()\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n if (this.status === 'generating') {\n this.setStatus('idle')\n if (runId) {\n this.devtoolsBridge.finishRun(runId, 'run:cancelled', 'cancelled')\n }\n }\n // A stopped run is no longer resumable. Without this the in-memory\n // snapshot stays `running`, and a remount's `maybeResumeInFlight` would\n // rejoin a run the user just cancelled.\n if (this.resumeSnapshot && this.resumeSnapshot.status === 'running') {\n this.resumeSnapshot = {\n ...this.resumeSnapshot,\n resumeState: null,\n status: 'idle',\n }\n this.notifyResumeSnapshotChanged()\n }\n }\n\n /**\n * Clear the result, error, and return to idle state. Also drops the client's\n * in-memory resume snapshot, so a remount restores nothing. The server-side\n * record is untouched — this client no longer writes one — so a full page\n * reload under `persistence: true` re-hydrates the last generation again.\n */\n reset(): void {\n this.stop()\n this.setResult(null)\n this.input = null\n this.progress = null\n this.devtoolsBridge.resetRuns()\n this.setError(undefined)\n this.setStatus('idle')\n this.clearResumeSnapshot()\n this.devtoolsBridge.emitState()\n }\n\n /**\n * Update options without recreating the client.\n */\n updateOptions(\n options: Partial<\n Pick<\n GenerationClientOptions<TInput, TResult, TOutput>,\n 'body' | 'onResult' | 'onError' | 'onProgress' | 'onChunk'\n >\n >,\n ): void {\n if (options.body !== undefined) {\n this.body = options.body ?? {}\n }\n if (options.onResult !== undefined) {\n this.callbacksRef.onResult = options.onResult\n }\n if (options.onError !== undefined) {\n this.callbacksRef.onError = options.onError\n }\n if (options.onProgress !== undefined) {\n this.callbacksRef.onProgress = options.onProgress\n }\n if (options.onChunk !== undefined) {\n this.callbacksRef.onChunk = options.onChunk\n }\n }\n\n dispose(): void {\n this.disposed = true\n // Teardown, NOT a user cancel: abort in-flight DELIVERY (this reader) but\n // do NOT call `stop()` — `stop()` marks the run non-resumable and wipes the\n // `running` snapshot, which is correct for a Stop button but wrong for an\n // unmount / React StrictMode dispose. Clearing it here would destroy the\n // in-memory resume state, so a remount of this same client instance could\n // never rejoin. (A real page revisit re-hydrates from the server instead.)\n // The run itself survives server-side (durable delivery), so the snapshot\n // must stay `running` for the remount to resume it.\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n this.devtoolsBridge.dispose()\n this.devtoolsMounted = false\n // Re-arm mount hydration + rejoin so a remount resumes from the (preserved)\n // snapshot. `mountDevtools` re-runs the hydration entry point and\n // `maybeResumeInFlight`, both individually guarded.\n this.serverHydrationStarted = false\n this.rejoinedRunId = undefined\n }\n\n // ===========================\n // Getters\n // ===========================\n\n getResult(): TOutput | null {\n return this.result\n }\n\n getIsLoading(): boolean {\n return this.isLoading\n }\n\n getError(): Error | undefined {\n return this.error\n }\n\n getStatus(): GenerationClientState {\n return this.status\n }\n\n getResumeSnapshot(): GenerationResumeSnapshot | undefined {\n return this.resumeSnapshot\n ? {\n ...this.resumeSnapshot,\n ...(this.resumeSnapshot.pendingArtifacts\n ? { pendingArtifacts: [...this.resumeSnapshot.pendingArtifacts] }\n : {}),\n ...(this.resumeSnapshot.result\n ? {\n result: {\n ...this.resumeSnapshot.result,\n ...(this.resumeSnapshot.result.artifacts\n ? { artifacts: [...this.resumeSnapshot.result.artifacts] }\n : {}),\n },\n }\n : {}),\n ...(this.resumeSnapshot.error\n ? { error: { ...this.resumeSnapshot.error } }\n : {}),\n ...(this.resumeSnapshot.lastEvent\n ? { lastEvent: { ...this.resumeSnapshot.lastEvent } }\n : {}),\n }\n : undefined\n }\n\n // ===========================\n // Private state setters\n // ===========================\n\n private setResult(rawResult: TResult | null): void {\n if (rawResult === null) {\n this.result = null\n this.callbacksRef.onResultChange?.(null)\n this.devtoolsBridge.recordResultChange()\n return\n }\n\n if (this.callbacksRef.onResult) {\n const transformed = this.callbacksRef.onResult(rawResult)\n if (transformed === null) {\n // null return → keep previous result unchanged, just re-emit\n this.devtoolsBridge.emitState()\n return\n }\n if (transformed !== undefined) {\n // Non-null, non-undefined → use transformed value\n this.result = transformed\n this.callbacksRef.onResultChange?.(this.result)\n this.devtoolsBridge.recordResultChange()\n return\n }\n }\n\n // No onResult callback, or callback returned void → use raw value as\n // TOutput. When the caller did not supply an onResult transform,\n // `TOutput` defaults to `TResult`, so the runtime cast is sound.\n // oxlint-disable-next-line eslint-js/no-restricted-syntax -- TOutput defaults to TResult when no onResult transform is supplied\n this.result = rawResult as unknown as TOutput\n this.callbacksRef.onResultChange?.(this.result)\n this.devtoolsBridge.recordResultChange()\n }\n\n private setIsLoading(isLoading: boolean): void {\n this.isLoading = isLoading\n this.callbacksRef.onLoadingChange?.(isLoading)\n this.devtoolsBridge.recordLoadingChange()\n }\n\n private setError(error: Error | undefined): void {\n this.error = error\n this.callbacksRef.onErrorChange?.(error)\n this.devtoolsBridge.recordErrorChange(error)\n }\n\n private setStatus(status: GenerationClientState): void {\n this.status = status\n this.callbacksRef.onStatusChange?.(status)\n this.devtoolsBridge.recordStatusChange(status)\n }\n\n private setProgress(value: number, message?: string): void {\n this.progress = {\n value,\n ...(message ? { message } : {}),\n }\n if (message === undefined) {\n this.callbacksRef.onProgress?.(value)\n } else {\n this.callbacksRef.onProgress?.(value, message)\n }\n this.devtoolsBridge.recordProgressChange()\n }\n\n private createDevtoolsMetadata(\n metadata?: Partial<AIDevtoolsClientMetadata>,\n ): AIDevtoolsClientMetadata {\n return {\n hookName: metadata?.hookName ?? 'useGeneration',\n ...(metadata?.framework ? { framework: metadata.framework } : {}),\n ...(metadata?.outputKind ? { outputKind: metadata.outputKind } : {}),\n ...(metadata?.name ? { name: metadata.name } : {}),\n }\n }\n\n private generateUniqueId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(7)}`\n }\n\n private createRunContext(runId: string): RunAgentInputContext {\n return {\n threadId: this.threadId,\n runId,\n }\n }\n\n private observeResumeSnapshot(chunk: StreamChunk): void {\n this.resumeSnapshot = updateGenerationResumeSnapshot(\n this.resumeSnapshot,\n chunk,\n )\n this.notifyResumeSnapshotChanged()\n }\n\n /**\n * Notify the (internal) snapshot listener AND emit the public resume state.\n * The snapshot stays internal (persistence + devtools); the hook consumes\n * `resumeState`, mirroring the chat client.\n */\n private notifyResumeSnapshotChanged(): void {\n this.callbacksRef.onResumeSnapshotChange?.(this.resumeSnapshot)\n this.emitResumeState()\n }\n\n /**\n * Derive the public `resumeState` from the internal snapshot: the in-flight\n * run identity, with any in-flight artifact refs folded under it. `null` once\n * no run is in flight.\n *\n * The snapshot is rebuilt for every chunk, so emitting unconditionally would\n * hand each framework hook a fresh object per chunk and re-render the\n * component on every stream event. `resumeState` only changes at run\n * boundaries and when artifacts land, so skip the notification unless it\n * materially changed — same gate the persistence writes use.\n */\n private emitResumeState(): void {\n const snapshot = this.resumeSnapshot\n const state = snapshot?.resumeState\n const resumeState: GenerationResumeState | null = state\n ? {\n ...state,\n ...(snapshot?.pendingArtifacts && snapshot.pendingArtifacts.length > 0\n ? { pendingArtifacts: [...snapshot.pendingArtifacts] }\n : {}),\n }\n : null\n const signature = JSON.stringify(resumeState)\n if (signature === this.lastEmittedResumeState) {\n return\n }\n this.lastEmittedResumeState = signature\n this.callbacksRef.onResumeStateChange?.(resumeState)\n }\n\n /**\n * Repaint the normal fields from a restored snapshot (client store or server\n * hydrate), so a reload presents the run in `result` / `status` / `error`\n * exactly as a just-finished run would, never a bolt-on snapshot object.\n * `isLoading` stays false: the client never auto-tails a restored run. The\n * snapshot is not re-persisted here (it came from storage / the server).\n *\n * When the activity's mapper DECLINES a `complete` snapshot the repaint\n * settles as an error instead: `success` with a `null` result is a state no\n * consumer can render, and it hides the real cause (an output artifact\n * persisted without a serve URL). A decline on any other status is expected —\n * a `running` snapshot has no result yet, the rejoin will deliver it.\n */\n private repaintFromSnapshot(snapshot: GenerationResumeSnapshot): void {\n this.resumeSnapshot = snapshot\n this.notifyResumeSnapshotChanged()\n this.setStatus(clientStateFromResumeStatus(snapshot.status))\n this.setError(\n snapshot.error\n ? Object.assign(\n new Error(snapshot.error.message),\n snapshot.error.code ? { code: snapshot.error.code } : {},\n )\n : undefined,\n )\n const restored = this.reconstructRestoredResult(snapshot)\n if (restored !== null) {\n this.setResult(restored)\n } else if (\n this.callbacksRef.reconstructResult &&\n snapshot.status === 'complete'\n ) {\n this.reportUnrestorableResult()\n }\n }\n\n /**\n * Report a `complete` snapshot the activity's mapper could not rebuild.\n * Runs after the status/error repaint above, so it wins over the snapshot's\n * own `complete` status.\n */\n private reportUnrestorableResult(): void {\n const error = new Error(GENERATION_UNRESTORABLE_RESULT_MESSAGE)\n this.setStatus('error')\n this.setError(error)\n this.callbacksRef.onError?.(error)\n }\n\n /**\n * Repaint a restored snapshot (client store or server hydrate) and, when it\n * reports a run still in flight, tail that run to completion via `joinRun`\n * (from the connection, or the `joinRun` option when the transport can't\n * carry one).\n *\n * A `running` snapshot that no `joinRun` handler can tail is repainted as an\n * interrupted error instead of a `generating` status that would never\n * settle: an interrupted generation cannot be resumed, only re-run.\n */\n private repaintRestoredSnapshot(\n snapshot: GenerationResumeSnapshot,\n activeRunId?: string,\n ): void {\n if (snapshot.status !== 'running') {\n this.repaintFromSnapshot(snapshot)\n return\n }\n const joinRun = this.connection?.joinRun ?? this.joinRunHandler\n const runId = activeRunId ?? snapshot.resumeState?.runId\n if (runId && joinRun) {\n this.repaintFromSnapshot(snapshot)\n this.rejoinInFlight(runId)\n return\n }\n this.repaintFromSnapshot({\n ...snapshot,\n resumeState: null,\n status: 'error',\n error: {\n message:\n 'The previous generation was interrupted before it finished and cannot be resumed — generate again to retry.',\n },\n })\n }\n\n /**\n * Build the restorable result shape from the snapshot and hand it to the\n * per-activity `reconstructResult` mapper (injected by the specialized\n * client/hook, which knows the concrete result type).\n *\n * Returns `null` both when no mapper is set (nothing to rebuild — `result`\n * simply stays null) and when the mapper declines. The caller distinguishes\n * the two: see {@link repaintFromSnapshot}.\n */\n private reconstructRestoredResult(\n snapshot: GenerationResumeSnapshot,\n ): TResult | null {\n const build = this.callbacksRef.reconstructResult\n if (!build) return null\n const result = snapshot.result\n const restored: GenerationRestoredResult = {\n ...(result?.id !== undefined ? { id: result.id } : {}),\n ...(result?.model !== undefined ? { model: result.model } : {}),\n ...(result?.status !== undefined ? { status: result.status } : {}),\n ...(result?.providerJobId !== undefined\n ? { providerJobId: result.providerJobId }\n : {}),\n ...(result?.expiresAt !== undefined\n ? { expiresAt: result.expiresAt }\n : {}),\n ...(result?.text !== undefined ? { text: result.text } : {}),\n ...(result?.usage !== undefined ? { usage: result.usage } : {}),\n ...(snapshot.activity !== undefined\n ? { activity: snapshot.activity }\n : {}),\n artifacts: result?.artifacts ?? [],\n }\n return build(restored)\n }\n\n /**\n * The plain (non-Response) fetcher path never observes stream chunks, so\n * the terminal snapshot is built here from the fetcher's own result. A\n * stale `error` from a previous run is intentionally dropped — this run\n * succeeded.\n */\n private completePlainFetcherResumeSnapshot(rawResult: unknown): void {\n const previous = this.resumeSnapshot\n const result = createGenerationResultSnapshot(rawResult)\n this.resumeSnapshot = {\n schemaVersion: 1,\n resumeState: null,\n status: 'complete',\n ...(previous?.activity ? { activity: previous.activity } : {}),\n ...(previous?.pendingArtifacts && previous.pendingArtifacts.length > 0\n ? { pendingArtifacts: [...previous.pendingArtifacts] }\n : {}),\n ...(result\n ? { result }\n : previous?.result\n ? { result: { ...previous.result } }\n : {}),\n }\n this.notifyResumeSnapshotChanged()\n }\n\n /**\n * Records a transport-level failure (network drop, throwing callback) in\n * the snapshot. Without this, only a server-emitted RUN_ERROR chunk would\n * mark the snapshot `error`, leaving a persisted record that claims the\n * run is still in flight.\n */\n private recordResumeSnapshotError(error: Error): void {\n // Surface the failure on the OBSERVABLE fields FIRST: a rejoin (or live\n // stream) that emits RUN_ERROR has already flipped the snapshot to `error`\n // via `observeResumeSnapshot`, so the early-return below would otherwise\n // skip this and leave `status` stuck on `generating` — the run would look\n // like it is still going forever. The guard avoids a duplicate `error`\n // emission on the live `generate()` path, which sets the status itself.\n if (this.status !== 'error') this.setStatus('error')\n this.setError(error)\n if (this.resumeSnapshot?.status === 'error') return\n if (!this.resumeSnapshot && !this.serverDriven) return\n const previous = this.resumeSnapshot\n this.resumeSnapshot = {\n schemaVersion: 1,\n resumeState: null,\n status: 'error',\n ...(previous?.activity ? { activity: previous.activity } : {}),\n ...(previous?.pendingArtifacts && previous.pendingArtifacts.length > 0\n ? { pendingArtifacts: [...previous.pendingArtifacts] }\n : {}),\n ...(previous?.result ? { result: { ...previous.result } } : {}),\n error: { message: error.message },\n }\n this.notifyResumeSnapshotChanged()\n }\n\n /**\n * Drop the client's in-memory snapshot and re-emit. Purely local — this\n * client writes no storage, so nothing persisted is removed.\n */\n private clearResumeSnapshot(): void {\n this.resumeSnapshot = undefined\n this.lastEmittedResumeState = undefined\n this.notifyResumeSnapshotChanged()\n }\n\n /**\n * Server-driven mount hydration entry point (`persistence: true`). Runs at\n * most once, from the commit-phase mount path (`mountDevtools`) — never the\n * constructor / render phase — so remounts and speculative renders can't\n * re-fire the hydrate GET.\n */\n private maybeHydrateFromServer(): void {\n if (!this.serverDriven || this.serverHydrationStarted) return\n this.serverHydrationStarted = true\n if (this.connection?.hydrateGeneration ?? this.hydrateGenerationHandler) {\n this.hydrateFromServer()\n } else {\n // `persistence: true` without any hydrate source can never restore\n // anything — warn rather than silently no-op.\n console.warn(\n '[TanStack AI] `persistence: true` (server-driven) needs a `hydrateGeneration` handler — either a connection that implements one (e.g. `fetchServerSentEvents` / `fetchHttpStream`, or `stream()` / `rpcStream()` with persistence handlers) or the `hydrateGeneration` option. Without one, nothing is persisted or restored.',\n )\n }\n }\n\n /**\n * Server-driven mount hydration (`persistence: true`). The client holds no\n * local snapshot; on mount it asks the server — keyed by the stable threadId —\n * for the last generation's resume snapshot, validates it, and repaints it. It\n * never auto-starts a run, and never blocks: a `generate()` that starts first\n * owns the client and hydration backs off, mirroring the chat client.\n *\n * A genuine **miss** (the server reports no record for the thread) is silent —\n * a fresh thread is not an error. A genuine **failure** (transport error, a\n * 403 from the authorize gate, a malformed body, a record the client's own\n * validator rejects) is surfaced through `status` / `error` / `onError`, so a\n * broken server is distinguishable from an empty one and the app can retry.\n */\n private hydrateFromServer(): void {\n const hydrate =\n this.connection?.hydrateGeneration ?? this.hydrateGenerationHandler\n if (!hydrate) return\n // A send that already started owns the client; don't stomp it.\n if (this.resumeSnapshot || this.isLoading || this.status !== 'idle') return\n void (async () => {\n let res: GenerationHydrationResult\n try {\n res = await hydrate(this.threadId)\n } catch (cause) {\n this.failHydration(\n createGenerationHydrationError(\n 'the request to the server did not succeed',\n cause,\n ),\n )\n return\n }\n // No record for this thread — a fresh thread, not a failure.\n if (!res.resumeSnapshot) return\n const snapshot = parseGenerationResumeSnapshot(res.resumeSnapshot)\n if (!snapshot) {\n this.failHydration(\n createGenerationHydrationError(\n 'the server returned a record this client cannot read (unknown schema version, or a missing/invalid `status` or `resumeState`)',\n ),\n )\n return\n }\n // Re-check: a send may have started while the fetch was in flight.\n if (this.resumeSnapshot || this.isLoading || this.status !== 'idle')\n return\n // A run still generating on the server: re-attach and finish it in place.\n this.repaintRestoredSnapshot(snapshot, res.activeRun?.runId)\n })()\n }\n\n /**\n * Surface a hydration failure on the observable fields. Skipped when a\n * `generate()` took ownership while the hydrate GET was in flight — the live\n * run's state must win over a stale mount-time failure.\n */\n private failHydration(error: Error): void {\n if (this.resumeSnapshot || this.isLoading || this.status !== 'idle') return\n this.setStatus('error')\n this.setError(error)\n this.callbacksRef.onError?.(error)\n }\n\n /**\n * Re-attach to an already-loaded `running` snapshot (the remount case). Safe\n * to call repeatedly: `rejoinInFlight` dedupes on `rejoinedRunId` and bails\n * when a run is already in flight, so on the first mount (where the rejoin was\n * already started from `repaintRestoredSnapshot`) this is a no-op.\n */\n private maybeResumeInFlight(): void {\n if (this.resumeSnapshot?.status !== 'running') return\n const runId = this.resumeSnapshot.resumeState?.runId\n if (runId) this.rejoinInFlight(runId)\n }\n\n /**\n * Re-attach to a run that is still generating and stream it to completion,\n * mirroring the chat client's mount-time rejoin. Reuses `processStream`, so\n * `result` / `progress` / `status` repaint from the replayed chunks exactly as\n * a live run does. Best-effort: a live `generate()` owns the client and is\n * never stomped, and the same run is only rejoined once.\n */\n private rejoinInFlight(runId: string): void {\n const joinRun = this.connection?.joinRun ?? this.joinRunHandler\n if (!joinRun) return\n if (this.rejoinedRunId === runId) return\n // A fresh send (or an in-progress rejoin) owns the client.\n if (this.isLoading || this.abortController) return\n this.rejoinedRunId = runId\n const controller = new AbortController()\n this.abortController = controller\n this.setIsLoading(true)\n this.setStatus('generating')\n void (async () => {\n try {\n await this.processStream(\n joinRun(runId, controller.signal),\n runId,\n controller.signal,\n )\n } catch (error) {\n if (!controller.signal.aborted) {\n const failure =\n error instanceof Error ? error : new Error(String(error))\n // Settles `status`/`error` AND rewrites the snapshot to a terminal\n // `error` with a null `resumeState`, so the next mount does not\n // rejoin this run again.\n this.recordResumeSnapshotError(failure)\n this.callbacksRef.onError?.(failure)\n }\n } finally {\n // Only reset if this rejoin still owns the client: a `stop()` +\n // fresh `generate()` may have replaced the controller while the tail\n // was settling, and that live run owns `isLoading` now.\n if (this.abortController === controller) {\n this.abortController = null\n this.setIsLoading(false)\n }\n }\n })()\n }\n}\n\nfunction completeProgressValue(\n progress: AIDevtoolsGenerationProgress | null,\n): AIDevtoolsGenerationProgress | null {\n if (!progress) return null\n const message = progress.message\n return {\n value: 100,\n ...(message ? { message } : {}),\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { AudioGenerationResult, ImageGenerationResult, SummarizationResult, TranscriptionResult, TTSResult } from '@tanstack/ai';
2
+ import { GenerationRestoredResult } from './generation-types.js';
3
+ /** image → `{ id, model, images: [{ url }], artifacts }`. */
4
+ export declare function reconstructImageResult(restored: GenerationRestoredResult): ImageGenerationResult | null;
5
+ /**
6
+ * tts → `{ id, model, audio: '', format, contentType, artifacts }`.
7
+ *
8
+ * Unlike {@link reconstructAudioResult}, `TTSResult.audio` is a bare base64
9
+ * string with no URL slot, and server-driven persistence never stores the raw
10
+ * bytes — only the durable serve URL on the artifact ref. So the restored
11
+ * result surfaces the audio through `artifacts` (each carrying `url`); consumers
12
+ * play the restored clip from `result.artifacts[0].url` and fall back to the
13
+ * live base64 `audio` only for a just-finished (non-restored) run.
14
+ */
15
+ export declare function reconstructSpeechResult(restored: GenerationRestoredResult): TTSResult | null;
16
+ /** audio → `{ id, model, audio: { url }, artifacts }`. */
17
+ export declare function reconstructAudioResult(restored: GenerationRestoredResult): AudioGenerationResult | null;
18
+ /** transcription → `{ id, model, text, artifacts }`. */
19
+ export declare function reconstructTranscriptionResult(restored: GenerationRestoredResult): TranscriptionResult | null;
20
+ /** summarize → `{ id, model, summary, usage }` (needs persisted `usage`). */
21
+ export declare function reconstructSummarizeResult(restored: GenerationRestoredResult): SummarizationResult | null;
@@ -0,0 +1,85 @@
1
+ //#region src/generation-reconstruct.ts
2
+ /**
3
+ * Per-activity `reconstructResult` mappers. On mount restore the generic
4
+ * `GenerationClient` hands each specialized hook a {@link GenerationRestoredResult}
5
+ * (the metadata that survived persistence plus the durable artifact refs, each
6
+ * carrying its serve `url`); the mapper rebuilds the concrete typed result so
7
+ * `result` repaints as if the run had just finished, with media resolved to the
8
+ * durable serve route rather than the provider's expired link.
9
+ *
10
+ * A mapper returns `null` when the snapshot cannot rebuild a valid result; then
11
+ * `result` stays null while `status` / `error` / `resumeState` still repaint.
12
+ */
13
+ /** Output artifact refs of a given media type that carry a durable serve URL. */
14
+ function mediaUrls(restored, mediaType) {
15
+ return restored.artifacts.filter((a) => a.role === "output" && a.source.mediaType === mediaType && a.url != null).map((a) => a.url);
16
+ }
17
+ /** image → `{ id, model, images: [{ url }], artifacts }`. */
18
+ function reconstructImageResult(restored) {
19
+ const urls = mediaUrls(restored, "image");
20
+ if (urls.length === 0) return null;
21
+ return {
22
+ id: restored.id ?? "",
23
+ model: restored.model ?? "",
24
+ images: urls.map((url) => ({ url })),
25
+ ...restored.artifacts.length > 0 ? { artifacts: restored.artifacts } : {}
26
+ };
27
+ }
28
+ /**
29
+ * tts → `{ id, model, audio: '', format, contentType, artifacts }`.
30
+ *
31
+ * Unlike {@link reconstructAudioResult}, `TTSResult.audio` is a bare base64
32
+ * string with no URL slot, and server-driven persistence never stores the raw
33
+ * bytes — only the durable serve URL on the artifact ref. So the restored
34
+ * result surfaces the audio through `artifacts` (each carrying `url`); consumers
35
+ * play the restored clip from `result.artifacts[0].url` and fall back to the
36
+ * live base64 `audio` only for a just-finished (non-restored) run.
37
+ */
38
+ function reconstructSpeechResult(restored) {
39
+ const ref = restored.artifacts.find((a) => a.role === "output" && a.source.mediaType === "audio" && a.url != null);
40
+ if (!ref) return null;
41
+ const contentType = ref.mimeType || void 0;
42
+ return {
43
+ id: restored.id ?? "",
44
+ model: restored.model ?? "",
45
+ audio: "",
46
+ format: contentType?.split("/")[1] ?? "",
47
+ ...contentType ? { contentType } : {},
48
+ artifacts: restored.artifacts
49
+ };
50
+ }
51
+ /** audio → `{ id, model, audio: { url }, artifacts }`. */
52
+ function reconstructAudioResult(restored) {
53
+ const [url] = mediaUrls(restored, "audio");
54
+ if (!url) return null;
55
+ return {
56
+ id: restored.id ?? "",
57
+ model: restored.model ?? "",
58
+ audio: { url },
59
+ ...restored.artifacts.length > 0 ? { artifacts: restored.artifacts } : {}
60
+ };
61
+ }
62
+ /** transcription → `{ id, model, text, artifacts }`. */
63
+ function reconstructTranscriptionResult(restored) {
64
+ if (restored.text === void 0) return null;
65
+ return {
66
+ id: restored.id ?? "",
67
+ model: restored.model ?? "",
68
+ text: restored.text,
69
+ ...restored.artifacts.length > 0 ? { artifacts: restored.artifacts } : {}
70
+ };
71
+ }
72
+ /** summarize → `{ id, model, summary, usage }` (needs persisted `usage`). */
73
+ function reconstructSummarizeResult(restored) {
74
+ if (restored.text === void 0 || restored.usage === void 0) return null;
75
+ return {
76
+ id: restored.id ?? "",
77
+ model: restored.model ?? "",
78
+ summary: restored.text,
79
+ usage: restored.usage
80
+ };
81
+ }
82
+ //#endregion
83
+ export { reconstructAudioResult, reconstructImageResult, reconstructSpeechResult, reconstructSummarizeResult, reconstructTranscriptionResult };
84
+
85
+ //# sourceMappingURL=generation-reconstruct.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generation-reconstruct.js","names":[],"sources":["../../src/generation-reconstruct.ts"],"sourcesContent":["import type {\n AudioGenerationResult,\n ImageGenerationResult,\n PersistedArtifactRef,\n SummarizationResult,\n TranscriptionResult,\n TTSResult,\n} from '@tanstack/ai'\nimport type { GenerationRestoredResult } from './generation-types'\n\n/**\n * Per-activity `reconstructResult` mappers. On mount restore the generic\n * `GenerationClient` hands each specialized hook a {@link GenerationRestoredResult}\n * (the metadata that survived persistence plus the durable artifact refs, each\n * carrying its serve `url`); the mapper rebuilds the concrete typed result so\n * `result` repaints as if the run had just finished, with media resolved to the\n * durable serve route rather than the provider's expired link.\n *\n * A mapper returns `null` when the snapshot cannot rebuild a valid result; then\n * `result` stays null while `status` / `error` / `resumeState` still repaint.\n */\n\n/** Output artifact refs of a given media type that carry a durable serve URL. */\nfunction mediaUrls(\n restored: GenerationRestoredResult,\n mediaType: PersistedArtifactRef['source']['mediaType'],\n): Array<string> {\n return restored.artifacts\n .filter(\n (a) =>\n a.role === 'output' &&\n a.source.mediaType === mediaType &&\n a.url != null,\n )\n .map((a) => a.url as string)\n}\n\n/** image → `{ id, model, images: [{ url }], artifacts }`. */\nexport function reconstructImageResult(\n restored: GenerationRestoredResult,\n): ImageGenerationResult | null {\n const urls = mediaUrls(restored, 'image')\n if (urls.length === 0) return null\n return {\n id: restored.id ?? '',\n model: restored.model ?? '',\n images: urls.map((url) => ({ url })),\n ...(restored.artifacts.length > 0 ? { artifacts: restored.artifacts } : {}),\n }\n}\n\n/**\n * tts → `{ id, model, audio: '', format, contentType, artifacts }`.\n *\n * Unlike {@link reconstructAudioResult}, `TTSResult.audio` is a bare base64\n * string with no URL slot, and server-driven persistence never stores the raw\n * bytes — only the durable serve URL on the artifact ref. So the restored\n * result surfaces the audio through `artifacts` (each carrying `url`); consumers\n * play the restored clip from `result.artifacts[0].url` and fall back to the\n * live base64 `audio` only for a just-finished (non-restored) run.\n */\nexport function reconstructSpeechResult(\n restored: GenerationRestoredResult,\n): TTSResult | null {\n const ref = restored.artifacts.find(\n (a) =>\n a.role === 'output' && a.source.mediaType === 'audio' && a.url != null,\n )\n if (!ref) return null\n const contentType = ref.mimeType || undefined\n return {\n id: restored.id ?? '',\n model: restored.model ?? '',\n // Bytes live in the blob store, served at `ref.url`; the base64 field can't\n // be rebuilt from the snapshot, so it stays empty on restore.\n audio: '',\n format: contentType?.split('/')[1] ?? '',\n ...(contentType ? { contentType } : {}),\n artifacts: restored.artifacts,\n }\n}\n\n/** audio → `{ id, model, audio: { url }, artifacts }`. */\nexport function reconstructAudioResult(\n restored: GenerationRestoredResult,\n): AudioGenerationResult | null {\n const [url] = mediaUrls(restored, 'audio')\n if (!url) return null\n return {\n id: restored.id ?? '',\n model: restored.model ?? '',\n audio: { url },\n ...(restored.artifacts.length > 0 ? { artifacts: restored.artifacts } : {}),\n }\n}\n\n/** transcription → `{ id, model, text, artifacts }`. */\nexport function reconstructTranscriptionResult(\n restored: GenerationRestoredResult,\n): TranscriptionResult | null {\n if (restored.text === undefined) return null\n return {\n id: restored.id ?? '',\n model: restored.model ?? '',\n text: restored.text,\n ...(restored.artifacts.length > 0 ? { artifacts: restored.artifacts } : {}),\n }\n}\n\n/** summarize → `{ id, model, summary, usage }` (needs persisted `usage`). */\nexport function reconstructSummarizeResult(\n restored: GenerationRestoredResult,\n): SummarizationResult | null {\n if (restored.text === undefined || restored.usage === undefined) return null\n return {\n id: restored.id ?? '',\n model: restored.model ?? '',\n summary: restored.text,\n usage: restored.usage,\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAuBA,SAAS,UACP,UACA,WACe;CACf,OAAO,SAAS,UACb,QACE,MACC,EAAE,SAAS,YACX,EAAE,OAAO,cAAc,aACvB,EAAE,OAAO,IACb,CAAC,CACA,KAAK,MAAM,EAAE,GAAa;AAC/B;;AAGA,SAAgB,uBACd,UAC8B;CAC9B,MAAM,OAAO,UAAU,UAAU,OAAO;CACxC,IAAI,KAAK,WAAW,GAAG,OAAO;CAC9B,OAAO;EACL,IAAI,SAAS,MAAM;EACnB,OAAO,SAAS,SAAS;EACzB,QAAQ,KAAK,KAAK,SAAS,EAAE,IAAI,EAAE;EACnC,GAAI,SAAS,UAAU,SAAS,IAAI,EAAE,WAAW,SAAS,UAAU,IAAI,CAAC;CAC3E;AACF;;;;;;;;;;;AAYA,SAAgB,wBACd,UACkB;CAClB,MAAM,MAAM,SAAS,UAAU,MAC5B,MACC,EAAE,SAAS,YAAY,EAAE,OAAO,cAAc,WAAW,EAAE,OAAO,IACtE;CACA,IAAI,CAAC,KAAK,OAAO;CACjB,MAAM,cAAc,IAAI,YAAY,KAAA;CACpC,OAAO;EACL,IAAI,SAAS,MAAM;EACnB,OAAO,SAAS,SAAS;EAGzB,OAAO;EACP,QAAQ,aAAa,MAAM,GAAG,CAAC,CAAC,MAAM;EACtC,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;EACrC,WAAW,SAAS;CACtB;AACF;;AAGA,SAAgB,uBACd,UAC8B;CAC9B,MAAM,CAAC,OAAO,UAAU,UAAU,OAAO;CACzC,IAAI,CAAC,KAAK,OAAO;CACjB,OAAO;EACL,IAAI,SAAS,MAAM;EACnB,OAAO,SAAS,SAAS;EACzB,OAAO,EAAE,IAAI;EACb,GAAI,SAAS,UAAU,SAAS,IAAI,EAAE,WAAW,SAAS,UAAU,IAAI,CAAC;CAC3E;AACF;;AAGA,SAAgB,+BACd,UAC4B;CAC5B,IAAI,SAAS,SAAS,KAAA,GAAW,OAAO;CACxC,OAAO;EACL,IAAI,SAAS,MAAM;EACnB,OAAO,SAAS,SAAS;EACzB,MAAM,SAAS;EACf,GAAI,SAAS,UAAU,SAAS,IAAI,EAAE,WAAW,SAAS,UAAU,IAAI,CAAC;CAC3E;AACF;;AAGA,SAAgB,2BACd,UAC4B;CAC5B,IAAI,SAAS,SAAS,KAAA,KAAa,SAAS,UAAU,KAAA,GAAW,OAAO;CACxE,OAAO;EACL,IAAI,SAAS,MAAM;EACnB,OAAO,SAAS,SAAS;EACzB,SAAS,SAAS;EAClB,OAAO,SAAS;CAClB;AACF"}
@@ -1,5 +1,5 @@
1
- import { MediaPrompt, StreamChunk } from '@tanstack/ai/client';
2
- import { TranscriptionResponseFormat } from '@tanstack/ai';
1
+ import { MediaPrompt, PersistedArtifactRef, StreamChunk } from '@tanstack/ai/client';
2
+ import { TokenUsage, TranscriptionResponseFormat } from '@tanstack/ai';
3
3
  import { ConnectConnectionAdapter } from './connection-adapters.js';
4
4
  import { AIDevtoolsClientMetadata } from './devtools.js';
5
5
  import { GenerationDevtoolsBridgeFactory, VideoDevtoolsBridgeFactory } from './devtools-noop.js';
@@ -37,6 +37,163 @@ export type InferGenerationOutput<TResult, TFn> = TFn extends (result: any) => i
37
37
  * Simpler than ChatClientState since generation is a single request/response cycle.
38
38
  */
39
39
  export type GenerationClientState = 'idle' | 'generating' | 'success' | 'error';
40
+ /**
41
+ * Status of a persisted/restored generation run.
42
+ *
43
+ * `running` / `complete` / `error` are the three the server-side mapper emits
44
+ * over the wire. `idle` is client-local only: `stop()` rewrites a `running`
45
+ * snapshot to it so a cancelled run is no longer resumable.
46
+ *
47
+ * @internal
48
+ */
49
+ export type GenerationResumeStatus = 'idle' | 'running' | 'complete' | 'error';
50
+ /**
51
+ * Thrown when a generation stream ends without a terminal `RUN_FINISHED` /
52
+ * `RUN_ERROR` chunk — a proxy/load-balancer idle timeout, a server restart
53
+ * mid-run, or a durable log whose terminal append never landed. The run's
54
+ * outcome is unknowable from the client, so it settles as an error rather than
55
+ * leaving the client stuck on `generating` forever.
56
+ */
57
+ export declare const GENERATION_STREAM_TRUNCATED_MESSAGE = "The generation stream ended before the run finished (no RUN_FINISHED or RUN_ERROR was received) \u2014 the connection was interrupted. Generate again to retry.";
58
+ /**
59
+ * Reported when a restored snapshot says the run completed but the activity's
60
+ * `reconstructResult` mapper cannot rebuild a result from it — typically an
61
+ * output artifact persisted without a serve `url`. Surfacing it beats a
62
+ * `success` status with a `null` result, which no consumer can render.
63
+ */
64
+ export declare const GENERATION_UNRESTORABLE_RESULT_MESSAGE = "The stored generation completed but its result could not be rebuilt from the persisted record (its output artifact carries no serve URL, or the fields this activity needs were not persisted). Generate again to produce a fresh result.";
65
+ /**
66
+ * Wrap a mount-hydration failure with context. Genuine failures (transport
67
+ * error, a 403 from the authorize gate, an unparseable body, a record the
68
+ * client's validator rejects) must reach the app; only a genuine miss — the
69
+ * server reporting no record for the thread — stays silent.
70
+ */
71
+ export declare function createGenerationHydrationError(detail: string, cause?: unknown): Error;
72
+ /**
73
+ * Map a persisted resume status to the client's live state machine on restore:
74
+ * complete → success, error → error, running → generating, idle → idle. A
75
+ * restored `running` only reaches this mapping when a `joinRun` handler can
76
+ * tail the run to completion; without one the client rewrites the snapshot to
77
+ * `error` (interrupted) before repainting, so it never sticks on `generating`.
78
+ */
79
+ export declare function clientStateFromResumeStatus(status: GenerationResumeStatus): GenerationClientState;
80
+ /** @internal */
81
+ export interface GenerationResumeState {
82
+ threadId: string;
83
+ runId: string;
84
+ /**
85
+ * Artifact refs observed while the run is still in flight. Non-null only while
86
+ * a run is streaming (`resumeState` itself is null once it ends); the final
87
+ * refs move onto `result.artifacts` when the run completes.
88
+ */
89
+ pendingArtifacts?: Array<PersistedArtifactRef>;
90
+ }
91
+ /** @internal */
92
+ export interface GenerationResultSnapshot {
93
+ id?: string;
94
+ model?: string;
95
+ status?: string;
96
+ /**
97
+ * The provider's async job handle (e.g. a Veo/fal video job id used for
98
+ * status polling) — NOT the generation's own `runId`, which lives on
99
+ * {@link GenerationResumeState.runId}.
100
+ */
101
+ providerJobId?: string;
102
+ expiresAt?: string;
103
+ /**
104
+ * The text output of a text activity (a transcription's `text` or a summary's
105
+ * `summary`). Persisted so a text generation restores its result on reload
106
+ * (text is small and not bytes). Absent for media activities, whose output
107
+ * restores from `artifacts`.
108
+ */
109
+ text?: string;
110
+ /** Token usage, persisted so a text result that requires it can be rebuilt. */
111
+ usage?: TokenUsage;
112
+ artifacts?: Array<PersistedArtifactRef>;
113
+ }
114
+ /** @internal */
115
+ export interface GenerationErrorSnapshot {
116
+ message: string;
117
+ code?: string;
118
+ }
119
+ /** @internal */
120
+ export interface GenerationEventSnapshot {
121
+ type: StreamChunk['type'];
122
+ name?: string;
123
+ timestamp?: number;
124
+ }
125
+ /** @internal */
126
+ export interface GenerationResumeSnapshot {
127
+ /**
128
+ * Version of the snapshot shape. Written on every snapshot the client builds
129
+ * so future shape changes can migrate (or reject) an older record hydrated
130
+ * from the server. Absent means `1`.
131
+ */
132
+ schemaVersion?: 1;
133
+ resumeState: GenerationResumeState | null;
134
+ status: GenerationResumeStatus;
135
+ activity?: PersistedArtifactRef['source']['activity'];
136
+ pendingArtifacts?: Array<PersistedArtifactRef>;
137
+ result?: GenerationResultSnapshot;
138
+ error?: GenerationErrorSnapshot;
139
+ lastEvent?: GenerationEventSnapshot;
140
+ }
141
+ /**
142
+ * The `persistence` / `threadId` / `id` identity shared by every generation hook.
143
+ *
144
+ * Turning persistence on **requires** a `threadId`, the stable scope runs are
145
+ * filed under. Without one the client would hydrate by a generated id that
146
+ * changes every reload, so nothing would ever restore; making it a type error
147
+ * means the compiler asks for the scope instead of the runtime inventing one.
148
+ *
149
+ * `threadId` is the single identity for the hook, the AG-UI wire thread, and
150
+ * persistence. Legacy `id` is deprecated and typed `never` whenever
151
+ * `threadId` is supplied — pass one scope, not two.
152
+ *
153
+ * Ephemeral generations (no `persistence`, or `persistence: false`) may still
154
+ * pass a deprecated `id` when they have no `threadId`, as a wire/devtools
155
+ * fallback. Prefer giving them a `threadId` instead.
156
+ *
157
+ * USAGE: intersect this onto a hook's parameter and subtract the three keys
158
+ * from the options interface, leaving that interface a plain (non-union)
159
+ * object so `Pick` / `Omit` composition elsewhere keeps working:
160
+ *
161
+ * ```ts
162
+ * options: Omit<UseGenerateImageOptions, 'onResult' | 'persistence' | 'threadId' | 'id'> & {
163
+ * onResult?: (result: ImageGenerationResult) => TTransformed
164
+ * } & GenerationPersistenceOptions
165
+ * ```
166
+ *
167
+ * Do NOT bake the union into the options interface itself: a later plain `Omit`
168
+ * over a union collapses it to a single object type and the requirement
169
+ * silently disappears. `use-generation-persistence-types.test.ts` pins this.
170
+ */
171
+ export type GenerationPersistenceOptions = {
172
+ persistence: true;
173
+ /** Required by `persistence` — the stable scope runs are filed under. */
174
+ threadId: string;
175
+ /**
176
+ * @deprecated Prefer `threadId`. Not allowed when `threadId` is set —
177
+ * `threadId` is the single identity for the hook, the wire, and persistence.
178
+ */
179
+ id?: never;
180
+ } | {
181
+ persistence?: false | undefined;
182
+ /** Stable scope for the generation slot (also the wire / devtools identity). */
183
+ threadId: string;
184
+ /**
185
+ * @deprecated Prefer `threadId`. Not allowed when `threadId` is set.
186
+ */
187
+ id?: never;
188
+ } | {
189
+ persistence?: false | undefined;
190
+ threadId?: undefined;
191
+ /**
192
+ * @deprecated Prefer `threadId` as the single identity. Only allowed when
193
+ * `threadId` is omitted — legacy wire/devtools fallback for ephemeral runs.
194
+ */
195
+ id?: string;
196
+ };
40
197
  /**
41
198
  * Well-known CUSTOM event names used by generation clients.
42
199
  * These events are emitted by the server-side streaming helpers
@@ -45,6 +202,8 @@ export type GenerationClientState = 'idle' | 'generating' | 'success' | 'error';
45
202
  export declare const GENERATION_EVENTS: {
46
203
  /** The generation result payload */
47
204
  readonly RESULT: "generation:result";
205
+ /** Persisted artifact refs for generated media */
206
+ readonly ARTIFACTS: "generation:artifacts";
48
207
  /** Progress update (0-100) with optional message */
49
208
  readonly PROGRESS: "generation:progress";
50
209
  /** Video job created with jobId */
@@ -89,12 +248,80 @@ export type GenerationTransport<TInput, TResult> = {
89
248
  * @template TOutput - The output type after optional transform (defaults to TResult)
90
249
  */
91
250
  export interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {
92
- /** Unique identifier for this generation client instance */
251
+ /**
252
+ * @deprecated Prefer {@link GenerationClientOptions.threadId}. Legacy instance
253
+ * id used only as a wire/devtools fallback when `threadId` is omitted. When
254
+ * both are passed, `threadId` wins and `id` is ignored. Framework hooks type
255
+ * `id` as `never` whenever `threadId` is set — see
256
+ * {@link GenerationPersistenceOptions}.
257
+ */
93
258
  id?: string;
259
+ /**
260
+ * The **scope** this generation belongs to: a stable, app-chosen name for the
261
+ * slot successive runs fill, not a link to a chat conversation. This is the
262
+ * single identity for the client — wire thread id, devtools hook id, and
263
+ * persistence key.
264
+ *
265
+ * A generation hook starts empty and produces many runs over its life — each
266
+ * run gets its own `runId`, but they all belong to one scope. Persistence
267
+ * keys on this: server-driven hydrates the last run for it on mount. It is
268
+ * also sent as the AG-UI thread id on the wire, since the protocol requires
269
+ * one.
270
+ *
271
+ * Derive it from your own domain — it must be meaningful before any media
272
+ * exists and identical after a reload:
273
+ *
274
+ * ```ts
275
+ * threadId: `video-${videoId}-start-frame`
276
+ * ```
277
+ *
278
+ * **Required whenever `persistence` is set.** An app that cannot name the
279
+ * scope has nothing to restore *to*, and a generated fallback would key each
280
+ * reload differently — silently restoring nothing. Optional only for
281
+ * ephemeral runs, where it falls back to deprecated `id` (or a generated id)
282
+ * purely to satisfy the wire and nothing is written.
283
+ */
284
+ threadId?: string;
94
285
  /** Additional body parameters to send with connect-based adapter requests */
95
286
  body?: Record<string, any>;
96
287
  /** Metadata used to register this generation hook with TanStack AI Devtools */
97
288
  devtools?: Partial<AIDevtoolsClientMetadata>;
289
+ /**
290
+ * How this generation persists across reloads.
291
+ *
292
+ * - Omit or `false`: ephemeral, in-memory only.
293
+ * - `true`: server-driven. On mount the client hydrates the last generation
294
+ * for its `threadId` from the server (needs a `hydrateGeneration` handler,
295
+ * from the connection or the option below) and repaints that snapshot. It
296
+ * never auto-starts a run.
297
+ *
298
+ * The record lives on the server, written by `withGenerationPersistence`. The
299
+ * browser caches nothing, so a generation's history is never duplicated into
300
+ * client storage.
301
+ */
302
+ persistence?: boolean;
303
+ /**
304
+ * Server-driven hydration handler, for transports that don't carry one on
305
+ * the connection: supply it alongside `fetcher` (or a `stream()` /
306
+ * `rpcStream()` connection built without handlers) so `persistence: true`
307
+ * can restore the last generation for `threadId` on mount. Typically a
308
+ * one-line TanStack Start server-function call backed by
309
+ * `getGenerationHydration` from `@tanstack/ai-persistence`.
310
+ *
311
+ * A connection's own `hydrateGeneration` takes precedence when both exist.
312
+ */
313
+ hydrateGeneration?: ConnectConnectionAdapter['hydrateGeneration'];
314
+ /**
315
+ * Re-attach handler for a run that is still generating, for transports that
316
+ * don't carry one on the connection. The client tails this on mount when a
317
+ * restored/hydrated snapshot reports a run in flight, replaying it to
318
+ * completion in place. Without it, a restored `running` snapshot surfaces
319
+ * as an (interrupted) error — an interrupted generation cannot be resumed,
320
+ * only re-run.
321
+ *
322
+ * A connection's own `joinRun` takes precedence when both exist.
323
+ */
324
+ joinRun?: ConnectConnectionAdapter['joinRun'];
98
325
  /**
99
326
  * Factory that constructs the devtools bridge. Default is a no-op
100
327
  * factory; the real implementation lives in `@tanstack/ai-client/devtools`.
@@ -123,7 +350,62 @@ export interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {
123
350
  onErrorChange?: (error: Error | undefined) => void;
124
351
  /** @internal Called when generation status changes */
125
352
  onStatusChange?: (status: GenerationClientState) => void;
353
+ /** @internal Called when lightweight resume snapshot changes. Receives `undefined` when the snapshot is cleared by `reset()`. */
354
+ onResumeSnapshotChange?: (snapshot: GenerationResumeSnapshot | undefined) => void;
355
+ /** @internal Called when the in-flight run identity changes. `null` once no run is in flight. Mirrors the chat client's resume-state callback. */
356
+ onResumeStateChange?: (resumeState: GenerationResumeState | null) => void;
357
+ /**
358
+ * @internal Rebuild a typed result from a restored snapshot, injected by each
359
+ * specialized client/hook (which knows the concrete result shape). Called on
360
+ * mount restore (client store or server hydrate) so `result` repaints as if the
361
+ * run had just finished, with media resolved to the durable serve URL. Returns
362
+ * `null` when the snapshot cannot rebuild a result (then `result` stays null;
363
+ * `status` / `error` / `resumeState` still repaint).
364
+ */
365
+ reconstructResult?: (restored: GenerationRestoredResult) => TResult | null;
126
366
  }
367
+ /**
368
+ * The restorable shape handed to a client's `reconstructResult` mapper: the
369
+ * result metadata that survived persistence plus the durable artifact refs (each
370
+ * carrying its serve {@link PersistedArtifactRef.url}). The specialized client
371
+ * turns this into its own typed result (image `images`, video `url`, text
372
+ * `text`, ...).
373
+ */
374
+ export interface GenerationRestoredResult {
375
+ id?: string;
376
+ model?: string;
377
+ status?: string;
378
+ /** The provider's async job handle — see {@link GenerationResultSnapshot.providerJobId}. */
379
+ providerJobId?: string;
380
+ expiresAt?: string;
381
+ text?: string;
382
+ usage?: TokenUsage;
383
+ activity?: PersistedArtifactRef['source']['activity'];
384
+ artifacts: Array<PersistedArtifactRef>;
385
+ }
386
+ /**
387
+ * Reduces one observed stream chunk into the lightweight resume snapshot.
388
+ *
389
+ * A `RUN_STARTED` chunk begins a fresh run, so stale `result` / `error` /
390
+ * `pendingArtifacts` from a previous run are dropped rather than carried into
391
+ * the new run's snapshot.
392
+ *
393
+ * @internal
394
+ */
395
+ export declare function updateGenerationResumeSnapshot(previous: GenerationResumeSnapshot | null | undefined, chunk: StreamChunk): GenerationResumeSnapshot;
396
+ /**
397
+ * Validates an untrusted value (a hydration body resolved by the server) into a
398
+ * {@link GenerationResumeSnapshot}, or returns `undefined` when the value is
399
+ * not a usable snapshot.
400
+ *
401
+ * A hydrated record is outside the type system: it may be stale, truncated, or
402
+ * written by a different version. Every field is re-validated with the same
403
+ * narrowing the live chunk reducer uses. `lastEvent` is not restored, since it
404
+ * describes a transient stream position with no meaning after a reload.
405
+ *
406
+ * @internal
407
+ */
408
+ export declare function parseGenerationResumeSnapshot(value: unknown): GenerationResumeSnapshot | undefined;
127
409
  /**
128
410
  * Video status information returned during job polling.
129
411
  */
@@ -151,6 +433,8 @@ export interface VideoGenerateResult {
151
433
  url: string;
152
434
  /** When the URL expires, if applicable */
153
435
  expiresAt?: Date;
436
+ /** Persisted artifact references for generated assets, when available */
437
+ artifacts?: Array<PersistedArtifactRef>;
154
438
  }
155
439
  /**
156
440
  * Options for the VideoGenerationClient.
@@ -260,3 +544,5 @@ export interface VideoGenerateInput {
260
544
  /** Model-specific options */
261
545
  modelOptions?: Record<string, any>;
262
546
  }
547
+ /** @internal Narrows an untrusted result payload into the persisted result snapshot shape. */
548
+ export declare function createGenerationResultSnapshot(value: unknown): GenerationResultSnapshot | undefined;