@tanstack/ai-persistence 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/esm/blob-range.d.ts +51 -0
  2. package/dist/esm/blob-range.js +84 -0
  3. package/dist/esm/blob-range.js.map +1 -0
  4. package/dist/esm/capabilities.d.ts +5 -0
  5. package/dist/esm/capabilities.js +16 -0
  6. package/dist/esm/capabilities.js.map +1 -0
  7. package/dist/esm/index.d.ts +13 -0
  8. package/dist/esm/index.js +9 -0
  9. package/dist/esm/memory.d.ts +19 -0
  10. package/dist/esm/memory.js +319 -0
  11. package/dist/esm/memory.js.map +1 -0
  12. package/dist/esm/middleware.d.ts +252 -0
  13. package/dist/esm/middleware.js +872 -0
  14. package/dist/esm/middleware.js.map +1 -0
  15. package/dist/esm/reconstruct-generation.d.ts +129 -0
  16. package/dist/esm/reconstruct-generation.js +148 -0
  17. package/dist/esm/reconstruct-generation.js.map +1 -0
  18. package/dist/esm/reconstruct.d.ts +79 -0
  19. package/dist/esm/reconstruct.js +75 -0
  20. package/dist/esm/reconstruct.js.map +1 -0
  21. package/dist/esm/retrieve.d.ts +40 -0
  22. package/dist/esm/retrieve.js +54 -0
  23. package/dist/esm/retrieve.js.map +1 -0
  24. package/dist/esm/testkit/conformance.d.ts +33 -0
  25. package/dist/esm/testkit/conformance.js +997 -0
  26. package/dist/esm/testkit/conformance.js.map +1 -0
  27. package/dist/esm/types.d.ts +554 -0
  28. package/dist/esm/types.js +103 -0
  29. package/dist/esm/types.js.map +1 -0
  30. package/package.json +71 -0
  31. package/skills/ai-persistence/SKILL.md +218 -0
  32. package/skills/ai-persistence/build-cloudflare-adapter/SKILL.md +313 -0
  33. package/skills/ai-persistence/build-cloudflare-artifact-store/SKILL.md +693 -0
  34. package/skills/ai-persistence/build-custom-adapter/SKILL.md +328 -0
  35. package/skills/ai-persistence/build-drizzle-adapter/SKILL.md +562 -0
  36. package/skills/ai-persistence/build-prisma-adapter/SKILL.md +518 -0
  37. package/skills/ai-persistence/server/SKILL.md +210 -0
  38. package/skills/ai-persistence/stores/SKILL.md +485 -0
  39. package/src/blob-range.ts +101 -0
  40. package/src/capabilities.ts +18 -0
  41. package/src/index.ts +114 -0
  42. package/src/memory.ts +491 -0
  43. package/src/middleware.ts +1795 -0
  44. package/src/reconstruct-generation.ts +244 -0
  45. package/src/reconstruct.ts +149 -0
  46. package/src/retrieve.ts +77 -0
  47. package/src/testkit/conformance.ts +1288 -0
  48. package/src/types.ts +878 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.js","names":[],"sources":["../../src/middleware.ts"],"sourcesContent":["import {\n defineChatMiddleware,\n getDetachableRun,\n wasCancelRequested,\n} from '@tanstack/ai'\nimport { providePendingTurn } from '@tanstack/ai/adapter-internals'\nimport { base64ToUint8Array } from '@tanstack/ai-utils'\nimport {\n InterruptsCapability,\n PersistenceCapability,\n provideInterrupts,\n providePersistence,\n} from './capabilities'\nimport {\n validateChatPersistenceStores,\n validateGenerationPersistenceStores,\n} from './types'\nimport type {\n AbortInfo,\n ChatMiddleware,\n ChatMiddlewareConfig,\n ChatMiddlewareContext,\n ChatResumeToolState,\n ErrorInfo,\n FinishInfo,\n GenerationAbortInfo,\n GenerationErrorInfo,\n GenerationFinishInfo,\n GenerationMiddleware,\n GenerationMiddlewareContext,\n ModelMessage,\n PersistedArtifactActivity,\n PersistedArtifactRef,\n PersistedArtifactRole,\n RunAgentResumeItem,\n StreamChunk,\n ToolApprovalResolution,\n TokenUsage,\n} from '@tanstack/ai'\nimport type {\n AIPersistence,\n AIPersistenceStores,\n ArtifactRecord,\n BlobBody,\n ChatTranscriptStores,\n InterruptRecord,\n RunStore,\n} from './types'\nimport { artifactBlobKey } from './retrieve'\n\n/**\n * How generated media is turned into durable artifacts: which pieces of a\n * result become artifacts, what they are named, where their bytes land, and how\n * the bytes are fetched when the provider returns a URL rather than inline data.\n *\n * Consumed by {@link withGenerationPersistence} through\n * {@link WithGenerationPersistenceOptions}. Chat persistence has no artifacts —\n * its options are {@link WithPersistenceOptions}.\n */\nexport interface ArtifactPersistenceOptions {\n extractArtifacts?: (\n input: GenerationArtifactExtractionInput,\n ) =>\n | Array<GenerationArtifactDescriptor | PersistedArtifactRef>\n | Promise<Array<GenerationArtifactDescriptor | PersistedArtifactRef>>\n nameArtifact?: (input: GenerationArtifactNameInput) => string\n /**\n * Map a freshly-persisted artifact ref to the durable app-origin URL that\n * serves its bytes (your `GET` route around `retrieveArtifact` /\n * `retrieveBlob`). The returned URL is stamped onto `ref.url` and written into\n * the result's media field, so both the live and the restored result render\n * durable media from your own origin instead of the provider's expiring link.\n * Return `undefined` to leave a ref without a durable URL.\n */\n artifactUrl?: (ref: PersistedArtifactRef) => string | undefined\n /**\n * Choose the blob-store key each artifact's bytes are written under, so\n * generated media can land in your own folder structure rather than the\n * default `artifacts/<runId>/<artifactId>`.\n *\n * ```ts\n * storageKey: ({ runId, artifactId, mimeType }) =>\n * `video/${videoId}/frames/${runId}-${artifactId}.png`\n * ```\n *\n * Server-side only, and deliberately so: a key supplied by the browser would\n * be a path-traversal and cross-tenant-write vector.\n *\n * The resolved key is recorded on `ArtifactRecord.blobKey`, because once the\n * path is arbitrary a reader can no longer recompute it. Returning a\n * non-unique key overwrites — include `artifactId` (or something equally\n * unique) unless you intend that.\n */\n storageKey?: (input: {\n artifactId: string\n runId: string\n threadId: string\n role: PersistedArtifactRole\n activity: PersistedArtifactActivity\n path: string\n mimeType: string\n name: string\n }) => string\n /**\n * Opt in to fetching prompt media referenced by URL (`role: 'input'`).\n *\n * Off by default, and deliberately expressed as a predicate rather than a\n * boolean: input URLs come from the caller, so fetching them server-side\n * turns your server into a proxy for whatever the caller names — cloud\n * metadata endpoints, `localhost` admin services, anything your network can\n * reach. The bytes are also redundant in the common case, since the client\n * already had the media it referenced.\n *\n * Enable this only when you genuinely need a durable copy of caller-supplied\n * media (a \"paste an image URL\" input box, say), and validate the target:\n *\n * ```ts\n * allowInputUrl: ({ url }) => url.hostname.endsWith('.cdn.example.com')\n * ```\n *\n * Requests are additionally forced through the same baseline checks every\n * artifact fetch gets (http/https only, timeout, size cap), plus — because\n * the target is untrusted — a loopback/private/link-local host block and\n * `redirect: 'manual'` so a 302 cannot hop to an internal address. Those are\n * a backstop, not a substitute for a narrow predicate: a hostname that\n * resolves to a private address still passes a literal-IP check.\n */\n allowInputUrl?: (input: {\n url: URL\n descriptor: GenerationArtifactDescriptor\n }) => boolean | Promise<boolean>\n /** Abort an artifact fetch after this many ms. Default 30_000. */\n artifactFetchTimeoutMs?: number\n /**\n * Refuse an artifact body larger than this many bytes. Default 1 GiB.\n *\n * This is a bound on TRANSFER, not on memory: the URL path streams into the\n * blob store and never buffers, so a 1 GiB artifact costs a streaming store\n * (R2, S3, filesystem) flat memory. What the cap buys is a ceiling on what a\n * broken or hostile origin can make you pull and store — `content-length` is\n * advisory, so without it an artifact fetch is an unbounded transfer billed\n * to you.\n *\n * Pass `false` to remove the ceiling entirely. That also removes the\n * cap-enforcing `TransformStream` wrapper, so the fetched body reaches your\n * store exactly as `fetch` produced it — on workerd that means it keeps its\n * native declared length and `R2Bucket.put` can single-shot it with no hint,\n * no multipart, and nothing buffered. Do that when you trust the origins you\n * fetch from (your provider's CDN); keep the cap when `allowInputUrl` lets\n * callers name the URL.\n */\n maxArtifactBytes?: number | false\n /**\n * `fetch` used to download artifact bytes. Defaults to the global. Inject to\n * route downloads through a proxy or an egress-restricted agent — the most\n * robust SSRF control available here, since it can resolve and check the\n * address actually connected to.\n */\n artifactFetch?: typeof globalThis.fetch\n}\n\n/**\n * Options for {@link withGenerationPersistence}: everything in\n * {@link ArtifactPersistenceOptions}, plus an optional scope override.\n */\nexport interface WithGenerationPersistenceOptions extends ArtifactPersistenceOptions {\n /**\n * Override the scope runs are filed under. Defaults to the `threadId` you\n * passed the activity, which is normally what you want, so leave this unset\n * unless the record belongs somewhere other than the activity's own scope.\n */\n threadId?: string\n}\n\n/**\n * The slot this generation's runs are filed under: `ctx.threadId` (the\n * `threadId` the caller passed the activity), or the option when it overrides.\n *\n * Throws when neither supplies one. A run filed under no scope can never be\n * hydrated by one, so `persistence: true` would restore nothing, forever. That\n * is worth failing loudly for, since the alternative is a silent hole a reader\n * cannot diagnose from behavior.\n */\nfunction generationScope(\n ctx: GenerationMiddlewareContext,\n opts: WithGenerationPersistenceOptions,\n): string {\n const threadId = opts.threadId ?? ctx.threadId\n if (threadId === undefined || threadId.length === 0) {\n throw new Error(\n 'Generation persistence requires a `threadId`, the stable scope successive ' +\n 'runs are filed under. Pass it to the activity, e.g. ' +\n '`generateImage({ threadId, middleware: [withGenerationPersistence(p)] })`, ' +\n 'or override it with `withGenerationPersistence(p, { threadId })`.',\n )\n }\n return threadId\n}\n\nconst DEFAULT_ARTIFACT_FETCH_TIMEOUT_MS = 30_000\n// 1 GiB, because generated video clips routinely run to a few hundred MB and\n// the old 100 MiB default silently failed them. The cap is a drain-time\n// counter, not a buffer: the URL path streams into the store, so raising it\n// costs a streaming store nothing in memory. It still earns its keep as the\n// only ceiling on what a runaway or hostile origin can make you transfer and\n// store (`content-length` is advisory, and on a compressed reply it measures\n// the compressed body). `maxArtifactBytes: false` removes it — and the wrapper\n// with it, which is the zero-copy path onto workerd + R2.\nconst DEFAULT_MAX_ARTIFACT_BYTES = 1024 * 1024 * 1024\n\nexport interface GenerationArtifactDescriptor {\n role: PersistedArtifactRole\n path: string\n mediaType?: PersistedArtifactRef['source']['mediaType']\n mimeType?: string\n bytes?: BlobBody\n url?: string\n json?: unknown\n name?: string\n jobId?: string\n expiresAt?: string | Date\n}\n\nexport interface GenerationArtifactExtractionInput {\n activity: PersistedArtifactActivity\n provider: string\n model: string\n threadId: string\n runId: string\n inputs: unknown\n result: unknown\n}\n\nexport interface GenerationArtifactNameInput {\n descriptor: GenerationArtifactDescriptor\n activity: PersistedArtifactActivity\n provider: string\n model: string\n threadId: string\n runId: string\n index: number\n}\n\ninterface RunStateEntry {\n merged: boolean\n interrupted: boolean\n /**\n * Resumes accepted in `onConfig` but not yet committed to the interrupt\n * store. They are applied (resolve/cancel) only once the run reaches a\n * successful boundary — see {@link commitPendingResumes}. Left uncommitted\n * (still pending in the store) if the run fails or aborts first.\n */\n pendingResumes?: {\n pending: Array<InterruptRecord>\n resumeByInterruptId: Map<string, RunAgentResumeItem>\n }\n /** Accumulated terminal-turn text, for throttled streaming snapshots (B). */\n streamingText?: string\n /** Epoch ms of the last streaming snapshot, to throttle writes (B). */\n lastSnapshotAt?: number\n /**\n * The current assistant turn's stream messageId, captured from\n * `TEXT_MESSAGE_START`. Persisted onto the assistant message so its identity\n * survives the persist → hydrate round-trip and a reload can resume the same\n * bubble in place.\n */\n streamingMessageId?: string\n}\n\nconst runState = new WeakMap<object, RunStateEntry>()\n\nconst validResumeStatuses = new Set(['resolved', 'cancelled'])\n\nfunction validatePendingResumes(\n pending: Array<InterruptRecord>,\n resume: Array<RunAgentResumeItem> | undefined,\n): Map<string, RunAgentResumeItem> {\n const pendingInterruptIds = new Set(\n pending.map((interrupt) => interrupt.interruptId),\n )\n const resumeByInterruptId = new Map(\n (resume ?? []).map((entry) => [entry.interruptId, entry]),\n )\n if (pending.length === 0) {\n const staleEntry = resume?.[0]\n if (staleEntry) {\n throw new Error(\n `Resume entry references non-pending interrupt ${staleEntry.interruptId}.`,\n )\n }\n return resumeByInterruptId\n }\n if (!resume || resume.length === 0) {\n throw new Error(\n `Thread has pending interrupts; resume is required before accepting new input.`,\n )\n }\n\n for (const interrupt of pending) {\n const entry = resumeByInterruptId.get(interrupt.interruptId)\n if (!entry) {\n throw new Error(\n `Missing resume entry for pending interrupt ${interrupt.interruptId}.`,\n )\n }\n if (!validResumeStatuses.has(entry.status)) {\n throw new Error(\n `Invalid resume status for pending interrupt ${interrupt.interruptId}: ${entry.status}.`,\n )\n }\n }\n for (const entry of resume) {\n if (!pendingInterruptIds.has(entry.interruptId)) {\n throw new Error(\n `Resume entry references non-pending interrupt ${entry.interruptId}.`,\n )\n }\n }\n return resumeByInterruptId\n}\n\nasync function applyPendingResumes(\n pending: Array<InterruptRecord>,\n resumeByInterruptId: Map<string, RunAgentResumeItem>,\n interrupts: NonNullable<AIPersistence['stores']['interrupts']>,\n): Promise<void> {\n for (const interrupt of pending) {\n const entry = resumeByInterruptId.get(interrupt.interruptId)\n if (!entry) continue\n if (entry.status === 'resolved') {\n await interrupts.resolve(interrupt.interruptId, entry.payload)\n } else {\n await interrupts.cancel(interrupt.interruptId)\n }\n }\n}\n\n/**\n * Commit the resumes stashed in `onConfig`, marking each resumed interrupt\n * resolved/cancelled. Called only from success boundaries (`onFinish`, and the\n * `onChunk` interrupt boundary) so a provider failure or abort between accepting\n * the resume and reaching a boundary leaves the interrupts pending — the\n * approval is not consumed and a retry with the same resume succeeds. Idempotent\n * and a no-op when nothing is stashed.\n */\nasync function commitPendingResumes(\n state: RunStateEntry | undefined,\n interrupts: AIPersistence['stores']['interrupts'],\n): Promise<void> {\n if (!state?.pendingResumes || !interrupts) return\n const { pending, resumeByInterruptId } = state.pendingResumes\n // Apply first; only clear the in-memory stash after every resolve/cancel\n // succeeds so a mid-loop store failure can still re-drive remaining ids\n // if the hook is retried (or a later boundary re-enters commit).\n await applyPendingResumes(pending, resumeByInterruptId, interrupts)\n state.pendingResumes = undefined\n}\n\nfunction objectValue(value: unknown): Record<string, unknown> | null {\n return value && typeof value === 'object'\n ? (value as Record<string, unknown>)\n : null\n}\n\nfunction stringField(\n value: Record<string, unknown>,\n key: string,\n): string | undefined {\n return typeof value[key] === 'string' ? value[key] : undefined\n}\n\nfunction interruptKind(interrupt: InterruptRecord): string | undefined {\n const metadata = objectValue(interrupt.payload.metadata)\n return metadata ? stringField(metadata, 'kind') : undefined\n}\n\nfunction resolvedApprovalDecision(entry: RunAgentResumeItem): boolean {\n if (entry.status === 'cancelled') return false\n const payload = objectValue(entry.payload)\n // Fail closed: persisted resume payloads may be malformed or truncated, so a\n // missing/non-boolean `approved` denies the tool rather than running it.\n return typeof payload?.approved === 'boolean' ? payload.approved : false\n}\n\n/**\n * Translate the persisted pending interrupts + the resume batch into the\n * `ChatResumeToolState` the chat engine consumes. This is the server-authoritative\n * counterpart to the engine's ephemeral (client-history) reconstruction: because\n * the persistence flow sends empty client messages, the engine has no history to\n * rebuild from, so persistence supplies the resume state directly (and clears\n * `config.resume` so the ephemeral path is skipped — see `onConfig`).\n */\nfunction resumeToolStateFromPending(\n pending: Array<InterruptRecord>,\n resumeByInterruptId: Map<string, RunAgentResumeItem>,\n): ChatResumeToolState | undefined {\n const approvals = new Map<string, ToolApprovalResolution>()\n const clientToolResults = new Map<string, unknown>()\n\n for (const interrupt of pending) {\n const entry = resumeByInterruptId.get(interrupt.interruptId)\n if (!entry) continue\n\n const kind = interruptKind(interrupt)\n const reason = stringField(interrupt.payload, 'reason')\n const toolCallId = stringField(interrupt.payload, 'toolCallId')\n\n if (kind === 'approval' || reason === 'approval_required') {\n approvals.set(interrupt.interruptId, resolvedApprovalDecision(entry))\n continue\n }\n\n if (\n entry.status === 'resolved' &&\n toolCallId &&\n (kind === 'client_tool' || reason === 'client_tool_input')\n ) {\n clientToolResults.set(toolCallId, entry.payload)\n }\n }\n\n if (approvals.size === 0 && clientToolResults.size === 0) return undefined\n return { approvals, clientToolResults }\n}\n\n/**\n * Build the transcript to persist when a run finishes successfully.\n *\n * The chat engine appends an assistant message to the middleware message list\n * only when that turn carries tool calls (to feed the agent loop); a run's\n * terminal *text* reply is never appended. So `ctx.messages` at `onFinish` is\n * missing the assistant's final answer. Reattach it from the finish info —\n * `info.content` is the last turn's accumulated text (reset each cycle) — so\n * the stored thread is the complete conversation a server-authoritative client\n * hydrates on load. A guard avoids duplicating a terminal assistant turn should\n * the engine ever start appending it itself.\n */\nfunction finishedTranscript(\n messages: ReadonlyArray<ModelMessage>,\n info: FinishInfo,\n messageId: string | undefined,\n): Array<ModelMessage> {\n const transcript = [...messages]\n const last = transcript[transcript.length - 1]\n const alreadyPresent =\n last?.role === 'assistant' &&\n last.toolCalls === undefined &&\n last.content === info.content\n if (info.content && !alreadyPresent) {\n // Stamp the terminal turn with its stream messageId so a hydrated bubble\n // keeps the same identity as the live stream (in-place resume on reload).\n transcript.push({\n role: 'assistant',\n content: info.content,\n ...(messageId ? { id: messageId } : {}),\n })\n }\n return transcript\n}\n\nfunction interruptPayload(interrupt: unknown): Record<string, unknown> {\n return interrupt && typeof interrupt === 'object'\n ? { ...(interrupt as Record<string, unknown>) }\n : { value: interrupt }\n}\n\n// ---------------------------------------------------------------------------\n// Generation artifact extraction / persistence\n// ---------------------------------------------------------------------------\n\nfunction isArtifactRef(value: unknown): value is PersistedArtifactRef {\n const record = objectValue(value)\n return !!record && typeof record.artifactId === 'string'\n}\n\nfunction mediaActivity(\n activity: GenerationMiddlewareContext['activity'],\n): PersistedArtifactActivity | undefined {\n return activity === 'image' ||\n activity === 'audio' ||\n activity === 'tts' ||\n activity === 'video' ||\n activity === 'transcription'\n ? activity\n : undefined\n}\n\nfunction parseDataUrl(\n value: string,\n): { mimeType: string; bytes: Uint8Array } | undefined {\n const match = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(value)\n if (!match) return undefined\n const mimeType = match[1] || 'application/octet-stream'\n const raw = match[3] ?? ''\n // A plain (non-base64) data URL may carry a bare `%` (`data:text/plain,100%`),\n // which makes `decodeURIComponent` throw. Fall back to the literal payload so\n // a malformed escape doesn't fail the whole generation.\n let payload: string\n try {\n payload = decodeURIComponent(raw)\n } catch {\n payload = raw\n }\n return {\n mimeType,\n bytes: match[2]\n ? base64ToUint8Array(payload)\n : new TextEncoder().encode(payload),\n }\n}\n\nfunction extensionForMime(mimeType: string | undefined): string {\n if (mimeType === undefined) return 'bin'\n\n switch (mimeType) {\n case 'image/png':\n return 'png'\n case 'image/jpeg':\n return 'jpg'\n case 'audio/wav':\n return 'wav'\n case 'audio/mpeg':\n return 'mp3'\n case 'audio/mp3':\n return 'mp3'\n case 'video/mp4':\n return 'mp4'\n case 'application/json':\n return 'json'\n default:\n return 'bin'\n }\n}\n\nfunction defaultArtifactName(\n descriptor: GenerationArtifactDescriptor,\n activity: PersistedArtifactActivity,\n index: number,\n): string {\n const ext = extensionForMime(descriptor.mimeType)\n return `${activity}-${descriptor.role}-${descriptor.mediaType ?? 'artifact'}-${index}.${ext}`\n}\n\nfunction sourcePartDescriptors(\n part: unknown,\n role: PersistedArtifactRole,\n path: string,\n): Array<GenerationArtifactDescriptor> {\n const record = objectValue(part)\n const type = stringField(record ?? {}, 'type')\n const source = objectValue(record?.source)\n if (\n !record ||\n !source ||\n (type !== 'image' && type !== 'audio' && type !== 'video')\n ) {\n return []\n }\n const sourceType = stringField(source, 'type')\n const mimeType = stringField(source, 'mimeType') ?? `${type}/mpeg`\n if (sourceType === 'data') {\n const value = stringField(source, 'value')\n if (!value) return []\n return [\n {\n role,\n path,\n mediaType: type,\n mimeType,\n bytes: base64ToUint8Array(value),\n },\n ]\n }\n if (sourceType === 'url') {\n const value = stringField(source, 'value')\n if (!value) return []\n return [{ role, path, mediaType: type, mimeType, url: value }]\n }\n return []\n}\n\nfunction promptInputDescriptors(\n inputs: unknown,\n): Array<GenerationArtifactDescriptor> {\n const prompt = objectValue(inputs)?.prompt\n if (!Array.isArray(prompt)) return []\n\n const counts: Record<string, number> = { image: 0, audio: 0, video: 0 }\n const descriptors: Array<GenerationArtifactDescriptor> = []\n for (const part of prompt) {\n const type = stringField(objectValue(part) ?? {}, 'type')\n if (type !== 'image' && type !== 'audio' && type !== 'video') continue\n const index = counts[type] ?? 0\n counts[type] = index + 1\n descriptors.push(\n ...sourcePartDescriptors(part, 'input', `prompt.${type}s.${index}`),\n )\n }\n return descriptors\n}\n\nfunction generatedMediaDescriptor(args: {\n role: PersistedArtifactRole\n path: string\n mediaType: 'image' | 'audio' | 'video'\n mimeType: string\n media: unknown\n jobId?: string\n expiresAt?: string | Date\n}): GenerationArtifactDescriptor | undefined {\n const media = objectValue(args.media)\n if (!media) return undefined\n const b64Json = stringField(media, 'b64Json')\n if (b64Json) {\n return {\n role: args.role,\n path: args.path,\n mediaType: args.mediaType,\n mimeType: stringField(media, 'contentType') ?? args.mimeType,\n bytes: base64ToUint8Array(b64Json),\n jobId: args.jobId,\n expiresAt: args.expiresAt,\n }\n }\n const url = stringField(media, 'url')\n if (url) {\n return {\n role: args.role,\n path: args.path,\n mediaType: args.mediaType,\n mimeType: stringField(media, 'contentType') ?? args.mimeType,\n url,\n jobId: args.jobId,\n expiresAt: args.expiresAt,\n }\n }\n return undefined\n}\n\nfunction builtInArtifactDescriptors(\n activity: PersistedArtifactActivity,\n inputs: unknown,\n result: unknown,\n): Array<GenerationArtifactDescriptor> {\n const descriptors = promptInputDescriptors(inputs)\n const output = objectValue(result)\n if (!output) return descriptors\n\n if (activity === 'image' && Array.isArray(output.images)) {\n output.images.forEach((image, index) => {\n const descriptor = generatedMediaDescriptor({\n role: 'output',\n path: `images.${index}`,\n mediaType: 'image',\n mimeType: 'image/png',\n media: image,\n })\n if (descriptor) descriptors.push(descriptor)\n })\n }\n\n if (activity === 'audio') {\n const descriptor = generatedMediaDescriptor({\n role: 'output',\n path: 'audio',\n mediaType: 'audio',\n mimeType: 'audio/mpeg',\n media: output.audio,\n })\n if (descriptor) descriptors.push(descriptor)\n }\n\n if (activity === 'tts') {\n const audio = stringField(output, 'audio')\n if (audio) {\n const format = stringField(output, 'format')\n descriptors.push({\n role: 'output',\n path: 'audio',\n mediaType: 'audio',\n mimeType:\n stringField(output, 'contentType') ??\n (format ? `audio/${format}` : 'audio/mpeg'),\n bytes: base64ToUint8Array(audio),\n })\n }\n }\n\n if (activity === 'video' && typeof output.url === 'string') {\n descriptors.push({\n role: 'output',\n path: 'video',\n mediaType: 'video',\n mimeType: 'video/mp4',\n url: output.url,\n jobId: stringField(output, 'jobId'),\n expiresAt:\n output.expiresAt instanceof Date ? output.expiresAt : undefined,\n })\n }\n\n if (activity === 'transcription') {\n const audio = objectValue(inputs)?.audio\n if (typeof audio === 'string') {\n const data = parseDataUrl(audio)\n descriptors.push({\n role: 'input',\n path: 'audio',\n mediaType: 'audio',\n mimeType: data?.mimeType ?? 'audio/mpeg',\n bytes: data?.bytes ?? base64ToUint8Array(audio),\n })\n } else if (audio instanceof ArrayBuffer) {\n descriptors.push({\n role: 'input',\n path: 'audio',\n mediaType: 'audio',\n mimeType: 'audio/mpeg',\n bytes: audio.slice(0),\n })\n } else if (typeof Blob !== 'undefined' && audio instanceof Blob) {\n descriptors.push({\n role: 'input',\n path: 'audio',\n mediaType: 'audio',\n mimeType: audio.type || 'audio/mpeg',\n bytes: audio,\n })\n }\n if (Array.isArray(output.segments) || Array.isArray(output.words)) {\n descriptors.push({\n role: 'output',\n path: 'transcription',\n mediaType: 'json',\n mimeType: 'application/json',\n json: output,\n })\n }\n }\n\n return descriptors\n}\n\n/**\n * Reject hosts that only make sense as an SSRF target: loopback, link-local\n * (including the cloud metadata address), private, and unique-local ranges.\n *\n * Applied to caller-supplied input URLs only. Provider result URLs skip it on\n * purpose — a self-hosted or local provider legitimately returns a `localhost`\n * URL, and those live inside the same trust boundary as the adapter itself.\n *\n * This checks IP *literals*. A hostname that resolves to a private address\n * passes, which is why `allowInputUrl` is required rather than optional.\n */\nfunction isBlockedInputHost(hostname: string): boolean {\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, '')\n if (host === 'localhost' || host.endsWith('.localhost')) return true\n\n const ipv4 = /^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/.exec(host)\n if (ipv4) {\n const [a, b] = [Number(ipv4[1]), Number(ipv4[2])]\n if (a === 127 || a === 0 || a === 10) return true\n if (a === 169 && b === 254) return true // link-local + cloud metadata\n if (a === 172 && b >= 16 && b <= 31) return true\n if (a === 192 && b === 168) return true\n return false\n }\n\n if (host === '::' || host === '::1') return true\n if (host.startsWith('fe80:')) return true // link-local\n if (/^f[cd][0-9a-f]{2}:/.test(host)) return true // unique-local\n // IPv4-mapped IPv6 — re-check the embedded address. `new URL()` normalizes\n // `::ffff:127.0.0.1` to the hex form `::ffff:7f00:1`, so accept both.\n const mappedDotted = /^::ffff:(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})$/.exec(\n host,\n )\n if (mappedDotted?.[1]) return isBlockedInputHost(mappedDotted[1])\n const mappedHex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host)\n if (mappedHex?.[1] && mappedHex[2]) {\n const high = Number.parseInt(mappedHex[1], 16)\n const low = Number.parseInt(mappedHex[2], 16)\n return isBlockedInputHost(\n `${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`,\n )\n }\n return false\n}\n\n/**\n * Fail the stream once more than `maxBytes` have passed through, so an\n * unexpectedly huge artifact can't fill the blob store.\n *\n * Only used when the response does NOT already bound itself — a chunked reply,\n * or a content-encoded one whose declared length describes the compressed\n * bytes. When `content-length` describes the body the store will drain, HTTP\n * framing is the bound and wrapping would only cost the caller the declared\n * length: a `TransformStream`'s readable side carries none, which is what\n * pushes a length-strict runtime (workerd + R2) onto a multipart upload.\n */\nfunction capBodySize(\n body: ReadableStream<Uint8Array>,\n maxBytes: number,\n url: string,\n): ReadableStream<Uint8Array> {\n let seen = 0\n return body.pipeThrough(\n new TransformStream<Uint8Array, Uint8Array>({\n transform(chunk, controller) {\n seen += chunk.byteLength\n if (seen > maxBytes) {\n controller.error(\n new Error(\n `Artifact at ${url} exceeds maxArtifactBytes (${maxBytes}).`,\n ),\n )\n return\n }\n controller.enqueue(chunk)\n },\n }),\n )\n}\n\n/**\n * Resolve a descriptor to the bytes to store. Returns `undefined` when the\n * descriptor is deliberately not persisted — today that means a caller-supplied\n * input URL without an `allowInputUrl` opt-in.\n */\nasync function descriptorBody(\n descriptor: GenerationArtifactDescriptor,\n opts: ArtifactPersistenceOptions | undefined,\n): Promise<\n | {\n body: BlobBody\n size: number\n /**\n * Exact byte length of a streamed body, when the origin declared one\n * that survives decoding — forwarded to `BlobStore.put` as\n * `BlobPutOptions.expectedLength`. Undefined when unknown.\n */\n expectedLength?: number\n mimeType: string\n sourceUrl?: string\n }\n | undefined\n> {\n if (descriptor.json !== undefined) {\n const body = JSON.stringify(descriptor.json)\n return {\n body,\n size: new TextEncoder().encode(body).byteLength,\n mimeType: descriptor.mimeType ?? 'application/json',\n }\n }\n\n if (descriptor.bytes !== undefined) {\n const body = descriptor.bytes\n let size: number\n if (typeof body === 'string') {\n size = new TextEncoder().encode(body).byteLength\n } else if (body instanceof ArrayBuffer) {\n size = body.byteLength\n } else if (ArrayBuffer.isView(body)) {\n size = body.byteLength\n } else if (typeof Blob !== 'undefined' && body instanceof Blob) {\n size = body.size\n } else {\n size = 0\n }\n return {\n body,\n size,\n mimeType: descriptor.mimeType ?? 'application/octet-stream',\n }\n }\n\n if (descriptor.url) {\n const data = parseDataUrl(descriptor.url)\n if (data) {\n return {\n body: data.bytes,\n size: data.bytes.byteLength,\n mimeType: descriptor.mimeType ?? data.mimeType,\n }\n }\n // A caller-controlled input URL is never fetched unless the app opted in\n // with a validating predicate. Skipped, not thrown: not mirroring someone\n // else's URL is the intended default, and the run itself is fine.\n const isCallerSupplied = descriptor.role === 'input'\n const allowInputUrl = opts?.allowInputUrl\n if (isCallerSupplied && !allowInputUrl) return undefined\n\n let target: URL\n try {\n target = new URL(descriptor.url)\n } catch {\n throw new Error(\n `Failed to persist artifact: ${descriptor.url} is not a valid URL.`,\n )\n }\n if (target.protocol !== 'https:' && target.protocol !== 'http:') {\n throw new Error(\n `Refusing to fetch artifact over ${target.protocol} (${descriptor.path}).`,\n )\n }\n if (allowInputUrl && isCallerSupplied) {\n if (isBlockedInputHost(target.hostname)) {\n throw new Error(\n `Refusing to fetch input artifact from internal host ${target.hostname}.`,\n )\n }\n if (!(await allowInputUrl({ url: target, descriptor }))) {\n throw new Error(\n `Refusing to fetch input artifact from ${target.hostname}: rejected by allowInputUrl.`,\n )\n }\n }\n\n const maxBytes = opts?.maxArtifactBytes ?? DEFAULT_MAX_ARTIFACT_BYTES\n const fetchArtifact = opts?.artifactFetch ?? globalThis.fetch\n const response = await fetchArtifact(target, {\n // Provider CDNs redirect routinely, so output fetches follow. An input\n // fetch must not: a 302 would land on a host neither check ever saw.\n redirect: isCallerSupplied ? 'manual' : 'follow',\n signal: AbortSignal.timeout(\n opts?.artifactFetchTimeoutMs ?? DEFAULT_ARTIFACT_FETCH_TIMEOUT_MS,\n ),\n })\n if (isCallerSupplied && response.status >= 300 && response.status < 400) {\n throw new Error(\n `Refusing to follow a redirect for input artifact ${descriptor.path}.`,\n )\n }\n if (!response.ok) {\n throw new Error(\n `Failed to persist artifact from ${descriptor.url}: HTTP ${response.status}`,\n )\n }\n // `headers.get` returns null when the header is absent, and\n // `Number(null) === 0` — parse only a present header, or a chunked reply\n // would read as a declared length of 0 (harmless, but the early-reject\n // below would silently never be reachable for it).\n const contentLength = response.headers.get('content-length')\n const declaredLength =\n contentLength === null ? undefined : Number(contentLength)\n if (\n maxBytes !== false &&\n declaredLength !== undefined &&\n Number.isFinite(declaredLength) &&\n declaredLength > maxBytes\n ) {\n throw new Error(\n `Artifact at ${descriptor.url} exceeds maxArtifactBytes (${maxBytes}).`,\n )\n }\n const mimeType =\n descriptor.mimeType ??\n response.headers.get('content-type') ??\n 'application/octet-stream'\n // A declared length is the DECODED body's length only when the response is\n // not content-encoded: fetch transparently decompresses, so on a gzipped\n // reply `content-length` measures the compressed bytes and the decoded\n // stream can be arbitrarily longer. Only trust it when it provably\n // describes what the store will drain.\n const encoding = response.headers.get('content-encoding')\n const decodedLengthIsKnown =\n declaredLength !== undefined &&\n Number.isFinite(declaredLength) &&\n (encoding === null || encoding === 'identity')\n const expectedLength = decodedLengthIsKnown ? declaredLength : undefined\n // Stream the body straight into the blob store instead of buffering the\n // whole artifact in memory. `size` is left 0 (unknown up front); the store\n // records the actual byte length as it drains the stream. Fall back to\n // buffering only when the response has no body to stream.\n if (response.body) {\n return {\n // Wrap ONLY when the response does not already bound itself. A\n // trustworthy `content-length` was checked against the cap above, and\n // HTTP framing holds the origin to it — a body cannot exceed a length\n // it declared — so the counter would add nothing and cost everything:\n // it is a TransformStream, whose readable side has no declared length,\n // and that missing length is precisely what breaks `R2Bucket.put`.\n // Unwrapped, the runtime's own length rides along and R2 single-shots\n // the stream. What still needs the counter: a chunked reply (no\n // declared length at all) and a content-encoded one (declared length\n // measures the compressed bytes, so the decoded stream is a\n // decompression bomb waiting to happen).\n body:\n maxBytes === false || decodedLengthIsKnown\n ? response.body\n : capBodySize(response.body, maxBytes, descriptor.url),\n size: 0,\n expectedLength,\n mimeType,\n sourceUrl: descriptor.url,\n }\n }\n const body = await response.arrayBuffer()\n if (maxBytes !== false && body.byteLength > maxBytes) {\n throw new Error(\n `Artifact at ${descriptor.url} exceeds maxArtifactBytes (${maxBytes}).`,\n )\n }\n return {\n body,\n size: body.byteLength,\n mimeType,\n sourceUrl: descriptor.url,\n }\n }\n\n throw new Error(\n `Artifact descriptor ${descriptor.path} has no bytes, url, or json.`,\n )\n}\n\nasync function persistGenerationArtifacts(\n persistence: AIPersistence,\n opts: WithGenerationPersistenceOptions,\n ctx: GenerationMiddlewareContext,\n result: unknown,\n): Promise<Array<PersistedArtifactRef>> {\n const activity = mediaActivity(ctx.activity)\n if (!activity) return []\n\n // Resolved the same way the run record is, so an artifact always lands in the\n // same slot as the run that produced it.\n const threadId = generationScope(ctx, opts)\n const runId = ctx.runId ?? ctx.requestId\n const extractionInput: GenerationArtifactExtractionInput = {\n activity,\n provider: ctx.provider,\n model: ctx.model,\n threadId,\n runId,\n inputs: ctx.artifactInputs,\n result,\n }\n const extracted =\n opts?.extractArtifacts !== undefined\n ? await opts.extractArtifacts(extractionInput)\n : builtInArtifactDescriptors(activity, ctx.artifactInputs, result)\n\n if (extracted.length === 0) return []\n\n const existingRefs = extracted.filter(isArtifactRef)\n const descriptors = extracted.filter(\n (item): item is GenerationArtifactDescriptor => !isArtifactRef(item),\n )\n if (descriptors.length === 0) return existingRefs\n\n if (!persistence.stores.artifacts || !persistence.stores.blobs) {\n throw new Error(\n 'Generation artifact persistence requires stores.artifacts and stores.blobs.',\n )\n }\n\n const refs: Array<PersistedArtifactRef> = [...existingRefs]\n for (const [index, descriptor] of descriptors.entries()) {\n const artifactId = ctx.createId('artifact')\n const resolved = await descriptorBody(descriptor, opts)\n // Deliberately not persisted (an input URL with no `allowInputUrl` opt-in):\n // no blob, no record, no ref — the rest of the run is unaffected.\n if (!resolved) continue\n const { body, size, expectedLength, mimeType, sourceUrl } = resolved\n // Resolved before the blob write so `storageKey` can build a path from the\n // final filename (extensions, slugs) rather than guessing at one.\n const name =\n opts?.nameArtifact?.({\n descriptor: { ...descriptor, mimeType },\n activity,\n provider: ctx.provider,\n model: ctx.model,\n threadId,\n runId,\n index,\n }) ??\n descriptor.name ??\n defaultArtifactName({ ...descriptor, mimeType }, activity, index)\n const key =\n opts?.storageKey?.({\n artifactId,\n runId,\n threadId,\n role: descriptor.role,\n activity,\n path: descriptor.path,\n mimeType,\n name,\n }) ?? artifactBlobKey({ runId, artifactId })\n const stored = await persistence.stores.blobs.put(key, body, {\n contentType: mimeType,\n // Exact decoded length when the origin declared one — lets a store\n // single-shot the stream (e.g. R2 via FixedLengthStream) instead of\n // buffering or going multipart. Absent when unknown.\n ...(expectedLength !== undefined ? { expectedLength } : {}),\n customMetadata: {\n runId,\n threadId,\n role: descriptor.role,\n activity,\n path: descriptor.path,\n },\n })\n // For streamed downloads the descriptor size is unknown (0); the store\n // reports the real byte length once it has drained the stream.\n const resolvedSize = size || stored.size || 0\n const createdAtMs = Date.now()\n const record: ArtifactRecord = {\n artifactId,\n runId,\n threadId,\n // Always recorded: with a custom `storageKey` the path is no longer\n // derivable from the record, so the reader has to be told where it went.\n blobKey: key,\n name,\n mimeType,\n size: resolvedSize,\n sourceUrl,\n createdAt: createdAtMs,\n }\n await persistence.stores.artifacts.save(record)\n refs.push({\n role: descriptor.role,\n artifactId,\n threadId,\n runId,\n name,\n mimeType,\n size: resolvedSize,\n createdAt: new Date(createdAtMs).toISOString(),\n ...(sourceUrl ? { sourceUrl } : {}),\n source: {\n activity,\n path: descriptor.path,\n provider: ctx.provider,\n model: ctx.model,\n mediaType: descriptor.mediaType,\n jobId: descriptor.jobId,\n expiresAt:\n descriptor.expiresAt instanceof Date\n ? descriptor.expiresAt.toISOString()\n : descriptor.expiresAt,\n },\n })\n }\n\n // Stamp the durable app-origin serve URL onto every ref that lacks one, so\n // clients render + restore media from your own origin, not the provider link.\n if (opts?.artifactUrl) {\n for (let i = 0; i < refs.length; i++) {\n const ref = refs[i]\n if (ref && !ref.url) {\n const url = opts.artifactUrl(ref)\n if (url) refs[i] = { ...ref, url }\n }\n }\n }\n\n return refs\n}\n\n/**\n * Rewrite the live result's media fields to each output ref's durable serve URL\n * (`ref.url`), so the live result matches what a reload restores. Keyed off the\n * ref's `source.path`: `images.<i>` → `result.images[i].url`, `video` →\n * `result.url`, `audio` (object) → `result.audio.url`. tts (a base64 string) and\n * transcription (json) have no media-URL field, so they are left as-is; their\n * durable bytes are reachable via `result.artifacts`. A no-op when no ref has a\n * `url`.\n */\nfunction applyDurableMediaUrls(\n result: Record<string, unknown>,\n refs: Array<PersistedArtifactRef>,\n): Record<string, unknown> {\n let next = result\n for (const ref of refs) {\n if (ref.role !== 'output' || !ref.url) continue\n const path = ref.source.path\n if (path.startsWith('images.')) {\n const index = Number(path.slice('images.'.length))\n const images = next.images\n if (Array.isArray(images) && objectValue(images[index])) {\n const cloned = [...images]\n cloned[index] = { ...objectValue(images[index]), url: ref.url }\n next = { ...next, images: cloned }\n }\n } else if (path === 'video') {\n next = { ...next, url: ref.url }\n } else if (path === 'audio' && objectValue(next.audio)) {\n next = { ...next, audio: { ...objectValue(next.audio), url: ref.url } }\n }\n }\n return next\n}\n\n// ---------------------------------------------------------------------------\n// Shared store / feature plan\n// ---------------------------------------------------------------------------\n\ninterface PersistencePlan {\n wantsInterrupts: boolean\n wantsArtifactPersistence: boolean\n runs: AIPersistence['stores']['runs']\n}\n\nfunction resolvePersistencePlan(persistence: AIPersistence): PersistencePlan {\n return {\n wantsInterrupts: persistence.stores.interrupts !== undefined,\n wantsArtifactPersistence:\n persistence.stores.artifacts !== undefined &&\n persistence.stores.blobs !== undefined,\n runs: persistence.stores.runs,\n }\n}\n\ntype StoreIsDefinitelyPresent<\n TStores extends AIPersistenceStores,\n TKey extends keyof AIPersistenceStores,\n> = TKey extends keyof TStores\n ? object extends Pick<TStores, TKey>\n ? false\n : [Exclude<TStores[TKey], undefined>] extends [never]\n ? false\n : true\n : false\n\ntype StoreIsDefinitelyAbsent<\n TStores extends AIPersistenceStores,\n TKey extends keyof AIPersistenceStores,\n> = TKey extends keyof TStores\n ? [Exclude<TStores[TKey], undefined>] extends [never]\n ? true\n : false\n : true\n\n/**\n * Chat entrypoint invalid when:\n * - `messages` is known-absent, or\n * - `interrupts` is known-present without `runs`.\n *\n * Fully optional bags (`AIPersistence` with all `?` keys) stay assignable and\n * are checked at runtime by {@link validateChatPersistenceStores}.\n */\ntype InvalidChatPersistence<TStores extends AIPersistenceStores> =\n StoreIsDefinitelyAbsent<TStores, 'messages'> extends true\n ? true\n : StoreIsDefinitelyPresent<TStores, 'interrupts'> extends true\n ? StoreIsDefinitelyAbsent<TStores, 'runs'>\n : false\n\n/**\n * Generation entrypoint invalid when `generationRuns` is known-absent, or when\n * exactly one of `artifacts` / `blobs` is present (artifact persistence needs\n * both).\n */\ntype InvalidGenerationPersistence<TStores extends AIPersistenceStores> =\n StoreIsDefinitelyAbsent<TStores, 'generationRuns'> extends true\n ? true\n : StoreIsDefinitelyPresent<TStores, 'artifacts'> extends true\n ? StoreIsDefinitelyAbsent<TStores, 'blobs'>\n : StoreIsDefinitelyPresent<TStores, 'blobs'> extends true\n ? StoreIsDefinitelyAbsent<TStores, 'artifacts'>\n : false\n\ntype ValidChatPersistence<TStores extends AIPersistenceStores> =\n InvalidChatPersistence<TStores> extends true ? never : unknown\n\ntype ValidGenerationPersistence<TStores extends AIPersistenceStores> =\n InvalidGenerationPersistence<TStores> extends true ? never : unknown\n\nasync function createOrResumeRun(\n runs: RunStore | undefined,\n runId: string,\n threadId: string,\n): Promise<void> {\n await runs?.createOrResume({\n runId,\n threadId,\n startedAt: Date.now(),\n })\n}\n\nasync function completeRun(\n runs: RunStore | undefined,\n runId: string,\n usage?: TokenUsage,\n): Promise<void> {\n await runs?.update(runId, {\n status: 'completed',\n finishedAt: Date.now(),\n ...(usage ? { usage } : {}),\n })\n}\n\nasync function failRun(\n runs: RunStore | undefined,\n runId: string,\n error: unknown,\n): Promise<void> {\n // `RunRecord.error` is a structured `RunError`. Only `message` is filled in\n // here: the middleware sees an opaque thrown value, and inventing a `code`\n // from it would fabricate the stable classification consumers branch on. A\n // provider-supplied code reaches the record through the adapter layer.\n await runs?.update(runId, {\n status: 'failed',\n finishedAt: Date.now(),\n error: { message: error instanceof Error ? error.message : String(error) },\n })\n}\n\n/**\n * Record a human-in-the-loop PAUSE.\n *\n * Deliberately writes NO `finishedAt`: `'interrupted'` is not a terminal status\n * (`isTerminalRunStatus('interrupted')` is `false`), and stamping a terminal\n * timestamp on it told every reader the run was over while it was in fact\n * waiting for a human. Only `abortRun`/`completeRun`/`failRun` finish a run.\n */\nexport async function interruptRun(\n runs: RunStore | undefined,\n runId: string,\n): Promise<void> {\n await runs?.update(runId, {\n status: 'interrupted',\n })\n}\n\n/**\n * Record that the run has ended for good — an explicit cancel, or a disconnect\n * on a run that has nothing to reattach to. Terminal, so it carries\n * `finishedAt`.\n */\nexport async function abortRun(\n runs: RunStore | undefined,\n runId: string,\n): Promise<void> {\n await runs?.update(runId, {\n status: 'aborted',\n finishedAt: Date.now(),\n })\n}\n\n/**\n * Whether some middleware has declared this run detachable — i.e. it has a\n * durable event log and a run store, so a disconnect can be survived and the\n * run picked back up rather than destroyed.\n *\n * The capability is read from CORE, never from `@tanstack/ai-sandbox`: sandbox\n * provides it, persistence consumes it, and a persistence → sandbox import\n * would invert the layering.\n */\nfunction detachableRun(ctx: ChatMiddlewareContext): boolean {\n return getDetachableRun(ctx, { optional: true }) === true\n}\n\n// ---------------------------------------------------------------------------\n// Chat middleware\n// ---------------------------------------------------------------------------\n\n/**\n * Chat-only **state** persistence middleware. Provides durable transcript,\n * run records, and interrupts for `chat()`. Does **not** provide locks —\n * use `withLocks` from `@tanstack/ai` for multi-instance coordination.\n *\n * This middleware never mutates the chunk stream; delivery durability\n * (replaying a disconnected/reloaded stream) is a separate transport-layer\n * concern (see the resumable-streams docs).\n *\n * Requires `stores.messages`. When `stores.interrupts` is present,\n * `stores.runs` is also required.\n *\n * ⚠️ AUTHORITATIVE-HISTORY CONTRACT: when a request carries a non-empty\n * `messages` array it is treated as the FULL conversation history and, on\n * finish, **overwrites** the entire stored thread. Post only the complete\n * transcript, never a delta — sending just the newest message(s) will replace\n * (and thereby destroy) the stored thread. To continue a stored thread without\n * resending history, pass an empty `messages` array and the stored transcript\n * is loaded and used.\n */\nexport interface WithPersistenceOptions {\n /**\n * Also persist a throttled snapshot of the in-progress assistant reply while\n * it streams. Off by default — the transcript is otherwise persisted at the\n * pending turn (`onStart`), interrupt boundaries, and completion (`onFinish`).\n * Enable it to recover partial output if the process dies mid-generation, at\n * the cost of extra writes. Snapshots are throttled to at most one per\n * {@link WithPersistenceOptions.snapshotIntervalMs}.\n */\n snapshotStreaming?: boolean\n /**\n * Minimum milliseconds between streaming snapshots when `snapshotStreaming`\n * is on. Defaults to 1000.\n */\n snapshotIntervalMs?: number\n}\n\n/**\n * @param persistence - Must satisfy {@link ChatTranscriptStores} (messages\n * required). Known-absent `messages` or `interrupts` without `runs` fail at\n * compile time; fully dynamic bags are checked at runtime.\n */\nexport function withPersistence<TStores extends ChatTranscriptStores>(\n persistence: AIPersistence<TStores> & ValidChatPersistence<TStores>,\n options: WithPersistenceOptions = {},\n): ChatMiddleware {\n // Runtime validation covers dynamic bags that bypass the generic constraint.\n validateChatPersistenceStores(persistence)\n const snapshotStreaming = options.snapshotStreaming ?? false\n const snapshotIntervalMs = options.snapshotIntervalMs ?? 1000\n const plan = resolvePersistencePlan(persistence)\n const { wantsInterrupts, runs } = plan\n const messageStore = persistence.stores.messages\n if (!messageStore) {\n // validateChatPersistenceStores already throws; this narrows for TypeScript.\n throw new Error('Chat persistence requires stores.messages.')\n }\n\n const provides = [\n PersistenceCapability,\n ...(wantsInterrupts ? [InterruptsCapability] : []),\n ]\n\n return defineChatMiddleware({\n name: 'chat-persistence',\n provides,\n setup(ctx: ChatMiddlewareContext) {\n providePersistence(ctx, persistence)\n\n runState.set(ctx, {\n merged: false,\n interrupted: false,\n })\n\n if (wantsInterrupts && persistence.stores.interrupts) {\n provideInterrupts(ctx, persistence.stores.interrupts)\n }\n\n // Offer the pending-turn seam so a middleware that is about to be SLOW can\n // have the user's turn stored before it starts. Only `onStart` stores the\n // turn otherwise, and `onStart` runs after every middleware `setup` — which\n // is milliseconds for a normal run and MINUTES for one that builds a sandbox.\n // For that whole window the thread reads as empty, so a reload or a second\n // device shows no sign of the message the user just sent.\n //\n // Offering it changes nothing on its own: a run whose middleware never calls\n // it behaves exactly as before. See `PendingTurnCapability`.\n providePendingTurn(ctx, {\n snapshot: async () => {\n const stored = await messageStore.loadThread(ctx.threadId)\n // The SAME rule `onConfig` applies when it merges. Kept here, in the\n // owner, because `saveThread` REPLACES the thread: a caller that stored\n // only the newly-sent list would delete the history.\n const list = ctx.messages.length > 0 ? [...ctx.messages] : stored\n await messageStore.saveThread(ctx.threadId, list)\n },\n })\n },\n\n async onConfig(ctx: ChatMiddlewareContext, config: ChatMiddlewareConfig) {\n if (ctx.phase !== 'init') return\n\n const patch: Partial<ChatMiddlewareConfig> = {}\n\n if (wantsInterrupts && persistence.stores.interrupts) {\n const pending = await persistence.stores.interrupts.listPending(\n ctx.threadId,\n )\n // Gate: a thread with pending interrupts must carry a resume batch that\n // references them.\n const resumeByInterruptId = validatePendingResumes(\n pending,\n config.resume,\n )\n // Persistence is the server-authoritative resume path: translate the\n // persisted interrupts into the engine's resume tool state and CLEAR\n // `config.resume`, so the engine skips its ephemeral reconstruction\n // (which needs a parentRunId and the client message history the\n // persistence flow deliberately omits).\n if ((config.resume?.length ?? 0) > 0) {\n const resumeToolState = resumeToolStateFromPending(\n pending,\n resumeByInterruptId,\n )\n patch.resume = []\n if (resumeToolState) patch.resumeToolState = resumeToolState\n }\n // Defer marking these interrupts resolved/cancelled until the run\n // succeeds (see commitPendingResumes). Committing here would consume the\n // approval even if the run then failed, breaking a retry.\n const state = runState.get(ctx)\n if (state && pending.length > 0) {\n state.pendingResumes = { pending, resumeByInterruptId }\n }\n }\n\n await createOrResumeRun(runs, ctx.runId, ctx.threadId)\n\n {\n const state = runState.get(ctx)\n if (!state?.merged) {\n if (state) state.merged = true\n const stored = await messageStore.loadThread(ctx.threadId)\n patch.messages = config.messages.length > 0 ? config.messages : stored\n }\n }\n\n return Object.keys(patch).length > 0 ? patch : undefined\n },\n\n async onStart(ctx: ChatMiddlewareContext) {\n // (A) Persist the pending turn (the just-submitted user message plus any\n // prior history) as soon as the run starts, so a reload mid-run rehydrates\n // it before the assistant reply exists. Best-effort: a failed eager\n // snapshot must not abort the run — the authoritative save is `onFinish`.\n try {\n await messageStore.saveThread(ctx.threadId, [...ctx.messages])\n } catch {\n // Eager pre-save is best-effort; the run continues and onFinish saves.\n }\n },\n\n async onChunk(ctx: ChatMiddlewareContext, chunk: StreamChunk) {\n // Always capture the current assistant turn's stream messageId (cheap),\n // regardless of snapshotStreaming — it's persisted onto the assistant\n // message so its identity survives hydrate and a reload resumes the same\n // bubble in place.\n if (chunk.type === 'TEXT_MESSAGE_START') {\n const s = runState.get(ctx)\n if (s) {\n s.streamingMessageId = chunk.messageId\n s.streamingText = ''\n }\n }\n\n // (B) Optional throttled snapshot of the in-progress assistant reply, so\n // partial output survives a crash/reload before onFinish. Off unless\n // `snapshotStreaming` is set. We accumulate the terminal turn's text here\n // (the engine only appends assistant turns with tool calls to\n // `ctx.messages`, never a streaming text reply), then persist\n // `ctx.messages` + that partial assistant message (tagged with its id).\n if (\n snapshotStreaming &&\n chunk.type === 'TEXT_MESSAGE_CONTENT' &&\n typeof chunk.delta === 'string'\n ) {\n const snapshotState = runState.get(ctx)\n if (snapshotState) {\n snapshotState.streamingText =\n (snapshotState.streamingText ?? '') + chunk.delta\n const now = Date.now()\n if (now - (snapshotState.lastSnapshotAt ?? 0) >= snapshotIntervalMs) {\n snapshotState.lastSnapshotAt = now\n try {\n await messageStore.saveThread(ctx.threadId, [\n ...ctx.messages,\n {\n role: 'assistant',\n content: snapshotState.streamingText,\n ...(snapshotState.streamingMessageId\n ? { id: snapshotState.streamingMessageId }\n : {}),\n },\n ])\n } catch {\n // Streaming snapshots are best-effort; onFinish persists final.\n }\n }\n }\n }\n\n // State-only: react to the interrupt boundary (create interrupt records,\n // mark the run interrupted, snapshot thread messages). The chunk stream is\n // never mutated — delivery durability is a transport-layer concern.\n if (\n chunk.type !== 'RUN_FINISHED' ||\n chunk.outcome?.type !== 'interrupt'\n ) {\n return\n }\n const state = runState.get(ctx)\n if (!state) return\n\n if (wantsInterrupts && persistence.stores.interrupts) {\n // The run reached a new interrupt boundary, so the resumes it consumed\n // are committed before the fresh interrupts are recorded.\n await commitPendingResumes(state, persistence.stores.interrupts)\n for (const interrupt of chunk.outcome.interrupts) {\n await persistence.stores.interrupts.create({\n interruptId: interrupt.id,\n runId: ctx.runId,\n threadId: ctx.threadId,\n requestedAt: Date.now(),\n payload: interruptPayload(interrupt),\n })\n }\n }\n await interruptRun(runs, ctx.runId)\n await messageStore.saveThread(ctx.threadId, [...ctx.messages])\n state.interrupted = true\n },\n\n async onFinish(ctx: ChatMiddlewareContext, info: FinishInfo) {\n const state = runState.get(ctx)\n if (state?.interrupted) return\n // Transcript first: if saveThread fails the run stays non-completed and\n // resumes stay pending so a retry can re-apply them. Completing the run\n // or consuming approvals before the durable history lands leaves a\n // \"finished\" run whose transcript is missing the terminal turn.\n await messageStore.saveThread(\n ctx.threadId,\n finishedTranscript(ctx.messages, info, state?.streamingMessageId),\n )\n await completeRun(runs, ctx.runId, info.usage)\n await commitPendingResumes(state, persistence.stores.interrupts)\n },\n\n async onError(ctx: ChatMiddlewareContext, info: ErrorInfo) {\n await failRun(runs, ctx.runId, info.error)\n },\n\n async onAbort(ctx: ChatMiddlewareContext, info: AbortInfo) {\n // A user pressing Stop and a user closing the tab produce the IDENTICAL\n // connection close, so intent is not inferable from the abort. It arrives\n // out of band in two bands, and either is authoritative: in-process\n // (`info.cancelRequested`, set when the cancel aborted this host's signal)\n // and durable (`RunRecord.cancelRequested`, the only channel that reaches\n // a run being driven elsewhere).\n const cancelled =\n info.cancelRequested === true ||\n (runs !== undefined && (await wasCancelRequested(runs, ctx.runId)))\n\n // A run paused at an interrupt boundary is waiting for a HUMAN, not for\n // this socket. `chat()` skips its terminal hook at an actionable-wait\n // boundary, so its `finally` routes the disconnect here — and\n // terminalizing then produced a record claiming the run finished while\n // the interrupt rows stayed `'pending'` and `validatePendingResumes`\n // still threw on the next request. An explicit cancel is different: the\n // user gave up on the approval, so the cancel band stays authoritative.\n const state = runState.get(ctx)\n if (cancelled || (!detachableRun(ctx) && state?.interrupted !== true)) {\n await abortRun(runs, ctx.runId)\n return\n }\n // A plain disconnect on a detachable or interrupted run: write NOTHING.\n // Either the agent is still running and a later attach can take it over\n // (the record stays `'running'`; the detach path records `detachedSince`\n // for the reaper), or the run is paused at an interrupt and the record\n // must stay `'interrupted'` so the pending resumes can still be applied.\n },\n })\n}\n\n// ---------------------------------------------------------------------------\n// Generation middleware\n// ---------------------------------------------------------------------------\n\n/**\n * Generation-only persistence middleware. Tracks generation run status (run\n * records keyed by `runId`) and, when `stores.artifacts` + `stores.blobs` are\n * both provided, persists the generated media for image, audio, TTS, video, and\n * transcription activities.\n *\n * Requires `stores.generationRuns`. A generation activity has no conversation,\n * so the run is keyed on its own `runId` (`ctx.runId ?? ctx.requestId`), which\n * is never faked from anything else.\n *\n * A `threadId` is REQUIRED alongside it — not as a link to a chat, but as the\n * stable app-chosen slot successive runs of the same thing fill\n * (`product-123-hero`, `video-9-start-frame`). It is what\n * `stores.generationRuns.findLatestForThread` keys on, and therefore the only\n * way a run is ever hydrated again. It comes from the `threadId` passed to the\n * activity, or from {@link WithGenerationPersistenceOptions.threadId} when that\n * overrides it; supplying neither throws at `onStart` rather than filing a run\n * nothing can find.\n *\n * On success the terminal result metadata (ids, urls — never media bytes) and,\n * when artifact persistence is on, the persisted artifact refs are captured onto\n * the run record so a server-authoritative client can hydrate the last\n * generation for a thread via {@link reconstructGeneration}.\n */\nexport function withGenerationPersistence<TStores extends AIPersistenceStores>(\n persistence: AIPersistence<TStores> & ValidGenerationPersistence<TStores>,\n opts?: WithGenerationPersistenceOptions,\n): GenerationMiddleware\nexport function withGenerationPersistence(\n persistence: AIPersistence,\n opts: WithGenerationPersistenceOptions = {},\n): GenerationMiddleware {\n validateGenerationPersistenceStores(persistence)\n const { wantsArtifactPersistence } = resolvePersistencePlan(persistence)\n const generationRuns = persistence.stores.generationRuns\n if (!generationRuns) {\n // validateGenerationPersistenceStores already throws; this narrows for TypeScript.\n throw new Error('Generation persistence requires stores.generationRuns.')\n }\n\n const runIdOf = (ctx: GenerationMiddlewareContext): string =>\n ctx.runId ?? ctx.requestId\n\n return {\n name: 'generation-persistence',\n\n async onStart(ctx: GenerationMiddlewareContext) {\n const runId = runIdOf(ctx)\n await generationRuns.createOrResume({\n runId,\n activity: ctx.activity,\n provider: ctx.provider,\n model: ctx.model,\n startedAt: Date.now(),\n threadId: generationScope(ctx, opts),\n })\n\n // Extract + persist artifact bytes (media → blobs, metadata → artifacts)\n // and merge the resulting refs onto the result. Gated on artifact stores.\n if (wantsArtifactPersistence) {\n ctx.resultTransforms?.push(async (result) => {\n const refs = await persistGenerationArtifacts(\n persistence,\n opts,\n ctx,\n result,\n )\n if (refs.length === 0) return undefined\n const base = objectValue(result) ?? {}\n const existing = base.artifacts\n const withArtifacts = {\n ...base,\n artifacts: [...(Array.isArray(existing) ? existing : []), ...refs],\n }\n // Point the live result's media at the durable serve URL (when\n // `artifactUrl` stamped one), so live and restored results match.\n return applyDurableMediaUrls(withArtifacts, refs)\n })\n }\n\n // Always capture the terminal result metadata + any artifact refs onto the\n // run record. Registered AFTER the artifact transform so it observes the\n // fully-merged result (with the artifact refs attached). `result` is\n // metadata/urls only — the media bytes already live in the blob store.\n ctx.resultTransforms?.push(async (result) => {\n const rawArtifacts = objectValue(result)?.artifacts\n const artifacts = Array.isArray(rawArtifacts)\n ? rawArtifacts.filter(isArtifactRef)\n : []\n await generationRuns.update(runId, {\n result,\n ...(artifacts.length > 0 ? { artifacts } : {}),\n })\n return undefined\n })\n },\n\n async onFinish(\n ctx: GenerationMiddlewareContext,\n info: GenerationFinishInfo,\n ) {\n await generationRuns.update(runIdOf(ctx), {\n status: 'completed',\n finishedAt: Date.now(),\n ...(info.usage ? { usage: info.usage } : {}),\n })\n },\n\n async onError(ctx: GenerationMiddlewareContext, info: GenerationErrorInfo) {\n await generationRuns.update(runIdOf(ctx), {\n status: 'failed',\n finishedAt: Date.now(),\n error: {\n message:\n info.error instanceof Error\n ? info.error.message\n : String(info.error),\n },\n })\n },\n\n async onAbort(\n ctx: GenerationMiddlewareContext,\n _info: GenerationAbortInfo,\n ) {\n // Unconditional, unlike chat's: a generation job has no journal and no\n // agent loop, so there is nothing to reattach to. An aborted generation is\n // over, full stop — hence `'aborted'` (terminal) rather than\n // `'interrupted'`, which now means \"parked, waiting for a human\" and is\n // deliberately NOT terminal-shaped, so pairing it with `finishedAt` would\n // leave the run looking permanently active.\n await generationRuns.update(runIdOf(ctx), {\n status: 'aborted',\n finishedAt: Date.now(),\n })\n },\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ import { AIPersistence } from './types.js';
