@tanstack/ai-persistence 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/esm/blob-range.d.ts +51 -0
  2. package/dist/esm/blob-range.js +84 -0
  3. package/dist/esm/blob-range.js.map +1 -0
  4. package/dist/esm/capabilities.d.ts +5 -0
  5. package/dist/esm/capabilities.js +16 -0
  6. package/dist/esm/capabilities.js.map +1 -0
  7. package/dist/esm/index.d.ts +13 -0
  8. package/dist/esm/index.js +9 -0
  9. package/dist/esm/memory.d.ts +19 -0
  10. package/dist/esm/memory.js +319 -0
  11. package/dist/esm/memory.js.map +1 -0
  12. package/dist/esm/middleware.d.ts +252 -0
  13. package/dist/esm/middleware.js +872 -0
  14. package/dist/esm/middleware.js.map +1 -0
  15. package/dist/esm/reconstruct-generation.d.ts +129 -0
  16. package/dist/esm/reconstruct-generation.js +148 -0
  17. package/dist/esm/reconstruct-generation.js.map +1 -0
  18. package/dist/esm/reconstruct.d.ts +79 -0
  19. package/dist/esm/reconstruct.js +75 -0
  20. package/dist/esm/reconstruct.js.map +1 -0
  21. package/dist/esm/retrieve.d.ts +40 -0
  22. package/dist/esm/retrieve.js +54 -0
  23. package/dist/esm/retrieve.js.map +1 -0
  24. package/dist/esm/testkit/conformance.d.ts +33 -0
  25. package/dist/esm/testkit/conformance.js +997 -0
  26. package/dist/esm/testkit/conformance.js.map +1 -0
  27. package/dist/esm/types.d.ts +554 -0
  28. package/dist/esm/types.js +103 -0
  29. package/dist/esm/types.js.map +1 -0
  30. package/package.json +71 -0
  31. package/skills/ai-persistence/SKILL.md +218 -0
  32. package/skills/ai-persistence/build-cloudflare-adapter/SKILL.md +313 -0
  33. package/skills/ai-persistence/build-cloudflare-artifact-store/SKILL.md +693 -0
  34. package/skills/ai-persistence/build-custom-adapter/SKILL.md +328 -0
  35. package/skills/ai-persistence/build-drizzle-adapter/SKILL.md +562 -0
  36. package/skills/ai-persistence/build-prisma-adapter/SKILL.md +518 -0
  37. package/skills/ai-persistence/server/SKILL.md +210 -0
  38. package/skills/ai-persistence/stores/SKILL.md +485 -0
  39. package/src/blob-range.ts +101 -0
  40. package/src/capabilities.ts +18 -0
  41. package/src/index.ts +114 -0
  42. package/src/memory.ts +491 -0
  43. package/src/middleware.ts +1795 -0
  44. package/src/reconstruct-generation.ts +244 -0
  45. package/src/reconstruct.ts +149 -0
  46. package/src/retrieve.ts +77 -0
  47. package/src/testkit/conformance.ts +1288 -0
  48. package/src/types.ts +878 -0
