@tanstack/ai-persistence 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/middleware.js +3 -1
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/types.d.ts +2 -32
- package/dist/esm/types.js.map +1 -1
- package/package.json +3 -3
- package/src/middleware.ts +6 -0
- package/src/types.ts +2 -32
package/dist/esm/middleware.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateChatPersistenceStores, validateGenerationPersistenceStores } from "./types.js";
|
|
2
2
|
import { InterruptsCapability, PersistenceCapability, PersistenceCompletionCapability, provideInterrupts, providePersistence, providePersistenceCompletion } from "./capabilities.js";
|
|
3
3
|
import { artifactBlobKey } from "./retrieve.js";
|
|
4
|
-
import { InterruptResumeValidationError, defineChatMiddleware, fromSpecTokenUsage, getDetachableRun, readInterruptBinding, validateInterruptResumeBatch, wasCancelRequested } from "@tanstack/ai";
|
|
4
|
+
import { InterruptResumeValidationError, MetadataCapability, defineChatMiddleware, fromSpecTokenUsage, getDetachableRun, provideMetadata, readInterruptBinding, validateInterruptResumeBatch, wasCancelRequested } from "@tanstack/ai";
|
|
5
5
|
import { createInterruptBinding, getGenericInterruptDefinitionRegistry, providePendingTurn, rehydrateInterruptRequest, toRunErrorPayload } from "@tanstack/ai/adapter-internals";
|
|
6
6
|
import { base64ToUint8Array } from "@tanstack/ai-utils";
|
|
7
7
|
//#region src/middleware.ts
|
|
@@ -979,6 +979,7 @@ function withPersistence(persistence, options = {}) {
|
|
|
979
979
|
const provides = [
|
|
980
980
|
PersistenceCapability,
|
|
981
981
|
PersistenceCompletionCapability,
|
|
982
|
+
...persistence.stores.metadata ? [MetadataCapability] : [],
|
|
982
983
|
...wantsInterrupts ? [InterruptsCapability] : []
|
|
983
984
|
];
|
|
984
985
|
return defineChatMiddleware({
|
|
@@ -986,6 +987,7 @@ function withPersistence(persistence, options = {}) {
|
|
|
986
987
|
provides,
|
|
987
988
|
setup(ctx) {
|
|
988
989
|
providePersistence(ctx, persistence);
|
|
990
|
+
if (persistence.stores.metadata) provideMetadata(ctx, persistence.stores.metadata);
|
|
989
991
|
let resolveCompletion = () => void 0;
|
|
990
992
|
let rejectCompletion = () => void 0;
|
|
991
993
|
const completion = new Promise((resolve, reject) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","names":[],"sources":["../../src/middleware.ts"],"sourcesContent":["import {\n defineChatMiddleware,\n fromSpecTokenUsage,\n getDetachableRun,\n InterruptResumeValidationError,\n readInterruptBinding,\n validateInterruptResumeBatch,\n wasCancelRequested,\n} from '@tanstack/ai'\nimport {\n createInterruptBinding,\n getGenericInterruptDefinitionRegistry,\n providePendingTurn,\n rehydrateInterruptRequest,\n toRunErrorPayload,\n} from '@tanstack/ai/adapter-internals'\nimport type {\n GenericInterruptRequest,\n InterruptDefinition,\n} from '@tanstack/ai/adapter-internals'\nimport { base64ToUint8Array } from '@tanstack/ai-utils'\nimport {\n InterruptsCapability,\n PersistenceCapability,\n PersistenceCompletionCapability,\n provideInterrupts,\n providePersistence,\n providePersistenceCompletion,\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 Interrupt,\n PendingInterruptResumeRecord,\n PersistedArtifactActivity,\n PersistedArtifactRef,\n PersistedArtifactRole,\n RunAgentResumeItem,\n StreamChunk,\n Tool,\n ToolApprovalResolution,\n BilledUsage,\n TokenUsage,\n} from '@tanstack/ai'\nimport type {\n AIPersistence,\n AIPersistenceStores,\n ArtifactRecord,\n BlobBody,\n ChatTranscriptStores,\n InterruptCommitEntry,\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 /** Usage accumulated across every model call in this chat invocation. */\n usage?: TokenUsage\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 streamingMessageCreatedAt?: Date\n completion?: {\n promise: Promise<void>\n resolve: () => void\n reject: (error: unknown) => void\n }\n}\n\nconst runState = new WeakMap<object, RunStateEntry>()\n\nconst validResumeStatuses = new Set(['resolved', 'cancelled'])\n\nfunction mergeMaps<K, V>(\n left?: ReadonlyMap<K, V>,\n right?: ReadonlyMap<K, V>,\n): Map<K, V> | undefined {\n if (!left && !right) return undefined\n return new Map([...(left ?? []), ...(right ?? [])])\n}\n\nfunction mergeSets<T>(\n left?: ReadonlySet<T>,\n right?: ReadonlySet<T>,\n): Set<T> | undefined {\n if (!left && !right) return undefined\n return new Set([...(left ?? []), ...(right ?? [])])\n}\n\nfunction mergeResumeToolState(\n left: ChatResumeToolState | undefined,\n right: ChatResumeToolState | undefined,\n): ChatResumeToolState | undefined {\n if (!left) return right\n if (!right) return left\n return {\n approvals: mergeMaps(left.approvals, right.approvals),\n clientToolResults: mergeMaps(\n left.clientToolResults,\n right.clientToolResults,\n ),\n genericInterrupts: mergeMaps(\n left.genericInterrupts,\n right.genericInterrupts,\n ),\n genericInterruptRequests: mergeMaps(\n left.genericInterruptRequests,\n right.genericInterruptRequests,\n ),\n deniedToolResults: mergeMaps(\n left.deniedToolResults,\n right.deniedToolResults,\n ),\n cancelledToolCallIds: mergeSets(\n left.cancelledToolCallIds,\n right.cancelledToolCallIds,\n ),\n }\n}\n\nfunction rejectMixedRunPending(\n pending: Array<InterruptRecord>,\n ctx: Pick<ChatMiddlewareContext, 'threadId' | 'runId'>,\n): void {\n const runIds = new Set(pending.map((interrupt) => interrupt.runId))\n if (runIds.size <= 1) return\n throw new InterruptResumeValidationError([\n {\n scope: 'batch',\n threadId: ctx.threadId,\n interruptedRunId: ctx.runId,\n generation: 0,\n interruptIds: pending.map((interrupt) => interrupt.interruptId),\n code: 'stale',\n message: 'Thread has pending interrupts from more than one run.',\n source: 'server',\n retryable: false,\n },\n ])\n}\n\nfunction validatePendingResumes(\n pending: Array<InterruptRecord>,\n resume: Array<RunAgentResumeItem> | undefined,\n ctx: Pick<ChatMiddlewareContext, 'threadId' | 'runId'>,\n): Map<string, RunAgentResumeItem> {\n const interruptedRunId = pending[0]?.runId ?? ctx.runId\n const failure = (\n interruptId: string,\n code: 'conflict' | 'unknown-interrupt',\n message: string,\n ): never => {\n throw new InterruptResumeValidationError([\n {\n scope: 'item',\n threadId: ctx.threadId,\n interruptedRunId,\n generation: 0,\n interruptId,\n code,\n message,\n source: 'client',\n retryable: false,\n },\n {\n scope: 'batch',\n threadId: ctx.threadId,\n interruptedRunId,\n generation: 0,\n interruptIds: pending.map((interrupt) => interrupt.interruptId),\n code: code === 'conflict' ? 'conflict' : 'incomplete-batch',\n message:\n 'Resume entries must resolve or cancel the complete interrupt batch.',\n source: 'client',\n retryable: false,\n },\n ])\n }\n const pendingInterruptIds = new Set(\n pending.map((interrupt) => interrupt.interruptId),\n )\n const resumeByInterruptId = new Map<string, RunAgentResumeItem>()\n for (const entry of resume ?? []) {\n if (resumeByInterruptId.has(entry.interruptId)) {\n return failure(\n entry.interruptId,\n 'conflict',\n `Interrupt ${entry.interruptId} has duplicate resume entries.`,\n )\n }\n resumeByInterruptId.set(entry.interruptId, entry)\n }\n if (pending.length === 0) {\n const staleEntry = resume?.[0]\n if (staleEntry) {\n return failure(\n staleEntry.interruptId,\n 'unknown-interrupt',\n `Resume entry references non-pending interrupt ${staleEntry.interruptId}.`,\n )\n }\n return resumeByInterruptId\n }\n const firstPending = pending[0]\n if (firstPending === undefined) return resumeByInterruptId\n if (!resume || resume.length === 0) {\n return failure(\n firstPending.interruptId,\n 'unknown-interrupt',\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 return failure(\n interrupt.interruptId,\n 'unknown-interrupt',\n `Missing resume entry for pending interrupt ${interrupt.interruptId}.`,\n )\n }\n if (!validResumeStatuses.has(entry.status)) {\n return failure(\n interrupt.interruptId,\n 'unknown-interrupt',\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 return failure(\n entry.interruptId,\n 'unknown-interrupt',\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 const entries: Array<InterruptCommitEntry> = []\n for (const interrupt of pending) {\n const entry = resumeByInterruptId.get(interrupt.interruptId)\n if (!entry) continue\n if (entry.status === 'resolved') {\n entries.push({\n interruptId: interrupt.interruptId,\n status: 'resolved',\n response: entry.payload,\n })\n } else {\n entries.push({\n interruptId: interrupt.interruptId,\n status: 'cancelled',\n })\n }\n }\n if (interrupts.commitBatch) {\n await interrupts.commitBatch(entries)\n return\n }\n const ids = new Set<string>()\n for (const entry of entries) {\n if (ids.has(entry.interruptId)) {\n throw new Error(\n `Interrupt batch contains duplicate id: ${entry.interruptId}.`,\n )\n }\n ids.add(entry.interruptId)\n const existing = await interrupts.get(entry.interruptId)\n if (!existing) {\n throw new Error(\n `Interrupt batch references missing id: ${entry.interruptId}.`,\n )\n }\n if (existing.status !== 'pending') {\n throw new Error(\n `Interrupt batch references non-pending id: ${entry.interruptId}.`,\n )\n }\n }\n for (const entry of entries) {\n if (entry.status === 'resolved') {\n await interrupts.resolve(entry.interruptId, entry.response)\n } else {\n await interrupts.cancel(entry.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 hasReservedInterruptBinding(payload: unknown): boolean {\n const descriptor = objectValue(payload)\n const metadata = objectValue(descriptor?.metadata)\n return !!metadata && 'tanstack:interruptBinding' in metadata\n}\n\nfunction isPersistedInterruptDescriptor(\n value: unknown,\n): value is Interrupt & { reason: string; message: string } {\n const record = objectValue(value)\n return (\n !!record &&\n typeof record.id === 'string' &&\n typeof record.reason === 'string' &&\n typeof record.message === 'string'\n )\n}\n\n/**\n * Does this pending record belong to the TanStack chat resume protocol?\n *\n * An external system can persist an AG-UI descriptor in the same durable\n * thread. A descriptor without a TanStack binding or legacy tool marker stays\n * pending for its owner, but it does not make this resume incomplete. Older\n * opaque records remain owned because their provenance cannot be known.\n */\nfunction isChatOwnedPendingInterrupt(interrupt: InterruptRecord): boolean {\n const kind = interruptKind(interrupt)\n return (\n !isPersistedInterruptDescriptor(interrupt.payload) ||\n stringField(interrupt.payload, 'toolCallId') !== undefined ||\n kind === 'approval' ||\n kind === 'client_tool' ||\n hasReservedInterruptBinding(interrupt.payload)\n )\n}\n\nfunction durableGenericFailure(\n ctx: Pick<ChatMiddlewareContext, 'threadId' | 'runId'>,\n persisted: InterruptRecord,\n message: string,\n): InterruptResumeValidationError {\n return new InterruptResumeValidationError([\n {\n scope: 'item',\n threadId: ctx.threadId,\n interruptedRunId: persisted.runId || ctx.runId,\n generation: 0,\n interruptId: persisted.interruptId,\n code: 'stale',\n message,\n source: 'server',\n retryable: false,\n },\n {\n scope: 'batch',\n threadId: ctx.threadId,\n interruptedRunId: persisted.runId || ctx.runId,\n generation: 0,\n interruptIds: [persisted.interruptId],\n code: 'item-validation-failed',\n message: 'One or more persisted interrupt records are invalid.',\n source: 'server',\n retryable: false,\n },\n ])\n}\n\nasync function durableGenericResumeState(\n ctx: ChatMiddlewareContext,\n pending: Array<InterruptRecord>,\n resume: ReadonlyArray<RunAgentResumeItem>,\n tools: Array<Tool>,\n): Promise<ChatResumeToolState | undefined> {\n const registry = getGenericInterruptDefinitionRegistry(ctx, {\n optional: true,\n })\n const records: Array<PendingInterruptResumeRecord> = []\n\n for (const persisted of pending) {\n if (!isPersistedInterruptDescriptor(persisted.payload)) {\n if (hasReservedInterruptBinding(persisted.payload)) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted interrupt ${persisted.interruptId} has an invalid binding descriptor.`,\n )\n }\n continue\n }\n const descriptor = persisted.payload\n const binding = readInterruptBinding(descriptor)\n if (!binding) {\n if (hasReservedInterruptBinding(descriptor)) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted interrupt ${persisted.interruptId} has an invalid or incomplete binding.`,\n )\n }\n continue\n }\n if (\n descriptor.id !== persisted.interruptId ||\n binding.interruptId !== persisted.interruptId ||\n binding.interruptedRunId !== persisted.runId ||\n binding.generation !== 0\n ) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted interrupt ${persisted.interruptId} has stale correlation metadata.`,\n )\n }\n if (binding.kind !== 'generic') {\n records.push({\n interruptId: persisted.interruptId,\n payload: descriptor,\n binding,\n })\n continue\n }\n if (\n !binding.definitionId ||\n !binding.key ||\n binding.batchIndex === undefined\n ) {\n records.push({\n interruptId: persisted.interruptId,\n payload: descriptor,\n binding,\n })\n continue\n }\n if (!registry) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted generic interrupt ${persisted.interruptId} cannot be restored because no interrupt registry is available.`,\n )\n }\n const definition = registry.definitions.get(binding.definitionId)\n if (!definition) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted generic interrupt definition ${binding.definitionId} is unavailable.`,\n )\n }\n const metadata = objectValue(descriptor.metadata)\n const payload = metadata?.['tanstack:interruptPayload']\n let request: GenericInterruptRequest<\n InterruptDefinition<any, any, any, any>\n >\n try {\n request = rehydrateInterruptRequest(definition, {\n key: binding.key,\n reason: descriptor.reason,\n message: descriptor.message,\n ...(descriptor.expiresAt !== undefined\n ? { expiresAt: descriptor.expiresAt }\n : {}),\n ...(payload !== undefined ? { payload } : {}),\n })\n } catch (error) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted generic interrupt ${persisted.interruptId} is invalid: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n const emitted = createInterruptBinding(request, {\n batchIndex: binding.batchIndex,\n })\n if (\n emitted.descriptor.responseSchemaHash !== binding.responseSchemaHash ||\n emitted.descriptor.payloadSchemaHash !== binding.payloadSchemaHash ||\n binding.interruptId !== persisted.interruptId\n ) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted generic interrupt ${persisted.interruptId} is stale.`,\n )\n }\n records.push({\n interruptId: persisted.interruptId,\n payload: descriptor,\n binding,\n genericRequest: request,\n })\n }\n\n const firstRecord = records[0]\n if (firstRecord === undefined) return undefined\n const interruptedRunId = firstRecord.binding.interruptedRunId\n const generation = firstRecord.binding.generation\n const validated = await validateInterruptResumeBatch({\n threadId: ctx.threadId,\n interruptedRunId,\n generation,\n pending: records,\n resume: resume.filter((entry) =>\n records.some((record) => record.interruptId === entry.interruptId),\n ),\n tools,\n })\n if (validated.errors.length > 0 || !validated.resumeToolState) {\n throw new InterruptResumeValidationError(validated.errors)\n }\n type GenericRecord = PendingInterruptResumeRecord & {\n binding: Extract<\n PendingInterruptResumeRecord['binding'],\n { kind: 'generic' }\n >\n genericRequest: GenericInterruptRequest<\n InterruptDefinition<any, any, any, any>\n >\n }\n const isGenericRecord = (\n record: PendingInterruptResumeRecord,\n ): record is GenericRecord =>\n record.binding.kind === 'generic' && record.genericRequest !== undefined\n const genericRecords: Array<{ record: GenericRecord; batchIndex: number }> =\n []\n const batchIndexes = new Set<number>()\n for (const record of records) {\n if (!isGenericRecord(record)) continue\n const batchIndex = record.binding.batchIndex\n if (batchIndex === undefined || batchIndexes.has(batchIndex)) {\n throw new InterruptResumeValidationError([\n {\n scope: 'batch',\n threadId: ctx.threadId,\n interruptedRunId,\n generation,\n interruptIds: records.map((item) => item.interruptId),\n code: 'stale',\n message:\n 'Persisted generic interrupts have duplicate or invalid batch indexes.',\n source: 'server',\n retryable: false,\n },\n ])\n }\n batchIndexes.add(batchIndex)\n genericRecords.push({ record, batchIndex })\n }\n genericRecords.sort((left, right) => left.batchIndex - right.batchIndex)\n return {\n ...validated.resumeToolState,\n genericInterruptRequests: new Map(\n genericRecords.flatMap(({ record }) =>\n record.genericRequest\n ? [[record.interruptId, record.genericRequest] as const]\n : [],\n ),\n ),\n }\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 const cancelledToolCallIds = new Set<string>()\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 (entry.status === 'cancelled' && toolCallId) {\n cancelledToolCallIds.add(toolCallId)\n }\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 (\n approvals.size === 0 &&\n clientToolResults.size === 0 &&\n cancelledToolCallIds.size === 0\n ) {\n return undefined\n }\n return { approvals, clientToolResults, cancelledToolCallIds }\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<TokenUsage | undefined> {\n const run = await runs?.createOrResume({\n runId,\n threadId,\n startedAt: Date.now(),\n })\n return run?.usage\n}\n\nfunction sumOptionalNumber(\n current: number | undefined,\n next: number | undefined,\n): number | undefined {\n if (current === undefined) return next\n if (next === undefined) return current\n return current + next\n}\n\nfunction sumNumberFields<T extends object>(\n current: T | undefined,\n next: T | undefined,\n): T | undefined {\n if (!current) return next\n if (!next) return current\n\n const result = { ...current }\n for (const key of Object.keys(next) as Array<keyof T>) {\n const currentValue = current[key]\n const nextValue = next[key]\n if (typeof nextValue === 'number') {\n result[key] = ((typeof currentValue === 'number' ? currentValue : 0) +\n nextValue) as T[keyof T]\n }\n }\n return result\n}\n\nfunction tokenUsageFromChunk(chunk: StreamChunk): TokenUsage | undefined {\n if (chunk.type !== 'RUN_FINISHED' && chunk.type !== 'RUN_ERROR') {\n return undefined\n }\n const usage = chunk.usage\n if (\n usage != null &&\n typeof usage === 'object' &&\n !Array.isArray(usage) &&\n 'promptTokens' in usage\n ) {\n return usage\n }\n const metadata = chunk.metadata\n const tanstack =\n metadata != null && typeof metadata === 'object' && 'tanstack' in metadata\n ? metadata.tanstack\n : undefined\n const leftover =\n tanstack != null && typeof tanstack === 'object' && !Array.isArray(tanstack)\n ? (tanstack as { usage?: TokenUsage }).usage\n : undefined\n return fromSpecTokenUsage(Array.isArray(usage) ? usage : undefined, leftover)\n}\n\nfunction accumulateTokenUsage(\n current: TokenUsage | undefined,\n next: TokenUsage,\n): TokenUsage {\n if (!current) return { ...next }\n\n const promptTokensDetails = sumNumberFields(\n current.promptTokensDetails,\n next.promptTokensDetails,\n )\n const completionTokensDetails = sumNumberFields(\n current.completionTokensDetails,\n next.completionTokensDetails,\n )\n const costDetails = sumNumberFields(current.costDetails, next.costDetails)\n // Provider-specific details are opaque, so retain the latest reported bag.\n const providerUsageDetails =\n next.providerUsageDetails ?? current.providerUsageDetails\n const durationSeconds = sumOptionalNumber(\n current.durationSeconds,\n next.durationSeconds,\n )\n const unitsBilled = sumOptionalNumber(current.unitsBilled, next.unitsBilled)\n const billed = accumulateBilled(current.billed, next.billed)\n const cost = sumOptionalNumber(current.cost, next.cost)\n\n return {\n ...current,\n ...next,\n promptTokens: current.promptTokens + next.promptTokens,\n completionTokens: current.completionTokens + next.completionTokens,\n totalTokens: current.totalTokens + next.totalTokens,\n ...(promptTokensDetails ? { promptTokensDetails } : {}),\n ...(completionTokensDetails ? { completionTokensDetails } : {}),\n ...(durationSeconds !== undefined ? { durationSeconds } : {}),\n ...(unitsBilled !== undefined ? { unitsBilled } : {}),\n ...(billed !== undefined ? { billed } : {}),\n ...(cost !== undefined ? { cost } : {}),\n ...(costDetails ? { costDetails } : {}),\n ...(providerUsageDetails ? { providerUsageDetails } : {}),\n }\n}\n\n/**\n * Sum billed quantities when both reports use the same unit. Different units\n * cannot be added, so the later report wins.\n */\nfunction accumulateBilled(\n current: BilledUsage | undefined,\n next: BilledUsage | undefined,\n): BilledUsage | undefined {\n if (!current) return next\n if (!next) return current\n if (current.unit !== next.unit) return next\n return { quantity: current.quantity + next.quantity, unit: current.unit }\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 usage?: TokenUsage,\n): Promise<void> {\n const runError = toRunErrorPayload(error)\n await runs?.update(runId, {\n status: 'failed',\n finishedAt: Date.now(),\n error: {\n message: runError.message,\n ...(runError.code !== undefined ? { code: runError.code } : {}),\n },\n ...(usage ? { usage } : {}),\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 usage?: TokenUsage,\n): Promise<void> {\n await runs?.update(runId, {\n status: 'interrupted',\n ...(usage ? { usage } : {}),\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 usage?: TokenUsage,\n): Promise<void> {\n await runs?.update(runId, {\n status: 'aborted',\n finishedAt: Date.now(),\n ...(usage ? { usage } : {}),\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 PersistenceCompletionCapability,\n ...(wantsInterrupts ? [InterruptsCapability] : []),\n ]\n\n return defineChatMiddleware({\n name: 'chat-persistence',\n provides,\n setup(ctx: ChatMiddlewareContext) {\n providePersistence(ctx, persistence)\n\n let resolveCompletion: () => void = () => undefined\n let rejectCompletion: (error: unknown) => void = () => undefined\n const completion = new Promise<void>((resolve, reject) => {\n resolveCompletion = resolve\n rejectCompletion = reject\n })\n // Consumers may not need this capability. Mark the rejection handled while\n // preserving the original promise for callers that do await it.\n void completion.catch(() => undefined)\n\n runState.set(ctx, {\n merged: false,\n interrupted: false,\n completion: {\n promise: completion,\n resolve: resolveCompletion,\n reject: rejectCompletion,\n },\n })\n providePersistenceCompletion(ctx, {\n waitForRunCompletion: () => completion,\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 only records that this chat owns. A foreign AG-UI interrupt can\n // share the durable thread, but its owner resolves it outside this\n // resume protocol. Including it would deadlock this chat resume.\n const ownedPending = pending.filter(isChatOwnedPendingInterrupt)\n rejectMixedRunPending(ownedPending, ctx)\n const resumeByInterruptId = validatePendingResumes(\n ownedPending,\n config.resume,\n ctx,\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 ownedPending,\n resumeByInterruptId,\n )\n const genericResumeState = await durableGenericResumeState(\n ctx,\n ownedPending,\n config.resume ?? [],\n config.tools,\n )\n patch.resume = []\n if (resumeToolState || genericResumeState) {\n patch.resumeToolState = mergeResumeToolState(\n resumeToolState,\n genericResumeState,\n )\n }\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 && ownedPending.length > 0) {\n state.pendingResumes = { pending: ownedPending, resumeByInterruptId }\n }\n }\n\n const storedUsage = await createOrResumeRun(runs, ctx.runId, ctx.threadId)\n\n const state = runState.get(ctx)\n // A continuation has a fresh middleware context but resumes the same run.\n if (state && storedUsage) state.usage = storedUsage\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 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 // Capture the current assistant turn's identity for optional in-progress\n // snapshots. Completed messages already live in `ctx.messages`.\n if (snapshotStreaming && ctx.phase === 'modelStream') {\n const s = runState.get(ctx)\n if (s && chunk.type === 'TEXT_MESSAGE_START') {\n // An empty/malformed messageId means \"no identity\" (matching the\n // engine's convention), leaving room for the TOOL_CALL_START\n // parentMessageId fallback below — but the per-turn accumulator\n // still resets so snapshots never mix text across turns.\n s.streamingMessageId =\n typeof chunk.messageId === 'string' && chunk.messageId !== ''\n ? chunk.messageId\n : undefined\n s.streamingMessageCreatedAt = new Date()\n s.streamingText = ''\n } else if (\n s &&\n chunk.type === 'TOOL_CALL_START' &&\n typeof chunk.parentMessageId === 'string' &&\n chunk.parentMessageId !== '' &&\n s.streamingMessageId === undefined\n ) {\n s.streamingMessageId = chunk.parentMessageId\n s.streamingMessageCreatedAt ??= new Date()\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. The completed turn enters `ctx.messages`\n // only after streaming ends, so accumulate its text here and 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 ...(snapshotState.streamingMessageCreatedAt\n ? { createdAt: snapshotState.streamingMessageCreatedAt }\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 // Adapter terminals arrive before `onUsage`; synthesized tool boundaries\n // arrive after it with the same usage already in state.\n const chunkUsage = tokenUsageFromChunk(chunk)\n const usage =\n ctx.phase === 'modelStream' && chunkUsage\n ? accumulateTokenUsage(state.usage, chunkUsage)\n : (state.usage ?? chunkUsage)\n state.usage = usage\n await interruptRun(runs, ctx.runId, usage)\n await messageStore.saveThread(ctx.threadId, [...ctx.messages])\n state.interrupted = true\n },\n\n onUsage(ctx: ChatMiddlewareContext, usage: TokenUsage) {\n const state = runState.get(ctx)\n if (!state || state.interrupted) return\n state.usage = accumulateTokenUsage(state.usage, usage)\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 try {\n await messageStore.saveThread(ctx.threadId, [...ctx.messages])\n await commitPendingResumes(state, persistence.stores.interrupts)\n await completeRun(runs, ctx.runId, state?.usage ?? info.usage)\n state?.completion?.resolve()\n } catch (error) {\n // Core has already selected its terminal hook. Persist the failed run\n // here, so a failed transcript save or batch write does not leave an\n // interrupted or completed run whose pending records need retrying.\n try {\n await failRun(runs, ctx.runId, error, state?.usage)\n } finally {\n state?.completion?.reject(error)\n }\n throw error\n }\n },\n\n async onError(ctx: ChatMiddlewareContext, info: ErrorInfo) {\n try {\n await failRun(runs, ctx.runId, info.error, runState.get(ctx)?.usage)\n } finally {\n runState.get(ctx)?.completion?.reject(info.error)\n }\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 // 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 let terminal = false\n try {\n // The durable cancel read is best-effort. It must not bypass the\n // terminal persistence path or prevent the completion promise from\n // settling when the run store is unavailable.\n const cancelled =\n info.cancelRequested === true ||\n (runs !== undefined && (await wasCancelRequested(runs, ctx.runId)))\n terminal =\n cancelled || (!detachableRun(ctx) && state?.interrupted !== true)\n if (terminal) {\n await abortRun(runs, ctx.runId, state?.usage)\n }\n } finally {\n if (terminal) state?.completion?.reject(info.reason)\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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"}
|
|
1
|
+
{"version":3,"file":"middleware.js","names":[],"sources":["../../src/middleware.ts"],"sourcesContent":["import {\n defineChatMiddleware,\n fromSpecTokenUsage,\n getDetachableRun,\n InterruptResumeValidationError,\n MetadataCapability,\n provideMetadata,\n readInterruptBinding,\n validateInterruptResumeBatch,\n wasCancelRequested,\n} from '@tanstack/ai'\nimport {\n createInterruptBinding,\n getGenericInterruptDefinitionRegistry,\n providePendingTurn,\n rehydrateInterruptRequest,\n toRunErrorPayload,\n} from '@tanstack/ai/adapter-internals'\nimport type {\n GenericInterruptRequest,\n InterruptDefinition,\n} from '@tanstack/ai/adapter-internals'\nimport { base64ToUint8Array } from '@tanstack/ai-utils'\nimport {\n InterruptsCapability,\n PersistenceCapability,\n PersistenceCompletionCapability,\n provideInterrupts,\n providePersistence,\n providePersistenceCompletion,\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 Interrupt,\n PendingInterruptResumeRecord,\n PersistedArtifactActivity,\n PersistedArtifactRef,\n PersistedArtifactRole,\n RunAgentResumeItem,\n StreamChunk,\n Tool,\n ToolApprovalResolution,\n BilledUsage,\n TokenUsage,\n} from '@tanstack/ai'\nimport type {\n AIPersistence,\n AIPersistenceStores,\n ArtifactRecord,\n BlobBody,\n ChatTranscriptStores,\n InterruptCommitEntry,\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 /** Usage accumulated across every model call in this chat invocation. */\n usage?: TokenUsage\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 streamingMessageCreatedAt?: Date\n completion?: {\n promise: Promise<void>\n resolve: () => void\n reject: (error: unknown) => void\n }\n}\n\nconst runState = new WeakMap<object, RunStateEntry>()\n\nconst validResumeStatuses = new Set(['resolved', 'cancelled'])\n\nfunction mergeMaps<K, V>(\n left?: ReadonlyMap<K, V>,\n right?: ReadonlyMap<K, V>,\n): Map<K, V> | undefined {\n if (!left && !right) return undefined\n return new Map([...(left ?? []), ...(right ?? [])])\n}\n\nfunction mergeSets<T>(\n left?: ReadonlySet<T>,\n right?: ReadonlySet<T>,\n): Set<T> | undefined {\n if (!left && !right) return undefined\n return new Set([...(left ?? []), ...(right ?? [])])\n}\n\nfunction mergeResumeToolState(\n left: ChatResumeToolState | undefined,\n right: ChatResumeToolState | undefined,\n): ChatResumeToolState | undefined {\n if (!left) return right\n if (!right) return left\n return {\n approvals: mergeMaps(left.approvals, right.approvals),\n clientToolResults: mergeMaps(\n left.clientToolResults,\n right.clientToolResults,\n ),\n genericInterrupts: mergeMaps(\n left.genericInterrupts,\n right.genericInterrupts,\n ),\n genericInterruptRequests: mergeMaps(\n left.genericInterruptRequests,\n right.genericInterruptRequests,\n ),\n deniedToolResults: mergeMaps(\n left.deniedToolResults,\n right.deniedToolResults,\n ),\n cancelledToolCallIds: mergeSets(\n left.cancelledToolCallIds,\n right.cancelledToolCallIds,\n ),\n }\n}\n\nfunction rejectMixedRunPending(\n pending: Array<InterruptRecord>,\n ctx: Pick<ChatMiddlewareContext, 'threadId' | 'runId'>,\n): void {\n const runIds = new Set(pending.map((interrupt) => interrupt.runId))\n if (runIds.size <= 1) return\n throw new InterruptResumeValidationError([\n {\n scope: 'batch',\n threadId: ctx.threadId,\n interruptedRunId: ctx.runId,\n generation: 0,\n interruptIds: pending.map((interrupt) => interrupt.interruptId),\n code: 'stale',\n message: 'Thread has pending interrupts from more than one run.',\n source: 'server',\n retryable: false,\n },\n ])\n}\n\nfunction validatePendingResumes(\n pending: Array<InterruptRecord>,\n resume: Array<RunAgentResumeItem> | undefined,\n ctx: Pick<ChatMiddlewareContext, 'threadId' | 'runId'>,\n): Map<string, RunAgentResumeItem> {\n const interruptedRunId = pending[0]?.runId ?? ctx.runId\n const failure = (\n interruptId: string,\n code: 'conflict' | 'unknown-interrupt',\n message: string,\n ): never => {\n throw new InterruptResumeValidationError([\n {\n scope: 'item',\n threadId: ctx.threadId,\n interruptedRunId,\n generation: 0,\n interruptId,\n code,\n message,\n source: 'client',\n retryable: false,\n },\n {\n scope: 'batch',\n threadId: ctx.threadId,\n interruptedRunId,\n generation: 0,\n interruptIds: pending.map((interrupt) => interrupt.interruptId),\n code: code === 'conflict' ? 'conflict' : 'incomplete-batch',\n message:\n 'Resume entries must resolve or cancel the complete interrupt batch.',\n source: 'client',\n retryable: false,\n },\n ])\n }\n const pendingInterruptIds = new Set(\n pending.map((interrupt) => interrupt.interruptId),\n )\n const resumeByInterruptId = new Map<string, RunAgentResumeItem>()\n for (const entry of resume ?? []) {\n if (resumeByInterruptId.has(entry.interruptId)) {\n return failure(\n entry.interruptId,\n 'conflict',\n `Interrupt ${entry.interruptId} has duplicate resume entries.`,\n )\n }\n resumeByInterruptId.set(entry.interruptId, entry)\n }\n if (pending.length === 0) {\n const staleEntry = resume?.[0]\n if (staleEntry) {\n return failure(\n staleEntry.interruptId,\n 'unknown-interrupt',\n `Resume entry references non-pending interrupt ${staleEntry.interruptId}.`,\n )\n }\n return resumeByInterruptId\n }\n const firstPending = pending[0]\n if (firstPending === undefined) return resumeByInterruptId\n if (!resume || resume.length === 0) {\n return failure(\n firstPending.interruptId,\n 'unknown-interrupt',\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 return failure(\n interrupt.interruptId,\n 'unknown-interrupt',\n `Missing resume entry for pending interrupt ${interrupt.interruptId}.`,\n )\n }\n if (!validResumeStatuses.has(entry.status)) {\n return failure(\n interrupt.interruptId,\n 'unknown-interrupt',\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 return failure(\n entry.interruptId,\n 'unknown-interrupt',\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 const entries: Array<InterruptCommitEntry> = []\n for (const interrupt of pending) {\n const entry = resumeByInterruptId.get(interrupt.interruptId)\n if (!entry) continue\n if (entry.status === 'resolved') {\n entries.push({\n interruptId: interrupt.interruptId,\n status: 'resolved',\n response: entry.payload,\n })\n } else {\n entries.push({\n interruptId: interrupt.interruptId,\n status: 'cancelled',\n })\n }\n }\n if (interrupts.commitBatch) {\n await interrupts.commitBatch(entries)\n return\n }\n const ids = new Set<string>()\n for (const entry of entries) {\n if (ids.has(entry.interruptId)) {\n throw new Error(\n `Interrupt batch contains duplicate id: ${entry.interruptId}.`,\n )\n }\n ids.add(entry.interruptId)\n const existing = await interrupts.get(entry.interruptId)\n if (!existing) {\n throw new Error(\n `Interrupt batch references missing id: ${entry.interruptId}.`,\n )\n }\n if (existing.status !== 'pending') {\n throw new Error(\n `Interrupt batch references non-pending id: ${entry.interruptId}.`,\n )\n }\n }\n for (const entry of entries) {\n if (entry.status === 'resolved') {\n await interrupts.resolve(entry.interruptId, entry.response)\n } else {\n await interrupts.cancel(entry.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 hasReservedInterruptBinding(payload: unknown): boolean {\n const descriptor = objectValue(payload)\n const metadata = objectValue(descriptor?.metadata)\n return !!metadata && 'tanstack:interruptBinding' in metadata\n}\n\nfunction isPersistedInterruptDescriptor(\n value: unknown,\n): value is Interrupt & { reason: string; message: string } {\n const record = objectValue(value)\n return (\n !!record &&\n typeof record.id === 'string' &&\n typeof record.reason === 'string' &&\n typeof record.message === 'string'\n )\n}\n\n/**\n * Does this pending record belong to the TanStack chat resume protocol?\n *\n * An external system can persist an AG-UI descriptor in the same durable\n * thread. A descriptor without a TanStack binding or legacy tool marker stays\n * pending for its owner, but it does not make this resume incomplete. Older\n * opaque records remain owned because their provenance cannot be known.\n */\nfunction isChatOwnedPendingInterrupt(interrupt: InterruptRecord): boolean {\n const kind = interruptKind(interrupt)\n return (\n !isPersistedInterruptDescriptor(interrupt.payload) ||\n stringField(interrupt.payload, 'toolCallId') !== undefined ||\n kind === 'approval' ||\n kind === 'client_tool' ||\n hasReservedInterruptBinding(interrupt.payload)\n )\n}\n\nfunction durableGenericFailure(\n ctx: Pick<ChatMiddlewareContext, 'threadId' | 'runId'>,\n persisted: InterruptRecord,\n message: string,\n): InterruptResumeValidationError {\n return new InterruptResumeValidationError([\n {\n scope: 'item',\n threadId: ctx.threadId,\n interruptedRunId: persisted.runId || ctx.runId,\n generation: 0,\n interruptId: persisted.interruptId,\n code: 'stale',\n message,\n source: 'server',\n retryable: false,\n },\n {\n scope: 'batch',\n threadId: ctx.threadId,\n interruptedRunId: persisted.runId || ctx.runId,\n generation: 0,\n interruptIds: [persisted.interruptId],\n code: 'item-validation-failed',\n message: 'One or more persisted interrupt records are invalid.',\n source: 'server',\n retryable: false,\n },\n ])\n}\n\nasync function durableGenericResumeState(\n ctx: ChatMiddlewareContext,\n pending: Array<InterruptRecord>,\n resume: ReadonlyArray<RunAgentResumeItem>,\n tools: Array<Tool>,\n): Promise<ChatResumeToolState | undefined> {\n const registry = getGenericInterruptDefinitionRegistry(ctx, {\n optional: true,\n })\n const records: Array<PendingInterruptResumeRecord> = []\n\n for (const persisted of pending) {\n if (!isPersistedInterruptDescriptor(persisted.payload)) {\n if (hasReservedInterruptBinding(persisted.payload)) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted interrupt ${persisted.interruptId} has an invalid binding descriptor.`,\n )\n }\n continue\n }\n const descriptor = persisted.payload\n const binding = readInterruptBinding(descriptor)\n if (!binding) {\n if (hasReservedInterruptBinding(descriptor)) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted interrupt ${persisted.interruptId} has an invalid or incomplete binding.`,\n )\n }\n continue\n }\n if (\n descriptor.id !== persisted.interruptId ||\n binding.interruptId !== persisted.interruptId ||\n binding.interruptedRunId !== persisted.runId ||\n binding.generation !== 0\n ) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted interrupt ${persisted.interruptId} has stale correlation metadata.`,\n )\n }\n if (binding.kind !== 'generic') {\n records.push({\n interruptId: persisted.interruptId,\n payload: descriptor,\n binding,\n })\n continue\n }\n if (\n !binding.definitionId ||\n !binding.key ||\n binding.batchIndex === undefined\n ) {\n records.push({\n interruptId: persisted.interruptId,\n payload: descriptor,\n binding,\n })\n continue\n }\n if (!registry) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted generic interrupt ${persisted.interruptId} cannot be restored because no interrupt registry is available.`,\n )\n }\n const definition = registry.definitions.get(binding.definitionId)\n if (!definition) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted generic interrupt definition ${binding.definitionId} is unavailable.`,\n )\n }\n const metadata = objectValue(descriptor.metadata)\n const payload = metadata?.['tanstack:interruptPayload']\n let request: GenericInterruptRequest<\n InterruptDefinition<any, any, any, any>\n >\n try {\n request = rehydrateInterruptRequest(definition, {\n key: binding.key,\n reason: descriptor.reason,\n message: descriptor.message,\n ...(descriptor.expiresAt !== undefined\n ? { expiresAt: descriptor.expiresAt }\n : {}),\n ...(payload !== undefined ? { payload } : {}),\n })\n } catch (error) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted generic interrupt ${persisted.interruptId} is invalid: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n const emitted = createInterruptBinding(request, {\n batchIndex: binding.batchIndex,\n })\n if (\n emitted.descriptor.responseSchemaHash !== binding.responseSchemaHash ||\n emitted.descriptor.payloadSchemaHash !== binding.payloadSchemaHash ||\n binding.interruptId !== persisted.interruptId\n ) {\n throw durableGenericFailure(\n ctx,\n persisted,\n `Persisted generic interrupt ${persisted.interruptId} is stale.`,\n )\n }\n records.push({\n interruptId: persisted.interruptId,\n payload: descriptor,\n binding,\n genericRequest: request,\n })\n }\n\n const firstRecord = records[0]\n if (firstRecord === undefined) return undefined\n const interruptedRunId = firstRecord.binding.interruptedRunId\n const generation = firstRecord.binding.generation\n const validated = await validateInterruptResumeBatch({\n threadId: ctx.threadId,\n interruptedRunId,\n generation,\n pending: records,\n resume: resume.filter((entry) =>\n records.some((record) => record.interruptId === entry.interruptId),\n ),\n tools,\n })\n if (validated.errors.length > 0 || !validated.resumeToolState) {\n throw new InterruptResumeValidationError(validated.errors)\n }\n type GenericRecord = PendingInterruptResumeRecord & {\n binding: Extract<\n PendingInterruptResumeRecord['binding'],\n { kind: 'generic' }\n >\n genericRequest: GenericInterruptRequest<\n InterruptDefinition<any, any, any, any>\n >\n }\n const isGenericRecord = (\n record: PendingInterruptResumeRecord,\n ): record is GenericRecord =>\n record.binding.kind === 'generic' && record.genericRequest !== undefined\n const genericRecords: Array<{ record: GenericRecord; batchIndex: number }> =\n []\n const batchIndexes = new Set<number>()\n for (const record of records) {\n if (!isGenericRecord(record)) continue\n const batchIndex = record.binding.batchIndex\n if (batchIndex === undefined || batchIndexes.has(batchIndex)) {\n throw new InterruptResumeValidationError([\n {\n scope: 'batch',\n threadId: ctx.threadId,\n interruptedRunId,\n generation,\n interruptIds: records.map((item) => item.interruptId),\n code: 'stale',\n message:\n 'Persisted generic interrupts have duplicate or invalid batch indexes.',\n source: 'server',\n retryable: false,\n },\n ])\n }\n batchIndexes.add(batchIndex)\n genericRecords.push({ record, batchIndex })\n }\n genericRecords.sort((left, right) => left.batchIndex - right.batchIndex)\n return {\n ...validated.resumeToolState,\n genericInterruptRequests: new Map(\n genericRecords.flatMap(({ record }) =>\n record.genericRequest\n ? [[record.interruptId, record.genericRequest] as const]\n : [],\n ),\n ),\n }\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 const cancelledToolCallIds = new Set<string>()\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 (entry.status === 'cancelled' && toolCallId) {\n cancelledToolCallIds.add(toolCallId)\n }\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 (\n approvals.size === 0 &&\n clientToolResults.size === 0 &&\n cancelledToolCallIds.size === 0\n ) {\n return undefined\n }\n return { approvals, clientToolResults, cancelledToolCallIds }\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<TokenUsage | undefined> {\n const run = await runs?.createOrResume({\n runId,\n threadId,\n startedAt: Date.now(),\n })\n return run?.usage\n}\n\nfunction sumOptionalNumber(\n current: number | undefined,\n next: number | undefined,\n): number | undefined {\n if (current === undefined) return next\n if (next === undefined) return current\n return current + next\n}\n\nfunction sumNumberFields<T extends object>(\n current: T | undefined,\n next: T | undefined,\n): T | undefined {\n if (!current) return next\n if (!next) return current\n\n const result = { ...current }\n for (const key of Object.keys(next) as Array<keyof T>) {\n const currentValue = current[key]\n const nextValue = next[key]\n if (typeof nextValue === 'number') {\n result[key] = ((typeof currentValue === 'number' ? currentValue : 0) +\n nextValue) as T[keyof T]\n }\n }\n return result\n}\n\nfunction tokenUsageFromChunk(chunk: StreamChunk): TokenUsage | undefined {\n if (chunk.type !== 'RUN_FINISHED' && chunk.type !== 'RUN_ERROR') {\n return undefined\n }\n const usage = chunk.usage\n if (\n usage != null &&\n typeof usage === 'object' &&\n !Array.isArray(usage) &&\n 'promptTokens' in usage\n ) {\n return usage\n }\n const metadata = chunk.metadata\n const tanstack =\n metadata != null && typeof metadata === 'object' && 'tanstack' in metadata\n ? metadata.tanstack\n : undefined\n const leftover =\n tanstack != null && typeof tanstack === 'object' && !Array.isArray(tanstack)\n ? (tanstack as { usage?: TokenUsage }).usage\n : undefined\n return fromSpecTokenUsage(Array.isArray(usage) ? usage : undefined, leftover)\n}\n\nfunction accumulateTokenUsage(\n current: TokenUsage | undefined,\n next: TokenUsage,\n): TokenUsage {\n if (!current) return { ...next }\n\n const promptTokensDetails = sumNumberFields(\n current.promptTokensDetails,\n next.promptTokensDetails,\n )\n const completionTokensDetails = sumNumberFields(\n current.completionTokensDetails,\n next.completionTokensDetails,\n )\n const costDetails = sumNumberFields(current.costDetails, next.costDetails)\n // Provider-specific details are opaque, so retain the latest reported bag.\n const providerUsageDetails =\n next.providerUsageDetails ?? current.providerUsageDetails\n const durationSeconds = sumOptionalNumber(\n current.durationSeconds,\n next.durationSeconds,\n )\n const unitsBilled = sumOptionalNumber(current.unitsBilled, next.unitsBilled)\n const billed = accumulateBilled(current.billed, next.billed)\n const cost = sumOptionalNumber(current.cost, next.cost)\n\n return {\n ...current,\n ...next,\n promptTokens: current.promptTokens + next.promptTokens,\n completionTokens: current.completionTokens + next.completionTokens,\n totalTokens: current.totalTokens + next.totalTokens,\n ...(promptTokensDetails ? { promptTokensDetails } : {}),\n ...(completionTokensDetails ? { completionTokensDetails } : {}),\n ...(durationSeconds !== undefined ? { durationSeconds } : {}),\n ...(unitsBilled !== undefined ? { unitsBilled } : {}),\n ...(billed !== undefined ? { billed } : {}),\n ...(cost !== undefined ? { cost } : {}),\n ...(costDetails ? { costDetails } : {}),\n ...(providerUsageDetails ? { providerUsageDetails } : {}),\n }\n}\n\n/**\n * Sum billed quantities when both reports use the same unit. Different units\n * cannot be added, so the later report wins.\n */\nfunction accumulateBilled(\n current: BilledUsage | undefined,\n next: BilledUsage | undefined,\n): BilledUsage | undefined {\n if (!current) return next\n if (!next) return current\n if (current.unit !== next.unit) return next\n return { quantity: current.quantity + next.quantity, unit: current.unit }\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 usage?: TokenUsage,\n): Promise<void> {\n const runError = toRunErrorPayload(error)\n await runs?.update(runId, {\n status: 'failed',\n finishedAt: Date.now(),\n error: {\n message: runError.message,\n ...(runError.code !== undefined ? { code: runError.code } : {}),\n },\n ...(usage ? { usage } : {}),\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 usage?: TokenUsage,\n): Promise<void> {\n await runs?.update(runId, {\n status: 'interrupted',\n ...(usage ? { usage } : {}),\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 usage?: TokenUsage,\n): Promise<void> {\n await runs?.update(runId, {\n status: 'aborted',\n finishedAt: Date.now(),\n ...(usage ? { usage } : {}),\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 PersistenceCompletionCapability,\n ...(persistence.stores.metadata ? [MetadataCapability] : []),\n ...(wantsInterrupts ? [InterruptsCapability] : []),\n ]\n\n return defineChatMiddleware({\n name: 'chat-persistence',\n provides,\n setup(ctx: ChatMiddlewareContext) {\n providePersistence(ctx, persistence)\n if (persistence.stores.metadata) {\n provideMetadata(ctx, persistence.stores.metadata)\n }\n\n let resolveCompletion: () => void = () => undefined\n let rejectCompletion: (error: unknown) => void = () => undefined\n const completion = new Promise<void>((resolve, reject) => {\n resolveCompletion = resolve\n rejectCompletion = reject\n })\n // Consumers may not need this capability. Mark the rejection handled while\n // preserving the original promise for callers that do await it.\n void completion.catch(() => undefined)\n\n runState.set(ctx, {\n merged: false,\n interrupted: false,\n completion: {\n promise: completion,\n resolve: resolveCompletion,\n reject: rejectCompletion,\n },\n })\n providePersistenceCompletion(ctx, {\n waitForRunCompletion: () => completion,\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 only records that this chat owns. A foreign AG-UI interrupt can\n // share the durable thread, but its owner resolves it outside this\n // resume protocol. Including it would deadlock this chat resume.\n const ownedPending = pending.filter(isChatOwnedPendingInterrupt)\n rejectMixedRunPending(ownedPending, ctx)\n const resumeByInterruptId = validatePendingResumes(\n ownedPending,\n config.resume,\n ctx,\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 ownedPending,\n resumeByInterruptId,\n )\n const genericResumeState = await durableGenericResumeState(\n ctx,\n ownedPending,\n config.resume ?? [],\n config.tools,\n )\n patch.resume = []\n if (resumeToolState || genericResumeState) {\n patch.resumeToolState = mergeResumeToolState(\n resumeToolState,\n genericResumeState,\n )\n }\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 && ownedPending.length > 0) {\n state.pendingResumes = { pending: ownedPending, resumeByInterruptId }\n }\n }\n\n const storedUsage = await createOrResumeRun(runs, ctx.runId, ctx.threadId)\n\n const state = runState.get(ctx)\n // A continuation has a fresh middleware context but resumes the same run.\n if (state && storedUsage) state.usage = storedUsage\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 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 // Capture the current assistant turn's identity for optional in-progress\n // snapshots. Completed messages already live in `ctx.messages`.\n if (snapshotStreaming && ctx.phase === 'modelStream') {\n const s = runState.get(ctx)\n if (s && chunk.type === 'TEXT_MESSAGE_START') {\n // An empty/malformed messageId means \"no identity\" (matching the\n // engine's convention), leaving room for the TOOL_CALL_START\n // parentMessageId fallback below — but the per-turn accumulator\n // still resets so snapshots never mix text across turns.\n s.streamingMessageId =\n typeof chunk.messageId === 'string' && chunk.messageId !== ''\n ? chunk.messageId\n : undefined\n s.streamingMessageCreatedAt = new Date()\n s.streamingText = ''\n } else if (\n s &&\n chunk.type === 'TOOL_CALL_START' &&\n typeof chunk.parentMessageId === 'string' &&\n chunk.parentMessageId !== '' &&\n s.streamingMessageId === undefined\n ) {\n s.streamingMessageId = chunk.parentMessageId\n s.streamingMessageCreatedAt ??= new Date()\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. The completed turn enters `ctx.messages`\n // only after streaming ends, so accumulate its text here and 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 ...(snapshotState.streamingMessageCreatedAt\n ? { createdAt: snapshotState.streamingMessageCreatedAt }\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 // Adapter terminals arrive before `onUsage`; synthesized tool boundaries\n // arrive after it with the same usage already in state.\n const chunkUsage = tokenUsageFromChunk(chunk)\n const usage =\n ctx.phase === 'modelStream' && chunkUsage\n ? accumulateTokenUsage(state.usage, chunkUsage)\n : (state.usage ?? chunkUsage)\n state.usage = usage\n await interruptRun(runs, ctx.runId, usage)\n await messageStore.saveThread(ctx.threadId, [...ctx.messages])\n state.interrupted = true\n },\n\n onUsage(ctx: ChatMiddlewareContext, usage: TokenUsage) {\n const state = runState.get(ctx)\n if (!state || state.interrupted) return\n state.usage = accumulateTokenUsage(state.usage, usage)\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 try {\n await messageStore.saveThread(ctx.threadId, [...ctx.messages])\n await commitPendingResumes(state, persistence.stores.interrupts)\n await completeRun(runs, ctx.runId, state?.usage ?? info.usage)\n state?.completion?.resolve()\n } catch (error) {\n // Core has already selected its terminal hook. Persist the failed run\n // here, so a failed transcript save or batch write does not leave an\n // interrupted or completed run whose pending records need retrying.\n try {\n await failRun(runs, ctx.runId, error, state?.usage)\n } finally {\n state?.completion?.reject(error)\n }\n throw error\n }\n },\n\n async onError(ctx: ChatMiddlewareContext, info: ErrorInfo) {\n try {\n await failRun(runs, ctx.runId, info.error, runState.get(ctx)?.usage)\n } finally {\n runState.get(ctx)?.completion?.reject(info.error)\n }\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 // 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 let terminal = false\n try {\n // The durable cancel read is best-effort. It must not bypass the\n // terminal persistence path or prevent the completion promise from\n // settling when the run store is unavailable.\n const cancelled =\n info.cancelRequested === true ||\n (runs !== undefined && (await wasCancelRequested(runs, ctx.runId)))\n terminal =\n cancelled || (!detachableRun(ctx) && state?.interrupted !== true)\n if (terminal) {\n await abortRun(runs, ctx.runId, state?.usage)\n }\n } finally {\n if (terminal) state?.completion?.reject(info.reason)\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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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ModelMessage, PersistedArtifactRef, RunStatus, RunStore, Scope, TokenUsage } from '@tanstack/ai';
|
|
2
|
-
export type { Scope };
|
|
1
|
+
import { ModelMessage, MetadataStore, PersistedArtifactRef, RunStatus, RunStore, Scope, TokenUsage } from '@tanstack/ai';
|
|
2
|
+
export type { MetadataStore, Scope };
|
|
3
3
|
/**
|
|
4
4
|
* Durable store for a thread's full message transcript.
|
|
5
5
|
*
|
|
@@ -211,36 +211,6 @@ export interface InterruptStore {
|
|
|
211
211
|
/** Pending interrupts for a run, ordered by `requestedAt` ascending. */
|
|
212
212
|
listPendingByRun: (runId: string) => Promise<Array<InterruptRecord>>;
|
|
213
213
|
}
|
|
214
|
-
/**
|
|
215
|
-
* Namespaced key/value store for arbitrary JSON metadata (app-owned).
|
|
216
|
-
*
|
|
217
|
-
* The first argument is an **app-defined namespace string**, not the shared
|
|
218
|
-
* {@link Scope} identity type from `@tanstack/ai`. Composite identity is
|
|
219
|
-
* `(namespace, key)` as two independent fields (SQL backends use a composite
|
|
220
|
-
* primary key; the in-memory store uses nested maps). Do not encode both into a
|
|
221
|
-
* single delimited string — `${namespace}:${key}` collides when either part
|
|
222
|
-
* contains `:`.
|
|
223
|
-
*
|
|
224
|
-
* The same `key` under different namespaces is independent.
|
|
225
|
-
*/
|
|
226
|
-
export interface MetadataStore {
|
|
227
|
-
/**
|
|
228
|
-
* Return the stored value for `(namespace, key)`, or `null` if absent.
|
|
229
|
-
*
|
|
230
|
-
* CAVEAT: the return type is `unknown | null`, where `| null` collapses into
|
|
231
|
-
* `unknown` — a stored value of `null` is therefore **indistinguishable from
|
|
232
|
-
* absence** at the type level. Callers that must persist a real `null`
|
|
233
|
-
* distinctly from "not set" should wrap it (e.g. store `{ value: null }`).
|
|
234
|
-
*/
|
|
235
|
-
get: (namespace: string, key: string) => Promise<unknown | null>;
|
|
236
|
-
/** Insert or overwrite the value for `(namespace, key)`. */
|
|
237
|
-
set: (namespace: string, key: string, value: unknown) => Promise<void>;
|
|
238
|
-
/**
|
|
239
|
-
* Remove `(namespace, key)`. A no-op if absent. Does not affect other
|
|
240
|
-
* namespaces.
|
|
241
|
-
*/
|
|
242
|
-
delete: (namespace: string, key: string) => Promise<void>;
|
|
243
|
-
}
|
|
244
214
|
/** Type a {@link MessageStore} implementation inline. */
|
|
245
215
|
export declare function defineMessageStore(store: MessageStore): MessageStore;
|
|
246
216
|
/** Type an {@link InterruptStore} implementation inline. */
|
package/dist/esm/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","names":[],"sources":["../../src/types.ts"],"sourcesContent":["import type {\n ModelMessage,\n PersistedArtifactRef,\n RunStatus,\n RunStore,\n Scope,\n TokenUsage,\n} from '@tanstack/ai'\n\n// Re-export the shared identity type so app code can import Scope from either\n// `@tanstack/ai` or `@tanstack/ai-persistence`. See {@link Scope} security notes:\n// pair a client-visible `threadId` with a server-trusted `userId`/`tenantId`\n// before authorizing load/save (e.g. via `reconstructChat({ authorize })`).\nexport type { Scope }\n\n// ===========================================================================\n// Store contracts\n// ===========================================================================\n//\n// EVOLUTION POLICY\n// ----------------\n// These store interfaces are the compatibility surface between the core\n// middleware and every backend — the in-memory reference store and every\n// adapter an application writes against its own database.\n//\n// - Store METHODS are REQUIRED. A new method is a breaking contract change:\n// every adapter gets a compile error and implements it. Do NOT add methods\n// as optional-and-feature-detected (`store.method?.(...)`) — an adapter\n// that has not implemented one is then indistinguishable from one whose\n// answer is legitimately empty, so the feature silently does nothing in\n// production instead of failing at build time. `findActiveRun` was optional\n// for exactly one release cycle and cost us precisely that: reconnect\n// degraded to \"no active run\" on every backend that had not caught up.\n// - Capability tiers belong at the STORE level, not the method level. A\n// backend that only stores a transcript declares `ChatTranscriptStores`\n// (no `runs`); it does not declare a half-implemented `RunStore`.\n// - Never tighten an existing method's required arguments or widen its\n// required return shape in a breaking way.\n//\n// The shared conformance testkit (`./testkit/conformance.ts`) is the\n// authoritative compatibility gate: every invariant documented on the methods\n// below is asserted there, and every backend runs the identical suite. If an\n// invariant is not encoded in the testkit, adapters cannot discover it — so\n// promote new invariants into both the JSDoc here AND the testkit.\n//\n// TIMESTAMP CONVENTION\n// --------------------\n// Store *records* (`RunRecord`, `InterruptRecord`, `ArtifactRecord`,\n// `BlobRecord`) speak **epoch milliseconds** (`number`), the native unit for\n// SQL/`BIGINT` columns and `Date.now()`. Wire/result references that leave the\n// persistence layer (e.g. core's `PersistedArtifactRef.createdAt`) speak\n// **ISO-8601 strings**. The middleware performs the number→ISO conversion at\n// the boundary; do not mix the two on a single field.\n\n/**\n * Durable store for a thread's full message transcript.\n *\n * A \"thread\" is the unit of conversation history. The key is\n * {@link Scope.threadId} (the same conversation id as\n * `ChatMiddlewareContext.threadId`). Store methods take a bare string for\n * adapter simplicity; multi-user isolation is the **host's** job — authorize\n * against `Scope.userId` / `Scope.tenantId` (derived server-side from session)\n * before calling load/save, and never treat a client-supplied thread id alone\n * as an ownership proof (see `Scope` security notes in `@tanstack/ai`).\n *\n * `saveThread` always receives and persists the **complete, authoritative**\n * message list — it is an overwrite, never an append. The middleware snapshots\n * `ctx.messages` (the full running transcript) into it.\n */\nexport interface MessageStore {\n /**\n * Return the full stored transcript for `threadId` ({@link Scope.threadId}),\n * in insertion order.\n *\n * INVARIANT: returns an empty array (never `null`/`undefined`) for a thread\n * that was never saved. Callers treat `[]` as \"no history\".\n */\n loadThread: (threadId: string) => Promise<Array<ModelMessage>>\n /**\n * Overwrite the stored transcript for `threadId` with `messages`.\n *\n * INVARIANT: this is a full replace. `messages` is the complete authoritative\n * history; the previous contents are discarded (not merged or appended).\n */\n saveThread: (threadId: string, messages: Array<ModelMessage>) => Promise<void>\n}\n\n// Run lifecycle types live in `@tanstack/ai` and are re-exported here: one run,\n// one record — shared by this package's `runs` store and `@tanstack/ai-sandbox`'s\n// run driver, instead of each package keeping a rival definition that can drift.\nexport type {\n RunStatus,\n TerminalRunStatus,\n RunRecord,\n RunStore,\n} from '@tanstack/ai'\nexport { isTerminalRunStatus, defineRunStore } from '@tanstack/ai'\n\n/**\n * Lifecycle status of a generation run. Deliberately the same vocabulary as\n * {@link RunStatus}, so an adapter that stores both kinds of run can share one\n * status column and one set of checks.\n */\nexport type GenerationRunStatus = RunStatus\n\n/**\n * A single generation run (one `generateImage` / `generateVideo` / … call).\n *\n * Its primary identity is `runId`: the run/request id the activity mints, the\n * same AG-UI run id the client sends on the wire. `threadId` is the SLOT the\n * run fills, a stable app-chosen name that groups successive runs of the same\n * thing, and it is what a server-driven client hydrates by. Generation state is\n * kept here, never in the chat {@link RunStore}.\n *\n * `result` holds terminal result METADATA (ids, model, urls, a provider video\n * job id), never the media bytes — those live in a {@link BlobStore}.\n * `artifacts` are the durable {@link PersistedArtifactRef}s, present only when\n * byte storage is on.\n *\n * @property startedAt - Epoch ms when the run was first created.\n * @property finishedAt - Epoch ms when the run reached a terminal status.\n */\nexport interface GenerationRunRecord {\n runId: string\n /**\n * The scope this run belongs to: a stable, app-chosen name for the slot\n * successive runs fill (`product-123-hero`, `video-9-start-frame`).\n *\n * REQUIRED, per the store-contract rule at the top of this file.\n * {@link GenerationRunStore.findLatestForThread} is the only query that\n * hydrates a run, and it keys on this — so a record without one can be\n * written and then never found again. `withGenerationPersistence` already\n * refuses to start a run without a scope, and a server-driven client\n * discards a snapshot that arrives without one, so an optional field here\n * only described a record no path could produce and no client would accept.\n */\n threadId: string\n /** `'image' | 'audio' | 'tts' | 'video' | 'transcription'`. */\n activity: string\n provider: string\n model: string\n status: GenerationRunStatus\n startedAt: number\n finishedAt?: number\n error?: { message: string; code?: string }\n /** Terminal result metadata (ids, model, urls). Never the media bytes. */\n result?: unknown\n /** Durable artifact references, when an artifacts + blobs backend is used. */\n artifacts?: Array<PersistedArtifactRef>\n usage?: TokenUsage\n}\n\n/**\n * Durable store for generation run records, the generation counterpart to\n * {@link RunStore}. Keyed by its own `runId`, with `threadId` the slot\n * {@link GenerationRunStore.findLatestForThread} looks runs up by.\n */\nexport interface GenerationRunStore {\n /**\n * Create a run record, or return the existing one if `runId` is already\n * present (resume).\n *\n * INVARIANT (idempotency): a second call for a `runId` returns the existing\n * record unchanged; `startedAt`/`activity`/`provider`/`model`/`threadId` are\n * not mutated. `status` defaults to `'running'` on first creation.\n */\n createOrResume: (\n input: Pick<\n GenerationRunRecord,\n 'runId' | 'threadId' | 'activity' | 'provider' | 'model' | 'startedAt'\n > & { status?: GenerationRunStatus },\n ) => Promise<GenerationRunRecord>\n /**\n * Patch a run record's mutable fields.\n *\n * INVARIANT: patching a `runId` that does not exist is a **no-op** — it must\n * not throw and must not create a record.\n */\n update: (\n runId: string,\n patch: Partial<\n Pick<\n GenerationRunRecord,\n 'status' | 'finishedAt' | 'error' | 'result' | 'artifacts' | 'usage'\n >\n >,\n ) => Promise<void>\n /** Return the run record for `runId`, or `null` if none exists. */\n get: (runId: string) => Promise<GenerationRunRecord | null>\n /**\n * The most recent run linked to `threadId`, or `null`.\n *\n * REQUIRED, per the store-contract rule at the top of this file: a\n * server-authoritative client hydrates by the stable thread id on every\n * mount, so an adapter without this would be indistinguishable from one that\n * legitimately has no run — `persistence: true` would silently restore\n * nothing, forever. `null` is the correct answer only when the thread really\n * has no runs. The chat parallel is {@link RunStore.findActiveRun}.\n */\n findLatestForThread: (threadId: string) => Promise<GenerationRunRecord | null>\n}\n\n/** Lifecycle status of a human-in-the-loop interrupt. */\nexport type InterruptStatus = 'pending' | 'resolved' | 'cancelled'\n\n/**\n * A human-in-the-loop interrupt (tool approval, client-tool input request, …).\n *\n * @property requestedAt - Epoch ms when the interrupt was created.\n * @property resolvedAt - Epoch ms when the interrupt was resolved/cancelled;\n * absent while pending.\n */\nexport interface InterruptRecord {\n interruptId: string\n runId: string\n threadId: string\n status: InterruptStatus\n requestedAt: number\n resolvedAt?: number\n payload: Record<string, unknown>\n response?: unknown\n}\n\n/** A terminal interrupt write for {@link InterruptStore.commitBatch}. */\nexport type InterruptCommitEntry =\n | {\n interruptId: string\n status: 'resolved'\n response?: unknown\n }\n | {\n interruptId: string\n status: 'cancelled'\n }\n\n/** Durable store for human-in-the-loop interrupts. */\nexport interface InterruptStore {\n /**\n * Persist a new interrupt in the `'pending'` state.\n *\n * The record is accepted without `status`/`resolvedAt` so a \"born resolved\"\n * interrupt is unrepresentable — every interrupt begins pending and only\n * `resolve`/`cancel` may move it to a terminal state.\n *\n * INVARIANT (insert-if-absent): if an interrupt with the same `interruptId`\n * already exists, `create` is a **no-op** — it must NOT overwrite the\n * existing record. This is the canonical behaviour (SQL backends implement it\n * via `ON CONFLICT DO NOTHING` / upsert-with-empty-update), so a duplicate\n * create can never clobber a resolved interrupt back to pending.\n */\n create: (\n record: Omit<InterruptRecord, 'status' | 'resolvedAt'>,\n ) => Promise<void>\n /**\n * Move an interrupt to `'resolved'`, stamping `resolvedAt` and storing\n * `response`. A no-op if `interruptId` does not exist.\n */\n resolve: (interruptId: string, response?: unknown) => Promise<void>\n /**\n * Move an interrupt to `'cancelled'`, stamping `resolvedAt`. A no-op if\n * `interruptId` does not exist.\n */\n cancel: (interruptId: string) => Promise<void>\n /**\n * Commit terminal writes for a validated resume batch.\n *\n * Optional. When present, `withPersistence` calls it once instead of\n * calling `resolve` and `cancel` for each entry. Apply every entry or none.\n *\n * Reject the whole batch (throw, writing nothing) when any entry has a\n * duplicate `interruptId`, references an `interruptId` that does not exist,\n * or references an interrupt whose status is not `'pending'`. This is\n * stricter than `resolve` / `cancel`, which are no-ops for a missing\n * `interruptId`.\n */\n commitBatch?: (entries: ReadonlyArray<InterruptCommitEntry>) => Promise<void>\n /** Return the interrupt for `interruptId`, or `null` if none exists. */\n get: (interruptId: string) => Promise<InterruptRecord | null>\n /**\n * All interrupts for a thread.\n *\n * INVARIANT: ordered by insertion (equivalently `requestedAt` ascending). SQL\n * backends MUST `ORDER BY requested_at` — the middleware and testkit rely on\n * this stable ordering.\n */\n list: (threadId: string) => Promise<Array<InterruptRecord>>\n /** Pending interrupts for a thread, ordered by `requestedAt` ascending. */\n listPending: (threadId: string) => Promise<Array<InterruptRecord>>\n /** All interrupts for a run, ordered by `requestedAt` ascending. */\n listByRun: (runId: string) => Promise<Array<InterruptRecord>>\n /** Pending interrupts for a run, ordered by `requestedAt` ascending. */\n listPendingByRun: (runId: string) => Promise<Array<InterruptRecord>>\n}\n\n/**\n * Namespaced key/value store for arbitrary JSON metadata (app-owned).\n *\n * The first argument is an **app-defined namespace string**, not the shared\n * {@link Scope} identity type from `@tanstack/ai`. Composite identity is\n * `(namespace, key)` as two independent fields (SQL backends use a composite\n * primary key; the in-memory store uses nested maps). Do not encode both into a\n * single delimited string — `${namespace}:${key}` collides when either part\n * contains `:`.\n *\n * The same `key` under different namespaces is independent.\n */\nexport interface MetadataStore {\n /**\n * Return the stored value for `(namespace, key)`, or `null` if absent.\n *\n * CAVEAT: the return type is `unknown | null`, where `| null` collapses into\n * `unknown` — a stored value of `null` is therefore **indistinguishable from\n * absence** at the type level. Callers that must persist a real `null`\n * distinctly from \"not set\" should wrap it (e.g. store `{ value: null }`).\n */\n get: (namespace: string, key: string) => Promise<unknown | null>\n /** Insert or overwrite the value for `(namespace, key)`. */\n set: (namespace: string, key: string, value: unknown) => Promise<void>\n /**\n * Remove `(namespace, key)`. A no-op if absent. Does not affect other\n * namespaces.\n */\n delete: (namespace: string, key: string) => Promise<void>\n}\n\n// ===========================================================================\n// Store typers\n// ===========================================================================\n//\n// Identity helpers that type a store implementation inline: pass an object\n// literal and get autocomplete + contract checking, with no separate\n// `: MessageStore` return annotation. They compose into `defineAIPersistence`,\n// which infers **exact presence** — a store you define becomes a defined,\n// non-optional, autocompleted key on `persistence.stores`, and accessing a store\n// you did not define is a compile error.\n//\n// ```ts\n// const persistence = defineAIPersistence({\n// stores: {\n// messages: defineMessageStore({ loadThread, saveThread }),\n// runs: defineRunStore({ createOrResume, update, get, findActiveRun }),\n// },\n// })\n// persistence.stores.runs // RunStore (defined)\n// persistence.stores.interrupts // compile error — not provided\n// ```\n//\n// Presence is per STORE, not per method: every method of a store you define is\n// required (see the evolution policy above). Omitting one is a compile error,\n// not a partial store.\n\n/** Type a {@link MessageStore} implementation inline. */\nexport function defineMessageStore(store: MessageStore): MessageStore {\n return store\n}\n/** Type an {@link InterruptStore} implementation inline. */\nexport function defineInterruptStore(store: InterruptStore): InterruptStore {\n return store\n}\n/** Type a {@link MetadataStore} implementation inline. */\nexport function defineMetadataStore(store: MetadataStore): MetadataStore {\n return store\n}\n/** Type a {@link GenerationRunStore} implementation inline. */\nexport function defineGenerationRunStore(\n store: GenerationRunStore,\n): GenerationRunStore {\n return store\n}\n/** Type an {@link ArtifactStore} implementation inline. */\nexport function defineArtifactStore(store: ArtifactStore): ArtifactStore {\n return store\n}\n/** Type a {@link BlobStore} implementation inline. */\nexport function defineBlobStore(store: BlobStore): BlobStore {\n return store\n}\n\n/**\n * Metadata row describing a persisted artifact (generated media, tool output).\n *\n * The bytes themselves live in a {@link BlobStore}; this record holds the\n * descriptive metadata and an optional `sourceUrl` for reference-only\n * backends.\n *\n * @property createdAt - Epoch ms. (Core's wire-facing `PersistedArtifactRef`\n * exposes the same instant as an ISO string; see the timestamp convention.)\n */\nexport interface ArtifactRecord {\n artifactId: string\n runId: string\n threadId: string\n /**\n * The blob-store key these bytes actually live under.\n *\n * Optional for backwards compatibility: records written before this existed\n * resolve via the default `artifacts/<runId>/<artifactId>` convention. New\n * records always carry it, which is what lets `storageKey` put bytes anywhere\n * — a reader can no longer recompute the path, so it has to be remembered.\n * Use `resolveArtifactBlobKey(record)` rather than reading it directly.\n */\n blobKey?: string\n name: string\n mimeType: string\n size: number\n sourceUrl?: string\n createdAt: number\n}\n\n/** Durable store for artifact metadata records. */\nexport interface ArtifactStore {\n /** Insert or overwrite the artifact metadata record. */\n save: (record: ArtifactRecord) => Promise<void>\n /** Return the artifact for `artifactId`, or `null` if none exists. */\n get: (artifactId: string) => Promise<ArtifactRecord | null>\n /**\n * All artifacts for a run in deterministic snapshot order: `createdAt`\n * ascending, then `artifactId` ascending by the unsigned UTF-8 bytes of\n * each string (compare bytes left-to-right; shorter equal prefixes first).\n * Returns `[]` when the run has none.\n */\n list: (runId: string) => Promise<Array<ArtifactRecord>>\n /**\n * All artifacts for a thread in deterministic snapshot order.\n * Records are ordered by `createdAt` ascending, then by `artifactId` using\n * the unsigned UTF-8 bytes of each string (compare bytes left-to-right; shorter\n * equal prefixes first).\n */\n listForThread: (threadId: string) => Promise<Array<ArtifactRecord>>\n /**\n * Delete a single artifact by id. A no-op if absent, mirroring\n * {@link BlobStore.delete} — the two are written and deleted as a pair, so\n * their contracts match.\n */\n delete: (artifactId: string) => Promise<void>\n /**\n * Delete every artifact belonging to `runId`. A no-op when the run has none.\n *\n * Required rather than feature-detected: retention and erasure are the point\n * of storing media durably, and an adapter silently lacking deletion is\n * indistinguishable from one where there was nothing to delete.\n */\n deleteForRun: (runId: string) => Promise<void>\n}\n\n/**\n * Accepted body shapes for {@link BlobStore.put}. `ArrayBufferView` already\n * covers `Uint8Array` and every other typed-array/`DataView`, so no separate\n * `Uint8Array` member is needed.\n */\nexport type BlobBody =\n | ReadableStream<Uint8Array>\n | ArrayBuffer\n | ArrayBufferView\n | string\n | Blob\n\n/**\n * Metadata for a stored blob.\n *\n * @property size - Byte length, when known.\n * @property createdAt - Epoch ms first written.\n * @property updatedAt - Epoch ms last overwritten.\n */\nexport interface BlobRecord {\n key: string\n size?: number\n etag?: string\n contentType?: string\n customMetadata?: Record<string, string>\n createdAt?: number\n updatedAt?: number\n}\n\n/**\n * A byte range to read, in the shape an HTTP `Range` header resolves to.\n *\n * `offset` is measured from the start of the object and must be inside it;\n * `length` defaults to \"everything from `offset` to the end\" and is clamped to\n * the end when it overshoots. Suffix ranges (`bytes=-500`) are the caller's to\n * resolve against the known size — a serve route has the size on the artifact\n * record, and has to compare against it anyway to answer `416` before reading.\n */\nexport interface BlobRange {\n offset: number\n length?: number\n}\n\n/** Options for {@link BlobStore.get}. */\nexport interface BlobGetOptions {\n /**\n * Read only this slice of the object. `body`, `arrayBuffer()` and `text()`\n * then cover the slice, `size` still reports the WHOLE object, and `range`\n * reports the slice actually served — the three numbers a `206` response\n * needs (`Content-Range: bytes <offset>-<offset+length-1>/<size>`).\n */\n range?: BlobRange\n}\n\n/** A stored blob's metadata plus lazy accessors for its bytes. */\nexport interface BlobObject extends BlobRecord {\n arrayBuffer: () => Promise<ArrayBuffer>\n text: () => Promise<string>\n body?: ReadableStream<Uint8Array>\n /**\n * The slice this object exposes, when a {@link BlobGetOptions.range} was\n * requested and honoured: `offset` as asked, `length` as actually served\n * (clamped to the end of the object). Absent on a whole-object read.\n */\n range?: { offset: number; length: number }\n}\n\n/**\n * One page of a {@link BlobStore.list} scan.\n *\n * @property cursor - Opaque continuation token; present only when `truncated`.\n * @property truncated - `true` when more objects match beyond this page.\n */\nexport interface BlobListPage {\n objects: Array<BlobRecord>\n cursor?: string\n truncated?: boolean\n}\n\nexport interface BlobPutOptions {\n contentType?: string\n customMetadata?: Record<string, string>\n /**\n * The exact byte length of `body`, when the producer knows it up front.\n *\n * Advisory, not a contract the store must honor: it exists so a store can\n * pick an upload strategy knowingly instead of discovering the length by\n * buffering. Most useful to an SDK that wants the length as a separate\n * argument rather than reading it off the stream — S3's `PutObject`\n * (`ContentLength`) is the archetype — and to a runtime that can re-attach\n * one (workerd's `FixedLengthStream` ahead of `R2Bucket.put`).\n *\n * Only ever set when the length is exact — a wrong value is worse than none,\n * since runtimes that enforce declared lengths fail the write. Absent means\n * unknown, and a store must accept a length-less stream regardless:\n * producers hand one over whenever the origin does not declare a length.\n */\n expectedLength?: number\n}\n\nexport interface BlobListOptions {\n prefix?: string\n cursor?: string\n limit?: number\n}\n\n/** Durable object/blob store (byte-storing or reference-only backends). */\nexport interface BlobStore {\n /** Insert or overwrite the object at `key`, returning its metadata. */\n put: (\n key: string,\n body: BlobBody,\n options?: BlobPutOptions,\n ) => Promise<BlobRecord>\n /**\n * Return the object at `key` (metadata + byte accessors), or `null`.\n *\n * RANGE SEMANTICS: with `options.range`, return only that slice — the bytes\n * a `206` response carries — and report it back as `range`. `size` still\n * reports the whole object, so the caller can build `Content-Range` without\n * a second `head`. The reported `length` is what was actually served: a\n * requested `length` past the end clamps. An `offset` at or past the end is\n * a caller error, not a store one — the size is on the artifact record, so a\n * serve route answers `416` before ever asking the store.\n *\n * Range support is part of the contract for any store that holds bytes (the\n * conformance testkit asserts it): serving a whole file where a slice was\n * asked for is what makes `<video>` seeking, and Safari playback at all,\n * fail. A reference-only backend that stores no bytes skips `blobs`\n * entirely rather than half-implementing it.\n */\n get: (key: string, options?: BlobGetOptions) => Promise<BlobObject | null>\n /** Return only the metadata for `key`, or `null`. */\n head: (key: string) => Promise<BlobRecord | null>\n /** Remove the object at `key`. A no-op if absent. */\n delete: (key: string) => Promise<void>\n /**\n * List objects, optionally filtered by `prefix`, in ascending key order.\n *\n * CURSOR SEMANTICS: `prefix` matches literally and case-sensitively (SQL\n * backends must escape LIKE metacharacters, so `run_` matches only the exact\n * bytes `run_`, not `_` as a wildcard). When `limit` is given and more keys\n * match, the page is `truncated: true` with a `cursor`; passing that `cursor`\n * back returns the strictly-following keys (keys `> cursor`). Cursor ordering\n * is the same byte ordering as the sort, so paging visits every key exactly\n * once with no gaps or repeats. `limit: 0` yields an empty, untruncated page\n * with no cursor.\n */\n list: (options?: BlobListOptions) => Promise<BlobListPage>\n}\n\n/**\n * Sparse bag of **state** store keys — composition / validation only.\n *\n * **Not a public product shape.** Prefer the named chat shapes below\n * ({@link ChatTranscriptStores}, {@link ChatPersistenceStores},\n * {@link ChatWithInterruptsStores}). Locks are not included — use\n * `withLocks` from `@tanstack/ai`.\n *\n * @internal Exported from this module for generics; the package root does not\n * re-export this type — use a named shape or `AIPersistence<{ … }>` instead.\n */\nexport interface AIPersistenceStores {\n messages?: MessageStore\n runs?: RunStore\n interrupts?: InterruptStore\n metadata?: MetadataStore\n generationRuns?: GenerationRunStore\n artifacts?: ArtifactStore\n blobs?: BlobStore\n}\n\n/**\n * Chat floor: durable transcript. `messages` is required.\n *\n * `runs` / `interrupts` / `metadata` remain optional. If `interrupts` is set,\n * `runs` is required (enforced by `withPersistence` / validators).\n */\nexport interface ChatTranscriptStores {\n messages: MessageStore\n runs?: RunStore\n interrupts?: InterruptStore\n metadata?: MetadataStore\n}\n\n/**\n * Full chat durability — all four state stores are present. This is what\n * `memoryPersistence()` returns, and the shape most adapters should declare.\n *\n * Backends that only need a transcript should use\n * {@link ChatTranscriptStores} instead.\n */\nexport interface ChatPersistenceStores {\n messages: MessageStore\n runs: RunStore\n interrupts: InterruptStore\n metadata: MetadataStore\n}\n\n/**\n * Chat with durable human-in-the-loop interrupts (and optional metadata).\n * Implies `runs` (interrupt records are run-scoped).\n *\n * Prefer {@link ChatPersistenceStores} when you also have metadata (packaged\n * backends). Use this when interrupts are required but metadata is not.\n */\nexport interface ChatWithInterruptsStores {\n messages: MessageStore\n runs: RunStore\n interrupts: InterruptStore\n metadata?: MetadataStore\n}\n\n/**\n * Persistence aggregate. Parameterize with a named store shape, or a sparse\n * map for composition (`defineAIPersistence` / `composePersistence`).\n *\n * Default is the sparse bag so untyped / dynamic bags still type-check;\n * prefer {@link ChatTranscriptPersistence} or {@link ChatPersistence} at\n * call sites.\n */\nexport interface AIPersistence<\n TStores extends AIPersistenceStores = AIPersistenceStores,\n> {\n stores: ExactStoreKeys<TStores>\n}\n\n/** {@link AIPersistence} for {@link ChatTranscriptStores}. */\nexport type ChatTranscriptPersistence = AIPersistence<ChatTranscriptStores>\n\n/** {@link AIPersistence} for {@link ChatPersistenceStores}. */\nexport type ChatPersistence = AIPersistence<ChatPersistenceStores>\n\n/** {@link AIPersistence} for {@link ChatWithInterruptsStores}. */\nexport type ChatWithInterruptsPersistence =\n AIPersistence<ChatWithInterruptsStores>\n\ntype StoreKey = keyof AIPersistenceStores\ntype ExactStoreKeys<TStores> =\n Exclude<keyof TStores, StoreKey> extends never\n ? TStores\n : TStores & Record<Exclude<keyof TStores, StoreKey>, never>\n\nexport type AIPersistenceOverrides = {\n [TKey in StoreKey]?: AIPersistenceStores[TKey] | false\n}\n\ntype BaseStoreValue<\n TBase extends AIPersistenceStores,\n TKey extends StoreKey,\n> = TKey extends keyof TBase ? TBase[TKey] : never\n\ntype OverrideStoreValue<\n TOverrides extends AIPersistenceOverrides,\n TKey extends StoreKey,\n> = TKey extends keyof TOverrides ? TOverrides[TKey] : never\n\ntype ResolvedStoreValue<\n TBase extends AIPersistenceStores,\n TOverrides extends AIPersistenceOverrides,\n TKey extends StoreKey,\n> = TKey extends keyof TOverrides\n ?\n | Exclude<OverrideStoreValue<TOverrides, TKey>, false | undefined>\n | (undefined extends OverrideStoreValue<TOverrides, TKey>\n ? Exclude<BaseStoreValue<TBase, TKey>, undefined>\n : never)\n : Exclude<BaseStoreValue<TBase, TKey>, undefined>\n\ntype BaseStoreIsRequired<\n TBase extends AIPersistenceStores,\n TKey extends StoreKey,\n> = TKey extends keyof TBase\n ? object extends Pick<TBase, TKey>\n ? false\n : true\n : false\n\ntype ResolvedStoreIsRequired<\n TBase extends AIPersistenceStores,\n TOverrides extends AIPersistenceOverrides,\n TKey extends StoreKey,\n> = TKey extends keyof TOverrides\n ? false extends OverrideStoreValue<TOverrides, TKey>\n ? false\n : undefined extends OverrideStoreValue<TOverrides, TKey>\n ? BaseStoreIsRequired<TBase, TKey>\n : true\n : BaseStoreIsRequired<TBase, TKey>\n\ntype ResolvedRequiredKeys<\n TBase extends AIPersistenceStores,\n TOverrides extends AIPersistenceOverrides,\n> = {\n [TKey in StoreKey]-?: [ResolvedStoreValue<TBase, TOverrides, TKey>] extends [\n never,\n ]\n ? never\n : ResolvedStoreIsRequired<TBase, TOverrides, TKey> extends true\n ? TKey\n : never\n}[StoreKey]\n\ntype ResolvedOptionalKeys<\n TBase extends AIPersistenceStores,\n TOverrides extends AIPersistenceOverrides,\n> = {\n [TKey in StoreKey]-?: [ResolvedStoreValue<TBase, TOverrides, TKey>] extends [\n never,\n ]\n ? never\n : ResolvedStoreIsRequired<TBase, TOverrides, TKey> extends true\n ? never\n : TKey\n}[StoreKey]\n\ntype Simplify<T> = { [TKey in keyof T]: T[TKey] }\n\nexport type ComposedAIPersistenceStores<\n TBase extends AIPersistenceStores,\n TOverrides extends AIPersistenceOverrides,\n> = Simplify<\n {\n [TKey in ResolvedRequiredKeys<TBase, TOverrides>]: ResolvedStoreValue<\n TBase,\n TOverrides,\n TKey\n >\n } & {\n [TKey in ResolvedOptionalKeys<TBase, TOverrides>]?: ResolvedStoreValue<\n TBase,\n TOverrides,\n TKey\n >\n }\n>\n\nconst storeKeys = [\n 'messages',\n 'runs',\n 'generationRuns',\n 'interrupts',\n 'metadata',\n 'artifacts',\n 'blobs',\n] satisfies Array<StoreKey>\n\nconst storeKeySet = new Set<string>(storeKeys)\n\nfunction assertKnownStoreKeys(stores: object, location: string): void {\n for (const key of Object.keys(stores)) {\n if (!storeKeySet.has(key)) {\n throw new Error(`Unknown AIPersistence ${location} key: ${key}`)\n }\n }\n}\n\nexport function validatePersistenceStoreKeys(persistence: AIPersistence): void {\n assertKnownStoreKeys(persistence.stores, 'store')\n}\n\n/**\n * Chat middleware entrypoint rules:\n * - `messages` is required (chat persistence means a durable transcript)\n * - `interrupts` requires `runs` (interrupt records are run-scoped)\n */\nexport function validateChatPersistenceStores(\n persistence: AIPersistence,\n): void {\n validatePersistenceStoreKeys(persistence)\n if (!persistence.stores.messages) {\n throw new Error('Chat persistence requires stores.messages.')\n }\n if (persistence.stores.interrupts && !persistence.stores.runs) {\n throw new Error('Chat persistence stores.interrupts requires stores.runs.')\n }\n}\n\n/**\n * Generation middleware entrypoint rule: `generationRuns` is required (the\n * generation run lifecycle is keyed on its own `runId`, not a chat conversation\n * `threadId`). When artifact persistence is used, `artifacts` and `blobs` must\n * be provided together.\n */\nexport function validateGenerationPersistenceStores(\n persistence: AIPersistence,\n): void {\n validatePersistenceStoreKeys(persistence)\n const hasArtifacts = persistence.stores.artifacts !== undefined\n const hasBlobs = persistence.stores.blobs !== undefined\n if (hasArtifacts !== hasBlobs) {\n throw new Error(\n 'Generation artifact persistence requires both stores.artifacts and stores.blobs.',\n )\n }\n if (!persistence.stores.generationRuns) {\n throw new Error('Generation persistence requires stores.generationRuns.')\n }\n}\n\n/**\n * Server hydrate entrypoint rule: `messages` is required.\n */\nexport function validateReconstructChatStores(\n persistence: AIPersistence,\n): void {\n validatePersistenceStoreKeys(persistence)\n if (!persistence.stores.messages) {\n throw new Error('reconstructChat requires stores.messages.')\n }\n}\n\n/**\n * Server hydrate entrypoint rule for generation: `generationRuns` is required.\n * The run store resolves the latest generation for a thread (or a specific run\n * id), so a server-authoritative client can hydrate the last generation's\n * status, result, and artifact refs on load.\n */\nexport function validateReconstructGenerationStores(\n persistence: AIPersistence,\n): void {\n validatePersistenceStoreKeys(persistence)\n if (!persistence.stores.generationRuns) {\n throw new Error('reconstructGeneration requires stores.generationRuns.')\n }\n}\n\nexport function defineAIPersistence<TStores extends AIPersistenceStores>(\n persistence: AIPersistence<ExactStoreKeys<TStores>>,\n): AIPersistence<TStores> {\n validatePersistenceStoreKeys(persistence)\n return persistence\n}\n\nexport function composePersistence<\n TBase extends AIPersistenceStores,\n TOverrides extends AIPersistenceOverrides,\n>(\n base: AIPersistence<TBase>,\n config: {\n overrides: ExactStoreKeys<TOverrides>\n },\n): AIPersistence<ComposedAIPersistenceStores<TBase, TOverrides>>\nexport function composePersistence(\n base: AIPersistence,\n config: { overrides: AIPersistenceOverrides },\n): AIPersistence {\n validatePersistenceStoreKeys(base)\n assertKnownStoreKeys(config.overrides, 'override')\n\n const stores: AIPersistenceStores = { ...base.stores }\n for (const key of storeKeys) {\n if (!Object.prototype.hasOwnProperty.call(config.overrides, key)) continue\n const override = config.overrides[key]\n if (override === false) {\n delete stores[key]\n } else if (override !== undefined) {\n setStore(stores, key, override)\n }\n }\n return { stores }\n}\n\nfunction setStore<TKey extends StoreKey>(\n stores: AIPersistenceStores,\n key: TKey,\n value: NonNullable<AIPersistenceStores[TKey]>,\n): void {\n stores[key] = value\n}\n"],"mappings":";;;AAgWA,SAAgB,mBAAmB,OAAmC;CACpE,OAAO;AACT;;AAEA,SAAgB,qBAAqB,OAAuC;CAC1E,OAAO;AACT;;AAEA,SAAgB,oBAAoB,OAAqC;CACvE,OAAO;AACT;;AAEA,SAAgB,yBACd,OACoB;CACpB,OAAO;AACT;;AAEA,SAAgB,oBAAoB,OAAqC;CACvE,OAAO;AACT;;AAEA,SAAgB,gBAAgB,OAA6B;CAC3D,OAAO;AACT;AAsZA,IAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,IAAM,cAAc,IAAI,IAAY,SAAS;AAE7C,SAAS,qBAAqB,QAAgB,UAAwB;CACpE,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAClC,IAAI,CAAC,YAAY,IAAI,GAAG,GACtB,MAAM,IAAI,MAAM,yBAAyB,SAAS,QAAQ,KAAK;AAGrE;AAEA,SAAgB,6BAA6B,aAAkC;CAC7E,qBAAqB,YAAY,QAAQ,OAAO;AAClD;;;;;;AAOA,SAAgB,8BACd,aACM;CACN,6BAA6B,WAAW;CACxC,IAAI,CAAC,YAAY,OAAO,UACtB,MAAM,IAAI,MAAM,4CAA4C;CAE9D,IAAI,YAAY,OAAO,cAAc,CAAC,YAAY,OAAO,MACvD,MAAM,IAAI,MAAM,0DAA0D;AAE9E;;;;;;;AAQA,SAAgB,oCACd,aACM;CACN,6BAA6B,WAAW;CAGxC,IAFqB,YAAY,OAAO,cAAc,KAAA,OACrC,YAAY,OAAO,UAAU,KAAA,IAE5C,MAAM,IAAI,MACR,kFACF;CAEF,IAAI,CAAC,YAAY,OAAO,gBACtB,MAAM,IAAI,MAAM,wDAAwD;AAE5E;;;;AAKA,SAAgB,8BACd,aACM;CACN,6BAA6B,WAAW;CACxC,IAAI,CAAC,YAAY,OAAO,UACtB,MAAM,IAAI,MAAM,2CAA2C;AAE/D;;;;;;;AAQA,SAAgB,oCACd,aACM;CACN,6BAA6B,WAAW;CACxC,IAAI,CAAC,YAAY,OAAO,gBACtB,MAAM,IAAI,MAAM,uDAAuD;AAE3E;AAEA,SAAgB,oBACd,aACwB;CACxB,6BAA6B,WAAW;CACxC,OAAO;AACT;AAWA,SAAgB,mBACd,MACA,QACe;CACf,6BAA6B,IAAI;CACjC,qBAAqB,OAAO,WAAW,UAAU;CAEjD,MAAM,SAA8B,EAAE,GAAG,KAAK,OAAO;CACrD,KAAK,MAAM,OAAO,WAAW;EAC3B,IAAI,CAAC,OAAO,UAAU,eAAe,KAAK,OAAO,WAAW,GAAG,GAAG;EAClE,MAAM,WAAW,OAAO,UAAU;EAClC,IAAI,aAAa,OACf,OAAO,OAAO;OACT,IAAI,aAAa,KAAA,GACtB,SAAS,QAAQ,KAAK,QAAQ;CAElC;CACA,OAAO,EAAE,OAAO;AAClB;AAEA,SAAS,SACP,QACA,KACA,OACM;CACN,OAAO,OAAO;AAChB"}
|
|
1
|
+
{"version":3,"file":"types.js","names":[],"sources":["../../src/types.ts"],"sourcesContent":["import type {\n ModelMessage,\n MetadataStore,\n PersistedArtifactRef,\n RunStatus,\n RunStore,\n Scope,\n TokenUsage,\n} from '@tanstack/ai'\n\n// Re-export the shared identity type so app code can import Scope from either\n// `@tanstack/ai` or `@tanstack/ai-persistence`. See {@link Scope} security notes:\n// pair a client-visible `threadId` with a server-trusted `userId`/`tenantId`\n// before authorizing load/save (e.g. via `reconstructChat({ authorize })`).\nexport type { MetadataStore, Scope }\n\n// ===========================================================================\n// Store contracts\n// ===========================================================================\n//\n// EVOLUTION POLICY\n// ----------------\n// These store interfaces are the compatibility surface between the core\n// middleware and every backend — the in-memory reference store and every\n// adapter an application writes against its own database.\n//\n// - Store METHODS are REQUIRED. A new method is a breaking contract change:\n// every adapter gets a compile error and implements it. Do NOT add methods\n// as optional-and-feature-detected (`store.method?.(...)`) — an adapter\n// that has not implemented one is then indistinguishable from one whose\n// answer is legitimately empty, so the feature silently does nothing in\n// production instead of failing at build time. `findActiveRun` was optional\n// for exactly one release cycle and cost us precisely that: reconnect\n// degraded to \"no active run\" on every backend that had not caught up.\n// - Capability tiers belong at the STORE level, not the method level. A\n// backend that only stores a transcript declares `ChatTranscriptStores`\n// (no `runs`); it does not declare a half-implemented `RunStore`.\n// - Never tighten an existing method's required arguments or widen its\n// required return shape in a breaking way.\n//\n// The shared conformance testkit (`./testkit/conformance.ts`) is the\n// authoritative compatibility gate: every invariant documented on the methods\n// below is asserted there, and every backend runs the identical suite. If an\n// invariant is not encoded in the testkit, adapters cannot discover it — so\n// promote new invariants into both the JSDoc here AND the testkit.\n//\n// TIMESTAMP CONVENTION\n// --------------------\n// Store *records* (`RunRecord`, `InterruptRecord`, `ArtifactRecord`,\n// `BlobRecord`) speak **epoch milliseconds** (`number`), the native unit for\n// SQL/`BIGINT` columns and `Date.now()`. Wire/result references that leave the\n// persistence layer (e.g. core's `PersistedArtifactRef.createdAt`) speak\n// **ISO-8601 strings**. The middleware performs the number→ISO conversion at\n// the boundary; do not mix the two on a single field.\n\n/**\n * Durable store for a thread's full message transcript.\n *\n * A \"thread\" is the unit of conversation history. The key is\n * {@link Scope.threadId} (the same conversation id as\n * `ChatMiddlewareContext.threadId`). Store methods take a bare string for\n * adapter simplicity; multi-user isolation is the **host's** job — authorize\n * against `Scope.userId` / `Scope.tenantId` (derived server-side from session)\n * before calling load/save, and never treat a client-supplied thread id alone\n * as an ownership proof (see `Scope` security notes in `@tanstack/ai`).\n *\n * `saveThread` always receives and persists the **complete, authoritative**\n * message list — it is an overwrite, never an append. The middleware snapshots\n * `ctx.messages` (the full running transcript) into it.\n */\nexport interface MessageStore {\n /**\n * Return the full stored transcript for `threadId` ({@link Scope.threadId}),\n * in insertion order.\n *\n * INVARIANT: returns an empty array (never `null`/`undefined`) for a thread\n * that was never saved. Callers treat `[]` as \"no history\".\n */\n loadThread: (threadId: string) => Promise<Array<ModelMessage>>\n /**\n * Overwrite the stored transcript for `threadId` with `messages`.\n *\n * INVARIANT: this is a full replace. `messages` is the complete authoritative\n * history; the previous contents are discarded (not merged or appended).\n */\n saveThread: (threadId: string, messages: Array<ModelMessage>) => Promise<void>\n}\n\n// Run lifecycle types live in `@tanstack/ai` and are re-exported here: one run,\n// one record — shared by this package's `runs` store and `@tanstack/ai-sandbox`'s\n// run driver, instead of each package keeping a rival definition that can drift.\nexport type {\n RunStatus,\n TerminalRunStatus,\n RunRecord,\n RunStore,\n} from '@tanstack/ai'\nexport { isTerminalRunStatus, defineRunStore } from '@tanstack/ai'\n\n/**\n * Lifecycle status of a generation run. Deliberately the same vocabulary as\n * {@link RunStatus}, so an adapter that stores both kinds of run can share one\n * status column and one set of checks.\n */\nexport type GenerationRunStatus = RunStatus\n\n/**\n * A single generation run (one `generateImage` / `generateVideo` / … call).\n *\n * Its primary identity is `runId`: the run/request id the activity mints, the\n * same AG-UI run id the client sends on the wire. `threadId` is the SLOT the\n * run fills, a stable app-chosen name that groups successive runs of the same\n * thing, and it is what a server-driven client hydrates by. Generation state is\n * kept here, never in the chat {@link RunStore}.\n *\n * `result` holds terminal result METADATA (ids, model, urls, a provider video\n * job id), never the media bytes — those live in a {@link BlobStore}.\n * `artifacts` are the durable {@link PersistedArtifactRef}s, present only when\n * byte storage is on.\n *\n * @property startedAt - Epoch ms when the run was first created.\n * @property finishedAt - Epoch ms when the run reached a terminal status.\n */\nexport interface GenerationRunRecord {\n runId: string\n /**\n * The scope this run belongs to: a stable, app-chosen name for the slot\n * successive runs fill (`product-123-hero`, `video-9-start-frame`).\n *\n * REQUIRED, per the store-contract rule at the top of this file.\n * {@link GenerationRunStore.findLatestForThread} is the only query that\n * hydrates a run, and it keys on this — so a record without one can be\n * written and then never found again. `withGenerationPersistence` already\n * refuses to start a run without a scope, and a server-driven client\n * discards a snapshot that arrives without one, so an optional field here\n * only described a record no path could produce and no client would accept.\n */\n threadId: string\n /** `'image' | 'audio' | 'tts' | 'video' | 'transcription'`. */\n activity: string\n provider: string\n model: string\n status: GenerationRunStatus\n startedAt: number\n finishedAt?: number\n error?: { message: string; code?: string }\n /** Terminal result metadata (ids, model, urls). Never the media bytes. */\n result?: unknown\n /** Durable artifact references, when an artifacts + blobs backend is used. */\n artifacts?: Array<PersistedArtifactRef>\n usage?: TokenUsage\n}\n\n/**\n * Durable store for generation run records, the generation counterpart to\n * {@link RunStore}. Keyed by its own `runId`, with `threadId` the slot\n * {@link GenerationRunStore.findLatestForThread} looks runs up by.\n */\nexport interface GenerationRunStore {\n /**\n * Create a run record, or return the existing one if `runId` is already\n * present (resume).\n *\n * INVARIANT (idempotency): a second call for a `runId` returns the existing\n * record unchanged; `startedAt`/`activity`/`provider`/`model`/`threadId` are\n * not mutated. `status` defaults to `'running'` on first creation.\n */\n createOrResume: (\n input: Pick<\n GenerationRunRecord,\n 'runId' | 'threadId' | 'activity' | 'provider' | 'model' | 'startedAt'\n > & { status?: GenerationRunStatus },\n ) => Promise<GenerationRunRecord>\n /**\n * Patch a run record's mutable fields.\n *\n * INVARIANT: patching a `runId` that does not exist is a **no-op** — it must\n * not throw and must not create a record.\n */\n update: (\n runId: string,\n patch: Partial<\n Pick<\n GenerationRunRecord,\n 'status' | 'finishedAt' | 'error' | 'result' | 'artifacts' | 'usage'\n >\n >,\n ) => Promise<void>\n /** Return the run record for `runId`, or `null` if none exists. */\n get: (runId: string) => Promise<GenerationRunRecord | null>\n /**\n * The most recent run linked to `threadId`, or `null`.\n *\n * REQUIRED, per the store-contract rule at the top of this file: a\n * server-authoritative client hydrates by the stable thread id on every\n * mount, so an adapter without this would be indistinguishable from one that\n * legitimately has no run — `persistence: true` would silently restore\n * nothing, forever. `null` is the correct answer only when the thread really\n * has no runs. The chat parallel is {@link RunStore.findActiveRun}.\n */\n findLatestForThread: (threadId: string) => Promise<GenerationRunRecord | null>\n}\n\n/** Lifecycle status of a human-in-the-loop interrupt. */\nexport type InterruptStatus = 'pending' | 'resolved' | 'cancelled'\n\n/**\n * A human-in-the-loop interrupt (tool approval, client-tool input request, …).\n *\n * @property requestedAt - Epoch ms when the interrupt was created.\n * @property resolvedAt - Epoch ms when the interrupt was resolved/cancelled;\n * absent while pending.\n */\nexport interface InterruptRecord {\n interruptId: string\n runId: string\n threadId: string\n status: InterruptStatus\n requestedAt: number\n resolvedAt?: number\n payload: Record<string, unknown>\n response?: unknown\n}\n\n/** A terminal interrupt write for {@link InterruptStore.commitBatch}. */\nexport type InterruptCommitEntry =\n | {\n interruptId: string\n status: 'resolved'\n response?: unknown\n }\n | {\n interruptId: string\n status: 'cancelled'\n }\n\n/** Durable store for human-in-the-loop interrupts. */\nexport interface InterruptStore {\n /**\n * Persist a new interrupt in the `'pending'` state.\n *\n * The record is accepted without `status`/`resolvedAt` so a \"born resolved\"\n * interrupt is unrepresentable — every interrupt begins pending and only\n * `resolve`/`cancel` may move it to a terminal state.\n *\n * INVARIANT (insert-if-absent): if an interrupt with the same `interruptId`\n * already exists, `create` is a **no-op** — it must NOT overwrite the\n * existing record. This is the canonical behaviour (SQL backends implement it\n * via `ON CONFLICT DO NOTHING` / upsert-with-empty-update), so a duplicate\n * create can never clobber a resolved interrupt back to pending.\n */\n create: (\n record: Omit<InterruptRecord, 'status' | 'resolvedAt'>,\n ) => Promise<void>\n /**\n * Move an interrupt to `'resolved'`, stamping `resolvedAt` and storing\n * `response`. A no-op if `interruptId` does not exist.\n */\n resolve: (interruptId: string, response?: unknown) => Promise<void>\n /**\n * Move an interrupt to `'cancelled'`, stamping `resolvedAt`. A no-op if\n * `interruptId` does not exist.\n */\n cancel: (interruptId: string) => Promise<void>\n /**\n * Commit terminal writes for a validated resume batch.\n *\n * Optional. When present, `withPersistence` calls it once instead of\n * calling `resolve` and `cancel` for each entry. Apply every entry or none.\n *\n * Reject the whole batch (throw, writing nothing) when any entry has a\n * duplicate `interruptId`, references an `interruptId` that does not exist,\n * or references an interrupt whose status is not `'pending'`. This is\n * stricter than `resolve` / `cancel`, which are no-ops for a missing\n * `interruptId`.\n */\n commitBatch?: (entries: ReadonlyArray<InterruptCommitEntry>) => Promise<void>\n /** Return the interrupt for `interruptId`, or `null` if none exists. */\n get: (interruptId: string) => Promise<InterruptRecord | null>\n /**\n * All interrupts for a thread.\n *\n * INVARIANT: ordered by insertion (equivalently `requestedAt` ascending). SQL\n * backends MUST `ORDER BY requested_at` — the middleware and testkit rely on\n * this stable ordering.\n */\n list: (threadId: string) => Promise<Array<InterruptRecord>>\n /** Pending interrupts for a thread, ordered by `requestedAt` ascending. */\n listPending: (threadId: string) => Promise<Array<InterruptRecord>>\n /** All interrupts for a run, ordered by `requestedAt` ascending. */\n listByRun: (runId: string) => Promise<Array<InterruptRecord>>\n /** Pending interrupts for a run, ordered by `requestedAt` ascending. */\n listPendingByRun: (runId: string) => Promise<Array<InterruptRecord>>\n}\n\n// ===========================================================================\n// Store typers\n// ===========================================================================\n//\n// Identity helpers that type a store implementation inline: pass an object\n// literal and get autocomplete + contract checking, with no separate\n// `: MessageStore` return annotation. They compose into `defineAIPersistence`,\n// which infers **exact presence** — a store you define becomes a defined,\n// non-optional, autocompleted key on `persistence.stores`, and accessing a store\n// you did not define is a compile error.\n//\n// ```ts\n// const persistence = defineAIPersistence({\n// stores: {\n// messages: defineMessageStore({ loadThread, saveThread }),\n// runs: defineRunStore({ createOrResume, update, get, findActiveRun }),\n// },\n// })\n// persistence.stores.runs // RunStore (defined)\n// persistence.stores.interrupts // compile error — not provided\n// ```\n//\n// Presence is per STORE, not per method: every method of a store you define is\n// required (see the evolution policy above). Omitting one is a compile error,\n// not a partial store.\n\n/** Type a {@link MessageStore} implementation inline. */\nexport function defineMessageStore(store: MessageStore): MessageStore {\n return store\n}\n/** Type an {@link InterruptStore} implementation inline. */\nexport function defineInterruptStore(store: InterruptStore): InterruptStore {\n return store\n}\n/** Type a {@link MetadataStore} implementation inline. */\nexport function defineMetadataStore(store: MetadataStore): MetadataStore {\n return store\n}\n/** Type a {@link GenerationRunStore} implementation inline. */\nexport function defineGenerationRunStore(\n store: GenerationRunStore,\n): GenerationRunStore {\n return store\n}\n/** Type an {@link ArtifactStore} implementation inline. */\nexport function defineArtifactStore(store: ArtifactStore): ArtifactStore {\n return store\n}\n/** Type a {@link BlobStore} implementation inline. */\nexport function defineBlobStore(store: BlobStore): BlobStore {\n return store\n}\n\n/**\n * Metadata row describing a persisted artifact (generated media, tool output).\n *\n * The bytes themselves live in a {@link BlobStore}; this record holds the\n * descriptive metadata and an optional `sourceUrl` for reference-only\n * backends.\n *\n * @property createdAt - Epoch ms. (Core's wire-facing `PersistedArtifactRef`\n * exposes the same instant as an ISO string; see the timestamp convention.)\n */\nexport interface ArtifactRecord {\n artifactId: string\n runId: string\n threadId: string\n /**\n * The blob-store key these bytes actually live under.\n *\n * Optional for backwards compatibility: records written before this existed\n * resolve via the default `artifacts/<runId>/<artifactId>` convention. New\n * records always carry it, which is what lets `storageKey` put bytes anywhere\n * — a reader can no longer recompute the path, so it has to be remembered.\n * Use `resolveArtifactBlobKey(record)` rather than reading it directly.\n */\n blobKey?: string\n name: string\n mimeType: string\n size: number\n sourceUrl?: string\n createdAt: number\n}\n\n/** Durable store for artifact metadata records. */\nexport interface ArtifactStore {\n /** Insert or overwrite the artifact metadata record. */\n save: (record: ArtifactRecord) => Promise<void>\n /** Return the artifact for `artifactId`, or `null` if none exists. */\n get: (artifactId: string) => Promise<ArtifactRecord | null>\n /**\n * All artifacts for a run in deterministic snapshot order: `createdAt`\n * ascending, then `artifactId` ascending by the unsigned UTF-8 bytes of\n * each string (compare bytes left-to-right; shorter equal prefixes first).\n * Returns `[]` when the run has none.\n */\n list: (runId: string) => Promise<Array<ArtifactRecord>>\n /**\n * All artifacts for a thread in deterministic snapshot order.\n * Records are ordered by `createdAt` ascending, then by `artifactId` using\n * the unsigned UTF-8 bytes of each string (compare bytes left-to-right; shorter\n * equal prefixes first).\n */\n listForThread: (threadId: string) => Promise<Array<ArtifactRecord>>\n /**\n * Delete a single artifact by id. A no-op if absent, mirroring\n * {@link BlobStore.delete} — the two are written and deleted as a pair, so\n * their contracts match.\n */\n delete: (artifactId: string) => Promise<void>\n /**\n * Delete every artifact belonging to `runId`. A no-op when the run has none.\n *\n * Required rather than feature-detected: retention and erasure are the point\n * of storing media durably, and an adapter silently lacking deletion is\n * indistinguishable from one where there was nothing to delete.\n */\n deleteForRun: (runId: string) => Promise<void>\n}\n\n/**\n * Accepted body shapes for {@link BlobStore.put}. `ArrayBufferView` already\n * covers `Uint8Array` and every other typed-array/`DataView`, so no separate\n * `Uint8Array` member is needed.\n */\nexport type BlobBody =\n | ReadableStream<Uint8Array>\n | ArrayBuffer\n | ArrayBufferView\n | string\n | Blob\n\n/**\n * Metadata for a stored blob.\n *\n * @property size - Byte length, when known.\n * @property createdAt - Epoch ms first written.\n * @property updatedAt - Epoch ms last overwritten.\n */\nexport interface BlobRecord {\n key: string\n size?: number\n etag?: string\n contentType?: string\n customMetadata?: Record<string, string>\n createdAt?: number\n updatedAt?: number\n}\n\n/**\n * A byte range to read, in the shape an HTTP `Range` header resolves to.\n *\n * `offset` is measured from the start of the object and must be inside it;\n * `length` defaults to \"everything from `offset` to the end\" and is clamped to\n * the end when it overshoots. Suffix ranges (`bytes=-500`) are the caller's to\n * resolve against the known size — a serve route has the size on the artifact\n * record, and has to compare against it anyway to answer `416` before reading.\n */\nexport interface BlobRange {\n offset: number\n length?: number\n}\n\n/** Options for {@link BlobStore.get}. */\nexport interface BlobGetOptions {\n /**\n * Read only this slice of the object. `body`, `arrayBuffer()` and `text()`\n * then cover the slice, `size` still reports the WHOLE object, and `range`\n * reports the slice actually served — the three numbers a `206` response\n * needs (`Content-Range: bytes <offset>-<offset+length-1>/<size>`).\n */\n range?: BlobRange\n}\n\n/** A stored blob's metadata plus lazy accessors for its bytes. */\nexport interface BlobObject extends BlobRecord {\n arrayBuffer: () => Promise<ArrayBuffer>\n text: () => Promise<string>\n body?: ReadableStream<Uint8Array>\n /**\n * The slice this object exposes, when a {@link BlobGetOptions.range} was\n * requested and honoured: `offset` as asked, `length` as actually served\n * (clamped to the end of the object). Absent on a whole-object read.\n */\n range?: { offset: number; length: number }\n}\n\n/**\n * One page of a {@link BlobStore.list} scan.\n *\n * @property cursor - Opaque continuation token; present only when `truncated`.\n * @property truncated - `true` when more objects match beyond this page.\n */\nexport interface BlobListPage {\n objects: Array<BlobRecord>\n cursor?: string\n truncated?: boolean\n}\n\nexport interface BlobPutOptions {\n contentType?: string\n customMetadata?: Record<string, string>\n /**\n * The exact byte length of `body`, when the producer knows it up front.\n *\n * Advisory, not a contract the store must honor: it exists so a store can\n * pick an upload strategy knowingly instead of discovering the length by\n * buffering. Most useful to an SDK that wants the length as a separate\n * argument rather than reading it off the stream — S3's `PutObject`\n * (`ContentLength`) is the archetype — and to a runtime that can re-attach\n * one (workerd's `FixedLengthStream` ahead of `R2Bucket.put`).\n *\n * Only ever set when the length is exact — a wrong value is worse than none,\n * since runtimes that enforce declared lengths fail the write. Absent means\n * unknown, and a store must accept a length-less stream regardless:\n * producers hand one over whenever the origin does not declare a length.\n */\n expectedLength?: number\n}\n\nexport interface BlobListOptions {\n prefix?: string\n cursor?: string\n limit?: number\n}\n\n/** Durable object/blob store (byte-storing or reference-only backends). */\nexport interface BlobStore {\n /** Insert or overwrite the object at `key`, returning its metadata. */\n put: (\n key: string,\n body: BlobBody,\n options?: BlobPutOptions,\n ) => Promise<BlobRecord>\n /**\n * Return the object at `key` (metadata + byte accessors), or `null`.\n *\n * RANGE SEMANTICS: with `options.range`, return only that slice — the bytes\n * a `206` response carries — and report it back as `range`. `size` still\n * reports the whole object, so the caller can build `Content-Range` without\n * a second `head`. The reported `length` is what was actually served: a\n * requested `length` past the end clamps. An `offset` at or past the end is\n * a caller error, not a store one — the size is on the artifact record, so a\n * serve route answers `416` before ever asking the store.\n *\n * Range support is part of the contract for any store that holds bytes (the\n * conformance testkit asserts it): serving a whole file where a slice was\n * asked for is what makes `<video>` seeking, and Safari playback at all,\n * fail. A reference-only backend that stores no bytes skips `blobs`\n * entirely rather than half-implementing it.\n */\n get: (key: string, options?: BlobGetOptions) => Promise<BlobObject | null>\n /** Return only the metadata for `key`, or `null`. */\n head: (key: string) => Promise<BlobRecord | null>\n /** Remove the object at `key`. A no-op if absent. */\n delete: (key: string) => Promise<void>\n /**\n * List objects, optionally filtered by `prefix`, in ascending key order.\n *\n * CURSOR SEMANTICS: `prefix` matches literally and case-sensitively (SQL\n * backends must escape LIKE metacharacters, so `run_` matches only the exact\n * bytes `run_`, not `_` as a wildcard). When `limit` is given and more keys\n * match, the page is `truncated: true` with a `cursor`; passing that `cursor`\n * back returns the strictly-following keys (keys `> cursor`). Cursor ordering\n * is the same byte ordering as the sort, so paging visits every key exactly\n * once with no gaps or repeats. `limit: 0` yields an empty, untruncated page\n * with no cursor.\n */\n list: (options?: BlobListOptions) => Promise<BlobListPage>\n}\n\n/**\n * Sparse bag of **state** store keys — composition / validation only.\n *\n * **Not a public product shape.** Prefer the named chat shapes below\n * ({@link ChatTranscriptStores}, {@link ChatPersistenceStores},\n * {@link ChatWithInterruptsStores}). Locks are not included — use\n * `withLocks` from `@tanstack/ai`.\n *\n * @internal Exported from this module for generics; the package root does not\n * re-export this type — use a named shape or `AIPersistence<{ … }>` instead.\n */\nexport interface AIPersistenceStores {\n messages?: MessageStore\n runs?: RunStore\n interrupts?: InterruptStore\n metadata?: MetadataStore\n generationRuns?: GenerationRunStore\n artifacts?: ArtifactStore\n blobs?: BlobStore\n}\n\n/**\n * Chat floor: durable transcript. `messages` is required.\n *\n * `runs` / `interrupts` / `metadata` remain optional. If `interrupts` is set,\n * `runs` is required (enforced by `withPersistence` / validators).\n */\nexport interface ChatTranscriptStores {\n messages: MessageStore\n runs?: RunStore\n interrupts?: InterruptStore\n metadata?: MetadataStore\n}\n\n/**\n * Full chat durability — all four state stores are present. This is what\n * `memoryPersistence()` returns, and the shape most adapters should declare.\n *\n * Backends that only need a transcript should use\n * {@link ChatTranscriptStores} instead.\n */\nexport interface ChatPersistenceStores {\n messages: MessageStore\n runs: RunStore\n interrupts: InterruptStore\n metadata: MetadataStore\n}\n\n/**\n * Chat with durable human-in-the-loop interrupts (and optional metadata).\n * Implies `runs` (interrupt records are run-scoped).\n *\n * Prefer {@link ChatPersistenceStores} when you also have metadata (packaged\n * backends). Use this when interrupts are required but metadata is not.\n */\nexport interface ChatWithInterruptsStores {\n messages: MessageStore\n runs: RunStore\n interrupts: InterruptStore\n metadata?: MetadataStore\n}\n\n/**\n * Persistence aggregate. Parameterize with a named store shape, or a sparse\n * map for composition (`defineAIPersistence` / `composePersistence`).\n *\n * Default is the sparse bag so untyped / dynamic bags still type-check;\n * prefer {@link ChatTranscriptPersistence} or {@link ChatPersistence} at\n * call sites.\n */\nexport interface AIPersistence<\n TStores extends AIPersistenceStores = AIPersistenceStores,\n> {\n stores: ExactStoreKeys<TStores>\n}\n\n/** {@link AIPersistence} for {@link ChatTranscriptStores}. */\nexport type ChatTranscriptPersistence = AIPersistence<ChatTranscriptStores>\n\n/** {@link AIPersistence} for {@link ChatPersistenceStores}. */\nexport type ChatPersistence = AIPersistence<ChatPersistenceStores>\n\n/** {@link AIPersistence} for {@link ChatWithInterruptsStores}. */\nexport type ChatWithInterruptsPersistence =\n AIPersistence<ChatWithInterruptsStores>\n\ntype StoreKey = keyof AIPersistenceStores\ntype ExactStoreKeys<TStores> =\n Exclude<keyof TStores, StoreKey> extends never\n ? TStores\n : TStores & Record<Exclude<keyof TStores, StoreKey>, never>\n\nexport type AIPersistenceOverrides = {\n [TKey in StoreKey]?: AIPersistenceStores[TKey] | false\n}\n\ntype BaseStoreValue<\n TBase extends AIPersistenceStores,\n TKey extends StoreKey,\n> = TKey extends keyof TBase ? TBase[TKey] : never\n\ntype OverrideStoreValue<\n TOverrides extends AIPersistenceOverrides,\n TKey extends StoreKey,\n> = TKey extends keyof TOverrides ? TOverrides[TKey] : never\n\ntype ResolvedStoreValue<\n TBase extends AIPersistenceStores,\n TOverrides extends AIPersistenceOverrides,\n TKey extends StoreKey,\n> = TKey extends keyof TOverrides\n ?\n | Exclude<OverrideStoreValue<TOverrides, TKey>, false | undefined>\n | (undefined extends OverrideStoreValue<TOverrides, TKey>\n ? Exclude<BaseStoreValue<TBase, TKey>, undefined>\n : never)\n : Exclude<BaseStoreValue<TBase, TKey>, undefined>\n\ntype BaseStoreIsRequired<\n TBase extends AIPersistenceStores,\n TKey extends StoreKey,\n> = TKey extends keyof TBase\n ? object extends Pick<TBase, TKey>\n ? false\n : true\n : false\n\ntype ResolvedStoreIsRequired<\n TBase extends AIPersistenceStores,\n TOverrides extends AIPersistenceOverrides,\n TKey extends StoreKey,\n> = TKey extends keyof TOverrides\n ? false extends OverrideStoreValue<TOverrides, TKey>\n ? false\n : undefined extends OverrideStoreValue<TOverrides, TKey>\n ? BaseStoreIsRequired<TBase, TKey>\n : true\n : BaseStoreIsRequired<TBase, TKey>\n\ntype ResolvedRequiredKeys<\n TBase extends AIPersistenceStores,\n TOverrides extends AIPersistenceOverrides,\n> = {\n [TKey in StoreKey]-?: [ResolvedStoreValue<TBase, TOverrides, TKey>] extends [\n never,\n ]\n ? never\n : ResolvedStoreIsRequired<TBase, TOverrides, TKey> extends true\n ? TKey\n : never\n}[StoreKey]\n\ntype ResolvedOptionalKeys<\n TBase extends AIPersistenceStores,\n TOverrides extends AIPersistenceOverrides,\n> = {\n [TKey in StoreKey]-?: [ResolvedStoreValue<TBase, TOverrides, TKey>] extends [\n never,\n ]\n ? never\n : ResolvedStoreIsRequired<TBase, TOverrides, TKey> extends true\n ? never\n : TKey\n}[StoreKey]\n\ntype Simplify<T> = { [TKey in keyof T]: T[TKey] }\n\nexport type ComposedAIPersistenceStores<\n TBase extends AIPersistenceStores,\n TOverrides extends AIPersistenceOverrides,\n> = Simplify<\n {\n [TKey in ResolvedRequiredKeys<TBase, TOverrides>]: ResolvedStoreValue<\n TBase,\n TOverrides,\n TKey\n >\n } & {\n [TKey in ResolvedOptionalKeys<TBase, TOverrides>]?: ResolvedStoreValue<\n TBase,\n TOverrides,\n TKey\n >\n }\n>\n\nconst storeKeys = [\n 'messages',\n 'runs',\n 'generationRuns',\n 'interrupts',\n 'metadata',\n 'artifacts',\n 'blobs',\n] satisfies Array<StoreKey>\n\nconst storeKeySet = new Set<string>(storeKeys)\n\nfunction assertKnownStoreKeys(stores: object, location: string): void {\n for (const key of Object.keys(stores)) {\n if (!storeKeySet.has(key)) {\n throw new Error(`Unknown AIPersistence ${location} key: ${key}`)\n }\n }\n}\n\nexport function validatePersistenceStoreKeys(persistence: AIPersistence): void {\n assertKnownStoreKeys(persistence.stores, 'store')\n}\n\n/**\n * Chat middleware entrypoint rules:\n * - `messages` is required (chat persistence means a durable transcript)\n * - `interrupts` requires `runs` (interrupt records are run-scoped)\n */\nexport function validateChatPersistenceStores(\n persistence: AIPersistence,\n): void {\n validatePersistenceStoreKeys(persistence)\n if (!persistence.stores.messages) {\n throw new Error('Chat persistence requires stores.messages.')\n }\n if (persistence.stores.interrupts && !persistence.stores.runs) {\n throw new Error('Chat persistence stores.interrupts requires stores.runs.')\n }\n}\n\n/**\n * Generation middleware entrypoint rule: `generationRuns` is required (the\n * generation run lifecycle is keyed on its own `runId`, not a chat conversation\n * `threadId`). When artifact persistence is used, `artifacts` and `blobs` must\n * be provided together.\n */\nexport function validateGenerationPersistenceStores(\n persistence: AIPersistence,\n): void {\n validatePersistenceStoreKeys(persistence)\n const hasArtifacts = persistence.stores.artifacts !== undefined\n const hasBlobs = persistence.stores.blobs !== undefined\n if (hasArtifacts !== hasBlobs) {\n throw new Error(\n 'Generation artifact persistence requires both stores.artifacts and stores.blobs.',\n )\n }\n if (!persistence.stores.generationRuns) {\n throw new Error('Generation persistence requires stores.generationRuns.')\n }\n}\n\n/**\n * Server hydrate entrypoint rule: `messages` is required.\n */\nexport function validateReconstructChatStores(\n persistence: AIPersistence,\n): void {\n validatePersistenceStoreKeys(persistence)\n if (!persistence.stores.messages) {\n throw new Error('reconstructChat requires stores.messages.')\n }\n}\n\n/**\n * Server hydrate entrypoint rule for generation: `generationRuns` is required.\n * The run store resolves the latest generation for a thread (or a specific run\n * id), so a server-authoritative client can hydrate the last generation's\n * status, result, and artifact refs on load.\n */\nexport function validateReconstructGenerationStores(\n persistence: AIPersistence,\n): void {\n validatePersistenceStoreKeys(persistence)\n if (!persistence.stores.generationRuns) {\n throw new Error('reconstructGeneration requires stores.generationRuns.')\n }\n}\n\nexport function defineAIPersistence<TStores extends AIPersistenceStores>(\n persistence: AIPersistence<ExactStoreKeys<TStores>>,\n): AIPersistence<TStores> {\n validatePersistenceStoreKeys(persistence)\n return persistence\n}\n\nexport function composePersistence<\n TBase extends AIPersistenceStores,\n TOverrides extends AIPersistenceOverrides,\n>(\n base: AIPersistence<TBase>,\n config: {\n overrides: ExactStoreKeys<TOverrides>\n },\n): AIPersistence<ComposedAIPersistenceStores<TBase, TOverrides>>\nexport function composePersistence(\n base: AIPersistence,\n config: { overrides: AIPersistenceOverrides },\n): AIPersistence {\n validatePersistenceStoreKeys(base)\n assertKnownStoreKeys(config.overrides, 'override')\n\n const stores: AIPersistenceStores = { ...base.stores }\n for (const key of storeKeys) {\n if (!Object.prototype.hasOwnProperty.call(config.overrides, key)) continue\n const override = config.overrides[key]\n if (override === false) {\n delete stores[key]\n } else if (override !== undefined) {\n setStore(stores, key, override)\n }\n }\n return { stores }\n}\n\nfunction setStore<TKey extends StoreKey>(\n stores: AIPersistenceStores,\n key: TKey,\n value: NonNullable<AIPersistenceStores[TKey]>,\n): void {\n stores[key] = value\n}\n"],"mappings":";;;AAkUA,SAAgB,mBAAmB,OAAmC;CACpE,OAAO;AACT;;AAEA,SAAgB,qBAAqB,OAAuC;CAC1E,OAAO;AACT;;AAEA,SAAgB,oBAAoB,OAAqC;CACvE,OAAO;AACT;;AAEA,SAAgB,yBACd,OACoB;CACpB,OAAO;AACT;;AAEA,SAAgB,oBAAoB,OAAqC;CACvE,OAAO;AACT;;AAEA,SAAgB,gBAAgB,OAA6B;CAC3D,OAAO;AACT;AAsZA,IAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,IAAM,cAAc,IAAI,IAAY,SAAS;AAE7C,SAAS,qBAAqB,QAAgB,UAAwB;CACpE,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAClC,IAAI,CAAC,YAAY,IAAI,GAAG,GACtB,MAAM,IAAI,MAAM,yBAAyB,SAAS,QAAQ,KAAK;AAGrE;AAEA,SAAgB,6BAA6B,aAAkC;CAC7E,qBAAqB,YAAY,QAAQ,OAAO;AAClD;;;;;;AAOA,SAAgB,8BACd,aACM;CACN,6BAA6B,WAAW;CACxC,IAAI,CAAC,YAAY,OAAO,UACtB,MAAM,IAAI,MAAM,4CAA4C;CAE9D,IAAI,YAAY,OAAO,cAAc,CAAC,YAAY,OAAO,MACvD,MAAM,IAAI,MAAM,0DAA0D;AAE9E;;;;;;;AAQA,SAAgB,oCACd,aACM;CACN,6BAA6B,WAAW;CAGxC,IAFqB,YAAY,OAAO,cAAc,KAAA,OACrC,YAAY,OAAO,UAAU,KAAA,IAE5C,MAAM,IAAI,MACR,kFACF;CAEF,IAAI,CAAC,YAAY,OAAO,gBACtB,MAAM,IAAI,MAAM,wDAAwD;AAE5E;;;;AAKA,SAAgB,8BACd,aACM;CACN,6BAA6B,WAAW;CACxC,IAAI,CAAC,YAAY,OAAO,UACtB,MAAM,IAAI,MAAM,2CAA2C;AAE/D;;;;;;;AAQA,SAAgB,oCACd,aACM;CACN,6BAA6B,WAAW;CACxC,IAAI,CAAC,YAAY,OAAO,gBACtB,MAAM,IAAI,MAAM,uDAAuD;AAE3E;AAEA,SAAgB,oBACd,aACwB;CACxB,6BAA6B,WAAW;CACxC,OAAO;AACT;AAWA,SAAgB,mBACd,MACA,QACe;CACf,6BAA6B,IAAI;CACjC,qBAAqB,OAAO,WAAW,UAAU;CAEjD,MAAM,SAA8B,EAAE,GAAG,KAAK,OAAO;CACrD,KAAK,MAAM,OAAO,WAAW;EAC3B,IAAI,CAAC,OAAO,UAAU,eAAe,KAAK,OAAO,WAAW,GAAG,GAAG;EAClE,MAAM,WAAW,OAAO,UAAU;EAClC,IAAI,aAAa,OACf,OAAO,OAAO;OACT,IAAI,aAAa,KAAA,GACtB,SAAS,QAAQ,KAAK,QAAQ;CAElC;CACA,OAAO,EAAE,OAAO;AAClB;AAEA,SAAS,SACP,QACA,KACA,OACM;CACN,OAAO,OAAO;AAChB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-persistence",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Composable state persistence for TanStack AI messages, runs, interrupts, metadata, and locks.",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"vitest": "^4.1.10",
|
|
46
|
-
"@tanstack/ai": "^0.
|
|
46
|
+
"@tanstack/ai": "^0.52.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependenciesMeta": {
|
|
49
49
|
"vitest": {
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@vitest/coverage-v8": "4.1.10",
|
|
55
55
|
"vitest": "^4.1.10",
|
|
56
|
-
"@tanstack/ai": "0.
|
|
56
|
+
"@tanstack/ai": "0.52.0"
|
|
57
57
|
},
|
|
58
58
|
"scripts": {
|
|
59
59
|
"build": "vite build",
|
package/src/middleware.ts
CHANGED
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
fromSpecTokenUsage,
|
|
4
4
|
getDetachableRun,
|
|
5
5
|
InterruptResumeValidationError,
|
|
6
|
+
MetadataCapability,
|
|
7
|
+
provideMetadata,
|
|
6
8
|
readInterruptBinding,
|
|
7
9
|
validateInterruptResumeBatch,
|
|
8
10
|
wasCancelRequested,
|
|
@@ -1961,6 +1963,7 @@ export function withPersistence<TStores extends ChatTranscriptStores>(
|
|
|
1961
1963
|
const provides = [
|
|
1962
1964
|
PersistenceCapability,
|
|
1963
1965
|
PersistenceCompletionCapability,
|
|
1966
|
+
...(persistence.stores.metadata ? [MetadataCapability] : []),
|
|
1964
1967
|
...(wantsInterrupts ? [InterruptsCapability] : []),
|
|
1965
1968
|
]
|
|
1966
1969
|
|
|
@@ -1969,6 +1972,9 @@ export function withPersistence<TStores extends ChatTranscriptStores>(
|
|
|
1969
1972
|
provides,
|
|
1970
1973
|
setup(ctx: ChatMiddlewareContext) {
|
|
1971
1974
|
providePersistence(ctx, persistence)
|
|
1975
|
+
if (persistence.stores.metadata) {
|
|
1976
|
+
provideMetadata(ctx, persistence.stores.metadata)
|
|
1977
|
+
}
|
|
1972
1978
|
|
|
1973
1979
|
let resolveCompletion: () => void = () => undefined
|
|
1974
1980
|
let rejectCompletion: (error: unknown) => void = () => undefined
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ModelMessage,
|
|
3
|
+
MetadataStore,
|
|
3
4
|
PersistedArtifactRef,
|
|
4
5
|
RunStatus,
|
|
5
6
|
RunStore,
|
|
@@ -11,7 +12,7 @@ import type {
|
|
|
11
12
|
// `@tanstack/ai` or `@tanstack/ai-persistence`. See {@link Scope} security notes:
|
|
12
13
|
// pair a client-visible `threadId` with a server-trusted `userId`/`tenantId`
|
|
13
14
|
// before authorizing load/save (e.g. via `reconstructChat({ authorize })`).
|
|
14
|
-
export type { Scope }
|
|
15
|
+
export type { MetadataStore, Scope }
|
|
15
16
|
|
|
16
17
|
// ===========================================================================
|
|
17
18
|
// Store contracts
|
|
@@ -292,37 +293,6 @@ export interface InterruptStore {
|
|
|
292
293
|
listPendingByRun: (runId: string) => Promise<Array<InterruptRecord>>
|
|
293
294
|
}
|
|
294
295
|
|
|
295
|
-
/**
|
|
296
|
-
* Namespaced key/value store for arbitrary JSON metadata (app-owned).
|
|
297
|
-
*
|
|
298
|
-
* The first argument is an **app-defined namespace string**, not the shared
|
|
299
|
-
* {@link Scope} identity type from `@tanstack/ai`. Composite identity is
|
|
300
|
-
* `(namespace, key)` as two independent fields (SQL backends use a composite
|
|
301
|
-
* primary key; the in-memory store uses nested maps). Do not encode both into a
|
|
302
|
-
* single delimited string — `${namespace}:${key}` collides when either part
|
|
303
|
-
* contains `:`.
|
|
304
|
-
*
|
|
305
|
-
* The same `key` under different namespaces is independent.
|
|
306
|
-
*/
|
|
307
|
-
export interface MetadataStore {
|
|
308
|
-
/**
|
|
309
|
-
* Return the stored value for `(namespace, key)`, or `null` if absent.
|
|
310
|
-
*
|
|
311
|
-
* CAVEAT: the return type is `unknown | null`, where `| null` collapses into
|
|
312
|
-
* `unknown` — a stored value of `null` is therefore **indistinguishable from
|
|
313
|
-
* absence** at the type level. Callers that must persist a real `null`
|
|
314
|
-
* distinctly from "not set" should wrap it (e.g. store `{ value: null }`).
|
|
315
|
-
*/
|
|
316
|
-
get: (namespace: string, key: string) => Promise<unknown | null>
|
|
317
|
-
/** Insert or overwrite the value for `(namespace, key)`. */
|
|
318
|
-
set: (namespace: string, key: string, value: unknown) => Promise<void>
|
|
319
|
-
/**
|
|
320
|
-
* Remove `(namespace, key)`. A no-op if absent. Does not affect other
|
|
321
|
-
* namespaces.
|
|
322
|
-
*/
|
|
323
|
-
delete: (namespace: string, key: string) => Promise<void>
|
|
324
|
-
}
|
|
325
|
-
|
|
326
296
|
// ===========================================================================
|
|
327
297
|
// Store typers
|
|
328
298
|
// ===========================================================================
|