2
+ /**
3
+ * The JSON body `reconstructGeneration` returns and a server-authoritative
4
+ * client hydrates from on mount.
5
+ *
6
+ * `resumeSnapshot` mirrors the last generation run for the requested thread (or
7
+ * a specific run id): its terminal/running `status`, the `result` metadata and
8
+ * `error` it recorded, the `activity` it ran, and a `resumeState` cursor
9
+ * (present only while the run is still `running`) the client can use to tail the
10
+ * live generation. `null` when there is no matching run.
11
+ *
12
+ * `activeRun` is `{ runId }` when the resolved run is still `running`, else
13
+ * `null` — the parallel of {@link ReconstructedChat.activeRun}.
14
+ */
15
+ export interface ReconstructedGeneration {
16
+ resumeSnapshot: {
17
+ schemaVersion: 1;
18
+ resumeState: {
19
+ threadId: string;
20
+ runId: string;
21
+ } | null;
22
+ status: 'idle' | 'running' | 'complete' | 'error';
23
+ result?: unknown;
24
+ error?: {
25
+ message: string;
26
+ code?: string;
27
+ };
28
+ activity?: string;
29
+ } | null;
30
+ activeRun: {
31
+ runId: string;
32
+ } | null;
33
+ }
34
+ export interface ReconstructGenerationOptions {
35
+ /** Query parameter carrying the thread id. Defaults to `threadId`. */
36
+ param?: string;
37
+ /** Query parameter carrying the run id. Defaults to `runId`. */
38
+ runParam?: string;
39
+ /**
40
+ * Authorize access to the requested generation before loading it.
41
+ *
42
+ * ⚠️ Without this, any caller who knows or guesses `?threadId=` / `?runId=`
43
+ * receives the generation's status and result metadata. Multi-user /
44
+ * multi-tenant deployments **must** supply an authorization check (session →
45
+ * owned thread/run) or resolve a validated id in the route.
46
+ *
47
+ * Called with whichever id was supplied — the `runId` when present, else the
48
+ * `threadId`. Return:
49
+ * - `true` to allow the load
50
+ * - `false` for a default `403` response
51
+ * - a `Response` to return as-is (e.g. `401` with a body)
52
+ */
53
+ authorize?: (id: string, request: Request) => boolean | Response | Promise<boolean | Response>;
54
+ }
55
+ export interface GetGenerationHydrationOptions {
56
+ /**
57
+ * How to interpret `id`:
58
+ * - `'runId'` loads exactly that run via `stores.generationRuns.get`.
59
+ * - `'threadId'` (default) loads the latest run linked to the thread via
60
+ * `stores.generationRuns.findLatestForThread`.
61
+ */
62
+ by?: 'threadId' | 'runId';
63
+ }
64
+ /**
65
+ * The request-free core of {@link reconstructGeneration}: read the last
66
+ * generation run for a thread (or a specific run) straight from the
67
+ * `generationRuns` store and return the plain `{ resumeSnapshot, activeRun }`
68
+ * hydration payload a server-authoritative client adopts on mount.
69
+ *
70
+ * Use this from a TanStack Start server function (or any direct call) to back
71
+ * a client's `hydrateGeneration` handler without fabricating a `Request`:
72
+ *
73
+ * ```ts
74
+ * async function loadImageHydration({ data: threadId }: { data: string }) {
75
+ * // Do your own auth here — this helper does not enforce tenancy.
76
+ * return await getGenerationHydration(persistence, threadId)
77
+ * }
78
+ * ```
79
+ *
80
+ * Wire that body up as the server function's handler — build it with
81
+ * `createServerFn({ method: 'GET' })`, add an `inputValidator` that returns
82
+ * the thread id, then hand it the function above. (The chained call is shown
83
+ * split apart on purpose: Start's server-fn plugin decides which modules to
84
+ * transform by scanning source text for that call, and a package whose shipped
85
+ * comments contain it gets pulled into the transform.)
86
+ *
87
+ * ⚠️ Unlike {@link reconstructGeneration} this helper takes **no** `authorize`
88
+ * option — there is no `Request` to authorize against. Server-function callers
89
+ * must gate the call themselves (session → owned thread/run) before resolving
90
+ * the id, or any caller who guesses an id receives the run's status and result
91
+ * metadata.
92
+ *
93
+ * Returns `{ resumeSnapshot: null, activeRun: null }` when `id` is empty or no
94
+ * matching run exists, so the caller never has to special-case a first load.
95
+ */
96
+ export declare function getGenerationHydration(persistence: AIPersistence, id: string, options?: GetGenerationHydrationOptions): Promise<ReconstructedGeneration>;
97
+ /**
98
+ * Build the JSON `Response` a server-authoritative client hydrates a generation
99
+ * from on load. Reads a `?runId=` (preferred) or `?threadId=` from the request
100
+ * query and returns `{ resumeSnapshot, activeRun }`
101
+ * ({@link ReconstructedGeneration}):
102
+ *
103
+ * - Resolves the run by `runId` via `stores.generationRuns.get`, else the
104
+ * latest run filed under `threadId` via the required
105
+ * `stores.generationRuns.findLatestForThread`.
106
+ * - `resumeSnapshot` — the run mapped to a client snapshot (status, result,
107
+ * error, activity, and a `resumeState` cursor while still running), or `null`.
108
+ * - `activeRun` — `{ runId }` when the run is still generating, else `null`.
109
+ *
110
+ * Requires `stores.generationRuns`. Returns
111
+ * `{ resumeSnapshot: null, activeRun: null }` when no id is supplied or no
112
+ * matching run exists, so the caller never has to special-case a first load.
113
+ *
114
+ * This helper does **not** enforce tenancy by itself. Pass
115
+ * {@link ReconstructGenerationOptions.authorize} (or wrap the call in your own
116
+ * session gate) before exposing it on a public route.
117
+ *
118
+ * ```ts
119
+ * export async function GET(request: Request) {
120
+ * return reconstructGeneration(persistence, request, {
121
+ * authorize: async (id, req) => {
122
+ * const userId = await getSessionUserId(req)
123
+ * return userId != null && (await userOwnsThread(userId, id))
124
+ * },
125
+ * })
126
+ * }
127
+ * ```
128
+ */
129
+ export declare function reconstructGeneration(persistence: AIPersistence, request: Request, options?: ReconstructGenerationOptions): Promise<Response>;
@@ -0,0 +1,148 @@
1
+ import { validateReconstructGenerationStores } from "./types.js";
2
+ //#region src/reconstruct-generation.ts
3
+ /**
4
+ * Map the persisted run status to the client-facing resume-snapshot status.
5
+ * An `interrupted` or `aborted` run surfaces as `error` — the client has no live
6
+ * run to resume, and neither produced a usable result. (A generation abort is
7
+ * always terminal: there is no journal to reattach to, so `withGenerationPersistence`
8
+ * writes `'aborted'` rather than parking the run.)
9
+ */
10
+ function snapshotStatus(status) {
11
+ switch (status) {
12
+ case "running": return "running";
13
+ case "completed": return "complete";
14
+ case "failed":
15
+ case "interrupted":
16
+ case "aborted": return "error";
17
+ }
18
+ }
19
+ function runToSnapshot(run) {
20
+ const status = snapshotStatus(run.status);
21
+ return {
22
+ schemaVersion: 1,
23
+ resumeState: status === "running" ? {
24
+ runId: run.runId,
25
+ threadId: run.threadId
26
+ } : null,
27
+ status,
28
+ ...run.result !== void 0 ? { result: run.result } : {},
29
+ ...run.error !== void 0 ? { error: run.error } : {},
30
+ ...run.activity !== void 0 ? { activity: run.activity } : {}
31
+ };
32
+ }
33
+ function jsonResponse(body) {
34
+ return new Response(JSON.stringify(body), { headers: {
35
+ "content-type": "application/json",
36
+ "cache-control": "no-store"
37
+ } });
38
+ }
39
+ /**
40
+ * The request-free core of {@link reconstructGeneration}: read the last
41
+ * generation run for a thread (or a specific run) straight from the
42
+ * `generationRuns` store and return the plain `{ resumeSnapshot, activeRun }`
43
+ * hydration payload a server-authoritative client adopts on mount.
44
+ *
45
+ * Use this from a TanStack Start server function (or any direct call) to back
46
+ * a client's `hydrateGeneration` handler without fabricating a `Request`:
47
+ *
48
+ * ```ts
49
+ * async function loadImageHydration({ data: threadId }: { data: string }) {
50
+ * // Do your own auth here — this helper does not enforce tenancy.
51
+ * return await getGenerationHydration(persistence, threadId)
52
+ * }
53
+ * ```
54
+ *
55
+ * Wire that body up as the server function's handler — build it with
56
+ * `createServerFn({ method: 'GET' })`, add an `inputValidator` that returns
57
+ * the thread id, then hand it the function above. (The chained call is shown
58
+ * split apart on purpose: Start's server-fn plugin decides which modules to
59
+ * transform by scanning source text for that call, and a package whose shipped
60
+ * comments contain it gets pulled into the transform.)
61
+ *
62
+ * ⚠️ Unlike {@link reconstructGeneration} this helper takes **no** `authorize`
63
+ * option — there is no `Request` to authorize against. Server-function callers
64
+ * must gate the call themselves (session → owned thread/run) before resolving
65
+ * the id, or any caller who guesses an id receives the run's status and result
66
+ * metadata.
67
+ *
68
+ * Returns `{ resumeSnapshot: null, activeRun: null }` when `id` is empty or no
69
+ * matching run exists, so the caller never has to special-case a first load.
70
+ */
71
+ async function getGenerationHydration(persistence, id, options) {
72
+ validateReconstructGenerationStores(persistence);
73
+ const runStore = persistence.stores.generationRuns;
74
+ if (!runStore) throw new Error("getGenerationHydration requires stores.generationRuns.");
75
+ if (!id) return {
76
+ resumeSnapshot: null,
77
+ activeRun: null
78
+ };
79
+ const run = options?.by === "runId" ? await runStore.get(id) : await runStore.findLatestForThread(id);
80
+ if (!run) return {
81
+ resumeSnapshot: null,
82
+ activeRun: null
83
+ };
84
+ return {
85
+ resumeSnapshot: runToSnapshot(run),
86
+ activeRun: run.status === "running" ? { runId: run.runId } : null
87
+ };
88
+ }
89
+ /**
90
+ * Build the JSON `Response` a server-authoritative client hydrates a generation
91
+ * from on load. Reads a `?runId=` (preferred) or `?threadId=` from the request
92
+ * query and returns `{ resumeSnapshot, activeRun }`
93
+ * ({@link ReconstructedGeneration}):
94
+ *
95
+ * - Resolves the run by `runId` via `stores.generationRuns.get`, else the
96
+ * latest run filed under `threadId` via the required
97
+ * `stores.generationRuns.findLatestForThread`.
98
+ * - `resumeSnapshot` — the run mapped to a client snapshot (status, result,
99
+ * error, activity, and a `resumeState` cursor while still running), or `null`.
100
+ * - `activeRun` — `{ runId }` when the run is still generating, else `null`.
101
+ *
102
+ * Requires `stores.generationRuns`. Returns
103
+ * `{ resumeSnapshot: null, activeRun: null }` when no id is supplied or no
104
+ * matching run exists, so the caller never has to special-case a first load.
105
+ *
106
+ * This helper does **not** enforce tenancy by itself. Pass
107
+ * {@link ReconstructGenerationOptions.authorize} (or wrap the call in your own
108
+ * session gate) before exposing it on a public route.
109
+ *
110
+ * ```ts
111
+ * export async function GET(request: Request) {
112
+ * return reconstructGeneration(persistence, request, {
113
+ * authorize: async (id, req) => {
114
+ * const userId = await getSessionUserId(req)
115
+ * return userId != null && (await userOwnsThread(userId, id))
116
+ * },
117
+ * })
118
+ * }
119
+ * ```
120
+ */
121
+ async function reconstructGeneration(persistence, request, options) {
122
+ const params = new URL(request.url).searchParams;
123
+ const runParam = options?.runParam ?? "runId";
124
+ const threadParam = options?.param ?? "threadId";
125
+ const runId = params.get(runParam) ?? "";
126
+ const threadId = params.get(threadParam) ?? "";
127
+ const id = runId || threadId;
128
+ if (!id) return jsonResponse({
129
+ resumeSnapshot: null,
130
+ activeRun: null
131
+ });
132
+ if (options?.authorize) {
133
+ const decision = await options.authorize(id, request);
134
+ if (decision instanceof Response) return decision;
135
+ if (!decision) return new Response(JSON.stringify({ error: "Forbidden" }), {
136
+ status: 403,
137
+ headers: {
138
+ "content-type": "application/json",
139
+ "cache-control": "no-store"
140
+ }
141
+ });
142
+ }
143
+ return jsonResponse(await getGenerationHydration(persistence, id, { by: runId ? "runId" : "threadId" }));
144
+ }
145
+ //#endregion
146
+ export { getGenerationHydration, reconstructGeneration };
147
+
148
+ //# sourceMappingURL=reconstruct-generation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconstruct-generation.js","names":[],"sources":["../../src/reconstruct-generation.ts"],"sourcesContent":["import { validateReconstructGenerationStores } from './types'\nimport type { AIPersistence, GenerationRunRecord } from './types'\n\n/**\n * The JSON body `reconstructGeneration` returns and a server-authoritative\n * client hydrates from on mount.\n *\n * `resumeSnapshot` mirrors the last generation run for the requested thread (or\n * a specific run id): its terminal/running `status`, the `result` metadata and\n * `error` it recorded, the `activity` it ran, and a `resumeState` cursor\n * (present only while the run is still `running`) the client can use to tail the\n * live generation. `null` when there is no matching run.\n *\n * `activeRun` is `{ runId }` when the resolved run is still `running`, else\n * `null` — the parallel of {@link ReconstructedChat.activeRun}.\n */\nexport interface ReconstructedGeneration {\n resumeSnapshot: {\n schemaVersion: 1\n resumeState: { threadId: string; runId: string } | null\n status: 'idle' | 'running' | 'complete' | 'error'\n result?: unknown\n error?: { message: string; code?: string }\n activity?: string\n } | null\n activeRun: { runId: string } | null\n}\n\nexport interface ReconstructGenerationOptions {\n /** Query parameter carrying the thread id. Defaults to `threadId`. */\n param?: string\n /** Query parameter carrying the run id. Defaults to `runId`. */\n runParam?: string\n /**\n * Authorize access to the requested generation before loading it.\n *\n * ⚠️ Without this, any caller who knows or guesses `?threadId=` / `?runId=`\n * receives the generation's status and result metadata. Multi-user /\n * multi-tenant deployments **must** supply an authorization check (session →\n * owned thread/run) or resolve a validated id in the route.\n *\n * Called with whichever id was supplied — the `runId` when present, else the\n * `threadId`. Return:\n * - `true` to allow the load\n * - `false` for a default `403` response\n * - a `Response` to return as-is (e.g. `401` with a body)\n */\n authorize?: (\n id: string,\n request: Request,\n ) => boolean | Response | Promise<boolean | Response>\n}\n\n/**\n * Map the persisted run status to the client-facing resume-snapshot status.\n * An `interrupted` or `aborted` run surfaces as `error` — the client has no live\n * run to resume, and neither produced a usable result. (A generation abort is\n * always terminal: there is no journal to reattach to, so `withGenerationPersistence`\n * writes `'aborted'` rather than parking the run.)\n */\nfunction snapshotStatus(\n status: GenerationRunRecord['status'],\n): 'running' | 'complete' | 'error' {\n switch (status) {\n case 'running':\n return 'running'\n case 'completed':\n return 'complete'\n case 'failed':\n case 'interrupted':\n case 'aborted':\n return 'error'\n }\n}\n\nfunction runToSnapshot(\n run: GenerationRunRecord,\n): NonNullable<ReconstructedGeneration['resumeSnapshot']> {\n const status = snapshotStatus(run.status)\n return {\n schemaVersion: 1,\n resumeState:\n status === 'running'\n ? { runId: run.runId, threadId: run.threadId }\n : null,\n status,\n ...(run.result !== undefined ? { result: run.result } : {}),\n ...(run.error !== undefined ? { error: run.error } : {}),\n ...(run.activity !== undefined ? { activity: run.activity } : {}),\n }\n}\n\nfunction jsonResponse(body: ReconstructedGeneration): Response {\n return new Response(JSON.stringify(body), {\n headers: {\n 'content-type': 'application/json',\n 'cache-control': 'no-store',\n },\n })\n}\n\nexport interface GetGenerationHydrationOptions {\n /**\n * How to interpret `id`:\n * - `'runId'` loads exactly that run via `stores.generationRuns.get`.\n * - `'threadId'` (default) loads the latest run linked to the thread via\n * `stores.generationRuns.findLatestForThread`.\n */\n by?: 'threadId' | 'runId'\n}\n\n/**\n * The request-free core of {@link reconstructGeneration}: read the last\n * generation run for a thread (or a specific run) straight from the\n * `generationRuns` store and return the plain `{ resumeSnapshot, activeRun }`\n * hydration payload a server-authoritative client adopts on mount.\n *\n * Use this from a TanStack Start server function (or any direct call) to back\n * a client's `hydrateGeneration` handler without fabricating a `Request`:\n *\n * ```ts\n * async function loadImageHydration({ data: threadId }: { data: string }) {\n * // Do your own auth here — this helper does not enforce tenancy.\n * return await getGenerationHydration(persistence, threadId)\n * }\n * ```\n *\n * Wire that body up as the server function's handler — build it with\n * `createServerFn({ method: 'GET' })`, add an `inputValidator` that returns\n * the thread id, then hand it the function above. (The chained call is shown\n * split apart on purpose: Start's server-fn plugin decides which modules to\n * transform by scanning source text for that call, and a package whose shipped\n * comments contain it gets pulled into the transform.)\n *\n * ⚠️ Unlike {@link reconstructGeneration} this helper takes **no** `authorize`\n * option — there is no `Request` to authorize against. Server-function callers\n * must gate the call themselves (session → owned thread/run) before resolving\n * the id, or any caller who guesses an id receives the run's status and result\n * metadata.\n *\n * Returns `{ resumeSnapshot: null, activeRun: null }` when `id` is empty or no\n * matching run exists, so the caller never has to special-case a first load.\n */\nexport async function getGenerationHydration(\n persistence: AIPersistence,\n id: string,\n options?: GetGenerationHydrationOptions,\n): Promise<ReconstructedGeneration> {\n validateReconstructGenerationStores(persistence)\n const runStore = persistence.stores.generationRuns\n if (!runStore) {\n // validateReconstructGenerationStores already throws; this narrows for TS.\n throw new Error('getGenerationHydration requires stores.generationRuns.')\n }\n\n if (!id) {\n return { resumeSnapshot: null, activeRun: null }\n }\n\n const run =\n options?.by === 'runId'\n ? await runStore.get(id)\n : await runStore.findLatestForThread(id)\n\n if (!run) {\n return { resumeSnapshot: null, activeRun: null }\n }\n\n return {\n resumeSnapshot: runToSnapshot(run),\n activeRun: run.status === 'running' ? { runId: run.runId } : null,\n }\n}\n\n/**\n * Build the JSON `Response` a server-authoritative client hydrates a generation\n * from on load. Reads a `?runId=` (preferred) or `?threadId=` from the request\n * query and returns `{ resumeSnapshot, activeRun }`\n * ({@link ReconstructedGeneration}):\n *\n * - Resolves the run by `runId` via `stores.generationRuns.get`, else the\n * latest run filed under `threadId` via the required\n * `stores.generationRuns.findLatestForThread`.\n * - `resumeSnapshot` — the run mapped to a client snapshot (status, result,\n * error, activity, and a `resumeState` cursor while still running), or `null`.\n * - `activeRun` — `{ runId }` when the run is still generating, else `null`.\n *\n * Requires `stores.generationRuns`. Returns\n * `{ resumeSnapshot: null, activeRun: null }` when no id is supplied or no\n * matching run exists, so the caller never has to special-case a first load.\n *\n * This helper does **not** enforce tenancy by itself. Pass\n * {@link ReconstructGenerationOptions.authorize} (or wrap the call in your own\n * session gate) before exposing it on a public route.\n *\n * ```ts\n * export async function GET(request: Request) {\n * return reconstructGeneration(persistence, request, {\n * authorize: async (id, req) => {\n * const userId = await getSessionUserId(req)\n * return userId != null && (await userOwnsThread(userId, id))\n * },\n * })\n * }\n * ```\n */\nexport async function reconstructGeneration(\n persistence: AIPersistence,\n request: Request,\n options?: ReconstructGenerationOptions,\n): Promise<Response> {\n const params = new URL(request.url).searchParams\n const runParam = options?.runParam ?? 'runId'\n const threadParam = options?.param ?? 'threadId'\n const runId = params.get(runParam) ?? ''\n const threadId = params.get(threadParam) ?? ''\n\n const id = runId || threadId\n if (!id) {\n return jsonResponse({ resumeSnapshot: null, activeRun: null })\n }\n\n if (options?.authorize) {\n const decision = await options.authorize(id, request)\n if (decision instanceof Response) {\n return decision\n }\n if (!decision) {\n return new Response(JSON.stringify({ error: 'Forbidden' }), {\n status: 403,\n headers: {\n 'content-type': 'application/json',\n 'cache-control': 'no-store',\n },\n })\n }\n }\n\n return jsonResponse(\n await getGenerationHydration(persistence, id, {\n by: runId ? 'runId' : 'threadId',\n }),\n )\n}\n"],"mappings":";;;;;;;;;AA4DA,SAAS,eACP,QACkC;CAClC,QAAQ,QAAR;EACE,KAAK,WACH,OAAO;EACT,KAAK,aACH,OAAO;EACT,KAAK;EACL,KAAK;EACL,KAAK,WACH,OAAO;CACX;AACF;AAEA,SAAS,cACP,KACwD;CACxD,MAAM,SAAS,eAAe,IAAI,MAAM;CACxC,OAAO;EACL,eAAe;EACf,aACE,WAAW,YACP;GAAE,OAAO,IAAI;GAAO,UAAU,IAAI;EAAS,IAC3C;EACN;EACA,GAAI,IAAI,WAAW,KAAA,IAAY,EAAE,QAAQ,IAAI,OAAO,IAAI,CAAC;EACzD,GAAI,IAAI,UAAU,KAAA,IAAY,EAAE,OAAO,IAAI,MAAM,IAAI,CAAC;EACtD,GAAI,IAAI,aAAa,KAAA,IAAY,EAAE,UAAU,IAAI,SAAS,IAAI,CAAC;CACjE;AACF;AAEA,SAAS,aAAa,MAAyC;CAC7D,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG,EACxC,SAAS;EACP,gBAAgB;EAChB,iBAAiB;CACnB,EACF,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,eAAsB,uBACpB,aACA,IACA,SACkC;CAClC,oCAAoC,WAAW;CAC/C,MAAM,WAAW,YAAY,OAAO;CACpC,IAAI,CAAC,UAEH,MAAM,IAAI,MAAM,wDAAwD;CAG1E,IAAI,CAAC,IACH,OAAO;EAAE,gBAAgB;EAAM,WAAW;CAAK;CAGjD,MAAM,MACJ,SAAS,OAAO,UACZ,MAAM,SAAS,IAAI,EAAE,IACrB,MAAM,SAAS,oBAAoB,EAAE;CAE3C,IAAI,CAAC,KACH,OAAO;EAAE,gBAAgB;EAAM,WAAW;CAAK;CAGjD,OAAO;EACL,gBAAgB,cAAc,GAAG;EACjC,WAAW,IAAI,WAAW,YAAY,EAAE,OAAO,IAAI,MAAM,IAAI;CAC/D;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,eAAsB,sBACpB,aACA,SACA,SACmB;CACnB,MAAM,SAAS,IAAI,IAAI,QAAQ,GAAG,CAAC,CAAC;CACpC,MAAM,WAAW,SAAS,YAAY;CACtC,MAAM,cAAc,SAAS,SAAS;CACtC,MAAM,QAAQ,OAAO,IAAI,QAAQ,KAAK;CACtC,MAAM,WAAW,OAAO,IAAI,WAAW,KAAK;CAE5C,MAAM,KAAK,SAAS;CACpB,IAAI,CAAC,IACH,OAAO,aAAa;EAAE,gBAAgB;EAAM,WAAW;CAAK,CAAC;CAG/D,IAAI,SAAS,WAAW;EACtB,MAAM,WAAW,MAAM,QAAQ,UAAU,IAAI,OAAO;EACpD,IAAI,oBAAoB,UACtB,OAAO;EAET,IAAI,CAAC,UACH,OAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,YAAY,CAAC,GAAG;GAC1D,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,iBAAiB;GACnB;EACF,CAAC;CAEL;CAEA,OAAO,aACL,MAAM,uBAAuB,aAAa,IAAI,EAC5C,IAAI,QAAQ,UAAU,WACxB,CAAC,CACH;AACF"}
@@ -0,0 +1,79 @@
1
+ import { UIMessage } from '@tanstack/ai';
2
+ import { AIPersistence, ChatTranscriptStores } from './types.js';
3
+ /**
4
+ * The JSON body `reconstructChat` returns and a server-authoritative client
5
+ * hydrates from on mount.
6
+ *
7
+ * `messages` is the stored transcript as UI messages (ready to paint).
8
+ * `activeRun` is a cursor to a run still generating for the thread, or `null` —
9
+ * resolved from the STABLE thread id via `stores.runs.findActiveRun`, so the
10
+ * client learns "there is a live run to tail" without ever handling a run id.
11
+ * `interrupts` is the thread's pending human-in-the-loop interrupts (tool
12
+ * approvals, client-tool/generic waits) and the run they paused, or `null` —
13
+ * so a reload (or another device) re-prompts the approval from the SERVER, not
14
+ * from client storage. Resolved via `stores.interrupts.listPending`.
15
+ */
16
+ export interface ReconstructedChat {
17
+ messages: Array<UIMessage>;
18
+ activeRun: {
19
+ runId: string;
20
+ } | null;
21
+ interrupts: {
22
+ runId: string;
23
+ pending: Array<Record<string, unknown>>;
24
+ } | null;
25
+ }
26
+ export interface ReconstructChatOptions {
27
+ /** Query parameter carrying the thread id. Defaults to `threadId`. */
28
+ param?: string;
29
+ /**
30
+ * Authorize access to the requested thread before loading history.
31
+ *
32
+ * ⚠️ Without this, any caller who knows or guesses `?threadId=` receives the
33
+ * full transcript. Multi-user / multi-tenant deployments **must** supply
34
+ * an authorization check (session → owned threads) or resolve a validated
35
+ * thread id in the route and pass it via a custom `param` that only your
36
+ * server sets.
37
+ *
38
+ * Return:
39
+ * - `true` to allow the load
40
+ * - `false` for a default `403` response
41
+ * - a `Response` to return as-is (e.g. `401` with a body)
42
+ */
43
+ authorize?: (threadId: string, request: Request) => boolean | Response | Promise<boolean | Response>;
44
+ }
45
+ /**
46
+ * Build the JSON `Response` a server-authoritative client hydrates from on load
47
+ * (see the client-persistence guide). Reads the thread id from the request query
48
+ * (`?threadId=` by default) and returns `{ messages, activeRun, interrupts }`
49
+ * ({@link ReconstructedChat}):
50
+ *
51
+ * - `messages` — the stored transcript as UI messages.
52
+ * - `activeRun` — `{ runId }` if a run is still generating for the thread (so the
53
+ * client tails it via the durability stream), else `null`. Resolved via the
54
+ * required `stores.runs.findActiveRun`; `null` when the `runs` store is absent.
55
+ * - `interrupts` — `{ runId, pending }` if the thread has pending human-in-the-loop
56
+ * interrupts (a paused approval / wait) and the run they paused, else `null`, so
57
+ * a reload re-prompts the decision from the server. Resolved via the optional
58
+ * `stores.interrupts.listPending`; `null` when that store is absent.
59
+ *
60
+ * Requires `stores.messages`. Returns an empty transcript with no active run
61
+ * and no interrupts when the thread id is missing or the thread is unknown, so
62
+ * the caller never has to special-case a first load.
63
+ *
64
+ * This helper does **not** enforce tenancy by itself. Pass
65
+ * {@link ReconstructChatOptions.authorize} (or wrap the call in your own
66
+ * session gate) before exposing it on a public route.
67
+ *
68
+ * ```ts
69
+ * export async function GET(request: Request) {
70
+ * return reconstructChat(persistence, request, {
71
+ * authorize: async (threadId, req) => {
72
+ * const userId = await getSessionUserId(req)
73
+ * return userId != null && (await userOwnsThread(userId, threadId))
74
+ * },
75
+ * })
76
+ * }
77
+ * ```
78
+ */
79
+ export declare function reconstructChat(persistence: AIPersistence<ChatTranscriptStores>, request: Request, options?: ReconstructChatOptions): Promise<Response>;