package/src/types.ts ADDED
@@ -0,0 +1,878 @@
1
+ import type {
2
+ ModelMessage,
3
+ PersistedArtifactRef,
4
+ RunStatus,
5
+ RunStore,
6
+ Scope,
7
+ TokenUsage,
8
+ } from '@tanstack/ai'
9
+
10
+ // Re-export the shared identity type so app code can import Scope from either
11
+ // `@tanstack/ai` or `@tanstack/ai-persistence`. See {@link Scope} security notes:
12
+ // pair a client-visible `threadId` with a server-trusted `userId`/`tenantId`
13
+ // before authorizing load/save (e.g. via `reconstructChat({ authorize })`).
14
+ export type { Scope }
15
+
16
+ // ===========================================================================
17
+ // Store contracts
18
+ // ===========================================================================
19
+ //
20
+ // EVOLUTION POLICY
21
+ // ----------------
22
+ // These store interfaces are the compatibility surface between the core
23
+ // middleware and every backend — the in-memory reference store and every
24
+ // adapter an application writes against its own database.
25
+ //
26
+ // - Store METHODS are REQUIRED. A new method is a breaking contract change:
27
+ // every adapter gets a compile error and implements it. Do NOT add methods
28
+ // as optional-and-feature-detected (`store.method?.(...)`) — an adapter
29
+ // that has not implemented one is then indistinguishable from one whose
30
+ // answer is legitimately empty, so the feature silently does nothing in
31
+ // production instead of failing at build time. `findActiveRun` was optional
32
+ // for exactly one release cycle and cost us precisely that: reconnect
33
+ // degraded to "no active run" on every backend that had not caught up.
34
+ // - Capability tiers belong at the STORE level, not the method level. A
35
+ // backend that only stores a transcript declares `ChatTranscriptStores`
36
+ // (no `runs`); it does not declare a half-implemented `RunStore`.
37
+ // - Never tighten an existing method's required arguments or widen its
38
+ // required return shape in a breaking way.
39
+ //
40
+ // The shared conformance testkit (`./testkit/conformance.ts`) is the
41
+ // authoritative compatibility gate: every invariant documented on the methods
42
+ // below is asserted there, and every backend runs the identical suite. If an
43
+ // invariant is not encoded in the testkit, adapters cannot discover it — so
44
+ // promote new invariants into both the JSDoc here AND the testkit.
45
+ //
46
+ // TIMESTAMP CONVENTION
47
+ // --------------------
48
+ // Store *records* (`RunRecord`, `InterruptRecord`, `ArtifactRecord`,
49
+ // `BlobRecord`) speak **epoch milliseconds** (`number`), the native unit for
50
+ // SQL/`BIGINT` columns and `Date.now()`. Wire/result references that leave the
51
+ // persistence layer (e.g. core's `PersistedArtifactRef.createdAt`) speak
52
+ // **ISO-8601 strings**. The middleware performs the number→ISO conversion at
53
+ // the boundary; do not mix the two on a single field.
54
+
55
+ /**
56
+ * Durable store for a thread's full message transcript.
57
+ *
58
+ * A "thread" is the unit of conversation history. The key is
59
+ * {@link Scope.threadId} (the same conversation id as
60
+ * `ChatMiddlewareContext.threadId`). Store methods take a bare string for
61
+ * adapter simplicity; multi-user isolation is the **host's** job — authorize
62
+ * against `Scope.userId` / `Scope.tenantId` (derived server-side from session)
63
+ * before calling load/save, and never treat a client-supplied thread id alone
64
+ * as an ownership proof (see `Scope` security notes in `@tanstack/ai`).
65
+ *
66
+ * `saveThread` always receives and persists the **complete, authoritative**
67
+ * message list — it is an overwrite, never an append. The middleware snapshots
68
+ * `ctx.messages` (the full running transcript) into it.
69
+ */
70
+ export interface MessageStore {
71
+ /**
72
+ * Return the full stored transcript for `threadId` ({@link Scope.threadId}),
73
+ * in insertion order.
74
+ *
75
+ * INVARIANT: returns an empty array (never `null`/`undefined`) for a thread
76
+ * that was never saved. Callers treat `[]` as "no history".
77
+ */
78
+ loadThread: (threadId: string) => Promise<Array<ModelMessage>>
79
+ /**
80
+ * Overwrite the stored transcript for `threadId` with `messages`.
81
+ *
82
+ * INVARIANT: this is a full replace. `messages` is the complete authoritative
83
+ * history; the previous contents are discarded (not merged or appended).
84
+ */
85
+ saveThread: (threadId: string, messages: Array<ModelMessage>) => Promise<void>
86
+ }
87
+
88
+ // Run lifecycle types live in `@tanstack/ai` and are re-exported here: one run,
89
+ // one record — shared by this package's `runs` store and `@tanstack/ai-sandbox`'s
90
+ // run driver, instead of each package keeping a rival definition that can drift.
91
+ export type {
92
+ RunStatus,
93
+ TerminalRunStatus,
94
+ RunRecord,
95
+ RunStore,
96
+ } from '@tanstack/ai'
97
+ export { isTerminalRunStatus, defineRunStore } from '@tanstack/ai'
98
+
99
+ /**
100
+ * Lifecycle status of a generation run. Deliberately the same vocabulary as
101
+ * {@link RunStatus}, so an adapter that stores both kinds of run can share one
102
+ * status column and one set of checks.
103
+ */
104
+ export type GenerationRunStatus = RunStatus
105
+
106
+ /**
107
+ * A single generation run (one `generateImage` / `generateVideo` / … call).
108
+ *
109
+ * Its primary identity is `runId`: the run/request id the activity mints, the
110
+ * same AG-UI run id the client sends on the wire. `threadId` is the SLOT the
111
+ * run fills, a stable app-chosen name that groups successive runs of the same
112
+ * thing, and it is what a server-driven client hydrates by. Generation state is
113
+ * kept here, never in the chat {@link RunStore}.
114
+ *
115
+ * `result` holds terminal result METADATA (ids, model, urls, a provider video
116
+ * job id), never the media bytes — those live in a {@link BlobStore}.
117
+ * `artifacts` are the durable {@link PersistedArtifactRef}s, present only when
118
+ * byte storage is on.
119
+ *
120
+ * @property startedAt - Epoch ms when the run was first created.
121
+ * @property finishedAt - Epoch ms when the run reached a terminal status.
122
+ */
123
+ export interface GenerationRunRecord {
124
+ runId: string
125
+ /**
126
+ * The scope this run belongs to: a stable, app-chosen name for the slot
127
+ * successive runs fill (`product-123-hero`, `video-9-start-frame`).
128
+ *
129
+ * REQUIRED, per the store-contract rule at the top of this file.
130
+ * {@link GenerationRunStore.findLatestForThread} is the only query that
131
+ * hydrates a run, and it keys on this — so a record without one can be
132
+ * written and then never found again. `withGenerationPersistence` already
133
+ * refuses to start a run without a scope, and a server-driven client
134
+ * discards a snapshot that arrives without one, so an optional field here
135
+ * only described a record no path could produce and no client would accept.
136
+ */
137
+ threadId: string
138
+ /** `'image' | 'audio' | 'tts' | 'video' | 'transcription'`. */
139
+ activity: string
140
+ provider: string
141
+ model: string
142
+ status: GenerationRunStatus
143
+ startedAt: number
144
+ finishedAt?: number
145
+ error?: { message: string; code?: string }
146
+ /** Terminal result metadata (ids, model, urls). Never the media bytes. */
147
+ result?: unknown
148
+ /** Durable artifact references, when an artifacts + blobs backend is used. */
149
+ artifacts?: Array<PersistedArtifactRef>
150
+ usage?: TokenUsage
151
+ }
152
+
153
+ /**
154
+ * Durable store for generation run records, the generation counterpart to
155
+ * {@link RunStore}. Keyed by its own `runId`, with `threadId` the slot
156
+ * {@link GenerationRunStore.findLatestForThread} looks runs up by.
157
+ */
158
+ export interface GenerationRunStore {
159
+ /**
160
+ * Create a run record, or return the existing one if `runId` is already
161
+ * present (resume).
162
+ *
163
+ * INVARIANT (idempotency): a second call for a `runId` returns the existing
164
+ * record unchanged; `startedAt`/`activity`/`provider`/`model`/`threadId` are
165
+ * not mutated. `status` defaults to `'running'` on first creation.
166
+ */
167
+ createOrResume: (
168
+ input: Pick<
169
+ GenerationRunRecord,
170
+ 'runId' | 'threadId' | 'activity' | 'provider' | 'model' | 'startedAt'
171
+ > & { status?: GenerationRunStatus },
172
+ ) => Promise<GenerationRunRecord>
173
+ /**
174
+ * Patch a run record's mutable fields.
175
+ *
176
+ * INVARIANT: patching a `runId` that does not exist is a **no-op** — it must
177
+ * not throw and must not create a record.
178
+ */
179
+ update: (
180
+ runId: string,
181
+ patch: Partial<
182
+ Pick<
183
+ GenerationRunRecord,
184
+ 'status' | 'finishedAt' | 'error' | 'result' | 'artifacts' | 'usage'
185
+ >
186
+ >,
187
+ ) => Promise<void>
188
+ /** Return the run record for `runId`, or `null` if none exists. */
189
+ get: (runId: string) => Promise<GenerationRunRecord | null>
190
+ /**
191
+ * The most recent run linked to `threadId`, or `null`.
192
+ *
193
+ * REQUIRED, per the store-contract rule at the top of this file: a
194
+ * server-authoritative client hydrates by the stable thread id on every
195
+ * mount, so an adapter without this would be indistinguishable from one that
196
+ * legitimately has no run — `persistence: true` would silently restore
197
+ * nothing, forever. `null` is the correct answer only when the thread really
198
+ * has no runs. The chat parallel is {@link RunStore.findActiveRun}.
199
+ */
200
+ findLatestForThread: (threadId: string) => Promise<GenerationRunRecord | null>
201
+ }
202
+
203
+ /** Lifecycle status of a human-in-the-loop interrupt. */
204
+ export type InterruptStatus = 'pending' | 'resolved' | 'cancelled'
205
+
206
+ /**
207
+ * A human-in-the-loop interrupt (tool approval, client-tool input request, …).
208
+ *
209
+ * @property requestedAt - Epoch ms when the interrupt was created.
210
+ * @property resolvedAt - Epoch ms when the interrupt was resolved/cancelled;
211
+ * absent while pending.
212
+ */
213
+ export interface InterruptRecord {
214
+ interruptId: string
215
+ runId: string
216
+ threadId: string
217
+ status: InterruptStatus
218
+ requestedAt: number
219
+ resolvedAt?: number
220
+ payload: Record<string, unknown>
221
+ response?: unknown
222
+ }
223
+
224
+ /** Durable store for human-in-the-loop interrupts. */
225
+ export interface InterruptStore {
226
+ /**
227
+ * Persist a new interrupt in the `'pending'` state.
228
+ *
229
+ * The record is accepted without `status`/`resolvedAt` so a "born resolved"
230
+ * interrupt is unrepresentable — every interrupt begins pending and only
231
+ * `resolve`/`cancel` may move it to a terminal state.
232
+ *
233
+ * INVARIANT (insert-if-absent): if an interrupt with the same `interruptId`
234
+ * already exists, `create` is a **no-op** — it must NOT overwrite the
235
+ * existing record. This is the canonical behaviour (SQL backends implement it
236
+ * via `ON CONFLICT DO NOTHING` / upsert-with-empty-update), so a duplicate
237
+ * create can never clobber a resolved interrupt back to pending.
238
+ */
239
+ create: (
240
+ record: Omit<InterruptRecord, 'status' | 'resolvedAt'>,
241
+ ) => Promise<void>
242
+ /**
243
+ * Move an interrupt to `'resolved'`, stamping `resolvedAt` and storing
244
+ * `response`. A no-op if `interruptId` does not exist.
245
+ */
246
+ resolve: (interruptId: string, response?: unknown) => Promise<void>
247
+ /**
248
+ * Move an interrupt to `'cancelled'`, stamping `resolvedAt`. A no-op if
249
+ * `interruptId` does not exist.
250
+ */
251
+ cancel: (interruptId: string) => Promise<void>
252
+ /** Return the interrupt for `interruptId`, or `null` if none exists. */
253
+ get: (interruptId: string) => Promise<InterruptRecord | null>
254
+ /**
255
+ * All interrupts for a thread.
256
+ *
257
+ * INVARIANT: ordered by insertion (equivalently `requestedAt` ascending). SQL
258
+ * backends MUST `ORDER BY requested_at` — the middleware and testkit rely on
259
+ * this stable ordering.
260
+ */
261
+ list: (threadId: string) => Promise<Array<InterruptRecord>>
262
+ /** Pending interrupts for a thread, ordered by `requestedAt` ascending. */
263
+ listPending: (threadId: string) => Promise<Array<InterruptRecord>>
264
+ /** All interrupts for a run, ordered by `requestedAt` ascending. */
265
+ listByRun: (runId: string) => Promise<Array<InterruptRecord>>
266
+ /** Pending interrupts for a run, ordered by `requestedAt` ascending. */
267
+ listPendingByRun: (runId: string) => Promise<Array<InterruptRecord>>
268
+ }
269
+
270
+ /**
271
+ * Namespaced key/value store for arbitrary JSON metadata (app-owned).
272
+ *
273
+ * The first argument is an **app-defined namespace string**, not the shared
274
+ * {@link Scope} identity type from `@tanstack/ai`. Composite identity is
275
+ * `(namespace, key)` as two independent fields (SQL backends use a composite
276
+ * primary key; the in-memory store uses nested maps). Do not encode both into a
277
+ * single delimited string — `${namespace}:${key}` collides when either part
278
+ * contains `:`.
279
+ *
280
+ * The same `key` under different namespaces is independent.
281
+ */
282
+ export interface MetadataStore {
283
+ /**
284
+ * Return the stored value for `(namespace, key)`, or `null` if absent.
285
+ *
286
+ * CAVEAT: the return type is `unknown | null`, where `| null` collapses into
287
+ * `unknown` — a stored value of `null` is therefore **indistinguishable from
288
+ * absence** at the type level. Callers that must persist a real `null`
289
+ * distinctly from "not set" should wrap it (e.g. store `{ value: null }`).
290
+ */
291
+ get: (namespace: string, key: string) => Promise<unknown | null>
292
+ /** Insert or overwrite the value for `(namespace, key)`. */
293
+ set: (namespace: string, key: string, value: unknown) => Promise<void>
294
+ /**
295
+ * Remove `(namespace, key)`. A no-op if absent. Does not affect other
296
+ * namespaces.
297
+ */
298
+ delete: (namespace: string, key: string) => Promise<void>
299
+ }
300
+
301
+ // ===========================================================================
302
+ // Store typers
303
+ // ===========================================================================
304
+ //
305
+ // Identity helpers that type a store implementation inline: pass an object
306
+ // literal and get autocomplete + contract checking, with no separate
307
+ // `: MessageStore` return annotation. They compose into `defineAIPersistence`,
308
+ // which infers **exact presence** — a store you define becomes a defined,
309
+ // non-optional, autocompleted key on `persistence.stores`, and accessing a store
310
+ // you did not define is a compile error.
311
+ //
312
+ // ```ts
313
+ // const persistence = defineAIPersistence({
314
+ // stores: {
315
+ // messages: defineMessageStore({ loadThread, saveThread }),
316
+ // runs: defineRunStore({ createOrResume, update, get, findActiveRun }),
317
+ // },
318
+ // })
319
+ // persistence.stores.runs // RunStore (defined)
320
+ // persistence.stores.interrupts // compile error — not provided
321
+ // ```
322
+ //
323
+ // Presence is per STORE, not per method: every method of a store you define is
324
+ // required (see the evolution policy above). Omitting one is a compile error,
325
+ // not a partial store.
326
+
327
+ /** Type a {@link MessageStore} implementation inline. */
328
+ export function defineMessageStore(store: MessageStore): MessageStore {
329
+ return store
330
+ }
331
+ /** Type an {@link InterruptStore} implementation inline. */
332
+ export function defineInterruptStore(store: InterruptStore): InterruptStore {
333
+ return store
334
+ }
335
+ /** Type a {@link MetadataStore} implementation inline. */
336
+ export function defineMetadataStore(store: MetadataStore): MetadataStore {
337
+ return store
338
+ }
339
+ /** Type a {@link GenerationRunStore} implementation inline. */
340
+ export function defineGenerationRunStore(
341
+ store: GenerationRunStore,
342
+ ): GenerationRunStore {
343
+ return store
344
+ }
345
+ /** Type an {@link ArtifactStore} implementation inline. */
346
+ export function defineArtifactStore(store: ArtifactStore): ArtifactStore {
347
+ return store
348
+ }
349
+ /** Type a {@link BlobStore} implementation inline. */
350
+ export function defineBlobStore(store: BlobStore): BlobStore {
351
+ return store
352
+ }
353
+
354
+ /**
355
+ * Metadata row describing a persisted artifact (generated media, tool output).
356
+ *
357
+ * The bytes themselves live in a {@link BlobStore}; this record holds the
358
+ * descriptive metadata and an optional `sourceUrl` for reference-only
359
+ * backends.
360
+ *
361
+ * @property createdAt - Epoch ms. (Core's wire-facing `PersistedArtifactRef`
362
+ * exposes the same instant as an ISO string; see the timestamp convention.)
363
+ */
364
+ export interface ArtifactRecord {
365
+ artifactId: string
366
+ runId: string
367
+ threadId: string
368
+ /**
369
+ * The blob-store key these bytes actually live under.
370
+ *
371
+ * Optional for backwards compatibility: records written before this existed
372
+ * resolve via the default `artifacts/<runId>/<artifactId>` convention. New
373
+ * records always carry it, which is what lets `storageKey` put bytes anywhere
374
+ * — a reader can no longer recompute the path, so it has to be remembered.
375
+ * Use `resolveArtifactBlobKey(record)` rather than reading it directly.
376
+ */
377
+ blobKey?: string
378
+ name: string
379
+ mimeType: string
380
+ size: number
381
+ sourceUrl?: string
382
+ createdAt: number
383
+ }
384
+
385
+ /** Durable store for artifact metadata records. */
386
+ export interface ArtifactStore {
387
+ /** Insert or overwrite the artifact metadata record. */
388
+ save: (record: ArtifactRecord) => Promise<void>
389
+ /** Return the artifact for `artifactId`, or `null` if none exists. */
390
+ get: (artifactId: string) => Promise<ArtifactRecord | null>
391
+ /** All artifacts for a run. Returns `[]` when the run has none. */
392
+ list: (runId: string) => Promise<Array<ArtifactRecord>>
393
+ /**
394
+ * Delete a single artifact by id. A no-op if absent, mirroring
395
+ * {@link BlobStore.delete} — the two are written and deleted as a pair, so
396
+ * their contracts match.
397
+ */
398
+ delete: (artifactId: string) => Promise<void>
399
+ /**
400
+ * Delete every artifact belonging to `runId`. A no-op when the run has none.
401
+ *
402
+ * Required rather than feature-detected: retention and erasure are the point
403
+ * of storing media durably, and an adapter silently lacking deletion is
404
+ * indistinguishable from one where there was nothing to delete.
405
+ */
406
+ deleteForRun: (runId: string) => Promise<void>
407
+ }
408
+
409
+ /**
410
+ * Accepted body shapes for {@link BlobStore.put}. `ArrayBufferView` already
411
+ * covers `Uint8Array` and every other typed-array/`DataView`, so no separate
412
+ * `Uint8Array` member is needed.
413
+ */
414
+ export type BlobBody =
415
+ | ReadableStream<Uint8Array>
416
+ | ArrayBuffer
417
+ | ArrayBufferView
418
+ | string
419
+ | Blob
420
+
421
+ /**
422
+ * Metadata for a stored blob.
423
+ *
424
+ * @property size - Byte length, when known.
425
+ * @property createdAt - Epoch ms first written.
426
+ * @property updatedAt - Epoch ms last overwritten.
427
+ */
428
+ export interface BlobRecord {
429
+ key: string
430
+ size?: number
431
+ etag?: string
432
+ contentType?: string
433
+ customMetadata?: Record<string, string>
434
+ createdAt?: number
435
+ updatedAt?: number
436
+ }
437
+
438
+ /**
439
+ * A byte range to read, in the shape an HTTP `Range` header resolves to.
440
+ *
441
+ * `offset` is measured from the start of the object and must be inside it;
442
+ * `length` defaults to "everything from `offset` to the end" and is clamped to
443
+ * the end when it overshoots. Suffix ranges (`bytes=-500`) are the caller's to
444
+ * resolve against the known size — a serve route has the size on the artifact
445
+ * record, and has to compare against it anyway to answer `416` before reading.
446
+ */
447
+ export interface BlobRange {
448
+ offset: number
449
+ length?: number
450
+ }
451
+
452
+ /** Options for {@link BlobStore.get}. */
453
+ export interface BlobGetOptions {
454
+ /**
455
+ * Read only this slice of the object. `body`, `arrayBuffer()` and `text()`
456
+ * then cover the slice, `size` still reports the WHOLE object, and `range`
457
+ * reports the slice actually served — the three numbers a `206` response
458
+ * needs (`Content-Range: bytes <offset>-<offset+length-1>/<size>`).
459
+ */
460
+ range?: BlobRange
461
+ }
462
+
463
+ /** A stored blob's metadata plus lazy accessors for its bytes. */
464
+ export interface BlobObject extends BlobRecord {
465
+ arrayBuffer: () => Promise<ArrayBuffer>
466
+ text: () => Promise<string>
467
+ body?: ReadableStream<Uint8Array>
468
+ /**
469
+ * The slice this object exposes, when a {@link BlobGetOptions.range} was
470
+ * requested and honoured: `offset` as asked, `length` as actually served
471
+ * (clamped to the end of the object). Absent on a whole-object read.
472
+ */
473
+ range?: { offset: number; length: number }
474
+ }
475
+
476
+ /**
477
+ * One page of a {@link BlobStore.list} scan.
478
+ *
479
+ * @property cursor - Opaque continuation token; present only when `truncated`.
480
+ * @property truncated - `true` when more objects match beyond this page.
481
+ */
482
+ export interface BlobListPage {
483
+ objects: Array<BlobRecord>
484
+ cursor?: string
485
+ truncated?: boolean
486
+ }
487
+
488
+ export interface BlobPutOptions {
489
+ contentType?: string
490
+ customMetadata?: Record<string, string>
491
+ /**
492
+ * The exact byte length of `body`, when the producer knows it up front.
493
+ *
494
+ * Advisory, not a contract the store must honor: it exists so a store can
495
+ * pick an upload strategy knowingly instead of discovering the length by
496
+ * buffering. Most useful to an SDK that wants the length as a separate
497
+ * argument rather than reading it off the stream — S3's `PutObject`
498
+ * (`ContentLength`) is the archetype — and to a runtime that can re-attach
499
+ * one (workerd's `FixedLengthStream` ahead of `R2Bucket.put`).
500
+ *
501
+ * Only ever set when the length is exact — a wrong value is worse than none,
502
+ * since runtimes that enforce declared lengths fail the write. Absent means
503
+ * unknown, and a store must accept a length-less stream regardless:
504
+ * producers hand one over whenever the origin does not declare a length.
505
+ */
506
+ expectedLength?: number
507
+ }
508
+
509
+ export interface BlobListOptions {
510
+ prefix?: string
511
+ cursor?: string
512
+ limit?: number
513
+ }
514
+
515
+ /** Durable object/blob store (byte-storing or reference-only backends). */
516
+ export interface BlobStore {
517
+ /** Insert or overwrite the object at `key`, returning its metadata. */
518
+ put: (
519
+ key: string,
520
+ body: BlobBody,
521
+ options?: BlobPutOptions,
522
+ ) => Promise<BlobRecord>
523
+ /**
524
+ * Return the object at `key` (metadata + byte accessors), or `null`.
525
+ *
526
+ * RANGE SEMANTICS: with `options.range`, return only that slice — the bytes
527
+ * a `206` response carries — and report it back as `range`. `size` still
528
+ * reports the whole object, so the caller can build `Content-Range` without
529
+ * a second `head`. The reported `length` is what was actually served: a
530
+ * requested `length` past the end clamps. An `offset` at or past the end is
531
+ * a caller error, not a store one — the size is on the artifact record, so a
532
+ * serve route answers `416` before ever asking the store.
533
+ *
534
+ * Range support is part of the contract for any store that holds bytes (the
535
+ * conformance testkit asserts it): serving a whole file where a slice was
536
+ * asked for is what makes `<video>` seeking, and Safari playback at all,
537
+ * fail. A reference-only backend that stores no bytes skips `blobs`
538
+ * entirely rather than half-implementing it.
539
+ */
540
+ get: (key: string, options?: BlobGetOptions) => Promise<BlobObject | null>
541
+ /** Return only the metadata for `key`, or `null`. */
542
+ head: (key: string) => Promise<BlobRecord | null>
543
+ /** Remove the object at `key`. A no-op if absent. */
544
+ delete: (key: string) => Promise<void>
545
+ /**
546
+ * List objects, optionally filtered by `prefix`, in ascending key order.
547
+ *
548
+ * CURSOR SEMANTICS: `prefix` matches literally and case-sensitively (SQL
549
+ * backends must escape LIKE metacharacters, so `run_` matches only the exact
550
+ * bytes `run_`, not `_` as a wildcard). When `limit` is given and more keys
551
+ * match, the page is `truncated: true` with a `cursor`; passing that `cursor`
552
+ * back returns the strictly-following keys (keys `> cursor`). Cursor ordering
553
+ * is the same byte ordering as the sort, so paging visits every key exactly
554
+ * once with no gaps or repeats. `limit: 0` yields an empty, untruncated page
555
+ * with no cursor.
556
+ */
557
+ list: (options?: BlobListOptions) => Promise<BlobListPage>
558
+ }
559
+
560
+ /**
561
+ * Sparse bag of **state** store keys — composition / validation only.
562
+ *
563
+ * **Not a public product shape.** Prefer the named chat shapes below
564
+ * ({@link ChatTranscriptStores}, {@link ChatPersistenceStores},
565
+ * {@link ChatWithInterruptsStores}). Locks are not included — use
566
+ * `withLocks` from `@tanstack/ai`.
567
+ *
568
+ * @internal Exported from this module for generics; the package root does not
569
+ * re-export this type — use a named shape or `AIPersistence<{ … }>` instead.
570
+ */
571
+ export interface AIPersistenceStores {
572
+ messages?: MessageStore
573
+ runs?: RunStore
574
+ interrupts?: InterruptStore
575
+ metadata?: MetadataStore
576
+ generationRuns?: GenerationRunStore
577
+ artifacts?: ArtifactStore
578
+ blobs?: BlobStore
579
+ }
580
+
581
+ /**
582
+ * Chat floor: durable transcript. `messages` is required.
583
+ *
584
+ * `runs` / `interrupts` / `metadata` remain optional. If `interrupts` is set,
585
+ * `runs` is required (enforced by `withPersistence` / validators).
586
+ */
587
+ export interface ChatTranscriptStores {
588
+ messages: MessageStore
589
+ runs?: RunStore
590
+ interrupts?: InterruptStore
591
+ metadata?: MetadataStore
592
+ }
593
+
594
+ /**
595
+ * Full chat durability — all four state stores are present. This is what
596
+ * `memoryPersistence()` returns, and the shape most adapters should declare.
597
+ *
598
+ * Backends that only need a transcript should use
599
+ * {@link ChatTranscriptStores} instead.
600
+ */
601
+ export interface ChatPersistenceStores {
602
+ messages: MessageStore
603
+ runs: RunStore
604
+ interrupts: InterruptStore
605
+ metadata: MetadataStore
606
+ }
607
+
608
+ /**
609
+ * Chat with durable human-in-the-loop interrupts (and optional metadata).
610
+ * Implies `runs` (interrupt records are run-scoped).
611
+ *
612
+ * Prefer {@link ChatPersistenceStores} when you also have metadata (packaged
613
+ * backends). Use this when interrupts are required but metadata is not.
614
+ */
615
+ export interface ChatWithInterruptsStores {
616
+ messages: MessageStore
617
+ runs: RunStore
618
+ interrupts: InterruptStore
619
+ metadata?: MetadataStore
620
+ }
621
+
622
+ /**
623
+ * Persistence aggregate. Parameterize with a named store shape, or a sparse
624
+ * map for composition (`defineAIPersistence` / `composePersistence`).
625
+ *
626
+ * Default is the sparse bag so untyped / dynamic bags still type-check;
627
+ * prefer {@link ChatTranscriptPersistence} or {@link ChatPersistence} at
628
+ * call sites.
629
+ */
630
+ export interface AIPersistence<
631
+ TStores extends AIPersistenceStores = AIPersistenceStores,
632
+ > {
633
+ stores: ExactStoreKeys<TStores>
634
+ }
635
+
636
+ /** {@link AIPersistence} for {@link ChatTranscriptStores}. */
637
+ export type ChatTranscriptPersistence = AIPersistence<ChatTranscriptStores>
638
+
639
+ /** {@link AIPersistence} for {@link ChatPersistenceStores}. */
640
+ export type ChatPersistence = AIPersistence<ChatPersistenceStores>
641
+
642
+ /** {@link AIPersistence} for {@link ChatWithInterruptsStores}. */
643
+ export type ChatWithInterruptsPersistence =
644
+ AIPersistence<ChatWithInterruptsStores>
645
+
646
+ type StoreKey = keyof AIPersistenceStores
647
+ type ExactStoreKeys<TStores> =
648
+ Exclude<keyof TStores, StoreKey> extends never
649
+ ? TStores
650
+ : TStores & Record<Exclude<keyof TStores, StoreKey>, never>
651
+
652
+ export type AIPersistenceOverrides = {
653
+ [TKey in StoreKey]?: AIPersistenceStores[TKey] | false
654
+ }
655
+
656
+ type BaseStoreValue<
657
+ TBase extends AIPersistenceStores,
658
+ TKey extends StoreKey,
659
+ > = TKey extends keyof TBase ? TBase[TKey] : never
660
+
661
+ type OverrideStoreValue<
662
+ TOverrides extends AIPersistenceOverrides,
663
+ TKey extends StoreKey,
664
+ > = TKey extends keyof TOverrides ? TOverrides[TKey] : never
665
+
666
+ type ResolvedStoreValue<
667
+ TBase extends AIPersistenceStores,
668
+ TOverrides extends AIPersistenceOverrides,
669
+ TKey extends StoreKey,
670
+ > = TKey extends keyof TOverrides
671
+ ?
672
+ | Exclude<OverrideStoreValue<TOverrides, TKey>, false | undefined>
673
+ | (undefined extends OverrideStoreValue<TOverrides, TKey>
674
+ ? Exclude<BaseStoreValue<TBase, TKey>, undefined>
675
+ : never)
676
+ : Exclude<BaseStoreValue<TBase, TKey>, undefined>
677
+
678
+ type BaseStoreIsRequired<
679
+ TBase extends AIPersistenceStores,
680
+ TKey extends StoreKey,
681
+ > = TKey extends keyof TBase
682
+ ? object extends Pick<TBase, TKey>
683
+ ? false
684
+ : true
685
+ : false
686
+
687
+ type ResolvedStoreIsRequired<
688
+ TBase extends AIPersistenceStores,
689
+ TOverrides extends AIPersistenceOverrides,
690
+ TKey extends StoreKey,
691
+ > = TKey extends keyof TOverrides
692
+ ? false extends OverrideStoreValue<TOverrides, TKey>
693
+ ? false
694
+ : undefined extends OverrideStoreValue<TOverrides, TKey>
695
+ ? BaseStoreIsRequired<TBase, TKey>
696
+ : true
697
+ : BaseStoreIsRequired<TBase, TKey>
698
+
699
+ type ResolvedRequiredKeys<
700
+ TBase extends AIPersistenceStores,
701
+ TOverrides extends AIPersistenceOverrides,
702
+ > = {
703
+ [TKey in StoreKey]-?: [ResolvedStoreValue<TBase, TOverrides, TKey>] extends [
704
+ never,
705
+ ]
706
+ ? never
707
+ : ResolvedStoreIsRequired<TBase, TOverrides, TKey> extends true
708
+ ? TKey
709
+ : never
710
+ }[StoreKey]
711
+
712
+ type ResolvedOptionalKeys<
713
+ TBase extends AIPersistenceStores,
714
+ TOverrides extends AIPersistenceOverrides,
715
+ > = {
716
+ [TKey in StoreKey]-?: [ResolvedStoreValue<TBase, TOverrides, TKey>] extends [
717
+ never,
718
+ ]
719
+ ? never
720
+ : ResolvedStoreIsRequired<TBase, TOverrides, TKey> extends true
721
+ ? never
722
+ : TKey
723
+ }[StoreKey]
724
+
725
+ type Simplify<T> = { [TKey in keyof T]: T[TKey] }
726
+
727
+ export type ComposedAIPersistenceStores<
728
+ TBase extends AIPersistenceStores,
729
+ TOverrides extends AIPersistenceOverrides,
730
+ > = Simplify<
731
+ {
732
+ [TKey in ResolvedRequiredKeys<TBase, TOverrides>]: ResolvedStoreValue<
733
+ TBase,
734
+ TOverrides,
735
+ TKey
736
+ >
737
+ } & {
738
+ [TKey in ResolvedOptionalKeys<TBase, TOverrides>]?: ResolvedStoreValue<
739
+ TBase,
740
+ TOverrides,
741
+ TKey
742
+ >
743
+ }
744
+ >
745
+
746
+ const storeKeys = [
747
+ 'messages',
748
+ 'runs',
749
+ 'generationRuns',
750
+ 'interrupts',
751
+ 'metadata',
752
+ 'artifacts',
753
+ 'blobs',
754
+ ] satisfies Array<StoreKey>
755
+
756
+ const storeKeySet = new Set<string>(storeKeys)
757
+
758
+ function assertKnownStoreKeys(stores: object, location: string): void {
759
+ for (const key of Object.keys(stores)) {
760
+ if (!storeKeySet.has(key)) {
761
+ throw new Error(`Unknown AIPersistence ${location} key: ${key}`)
762
+ }
763
+ }
764
+ }
765
+
766
+ export function validatePersistenceStoreKeys(persistence: AIPersistence): void {
767
+ assertKnownStoreKeys(persistence.stores, 'store')
768
+ }
769
+
770
+ /**
771
+ * Chat middleware entrypoint rules:
772
+ * - `messages` is required (chat persistence means a durable transcript)
773
+ * - `interrupts` requires `runs` (interrupt records are run-scoped)
774
+ */
775
+ export function validateChatPersistenceStores(
776
+ persistence: AIPersistence,
777
+ ): void {
778
+ validatePersistenceStoreKeys(persistence)
779
+ if (!persistence.stores.messages) {
780
+ throw new Error('Chat persistence requires stores.messages.')
781
+ }
782
+ if (persistence.stores.interrupts && !persistence.stores.runs) {
783
+ throw new Error('Chat persistence stores.interrupts requires stores.runs.')
784
+ }
785
+ }
786
+
787
+ /**
788
+ * Generation middleware entrypoint rule: `generationRuns` is required (the
789
+ * generation run lifecycle is keyed on its own `runId`, not a chat conversation
790
+ * `threadId`). When artifact persistence is used, `artifacts` and `blobs` must
791
+ * be provided together.
792
+ */
793
+ export function validateGenerationPersistenceStores(
794
+ persistence: AIPersistence,
795
+ ): void {
796
+ validatePersistenceStoreKeys(persistence)
797
+ const hasArtifacts = persistence.stores.artifacts !== undefined
798
+ const hasBlobs = persistence.stores.blobs !== undefined
799
+ if (hasArtifacts !== hasBlobs) {
800
+ throw new Error(
801
+ 'Generation artifact persistence requires both stores.artifacts and stores.blobs.',
802
+ )
803
+ }
804
+ if (!persistence.stores.generationRuns) {
805
+ throw new Error('Generation persistence requires stores.generationRuns.')
806
+ }
807
+ }
808
+
809
+ /**
810
+ * Server hydrate entrypoint rule: `messages` is required.
811
+ */
812
+ export function validateReconstructChatStores(
813
+ persistence: AIPersistence,
814
+ ): void {
815
+ validatePersistenceStoreKeys(persistence)
816
+ if (!persistence.stores.messages) {
817
+ throw new Error('reconstructChat requires stores.messages.')
818
+ }
819
+ }
820
+
821
+ /**
822
+ * Server hydrate entrypoint rule for generation: `generationRuns` is required.
823
+ * The run store resolves the latest generation for a thread (or a specific run
824
+ * id), so a server-authoritative client can hydrate the last generation's
825
+ * status, result, and artifact refs on load.
826
+ */
827
+ export function validateReconstructGenerationStores(
828
+ persistence: AIPersistence,
829
+ ): void {
830
+ validatePersistenceStoreKeys(persistence)
831
+ if (!persistence.stores.generationRuns) {
832
+ throw new Error('reconstructGeneration requires stores.generationRuns.')
833
+ }
834
+ }
835
+
836
+ export function defineAIPersistence<TStores extends AIPersistenceStores>(
837
+ persistence: AIPersistence<ExactStoreKeys<TStores>>,
838
+ ): AIPersistence<TStores> {
839
+ validatePersistenceStoreKeys(persistence)
840
+ return persistence
841
+ }
842
+
843
+ export function composePersistence<
844
+ TBase extends AIPersistenceStores,
845
+ TOverrides extends AIPersistenceOverrides,
846
+ >(
847
+ base: AIPersistence<TBase>,
848
+ config: {
849
+ overrides: ExactStoreKeys<TOverrides>
850
+ },
851
+ ): AIPersistence<ComposedAIPersistenceStores<TBase, TOverrides>>
852
+ export function composePersistence(
853
+ base: AIPersistence,
854
+ config: { overrides: AIPersistenceOverrides },
855
+ ): AIPersistence {
856
+ validatePersistenceStoreKeys(base)
857
+ assertKnownStoreKeys(config.overrides, 'override')
858
+
859
+ const stores: AIPersistenceStores = { ...base.stores }
860
+ for (const key of storeKeys) {
861
+ if (!Object.prototype.hasOwnProperty.call(config.overrides, key)) continue
862
+ const override = config.overrides[key]
863
+ if (override === false) {
864
+ delete stores[key]
865
+ } else if (override !== undefined) {
866
+ setStore(stores, key, override)
867
+ }
868
+ }
869
+ return { stores }
870
+ }
871
+
872
+ function setStore<TKey extends StoreKey>(
873
+ stores: AIPersistenceStores,
874
+ key: TKey,
875
+ value: NonNullable<AIPersistenceStores[TKey]>,
876
+ ): void {
877
+ stores[key] = value
878
+ }