@tanstack/ai-client 0.25.2 → 0.26.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.
@@ -1,6 +1,7 @@
1
1
  import { createNoOpGenerationDevtoolsBridge } from "./devtools-noop.js";
2
2
  import { GENERATION_EVENTS, GENERATION_STREAM_TRUNCATED_MESSAGE, GENERATION_UNRESTORABLE_RESULT_MESSAGE, clientStateFromResumeStatus, createGenerationHydrationError, createGenerationResultSnapshot, parseGenerationResumeSnapshot, updateGenerationResumeSnapshot } from "./generation-types.js";
3
3
  import { parseSSEResponse } from "./sse-parser.js";
4
+ import { restoreInboundChunk } from "@tanstack/ai/client";
4
5
  //#region src/generation-client.ts
5
6
  /**
6
7
  * A lightweight, generic client for one-shot generation tasks
@@ -194,8 +195,9 @@ var GenerationClient = class {
194
195
  async processStream(source, fallbackRunId, signal) {
195
196
  let streamRunId;
196
197
  let sawTerminalChunk = false;
197
- for await (const chunk of source) {
198
+ for await (const raw of source) {
198
199
  if (signal.aborted) break;
200
+ const chunk = restoreInboundChunk(raw);
199
201
  this.callbacksRef.onChunk?.(chunk);
200
202
  this.observeResumeSnapshot(chunk);
201
203
  const chunkRunId = "runId" in chunk && typeof chunk.runId === "string" ? chunk.runId : void 0;
@@ -220,7 +222,7 @@ var GenerationClient = class {
220
222
  break;
221
223
  case "RUN_ERROR": {
222
224
  this.devtoolsBridge.ensureRunStarted(chunkRunId ?? streamRunId ?? fallbackRunId);
223
- const msg = chunk.message || chunk.error?.message || "An error occurred";
225
+ const msg = chunk.message || "An error occurred";
224
226
  throw new Error(msg);
225
227
  }
226
228
  }
@@ -1 +1 @@
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 GenerationPersistenceOptions,\n GenerationRestoredResult,\n GenerationResumeSnapshot,\n GenerationResumeState,\n GenerationTransport,\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 uniqueId: string\n private readonly devtoolsMetadata: AIDevtoolsClientMetadata\n private readonly devtoolsBridge: GenerationDevtoolsBridge<TOutput>\n private 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: Omit<\n GenerationClientOptions<TInput, TResult, TOutput>,\n 'persistence' | 'threadId'\n > &\n GenerationPersistenceOptions &\n GenerationTransport<TInput, TResult>,\n ) {\n // `threadId` is the only identity. Do not mint a random id during\n // construct: hooks build this client during render.\n this.threadId = options.threadId ?? ''\n this.uniqueId = this.threadId\n // The persistence scope is the explicit `threadId` and nothing else.\n // The types require it whenever `persistence` is set. This field keeps a\n // generated wire id from 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 const client = this\n return {\n get hookId() {\n return client.uniqueId\n },\n get clientId() {\n return client.uniqueId\n },\n get threadId() {\n return client.threadId\n },\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 this.ensureThreadId()\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 ensureThreadId(): string {\n if (!this.threadId) {\n this.threadId = this.generateUniqueId('generation')\n }\n this.uniqueId = this.threadId\n return this.threadId\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgGA,IAAa,mBAAb,MAIE;CACA;CACA;CAIA;CAGA;CAGA;CACA;CACA;CACA;CACA;CAGA,eAAyC;CACzC;CACA,SAAiC;CACjC,QAA+B;CAC/B,WAAwD;CACxD,YAAoB;CACpB,QAAmC,KAAA;CACnC,SAAwC;CACxC;CACA;CACA,kBAAkD;CAClD;CACA;CACA,kBAA0B;CAC1B,WAAmB;CACnB,yBAAiC;CAEjC,YACE,SAMA;EAGA,KAAK,WAAW,QAAQ,YAAY;EACpC,KAAK,WAAW,KAAK;EAIrB,KAAK,mBAAmB,QAAQ;EAChC,KAAK,aAAa,QAAQ;EAC1B,KAAK,UAAU,QAAQ;EACvB,KAAK,2BAA2B,QAAQ;EACxC,KAAK,iBAAiB,QAAQ;EAC9B,KAAK,OAAO,QAAQ,QAAQ,CAAC;EAG7B,KAAK,eAAe,QAAQ,gBAAgB;EAK5C,IAAI,QAAQ,eAAe,CAAC,KAAK,kBAC/B,QAAQ,KACN,iMACF;EAEF,KAAK,eAAe;GAClB,UAAU,QAAQ;GAClB,SAAS,QAAQ;GACjB,YAAY,QAAQ;GACpB,SAAS,QAAQ;GACjB,gBAAgB,QAAQ;GACxB,iBAAiB,QAAQ;GACzB,eAAe,QAAQ;GACvB,gBAAgB,QAAQ;GACxB,wBAAwB,QAAQ;GAChC,qBAAqB,QAAQ;GAC7B,mBAAmB,QAAQ;EAC7B;EAEA,KAAK,mBAAmB,KAAK,uBAAuB,QAAQ,QAAQ;EACpE,KAAK,kBACH,QAAQ,yBAAyB,mCAAA,CACxB,KAAK,2BAA2B,CAAC;CAQ9C;CAEA,6BAA+E;EAC7E,MAAM,SAAS;EACf,OAAO;GACL,IAAI,SAAS;IACX,OAAO,OAAO;GAChB;GACA,IAAI,WAAW;IACb,OAAO,OAAO;GAChB;GACA,IAAI,WAAW;IACb,OAAO,OAAO;GAChB;GACA,UAAU,KAAK;GACf,qBAAqB;IACnB,OAAO,KAAK;IACZ,QAAQ,KAAK;IACb,UAAU,KAAK;IACf,QAAQ,KAAK;IACb,WAAW,KAAK;IAChB,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAM,QAAQ,IAAI,CAAC;GACpD;EACF;CACF;CAEA,gBAAsB;EACpB,KAAK,eAAe;EAKpB,KAAK,WAAW;EAChB,KAAK,uBAAuB;EAO5B,KAAK,oBAAoB;EACzB,IAAI,KAAK,iBACP;EAGF,KAAK,kBAAkB;EACvB,KAAK,eAAe,eAAe;EACnC,KAAK,eAAe,aAAa;CACnC;;;;;;CAOA,MAAM,SAAS,OAA8B;EAC3C,IAAI,KAAK,UAAU;EACnB,IAAI,KAAK,WAAW;EACpB,KAAK,cAAc;EAEnB,KAAK,QAAQ;EACb,KAAK,WAAW;EAChB,MAAM,QAAQ,KAAK,eAAe,SAAS,KAAK;EAChD,KAAK,aAAa,IAAI;EACtB,KAAK,UAAU,YAAY;EAC3B,KAAK,SAAS,KAAA,CAAS;EAEvB,MAAM,kBAAkB,IAAI,gBAAgB;EAC5C,KAAK,kBAAkB;EACvB,MAAM,EAAE,WAAW;EAEnB,IAAI;GACF,IAAI,KAAK,SAAS;IAEhB,MAAM,SAAS,MAAM,KAAK,QAAQ,OAAO,EAAE,OAAO,CAAC;IACnD,IAAI,OAAO,SAAS;IACpB,IAAI,kBAAkB,UAEpB,MAAM,KAAK,cACT,iBAAiB,QAAQ,MAAM,GAC/B,OACA,MACF;SACK;KACL,KAAK,eAAe,iBAAiB,KAAK;KAC1C,KAAK,UAAU,MAAM;KACrB,KAAK,UAAU,SAAS;KACxB,KAAK,mCAAmC,MAAM;IAChD;GACF,OAAO,IAAI,KAAK,YAAY;IAE1B,MAAM,aAAa;KAAE,GAAG,KAAK;KAAM,GAAG;IAAM;IAC5C,MAAM,SAAS,KAAK,WAAW,QAC7B,CAAC,GACD,YACA,QACA,KAAK,iBAAiB,KAAK,CAC7B;IACA,MAAM,KAAK,cAAc,QAAQ,OAAO,MAAM;GAChD,OACE,MAAM,IAAI,MACR,iEACF;GAEF,IAAI,CAAC,OAAO,WAAW,KAAK,WAAW,WAAW;IAKhD,KAAK,WAAW,sBAAsB,KAAK,QAAQ;IACnD,KAAK,eAAe,UAClB,KAAK,eAAe,eAAe,KAAK,OACxC,iBACA,WACF;GACF;EACF,SAAS,KAAc;GACrB,IAAI,OAAO,SAAS;GACpB,MAAM,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;GAChE,KAAK,SAAS,KAAK;GACnB,KAAK,UAAU,OAAO;GACtB,KAAK,0BAA0B,KAAK;GACpC,KAAK,eAAe,UAClB,KAAK,eAAe,eAAe,KAAK,OACxC,eACA,WACA,MAAM,OACR;GACA,KAAK,aAAa,UAAU,KAAK;EACnC,UAAU;GACR,IAAI,KAAK,oBAAoB,iBAAiB;IAC5C,KAAK,kBAAkB;IACvB,KAAK,aAAa,KAAK;GACzB;EACF;CACF;;;;;;;;;;;;;CAcA,MAAc,cACZ,QACA,eACA,QACe;EACf,IAAI;EACJ,IAAI,mBAAmB;EAEvB,WAAW,MAAM,SAAS,QAAQ;GAChC,IAAI,OAAO,SAAS;GAEpB,KAAK,aAAa,UAAU,KAAK;GACjC,KAAK,sBAAsB,KAAK;GAChC,MAAM,aACJ,WAAW,SAAS,OAAO,MAAM,UAAU,WACvC,MAAM,QACN,KAAA;GAGN,QAAQ,MAAM,MAAd;IACE,KAAK;KACH,cAAc,MAAM;KACpB,KAAK,eAAe,iBAAiB,MAAM,KAAK;KAChD;IAEF,KAAK;KACH,KAAK,eAAe,iBAAiB,eAAe,aAAa;KACjE,IAAI,MAAM,SAAS,kBAAkB,QACnC,KAAK,UAAU,MAAM,KAAgB;UAChC,IAAI,MAAM,SAAS,kBAAkB,UAAU;MACpD,MAAM,EAAE,UAAU,YAAY,MAAM;MAIpC,KAAK,YAAY,UAAU,OAAO;KACpC;KACA;IAEF,KAAK;KACH,cAAc,MAAM;KACpB,mBAAmB;KACnB,KAAK,eAAe,iBAAiB,MAAM,KAAK;KAChD,KAAK,UAAU,SAAS;KACxB;IAEF,KAAK,aAAa;KAChB,KAAK,eAAe,iBAClB,cAAc,eAAe,aAC/B;KAEA,MAAM,MACH,MAAM,WACP,MAAM,OAAO,WACb;KACF,MAAM,IAAI,MAAM,GAAG;IACrB;GAGF;EACF;EAGA,IAAI,CAAC,oBAAoB,CAAC,OAAO,SAC/B,MAAM,IAAI,MAAM,mCAAmC;CAEvD;;;;CAKA,OAAa;EACX,MAAM,QAAQ,KAAK,eAAe,eAAe;EACjD,IAAI,KAAK,iBAAiB;GACxB,KAAK,gBAAgB,MAAM;GAC3B,KAAK,kBAAkB;EACzB;EACA,KAAK,aAAa,KAAK;EACvB,IAAI,KAAK,WAAW,cAAc;GAChC,KAAK,UAAU,MAAM;GACrB,IAAI,OACF,KAAK,eAAe,UAAU,OAAO,iBAAiB,WAAW;EAErE;EAIA,IAAI,KAAK,kBAAkB,KAAK,eAAe,WAAW,WAAW;GACnE,KAAK,iBAAiB;IACpB,GAAG,KAAK;IACR,aAAa;IACb,QAAQ;GACV;GACA,KAAK,4BAA4B;EACnC;CACF;;;;;;;CAQA,QAAc;EACZ,KAAK,KAAK;EACV,KAAK,UAAU,IAAI;EACnB,KAAK,QAAQ;EACb,KAAK,WAAW;EAChB,KAAK,eAAe,UAAU;EAC9B,KAAK,SAAS,KAAA,CAAS;EACvB,KAAK,UAAU,MAAM;EACrB,KAAK,oBAAoB;EACzB,KAAK,eAAe,UAAU;CAChC;;;;CAKA,cACE,SAMM;EACN,IAAI,QAAQ,SAAS,KAAA,GACnB,KAAK,OAAO,QAAQ,QAAQ,CAAC;EAE/B,IAAI,QAAQ,aAAa,KAAA,GACvB,KAAK,aAAa,WAAW,QAAQ;EAEvC,IAAI,QAAQ,YAAY,KAAA,GACtB,KAAK,aAAa,UAAU,QAAQ;EAEtC,IAAI,QAAQ,eAAe,KAAA,GACzB,KAAK,aAAa,aAAa,QAAQ;EAEzC,IAAI,QAAQ,YAAY,KAAA,GACtB,KAAK,aAAa,UAAU,QAAQ;CAExC;CAEA,UAAgB;EACd,KAAK,WAAW;EAShB,IAAI,KAAK,iBAAiB;GACxB,KAAK,gBAAgB,MAAM;GAC3B,KAAK,kBAAkB;EACzB;EACA,KAAK,aAAa,KAAK;EACvB,KAAK,eAAe,QAAQ;EAC5B,KAAK,kBAAkB;EAIvB,KAAK,yBAAyB;EAC9B,KAAK,gBAAgB,KAAA;CACvB;CAMA,YAA4B;EAC1B,OAAO,KAAK;CACd;CAEA,eAAwB;EACtB,OAAO,KAAK;CACd;CAEA,WAA8B;EAC5B,OAAO,KAAK;CACd;CAEA,YAAmC;EACjC,OAAO,KAAK;CACd;CAEA,oBAA0D;EACxD,OAAO,KAAK,iBACR;GACE,GAAG,KAAK;GACR,GAAI,KAAK,eAAe,mBACpB,EAAE,kBAAkB,CAAC,GAAG,KAAK,eAAe,gBAAgB,EAAE,IAC9D,CAAC;GACL,GAAI,KAAK,eAAe,SACpB,EACE,QAAQ;IACN,GAAG,KAAK,eAAe;IACvB,GAAI,KAAK,eAAe,OAAO,YAC3B,EAAE,WAAW,CAAC,GAAG,KAAK,eAAe,OAAO,SAAS,EAAE,IACvD,CAAC;GACP,EACF,IACA,CAAC;GACL,GAAI,KAAK,eAAe,QACpB,EAAE,OAAO,EAAE,GAAG,KAAK,eAAe,MAAM,EAAE,IAC1C,CAAC;GACL,GAAI,KAAK,eAAe,YACpB,EAAE,WAAW,EAAE,GAAG,KAAK,eAAe,UAAU,EAAE,IAClD,CAAC;EACP,IACA,KAAA;CACN;CAMA,UAAkB,WAAiC;EACjD,IAAI,cAAc,MAAM;GACtB,KAAK,SAAS;GACd,KAAK,aAAa,iBAAiB,IAAI;GACvC,KAAK,eAAe,mBAAmB;GACvC;EACF;EAEA,IAAI,KAAK,aAAa,UAAU;GAC9B,MAAM,cAAc,KAAK,aAAa,SAAS,SAAS;GACxD,IAAI,gBAAgB,MAAM;IAExB,KAAK,eAAe,UAAU;IAC9B;GACF;GACA,IAAI,gBAAgB,KAAA,GAAW;IAE7B,KAAK,SAAS;IACd,KAAK,aAAa,iBAAiB,KAAK,MAAM;IAC9C,KAAK,eAAe,mBAAmB;IACvC;GACF;EACF;EAMA,KAAK,SAAS;EACd,KAAK,aAAa,iBAAiB,KAAK,MAAM;EAC9C,KAAK,eAAe,mBAAmB;CACzC;CAEA,aAAqB,WAA0B;EAC7C,KAAK,YAAY;EACjB,KAAK,aAAa,kBAAkB,SAAS;EAC7C,KAAK,eAAe,oBAAoB;CAC1C;CAEA,SAAiB,OAAgC;EAC/C,KAAK,QAAQ;EACb,KAAK,aAAa,gBAAgB,KAAK;EACvC,KAAK,eAAe,kBAAkB,KAAK;CAC7C;CAEA,UAAkB,QAAqC;EACrD,KAAK,SAAS;EACd,KAAK,aAAa,iBAAiB,MAAM;EACzC,KAAK,eAAe,mBAAmB,MAAM;CAC/C;CAEA,YAAoB,OAAe,SAAwB;EACzD,KAAK,WAAW;GACd;GACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EAC/B;EACA,IAAI,YAAY,KAAA,GACd,KAAK,aAAa,aAAa,KAAK;OAEpC,KAAK,aAAa,aAAa,OAAO,OAAO;EAE/C,KAAK,eAAe,qBAAqB;CAC3C;CAEA,uBACE,UAC0B;EAC1B,OAAO;GACL,UAAU,UAAU,YAAY;GAChC,GAAI,UAAU,YAAY,EAAE,WAAW,SAAS,UAAU,IAAI,CAAC;GAC/D,GAAI,UAAU,aAAa,EAAE,YAAY,SAAS,WAAW,IAAI,CAAC;GAClE,GAAI,UAAU,OAAO,EAAE,MAAM,SAAS,KAAK,IAAI,CAAC;EAClD;CACF;CAEA,iBAAiC;EAC/B,IAAI,CAAC,KAAK,UACR,KAAK,WAAW,KAAK,iBAAiB,YAAY;EAEpD,KAAK,WAAW,KAAK;EACrB,OAAO,KAAK;CACd;CAEA,iBAAyB,QAAwB;EAC/C,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,UAAU,CAAC;CAC1E;CAEA,iBAAyB,OAAqC;EAC5D,OAAO;GACL,UAAU,KAAK;GACf;EACF;CACF;CAEA,sBAA8B,OAA0B;EACtD,KAAK,iBAAiB,+BACpB,KAAK,gBACL,KACF;EACA,KAAK,4BAA4B;CACnC;;;;;;CAOA,8BAA4C;EAC1C,KAAK,aAAa,yBAAyB,KAAK,cAAc;EAC9D,KAAK,gBAAgB;CACvB;;;;;;;;;;;;CAaA,kBAAgC;EAC9B,MAAM,WAAW,KAAK;EACtB,MAAM,QAAQ,UAAU;EACxB,MAAM,cAA4C,QAC9C;GACE,GAAG;GACH,GAAI,UAAU,oBAAoB,SAAS,iBAAiB,SAAS,IACjE,EAAE,kBAAkB,CAAC,GAAG,SAAS,gBAAgB,EAAE,IACnD,CAAC;EACP,IACA;EACJ,MAAM,YAAY,KAAK,UAAU,WAAW;EAC5C,IAAI,cAAc,KAAK,wBACrB;EAEF,KAAK,yBAAyB;EAC9B,KAAK,aAAa,sBAAsB,WAAW;CACrD;;;;;;;;;;;;;;CAeA,oBAA4B,UAA0C;EACpE,KAAK,iBAAiB;EACtB,KAAK,4BAA4B;EACjC,KAAK,UAAU,4BAA4B,SAAS,MAAM,CAAC;EAC3D,KAAK,SACH,SAAS,QACL,OAAO,OACL,IAAI,MAAM,SAAS,MAAM,OAAO,GAChC,SAAS,MAAM,OAAO,EAAE,MAAM,SAAS,MAAM,KAAK,IAAI,CAAC,CACzD,IACA,KAAA,CACN;EACA,MAAM,WAAW,KAAK,0BAA0B,QAAQ;EACxD,IAAI,aAAa,MACf,KAAK,UAAU,QAAQ;OAClB,IACL,KAAK,aAAa,qBAClB,SAAS,WAAW,YAEpB,KAAK,yBAAyB;CAElC;;;;;;CAOA,2BAAyC;EACvC,MAAM,QAAQ,IAAI,MAAM,sCAAsC;EAC9D,KAAK,UAAU,OAAO;EACtB,KAAK,SAAS,KAAK;EACnB,KAAK,aAAa,UAAU,KAAK;CACnC;;;;;;;;;;;CAYA,wBACE,UACA,aACM;EACN,IAAI,SAAS,WAAW,WAAW;GACjC,KAAK,oBAAoB,QAAQ;GACjC;EACF;EACA,MAAM,UAAU,KAAK,YAAY,WAAW,KAAK;EACjD,MAAM,QAAQ,eAAe,SAAS,aAAa;EACnD,IAAI,SAAS,SAAS;GACpB,KAAK,oBAAoB,QAAQ;GACjC,KAAK,eAAe,KAAK;GACzB;EACF;EACA,KAAK,oBAAoB;GACvB,GAAG;GACH,aAAa;GACb,QAAQ;GACR,OAAO,EACL,SACE,8GACJ;EACF,CAAC;CACH;;;;;;;;;;CAWA,0BACE,UACgB;EAChB,MAAM,QAAQ,KAAK,aAAa;EAChC,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,SAAS,SAAS;EAkBxB,OAAO,MAAM;GAhBX,GAAI,QAAQ,OAAO,KAAA,IAAY,EAAE,IAAI,OAAO,GAAG,IAAI,CAAC;GACpD,GAAI,QAAQ,UAAU,KAAA,IAAY,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;GAC7D,GAAI,QAAQ,WAAW,KAAA,IAAY,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;GAChE,GAAI,QAAQ,kBAAkB,KAAA,IAC1B,EAAE,eAAe,OAAO,cAAc,IACtC,CAAC;GACL,GAAI,QAAQ,cAAc,KAAA,IACtB,EAAE,WAAW,OAAO,UAAU,IAC9B,CAAC;GACL,GAAI,QAAQ,SAAS,KAAA,IAAY,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;GAC1D,GAAI,QAAQ,UAAU,KAAA,IAAY,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;GAC7D,GAAI,SAAS,aAAa,KAAA,IACtB,EAAE,UAAU,SAAS,SAAS,IAC9B,CAAC;GACL,WAAW,QAAQ,aAAa,CAAC;EAEtB,CAAQ;CACvB;;;;;;;CAQA,mCAA2C,WAA0B;EACnE,MAAM,WAAW,KAAK;EACtB,MAAM,SAAS,+BAA+B,SAAS;EACvD,KAAK,iBAAiB;GACpB,eAAe;GACf,aAAa;GACb,QAAQ;GACR,GAAI,UAAU,WAAW,EAAE,UAAU,SAAS,SAAS,IAAI,CAAC;GAC5D,GAAI,UAAU,oBAAoB,SAAS,iBAAiB,SAAS,IACjE,EAAE,kBAAkB,CAAC,GAAG,SAAS,gBAAgB,EAAE,IACnD,CAAC;GACL,GAAI,SACA,EAAE,OAAO,IACT,UAAU,SACR,EAAE,QAAQ,EAAE,GAAG,SAAS,OAAO,EAAE,IACjC,CAAC;EACT;EACA,KAAK,4BAA4B;CACnC;;;;;;;CAQA,0BAAkC,OAAoB;EAOpD,IAAI,KAAK,WAAW,SAAS,KAAK,UAAU,OAAO;EACnD,KAAK,SAAS,KAAK;EACnB,IAAI,KAAK,gBAAgB,WAAW,SAAS;EAC7C,IAAI,CAAC,KAAK,kBAAkB,CAAC,KAAK,cAAc;EAChD,MAAM,WAAW,KAAK;EACtB,KAAK,iBAAiB;GACpB,eAAe;GACf,aAAa;GACb,QAAQ;GACR,GAAI,UAAU,WAAW,EAAE,UAAU,SAAS,SAAS,IAAI,CAAC;GAC5D,GAAI,UAAU,oBAAoB,SAAS,iBAAiB,SAAS,IACjE,EAAE,kBAAkB,CAAC,GAAG,SAAS,gBAAgB,EAAE,IACnD,CAAC;GACL,GAAI,UAAU,SAAS,EAAE,QAAQ,EAAE,GAAG,SAAS,OAAO,EAAE,IAAI,CAAC;GAC7D,OAAO,EAAE,SAAS,MAAM,QAAQ;EAClC;EACA,KAAK,4BAA4B;CACnC;;;;;CAMA,sBAAoC;EAClC,KAAK,iBAAiB,KAAA;EACtB,KAAK,yBAAyB,KAAA;EAC9B,KAAK,4BAA4B;CACnC;;;;;;;CAQA,yBAAuC;EACrC,IAAI,CAAC,KAAK,gBAAgB,KAAK,wBAAwB;EACvD,KAAK,yBAAyB;EAC9B,IAAI,KAAK,YAAY,qBAAqB,KAAK,0BAC7C,KAAK,kBAAkB;OAIvB,QAAQ,KACN,+TACF;CAEJ;;;;;;;;;;;;;;CAeA,oBAAkC;EAChC,MAAM,UACJ,KAAK,YAAY,qBAAqB,KAAK;EAC7C,IAAI,CAAC,SAAS;EAEd,IAAI,KAAK,kBAAkB,KAAK,aAAa,KAAK,WAAW,QAAQ;EACrE,CAAM,YAAY;GAChB,IAAI;GACJ,IAAI;IACF,MAAM,MAAM,QAAQ,KAAK,QAAQ;GACnC,SAAS,OAAO;IACd,KAAK,cACH,+BACE,6CACA,KACF,CACF;IACA;GACF;GAEA,IAAI,CAAC,IAAI,gBAAgB;GACzB,MAAM,WAAW,8BAA8B,IAAI,cAAc;GACjE,IAAI,CAAC,UAAU;IACb,KAAK,cACH,+BACE,+HACF,CACF;IACA;GACF;GAEA,IAAI,KAAK,kBAAkB,KAAK,aAAa,KAAK,WAAW,QAC3D;GAEF,KAAK,wBAAwB,UAAU,IAAI,WAAW,KAAK;EAC7D,EAAA,CAAG;CACL;;;;;;CAOA,cAAsB,OAAoB;EACxC,IAAI,KAAK,kBAAkB,KAAK,aAAa,KAAK,WAAW,QAAQ;EACrE,KAAK,UAAU,OAAO;EACtB,KAAK,SAAS,KAAK;EACnB,KAAK,aAAa,UAAU,KAAK;CACnC;;;;;;;CAQA,sBAAoC;EAClC,IAAI,KAAK,gBAAgB,WAAW,WAAW;EAC/C,MAAM,QAAQ,KAAK,eAAe,aAAa;EAC/C,IAAI,OAAO,KAAK,eAAe,KAAK;CACtC;;;;;;;;CASA,eAAuB,OAAqB;EAC1C,MAAM,UAAU,KAAK,YAAY,WAAW,KAAK;EACjD,IAAI,CAAC,SAAS;EACd,IAAI,KAAK,kBAAkB,OAAO;EAElC,IAAI,KAAK,aAAa,KAAK,iBAAiB;EAC5C,KAAK,gBAAgB;EACrB,MAAM,aAAa,IAAI,gBAAgB;EACvC,KAAK,kBAAkB;EACvB,KAAK,aAAa,IAAI;EACtB,KAAK,UAAU,YAAY;EAC3B,CAAM,YAAY;GAChB,IAAI;IACF,MAAM,KAAK,cACT,QAAQ,OAAO,WAAW,MAAM,GAChC,OACA,WAAW,MACb;GACF,SAAS,OAAO;IACd,IAAI,CAAC,WAAW,OAAO,SAAS;KAC9B,MAAM,UACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;KAI1D,KAAK,0BAA0B,OAAO;KACtC,KAAK,aAAa,UAAU,OAAO;IACrC;GACF,UAAU;IAIR,IAAI,KAAK,oBAAoB,YAAY;KACvC,KAAK,kBAAkB;KACvB,KAAK,aAAa,KAAK;IACzB;GACF;EACF,EAAA,CAAG;CACL;AACF;AAEA,SAAS,sBACP,UACqC;CACrC,IAAI,CAAC,UAAU,OAAO;CACtB,MAAM,UAAU,SAAS;CACzB,OAAO;EACL,OAAO;EACP,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;CAC/B;AACF"}
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 { restoreInboundChunk } from '@tanstack/ai/client'\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 GenerationPersistenceOptions,\n GenerationRestoredResult,\n GenerationResumeSnapshot,\n GenerationResumeState,\n GenerationTransport,\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 uniqueId: string\n private readonly devtoolsMetadata: AIDevtoolsClientMetadata\n private readonly devtoolsBridge: GenerationDevtoolsBridge<TOutput>\n private 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: Omit<\n GenerationClientOptions<TInput, TResult, TOutput>,\n 'persistence' | 'threadId'\n > &\n GenerationPersistenceOptions &\n GenerationTransport<TInput, TResult>,\n ) {\n // `threadId` is the only identity. Do not mint a random id during\n // construct: hooks build this client during render.\n this.threadId = options.threadId ?? ''\n this.uniqueId = this.threadId\n // The persistence scope is the explicit `threadId` and nothing else.\n // The types require it whenever `persistence` is set. This field keeps a\n // generated wire id from 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 const client = this\n return {\n get hookId() {\n return client.uniqueId\n },\n get clientId() {\n return client.uniqueId\n },\n get threadId() {\n return client.threadId\n },\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 this.ensureThreadId()\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 raw of source) {\n if (signal.aborted) break\n\n const chunk = restoreInboundChunk(raw)\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 // Spec RUN_ERROR message. Missing message uses this fallback.\n const msg =\n (chunk.message as string | undefined) || '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 ensureThreadId(): string {\n if (!this.threadId) {\n this.threadId = this.generateUniqueId('generation')\n }\n this.uniqueId = this.threadId\n return this.threadId\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiGA,IAAa,mBAAb,MAIE;CACA;CACA;CAIA;CAGA;CAGA;CACA;CACA;CACA;CACA;CAGA,eAAyC;CACzC;CACA,SAAiC;CACjC,QAA+B;CAC/B,WAAwD;CACxD,YAAoB;CACpB,QAAmC,KAAA;CACnC,SAAwC;CACxC;CACA;CACA,kBAAkD;CAClD;CACA;CACA,kBAA0B;CAC1B,WAAmB;CACnB,yBAAiC;CAEjC,YACE,SAMA;EAGA,KAAK,WAAW,QAAQ,YAAY;EACpC,KAAK,WAAW,KAAK;EAIrB,KAAK,mBAAmB,QAAQ;EAChC,KAAK,aAAa,QAAQ;EAC1B,KAAK,UAAU,QAAQ;EACvB,KAAK,2BAA2B,QAAQ;EACxC,KAAK,iBAAiB,QAAQ;EAC9B,KAAK,OAAO,QAAQ,QAAQ,CAAC;EAG7B,KAAK,eAAe,QAAQ,gBAAgB;EAK5C,IAAI,QAAQ,eAAe,CAAC,KAAK,kBAC/B,QAAQ,KACN,iMACF;EAEF,KAAK,eAAe;GAClB,UAAU,QAAQ;GAClB,SAAS,QAAQ;GACjB,YAAY,QAAQ;GACpB,SAAS,QAAQ;GACjB,gBAAgB,QAAQ;GACxB,iBAAiB,QAAQ;GACzB,eAAe,QAAQ;GACvB,gBAAgB,QAAQ;GACxB,wBAAwB,QAAQ;GAChC,qBAAqB,QAAQ;GAC7B,mBAAmB,QAAQ;EAC7B;EAEA,KAAK,mBAAmB,KAAK,uBAAuB,QAAQ,QAAQ;EACpE,KAAK,kBACH,QAAQ,yBAAyB,mCAAA,CACxB,KAAK,2BAA2B,CAAC;CAQ9C;CAEA,6BAA+E;EAC7E,MAAM,SAAS;EACf,OAAO;GACL,IAAI,SAAS;IACX,OAAO,OAAO;GAChB;GACA,IAAI,WAAW;IACb,OAAO,OAAO;GAChB;GACA,IAAI,WAAW;IACb,OAAO,OAAO;GAChB;GACA,UAAU,KAAK;GACf,qBAAqB;IACnB,OAAO,KAAK;IACZ,QAAQ,KAAK;IACb,UAAU,KAAK;IACf,QAAQ,KAAK;IACb,WAAW,KAAK;IAChB,GAAI,KAAK,QAAQ,EAAE,OAAO,KAAK,MAAM,QAAQ,IAAI,CAAC;GACpD;EACF;CACF;CAEA,gBAAsB;EACpB,KAAK,eAAe;EAKpB,KAAK,WAAW;EAChB,KAAK,uBAAuB;EAO5B,KAAK,oBAAoB;EACzB,IAAI,KAAK,iBACP;EAGF,KAAK,kBAAkB;EACvB,KAAK,eAAe,eAAe;EACnC,KAAK,eAAe,aAAa;CACnC;;;;;;CAOA,MAAM,SAAS,OAA8B;EAC3C,IAAI,KAAK,UAAU;EACnB,IAAI,KAAK,WAAW;EACpB,KAAK,cAAc;EAEnB,KAAK,QAAQ;EACb,KAAK,WAAW;EAChB,MAAM,QAAQ,KAAK,eAAe,SAAS,KAAK;EAChD,KAAK,aAAa,IAAI;EACtB,KAAK,UAAU,YAAY;EAC3B,KAAK,SAAS,KAAA,CAAS;EAEvB,MAAM,kBAAkB,IAAI,gBAAgB;EAC5C,KAAK,kBAAkB;EACvB,MAAM,EAAE,WAAW;EAEnB,IAAI;GACF,IAAI,KAAK,SAAS;IAEhB,MAAM,SAAS,MAAM,KAAK,QAAQ,OAAO,EAAE,OAAO,CAAC;IACnD,IAAI,OAAO,SAAS;IACpB,IAAI,kBAAkB,UAEpB,MAAM,KAAK,cACT,iBAAiB,QAAQ,MAAM,GAC/B,OACA,MACF;SACK;KACL,KAAK,eAAe,iBAAiB,KAAK;KAC1C,KAAK,UAAU,MAAM;KACrB,KAAK,UAAU,SAAS;KACxB,KAAK,mCAAmC,MAAM;IAChD;GACF,OAAO,IAAI,KAAK,YAAY;IAE1B,MAAM,aAAa;KAAE,GAAG,KAAK;KAAM,GAAG;IAAM;IAC5C,MAAM,SAAS,KAAK,WAAW,QAC7B,CAAC,GACD,YACA,QACA,KAAK,iBAAiB,KAAK,CAC7B;IACA,MAAM,KAAK,cAAc,QAAQ,OAAO,MAAM;GAChD,OACE,MAAM,IAAI,MACR,iEACF;GAEF,IAAI,CAAC,OAAO,WAAW,KAAK,WAAW,WAAW;IAKhD,KAAK,WAAW,sBAAsB,KAAK,QAAQ;IACnD,KAAK,eAAe,UAClB,KAAK,eAAe,eAAe,KAAK,OACxC,iBACA,WACF;GACF;EACF,SAAS,KAAc;GACrB,IAAI,OAAO,SAAS;GACpB,MAAM,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;GAChE,KAAK,SAAS,KAAK;GACnB,KAAK,UAAU,OAAO;GACtB,KAAK,0BAA0B,KAAK;GACpC,KAAK,eAAe,UAClB,KAAK,eAAe,eAAe,KAAK,OACxC,eACA,WACA,MAAM,OACR;GACA,KAAK,aAAa,UAAU,KAAK;EACnC,UAAU;GACR,IAAI,KAAK,oBAAoB,iBAAiB;IAC5C,KAAK,kBAAkB;IACvB,KAAK,aAAa,KAAK;GACzB;EACF;CACF;;;;;;;;;;;;;CAcA,MAAc,cACZ,QACA,eACA,QACe;EACf,IAAI;EACJ,IAAI,mBAAmB;EAEvB,WAAW,MAAM,OAAO,QAAQ;GAC9B,IAAI,OAAO,SAAS;GAEpB,MAAM,QAAQ,oBAAoB,GAAG;GACrC,KAAK,aAAa,UAAU,KAAK;GACjC,KAAK,sBAAsB,KAAK;GAChC,MAAM,aACJ,WAAW,SAAS,OAAO,MAAM,UAAU,WACvC,MAAM,QACN,KAAA;GAGN,QAAQ,MAAM,MAAd;IACE,KAAK;KACH,cAAc,MAAM;KACpB,KAAK,eAAe,iBAAiB,MAAM,KAAK;KAChD;IAEF,KAAK;KACH,KAAK,eAAe,iBAAiB,eAAe,aAAa;KACjE,IAAI,MAAM,SAAS,kBAAkB,QACnC,KAAK,UAAU,MAAM,KAAgB;UAChC,IAAI,MAAM,SAAS,kBAAkB,UAAU;MACpD,MAAM,EAAE,UAAU,YAAY,MAAM;MAIpC,KAAK,YAAY,UAAU,OAAO;KACpC;KACA;IAEF,KAAK;KACH,cAAc,MAAM;KACpB,mBAAmB;KACnB,KAAK,eAAe,iBAAiB,MAAM,KAAK;KAChD,KAAK,UAAU,SAAS;KACxB;IAEF,KAAK,aAAa;KAChB,KAAK,eAAe,iBAClB,cAAc,eAAe,aAC/B;KAEA,MAAM,MACH,MAAM,WAAkC;KAC3C,MAAM,IAAI,MAAM,GAAG;IACrB;GAGF;EACF;EAGA,IAAI,CAAC,oBAAoB,CAAC,OAAO,SAC/B,MAAM,IAAI,MAAM,mCAAmC;CAEvD;;;;CAKA,OAAa;EACX,MAAM,QAAQ,KAAK,eAAe,eAAe;EACjD,IAAI,KAAK,iBAAiB;GACxB,KAAK,gBAAgB,MAAM;GAC3B,KAAK,kBAAkB;EACzB;EACA,KAAK,aAAa,KAAK;EACvB,IAAI,KAAK,WAAW,cAAc;GAChC,KAAK,UAAU,MAAM;GACrB,IAAI,OACF,KAAK,eAAe,UAAU,OAAO,iBAAiB,WAAW;EAErE;EAIA,IAAI,KAAK,kBAAkB,KAAK,eAAe,WAAW,WAAW;GACnE,KAAK,iBAAiB;IACpB,GAAG,KAAK;IACR,aAAa;IACb,QAAQ;GACV;GACA,KAAK,4BAA4B;EACnC;CACF;;;;;;;CAQA,QAAc;EACZ,KAAK,KAAK;EACV,KAAK,UAAU,IAAI;EACnB,KAAK,QAAQ;EACb,KAAK,WAAW;EAChB,KAAK,eAAe,UAAU;EAC9B,KAAK,SAAS,KAAA,CAAS;EACvB,KAAK,UAAU,MAAM;EACrB,KAAK,oBAAoB;EACzB,KAAK,eAAe,UAAU;CAChC;;;;CAKA,cACE,SAMM;EACN,IAAI,QAAQ,SAAS,KAAA,GACnB,KAAK,OAAO,QAAQ,QAAQ,CAAC;EAE/B,IAAI,QAAQ,aAAa,KAAA,GACvB,KAAK,aAAa,WAAW,QAAQ;EAEvC,IAAI,QAAQ,YAAY,KAAA,GACtB,KAAK,aAAa,UAAU,QAAQ;EAEtC,IAAI,QAAQ,eAAe,KAAA,GACzB,KAAK,aAAa,aAAa,QAAQ;EAEzC,IAAI,QAAQ,YAAY,KAAA,GACtB,KAAK,aAAa,UAAU,QAAQ;CAExC;CAEA,UAAgB;EACd,KAAK,WAAW;EAShB,IAAI,KAAK,iBAAiB;GACxB,KAAK,gBAAgB,MAAM;GAC3B,KAAK,kBAAkB;EACzB;EACA,KAAK,aAAa,KAAK;EACvB,KAAK,eAAe,QAAQ;EAC5B,KAAK,kBAAkB;EAIvB,KAAK,yBAAyB;EAC9B,KAAK,gBAAgB,KAAA;CACvB;CAMA,YAA4B;EAC1B,OAAO,KAAK;CACd;CAEA,eAAwB;EACtB,OAAO,KAAK;CACd;CAEA,WAA8B;EAC5B,OAAO,KAAK;CACd;CAEA,YAAmC;EACjC,OAAO,KAAK;CACd;CAEA,oBAA0D;EACxD,OAAO,KAAK,iBACR;GACE,GAAG,KAAK;GACR,GAAI,KAAK,eAAe,mBACpB,EAAE,kBAAkB,CAAC,GAAG,KAAK,eAAe,gBAAgB,EAAE,IAC9D,CAAC;GACL,GAAI,KAAK,eAAe,SACpB,EACE,QAAQ;IACN,GAAG,KAAK,eAAe;IACvB,GAAI,KAAK,eAAe,OAAO,YAC3B,EAAE,WAAW,CAAC,GAAG,KAAK,eAAe,OAAO,SAAS,EAAE,IACvD,CAAC;GACP,EACF,IACA,CAAC;GACL,GAAI,KAAK,eAAe,QACpB,EAAE,OAAO,EAAE,GAAG,KAAK,eAAe,MAAM,EAAE,IAC1C,CAAC;GACL,GAAI,KAAK,eAAe,YACpB,EAAE,WAAW,EAAE,GAAG,KAAK,eAAe,UAAU,EAAE,IAClD,CAAC;EACP,IACA,KAAA;CACN;CAMA,UAAkB,WAAiC;EACjD,IAAI,cAAc,MAAM;GACtB,KAAK,SAAS;GACd,KAAK,aAAa,iBAAiB,IAAI;GACvC,KAAK,eAAe,mBAAmB;GACvC;EACF;EAEA,IAAI,KAAK,aAAa,UAAU;GAC9B,MAAM,cAAc,KAAK,aAAa,SAAS,SAAS;GACxD,IAAI,gBAAgB,MAAM;IAExB,KAAK,eAAe,UAAU;IAC9B;GACF;GACA,IAAI,gBAAgB,KAAA,GAAW;IAE7B,KAAK,SAAS;IACd,KAAK,aAAa,iBAAiB,KAAK,MAAM;IAC9C,KAAK,eAAe,mBAAmB;IACvC;GACF;EACF;EAMA,KAAK,SAAS;EACd,KAAK,aAAa,iBAAiB,KAAK,MAAM;EAC9C,KAAK,eAAe,mBAAmB;CACzC;CAEA,aAAqB,WAA0B;EAC7C,KAAK,YAAY;EACjB,KAAK,aAAa,kBAAkB,SAAS;EAC7C,KAAK,eAAe,oBAAoB;CAC1C;CAEA,SAAiB,OAAgC;EAC/C,KAAK,QAAQ;EACb,KAAK,aAAa,gBAAgB,KAAK;EACvC,KAAK,eAAe,kBAAkB,KAAK;CAC7C;CAEA,UAAkB,QAAqC;EACrD,KAAK,SAAS;EACd,KAAK,aAAa,iBAAiB,MAAM;EACzC,KAAK,eAAe,mBAAmB,MAAM;CAC/C;CAEA,YAAoB,OAAe,SAAwB;EACzD,KAAK,WAAW;GACd;GACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EAC/B;EACA,IAAI,YAAY,KAAA,GACd,KAAK,aAAa,aAAa,KAAK;OAEpC,KAAK,aAAa,aAAa,OAAO,OAAO;EAE/C,KAAK,eAAe,qBAAqB;CAC3C;CAEA,uBACE,UAC0B;EAC1B,OAAO;GACL,UAAU,UAAU,YAAY;GAChC,GAAI,UAAU,YAAY,EAAE,WAAW,SAAS,UAAU,IAAI,CAAC;GAC/D,GAAI,UAAU,aAAa,EAAE,YAAY,SAAS,WAAW,IAAI,CAAC;GAClE,GAAI,UAAU,OAAO,EAAE,MAAM,SAAS,KAAK,IAAI,CAAC;EAClD;CACF;CAEA,iBAAiC;EAC/B,IAAI,CAAC,KAAK,UACR,KAAK,WAAW,KAAK,iBAAiB,YAAY;EAEpD,KAAK,WAAW,KAAK;EACrB,OAAO,KAAK;CACd;CAEA,iBAAyB,QAAwB;EAC/C,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,UAAU,CAAC;CAC1E;CAEA,iBAAyB,OAAqC;EAC5D,OAAO;GACL,UAAU,KAAK;GACf;EACF;CACF;CAEA,sBAA8B,OAA0B;EACtD,KAAK,iBAAiB,+BACpB,KAAK,gBACL,KACF;EACA,KAAK,4BAA4B;CACnC;;;;;;CAOA,8BAA4C;EAC1C,KAAK,aAAa,yBAAyB,KAAK,cAAc;EAC9D,KAAK,gBAAgB;CACvB;;;;;;;;;;;;CAaA,kBAAgC;EAC9B,MAAM,WAAW,KAAK;EACtB,MAAM,QAAQ,UAAU;EACxB,MAAM,cAA4C,QAC9C;GACE,GAAG;GACH,GAAI,UAAU,oBAAoB,SAAS,iBAAiB,SAAS,IACjE,EAAE,kBAAkB,CAAC,GAAG,SAAS,gBAAgB,EAAE,IACnD,CAAC;EACP,IACA;EACJ,MAAM,YAAY,KAAK,UAAU,WAAW;EAC5C,IAAI,cAAc,KAAK,wBACrB;EAEF,KAAK,yBAAyB;EAC9B,KAAK,aAAa,sBAAsB,WAAW;CACrD;;;;;;;;;;;;;;CAeA,oBAA4B,UAA0C;EACpE,KAAK,iBAAiB;EACtB,KAAK,4BAA4B;EACjC,KAAK,UAAU,4BAA4B,SAAS,MAAM,CAAC;EAC3D,KAAK,SACH,SAAS,QACL,OAAO,OACL,IAAI,MAAM,SAAS,MAAM,OAAO,GAChC,SAAS,MAAM,OAAO,EAAE,MAAM,SAAS,MAAM,KAAK,IAAI,CAAC,CACzD,IACA,KAAA,CACN;EACA,MAAM,WAAW,KAAK,0BAA0B,QAAQ;EACxD,IAAI,aAAa,MACf,KAAK,UAAU,QAAQ;OAClB,IACL,KAAK,aAAa,qBAClB,SAAS,WAAW,YAEpB,KAAK,yBAAyB;CAElC;;;;;;CAOA,2BAAyC;EACvC,MAAM,QAAQ,IAAI,MAAM,sCAAsC;EAC9D,KAAK,UAAU,OAAO;EACtB,KAAK,SAAS,KAAK;EACnB,KAAK,aAAa,UAAU,KAAK;CACnC;;;;;;;;;;;CAYA,wBACE,UACA,aACM;EACN,IAAI,SAAS,WAAW,WAAW;GACjC,KAAK,oBAAoB,QAAQ;GACjC;EACF;EACA,MAAM,UAAU,KAAK,YAAY,WAAW,KAAK;EACjD,MAAM,QAAQ,eAAe,SAAS,aAAa;EACnD,IAAI,SAAS,SAAS;GACpB,KAAK,oBAAoB,QAAQ;GACjC,KAAK,eAAe,KAAK;GACzB;EACF;EACA,KAAK,oBAAoB;GACvB,GAAG;GACH,aAAa;GACb,QAAQ;GACR,OAAO,EACL,SACE,8GACJ;EACF,CAAC;CACH;;;;;;;;;;CAWA,0BACE,UACgB;EAChB,MAAM,QAAQ,KAAK,aAAa;EAChC,IAAI,CAAC,OAAO,OAAO;EACnB,MAAM,SAAS,SAAS;EAkBxB,OAAO,MAAM;GAhBX,GAAI,QAAQ,OAAO,KAAA,IAAY,EAAE,IAAI,OAAO,GAAG,IAAI,CAAC;GACpD,GAAI,QAAQ,UAAU,KAAA,IAAY,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;GAC7D,GAAI,QAAQ,WAAW,KAAA,IAAY,EAAE,QAAQ,OAAO,OAAO,IAAI,CAAC;GAChE,GAAI,QAAQ,kBAAkB,KAAA,IAC1B,EAAE,eAAe,OAAO,cAAc,IACtC,CAAC;GACL,GAAI,QAAQ,cAAc,KAAA,IACtB,EAAE,WAAW,OAAO,UAAU,IAC9B,CAAC;GACL,GAAI,QAAQ,SAAS,KAAA,IAAY,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;GAC1D,GAAI,QAAQ,UAAU,KAAA,IAAY,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;GAC7D,GAAI,SAAS,aAAa,KAAA,IACtB,EAAE,UAAU,SAAS,SAAS,IAC9B,CAAC;GACL,WAAW,QAAQ,aAAa,CAAC;EAEtB,CAAQ;CACvB;;;;;;;CAQA,mCAA2C,WAA0B;EACnE,MAAM,WAAW,KAAK;EACtB,MAAM,SAAS,+BAA+B,SAAS;EACvD,KAAK,iBAAiB;GACpB,eAAe;GACf,aAAa;GACb,QAAQ;GACR,GAAI,UAAU,WAAW,EAAE,UAAU,SAAS,SAAS,IAAI,CAAC;GAC5D,GAAI,UAAU,oBAAoB,SAAS,iBAAiB,SAAS,IACjE,EAAE,kBAAkB,CAAC,GAAG,SAAS,gBAAgB,EAAE,IACnD,CAAC;GACL,GAAI,SACA,EAAE,OAAO,IACT,UAAU,SACR,EAAE,QAAQ,EAAE,GAAG,SAAS,OAAO,EAAE,IACjC,CAAC;EACT;EACA,KAAK,4BAA4B;CACnC;;;;;;;CAQA,0BAAkC,OAAoB;EAOpD,IAAI,KAAK,WAAW,SAAS,KAAK,UAAU,OAAO;EACnD,KAAK,SAAS,KAAK;EACnB,IAAI,KAAK,gBAAgB,WAAW,SAAS;EAC7C,IAAI,CAAC,KAAK,kBAAkB,CAAC,KAAK,cAAc;EAChD,MAAM,WAAW,KAAK;EACtB,KAAK,iBAAiB;GACpB,eAAe;GACf,aAAa;GACb,QAAQ;GACR,GAAI,UAAU,WAAW,EAAE,UAAU,SAAS,SAAS,IAAI,CAAC;GAC5D,GAAI,UAAU,oBAAoB,SAAS,iBAAiB,SAAS,IACjE,EAAE,kBAAkB,CAAC,GAAG,SAAS,gBAAgB,EAAE,IACnD,CAAC;GACL,GAAI,UAAU,SAAS,EAAE,QAAQ,EAAE,GAAG,SAAS,OAAO,EAAE,IAAI,CAAC;GAC7D,OAAO,EAAE,SAAS,MAAM,QAAQ;EAClC;EACA,KAAK,4BAA4B;CACnC;;;;;CAMA,sBAAoC;EAClC,KAAK,iBAAiB,KAAA;EACtB,KAAK,yBAAyB,KAAA;EAC9B,KAAK,4BAA4B;CACnC;;;;;;;CAQA,yBAAuC;EACrC,IAAI,CAAC,KAAK,gBAAgB,KAAK,wBAAwB;EACvD,KAAK,yBAAyB;EAC9B,IAAI,KAAK,YAAY,qBAAqB,KAAK,0BAC7C,KAAK,kBAAkB;OAIvB,QAAQ,KACN,+TACF;CAEJ;;;;;;;;;;;;;;CAeA,oBAAkC;EAChC,MAAM,UACJ,KAAK,YAAY,qBAAqB,KAAK;EAC7C,IAAI,CAAC,SAAS;EAEd,IAAI,KAAK,kBAAkB,KAAK,aAAa,KAAK,WAAW,QAAQ;EACrE,CAAM,YAAY;GAChB,IAAI;GACJ,IAAI;IACF,MAAM,MAAM,QAAQ,KAAK,QAAQ;GACnC,SAAS,OAAO;IACd,KAAK,cACH,+BACE,6CACA,KACF,CACF;IACA;GACF;GAEA,IAAI,CAAC,IAAI,gBAAgB;GACzB,MAAM,WAAW,8BAA8B,IAAI,cAAc;GACjE,IAAI,CAAC,UAAU;IACb,KAAK,cACH,+BACE,+HACF,CACF;IACA;GACF;GAEA,IAAI,KAAK,kBAAkB,KAAK,aAAa,KAAK,WAAW,QAC3D;GAEF,KAAK,wBAAwB,UAAU,IAAI,WAAW,KAAK;EAC7D,EAAA,CAAG;CACL;;;;;;CAOA,cAAsB,OAAoB;EACxC,IAAI,KAAK,kBAAkB,KAAK,aAAa,KAAK,WAAW,QAAQ;EACrE,KAAK,UAAU,OAAO;EACtB,KAAK,SAAS,KAAK;EACnB,KAAK,aAAa,UAAU,KAAK;CACnC;;;;;;;CAQA,sBAAoC;EAClC,IAAI,KAAK,gBAAgB,WAAW,WAAW;EAC/C,MAAM,QAAQ,KAAK,eAAe,aAAa;EAC/C,IAAI,OAAO,KAAK,eAAe,KAAK;CACtC;;;;;;;;CASA,eAAuB,OAAqB;EAC1C,MAAM,UAAU,KAAK,YAAY,WAAW,KAAK;EACjD,IAAI,CAAC,SAAS;EACd,IAAI,KAAK,kBAAkB,OAAO;EAElC,IAAI,KAAK,aAAa,KAAK,iBAAiB;EAC5C,KAAK,gBAAgB;EACrB,MAAM,aAAa,IAAI,gBAAgB;EACvC,KAAK,kBAAkB;EACvB,KAAK,aAAa,IAAI;EACtB,KAAK,UAAU,YAAY;EAC3B,CAAM,YAAY;GAChB,IAAI;IACF,MAAM,KAAK,cACT,QAAQ,OAAO,WAAW,MAAM,GAChC,OACA,WAAW,MACb;GACF,SAAS,OAAO;IACd,IAAI,CAAC,WAAW,OAAO,SAAS;KAC9B,MAAM,UACJ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;KAI1D,KAAK,0BAA0B,OAAO;KACtC,KAAK,aAAa,UAAU,OAAO;IACrC;GACF,UAAU;IAIR,IAAI,KAAK,oBAAoB,YAAY;KACvC,KAAK,kBAAkB;KACvB,KAAK,aAAa,KAAK;IACzB;GACF;EACF,EAAA,CAAG;CACL;AACF;AAEA,SAAS,sBACP,UACqC;CACrC,IAAI,CAAC,UAAU,OAAO;CACtB,MAAM,UAAU,SAAS;CACzB,OAAO;EACL,OAAO;EACP,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;CAC/B;AACF"}
@@ -1,3 +1,4 @@
1
+ import { tanstackMetadata } from "@tanstack/ai/client";
1
2
  //#region src/generation-types.ts
2
3
  /**
3
4
  * Thrown when a generation stream ends without a terminal `RUN_FINISHED` /
@@ -68,8 +69,9 @@ var GENERATION_EVENTS = {
68
69
  * @internal
69
70
  */
