@tanstack/ai-persistence 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/esm/blob-range.d.ts +51 -0
  2. package/dist/esm/blob-range.js +84 -0
  3. package/dist/esm/blob-range.js.map +1 -0
  4. package/dist/esm/capabilities.d.ts +5 -0
  5. package/dist/esm/capabilities.js +16 -0
  6. package/dist/esm/capabilities.js.map +1 -0
  7. package/dist/esm/index.d.ts +13 -0
  8. package/dist/esm/index.js +9 -0
  9. package/dist/esm/memory.d.ts +19 -0
  10. package/dist/esm/memory.js +319 -0
  11. package/dist/esm/memory.js.map +1 -0
  12. package/dist/esm/middleware.d.ts +252 -0
  13. package/dist/esm/middleware.js +872 -0
  14. package/dist/esm/middleware.js.map +1 -0
  15. package/dist/esm/reconstruct-generation.d.ts +129 -0
  16. package/dist/esm/reconstruct-generation.js +148 -0
  17. package/dist/esm/reconstruct-generation.js.map +1 -0
  18. package/dist/esm/reconstruct.d.ts +79 -0
  19. package/dist/esm/reconstruct.js +75 -0
  20. package/dist/esm/reconstruct.js.map +1 -0
  21. package/dist/esm/retrieve.d.ts +40 -0
  22. package/dist/esm/retrieve.js +54 -0
  23. package/dist/esm/retrieve.js.map +1 -0
  24. package/dist/esm/testkit/conformance.d.ts +33 -0
  25. package/dist/esm/testkit/conformance.js +997 -0
  26. package/dist/esm/testkit/conformance.js.map +1 -0
  27. package/dist/esm/types.d.ts +554 -0
  28. package/dist/esm/types.js +103 -0
  29. package/dist/esm/types.js.map +1 -0
  30. package/package.json +71 -0
  31. package/skills/ai-persistence/SKILL.md +218 -0
  32. package/skills/ai-persistence/build-cloudflare-adapter/SKILL.md +313 -0
  33. package/skills/ai-persistence/build-cloudflare-artifact-store/SKILL.md +693 -0
  34. package/skills/ai-persistence/build-custom-adapter/SKILL.md +328 -0
  35. package/skills/ai-persistence/build-drizzle-adapter/SKILL.md +562 -0
  36. package/skills/ai-persistence/build-prisma-adapter/SKILL.md +518 -0
  37. package/skills/ai-persistence/server/SKILL.md +210 -0
  38. package/skills/ai-persistence/stores/SKILL.md +485 -0
  39. package/src/blob-range.ts +101 -0
  40. package/src/capabilities.ts +18 -0
  41. package/src/index.ts +114 -0
  42. package/src/memory.ts +491 -0
  43. package/src/middleware.ts +1795 -0
  44. package/src/reconstruct-generation.ts +244 -0
  45. package/src/reconstruct.ts +149 -0
  46. package/src/retrieve.ts +77 -0
  47. package/src/testkit/conformance.ts +1288 -0
  48. package/src/types.ts +878 -0
