@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,75 @@
|
|
|
1
|
+
import { validateReconstructChatStores } from "./types.js";
|
|
2
|
+
import { modelMessagesToUIMessages } from "@tanstack/ai";
|
|
3
|
+
//#region src/reconstruct.ts
|
|
4
|
+
/**
|
|
5
|
+
* Build the JSON `Response` a server-authoritative client hydrates from on load
|
|
6
|
+
* (see the client-persistence guide). Reads the thread id from the request query
|
|
7
|
+
* (`?threadId=` by default) and returns `{ messages, activeRun, interrupts }`
|
|
8
|
+
* ({@link ReconstructedChat}):
|
|
9
|
+
*
|
|
10
|
+
* - `messages` — the stored transcript as UI messages.
|
|
11
|
+
* - `activeRun` — `{ runId }` if a run is still generating for the thread (so the
|
|
12
|
+
* client tails it via the durability stream), else `null`. Resolved via the
|
|
13
|
+
* required `stores.runs.findActiveRun`; `null` when the `runs` store is absent.
|
|
14
|
+
* - `interrupts` — `{ runId, pending }` if the thread has pending human-in-the-loop
|
|
15
|
+
* interrupts (a paused approval / wait) and the run they paused, else `null`, so
|
|
16
|
+
* a reload re-prompts the decision from the server. Resolved via the optional
|
|
17
|
+
* `stores.interrupts.listPending`; `null` when that store is absent.
|
|
18
|
+
*
|
|
19
|
+
* Requires `stores.messages`. Returns an empty transcript with no active run
|
|
20
|
+
* and no interrupts when the thread id is missing or the thread is unknown, so
|
|
21
|
+
* the caller never has to special-case a first load.
|
|
22
|
+
*
|
|
23
|
+
* This helper does **not** enforce tenancy by itself. Pass
|
|
24
|
+
* {@link ReconstructChatOptions.authorize} (or wrap the call in your own
|
|
25
|
+
* session gate) before exposing it on a public route.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* export async function GET(request: Request) {
|
|
29
|
+
* return reconstructChat(persistence, request, {
|
|
30
|
+
* authorize: async (threadId, req) => {
|
|
31
|
+
* const userId = await getSessionUserId(req)
|
|
32
|
+
* return userId != null && (await userOwnsThread(userId, threadId))
|
|
33
|
+
* },
|
|
34
|
+
* })
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
async function reconstructChat(persistence, request, options) {
|
|
39
|
+
validateReconstructChatStores(persistence);
|
|
40
|
+
const messageStore = persistence.stores.messages;
|
|
41
|
+
if (!messageStore) throw new Error("reconstructChat requires stores.messages.");
|
|
42
|
+
const param = options?.param ?? "threadId";
|
|
43
|
+
const threadId = new URL(request.url).searchParams.get(param) ?? "";
|
|
44
|
+
if (threadId && options?.authorize) {
|
|
45
|
+
const decision = await options.authorize(threadId, request);
|
|
46
|
+
if (decision instanceof Response) return decision;
|
|
47
|
+
if (!decision) return new Response(JSON.stringify({ error: "Forbidden" }), {
|
|
48
|
+
status: 403,
|
|
49
|
+
headers: {
|
|
50
|
+
"content-type": "application/json",
|
|
51
|
+
"cache-control": "no-store"
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const active = threadId ? await persistence.stores.runs?.findActiveRun(threadId) : null;
|
|
56
|
+
const stored = threadId ? await messageStore.loadThread(threadId) : [];
|
|
57
|
+
const pending = threadId ? await persistence.stores.interrupts?.listPending(threadId) ?? [] : [];
|
|
58
|
+
const firstPending = pending[0];
|
|
59
|
+
const body = {
|
|
60
|
+
messages: modelMessagesToUIMessages(stored),
|
|
61
|
+
activeRun: active ? { runId: active.runId } : null,
|
|
62
|
+
interrupts: firstPending ? {
|
|
63
|
+
runId: firstPending.runId,
|
|
64
|
+
pending: pending.map((record) => record.payload)
|
|
65
|
+
} : null
|
|
66
|
+
};
|
|
67
|
+
return new Response(JSON.stringify(body), { headers: {
|
|
68
|
+
"content-type": "application/json",
|
|
69
|
+
"cache-control": "no-store"
|
|
70
|
+
} });
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { reconstructChat };
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=reconstruct.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconstruct.js","names":[],"sources":["../../src/reconstruct.ts"],"sourcesContent":["import { modelMessagesToUIMessages } from '@tanstack/ai'\nimport type { UIMessage } from '@tanstack/ai'\nimport { validateReconstructChatStores } from './types'\nimport type { AIPersistence, ChatTranscriptStores } from './types'\n\n/**\n * The JSON body `reconstructChat` returns and a server-authoritative client\n * hydrates from on mount.\n *\n * `messages` is the stored transcript as UI messages (ready to paint).\n * `activeRun` is a cursor to a run still generating for the thread, or `null` —\n * resolved from the STABLE thread id via `stores.runs.findActiveRun`, so the\n * client learns \"there is a live run to tail\" without ever handling a run id.\n * `interrupts` is the thread's pending human-in-the-loop interrupts (tool\n * approvals, client-tool/generic waits) and the run they paused, or `null` —\n * so a reload (or another device) re-prompts the approval from the SERVER, not\n * from client storage. Resolved via `stores.interrupts.listPending`.\n */\nexport interface ReconstructedChat {\n messages: Array<UIMessage>\n activeRun: { runId: string } | null\n interrupts: {\n runId: string\n pending: Array<Record<string, unknown>>\n } | null\n}\n\nexport interface ReconstructChatOptions {\n /** Query parameter carrying the thread id. Defaults to `threadId`. */\n param?: string\n /**\n * Authorize access to the requested thread before loading history.\n *\n * ⚠️ Without this, any caller who knows or guesses `?threadId=` receives the\n * full transcript. Multi-user / multi-tenant deployments **must** supply\n * an authorization check (session → owned threads) or resolve a validated\n * thread id in the route and pass it via a custom `param` that only your\n * server sets.\n *\n * Return:\n * - `true` to allow the load\n * - `false` for a default `403` response\n * - a `Response` to return as-is (e.g. `401` with a body)\n */\n authorize?: (\n threadId: string,\n request: Request,\n ) => boolean | Response | Promise<boolean | Response>\n}\n\n/**\n * Build the JSON `Response` a server-authoritative client hydrates from on load\n * (see the client-persistence guide). Reads the thread id from the request query\n * (`?threadId=` by default) and returns `{ messages, activeRun, interrupts }`\n * ({@link ReconstructedChat}):\n *\n * - `messages` — the stored transcript as UI messages.\n * - `activeRun` — `{ runId }` if a run is still generating for the thread (so the\n * client tails it via the durability stream), else `null`. Resolved via the\n * required `stores.runs.findActiveRun`; `null` when the `runs` store is absent.\n * - `interrupts` — `{ runId, pending }` if the thread has pending human-in-the-loop\n * interrupts (a paused approval / wait) and the run they paused, else `null`, so\n * a reload re-prompts the decision from the server. Resolved via the optional\n * `stores.interrupts.listPending`; `null` when that store is absent.\n *\n * Requires `stores.messages`. Returns an empty transcript with no active run\n * and no interrupts when the thread id is missing or the thread is unknown, so\n * the caller never has to special-case a first load.\n *\n * This helper does **not** enforce tenancy by itself. Pass\n * {@link ReconstructChatOptions.authorize} (or wrap the call in your own\n * session gate) before exposing it on a public route.\n *\n * ```ts\n * export async function GET(request: Request) {\n * return reconstructChat(persistence, request, {\n * authorize: async (threadId, req) => {\n * const userId = await getSessionUserId(req)\n * return userId != null && (await userOwnsThread(userId, threadId))\n * },\n * })\n * }\n * ```\n */\nexport async function reconstructChat(\n persistence: AIPersistence<ChatTranscriptStores>,\n request: Request,\n options?: ReconstructChatOptions,\n): Promise<Response> {\n validateReconstructChatStores(persistence)\n const messageStore = persistence.stores.messages\n if (!messageStore) {\n // validateReconstructChatStores already throws; this narrows for TypeScript.\n throw new Error('reconstructChat requires stores.messages.')\n }\n\n const param = options?.param ?? 'threadId'\n const threadId = new URL(request.url).searchParams.get(param) ?? ''\n\n if (threadId && options?.authorize) {\n const decision = await options.authorize(threadId, request)\n if (decision instanceof Response) {\n return decision\n }\n if (!decision) {\n return new Response(JSON.stringify({ error: 'Forbidden' }), {\n status: 403,\n headers: {\n 'content-type': 'application/json',\n 'cache-control': 'no-store',\n },\n })\n }\n }\n\n // Resolve the active run BEFORE reading the transcript. `withPersistence`\n // persists the final transcript BEFORE marking a run complete, so observing\n // \"no active run\" here guarantees the transcript read below is the FINAL one.\n // Reading them in the other order opens a finish-window race: a fast run that\n // completes between the two reads would return a stale streaming snapshot with\n // `activeRun: null`, leaving the client stuck on the partial (no run to tail).\n const active = threadId\n ? await persistence.stores.runs?.findActiveRun(threadId)\n : null\n const stored = threadId ? await messageStore.loadThread(threadId) : []\n // Pending interrupts for the thread, so a reload re-prompts the approval from\n // the server. Each stored `payload` is the full interrupt descriptor the\n // client hydrates; they share the run they paused.\n const pending = threadId\n ? ((await persistence.stores.interrupts?.listPending(threadId)) ?? [])\n : []\n const firstPending = pending[0]\n const body: ReconstructedChat = {\n messages: modelMessagesToUIMessages(stored),\n activeRun: active ? { runId: active.runId } : null,\n interrupts: firstPending\n ? {\n runId: firstPending.runId,\n pending: pending.map((record) => record.payload),\n }\n : null,\n }\n return new Response(JSON.stringify(body), {\n headers: {\n 'content-type': 'application/json',\n 'cache-control': 'no-store',\n },\n })\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,eAAsB,gBACpB,aACA,SACA,SACmB;CACnB,8BAA8B,WAAW;CACzC,MAAM,eAAe,YAAY,OAAO;CACxC,IAAI,CAAC,cAEH,MAAM,IAAI,MAAM,2CAA2C;CAG7D,MAAM,QAAQ,SAAS,SAAS;CAChC,MAAM,WAAW,IAAI,IAAI,QAAQ,GAAG,CAAC,CAAC,aAAa,IAAI,KAAK,KAAK;CAEjE,IAAI,YAAY,SAAS,WAAW;EAClC,MAAM,WAAW,MAAM,QAAQ,UAAU,UAAU,OAAO;EAC1D,IAAI,oBAAoB,UACtB,OAAO;EAET,IAAI,CAAC,UACH,OAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,YAAY,CAAC,GAAG;GAC1D,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,iBAAiB;GACnB;EACF,CAAC;CAEL;CAQA,MAAM,SAAS,WACX,MAAM,YAAY,OAAO,MAAM,cAAc,QAAQ,IACrD;CACJ,MAAM,SAAS,WAAW,MAAM,aAAa,WAAW,QAAQ,IAAI,CAAC;CAIrE,MAAM,UAAU,WACV,MAAM,YAAY,OAAO,YAAY,YAAY,QAAQ,KAAM,CAAC,IAClE,CAAC;CACL,MAAM,eAAe,QAAQ;CAC7B,MAAM,OAA0B;EAC9B,UAAU,0BAA0B,MAAM;EAC1C,WAAW,SAAS,EAAE,OAAO,OAAO,MAAM,IAAI;EAC9C,YAAY,eACR;GACE,OAAO,aAAa;GACpB,SAAS,QAAQ,KAAK,WAAW,OAAO,OAAO;EACjD,IACA;CACN;CACA,OAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG,EACxC,SAAS;EACP,gBAAgB;EAChB,iBAAiB;CACnB,EACF,CAAC;AACH"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AIPersistence, ArtifactRecord, BlobGetOptions, BlobObject } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The DEFAULT blob-store key a generation artifact's bytes are stored under,
|
|
4
|
+
* used when `withGenerationPersistence` is given no `storageKey` mapper.
|
|
5
|
+
*
|
|
6
|
+
* Reads go through {@link resolveArtifactBlobKey} instead: a record written with
|
|
7
|
+
* a custom `storageKey` carries its real key in `blobKey`, and recomputing the
|
|
8
|
+
* default would look in the wrong place.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function artifactBlobKey(ref: Pick<ArtifactRecord, 'runId' | 'artifactId'>): string;
|
|
13
|
+
/**
|
|
14
|
+
* The blob-store key to read an artifact's bytes from: the key recorded when it
|
|
15
|
+
* was written, falling back to the default convention for records written
|
|
16
|
+
* before `blobKey` existed.
|
|
17
|
+
*
|
|
18
|
+
* The fallback is what makes `blobKey` a non-breaking addition — and why the
|
|
19
|
+
* default convention can never be changed retroactively without one.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveArtifactBlobKey(record: ArtifactRecord): string;
|
|
22
|
+
/**
|
|
23
|
+
* Look up a persisted generation artifact's metadata by id. Returns `null` when
|
|
24
|
+
* the persistence has no `artifacts` store or no record matches — so a serve
|
|
25
|
+
* handler can map that straight to a 404.
|
|
26
|
+
*/
|
|
27
|
+
export declare function retrieveArtifact(persistence: AIPersistence, artifactId: string): Promise<ArtifactRecord | null>;
|
|
28
|
+
/**
|
|
29
|
+
* Look up a persisted generation artifact's stored bytes. Pass an `artifactId`
|
|
30
|
+
* (resolved to its record first) or an already-loaded {@link ArtifactRecord}
|
|
31
|
+
* (no second metadata lookup). Returns `null` when the artifact, its record, or
|
|
32
|
+
* its blob is missing, or the stores are not configured.
|
|
33
|
+
*
|
|
34
|
+
* Pass `options.range` to read one slice — how a serve route answers a `Range`
|
|
35
|
+
* request with `206` + `Content-Range` instead of the whole file, which is what
|
|
36
|
+
* `<video>` seeking is built on. Resolve the range against `record.size` and
|
|
37
|
+
* reply `416` yourself when it does not fit; the store is handed satisfiable
|
|
38
|
+
* ranges only. The returned object's `range` reports the slice actually served.
|
|
39
|
+
*/
|
|
40
|
+
export declare function retrieveBlob(persistence: AIPersistence, artifact: string | ArtifactRecord, options?: BlobGetOptions): Promise<BlobObject | null>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/retrieve.ts
|
|
2
|
+
/**
|
|
3
|
+
* The DEFAULT blob-store key a generation artifact's bytes are stored under,
|
|
4
|
+
* used when `withGenerationPersistence` is given no `storageKey` mapper.
|
|
5
|
+
*
|
|
6
|
+
* Reads go through {@link resolveArtifactBlobKey} instead: a record written with
|
|
7
|
+
* a custom `storageKey` carries its real key in `blobKey`, and recomputing the
|
|
8
|
+
* default would look in the wrong place.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
function artifactBlobKey(ref) {
|
|
13
|
+
return `artifacts/${ref.runId}/${ref.artifactId}`;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The blob-store key to read an artifact's bytes from: the key recorded when it
|
|
17
|
+
* was written, falling back to the default convention for records written
|
|
18
|
+
* before `blobKey` existed.
|
|
19
|
+
*
|
|
20
|
+
* The fallback is what makes `blobKey` a non-breaking addition — and why the
|
|
21
|
+
* default convention can never be changed retroactively without one.
|
|
22
|
+
*/
|
|
23
|
+
function resolveArtifactBlobKey(record) {
|
|
24
|
+
return record.blobKey ?? artifactBlobKey(record);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Look up a persisted generation artifact's metadata by id. Returns `null` when
|
|
28
|
+
* the persistence has no `artifacts` store or no record matches — so a serve
|
|
29
|
+
* handler can map that straight to a 404.
|
|
30
|
+
*/
|
|
31
|
+
async function retrieveArtifact(persistence, artifactId) {
|
|
32
|
+
return await persistence.stores.artifacts?.get(artifactId) ?? null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Look up a persisted generation artifact's stored bytes. Pass an `artifactId`
|
|
36
|
+
* (resolved to its record first) or an already-loaded {@link ArtifactRecord}
|
|
37
|
+
* (no second metadata lookup). Returns `null` when the artifact, its record, or
|
|
38
|
+
* its blob is missing, or the stores are not configured.
|
|
39
|
+
*
|
|
40
|
+
* Pass `options.range` to read one slice — how a serve route answers a `Range`
|
|
41
|
+
* request with `206` + `Content-Range` instead of the whole file, which is what
|
|
42
|
+
* `<video>` seeking is built on. Resolve the range against `record.size` and
|
|
43
|
+
* reply `416` yourself when it does not fit; the store is handed satisfiable
|
|
44
|
+
* ranges only. The returned object's `range` reports the slice actually served.
|
|
45
|
+
*/
|
|
46
|
+
async function retrieveBlob(persistence, artifact, options) {
|
|
47
|
+
const record = typeof artifact === "string" ? await retrieveArtifact(persistence, artifact) : artifact;
|
|
48
|
+
if (!record) return null;
|
|
49
|
+
return await persistence.stores.blobs?.get(resolveArtifactBlobKey(record), options) ?? null;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { artifactBlobKey, resolveArtifactBlobKey, retrieveArtifact, retrieveBlob };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=retrieve.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retrieve.js","names":[],"sources":["../../src/retrieve.ts"],"sourcesContent":["import type {\n AIPersistence,\n ArtifactRecord,\n BlobGetOptions,\n BlobObject,\n} from './types'\n\n/**\n * The DEFAULT blob-store key a generation artifact's bytes are stored under,\n * used when `withGenerationPersistence` is given no `storageKey` mapper.\n *\n * Reads go through {@link resolveArtifactBlobKey} instead: a record written with\n * a custom `storageKey` carries its real key in `blobKey`, and recomputing the\n * default would look in the wrong place.\n *\n * @internal\n */\nexport function artifactBlobKey(\n ref: Pick<ArtifactRecord, 'runId' | 'artifactId'>,\n): string {\n return `artifacts/${ref.runId}/${ref.artifactId}`\n}\n\n/**\n * The blob-store key to read an artifact's bytes from: the key recorded when it\n * was written, falling back to the default convention for records written\n * before `blobKey` existed.\n *\n * The fallback is what makes `blobKey` a non-breaking addition — and why the\n * default convention can never be changed retroactively without one.\n */\nexport function resolveArtifactBlobKey(record: ArtifactRecord): string {\n return record.blobKey ?? artifactBlobKey(record)\n}\n\n/**\n * Look up a persisted generation artifact's metadata by id. Returns `null` when\n * the persistence has no `artifacts` store or no record matches — so a serve\n * handler can map that straight to a 404.\n */\nexport async function retrieveArtifact(\n persistence: AIPersistence,\n artifactId: string,\n): Promise<ArtifactRecord | null> {\n const record = await persistence.stores.artifacts?.get(artifactId)\n return record ?? null\n}\n\n/**\n * Look up a persisted generation artifact's stored bytes. Pass an `artifactId`\n * (resolved to its record first) or an already-loaded {@link ArtifactRecord}\n * (no second metadata lookup). Returns `null` when the artifact, its record, or\n * its blob is missing, or the stores are not configured.\n *\n * Pass `options.range` to read one slice — how a serve route answers a `Range`\n * request with `206` + `Content-Range` instead of the whole file, which is what\n * `<video>` seeking is built on. Resolve the range against `record.size` and\n * reply `416` yourself when it does not fit; the store is handed satisfiable\n * ranges only. The returned object's `range` reports the slice actually served.\n */\nexport async function retrieveBlob(\n persistence: AIPersistence,\n artifact: string | ArtifactRecord,\n options?: BlobGetOptions,\n): Promise<BlobObject | null> {\n const record =\n typeof artifact === 'string'\n ? await retrieveArtifact(persistence, artifact)\n : artifact\n if (!record) return null\n\n const blob = await persistence.stores.blobs?.get(\n resolveArtifactBlobKey(record),\n options,\n )\n return blob ?? null\n}\n"],"mappings":";;;;;;;;;;;AAiBA,SAAgB,gBACd,KACQ;CACR,OAAO,aAAa,IAAI,MAAM,GAAG,IAAI;AACvC;;;;;;;;;AAUA,SAAgB,uBAAuB,QAAgC;CACrE,OAAO,OAAO,WAAW,gBAAgB,MAAM;AACjD;;;;;;AAOA,eAAsB,iBACpB,aACA,YACgC;CAEhC,OAAO,MADc,YAAY,OAAO,WAAW,IAAI,UAAU,KAChD;AACnB;;;;;;;;;;;;;AAcA,eAAsB,aACpB,aACA,UACA,SAC4B;CAC5B,MAAM,SACJ,OAAO,aAAa,WAChB,MAAM,iBAAiB,aAAa,QAAQ,IAC5C;CACN,IAAI,CAAC,QAAQ,OAAO;CAMpB,OAAO,MAJY,YAAY,OAAO,OAAO,IAC3C,uBAAuB,MAAM,GAC7B,OACF,KACe;AACjB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AIPersistence, AIPersistenceStores } from '../types.js';
|
|
2
|
+
type MakePersistence = () => Promise<AIPersistence> | AIPersistence;
|
|
3
|
+
/**
|
|
4
|
+
* Methods that are optional on the `RunStore` contract.
|
|
5
|
+
*
|
|
6
|
+
* `findActiveRun` is deliberately NOT here: it is REQUIRED, per the evolution
|
|
7
|
+
* policy in `../types.ts`. It was optional for one release cycle and silently
|
|
8
|
+
* disabled reconnect on every backend that had not caught up.
|
|
9
|
+
*/
|
|
10
|
+
type OptionalRunStoreMethod = 'listByThread' | 'listReclaimable';
|
|
11
|
+
/** Dotted `store.method` key a backend passes to declare an omitted method. */
|
|
12
|
+
export type PersistenceConformanceMethodKey = `runs.${OptionalRunStoreMethod}`;
|
|
13
|
+
export interface PersistenceConformanceOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Store keys this backend intentionally does not provide. Any store that is
|
|
16
|
+
* absent from the persistence and NOT listed here fails the suite, so a
|
|
17
|
+
* dropped/misconfigured store can never pass silently.
|
|
18
|
+
*/
|
|
19
|
+
skip?: Array<keyof AIPersistenceStores>;
|
|
20
|
+
/**
|
|
21
|
+
* OPTIONAL store methods this backend intentionally does not implement, as
|
|
22
|
+
* `'runs.listByThread'` and friends. A method that is absent and NOT listed
|
|
23
|
+
* here fails the suite; a listed one is reported as a skipped case.
|
|
24
|
+
*/
|
|
25
|
+
skipMethods?: Array<PersistenceConformanceMethodKey>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Register a Vitest suite that validates `makePersistence()` against the full
|
|
29
|
+
* `AIPersistence` contract — every store it provides, and none it declares
|
|
30
|
+
* skipped.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runPersistenceConformance(name: string, makePersistence: MakePersistence, options?: PersistenceConformanceOptions): void;
|
|
33
|
+
export {};
|