70
71
  function updateGenerationResumeSnapshot(previous, chunk) {
71
- const threadId = stringField(chunk, "threadId");
72
- const runId = stringField(chunk, "runId");
72
+ const tanstack = tanstackMetadata(chunk);
73
+ const threadId = stringField(chunk, "threadId") ?? (typeof tanstack?.threadId === "string" ? tanstack.threadId : void 0);
74
+ const runId = stringField(chunk, "runId") ?? (typeof tanstack?.runId === "string" ? tanstack.runId : void 0);
73
75
  const carried = chunk.type === "RUN_STARTED" ? void 0 : previous;
74
76
  const previousArtifacts = carried?.pendingArtifacts ?? [];
75
77
  const next = {
@@ -1 +1 @@
1
- {"version":3,"file":"generation-types.js","names":[],"sources":["../../src/generation-types.ts"],"sourcesContent":["import type {\n MediaPrompt,\n PersistedArtifactRef,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type { TokenUsage, TranscriptionResponseFormat } from '@tanstack/ai'\nimport type { ConnectConnectionAdapter } from './connection-adapters'\nimport type { AIDevtoolsClientMetadata } from './devtools'\nimport type {\n GenerationDevtoolsBridgeFactory,\n VideoDevtoolsBridgeFactory,\n} from './devtools-noop'\n\n// ===========================\n// Inference Utilities\n// ===========================\n\n/**\n * Maps an `onResult` transform's raw return type to the stored output type.\n *\n * - A concrete return (excluding null/void/undefined) becomes the output type.\n * - A return of only null/void/undefined falls back to TResult (the transform\n * reacted to the result or chose to keep it, rather than replacing it).\n *\n * Hooks infer `TReturn` directly from the `onResult` return position — a\n * covariant inference site that works even for an optional nested property —\n * which both contextually types the callback parameter as `TResult` and\n * narrows `result`. See issue #848.\n *\n * @template TResult - The raw result type from the generation\n * @template TReturn - The transform's return type (defaults to `void` when no\n * transform is provided)\n */\nexport type InferGenerationOutputFromReturn<TResult, TReturn> = [\n Exclude<TReturn, null | void | undefined>,\n] extends [never]\n ? TResult\n : Exclude<TReturn, null | void | undefined>\n\n/**\n * Infers the output type from an `onResult` callback's type.\n *\n * - If the callback returns a concrete type (excluding null/void/undefined), uses that type.\n * - If the callback only returns null/void/undefined, or is not provided, falls back to TResult.\n *\n * @template TResult - The raw result type from the generation\n * @template TFn - The onResult callback type (or undefined if not provided)\n */\nexport type InferGenerationOutput<TResult, TFn> = TFn extends (\n result: any,\n) => infer R\n ? InferGenerationOutputFromReturn<TResult, R>\n : TResult\n\n// ===========================\n// State\n// ===========================\n\n/**\n * State machine for generation clients.\n * Simpler than ChatClientState since generation is a single request/response cycle.\n */\nexport type GenerationClientState = 'idle' | 'generating' | 'success' | 'error'\n\n/**\n * Status of a persisted/restored generation run.\n *\n * `running` / `complete` / `error` are the three the server-side mapper emits\n * over the wire. `idle` is client-local only: `stop()` rewrites a `running`\n * snapshot to it so a cancelled run is no longer resumable.\n *\n * @internal\n */\nexport type GenerationResumeStatus = 'idle' | 'running' | 'complete' | 'error'\n\n/**\n * Thrown when a generation stream ends without a terminal `RUN_FINISHED` /\n * `RUN_ERROR` chunk — a proxy/load-balancer idle timeout, a server restart\n * mid-run, or a durable log whose terminal append never landed. The run's\n * outcome is unknowable from the client, so it settles as an error rather than\n * leaving the client stuck on `generating` forever.\n */\nexport const GENERATION_STREAM_TRUNCATED_MESSAGE =\n 'The generation stream ended before the run finished (no RUN_FINISHED or RUN_ERROR was received) — the connection was interrupted. Generate again to retry.'\n\n/**\n * Reported when a restored snapshot says the run completed but the activity's\n * `reconstructResult` mapper cannot rebuild a result from it — typically an\n * output artifact persisted without a serve `url`. Surfacing it beats a\n * `success` status with a `null` result, which no consumer can render.\n */\nexport const GENERATION_UNRESTORABLE_RESULT_MESSAGE =\n '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.'\n\n/**\n * Wrap a mount-hydration failure with context. Genuine failures (transport\n * error, a 403 from the authorize gate, an unparseable body, a record the\n * client's validator rejects) must reach the app; only a genuine miss — the\n * server reporting no record for the thread — stays silent.\n */\nexport function createGenerationHydrationError(\n detail: string,\n cause?: unknown,\n): Error {\n const suffix = cause instanceof Error ? `: ${cause.message}` : ''\n const error = new Error(\n `[TanStack AI] Restoring the last generation for this thread failed — ${detail}${suffix}`,\n )\n if (cause !== undefined) {\n error.cause = cause\n }\n return error\n}\n\n/**\n * Map a persisted resume status to the client's live state machine on restore:\n * complete → success, error → error, running → generating, idle → idle. A\n * restored `running` only reaches this mapping when a `joinRun` handler can\n * tail the run to completion; without one the client rewrites the snapshot to\n * `error` (interrupted) before repainting, so it never sticks on `generating`.\n */\nexport function clientStateFromResumeStatus(\n status: GenerationResumeStatus,\n): GenerationClientState {\n switch (status) {\n case 'complete':\n return 'success'\n case 'error':\n return 'error'\n case 'running':\n return 'generating'\n case 'idle':\n return 'idle'\n }\n}\n\n/** @internal */\nexport interface GenerationResumeState {\n threadId: string\n runId: string\n /**\n * Artifact refs observed while the run is still in flight. Non-null only while\n * a run is streaming (`resumeState` itself is null once it ends); the final\n * refs move onto `result.artifacts` when the run completes.\n */\n pendingArtifacts?: Array<PersistedArtifactRef>\n}\n\n/** @internal */\nexport interface GenerationResultSnapshot {\n id?: string\n model?: string\n status?: string\n /**\n * The provider's async job handle (e.g. a Veo/fal video job id used for\n * status polling) — NOT the generation's own `runId`, which lives on\n * {@link GenerationResumeState.runId}.\n */\n providerJobId?: string\n expiresAt?: string\n /**\n * The text output of a text activity (a transcription's `text` or a summary's\n * `summary`). Persisted so a text generation restores its result on reload\n * (text is small and not bytes). Absent for media activities, whose output\n * restores from `artifacts`.\n */\n text?: string\n /** Token usage, persisted so a text result that requires it can be rebuilt. */\n usage?: TokenUsage\n artifacts?: Array<PersistedArtifactRef>\n}\n\n/** @internal */\nexport interface GenerationErrorSnapshot {\n message: string\n code?: string\n}\n\n/** @internal */\nexport interface GenerationEventSnapshot {\n type: StreamChunk['type']\n name?: string\n timestamp?: number\n}\n\n/** @internal */\nexport interface GenerationResumeSnapshot {\n /**\n * Version of the snapshot shape. Written on every snapshot the client builds\n * so future shape changes can migrate (or reject) an older record hydrated\n * from the server. Absent means `1`.\n */\n schemaVersion?: 1\n resumeState: GenerationResumeState | null\n status: GenerationResumeStatus\n activity?: PersistedArtifactRef['source']['activity']\n pendingArtifacts?: Array<PersistedArtifactRef>\n result?: GenerationResultSnapshot\n error?: GenerationErrorSnapshot\n lastEvent?: GenerationEventSnapshot\n}\n\n/**\n * The `persistence` / `threadId` identity shared by every generation hook.\n *\n * Turning persistence on **requires** a `threadId`, the stable scope runs are\n * filed under. Without one the client would hydrate by a generated id that\n * changes every reload, so nothing would ever restore; making it a type error\n * means the compiler asks for the scope instead of the runtime inventing one.\n *\n * `threadId` is the only identity for the hook, the AG-UI wire thread, and\n * persistence. There is no separate instance `id`.\n *\n * USAGE: intersect this onto a hook's parameter and subtract `persistence`\n * and `threadId` from the options interface (plus any other keys the hook\n * redeclares, such as `onResult`), leaving that interface a plain (non-union)\n * object so `Pick` / `Omit` composition elsewhere keeps working:\n *\n * ```ts\n * options: Omit<UseGenerateImageOptions, 'onResult' | 'persistence' | 'threadId'> & {\n * onResult?: (result: ImageGenerationResult) => TTransformed\n * } & GenerationPersistenceOptions\n * ```\n *\n * Do NOT bake the union into the options interface itself: a later plain `Omit`\n * over a union collapses it to a single object type and the requirement\n * silently disappears. `use-generation-persistence-types.test.ts` pins this.\n */\nexport type GenerationPersistenceOptions =\n | {\n persistence: true\n /** Required by `persistence`. The stable scope runs are filed under. */\n threadId: string\n }\n | {\n persistence?: false | undefined\n /** Stable scope for the generation slot (also the wire / DevTools identity). */\n threadId?: string\n }\n\n// ===========================\n// Event Constants\n// ===========================\n\n/**\n * Well-known CUSTOM event names used by generation clients.\n * These events are emitted by the server-side streaming helpers\n * and consumed by the client-side GenerationClient.\n */\nexport const GENERATION_EVENTS = {\n /** The generation result payload */\n RESULT: 'generation:result',\n /** Persisted artifact refs for generated media */\n ARTIFACTS: 'generation:artifacts',\n /** Progress update (0-100) with optional message */\n PROGRESS: 'generation:progress',\n /** Video job created with jobId */\n VIDEO_JOB_CREATED: 'video:job:created',\n /** Video job status update */\n VIDEO_STATUS: 'video:status',\n} as const\n\n// ===========================\n// Transport Types\n// ===========================\n\n/**\n * Options passed to a fetcher function by the generation client.\n */\nexport interface GenerationFetcherOptions {\n /** AbortSignal that is triggered when the user calls `stop()` */\n signal: AbortSignal\n}\n\n/**\n * A direct async function that performs a generation request.\n *\n * Can return the result directly, or return a `Response` with an SSE body\n * (e.g., from a TanStack Start server function using `toServerSentEventsResponse()`).\n * When a `Response` is returned, the client will parse it as an SSE stream.\n *\n * @template TInput - The input type for the generation request\n * @template TResult - The result type returned by the generation\n */\nexport type GenerationFetcher<TInput, TResult> = (\n input: TInput,\n options?: GenerationFetcherOptions,\n) => Promise<TResult | Response>\n\n/**\n * Transport configuration for generation clients.\n * Supports either a connect-based streaming adapter or a direct fetcher function.\n */\nexport type GenerationTransport<TInput, TResult> =\n | { connection: ConnectConnectionAdapter; fetcher?: never }\n | { fetcher: GenerationFetcher<TInput, TResult>; connection?: never }\n\n// ===========================\n// Client Options\n// ===========================\n\n/**\n * Options for the GenerationClient.\n *\n * @template TInput - The input type for the generation request (used by consuming code)\n * @template TResult - The result type returned by the generation\n * @template TOutput - The output type after optional transform (defaults to TResult)\n */\n// eslint-disable-next-line @typescript-eslint/naming-convention -- _TInput is unused in the interface body but part of the public positional generic API (callers supply it for inference)\nexport interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {\n /**\n * The **scope** this generation belongs to: a stable, app-chosen name for the\n * slot successive runs fill, not a link to a chat conversation. This is the\n * only identity for the client: wire thread id, DevTools hook id, and\n * persistence key.\n *\n * A generation hook starts empty and produces many runs over its life. Each\n * run gets its own `runId`, but they all belong to one scope. Persistence\n * keys on this: server-driven hydrates the last run for it on mount. It is\n * also sent as the AG-UI thread id on the wire, since the protocol requires\n * one.\n *\n * Derive it from your own domain. It must be meaningful before any media\n * exists and identical after a reload:\n *\n * ```ts\n * threadId: `video-${videoId}-start-frame`\n * ```\n *\n * **Required whenever `persistence` is set.** An app that cannot name the\n * scope has nothing to restore *to*, and a generated fallback would key each\n * reload differently, silently restoring nothing. Optional only for\n * ephemeral runs. If omitted, the client mints a wire id after mount.\n */\n threadId?: string\n\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n\n /** Metadata used to register this generation hook with TanStack AI Devtools */\n devtools?: Partial<AIDevtoolsClientMetadata>\n\n /**\n * How this generation persists across reloads.\n *\n * - Omit or `false`: ephemeral, in-memory only.\n * - `true`: server-driven. On mount the client hydrates the last generation\n * for its `threadId` from the server (needs a `hydrateGeneration` handler,\n * from the connection or the option below) and repaints that snapshot. It\n * never auto-starts a run.\n *\n * The record lives on the server, written by `withGenerationPersistence`. The\n * browser caches nothing, so a generation's history is never duplicated into\n * client storage.\n */\n persistence?: boolean\n\n /**\n * Server-driven hydration handler, for transports that don't carry one on\n * the connection: supply it alongside `fetcher` (or a `stream()` /\n * `rpcStream()` connection built without handlers) so `persistence: true`\n * can restore the last generation for `threadId` on mount. Typically a\n * one-line TanStack Start server-function call backed by\n * `getGenerationHydration` from `@tanstack/ai-persistence`.\n *\n * A connection's own `hydrateGeneration` takes precedence when both exist.\n */\n hydrateGeneration?: ConnectConnectionAdapter['hydrateGeneration']\n\n /**\n * Re-attach handler for a run that is still generating, for transports that\n * don't carry one on the connection. The client tails this on mount when a\n * restored/hydrated snapshot reports a run in flight, replaying it to\n * completion in place. Without it, a restored `running` snapshot surfaces\n * as an (interrupted) error — an interrupted generation cannot be resumed,\n * only re-run.\n *\n * A connection's own `joinRun` takes precedence when both exist.\n */\n joinRun?: ConnectConnectionAdapter['joinRun']\n\n /**\n * Factory that constructs the devtools bridge. Default is a no-op\n * factory; the real implementation lives in `@tanstack/ai-client/devtools`.\n */\n devtoolsBridgeFactory?: GenerationDevtoolsBridgeFactory\n\n /**\n * Callback when a result is received. Can optionally return a transformed value\n * that replaces the stored result.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: TResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n\n // Framework state callbacks (set by hooks, not users)\n /** @internal Called when result changes */\n onResultChange?: (result: TOutput | null) => void\n /** @internal Called when loading state changes */\n onLoadingChange?: (isLoading: boolean) => void\n /** @internal Called when error state changes */\n onErrorChange?: (error: Error | undefined) => void\n /** @internal Called when generation status changes */\n onStatusChange?: (status: GenerationClientState) => void\n /** @internal Called when lightweight resume snapshot changes. Receives `undefined` when the snapshot is cleared by `reset()`. */\n onResumeSnapshotChange?: (\n snapshot: GenerationResumeSnapshot | undefined,\n ) => void\n /** @internal Called when the in-flight run identity changes. `null` once no run is in flight. Mirrors the chat client's resume-state callback. */\n onResumeStateChange?: (resumeState: GenerationResumeState | null) => void\n\n /**\n * @internal Rebuild a typed result from a restored snapshot, injected by each\n * specialized client/hook (which knows the concrete result shape). Called on\n * mount restore (client store or server hydrate) so `result` repaints as if the\n * run had just finished, with media resolved to the durable serve URL. Returns\n * `null` when the snapshot cannot rebuild a result (then `result` stays null;\n * `status` / `error` / `resumeState` still repaint).\n */\n reconstructResult?: (restored: GenerationRestoredResult) => TResult | null\n}\n\n/**\n * The restorable shape handed to a client's `reconstructResult` mapper: the\n * result metadata that survived persistence plus the durable artifact refs (each\n * carrying its serve {@link PersistedArtifactRef.url}). The specialized client\n * turns this into its own typed result (image `images`, video `url`, text\n * `text`, ...).\n */\nexport interface GenerationRestoredResult {\n id?: string\n model?: string\n status?: string\n /** The provider's async job handle — see {@link GenerationResultSnapshot.providerJobId}. */\n providerJobId?: string\n expiresAt?: string\n text?: string\n usage?: TokenUsage\n activity?: PersistedArtifactRef['source']['activity']\n artifacts: Array<PersistedArtifactRef>\n}\n\n/**\n * Reduces one observed stream chunk into the lightweight resume snapshot.\n *\n * A `RUN_STARTED` chunk begins a fresh run, so stale `result` / `error` /\n * `pendingArtifacts` from a previous run are dropped rather than carried into\n * the new run's snapshot.\n *\n * @internal\n */\nexport function updateGenerationResumeSnapshot(\n previous: GenerationResumeSnapshot | null | undefined,\n chunk: StreamChunk,\n): GenerationResumeSnapshot {\n const threadId = stringField(chunk, 'threadId')\n const runId = stringField(chunk, 'runId')\n const carried = chunk.type === 'RUN_STARTED' ? undefined : previous\n const previousArtifacts = carried?.pendingArtifacts ?? []\n const next: GenerationResumeSnapshot = {\n schemaVersion: 1,\n resumeState: carried?.resumeState ?? null,\n status: carried?.status ?? 'idle',\n ...(carried?.activity ? { activity: carried.activity } : {}),\n ...(previousArtifacts.length > 0\n ? { pendingArtifacts: [...previousArtifacts] }\n : {}),\n ...(carried?.result ? { result: { ...carried.result } } : {}),\n ...(carried?.error ? { error: { ...carried.error } } : {}),\n lastEvent: createGenerationEventSnapshot(chunk),\n }\n\n if (threadId && runId) {\n next.resumeState = { threadId, runId }\n next.status = 'running'\n } else if (chunk.type === 'RUN_STARTED') {\n next.status = 'running'\n }\n\n if (chunk.type === 'CUSTOM') {\n if (chunk.name === GENERATION_EVENTS.ARTIFACTS) {\n const artifacts = collectArtifactRefs(chunk.value)\n if (artifacts.length > 0) {\n next.pendingArtifacts = artifacts\n next.activity = artifacts[0]?.source.activity\n }\n } else if (chunk.name === GENERATION_EVENTS.RESULT) {\n const result = createGenerationResultSnapshot(chunk.value)\n if (result) {\n next.result = result\n if (result.artifacts && result.artifacts.length > 0) {\n next.pendingArtifacts = result.artifacts\n next.activity = result.artifacts[0]?.source.activity\n }\n }\n } else if (chunk.name === GENERATION_EVENTS.VIDEO_JOB_CREATED) {\n // Capture the provider job id as soon as the job exists — for a long\n // video run this is the one piece of identity worth having after a\n // reload, and the terminal `generation:result` may never arrive.\n const providerJobId = isObject(chunk.value)\n ? stringField(chunk.value, 'jobId')\n : undefined\n if (providerJobId) {\n next.result = { ...next.result, providerJobId }\n }\n }\n } else if (chunk.type === 'RUN_FINISHED') {\n next.resumeState = null\n next.status = 'complete'\n } else if (chunk.type === 'RUN_ERROR') {\n next.resumeState = null\n next.status = 'error'\n next.error = createGenerationErrorSnapshot(chunk)\n }\n\n return next\n}\n\n/**\n * Validates an untrusted value (a hydration body resolved by the server) into a\n * {@link GenerationResumeSnapshot}, or returns `undefined` when the value is\n * not a usable snapshot.\n *\n * A hydrated record is outside the type system: it may be stale, truncated, or\n * written by a different version. Every field is re-validated with the same\n * narrowing the live chunk reducer uses. `lastEvent` is not restored, since it\n * describes a transient stream position with no meaning after a reload.\n *\n * @internal\n */\nexport function parseGenerationResumeSnapshot(\n value: unknown,\n): GenerationResumeSnapshot | undefined {\n if (!isObject(value)) return undefined\n\n const schemaVersion = Reflect.get(value, 'schemaVersion')\n if (schemaVersion !== undefined && schemaVersion !== 1) return undefined\n\n const status = generationResumeStatusField(value, 'status')\n if (!status) return undefined\n\n const rawResumeState = Reflect.get(value, 'resumeState')\n let resumeState: GenerationResumeState | null = null\n if (rawResumeState !== null && rawResumeState !== undefined) {\n if (!isObject(rawResumeState)) return undefined\n const threadId = stringField(rawResumeState, 'threadId')\n const runId = stringField(rawResumeState, 'runId')\n if (!threadId || !runId) return undefined\n resumeState = { threadId, runId }\n }\n\n const snapshot: GenerationResumeSnapshot = {\n schemaVersion: 1,\n resumeState,\n status,\n }\n\n const activity = persistedArtifactActivityField(value, 'activity')\n if (activity) snapshot.activity = activity\n\n const pendingArtifacts = collectArtifactRefs(\n Reflect.get(value, 'pendingArtifacts'),\n )\n if (pendingArtifacts.length > 0) snapshot.pendingArtifacts = pendingArtifacts\n\n const result = createGenerationResultSnapshot(Reflect.get(value, 'result'))\n if (result) snapshot.result = result\n\n const rawError = Reflect.get(value, 'error')\n if (isObject(rawError)) {\n const message = stringField(rawError, 'message')\n if (message) {\n const code = stringField(rawError, 'code')\n snapshot.error = { message, ...(code ? { code } : {}) }\n }\n }\n\n return snapshot\n}\n\nfunction generationResumeStatusField(\n value: object,\n key: string,\n): GenerationResumeStatus | undefined {\n const field = stringField(value, key)\n if (field === undefined) return undefined\n\n switch (field) {\n case 'idle':\n case 'running':\n case 'complete':\n case 'error':\n return field\n default:\n return undefined\n }\n}\n\n// ===========================\n// Video-Specific Options\n// ===========================\n\n/**\n * Video status information returned during job polling.\n */\nexport interface VideoStatusInfo {\n /** Job identifier */\n jobId: string\n /** Current status of the video generation job */\n status: 'pending' | 'processing' | 'completed' | 'failed'\n /** Progress percentage (0-100), if available */\n progress?: number\n /** URL to the generated video (when completed) */\n url?: string\n /** Error message if status is 'failed' */\n error?: string\n}\n\n/**\n * Composite result for video generation (job completion).\n */\nexport interface VideoGenerateResult {\n /** Job identifier */\n jobId: string\n /** Final status */\n status: 'completed'\n /** URL to the generated video */\n url: string\n /** When the URL expires, if applicable */\n expiresAt?: Date\n /** Persisted artifact references for generated assets, when available */\n artifacts?: Array<PersistedArtifactRef>\n}\n\n/**\n * Options for the VideoGenerationClient.\n */\nexport interface VideoGenerationClientOptions<\n TOutput = VideoGenerateResult,\n> extends Omit<\n GenerationClientOptions<VideoGenerateInput, VideoGenerateResult, TOutput>,\n 'devtoolsBridgeFactory'\n> {\n /**\n * Factory that constructs the video devtools bridge. Default is a no-op\n * factory; the real implementation lives in `@tanstack/ai-client/devtools`.\n */\n devtoolsBridgeFactory?: VideoDevtoolsBridgeFactory\n\n /** Callback when a video job is created */\n onJobCreated?: (jobId: string) => void\n /** Callback on each status update */\n onStatusUpdate?: (status: VideoStatusInfo) => void\n\n // Framework state callbacks\n /** @internal Called when jobId changes */\n onJobIdChange?: (jobId: string | null) => void\n /** @internal Called when video status changes */\n onVideoStatusChange?: (status: VideoStatusInfo | null) => void\n}\n\n// ===========================\n// Input Types\n// ===========================\n\n/**\n * Input for image generation.\n */\nexport interface ImageGenerateInput {\n /**\n * Description of the desired image(s): plain text, or an ordered array of\n * content parts (text + image) for image-conditioned generation\n * (image-to-image, multi-reference, edit / inpaint).\n */\n prompt: MediaPrompt\n /** Number of images to generate (default: 1) */\n numberOfImages?: number\n /** Image size in WIDTHxHEIGHT format (e.g., \"1024x1024\") */\n size?: string\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for audio generation (music, sound effects).\n */\nexport interface AudioGenerateInput {\n /** Text description of the desired audio */\n prompt: string\n /** Desired duration in seconds */\n duration?: number\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for text-to-speech generation.\n */\nexport interface SpeechGenerateInput {\n /** The text to convert to speech */\n text: string\n /** The voice to use for generation */\n voice?: string\n /** The output audio format */\n format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'\n /** The speed of the generated audio (0.25 to 4.0) */\n speed?: number\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for audio transcription.\n */\nexport interface TranscriptionGenerateInput {\n /** The audio data to transcribe - can be base64 string, File, Blob, or ArrayBuffer */\n audio: string | File | Blob | ArrayBuffer\n /** The language of the audio in ISO-639-1 format (e.g., 'en') */\n language?: string\n /** An optional prompt to guide the transcription */\n prompt?: string\n /** The format of the transcription output */\n responseFormat?: TranscriptionResponseFormat\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for text summarization.\n */\nexport interface SummarizeGenerateInput {\n /** The text to summarize */\n text: string\n /** Maximum length of the summary */\n maxLength?: number\n /** Style of the summary */\n style?: 'bullet-points' | 'paragraph' | 'concise'\n /** Topics to focus on */\n focus?: Array<string>\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for video generation.\n */\nexport interface VideoGenerateInput {\n /**\n * Description of the desired video: plain text, or an ordered array of\n * content parts (text + image) for image-conditioned generation\n * (image-to-video, start/end frames).\n */\n prompt: MediaPrompt\n /** Video size — format depends on provider (e.g., \"16:9\", \"1280x720\") */\n size?: string\n /** Video duration in seconds */\n duration?: number\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\nfunction createGenerationEventSnapshot(\n chunk: StreamChunk,\n): GenerationEventSnapshot {\n const name = stringField(chunk, 'name')\n const timestamp = numberField(chunk, 'timestamp')\n return {\n type: chunk.type,\n ...(name ? { name } : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n }\n}\n\n/** @internal Narrows an untrusted result payload into the persisted result snapshot shape. */\nexport function createGenerationResultSnapshot(\n value: unknown,\n): GenerationResultSnapshot | undefined {\n if (!isObject(value)) return undefined\n\n const artifacts = collectArtifactRefs(Reflect.get(value, 'artifacts'))\n const snapshot: GenerationResultSnapshot = {}\n const id = stringField(value, 'id')\n const model = stringField(value, 'model')\n const status = stringField(value, 'status')\n // A live provider result carries its job handle as `jobId` (e.g.\n // `VideoGenerateResult.jobId`); a persisted snapshot carries it as\n // `providerJobId`. Accept both — this narrows raw results AND stored\n // snapshots.\n const providerJobId =\n stringField(value, 'providerJobId') ?? stringField(value, 'jobId')\n // A transcription's output is `text`; a summary's is `summary`. Capture either\n // under `text` so a text result restores on reload.\n const text = stringField(value, 'text') ?? stringField(value, 'summary')\n const usage = Reflect.get(value, 'usage')\n if (id) snapshot.id = id\n if (model) snapshot.model = model\n if (status) snapshot.status = status\n if (providerJobId) snapshot.providerJobId = providerJobId\n if (text) snapshot.text = text\n // Passthrough opaque token-usage metadata (untrusted; not deeply validated).\n if (isObject(usage)) snapshot.usage = usage as TokenUsage\n const expiresAt = Reflect.get(value, 'expiresAt')\n if (typeof expiresAt === 'string') {\n snapshot.expiresAt = expiresAt\n } else if (expiresAt instanceof Date && !Number.isNaN(expiresAt.getTime())) {\n // `toISOString()` throws on an invalid Date. This runs per chunk on live\n // provider values, so drop an unusable date like every other bad field\n // here rather than throwing out of the stream loop.\n snapshot.expiresAt = expiresAt.toISOString()\n }\n if (artifacts.length > 0) {\n snapshot.artifacts = artifacts\n }\n\n return Object.keys(snapshot).length > 0 ? snapshot : undefined\n}\n\nfunction createGenerationErrorSnapshot(\n chunk: StreamChunk,\n): GenerationErrorSnapshot {\n const message =\n stringField(chunk, 'message') ??\n nestedStringField(chunk, 'error', 'message') ??\n 'An error occurred'\n const code = stringField(chunk, 'code')\n return {\n message,\n ...(code ? { code } : {}),\n }\n}\n\nfunction collectArtifactRefs(value: unknown): Array<PersistedArtifactRef> {\n if (!Array.isArray(value)) return []\n const refs: Array<PersistedArtifactRef> = []\n for (const item of value) {\n const ref = createPersistedArtifactRefSnapshot(item)\n if (ref) {\n refs.push(ref)\n }\n }\n return refs\n}\n\nfunction createPersistedArtifactRefSnapshot(\n value: unknown,\n): PersistedArtifactRef | undefined {\n if (!isObject(value)) return undefined\n const source = Reflect.get(value, 'source')\n if (!isObject(source)) return undefined\n\n const role = persistedArtifactRoleField(value, 'role')\n const artifactId = stringField(value, 'artifactId')\n const threadId = stringField(value, 'threadId')\n const runId = stringField(value, 'runId')\n const name = stringField(value, 'name')\n const mimeType = stringField(value, 'mimeType')\n const size = numberField(value, 'size')\n const createdAt = stringField(value, 'createdAt')\n const activity = persistedArtifactActivityField(source, 'activity')\n const path = stringField(source, 'path')\n const provider = stringField(source, 'provider')\n const model = stringField(source, 'model')\n if (\n !role ||\n !artifactId ||\n !threadId ||\n !runId ||\n !name ||\n !mimeType ||\n size === undefined ||\n !createdAt ||\n !activity ||\n !path ||\n !provider ||\n !model\n ) {\n return undefined\n }\n\n const sourceUrl = durableUrlField(value, 'sourceUrl')\n const url = serveUrlField(value, 'url')\n const mediaType = persistedArtifactMediaTypeField(source, 'mediaType')\n const jobId = stringField(source, 'jobId')\n const expiresAt = stringField(source, 'expiresAt')\n\n return {\n role,\n artifactId,\n threadId,\n runId,\n name,\n mimeType,\n size,\n createdAt,\n ...(sourceUrl ? { sourceUrl } : {}),\n ...(url ? { url } : {}),\n source: {\n activity,\n path,\n provider,\n model,\n ...(mediaType ? { mediaType } : {}),\n ...(jobId ? { jobId } : {}),\n ...(expiresAt ? { expiresAt } : {}),\n },\n }\n}\n\nfunction durableUrlField(value: object, key: string): string | undefined {\n const field = stringField(value, key)\n if (!field || field.length > 2048) return undefined\n try {\n const url = new URL(field)\n return url.protocol === 'http:' || url.protocol === 'https:'\n ? field\n : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Validates an app-origin serve URL, which unlike a provider URL is usually a\n * same-origin path (`/api/.../artifact?id=...`). Accepts an absolute http(s) URL\n * or a path-absolute same-origin URL (single leading `/`); rejects\n * protocol-relative (`//host`), `javascript:` / `data:`, and anything else, since\n * this value is rendered as media `src`.\n */\nfunction serveUrlField(value: object, key: string): string | undefined {\n const field = stringField(value, key)\n if (!field || field.length > 2048) return undefined\n // A single leading `/` is a safe same-origin path. Reject protocol-relative\n // `//host` AND a backslash bypass (`/\\host` — the URL parser treats `\\` as `/`\n // for http(s), so it would resolve to a foreign origin as an `<img src>`).\n if (field.startsWith('/') && !field.startsWith('//') && !field.includes('\\\\'))\n return field\n try {\n const url = new URL(field)\n return url.protocol === 'http:' || url.protocol === 'https:'\n ? field\n : undefined\n } catch {\n return undefined\n }\n}\n\nfunction persistedArtifactRoleField(\n value: object,\n key: string,\n): PersistedArtifactRef['role'] | undefined {\n const field = stringField(value, key)\n return field === 'input' || field === 'output' ? field : undefined\n}\n\nfunction persistedArtifactActivityField(\n value: object,\n key: string,\n): PersistedArtifactRef['source']['activity'] | undefined {\n const field = stringField(value, key)\n if (field === undefined) return undefined\n\n switch (field) {\n case 'image':\n case 'audio':\n case 'tts':\n case 'video':\n case 'transcription':\n return field\n default:\n return undefined\n }\n}\n\nfunction persistedArtifactMediaTypeField(\n value: object,\n key: string,\n): PersistedArtifactRef['source']['mediaType'] | undefined {\n const field = stringField(value, key)\n if (field === undefined) return undefined\n\n switch (field) {\n case 'image':\n case 'audio':\n case 'video':\n case 'document':\n case 'json':\n return field\n default:\n return undefined\n }\n}\n\nfunction nestedStringField(\n value: object,\n key: string,\n nestedKey: string,\n): string | undefined {\n const nested = Reflect.get(value, key)\n return isObject(nested) ? stringField(nested, nestedKey) : undefined\n}\n\nfunction stringField(value: object, key: string): string | undefined {\n const field = Reflect.get(value, key)\n return typeof field === 'string' ? field : undefined\n}\n\nfunction numberField(value: object, key: string): number | undefined {\n const field = Reflect.get(value, key)\n return typeof field === 'number' ? field : undefined\n}\n\nfunction isObject(value: unknown): value is object {\n return typeof value === 'object' && value !== null\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"generation-types.js","names":[],"sources":["../../src/generation-types.ts"],"sourcesContent":["import { tanstackMetadata } from '@tanstack/ai/client'\nimport type {\n MediaPrompt,\n PersistedArtifactRef,\n StreamChunk,\n} from '@tanstack/ai/client'\nimport type { TokenUsage, TranscriptionResponseFormat } from '@tanstack/ai'\nimport type { ConnectConnectionAdapter } from './connection-adapters'\nimport type { AIDevtoolsClientMetadata } from './devtools'\nimport type {\n GenerationDevtoolsBridgeFactory,\n VideoDevtoolsBridgeFactory,\n} from './devtools-noop'\n\n// ===========================\n// Inference Utilities\n// ===========================\n\n/**\n * Maps an `onResult` transform's raw return type to the stored output type.\n *\n * - A concrete return (excluding null/void/undefined) becomes the output type.\n * - A return of only null/void/undefined falls back to TResult (the transform\n * reacted to the result or chose to keep it, rather than replacing it).\n *\n * Hooks infer `TReturn` directly from the `onResult` return position — a\n * covariant inference site that works even for an optional nested property —\n * which both contextually types the callback parameter as `TResult` and\n * narrows `result`. See issue #848.\n *\n * @template TResult - The raw result type from the generation\n * @template TReturn - The transform's return type (defaults to `void` when no\n * transform is provided)\n */\nexport type InferGenerationOutputFromReturn<TResult, TReturn> = [\n Exclude<TReturn, null | void | undefined>,\n] extends [never]\n ? TResult\n : Exclude<TReturn, null | void | undefined>\n\n/**\n * Infers the output type from an `onResult` callback's type.\n *\n * - If the callback returns a concrete type (excluding null/void/undefined), uses that type.\n * - If the callback only returns null/void/undefined, or is not provided, falls back to TResult.\n *\n * @template TResult - The raw result type from the generation\n * @template TFn - The onResult callback type (or undefined if not provided)\n */\nexport type InferGenerationOutput<TResult, TFn> = TFn extends (\n result: any,\n) => infer R\n ? InferGenerationOutputFromReturn<TResult, R>\n : TResult\n\n// ===========================\n// State\n// ===========================\n\n/**\n * State machine for generation clients.\n * Simpler than ChatClientState since generation is a single request/response cycle.\n */\nexport type GenerationClientState = 'idle' | 'generating' | 'success' | 'error'\n\n/**\n * Status of a persisted/restored generation run.\n *\n * `running` / `complete` / `error` are the three the server-side mapper emits\n * over the wire. `idle` is client-local only: `stop()` rewrites a `running`\n * snapshot to it so a cancelled run is no longer resumable.\n *\n * @internal\n */\nexport type GenerationResumeStatus = 'idle' | 'running' | 'complete' | 'error'\n\n/**\n * Thrown when a generation stream ends without a terminal `RUN_FINISHED` /\n * `RUN_ERROR` chunk — a proxy/load-balancer idle timeout, a server restart\n * mid-run, or a durable log whose terminal append never landed. The run's\n * outcome is unknowable from the client, so it settles as an error rather than\n * leaving the client stuck on `generating` forever.\n */\nexport const GENERATION_STREAM_TRUNCATED_MESSAGE =\n 'The generation stream ended before the run finished (no RUN_FINISHED or RUN_ERROR was received) — the connection was interrupted. Generate again to retry.'\n\n/**\n * Reported when a restored snapshot says the run completed but the activity's\n * `reconstructResult` mapper cannot rebuild a result from it — typically an\n * output artifact persisted without a serve `url`. Surfacing it beats a\n * `success` status with a `null` result, which no consumer can render.\n */\nexport const GENERATION_UNRESTORABLE_RESULT_MESSAGE =\n '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.'\n\n/**\n * Wrap a mount-hydration failure with context. Genuine failures (transport\n * error, a 403 from the authorize gate, an unparseable body, a record the\n * client's validator rejects) must reach the app; only a genuine miss — the\n * server reporting no record for the thread — stays silent.\n */\nexport function createGenerationHydrationError(\n detail: string,\n cause?: unknown,\n): Error {\n const suffix = cause instanceof Error ? `: ${cause.message}` : ''\n const error = new Error(\n `[TanStack AI] Restoring the last generation for this thread failed — ${detail}${suffix}`,\n )\n if (cause !== undefined) {\n error.cause = cause\n }\n return error\n}\n\n/**\n * Map a persisted resume status to the client's live state machine on restore:\n * complete → success, error → error, running → generating, idle → idle. A\n * restored `running` only reaches this mapping when a `joinRun` handler can\n * tail the run to completion; without one the client rewrites the snapshot to\n * `error` (interrupted) before repainting, so it never sticks on `generating`.\n */\nexport function clientStateFromResumeStatus(\n status: GenerationResumeStatus,\n): GenerationClientState {\n switch (status) {\n case 'complete':\n return 'success'\n case 'error':\n return 'error'\n case 'running':\n return 'generating'\n case 'idle':\n return 'idle'\n }\n}\n\n/** @internal */\nexport interface GenerationResumeState {\n threadId: string\n runId: string\n /**\n * Artifact refs observed while the run is still in flight. Non-null only while\n * a run is streaming (`resumeState` itself is null once it ends); the final\n * refs move onto `result.artifacts` when the run completes.\n */\n pendingArtifacts?: Array<PersistedArtifactRef>\n}\n\n/** @internal */\nexport interface GenerationResultSnapshot {\n id?: string\n model?: string\n status?: string\n /**\n * The provider's async job handle (e.g. a Veo/fal video job id used for\n * status polling) — NOT the generation's own `runId`, which lives on\n * {@link GenerationResumeState.runId}.\n */\n providerJobId?: string\n expiresAt?: string\n /**\n * The text output of a text activity (a transcription's `text` or a summary's\n * `summary`). Persisted so a text generation restores its result on reload\n * (text is small and not bytes). Absent for media activities, whose output\n * restores from `artifacts`.\n */\n text?: string\n /** Token usage, persisted so a text result that requires it can be rebuilt. */\n usage?: TokenUsage\n artifacts?: Array<PersistedArtifactRef>\n}\n\n/** @internal */\nexport interface GenerationErrorSnapshot {\n message: string\n code?: string\n}\n\n/** @internal */\nexport interface GenerationEventSnapshot {\n type: StreamChunk['type']\n name?: string\n timestamp?: number\n}\n\n/** @internal */\nexport interface GenerationResumeSnapshot {\n /**\n * Version of the snapshot shape. Written on every snapshot the client builds\n * so future shape changes can migrate (or reject) an older record hydrated\n * from the server. Absent means `1`.\n */\n schemaVersion?: 1\n resumeState: GenerationResumeState | null\n status: GenerationResumeStatus\n activity?: PersistedArtifactRef['source']['activity']\n pendingArtifacts?: Array<PersistedArtifactRef>\n result?: GenerationResultSnapshot\n error?: GenerationErrorSnapshot\n lastEvent?: GenerationEventSnapshot\n}\n\n/**\n * The `persistence` / `threadId` identity shared by every generation hook.\n *\n * Turning persistence on **requires** a `threadId`, the stable scope runs are\n * filed under. Without one the client would hydrate by a generated id that\n * changes every reload, so nothing would ever restore; making it a type error\n * means the compiler asks for the scope instead of the runtime inventing one.\n *\n * `threadId` is the only identity for the hook, the AG-UI wire thread, and\n * persistence. There is no separate instance `id`.\n *\n * USAGE: intersect this onto a hook's parameter and subtract `persistence`\n * and `threadId` from the options interface (plus any other keys the hook\n * redeclares, such as `onResult`), leaving that interface a plain (non-union)\n * object so `Pick` / `Omit` composition elsewhere keeps working:\n *\n * ```ts\n * options: Omit<UseGenerateImageOptions, 'onResult' | 'persistence' | 'threadId'> & {\n * onResult?: (result: ImageGenerationResult) => TTransformed\n * } & GenerationPersistenceOptions\n * ```\n *\n * Do NOT bake the union into the options interface itself: a later plain `Omit`\n * over a union collapses it to a single object type and the requirement\n * silently disappears. `use-generation-persistence-types.test.ts` pins this.\n */\nexport type GenerationPersistenceOptions =\n | {\n persistence: true\n /** Required by `persistence`. The stable scope runs are filed under. */\n threadId: string\n }\n | {\n persistence?: false | undefined\n /** Stable scope for the generation slot (also the wire / DevTools identity). */\n threadId?: string\n }\n\n// ===========================\n// Event Constants\n// ===========================\n\n/**\n * Well-known CUSTOM event names used by generation clients.\n * These events are emitted by the server-side streaming helpers\n * and consumed by the client-side GenerationClient.\n */\nexport const GENERATION_EVENTS = {\n /** The generation result payload */\n RESULT: 'generation:result',\n /** Persisted artifact refs for generated media */\n ARTIFACTS: 'generation:artifacts',\n /** Progress update (0-100) with optional message */\n PROGRESS: 'generation:progress',\n /** Video job created with jobId */\n VIDEO_JOB_CREATED: 'video:job:created',\n /** Video job status update */\n VIDEO_STATUS: 'video:status',\n} as const\n\n// ===========================\n// Transport Types\n// ===========================\n\n/**\n * Options passed to a fetcher function by the generation client.\n */\nexport interface GenerationFetcherOptions {\n /** AbortSignal that is triggered when the user calls `stop()` */\n signal: AbortSignal\n}\n\n/**\n * A direct async function that performs a generation request.\n *\n * Can return the result directly, or return a `Response` with an SSE body\n * (e.g., from a TanStack Start server function using `toServerSentEventsResponse()`).\n * When a `Response` is returned, the client will parse it as an SSE stream.\n *\n * @template TInput - The input type for the generation request\n * @template TResult - The result type returned by the generation\n */\nexport type GenerationFetcher<TInput, TResult> = (\n input: TInput,\n options?: GenerationFetcherOptions,\n) => Promise<TResult | Response>\n\n/**\n * Transport configuration for generation clients.\n * Supports either a connect-based streaming adapter or a direct fetcher function.\n */\nexport type GenerationTransport<TInput, TResult> =\n | { connection: ConnectConnectionAdapter; fetcher?: never }\n | { fetcher: GenerationFetcher<TInput, TResult>; connection?: never }\n\n// ===========================\n// Client Options\n// ===========================\n\n/**\n * Options for the GenerationClient.\n *\n * @template TInput - The input type for the generation request (used by consuming code)\n * @template TResult - The result type returned by the generation\n * @template TOutput - The output type after optional transform (defaults to TResult)\n */\n// eslint-disable-next-line @typescript-eslint/naming-convention -- _TInput is unused in the interface body but part of the public positional generic API (callers supply it for inference)\nexport interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {\n /**\n * The **scope** this generation belongs to: a stable, app-chosen name for the\n * slot successive runs fill, not a link to a chat conversation. This is the\n * only identity for the client: wire thread id, DevTools hook id, and\n * persistence key.\n *\n * A generation hook starts empty and produces many runs over its life. Each\n * run gets its own `runId`, but they all belong to one scope. Persistence\n * keys on this: server-driven hydrates the last run for it on mount. It is\n * also sent as the AG-UI thread id on the wire, since the protocol requires\n * one.\n *\n * Derive it from your own domain. It must be meaningful before any media\n * exists and identical after a reload:\n *\n * ```ts\n * threadId: `video-${videoId}-start-frame`\n * ```\n *\n * **Required whenever `persistence` is set.** An app that cannot name the\n * scope has nothing to restore *to*, and a generated fallback would key each\n * reload differently, silently restoring nothing. Optional only for\n * ephemeral runs. If omitted, the client mints a wire id after mount.\n */\n threadId?: string\n\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n\n /** Metadata used to register this generation hook with TanStack AI Devtools */\n devtools?: Partial<AIDevtoolsClientMetadata>\n\n /**\n * How this generation persists across reloads.\n *\n * - Omit or `false`: ephemeral, in-memory only.\n * - `true`: server-driven. On mount the client hydrates the last generation\n * for its `threadId` from the server (needs a `hydrateGeneration` handler,\n * from the connection or the option below) and repaints that snapshot. It\n * never auto-starts a run.\n *\n * The record lives on the server, written by `withGenerationPersistence`. The\n * browser caches nothing, so a generation's history is never duplicated into\n * client storage.\n */\n persistence?: boolean\n\n /**\n * Server-driven hydration handler, for transports that don't carry one on\n * the connection: supply it alongside `fetcher` (or a `stream()` /\n * `rpcStream()` connection built without handlers) so `persistence: true`\n * can restore the last generation for `threadId` on mount. Typically a\n * one-line TanStack Start server-function call backed by\n * `getGenerationHydration` from `@tanstack/ai-persistence`.\n *\n * A connection's own `hydrateGeneration` takes precedence when both exist.\n */\n hydrateGeneration?: ConnectConnectionAdapter['hydrateGeneration']\n\n /**\n * Re-attach handler for a run that is still generating, for transports that\n * don't carry one on the connection. The client tails this on mount when a\n * restored/hydrated snapshot reports a run in flight, replaying it to\n * completion in place. Without it, a restored `running` snapshot surfaces\n * as an (interrupted) error — an interrupted generation cannot be resumed,\n * only re-run.\n *\n * A connection's own `joinRun` takes precedence when both exist.\n */\n joinRun?: ConnectConnectionAdapter['joinRun']\n\n /**\n * Factory that constructs the devtools bridge. Default is a no-op\n * factory; the real implementation lives in `@tanstack/ai-client/devtools`.\n */\n devtoolsBridgeFactory?: GenerationDevtoolsBridgeFactory\n\n /**\n * Callback when a result is received. Can optionally return a transformed value\n * that replaces the stored result.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: TResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n\n // Framework state callbacks (set by hooks, not users)\n /** @internal Called when result changes */\n onResultChange?: (result: TOutput | null) => void\n /** @internal Called when loading state changes */\n onLoadingChange?: (isLoading: boolean) => void\n /** @internal Called when error state changes */\n onErrorChange?: (error: Error | undefined) => void\n /** @internal Called when generation status changes */\n onStatusChange?: (status: GenerationClientState) => void\n /** @internal Called when lightweight resume snapshot changes. Receives `undefined` when the snapshot is cleared by `reset()`. */\n onResumeSnapshotChange?: (\n snapshot: GenerationResumeSnapshot | undefined,\n ) => void\n /** @internal Called when the in-flight run identity changes. `null` once no run is in flight. Mirrors the chat client's resume-state callback. */\n onResumeStateChange?: (resumeState: GenerationResumeState | null) => void\n\n /**\n * @internal Rebuild a typed result from a restored snapshot, injected by each\n * specialized client/hook (which knows the concrete result shape). Called on\n * mount restore (client store or server hydrate) so `result` repaints as if the\n * run had just finished, with media resolved to the durable serve URL. Returns\n * `null` when the snapshot cannot rebuild a result (then `result` stays null;\n * `status` / `error` / `resumeState` still repaint).\n */\n reconstructResult?: (restored: GenerationRestoredResult) => TResult | null\n}\n\n/**\n * The restorable shape handed to a client's `reconstructResult` mapper: the\n * result metadata that survived persistence plus the durable artifact refs (each\n * carrying its serve {@link PersistedArtifactRef.url}). The specialized client\n * turns this into its own typed result (image `images`, video `url`, text\n * `text`, ...).\n */\nexport interface GenerationRestoredResult {\n id?: string\n model?: string\n status?: string\n /** The provider's async job handle — see {@link GenerationResultSnapshot.providerJobId}. */\n providerJobId?: string\n expiresAt?: string\n text?: string\n usage?: TokenUsage\n activity?: PersistedArtifactRef['source']['activity']\n artifacts: Array<PersistedArtifactRef>\n}\n\n/**\n * Reduces one observed stream chunk into the lightweight resume snapshot.\n *\n * A `RUN_STARTED` chunk begins a fresh run, so stale `result` / `error` /\n * `pendingArtifacts` from a previous run are dropped rather than carried into\n * the new run's snapshot.\n *\n * @internal\n */\nexport function updateGenerationResumeSnapshot(\n previous: GenerationResumeSnapshot | null | undefined,\n chunk: StreamChunk,\n): GenerationResumeSnapshot {\n const tanstack = tanstackMetadata(chunk)\n const threadId =\n stringField(chunk, 'threadId') ??\n (typeof tanstack?.threadId === 'string' ? tanstack.threadId : undefined)\n const runId =\n stringField(chunk, 'runId') ??\n (typeof tanstack?.runId === 'string' ? tanstack.runId : undefined)\n const carried = chunk.type === 'RUN_STARTED' ? undefined : previous\n const previousArtifacts = carried?.pendingArtifacts ?? []\n const next: GenerationResumeSnapshot = {\n schemaVersion: 1,\n resumeState: carried?.resumeState ?? null,\n status: carried?.status ?? 'idle',\n ...(carried?.activity ? { activity: carried.activity } : {}),\n ...(previousArtifacts.length > 0\n ? { pendingArtifacts: [...previousArtifacts] }\n : {}),\n ...(carried?.result ? { result: { ...carried.result } } : {}),\n ...(carried?.error ? { error: { ...carried.error } } : {}),\n lastEvent: createGenerationEventSnapshot(chunk),\n }\n\n if (threadId && runId) {\n next.resumeState = { threadId, runId }\n next.status = 'running'\n } else if (chunk.type === 'RUN_STARTED') {\n next.status = 'running'\n }\n\n if (chunk.type === 'CUSTOM') {\n if (chunk.name === GENERATION_EVENTS.ARTIFACTS) {\n const artifacts = collectArtifactRefs(chunk.value)\n if (artifacts.length > 0) {\n next.pendingArtifacts = artifacts\n next.activity = artifacts[0]?.source.activity\n }\n } else if (chunk.name === GENERATION_EVENTS.RESULT) {\n const result = createGenerationResultSnapshot(chunk.value)\n if (result) {\n next.result = result\n if (result.artifacts && result.artifacts.length > 0) {\n next.pendingArtifacts = result.artifacts\n next.activity = result.artifacts[0]?.source.activity\n }\n }\n } else if (chunk.name === GENERATION_EVENTS.VIDEO_JOB_CREATED) {\n // Capture the provider job id as soon as the job exists — for a long\n // video run this is the one piece of identity worth having after a\n // reload, and the terminal `generation:result` may never arrive.\n const providerJobId = isObject(chunk.value)\n ? stringField(chunk.value, 'jobId')\n : undefined\n if (providerJobId) {\n next.result = { ...next.result, providerJobId }\n }\n }\n } else if (chunk.type === 'RUN_FINISHED') {\n next.resumeState = null\n next.status = 'complete'\n } else if (chunk.type === 'RUN_ERROR') {\n next.resumeState = null\n next.status = 'error'\n next.error = createGenerationErrorSnapshot(chunk)\n }\n\n return next\n}\n\n/**\n * Validates an untrusted value (a hydration body resolved by the server) into a\n * {@link GenerationResumeSnapshot}, or returns `undefined` when the value is\n * not a usable snapshot.\n *\n * A hydrated record is outside the type system: it may be stale, truncated, or\n * written by a different version. Every field is re-validated with the same\n * narrowing the live chunk reducer uses. `lastEvent` is not restored, since it\n * describes a transient stream position with no meaning after a reload.\n *\n * @internal\n */\nexport function parseGenerationResumeSnapshot(\n value: unknown,\n): GenerationResumeSnapshot | undefined {\n if (!isObject(value)) return undefined\n\n const schemaVersion = Reflect.get(value, 'schemaVersion')\n if (schemaVersion !== undefined && schemaVersion !== 1) return undefined\n\n const status = generationResumeStatusField(value, 'status')\n if (!status) return undefined\n\n const rawResumeState = Reflect.get(value, 'resumeState')\n let resumeState: GenerationResumeState | null = null\n if (rawResumeState !== null && rawResumeState !== undefined) {\n if (!isObject(rawResumeState)) return undefined\n const threadId = stringField(rawResumeState, 'threadId')\n const runId = stringField(rawResumeState, 'runId')\n if (!threadId || !runId) return undefined\n resumeState = { threadId, runId }\n }\n\n const snapshot: GenerationResumeSnapshot = {\n schemaVersion: 1,\n resumeState,\n status,\n }\n\n const activity = persistedArtifactActivityField(value, 'activity')\n if (activity) snapshot.activity = activity\n\n const pendingArtifacts = collectArtifactRefs(\n Reflect.get(value, 'pendingArtifacts'),\n )\n if (pendingArtifacts.length > 0) snapshot.pendingArtifacts = pendingArtifacts\n\n const result = createGenerationResultSnapshot(Reflect.get(value, 'result'))\n if (result) snapshot.result = result\n\n const rawError = Reflect.get(value, 'error')\n if (isObject(rawError)) {\n const message = stringField(rawError, 'message')\n if (message) {\n const code = stringField(rawError, 'code')\n snapshot.error = { message, ...(code ? { code } : {}) }\n }\n }\n\n return snapshot\n}\n\nfunction generationResumeStatusField(\n value: object,\n key: string,\n): GenerationResumeStatus | undefined {\n const field = stringField(value, key)\n if (field === undefined) return undefined\n\n switch (field) {\n case 'idle':\n case 'running':\n case 'complete':\n case 'error':\n return field\n default:\n return undefined\n }\n}\n\n// ===========================\n// Video-Specific Options\n// ===========================\n\n/**\n * Video status information returned during job polling.\n */\nexport interface VideoStatusInfo {\n /** Job identifier */\n jobId: string\n /** Current status of the video generation job */\n status: 'pending' | 'processing' | 'completed' | 'failed'\n /** Progress percentage (0-100), if available */\n progress?: number\n /** URL to the generated video (when completed) */\n url?: string\n /** Error message if status is 'failed' */\n error?: string\n}\n\n/**\n * Composite result for video generation (job completion).\n */\nexport interface VideoGenerateResult {\n /** Job identifier */\n jobId: string\n /** Final status */\n status: 'completed'\n /** URL to the generated video */\n url: string\n /** When the URL expires, if applicable */\n expiresAt?: Date\n /** Persisted artifact references for generated assets, when available */\n artifacts?: Array<PersistedArtifactRef>\n}\n\n/**\n * Options for the VideoGenerationClient.\n */\nexport interface VideoGenerationClientOptions<\n TOutput = VideoGenerateResult,\n> extends Omit<\n GenerationClientOptions<VideoGenerateInput, VideoGenerateResult, TOutput>,\n 'devtoolsBridgeFactory'\n> {\n /**\n * Factory that constructs the video devtools bridge. Default is a no-op\n * factory; the real implementation lives in `@tanstack/ai-client/devtools`.\n */\n devtoolsBridgeFactory?: VideoDevtoolsBridgeFactory\n\n /** Callback when a video job is created */\n onJobCreated?: (jobId: string) => void\n /** Callback on each status update */\n onStatusUpdate?: (status: VideoStatusInfo) => void\n\n // Framework state callbacks\n /** @internal Called when jobId changes */\n onJobIdChange?: (jobId: string | null) => void\n /** @internal Called when video status changes */\n onVideoStatusChange?: (status: VideoStatusInfo | null) => void\n}\n\n// ===========================\n// Input Types\n// ===========================\n\n/**\n * Input for image generation.\n */\nexport interface ImageGenerateInput {\n /**\n * Description of the desired image(s): plain text, or an ordered array of\n * content parts (text + image) for image-conditioned generation\n * (image-to-image, multi-reference, edit / inpaint).\n */\n prompt: MediaPrompt\n /** Number of images to generate (default: 1) */\n numberOfImages?: number\n /** Image size in WIDTHxHEIGHT format (e.g., \"1024x1024\") */\n size?: string\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for audio generation (music, sound effects).\n */\nexport interface AudioGenerateInput {\n /** Text description of the desired audio */\n prompt: string\n /** Desired duration in seconds */\n duration?: number\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for text-to-speech generation.\n */\nexport interface SpeechGenerateInput {\n /** The text to convert to speech */\n text: string\n /** The voice to use for generation */\n voice?: string\n /** The output audio format */\n format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'\n /** The speed of the generated audio (0.25 to 4.0) */\n speed?: number\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for audio transcription.\n */\nexport interface TranscriptionGenerateInput {\n /** The audio data to transcribe - can be base64 string, File, Blob, or ArrayBuffer */\n audio: string | File | Blob | ArrayBuffer\n /** The language of the audio in ISO-639-1 format (e.g., 'en') */\n language?: string\n /** An optional prompt to guide the transcription */\n prompt?: string\n /** The format of the transcription output */\n responseFormat?: TranscriptionResponseFormat\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for text summarization.\n */\nexport interface SummarizeGenerateInput {\n /** The text to summarize */\n text: string\n /** Maximum length of the summary */\n maxLength?: number\n /** Style of the summary */\n style?: 'bullet-points' | 'paragraph' | 'concise'\n /** Topics to focus on */\n focus?: Array<string>\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for video generation.\n */\nexport interface VideoGenerateInput {\n /**\n * Description of the desired video: plain text, or an ordered array of\n * content parts (text + image) for image-conditioned generation\n * (image-to-video, start/end frames).\n */\n prompt: MediaPrompt\n /** Video size — format depends on provider (e.g., \"16:9\", \"1280x720\") */\n size?: string\n /** Video duration in seconds */\n duration?: number\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\nfunction createGenerationEventSnapshot(\n chunk: StreamChunk,\n): GenerationEventSnapshot {\n const name = stringField(chunk, 'name')\n const timestamp = numberField(chunk, 'timestamp')\n return {\n type: chunk.type,\n ...(name ? { name } : {}),\n ...(timestamp !== undefined ? { timestamp } : {}),\n }\n}\n\n/** @internal Narrows an untrusted result payload into the persisted result snapshot shape. */\nexport function createGenerationResultSnapshot(\n value: unknown,\n): GenerationResultSnapshot | undefined {\n if (!isObject(value)) return undefined\n\n const artifacts = collectArtifactRefs(Reflect.get(value, 'artifacts'))\n const snapshot: GenerationResultSnapshot = {}\n const id = stringField(value, 'id')\n const model = stringField(value, 'model')\n const status = stringField(value, 'status')\n // A live provider result carries its job handle as `jobId` (e.g.\n // `VideoGenerateResult.jobId`); a persisted snapshot carries it as\n // `providerJobId`. Accept both — this narrows raw results AND stored\n // snapshots.\n const providerJobId =\n stringField(value, 'providerJobId') ?? stringField(value, 'jobId')\n // A transcription's output is `text`; a summary's is `summary`. Capture either\n // under `text` so a text result restores on reload.\n const text = stringField(value, 'text') ?? stringField(value, 'summary')\n const usage = Reflect.get(value, 'usage')\n if (id) snapshot.id = id\n if (model) snapshot.model = model\n if (status) snapshot.status = status\n if (providerJobId) snapshot.providerJobId = providerJobId\n if (text) snapshot.text = text\n // Passthrough opaque token-usage metadata (untrusted; not deeply validated).\n if (isObject(usage)) snapshot.usage = usage as TokenUsage\n const expiresAt = Reflect.get(value, 'expiresAt')\n if (typeof expiresAt === 'string') {\n snapshot.expiresAt = expiresAt\n } else if (expiresAt instanceof Date && !Number.isNaN(expiresAt.getTime())) {\n // `toISOString()` throws on an invalid Date. This runs per chunk on live\n // provider values, so drop an unusable date like every other bad field\n // here rather than throwing out of the stream loop.\n snapshot.expiresAt = expiresAt.toISOString()\n }\n if (artifacts.length > 0) {\n snapshot.artifacts = artifacts\n }\n\n return Object.keys(snapshot).length > 0 ? snapshot : undefined\n}\n\nfunction createGenerationErrorSnapshot(\n chunk: StreamChunk,\n): GenerationErrorSnapshot {\n const message =\n stringField(chunk, 'message') ??\n nestedStringField(chunk, 'error', 'message') ??\n 'An error occurred'\n const code = stringField(chunk, 'code')\n return {\n message,\n ...(code ? { code } : {}),\n }\n}\n\nfunction collectArtifactRefs(value: unknown): Array<PersistedArtifactRef> {\n if (!Array.isArray(value)) return []\n const refs: Array<PersistedArtifactRef> = []\n for (const item of value) {\n const ref = createPersistedArtifactRefSnapshot(item)\n if (ref) {\n refs.push(ref)\n }\n }\n return refs\n}\n\nfunction createPersistedArtifactRefSnapshot(\n value: unknown,\n): PersistedArtifactRef | undefined {\n if (!isObject(value)) return undefined\n const source = Reflect.get(value, 'source')\n if (!isObject(source)) return undefined\n\n const role = persistedArtifactRoleField(value, 'role')\n const artifactId = stringField(value, 'artifactId')\n const threadId = stringField(value, 'threadId')\n const runId = stringField(value, 'runId')\n const name = stringField(value, 'name')\n const mimeType = stringField(value, 'mimeType')\n const size = numberField(value, 'size')\n const createdAt = stringField(value, 'createdAt')\n const activity = persistedArtifactActivityField(source, 'activity')\n const path = stringField(source, 'path')\n const provider = stringField(source, 'provider')\n const model = stringField(source, 'model')\n if (\n !role ||\n !artifactId ||\n !threadId ||\n !runId ||\n !name ||\n !mimeType ||\n size === undefined ||\n !createdAt ||\n !activity ||\n !path ||\n !provider ||\n !model\n ) {\n return undefined\n }\n\n const sourceUrl = durableUrlField(value, 'sourceUrl')\n const url = serveUrlField(value, 'url')\n const mediaType = persistedArtifactMediaTypeField(source, 'mediaType')\n const jobId = stringField(source, 'jobId')\n const expiresAt = stringField(source, 'expiresAt')\n\n return {\n role,\n artifactId,\n threadId,\n runId,\n name,\n mimeType,\n size,\n createdAt,\n ...(sourceUrl ? { sourceUrl } : {}),\n ...(url ? { url } : {}),\n source: {\n activity,\n path,\n provider,\n model,\n ...(mediaType ? { mediaType } : {}),\n ...(jobId ? { jobId } : {}),\n ...(expiresAt ? { expiresAt } : {}),\n },\n }\n}\n\nfunction durableUrlField(value: object, key: string): string | undefined {\n const field = stringField(value, key)\n if (!field || field.length > 2048) return undefined\n try {\n const url = new URL(field)\n return url.protocol === 'http:' || url.protocol === 'https:'\n ? field\n : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Validates an app-origin serve URL, which unlike a provider URL is usually a\n * same-origin path (`/api/.../artifact?id=...`). Accepts an absolute http(s) URL\n * or a path-absolute same-origin URL (single leading `/`); rejects\n * protocol-relative (`//host`), `javascript:` / `data:`, and anything else, since\n * this value is rendered as media `src`.\n */\nfunction serveUrlField(value: object, key: string): string | undefined {\n const field = stringField(value, key)\n if (!field || field.length > 2048) return undefined\n // A single leading `/` is a safe same-origin path. Reject protocol-relative\n // `//host` AND a backslash bypass (`/\\host` — the URL parser treats `\\` as `/`\n // for http(s), so it would resolve to a foreign origin as an `<img src>`).\n if (field.startsWith('/') && !field.startsWith('//') && !field.includes('\\\\'))\n return field\n try {\n const url = new URL(field)\n return url.protocol === 'http:' || url.protocol === 'https:'\n ? field\n : undefined\n } catch {\n return undefined\n }\n}\n\nfunction persistedArtifactRoleField(\n value: object,\n key: string,\n): PersistedArtifactRef['role'] | undefined {\n const field = stringField(value, key)\n return field === 'input' || field === 'output' ? field : undefined\n}\n\nfunction persistedArtifactActivityField(\n value: object,\n key: string,\n): PersistedArtifactRef['source']['activity'] | undefined {\n const field = stringField(value, key)\n if (field === undefined) return undefined\n\n switch (field) {\n case 'image':\n case 'audio':\n case 'tts':\n case 'video':\n case 'transcription':\n return field\n default:\n return undefined\n }\n}\n\nfunction persistedArtifactMediaTypeField(\n value: object,\n key: string,\n): PersistedArtifactRef['source']['mediaType'] | undefined {\n const field = stringField(value, key)\n if (field === undefined) return undefined\n\n switch (field) {\n case 'image':\n case 'audio':\n case 'video':\n case 'document':\n case 'json':\n return field\n default:\n return undefined\n }\n}\n\nfunction nestedStringField(\n value: object,\n key: string,\n nestedKey: string,\n): string | undefined {\n const nested = Reflect.get(value, key)\n return isObject(nested) ? stringField(nested, nestedKey) : undefined\n}\n\nfunction stringField(value: object, key: string): string | undefined {\n const field = Reflect.get(value, key)\n return typeof field === 'string' ? field : undefined\n}\n\nfunction numberField(value: object, key: string): number | undefined {\n const field = Reflect.get(value, key)\n return typeof field === 'number' ? field : undefined\n}\n\nfunction isObject(value: unknown): value is object {\n return typeof value === 'object' && value !== null\n}\n"],"mappings":"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"}
@@ -233,6 +233,18 @@ export interface MultimodalContent {
233
233
  * If not provided, a unique ID will be generated.
234
234
  */
235
235
  id?: string;
236
+ /**
237
+ * Optional AG-UI metadata bag copied onto the resulting UIMessage.
238
+ *
239
+ * @example
240
+ * ```ts
241
+ * await client.sendMessage({
242
+ * content: 'Show me failed logins',
243
+ * metadata: { author: { id: 'user-42', name: 'Dana' } },
244
+ * })
245
+ * ```
246
+ */
247
+ metadata?: Record<string, any>;
236
248
  }
237
249
  /**
238
250
  * Action taken when `sendMessage` is called while the client is busy
@@ -409,6 +421,11 @@ export interface UIMessage<TTools extends ReadonlyArray<AnyClientTool> = any, TD
409
421
  role: 'system' | 'user' | 'assistant';
410
422
  parts: Array<MessagePart<TTools, TData>>;
411
423
  createdAt?: Date;
424
+ /**
425
+ * Optional AG-UI metadata bag. TanStack writes the `tanstack` key.
426
+ * User keys stay at the top.
427
+ */
428
+ metadata?: Record<string, any>;
412
429
  }
413
430
  /**
414
431
  * A generic key/value storage adapter. `getItem` may be sync or async; the