@@ -0,0 +1,252 @@
1
+ import { ChatMiddleware, GenerationMiddleware, PersistedArtifactActivity, PersistedArtifactRef, PersistedArtifactRole } from '@tanstack/ai';
2
+ import { AIPersistence, AIPersistenceStores, BlobBody, ChatTranscriptStores, RunStore } from './types.js';
3
+ /**
4
+ * How generated media is turned into durable artifacts: which pieces of a
5
+ * result become artifacts, what they are named, where their bytes land, and how
6
+ * the bytes are fetched when the provider returns a URL rather than inline data.
7
+ *
8
+ * Consumed by {@link withGenerationPersistence} through
9
+ * {@link WithGenerationPersistenceOptions}. Chat persistence has no artifacts —
10
+ * its options are {@link WithPersistenceOptions}.
11
+ */
12
+ export interface ArtifactPersistenceOptions {
13
+ extractArtifacts?: (input: GenerationArtifactExtractionInput) => Array<GenerationArtifactDescriptor | PersistedArtifactRef> | Promise<Array<GenerationArtifactDescriptor | PersistedArtifactRef>>;
14
+ nameArtifact?: (input: GenerationArtifactNameInput) => string;
15
+ /**
16
+ * Map a freshly-persisted artifact ref to the durable app-origin URL that
17
+ * serves its bytes (your `GET` route around `retrieveArtifact` /
18
+ * `retrieveBlob`). The returned URL is stamped onto `ref.url` and written into
19
+ * the result's media field, so both the live and the restored result render
20
+ * durable media from your own origin instead of the provider's expiring link.
21
+ * Return `undefined` to leave a ref without a durable URL.
22
+ */
23
+ artifactUrl?: (ref: PersistedArtifactRef) => string | undefined;
24
+ /**
25
+ * Choose the blob-store key each artifact's bytes are written under, so
26
+ * generated media can land in your own folder structure rather than the
27
+ * default `artifacts/<runId>/<artifactId>`.
28
+ *
29
+ * ```ts
30
+ * storageKey: ({ runId, artifactId, mimeType }) =>
31
+ * `video/${videoId}/frames/${runId}-${artifactId}.png`
32
+ * ```
33
+ *
34
+ * Server-side only, and deliberately so: a key supplied by the browser would
35
+ * be a path-traversal and cross-tenant-write vector.
36
+ *
37
+ * The resolved key is recorded on `ArtifactRecord.blobKey`, because once the
38
+ * path is arbitrary a reader can no longer recompute it. Returning a
39
+ * non-unique key overwrites — include `artifactId` (or something equally
40
+ * unique) unless you intend that.
41
+ */
42
+ storageKey?: (input: {
43
+ artifactId: string;
44
+ runId: string;
45
+ threadId: string;
46
+ role: PersistedArtifactRole;
47
+ activity: PersistedArtifactActivity;
48
+ path: string;
49
+ mimeType: string;
50
+ name: string;
51
+ }) => string;
52
+ /**
53
+ * Opt in to fetching prompt media referenced by URL (`role: 'input'`).
54
+ *
55
+ * Off by default, and deliberately expressed as a predicate rather than a
56
+ * boolean: input URLs come from the caller, so fetching them server-side
57
+ * turns your server into a proxy for whatever the caller names — cloud
58
+ * metadata endpoints, `localhost` admin services, anything your network can
59
+ * reach. The bytes are also redundant in the common case, since the client
60
+ * already had the media it referenced.
61
+ *
62
+ * Enable this only when you genuinely need a durable copy of caller-supplied
63
+ * media (a "paste an image URL" input box, say), and validate the target:
64
+ *
65
+ * ```ts
66
+ * allowInputUrl: ({ url }) => url.hostname.endsWith('.cdn.example.com')
67
+ * ```
68
+ *
69
+ * Requests are additionally forced through the same baseline checks every
70
+ * artifact fetch gets (http/https only, timeout, size cap), plus — because
71
+ * the target is untrusted — a loopback/private/link-local host block and
72
+ * `redirect: 'manual'` so a 302 cannot hop to an internal address. Those are
73
+ * a backstop, not a substitute for a narrow predicate: a hostname that
74
+ * resolves to a private address still passes a literal-IP check.
75
+ */
76
+ allowInputUrl?: (input: {
77
+ url: URL;
78
+ descriptor: GenerationArtifactDescriptor;
79
+ }) => boolean | Promise<boolean>;
80
+ /** Abort an artifact fetch after this many ms. Default 30_000. */
81
+ artifactFetchTimeoutMs?: number;
82
+ /**
83
+ * Refuse an artifact body larger than this many bytes. Default 1 GiB.
84
+ *
85
+ * This is a bound on TRANSFER, not on memory: the URL path streams into the
86
+ * blob store and never buffers, so a 1 GiB artifact costs a streaming store
87
+ * (R2, S3, filesystem) flat memory. What the cap buys is a ceiling on what a
88
+ * broken or hostile origin can make you pull and store — `content-length` is
89
+ * advisory, so without it an artifact fetch is an unbounded transfer billed
90
+ * to you.
91
+ *
92
+ * Pass `false` to remove the ceiling entirely. That also removes the
93
+ * cap-enforcing `TransformStream` wrapper, so the fetched body reaches your
94
+ * store exactly as `fetch` produced it — on workerd that means it keeps its
95
+ * native declared length and `R2Bucket.put` can single-shot it with no hint,
96
+ * no multipart, and nothing buffered. Do that when you trust the origins you
97
+ * fetch from (your provider's CDN); keep the cap when `allowInputUrl` lets
98
+ * callers name the URL.
99
+ */
100
+ maxArtifactBytes?: number | false;
101
+ /**
102
+ * `fetch` used to download artifact bytes. Defaults to the global. Inject to
103
+ * route downloads through a proxy or an egress-restricted agent — the most
104
+ * robust SSRF control available here, since it can resolve and check the
105
+ * address actually connected to.
106
+ */
107
+ artifactFetch?: typeof globalThis.fetch;
108
+ }
109
+ /**
110
+ * Options for {@link withGenerationPersistence}: everything in
111
+ * {@link ArtifactPersistenceOptions}, plus an optional scope override.
112
+ */
113
+ export interface WithGenerationPersistenceOptions extends ArtifactPersistenceOptions {
114
+ /**
115
+ * Override the scope runs are filed under. Defaults to the `threadId` you
116
+ * passed the activity, which is normally what you want, so leave this unset
117
+ * unless the record belongs somewhere other than the activity's own scope.
118
+ */
119
+ threadId?: string;
120
+ }
121
+ export interface GenerationArtifactDescriptor {
122
+ role: PersistedArtifactRole;
123
+ path: string;
124
+ mediaType?: PersistedArtifactRef['source']['mediaType'];
125
+ mimeType?: string;
126
+ bytes?: BlobBody;
127
+ url?: string;
128
+ json?: unknown;
129
+ name?: string;
130
+ jobId?: string;
131
+ expiresAt?: string | Date;
132
+ }
133
+ export interface GenerationArtifactExtractionInput {
134
+ activity: PersistedArtifactActivity;
135
+ provider: string;
136
+ model: string;
137
+ threadId: string;
138
+ runId: string;
139
+ inputs: unknown;
140
+ result: unknown;
141
+ }
142
+ export interface GenerationArtifactNameInput {
143
+ descriptor: GenerationArtifactDescriptor;
144
+ activity: PersistedArtifactActivity;
145
+ provider: string;
146
+ model: string;
147
+ threadId: string;
148
+ runId: string;
149
+ index: number;
150
+ }
151
+ type StoreIsDefinitelyPresent<TStores extends AIPersistenceStores, TKey extends keyof AIPersistenceStores> = TKey extends keyof TStores ? object extends Pick<TStores, TKey> ? false : [Exclude<TStores[TKey], undefined>] extends [never] ? false : true : false;
152
+ type StoreIsDefinitelyAbsent<TStores extends AIPersistenceStores, TKey extends keyof AIPersistenceStores> = TKey extends keyof TStores ? [Exclude<TStores[TKey], undefined>] extends [never] ? true : false : true;
153
+ /**
154
+ * Chat entrypoint invalid when:
155
+ * - `messages` is known-absent, or
156
+ * - `interrupts` is known-present without `runs`.
157
+ *
158
+ * Fully optional bags (`AIPersistence` with all `?` keys) stay assignable and
159
+ * are checked at runtime by {@link validateChatPersistenceStores}.
160
+ */
161
+ type InvalidChatPersistence<TStores extends AIPersistenceStores> = StoreIsDefinitelyAbsent<TStores, 'messages'> extends true ? true : StoreIsDefinitelyPresent<TStores, 'interrupts'> extends true ? StoreIsDefinitelyAbsent<TStores, 'runs'> : false;
162
+ /**
163
+ * Generation entrypoint invalid when `generationRuns` is known-absent, or when
164
+ * exactly one of `artifacts` / `blobs` is present (artifact persistence needs
165
+ * both).
166
+ */
167
+ type InvalidGenerationPersistence<TStores extends AIPersistenceStores> = StoreIsDefinitelyAbsent<TStores, 'generationRuns'> extends true ? true : StoreIsDefinitelyPresent<TStores, 'artifacts'> extends true ? StoreIsDefinitelyAbsent<TStores, 'blobs'> : StoreIsDefinitelyPresent<TStores, 'blobs'> extends true ? StoreIsDefinitelyAbsent<TStores, 'artifacts'> : false;
168
+ type ValidChatPersistence<TStores extends AIPersistenceStores> = InvalidChatPersistence<TStores> extends true ? never : unknown;
169
+ type ValidGenerationPersistence<TStores extends AIPersistenceStores> = InvalidGenerationPersistence<TStores> extends true ? never : unknown;
170
+ /**
171
+ * Record a human-in-the-loop PAUSE.
172
+ *
173
+ * Deliberately writes NO `finishedAt`: `'interrupted'` is not a terminal status
174
+ * (`isTerminalRunStatus('interrupted')` is `false`), and stamping a terminal
175
+ * timestamp on it told every reader the run was over while it was in fact
176
+ * waiting for a human. Only `abortRun`/`completeRun`/`failRun` finish a run.
177
+ */
178
+ export declare function interruptRun(runs: RunStore | undefined, runId: string): Promise<void>;
179
+ /**
180
+ * Record that the run has ended for good — an explicit cancel, or a disconnect
181
+ * on a run that has nothing to reattach to. Terminal, so it carries
182
+ * `finishedAt`.
183
+ */
184
+ export declare function abortRun(runs: RunStore | undefined, runId: string): Promise<void>;
185
+ /**
186
+ * Chat-only **state** persistence middleware. Provides durable transcript,
187
+ * run records, and interrupts for `chat()`. Does **not** provide locks —
188
+ * use `withLocks` from `@tanstack/ai` for multi-instance coordination.
189
+ *
190
+ * This middleware never mutates the chunk stream; delivery durability
191
+ * (replaying a disconnected/reloaded stream) is a separate transport-layer
192
+ * concern (see the resumable-streams docs).
193
+ *
194
+ * Requires `stores.messages`. When `stores.interrupts` is present,
195
+ * `stores.runs` is also required.
196
+ *
197
+ * ⚠️ AUTHORITATIVE-HISTORY CONTRACT: when a request carries a non-empty
198
+ * `messages` array it is treated as the FULL conversation history and, on
199
+ * finish, **overwrites** the entire stored thread. Post only the complete
200
+ * transcript, never a delta — sending just the newest message(s) will replace
201
+ * (and thereby destroy) the stored thread. To continue a stored thread without
202
+ * resending history, pass an empty `messages` array and the stored transcript
203
+ * is loaded and used.
204
+ */
205
+ export interface WithPersistenceOptions {
206
+ /**
207
+ * Also persist a throttled snapshot of the in-progress assistant reply while
208
+ * it streams. Off by default — the transcript is otherwise persisted at the
209
+ * pending turn (`onStart`), interrupt boundaries, and completion (`onFinish`).
210
+ * Enable it to recover partial output if the process dies mid-generation, at
211
+ * the cost of extra writes. Snapshots are throttled to at most one per
212
+ * {@link WithPersistenceOptions.snapshotIntervalMs}.
213
+ */
214
+ snapshotStreaming?: boolean;
215
+ /**
216
+ * Minimum milliseconds between streaming snapshots when `snapshotStreaming`
217
+ * is on. Defaults to 1000.
218
+ */
219
+ snapshotIntervalMs?: number;
220
+ }
221
+ /**
222
+ * @param persistence - Must satisfy {@link ChatTranscriptStores} (messages
223
+ * required). Known-absent `messages` or `interrupts` without `runs` fail at
224
+ * compile time; fully dynamic bags are checked at runtime.
225
+ */
226
+ export declare function withPersistence<TStores extends ChatTranscriptStores>(persistence: AIPersistence<TStores> & ValidChatPersistence<TStores>, options?: WithPersistenceOptions): ChatMiddleware;
227
+ /**
228
+ * Generation-only persistence middleware. Tracks generation run status (run
229
+ * records keyed by `runId`) and, when `stores.artifacts` + `stores.blobs` are
230
+ * both provided, persists the generated media for image, audio, TTS, video, and
231
+ * transcription activities.
232
+ *
233
+ * Requires `stores.generationRuns`. A generation activity has no conversation,
234
+ * so the run is keyed on its own `runId` (`ctx.runId ?? ctx.requestId`), which
235
+ * is never faked from anything else.
236
+ *
237
+ * A `threadId` is REQUIRED alongside it — not as a link to a chat, but as the
238
+ * stable app-chosen slot successive runs of the same thing fill
239
+ * (`product-123-hero`, `video-9-start-frame`). It is what
240
+ * `stores.generationRuns.findLatestForThread` keys on, and therefore the only
241
+ * way a run is ever hydrated again. It comes from the `threadId` passed to the
242
+ * activity, or from {@link WithGenerationPersistenceOptions.threadId} when that
243
+ * overrides it; supplying neither throws at `onStart` rather than filing a run
244
+ * nothing can find.
245
+ *
246
+ * On success the terminal result metadata (ids, urls — never media bytes) and,
247
+ * when artifact persistence is on, the persisted artifact refs are captured onto
248
+ * the run record so a server-authoritative client can hydrate the last
249
+ * generation for a thread via {@link reconstructGeneration}.
250
+ */
251
+ export declare function withGenerationPersistence<TStores extends AIPersistenceStores>(persistence: AIPersistence<TStores> & ValidGenerationPersistence<TStores>, opts?: WithGenerationPersistenceOptions): GenerationMiddleware;
252
+ export {};