@tanstack/ai-persistence 0.2.0 → 0.4.1
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/capabilities.d.ts +6 -0
- package/dist/esm/capabilities.js +3 -1
- package/dist/esm/capabilities.js.map +1 -1
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +2 -2
- package/dist/esm/memory.js +41 -1
- package/dist/esm/memory.js.map +1 -1
- package/dist/esm/middleware.js +296 -54
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/testkit/conformance.js +63 -10
- package/dist/esm/testkit/conformance.js.map +1 -1
- package/dist/esm/types.d.ts +35 -1
- package/dist/esm/types.js.map +1 -1
- package/package.json +3 -3
- package/skills/ai-persistence/SKILL.md +7 -0
- package/skills/ai-persistence/build-cloudflare-artifact-store/SKILL.md +47 -25
- package/skills/ai-persistence/server/SKILL.md +17 -10
- package/src/capabilities.ts +10 -0
- package/src/index.ts +5 -0
- package/src/memory.ts +78 -1
- package/src/middleware.ts +543 -88
- package/src/testkit/conformance.ts +77 -12
- package/src/types.ts +38 -1
|
@@ -929,24 +929,24 @@ export function runPersistenceConformance(
|
|
|
929
929
|
|
|
930
930
|
await store.save(
|
|
931
931
|
artifact({
|
|
932
|
-
artifactId: '
|
|
932
|
+
artifactId: '\u{10000}',
|
|
933
933
|
blobKey: 'artifacts/run-art/art-a',
|
|
934
934
|
createdAt: 100,
|
|
935
935
|
}),
|
|
936
936
|
)
|
|
937
937
|
await store.save(
|
|
938
938
|
artifact({
|
|
939
|
-
artifactId: '
|
|
939
|
+
artifactId: '\u{e000}',
|
|
940
940
|
sourceUrl: 'https://provider.example/expiring.png',
|
|
941
|
-
createdAt:
|
|
941
|
+
createdAt: 100,
|
|
942
942
|
}),
|
|
943
943
|
)
|
|
944
944
|
await store.save(
|
|
945
945
|
artifact({ artifactId: 'art-c', runId: 'run-art-other' }),
|
|
946
946
|
)
|
|
947
947
|
|
|
948
|
-
expect(await store.get('
|
|
949
|
-
artifactId: '
|
|
948
|
+
expect(await store.get('\u{10000}')).toMatchObject({
|
|
949
|
+
artifactId: '\u{10000}',
|
|
950
950
|
runId: 'run-art',
|
|
951
951
|
threadId: 'thread-art',
|
|
952
952
|
blobKey: 'artifacts/run-art/art-a',
|
|
@@ -955,28 +955,93 @@ export function runPersistenceConformance(
|
|
|
955
955
|
size: 3,
|
|
956
956
|
createdAt: 100,
|
|
957
957
|
})
|
|
958
|
-
expect(await store.get('
|
|
958
|
+
expect(await store.get('\u{e000}')).toMatchObject({
|
|
959
959
|
sourceUrl: 'https://provider.example/expiring.png',
|
|
960
960
|
})
|
|
961
961
|
|
|
962
962
|
expect((await store.list('run-art')).map((r) => r.artifactId)).toEqual([
|
|
963
|
-
'
|
|
964
|
-
'
|
|
963
|
+
'\u{e000}',
|
|
964
|
+
'\u{10000}',
|
|
965
965
|
])
|
|
966
966
|
|
|
967
967
|
// save() is insert-OR-OVERWRITE: re-saving an id corrects the record.
|
|
968
968
|
await store.save(
|
|
969
|
-
artifact({ artifactId: '
|
|
969
|
+
artifact({ artifactId: '\u{10000}', name: 'renamed.png', size: 9 }),
|
|
970
970
|
)
|
|
971
|
-
const updated = await store.get('
|
|
971
|
+
const updated = await store.get('\u{10000}')
|
|
972
972
|
expect(updated).toMatchObject({ name: 'renamed.png', size: 9 })
|
|
973
973
|
expect(updated?.blobKey).toBeUndefined()
|
|
974
974
|
expect((await store.list('run-art')).map((r) => r.artifactId)).toEqual([
|
|
975
|
-
'
|
|
976
|
-
'
|
|
975
|
+
'\u{e000}',
|
|
976
|
+
'\u{10000}',
|
|
977
977
|
])
|
|
978
978
|
})
|
|
979
979
|
|
|
980
|
+
it('lists a thread in deterministic createdAt and artifactId order', async () => {
|
|
981
|
+
const store = resolveStore('artifacts')
|
|
982
|
+
if (!store) return
|
|
983
|
+
|
|
984
|
+
await store.save(
|
|
985
|
+
artifact({
|
|
986
|
+
artifactId: 'thread-b',
|
|
987
|
+
threadId: 'thread-order',
|
|
988
|
+
createdAt: 2,
|
|
989
|
+
}),
|
|
990
|
+
)
|
|
991
|
+
await store.save(
|
|
992
|
+
artifact({
|
|
993
|
+
artifactId: 'thread-a',
|
|
994
|
+
threadId: 'thread-order',
|
|
995
|
+
createdAt: 2,
|
|
996
|
+
}),
|
|
997
|
+
)
|
|
998
|
+
await store.save(
|
|
999
|
+
artifact({
|
|
1000
|
+
artifactId: 'thread-early',
|
|
1001
|
+
threadId: 'thread-order',
|
|
1002
|
+
createdAt: 1,
|
|
1003
|
+
}),
|
|
1004
|
+
)
|
|
1005
|
+
await store.save(
|
|
1006
|
+
artifact({
|
|
1007
|
+
artifactId: 'other',
|
|
1008
|
+
threadId: 'thread-other',
|
|
1009
|
+
createdAt: 0,
|
|
1010
|
+
}),
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
expect(
|
|
1014
|
+
(await store.listForThread('thread-order')).map((r) => r.artifactId),
|
|
1015
|
+
).toEqual(['thread-early', 'thread-a', 'thread-b'])
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
it('orders artifact IDs by UTF-8 bytes after createdAt', async () => {
|
|
1019
|
+
const store = resolveStore('artifacts')
|
|
1020
|
+
if (!store) return
|
|
1021
|
+
|
|
1022
|
+
await store.save(
|
|
1023
|
+
artifact({
|
|
1024
|
+
artifactId: '\u{10000}',
|
|
1025
|
+
threadId: 'thread-utf8',
|
|
1026
|
+
createdAt: 1,
|
|
1027
|
+
}),
|
|
1028
|
+
)
|
|
1029
|
+
await store.save(
|
|
1030
|
+
artifact({
|
|
1031
|
+
artifactId: '\u{e000}',
|
|
1032
|
+
threadId: 'thread-utf8',
|
|
1033
|
+
createdAt: 1,
|
|
1034
|
+
}),
|
|
1035
|
+
)
|
|
1036
|
+
await store.save(
|
|
1037
|
+
artifact({ artifactId: 'a', threadId: 'thread-utf8', createdAt: 1 }),
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
expect(
|
|
1041
|
+
(await store.listForThread('thread-utf8')).map((r) => r.artifactId),
|
|
1042
|
+
).toEqual(['a', '\u{e000}', '\u{10000}'])
|
|
1043
|
+
})
|
|
1044
|
+
|
|
980
1045
|
it('deletes one artifact and every artifact for a run', async () => {
|
|
981
1046
|
const store = resolveStore('artifacts')
|
|
982
1047
|
if (!store) return
|
package/src/types.ts
CHANGED
|
@@ -221,6 +221,18 @@ export interface InterruptRecord {
|
|
|
221
221
|
response?: unknown
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
/** A terminal interrupt write for {@link InterruptStore.commitBatch}. */
|
|
225
|
+
export type InterruptCommitEntry =
|
|
226
|
+
| {
|
|
227
|
+
interruptId: string
|
|
228
|
+
status: 'resolved'
|
|
229
|
+
response?: unknown
|
|
230
|
+
}
|
|
231
|
+
| {
|
|
232
|
+
interruptId: string
|
|
233
|
+
status: 'cancelled'
|
|
234
|
+
}
|
|
235
|
+
|
|
224
236
|
/** Durable store for human-in-the-loop interrupts. */
|
|
225
237
|
export interface InterruptStore {
|
|
226
238
|
/**
|
|
@@ -249,6 +261,19 @@ export interface InterruptStore {
|
|
|
249
261
|
* `interruptId` does not exist.
|
|
250
262
|
*/
|
|
251
263
|
cancel: (interruptId: string) => Promise<void>
|
|
264
|
+
/**
|
|
265
|
+
* Commit terminal writes for a validated resume batch.
|
|
266
|
+
*
|
|
267
|
+
* Optional. When present, `withPersistence` calls it once instead of
|
|
268
|
+
* calling `resolve` and `cancel` for each entry. Apply every entry or none.
|
|
269
|
+
*
|
|
270
|
+
* Reject the whole batch (throw, writing nothing) when any entry has a
|
|
271
|
+
* duplicate `interruptId`, references an `interruptId` that does not exist,
|
|
272
|
+
* or references an interrupt whose status is not `'pending'`. This is
|
|
273
|
+
* stricter than `resolve` / `cancel`, which are no-ops for a missing
|
|
274
|
+
* `interruptId`.
|
|
275
|
+
*/
|
|
276
|
+
commitBatch?: (entries: ReadonlyArray<InterruptCommitEntry>) => Promise<void>
|
|
252
277
|
/** Return the interrupt for `interruptId`, or `null` if none exists. */
|
|
253
278
|
get: (interruptId: string) => Promise<InterruptRecord | null>
|
|
254
279
|
/**
|
|
@@ -388,8 +413,20 @@ export interface ArtifactStore {
|
|
|
388
413
|
save: (record: ArtifactRecord) => Promise<void>
|
|
389
414
|
/** Return the artifact for `artifactId`, or `null` if none exists. */
|
|
390
415
|
get: (artifactId: string) => Promise<ArtifactRecord | null>
|
|
391
|
-
/**
|
|
416
|
+
/**
|
|
417
|
+
* All artifacts for a run in deterministic snapshot order: `createdAt`
|
|
418
|
+
* ascending, then `artifactId` ascending by the unsigned UTF-8 bytes of
|
|
419
|
+
* each string (compare bytes left-to-right; shorter equal prefixes first).
|
|
420
|
+
* Returns `[]` when the run has none.
|
|
421
|
+
*/
|
|
392
422
|
list: (runId: string) => Promise<Array<ArtifactRecord>>
|
|
423
|
+
/**
|
|
424
|
+
* All artifacts for a thread in deterministic snapshot order.
|
|
425
|
+
* Records are ordered by `createdAt` ascending, then by `artifactId` using
|
|
426
|
+
* the unsigned UTF-8 bytes of each string (compare bytes left-to-right; shorter
|
|
427
|
+
* equal prefixes first).
|
|
428
|
+
*/
|
|
429
|
+
listForThread: (threadId: string) => Promise<Array<ArtifactRecord>>
|
|
393
430
|
/**
|
|
394
431
|
* Delete a single artifact by id. A no-op if absent, mirroring
|
|
395
432
|
* {@link BlobStore.delete} — the two are written and deleted as a pair, so
|