@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.
@@ -929,24 +929,24 @@ export function runPersistenceConformance(
929
929
 
930
930
  await store.save(
931
931
  artifact({
932
- artifactId: 'art-a',
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: 'art-b',
939
+ artifactId: '\u{e000}',
940
940
  sourceUrl: 'https://provider.example/expiring.png',
941
- createdAt: 200,
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('art-a')).toMatchObject({
949
- artifactId: 'art-a',
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('art-b')).toMatchObject({
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
- 'art-a',
964
- 'art-b',
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: 'art-a', name: 'renamed.png', size: 9 }),
969
+ artifact({ artifactId: '\u{10000}', name: 'renamed.png', size: 9 }),
970
970
  )
971
- const updated = await store.get('art-a')
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
- 'art-a',
976
- 'art-b',
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
- /** All artifacts for a run. Returns `[]` when the run has none. */
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