@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,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 {};
|