@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.
- package/dist/esm/blob-range.d.ts +51 -0
- package/dist/esm/blob-range.js +84 -0
- package/dist/esm/blob-range.js.map +1 -0
- package/dist/esm/capabilities.d.ts +5 -0
- package/dist/esm/capabilities.js +16 -0
- package/dist/esm/capabilities.js.map +1 -0
- package/dist/esm/index.d.ts +13 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/memory.d.ts +19 -0
- package/dist/esm/memory.js +319 -0
- package/dist/esm/memory.js.map +1 -0
- package/dist/esm/middleware.d.ts +252 -0
- package/dist/esm/middleware.js +872 -0
- package/dist/esm/middleware.js.map +1 -0
- package/dist/esm/reconstruct-generation.d.ts +129 -0
- package/dist/esm/reconstruct-generation.js +148 -0
- package/dist/esm/reconstruct-generation.js.map +1 -0
- package/dist/esm/reconstruct.d.ts +79 -0
- package/dist/esm/reconstruct.js +75 -0
- package/dist/esm/reconstruct.js.map +1 -0
- package/dist/esm/retrieve.d.ts +40 -0
- package/dist/esm/retrieve.js +54 -0
- package/dist/esm/retrieve.js.map +1 -0
- package/dist/esm/testkit/conformance.d.ts +33 -0
- package/dist/esm/testkit/conformance.js +997 -0
- package/dist/esm/testkit/conformance.js.map +1 -0
- package/dist/esm/types.d.ts +554 -0
- package/dist/esm/types.js +103 -0
- package/dist/esm/types.js.map +1 -0
- package/package.json +71 -0
- package/skills/ai-persistence/SKILL.md +218 -0
- package/skills/ai-persistence/build-cloudflare-adapter/SKILL.md +313 -0
- package/skills/ai-persistence/build-cloudflare-artifact-store/SKILL.md +693 -0
- package/skills/ai-persistence/build-custom-adapter/SKILL.md +328 -0
- package/skills/ai-persistence/build-drizzle-adapter/SKILL.md +562 -0
- package/skills/ai-persistence/build-prisma-adapter/SKILL.md +518 -0
- package/skills/ai-persistence/server/SKILL.md +210 -0
- package/skills/ai-persistence/stores/SKILL.md +485 -0
- package/src/blob-range.ts +101 -0
- package/src/capabilities.ts +18 -0
- package/src/index.ts +114 -0
- package/src/memory.ts +491 -0
- package/src/middleware.ts +1795 -0
- package/src/reconstruct-generation.ts +244 -0
- package/src/reconstruct.ts +149 -0
- package/src/retrieve.ts +77 -0
- package/src/testkit/conformance.ts +1288 -0
- package/src/types.ts +878 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { ModelMessage, PersistedArtifactRef, RunStatus, RunStore, Scope, TokenUsage } from '@tanstack/ai';
|
|
2
|
+
export type { Scope };
|
|
3
|
+
/**
|
|
4
|
+
* Durable store for a thread's full message transcript.
|
|
5
|
+
*
|
|
6
|
+
* A "thread" is the unit of conversation history. The key is
|
|
7
|
+
* {@link Scope.threadId} (the same conversation id as
|
|
8
|
+
* `ChatMiddlewareContext.threadId`). Store methods take a bare string for
|
|
9
|
+
* adapter simplicity; multi-user isolation is the **host's** job — authorize
|
|
10
|
+
* against `Scope.userId` / `Scope.tenantId` (derived server-side from session)
|
|
11
|
+
* before calling load/save, and never treat a client-supplied thread id alone
|
|
12
|
+
* as an ownership proof (see `Scope` security notes in `@tanstack/ai`).
|
|
13
|
+
*
|
|
14
|
+
* `saveThread` always receives and persists the **complete, authoritative**
|
|
15
|
+
* message list — it is an overwrite, never an append. The middleware snapshots
|
|
16
|
+
* `ctx.messages` (the full running transcript) into it.
|
|
17
|
+
*/
|
|
18
|
+
export interface MessageStore {
|
|
19
|
+
/**
|
|
20
|
+
* Return the full stored transcript for `threadId` ({@link Scope.threadId}),
|
|
21
|
+
* in insertion order.
|
|
22
|
+
*
|
|
23
|
+
* INVARIANT: returns an empty array (never `null`/`undefined`) for a thread
|
|
24
|
+
* that was never saved. Callers treat `[]` as "no history".
|
|
25
|
+
*/
|
|
26
|
+
loadThread: (threadId: string) => Promise<Array<ModelMessage>>;
|
|
27
|
+
/**
|
|
28
|
+
* Overwrite the stored transcript for `threadId` with `messages`.
|
|
29
|
+
*
|
|
30
|
+
* INVARIANT: this is a full replace. `messages` is the complete authoritative
|
|
31
|
+
* history; the previous contents are discarded (not merged or appended).
|
|
32
|
+
*/
|
|
33
|
+
saveThread: (threadId: string, messages: Array<ModelMessage>) => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export type { RunStatus, TerminalRunStatus, RunRecord, RunStore, } from '@tanstack/ai';
|
|
36
|
+
export { isTerminalRunStatus, defineRunStore } from '@tanstack/ai';
|
|
37
|
+
/**
|
|
38
|
+
* Lifecycle status of a generation run. Deliberately the same vocabulary as
|
|
39
|
+
* {@link RunStatus}, so an adapter that stores both kinds of run can share one
|
|
40
|
+
* status column and one set of checks.
|
|
41
|
+
*/
|
|
42
|
+
export type GenerationRunStatus = RunStatus;
|
|
43
|
+
/**
|
|
44
|
+
* A single generation run (one `generateImage` / `generateVideo` / … call).
|
|
45
|
+
*
|
|
46
|
+
* Its primary identity is `runId`: the run/request id the activity mints, the
|
|
47
|
+
* same AG-UI run id the client sends on the wire. `threadId` is the SLOT the
|
|
48
|
+
* run fills, a stable app-chosen name that groups successive runs of the same
|
|
49
|
+
* thing, and it is what a server-driven client hydrates by. Generation state is
|
|
50
|
+
* kept here, never in the chat {@link RunStore}.
|
|
51
|
+
*
|
|
52
|
+
* `result` holds terminal result METADATA (ids, model, urls, a provider video
|
|
53
|
+
* job id), never the media bytes — those live in a {@link BlobStore}.
|
|
54
|
+
* `artifacts` are the durable {@link PersistedArtifactRef}s, present only when
|
|
55
|
+
* byte storage is on.
|
|
56
|
+
*
|
|
57
|
+
* @property startedAt - Epoch ms when the run was first created.
|
|
58
|
+
* @property finishedAt - Epoch ms when the run reached a terminal status.
|
|
59
|
+
*/
|
|
60
|
+
export interface GenerationRunRecord {
|
|
61
|
+
runId: string;
|
|
62
|
+
/**
|
|
63
|
+
* The scope this run belongs to: a stable, app-chosen name for the slot
|
|
64
|
+
* successive runs fill (`product-123-hero`, `video-9-start-frame`).
|
|
65
|
+
*
|
|
66
|
+
* REQUIRED, per the store-contract rule at the top of this file.
|
|
67
|
+
* {@link GenerationRunStore.findLatestForThread} is the only query that
|
|
68
|
+
* hydrates a run, and it keys on this — so a record without one can be
|
|
69
|
+
* written and then never found again. `withGenerationPersistence` already
|
|
70
|
+
* refuses to start a run without a scope, and a server-driven client
|
|
71
|
+
* discards a snapshot that arrives without one, so an optional field here
|
|
72
|
+
* only described a record no path could produce and no client would accept.
|
|
73
|
+
*/
|
|
74
|
+
threadId: string;
|
|
75
|
+
/** `'image' | 'audio' | 'tts' | 'video' | 'transcription'`. */
|
|
76
|
+
activity: string;
|
|
77
|
+
provider: string;
|
|
78
|
+
model: string;
|
|
79
|
+
status: GenerationRunStatus;
|
|
80
|
+
startedAt: number;
|
|
81
|
+
finishedAt?: number;
|
|
82
|
+
error?: {
|
|
83
|
+
message: string;
|
|
84
|
+
code?: string;
|
|
85
|
+
};
|
|
86
|
+
/** Terminal result metadata (ids, model, urls). Never the media bytes. */
|
|
87
|
+
result?: unknown;
|
|
88
|
+
/** Durable artifact references, when an artifacts + blobs backend is used. */
|
|
89
|
+
artifacts?: Array<PersistedArtifactRef>;
|
|
90
|
+
usage?: TokenUsage;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Durable store for generation run records, the generation counterpart to
|
|
94
|
+
* {@link RunStore}. Keyed by its own `runId`, with `threadId` the slot
|
|
95
|
+
* {@link GenerationRunStore.findLatestForThread} looks runs up by.
|
|
96
|
+
*/
|
|
97
|
+
export interface GenerationRunStore {
|
|
98
|
+
/**
|
|
99
|
+
* Create a run record, or return the existing one if `runId` is already
|
|
100
|
+
* present (resume).
|
|
101
|
+
*
|
|
102
|
+
* INVARIANT (idempotency): a second call for a `runId` returns the existing
|
|
103
|
+
* record unchanged; `startedAt`/`activity`/`provider`/`model`/`threadId` are
|
|
104
|
+
* not mutated. `status` defaults to `'running'` on first creation.
|
|
105
|
+
*/
|
|
106
|
+
createOrResume: (input: Pick<GenerationRunRecord, 'runId' | 'threadId' | 'activity' | 'provider' | 'model' | 'startedAt'> & {
|
|
107
|
+
status?: GenerationRunStatus;
|
|
108
|
+
}) => Promise<GenerationRunRecord>;
|
|
109
|
+
/**
|
|
110
|
+
* Patch a run record's mutable fields.
|
|
111
|
+
*
|
|
112
|
+
* INVARIANT: patching a `runId` that does not exist is a **no-op** — it must
|
|
113
|
+
* not throw and must not create a record.
|
|
114
|
+
*/
|
|
115
|
+
update: (runId: string, patch: Partial<Pick<GenerationRunRecord, 'status' | 'finishedAt' | 'error' | 'result' | 'artifacts' | 'usage'>>) => Promise<void>;
|
|
116
|
+
/** Return the run record for `runId`, or `null` if none exists. */
|
|
117
|
+
get: (runId: string) => Promise<GenerationRunRecord | null>;
|
|
118
|
+
/**
|
|
119
|
+
* The most recent run linked to `threadId`, or `null`.
|
|
120
|
+
*
|
|
121
|
+
* REQUIRED, per the store-contract rule at the top of this file: a
|
|
122
|
+
* server-authoritative client hydrates by the stable thread id on every
|
|
123
|
+
* mount, so an adapter without this would be indistinguishable from one that
|
|
124
|
+
* legitimately has no run — `persistence: true` would silently restore
|
|
125
|
+
* nothing, forever. `null` is the correct answer only when the thread really
|
|
126
|
+
* has no runs. The chat parallel is {@link RunStore.findActiveRun}.
|
|
127
|
+
*/
|
|
128
|
+
findLatestForThread: (threadId: string) => Promise<GenerationRunRecord | null>;
|
|
129
|
+
}
|
|
130
|
+
/** Lifecycle status of a human-in-the-loop interrupt. */
|
|
131
|
+
export type InterruptStatus = 'pending' | 'resolved' | 'cancelled';
|
|
132
|
+
/**
|
|
133
|
+
* A human-in-the-loop interrupt (tool approval, client-tool input request, …).
|
|
134
|
+
*
|
|
135
|
+
* @property requestedAt - Epoch ms when the interrupt was created.
|
|
136
|
+
* @property resolvedAt - Epoch ms when the interrupt was resolved/cancelled;
|
|
137
|
+
* absent while pending.
|
|
138
|
+
*/
|
|
139
|
+
export interface InterruptRecord {
|
|
140
|
+
interruptId: string;
|
|
141
|
+
runId: string;
|
|
142
|
+
threadId: string;
|
|
143
|
+
status: InterruptStatus;
|
|
144
|
+
requestedAt: number;
|
|
145
|
+
resolvedAt?: number;
|
|
146
|
+
payload: Record<string, unknown>;
|
|
147
|
+
response?: unknown;
|
|
148
|
+
}
|
|
149
|
+
/** Durable store for human-in-the-loop interrupts. */
|
|
150
|
+
export interface InterruptStore {
|
|
151
|
+
/**
|
|
152
|
+
* Persist a new interrupt in the `'pending'` state.
|
|
153
|
+
*
|
|
154
|
+
* The record is accepted without `status`/`resolvedAt` so a "born resolved"
|
|
155
|
+
* interrupt is unrepresentable — every interrupt begins pending and only
|
|
156
|
+
* `resolve`/`cancel` may move it to a terminal state.
|
|
157
|
+
*
|
|
158
|
+
* INVARIANT (insert-if-absent): if an interrupt with the same `interruptId`
|
|
159
|
+
* already exists, `create` is a **no-op** — it must NOT overwrite the
|
|
160
|
+
* existing record. This is the canonical behaviour (SQL backends implement it
|
|
161
|
+
* via `ON CONFLICT DO NOTHING` / upsert-with-empty-update), so a duplicate
|
|
162
|
+
* create can never clobber a resolved interrupt back to pending.
|
|
163
|
+
*/
|
|
164
|
+
create: (record: Omit<InterruptRecord, 'status' | 'resolvedAt'>) => Promise<void>;
|
|
165
|
+
/**
|
|
166
|
+
* Move an interrupt to `'resolved'`, stamping `resolvedAt` and storing
|
|
167
|
+
* `response`. A no-op if `interruptId` does not exist.
|
|
168
|
+
*/
|
|
169
|
+
resolve: (interruptId: string, response?: unknown) => Promise<void>;
|
|
170
|
+
/**
|
|
171
|
+
* Move an interrupt to `'cancelled'`, stamping `resolvedAt`. A no-op if
|
|
172
|
+
* `interruptId` does not exist.
|
|
173
|
+
*/
|
|
174
|
+
cancel: (interruptId: string) => Promise<void>;
|
|
175
|
+
/** Return the interrupt for `interruptId`, or `null` if none exists. */
|
|
176
|
+
get: (interruptId: string) => Promise<InterruptRecord | null>;
|
|
177
|
+
/**
|
|
178
|
+
* All interrupts for a thread.
|
|
179
|
+
*
|
|
180
|
+
* INVARIANT: ordered by insertion (equivalently `requestedAt` ascending). SQL
|
|
181
|
+
* backends MUST `ORDER BY requested_at` — the middleware and testkit rely on
|
|
182
|
+
* this stable ordering.
|
|
183
|
+
*/
|
|
184
|
+
list: (threadId: string) => Promise<Array<InterruptRecord>>;
|
|
185
|
+
/** Pending interrupts for a thread, ordered by `requestedAt` ascending. */
|
|
186
|
+
listPending: (threadId: string) => Promise<Array<InterruptRecord>>;
|
|
187
|
+
/** All interrupts for a run, ordered by `requestedAt` ascending. */
|
|
188
|
+
listByRun: (runId: string) => Promise<Array<InterruptRecord>>;
|
|
189
|
+
/** Pending interrupts for a run, ordered by `requestedAt` ascending. */
|
|
190
|
+
listPendingByRun: (runId: string) => Promise<Array<InterruptRecord>>;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Namespaced key/value store for arbitrary JSON metadata (app-owned).
|
|
194
|
+
*
|
|
195
|
+
* The first argument is an **app-defined namespace string**, not the shared
|
|
196
|
+
* {@link Scope} identity type from `@tanstack/ai`. Composite identity is
|
|
197
|
+
* `(namespace, key)` as two independent fields (SQL backends use a composite
|
|
198
|
+
* primary key; the in-memory store uses nested maps). Do not encode both into a
|
|
199
|
+
* single delimited string — `${namespace}:${key}` collides when either part
|
|
200
|
+
* contains `:`.
|
|
201
|
+
*
|
|
202
|
+
* The same `key` under different namespaces is independent.
|
|
203
|
+
*/
|
|
204
|
+
export interface MetadataStore {
|
|
205
|
+
/**
|
|
206
|
+
* Return the stored value for `(namespace, key)`, or `null` if absent.
|
|
207
|
+
*
|
|
208
|
+
* CAVEAT: the return type is `unknown | null`, where `| null` collapses into
|
|
209
|
+
* `unknown` — a stored value of `null` is therefore **indistinguishable from
|
|
210
|
+
* absence** at the type level. Callers that must persist a real `null`
|
|
211
|
+
* distinctly from "not set" should wrap it (e.g. store `{ value: null }`).
|
|
212
|
+
*/
|
|
213
|
+
get: (namespace: string, key: string) => Promise<unknown | null>;
|
|
214
|
+
/** Insert or overwrite the value for `(namespace, key)`. */
|
|
215
|
+
set: (namespace: string, key: string, value: unknown) => Promise<void>;
|
|
216
|
+
/**
|
|
217
|
+
* Remove `(namespace, key)`. A no-op if absent. Does not affect other
|
|
218
|
+
* namespaces.
|
|
219
|
+
*/
|
|
220
|
+
delete: (namespace: string, key: string) => Promise<void>;
|
|
221
|
+
}
|
|
222
|
+
/** Type a {@link MessageStore} implementation inline. */
|
|
223
|
+
export declare function defineMessageStore(store: MessageStore): MessageStore;
|
|
224
|
+
/** Type an {@link InterruptStore} implementation inline. */
|
|
225
|
+
export declare function defineInterruptStore(store: InterruptStore): InterruptStore;
|
|
226
|
+
/** Type a {@link MetadataStore} implementation inline. */
|
|
227
|
+
export declare function defineMetadataStore(store: MetadataStore): MetadataStore;
|
|
228
|
+
/** Type a {@link GenerationRunStore} implementation inline. */
|
|
229
|
+
export declare function defineGenerationRunStore(store: GenerationRunStore): GenerationRunStore;
|
|
230
|
+
/** Type an {@link ArtifactStore} implementation inline. */
|
|
231
|
+
export declare function defineArtifactStore(store: ArtifactStore): ArtifactStore;
|
|
232
|
+
/** Type a {@link BlobStore} implementation inline. */
|
|
233
|
+
export declare function defineBlobStore(store: BlobStore): BlobStore;
|
|
234
|
+
/**
|
|
235
|
+
* Metadata row describing a persisted artifact (generated media, tool output).
|
|
236
|
+
*
|
|
237
|
+
* The bytes themselves live in a {@link BlobStore}; this record holds the
|
|
238
|
+
* descriptive metadata and an optional `sourceUrl` for reference-only
|
|
239
|
+
* backends.
|
|
240
|
+
*
|
|
241
|
+
* @property createdAt - Epoch ms. (Core's wire-facing `PersistedArtifactRef`
|
|
242
|
+
* exposes the same instant as an ISO string; see the timestamp convention.)
|
|
243
|
+
*/
|
|
244
|
+
export interface ArtifactRecord {
|
|
245
|
+
artifactId: string;
|
|
246
|
+
runId: string;
|
|
247
|
+
threadId: string;
|
|
248
|
+
/**
|
|
249
|
+
* The blob-store key these bytes actually live under.
|
|
250
|
+
*
|
|
251
|
+
* Optional for backwards compatibility: records written before this existed
|
|
252
|
+
* resolve via the default `artifacts/<runId>/<artifactId>` convention. New
|
|
253
|
+
* records always carry it, which is what lets `storageKey` put bytes anywhere
|
|
254
|
+
* — a reader can no longer recompute the path, so it has to be remembered.
|
|
255
|
+
* Use `resolveArtifactBlobKey(record)` rather than reading it directly.
|
|
256
|
+
*/
|
|
257
|
+
blobKey?: string;
|
|
258
|
+
name: string;
|
|
259
|
+
mimeType: string;
|
|
260
|
+
size: number;
|
|
261
|
+
sourceUrl?: string;
|
|
262
|
+
createdAt: number;
|
|
263
|
+
}
|
|
264
|
+
/** Durable store for artifact metadata records. */
|
|
265
|
+
export interface ArtifactStore {
|
|
266
|
+
/** Insert or overwrite the artifact metadata record. */
|
|
267
|
+
save: (record: ArtifactRecord) => Promise<void>;
|
|
268
|
+
/** Return the artifact for `artifactId`, or `null` if none exists. */
|
|
269
|
+
get: (artifactId: string) => Promise<ArtifactRecord | null>;
|
|
270
|
+
/** All artifacts for a run. Returns `[]` when the run has none. */
|
|
271
|
+
list: (runId: string) => Promise<Array<ArtifactRecord>>;
|
|
272
|
+
/**
|
|
273
|
+
* Delete a single artifact by id. A no-op if absent, mirroring
|
|
274
|
+
* {@link BlobStore.delete} — the two are written and deleted as a pair, so
|
|
275
|
+
* their contracts match.
|
|
276
|
+
*/
|
|
277
|
+
delete: (artifactId: string) => Promise<void>;
|
|
278
|
+
/**
|
|
279
|
+
* Delete every artifact belonging to `runId`. A no-op when the run has none.
|
|
280
|
+
*
|
|
281
|
+
* Required rather than feature-detected: retention and erasure are the point
|
|
282
|
+
* of storing media durably, and an adapter silently lacking deletion is
|
|
283
|
+
* indistinguishable from one where there was nothing to delete.
|
|
284
|
+
*/
|
|
285
|
+
deleteForRun: (runId: string) => Promise<void>;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Accepted body shapes for {@link BlobStore.put}. `ArrayBufferView` already
|
|
289
|
+
* covers `Uint8Array` and every other typed-array/`DataView`, so no separate
|
|
290
|
+
* `Uint8Array` member is needed.
|
|
291
|
+
*/
|
|
292
|
+
export type BlobBody = ReadableStream<Uint8Array> | ArrayBuffer | ArrayBufferView | string | Blob;
|
|
293
|
+
/**
|
|
294
|
+
* Metadata for a stored blob.
|
|
295
|
+
*
|
|
296
|
+
* @property size - Byte length, when known.
|
|
297
|
+
* @property createdAt - Epoch ms first written.
|
|
298
|
+
* @property updatedAt - Epoch ms last overwritten.
|
|
299
|
+
*/
|
|
300
|
+
export interface BlobRecord {
|
|
301
|
+
key: string;
|
|
302
|
+
size?: number;
|
|
303
|
+
etag?: string;
|
|
304
|
+
contentType?: string;
|
|
305
|
+
customMetadata?: Record<string, string>;
|
|
306
|
+
createdAt?: number;
|
|
307
|
+
updatedAt?: number;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* A byte range to read, in the shape an HTTP `Range` header resolves to.
|
|
311
|
+
*
|
|
312
|
+
* `offset` is measured from the start of the object and must be inside it;
|
|
313
|
+
* `length` defaults to "everything from `offset` to the end" and is clamped to
|
|
314
|
+
* the end when it overshoots. Suffix ranges (`bytes=-500`) are the caller's to
|
|
315
|
+
* resolve against the known size — a serve route has the size on the artifact
|
|
316
|
+
* record, and has to compare against it anyway to answer `416` before reading.
|
|
317
|
+
*/
|
|
318
|
+
export interface BlobRange {
|
|
319
|
+
offset: number;
|
|
320
|
+
length?: number;
|
|
321
|
+
}
|
|
322
|
+
/** Options for {@link BlobStore.get}. */
|
|
323
|
+
export interface BlobGetOptions {
|
|
324
|
+
/**
|
|
325
|
+
* Read only this slice of the object. `body`, `arrayBuffer()` and `text()`
|
|
326
|
+
* then cover the slice, `size` still reports the WHOLE object, and `range`
|
|
327
|
+
* reports the slice actually served — the three numbers a `206` response
|
|
328
|
+
* needs (`Content-Range: bytes <offset>-<offset+length-1>/<size>`).
|
|
329
|
+
*/
|
|
330
|
+
range?: BlobRange;
|
|
331
|
+
}
|
|
332
|
+
/** A stored blob's metadata plus lazy accessors for its bytes. */
|
|
333
|
+
export interface BlobObject extends BlobRecord {
|
|
334
|
+
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
335
|
+
text: () => Promise<string>;
|
|
336
|
+
body?: ReadableStream<Uint8Array>;
|
|
337
|
+
/**
|
|
338
|
+
* The slice this object exposes, when a {@link BlobGetOptions.range} was
|
|
339
|
+
* requested and honoured: `offset` as asked, `length` as actually served
|
|
340
|
+
* (clamped to the end of the object). Absent on a whole-object read.
|
|
341
|
+
*/
|
|
342
|
+
range?: {
|
|
343
|
+
offset: number;
|
|
344
|
+
length: number;
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* One page of a {@link BlobStore.list} scan.
|
|
349
|
+
*
|
|
350
|
+
* @property cursor - Opaque continuation token; present only when `truncated`.
|
|
351
|
+
* @property truncated - `true` when more objects match beyond this page.
|
|
352
|
+
*/
|
|
353
|
+
export interface BlobListPage {
|
|
354
|
+
objects: Array<BlobRecord>;
|
|
355
|
+
cursor?: string;
|
|
356
|
+
truncated?: boolean;
|
|
357
|
+
}
|
|
358
|
+
export interface BlobPutOptions {
|
|
359
|
+
contentType?: string;
|
|
360
|
+
customMetadata?: Record<string, string>;
|
|
361
|
+
/**
|
|
362
|
+
* The exact byte length of `body`, when the producer knows it up front.
|
|
363
|
+
*
|
|
364
|
+
* Advisory, not a contract the store must honor: it exists so a store can
|
|
365
|
+
* pick an upload strategy knowingly instead of discovering the length by
|
|
366
|
+
* buffering. Most useful to an SDK that wants the length as a separate
|
|
367
|
+
* argument rather than reading it off the stream — S3's `PutObject`
|
|
368
|
+
* (`ContentLength`) is the archetype — and to a runtime that can re-attach
|
|
369
|
+
* one (workerd's `FixedLengthStream` ahead of `R2Bucket.put`).
|
|
370
|
+
*
|
|
371
|
+
* Only ever set when the length is exact — a wrong value is worse than none,
|
|
372
|
+
* since runtimes that enforce declared lengths fail the write. Absent means
|
|
373
|
+
* unknown, and a store must accept a length-less stream regardless:
|
|
374
|
+
* producers hand one over whenever the origin does not declare a length.
|
|
375
|
+
*/
|
|
376
|
+
expectedLength?: number;
|
|
377
|
+
}
|
|
378
|
+
export interface BlobListOptions {
|
|
379
|
+
prefix?: string;
|
|
380
|
+
cursor?: string;
|
|
381
|
+
limit?: number;
|
|
382
|
+
}
|
|
383
|
+
/** Durable object/blob store (byte-storing or reference-only backends). */
|
|
384
|
+
export interface BlobStore {
|
|
385
|
+
/** Insert or overwrite the object at `key`, returning its metadata. */
|
|
386
|
+
put: (key: string, body: BlobBody, options?: BlobPutOptions) => Promise<BlobRecord>;
|
|
387
|
+
/**
|
|
388
|
+
* Return the object at `key` (metadata + byte accessors), or `null`.
|
|
389
|
+
*
|
|
390
|
+
* RANGE SEMANTICS: with `options.range`, return only that slice — the bytes
|
|
391
|
+
* a `206` response carries — and report it back as `range`. `size` still
|
|
392
|
+
* reports the whole object, so the caller can build `Content-Range` without
|
|
393
|
+
* a second `head`. The reported `length` is what was actually served: a
|
|
394
|
+
* requested `length` past the end clamps. An `offset` at or past the end is
|
|
395
|
+
* a caller error, not a store one — the size is on the artifact record, so a
|
|
396
|
+
* serve route answers `416` before ever asking the store.
|
|
397
|
+
*
|
|
398
|
+
* Range support is part of the contract for any store that holds bytes (the
|
|
399
|
+
* conformance testkit asserts it): serving a whole file where a slice was
|
|
400
|
+
* asked for is what makes `<video>` seeking, and Safari playback at all,
|
|
401
|
+
* fail. A reference-only backend that stores no bytes skips `blobs`
|
|
402
|
+
* entirely rather than half-implementing it.
|
|
403
|
+
*/
|
|
404
|
+
get: (key: string, options?: BlobGetOptions) => Promise<BlobObject | null>;
|
|
405
|
+
/** Return only the metadata for `key`, or `null`. */
|
|
406
|
+
head: (key: string) => Promise<BlobRecord | null>;
|
|
407
|
+
/** Remove the object at `key`. A no-op if absent. */
|
|
408
|
+
delete: (key: string) => Promise<void>;
|
|
409
|
+
/**
|
|
410
|
+
* List objects, optionally filtered by `prefix`, in ascending key order.
|
|
411
|
+
*
|
|
412
|
+
* CURSOR SEMANTICS: `prefix` matches literally and case-sensitively (SQL
|
|
413
|
+
* backends must escape LIKE metacharacters, so `run_` matches only the exact
|
|
414
|
+
* bytes `run_`, not `_` as a wildcard). When `limit` is given and more keys
|
|
415
|
+
* match, the page is `truncated: true` with a `cursor`; passing that `cursor`
|
|
416
|
+
* back returns the strictly-following keys (keys `> cursor`). Cursor ordering
|
|
417
|
+
* is the same byte ordering as the sort, so paging visits every key exactly
|
|
418
|
+
* once with no gaps or repeats. `limit: 0` yields an empty, untruncated page
|
|
419
|
+
* with no cursor.
|
|
420
|
+
*/
|
|
421
|
+
list: (options?: BlobListOptions) => Promise<BlobListPage>;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Sparse bag of **state** store keys — composition / validation only.
|
|
425
|
+
*
|
|
426
|
+
* **Not a public product shape.** Prefer the named chat shapes below
|
|
427
|
+
* ({@link ChatTranscriptStores}, {@link ChatPersistenceStores},
|
|
428
|
+
* {@link ChatWithInterruptsStores}). Locks are not included — use
|
|
429
|
+
* `withLocks` from `@tanstack/ai`.
|
|
430
|
+
*
|
|
431
|
+
* @internal Exported from this module for generics; the package root does not
|
|
432
|
+
* re-export this type — use a named shape or `AIPersistence<{ … }>` instead.
|
|
433
|
+
*/
|
|
434
|
+
export interface AIPersistenceStores {
|
|
435
|
+
messages?: MessageStore;
|
|
436
|
+
runs?: RunStore;
|
|
437
|
+
interrupts?: InterruptStore;
|
|
438
|
+
metadata?: MetadataStore;
|
|
439
|
+
generationRuns?: GenerationRunStore;
|
|
440
|
+
artifacts?: ArtifactStore;
|
|
441
|
+
blobs?: BlobStore;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Chat floor: durable transcript. `messages` is required.
|
|
445
|
+
*
|
|
446
|
+
* `runs` / `interrupts` / `metadata` remain optional. If `interrupts` is set,
|
|
447
|
+
* `runs` is required (enforced by `withPersistence` / validators).
|
|
448
|
+
*/
|
|
449
|
+
export interface ChatTranscriptStores {
|
|
450
|
+
messages: MessageStore;
|
|
451
|
+
runs?: RunStore;
|
|
452
|
+
interrupts?: InterruptStore;
|
|
453
|
+
metadata?: MetadataStore;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Full chat durability — all four state stores are present. This is what
|
|
457
|
+
* `memoryPersistence()` returns, and the shape most adapters should declare.
|
|
458
|
+
*
|
|
459
|
+
* Backends that only need a transcript should use
|
|
460
|
+
* {@link ChatTranscriptStores} instead.
|
|
461
|
+
*/
|
|
462
|
+
export interface ChatPersistenceStores {
|
|
463
|
+
messages: MessageStore;
|
|
464
|
+
runs: RunStore;
|
|
465
|
+
interrupts: InterruptStore;
|
|
466
|
+
metadata: MetadataStore;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Chat with durable human-in-the-loop interrupts (and optional metadata).
|
|
470
|
+
* Implies `runs` (interrupt records are run-scoped).
|
|
471
|
+
*
|
|
472
|
+
* Prefer {@link ChatPersistenceStores} when you also have metadata (packaged
|
|
473
|
+
* backends). Use this when interrupts are required but metadata is not.
|
|
474
|
+
*/
|
|
475
|
+
export interface ChatWithInterruptsStores {
|
|
476
|
+
messages: MessageStore;
|
|
477
|
+
runs: RunStore;
|
|
478
|
+
interrupts: InterruptStore;
|
|
479
|
+
metadata?: MetadataStore;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Persistence aggregate. Parameterize with a named store shape, or a sparse
|
|
483
|
+
* map for composition (`defineAIPersistence` / `composePersistence`).
|
|
484
|
+
*
|
|
485
|
+
* Default is the sparse bag so untyped / dynamic bags still type-check;
|
|
486
|
+
* prefer {@link ChatTranscriptPersistence} or {@link ChatPersistence} at
|
|
487
|
+
* call sites.
|
|
488
|
+
*/
|
|
489
|
+
export interface AIPersistence<TStores extends AIPersistenceStores = AIPersistenceStores> {
|
|
490
|
+
stores: ExactStoreKeys<TStores>;
|
|
491
|
+
}
|
|
492
|
+
/** {@link AIPersistence} for {@link ChatTranscriptStores}. */
|
|
493
|
+
export type ChatTranscriptPersistence = AIPersistence<ChatTranscriptStores>;
|
|
494
|
+
/** {@link AIPersistence} for {@link ChatPersistenceStores}. */
|
|
495
|
+
export type ChatPersistence = AIPersistence<ChatPersistenceStores>;
|
|
496
|
+
/** {@link AIPersistence} for {@link ChatWithInterruptsStores}. */
|
|
497
|
+
export type ChatWithInterruptsPersistence = AIPersistence<ChatWithInterruptsStores>;
|
|
498
|
+
type StoreKey = keyof AIPersistenceStores;
|
|
499
|
+
type ExactStoreKeys<TStores> = Exclude<keyof TStores, StoreKey> extends never ? TStores : TStores & Record<Exclude<keyof TStores, StoreKey>, never>;
|
|
500
|
+
export type AIPersistenceOverrides = {
|
|
501
|
+
[TKey in StoreKey]?: AIPersistenceStores[TKey] | false;
|
|
502
|
+
};
|
|
503
|
+
type BaseStoreValue<TBase extends AIPersistenceStores, TKey extends StoreKey> = TKey extends keyof TBase ? TBase[TKey] : never;
|
|
504
|
+
type OverrideStoreValue<TOverrides extends AIPersistenceOverrides, TKey extends StoreKey> = TKey extends keyof TOverrides ? TOverrides[TKey] : never;
|
|
505
|
+
type ResolvedStoreValue<TBase extends AIPersistenceStores, TOverrides extends AIPersistenceOverrides, TKey extends StoreKey> = TKey extends keyof TOverrides ? Exclude<OverrideStoreValue<TOverrides, TKey>, false | undefined> | (undefined extends OverrideStoreValue<TOverrides, TKey> ? Exclude<BaseStoreValue<TBase, TKey>, undefined> : never) : Exclude<BaseStoreValue<TBase, TKey>, undefined>;
|
|
506
|
+
type BaseStoreIsRequired<TBase extends AIPersistenceStores, TKey extends StoreKey> = TKey extends keyof TBase ? object extends Pick<TBase, TKey> ? false : true : false;
|
|
507
|
+
type ResolvedStoreIsRequired<TBase extends AIPersistenceStores, TOverrides extends AIPersistenceOverrides, TKey extends StoreKey> = TKey extends keyof TOverrides ? false extends OverrideStoreValue<TOverrides, TKey> ? false : undefined extends OverrideStoreValue<TOverrides, TKey> ? BaseStoreIsRequired<TBase, TKey> : true : BaseStoreIsRequired<TBase, TKey>;
|
|
508
|
+
type ResolvedRequiredKeys<TBase extends AIPersistenceStores, TOverrides extends AIPersistenceOverrides> = {
|
|
509
|
+
[TKey in StoreKey]-?: [ResolvedStoreValue<TBase, TOverrides, TKey>] extends [
|
|
510
|
+
never
|
|
511
|
+
] ? never : ResolvedStoreIsRequired<TBase, TOverrides, TKey> extends true ? TKey : never;
|
|
512
|
+
}[StoreKey];
|
|
513
|
+
type ResolvedOptionalKeys<TBase extends AIPersistenceStores, TOverrides extends AIPersistenceOverrides> = {
|
|
514
|
+
[TKey in StoreKey]-?: [ResolvedStoreValue<TBase, TOverrides, TKey>] extends [
|
|
515
|
+
never
|
|
516
|
+
] ? never : ResolvedStoreIsRequired<TBase, TOverrides, TKey> extends true ? never : TKey;
|
|
517
|
+
}[StoreKey];
|
|
518
|
+
type Simplify<T> = {
|
|
519
|
+
[TKey in keyof T]: T[TKey];
|
|
520
|
+
};
|
|
521
|
+
export type ComposedAIPersistenceStores<TBase extends AIPersistenceStores, TOverrides extends AIPersistenceOverrides> = Simplify<{
|
|
522
|
+
[TKey in ResolvedRequiredKeys<TBase, TOverrides>]: ResolvedStoreValue<TBase, TOverrides, TKey>;
|
|
523
|
+
} & {
|
|
524
|
+
[TKey in ResolvedOptionalKeys<TBase, TOverrides>]?: ResolvedStoreValue<TBase, TOverrides, TKey>;
|
|
525
|
+
}>;
|
|
526
|
+
export declare function validatePersistenceStoreKeys(persistence: AIPersistence): void;
|
|
527
|
+
/**
|
|
528
|
+
* Chat middleware entrypoint rules:
|
|
529
|
+
* - `messages` is required (chat persistence means a durable transcript)
|
|
530
|
+
* - `interrupts` requires `runs` (interrupt records are run-scoped)
|
|
531
|
+
*/
|
|
532
|
+
export declare function validateChatPersistenceStores(persistence: AIPersistence): void;
|
|
533
|
+
/**
|
|
534
|
+
* Generation middleware entrypoint rule: `generationRuns` is required (the
|
|
535
|
+
* generation run lifecycle is keyed on its own `runId`, not a chat conversation
|
|
536
|
+
* `threadId`). When artifact persistence is used, `artifacts` and `blobs` must
|
|
537
|
+
* be provided together.
|
|
538
|
+
*/
|
|
539
|
+
export declare function validateGenerationPersistenceStores(persistence: AIPersistence): void;
|
|
540
|
+
/**
|
|
541
|
+
* Server hydrate entrypoint rule: `messages` is required.
|
|
542
|
+
*/
|
|
543
|
+
export declare function validateReconstructChatStores(persistence: AIPersistence): void;
|
|
544
|
+
/**
|
|
545
|
+
* Server hydrate entrypoint rule for generation: `generationRuns` is required.
|
|
546
|
+
* The run store resolves the latest generation for a thread (or a specific run
|
|
547
|
+
* id), so a server-authoritative client can hydrate the last generation's
|
|
548
|
+
* status, result, and artifact refs on load.
|
|
549
|
+
*/
|
|
550
|
+
export declare function validateReconstructGenerationStores(persistence: AIPersistence): void;
|
|
551
|
+
export declare function defineAIPersistence<TStores extends AIPersistenceStores>(persistence: AIPersistence<ExactStoreKeys<TStores>>): AIPersistence<TStores>;
|
|
552
|
+
export declare function composePersistence<TBase extends AIPersistenceStores, TOverrides extends AIPersistenceOverrides>(base: AIPersistence<TBase>, config: {
|
|
553
|
+
overrides: ExactStoreKeys<TOverrides>;
|
|
554
|
+
}): AIPersistence<ComposedAIPersistenceStores<TBase, TOverrides>>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { defineRunStore, isTerminalRunStatus } from "@tanstack/ai";
|
|
2
|
+
//#region src/types.ts
|
|
3
|
+
/** Type a {@link MessageStore} implementation inline. */
|
|
4
|
+
function defineMessageStore(store) {
|
|
5
|
+
return store;
|
|
6
|
+
}
|
|
7
|
+
/** Type an {@link InterruptStore} implementation inline. */
|
|
8
|
+
function defineInterruptStore(store) {
|
|
9
|
+
return store;
|
|
10
|
+
}
|
|
11
|
+
/** Type a {@link MetadataStore} implementation inline. */
|
|
12
|
+
function defineMetadataStore(store) {
|
|
13
|
+
return store;
|
|
14
|
+
}
|
|
15
|
+
/** Type a {@link GenerationRunStore} implementation inline. */
|
|
16
|
+
function defineGenerationRunStore(store) {
|
|
17
|
+
return store;
|
|
18
|
+
}
|
|
19
|
+
/** Type an {@link ArtifactStore} implementation inline. */
|
|
20
|
+
function defineArtifactStore(store) {
|
|
21
|
+
return store;
|
|
22
|
+
}
|
|
23
|
+
/** Type a {@link BlobStore} implementation inline. */
|
|
24
|
+
function defineBlobStore(store) {
|
|
25
|
+
return store;
|
|
26
|
+
}
|
|
27
|
+
var storeKeys = [
|
|
28
|
+
"messages",
|
|
29
|
+
"runs",
|
|
30
|
+
"generationRuns",
|
|
31
|
+
"interrupts",
|
|
32
|
+
"metadata",
|
|
33
|
+
"artifacts",
|
|
34
|
+
"blobs"
|
|
35
|
+
];
|
|
36
|
+
var storeKeySet = new Set(storeKeys);
|
|
37
|
+
function assertKnownStoreKeys(stores, location) {
|
|
38
|
+
for (const key of Object.keys(stores)) if (!storeKeySet.has(key)) throw new Error(`Unknown AIPersistence ${location} key: ${key}`);
|
|
39
|
+
}
|
|
40
|
+
function validatePersistenceStoreKeys(persistence) {
|
|
41
|
+
assertKnownStoreKeys(persistence.stores, "store");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Chat middleware entrypoint rules:
|
|
45
|
+
* - `messages` is required (chat persistence means a durable transcript)
|
|
46
|
+
* - `interrupts` requires `runs` (interrupt records are run-scoped)
|
|
47
|
+
*/
|
|
48
|
+
function validateChatPersistenceStores(persistence) {
|
|
49
|
+
validatePersistenceStoreKeys(persistence);
|
|
50
|
+
if (!persistence.stores.messages) throw new Error("Chat persistence requires stores.messages.");
|
|
51
|
+
if (persistence.stores.interrupts && !persistence.stores.runs) throw new Error("Chat persistence stores.interrupts requires stores.runs.");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Generation middleware entrypoint rule: `generationRuns` is required (the
|
|
55
|
+
* generation run lifecycle is keyed on its own `runId`, not a chat conversation
|
|
56
|
+
* `threadId`). When artifact persistence is used, `artifacts` and `blobs` must
|
|
57
|
+
* be provided together.
|
|
58
|
+
*/
|
|
59
|
+
function validateGenerationPersistenceStores(persistence) {
|
|
60
|
+
validatePersistenceStoreKeys(persistence);
|
|
61
|
+
if (persistence.stores.artifacts !== void 0 !== (persistence.stores.blobs !== void 0)) throw new Error("Generation artifact persistence requires both stores.artifacts and stores.blobs.");
|
|
62
|
+
if (!persistence.stores.generationRuns) throw new Error("Generation persistence requires stores.generationRuns.");
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Server hydrate entrypoint rule: `messages` is required.
|
|
66
|
+
*/
|
|
67
|
+
function validateReconstructChatStores(persistence) {
|
|
68
|
+
validatePersistenceStoreKeys(persistence);
|
|
69
|
+
if (!persistence.stores.messages) throw new Error("reconstructChat requires stores.messages.");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Server hydrate entrypoint rule for generation: `generationRuns` is required.
|
|
73
|
+
* The run store resolves the latest generation for a thread (or a specific run
|
|
74
|
+
* id), so a server-authoritative client can hydrate the last generation's
|
|
75
|
+
* status, result, and artifact refs on load.
|
|
76
|
+
*/
|
|
77
|
+
function validateReconstructGenerationStores(persistence) {
|
|
78
|
+
validatePersistenceStoreKeys(persistence);
|
|
79
|
+
if (!persistence.stores.generationRuns) throw new Error("reconstructGeneration requires stores.generationRuns.");
|
|
80
|
+
}
|
|
81
|
+
function defineAIPersistence(persistence) {
|
|
82
|
+
validatePersistenceStoreKeys(persistence);
|
|
83
|
+
return persistence;
|
|
84
|
+
}
|
|
85
|
+
function composePersistence(base, config) {
|
|
86
|
+
validatePersistenceStoreKeys(base);
|
|
87
|
+
assertKnownStoreKeys(config.overrides, "override");
|
|
88
|
+
const stores = { ...base.stores };
|
|
89
|
+
for (const key of storeKeys) {
|
|
90
|
+
if (!Object.prototype.hasOwnProperty.call(config.overrides, key)) continue;
|
|
91
|
+
const override = config.overrides[key];
|
|
92
|
+
if (override === false) delete stores[key];
|
|
93
|
+
else if (override !== void 0) setStore(stores, key, override);
|
|
94
|
+
}
|
|
95
|
+
return { stores };
|
|
96
|
+
}
|
|
97
|
+
function setStore(stores, key, value) {
|
|
98
|
+
stores[key] = value;
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
export { composePersistence, defineAIPersistence, defineArtifactStore, defineBlobStore, defineGenerationRunStore, defineInterruptStore, defineMessageStore, defineMetadataStore, defineRunStore, isTerminalRunStatus, validateChatPersistenceStores, validateGenerationPersistenceStores, validatePersistenceStoreKeys, validateReconstructChatStores, validateReconstructGenerationStores };
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=types.js.map
|