@tanstack/ai-persistence 0.6.4 → 0.6.7
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/memory.js +8 -1
- package/dist/esm/memory.js.map +1 -1
- package/dist/esm/merge-stored.d.ts +2 -0
- package/dist/esm/merge-stored.js +47 -0
- package/dist/esm/merge-stored.js.map +1 -0
- package/dist/esm/middleware.d.ts +2 -1
- package/dist/esm/middleware.js +18 -44
- package/dist/esm/middleware.js.map +1 -1
- package/dist/esm/reconstruct.js +90 -1
- package/dist/esm/reconstruct.js.map +1 -1
- package/dist/esm/subagent-runs.d.ts +40 -0
- package/dist/esm/subagent-runs.js +366 -0
- package/dist/esm/subagent-runs.js.map +1 -0
- package/dist/esm/testkit/conformance.d.ts +4 -1
- package/dist/esm/testkit/conformance.js +94 -1
- package/dist/esm/testkit/conformance.js.map +1 -1
- package/package.json +3 -3
- package/skills/ai-persistence/build-cloudflare-adapter/SKILL.md +41 -16
- package/skills/ai-persistence/build-custom-adapter/SKILL.md +49 -20
- package/skills/ai-persistence/build-drizzle-adapter/SKILL.md +54 -9
- package/skills/ai-persistence/build-prisma-adapter/SKILL.md +32 -6
- package/skills/ai-persistence/stores/SKILL.md +46 -29
- package/src/memory.ts +16 -0
- package/src/merge-stored.ts +67 -0
- package/src/middleware.ts +19 -67
- package/src/reconstruct.ts +155 -2
- package/src/subagent-runs.ts +581 -0
- package/src/testkit/conformance.ts +125 -2
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* a SKIPPED case, never as a pass. Silent gaps are not allowed: a case that did
|
|
20
20
|
* not run must never be indistinguishable from one that did. A chat-only
|
|
21
21
|
* adapter therefore passes `skip: ['generationRuns', 'artifacts', 'blobs']`,
|
|
22
|
-
* and a generation-only one skips the four state stores.
|
|
22
|
+
* and a generation-only one skips the four state stores. `listByParentRun` is
|
|
23
|
+
* the exception. Subagent support is opt-in, its cases skip on their own, and
|
|
24
|
+
* a `'runs.listByParentRun'` entry is accepted but has no effect.
|
|
23
25
|
*
|
|
24
26
|
* NOT COVERED HERE: the four durable-run fields on `RunRecord` (`sandboxKey`,
|
|
25
27
|
* `detachedSince`, `cancelRequested`, `driverEpoch`). They exist for durable
|
|
@@ -52,7 +54,10 @@ type MakePersistence = () => Promise<AIPersistence> | AIPersistence
|
|
|
52
54
|
* policy in `../types.ts`. It was optional for one release cycle and silently
|
|
53
55
|
* disabled reconnect on every backend that had not caught up.
|
|
54
56
|
*/
|
|
55
|
-
type OptionalRunStoreMethod =
|
|
57
|
+
type OptionalRunStoreMethod =
|
|
58
|
+
| 'listByThread'
|
|
59
|
+
| 'listByParentRun'
|
|
60
|
+
| 'listReclaimable'
|
|
56
61
|
|
|
57
62
|
/** Dotted `store.method` key a backend passes to declare an omitted method. */
|
|
58
63
|
export type PersistenceConformanceMethodKey = `runs.${OptionalRunStoreMethod}`
|
|
@@ -105,6 +110,9 @@ export interface PersistenceConformanceOptions {
|
|
|
105
110
|
* OPTIONAL store methods this backend intentionally does not implement, as
|
|
106
111
|
* `'runs.listByThread'` and friends. A method that is absent and NOT listed
|
|
107
112
|
* here fails the suite; a listed one is reported as a skipped case.
|
|
113
|
+
* `listByParentRun` is the exception. Subagent support is opt-in, its cases
|
|
114
|
+
* skip on their own, and a `'runs.listByParentRun'` entry is accepted but
|
|
115
|
+
* has no effect.
|
|
108
116
|
*/
|
|
109
117
|
skipMethods?: Array<PersistenceConformanceMethodKey>
|
|
110
118
|
}
|
|
@@ -170,6 +178,15 @@ export function runPersistenceConformance(
|
|
|
170
178
|
)
|
|
171
179
|
}
|
|
172
180
|
|
|
181
|
+
// Subagent support is opt-in. A store without `listByParentRun` skips the
|
|
182
|
+
// subagent checks (vitest reports them as skipped) and needs no
|
|
183
|
+
// `skipMethods` entry.
|
|
184
|
+
function supportsSubagents(
|
|
185
|
+
runs: RunStore,
|
|
186
|
+
): runs is RunStore & Required<Pick<RunStore, 'listByParentRun'>> {
|
|
187
|
+
return typeof runs.listByParentRun === 'function'
|
|
188
|
+
}
|
|
189
|
+
|
|
173
190
|
describe('messages', () => {
|
|
174
191
|
// One-argument loadThread is the full-thread contract. Paging
|
|
175
192
|
// (`limit` / `before`) is an optional hint; this suite does not require it.
|
|
@@ -368,6 +385,66 @@ export function runPersistenceConformance(
|
|
|
368
385
|
})
|
|
369
386
|
})
|
|
370
387
|
|
|
388
|
+
// `parentRunId`, `subagentRunId`, and `name` travel with createOrResume.
|
|
389
|
+
// A parent row omits them. A child row keeps the values from the first
|
|
390
|
+
// insert. A later createOrResume for that runId must not overwrite them.
|
|
391
|
+
// Only a store with subagent support (`listByParentRun`) must keep them.
|
|
392
|
+
it('round-trips subagent link fields and ignores them on resume', async (ctx) => {
|
|
393
|
+
const store = resolveStore('runs')
|
|
394
|
+
if (!store) return ctx.skip('store not provided')
|
|
395
|
+
if (!supportsSubagents(store)) {
|
|
396
|
+
return ctx.skip('runs.listByParentRun not implemented')
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const parent = await store.createOrResume({
|
|
400
|
+
runId: 'lp-parent',
|
|
401
|
+
threadId: 'lp-thread',
|
|
402
|
+
startedAt: 1,
|
|
403
|
+
})
|
|
404
|
+
expect(parent.parentRunId).toBeUndefined()
|
|
405
|
+
expect(parent.subagentRunId).toBeUndefined()
|
|
406
|
+
expect(parent.name).toBeUndefined()
|
|
407
|
+
|
|
408
|
+
const child = await store.createOrResume({
|
|
409
|
+
runId: 'lp-child',
|
|
410
|
+
threadId: 'subagent:lp-child',
|
|
411
|
+
startedAt: 2,
|
|
412
|
+
parentRunId: 'lp-parent',
|
|
413
|
+
subagentRunId: 'lp-sub',
|
|
414
|
+
name: 'researcher',
|
|
415
|
+
})
|
|
416
|
+
expect(child).toMatchObject({
|
|
417
|
+
runId: 'lp-child',
|
|
418
|
+
threadId: 'subagent:lp-child',
|
|
419
|
+
startedAt: 2,
|
|
420
|
+
parentRunId: 'lp-parent',
|
|
421
|
+
subagentRunId: 'lp-sub',
|
|
422
|
+
name: 'researcher',
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const resumed = await store.createOrResume({
|
|
426
|
+
runId: 'lp-child',
|
|
427
|
+
threadId: 'lp-other-thread',
|
|
428
|
+
startedAt: 99,
|
|
429
|
+
parentRunId: 'lp-other-parent',
|
|
430
|
+
subagentRunId: 'lp-other-sub',
|
|
431
|
+
name: 'writer',
|
|
432
|
+
})
|
|
433
|
+
expect(resumed).toMatchObject({
|
|
434
|
+
runId: 'lp-child',
|
|
435
|
+
threadId: 'subagent:lp-child',
|
|
436
|
+
startedAt: 2,
|
|
437
|
+
parentRunId: 'lp-parent',
|
|
438
|
+
subagentRunId: 'lp-sub',
|
|
439
|
+
name: 'researcher',
|
|
440
|
+
})
|
|
441
|
+
expect(await store.get('lp-child')).toMatchObject({
|
|
442
|
+
parentRunId: 'lp-parent',
|
|
443
|
+
subagentRunId: 'lp-sub',
|
|
444
|
+
name: 'researcher',
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
|
|
371
448
|
it('findActiveRun returns the most recent running run for a thread', async (ctx) => {
|
|
372
449
|
const store = resolveStore('runs')
|
|
373
450
|
if (!store) return ctx.skip('store not provided')
|
|
@@ -443,6 +520,52 @@ export function runPersistenceConformance(
|
|
|
443
520
|
expect(listed.map((r) => r.runId)).toEqual(['lt-a', 'lt-b'])
|
|
444
521
|
})
|
|
445
522
|
|
|
523
|
+
// `listByParentRun` is optional and is skipped when absent. A store that
|
|
524
|
+
// has it returns only that parent's children, oldest `startedAt` first,
|
|
525
|
+
// and [] for an unknown parent.
|
|
526
|
+
it('lists child runs by parent when supported', async (ctx) => {
|
|
527
|
+
const runs = resolveStore('runs')
|
|
528
|
+
if (!runs) return ctx.skip('store not provided')
|
|
529
|
+
if (!supportsSubagents(runs)) {
|
|
530
|
+
return ctx.skip('runs.listByParentRun not implemented')
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
await runs.createOrResume({
|
|
534
|
+
runId: 'lbp-parent',
|
|
535
|
+
threadId: 'lbp-thread',
|
|
536
|
+
startedAt: 1,
|
|
537
|
+
})
|
|
538
|
+
await runs.createOrResume({
|
|
539
|
+
runId: 'lbp-b',
|
|
540
|
+
threadId: 'subagent:lbp-b',
|
|
541
|
+
startedAt: 20,
|
|
542
|
+
parentRunId: 'lbp-parent',
|
|
543
|
+
subagentRunId: 'lbp-sub-b',
|
|
544
|
+
name: 'seo',
|
|
545
|
+
})
|
|
546
|
+
await runs.createOrResume({
|
|
547
|
+
runId: 'lbp-a',
|
|
548
|
+
threadId: 'subagent:lbp-a',
|
|
549
|
+
startedAt: 10,
|
|
550
|
+
parentRunId: 'lbp-parent',
|
|
551
|
+
subagentRunId: 'lbp-sub-a',
|
|
552
|
+
name: 'researcher',
|
|
553
|
+
})
|
|
554
|
+
await runs.createOrResume({
|
|
555
|
+
runId: 'lbp-other',
|
|
556
|
+
threadId: 'subagent:lbp-other',
|
|
557
|
+
startedAt: 5,
|
|
558
|
+
parentRunId: 'lbp-elsewhere',
|
|
559
|
+
subagentRunId: 'lbp-sub-other',
|
|
560
|
+
name: 'writer',
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
const listed = await runs.listByParentRun('lbp-parent')
|
|
564
|
+
expect(listed.map((run) => run.runId)).toEqual(['lbp-a', 'lbp-b'])
|
|
565
|
+
expect(listed.map((run) => run.name)).toEqual(['researcher', 'seo'])
|
|
566
|
+
expect(await runs.listByParentRun('lbp-missing')).toEqual([])
|
|
567
|
+
})
|
|
568
|
+
|
|
446
569
|
// `listReclaimable` is optional on the RunStore contract; a declared
|
|
447
570
|
// omission is reported as skipped and an undeclared one fails. Any
|
|
448
571
|
// backend that has it must
|