@namzu/sdk 1.1.0 → 1.3.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/CHANGELOG.md +98 -0
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +2 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +4 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/{runtime/query → constants}/continuation.d.ts +5 -0
- package/dist/constants/continuation.d.ts.map +1 -0
- package/dist/{runtime/query → constants}/continuation.js +5 -0
- package/dist/constants/continuation.js.map +1 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.d.ts.map +1 -1
- package/dist/constants/index.js +1 -0
- package/dist/constants/index.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +11 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +30 -1
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/provider/__tests__/registry.test.d.ts +5 -0
- package/dist/provider/__tests__/registry.test.d.ts.map +1 -1
- package/dist/provider/__tests__/registry.test.js +186 -1
- package/dist/provider/__tests__/registry.test.js.map +1 -1
- package/dist/provider/capabilities.d.ts +27 -0
- package/dist/provider/capabilities.d.ts.map +1 -0
- package/dist/provider/capabilities.js +29 -0
- package/dist/provider/capabilities.js.map +1 -0
- package/dist/provider/index.d.ts +3 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +2 -1
- package/dist/provider/index.js.map +1 -1
- package/dist/provider/registry.d.ts +81 -1
- package/dist/provider/registry.d.ts.map +1 -1
- package/dist/provider/registry.js +173 -7
- package/dist/provider/registry.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +8 -0
- package/dist/run/reporter.js.map +1 -1
- package/dist/runtime/query/__tests__/capability-negotiation.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.js +184 -0
- package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -0
- package/dist/runtime/query/__tests__/checkpoint-store.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/checkpoint-store.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/checkpoint-store.test.js +259 -0
- package/dist/runtime/query/__tests__/checkpoint-store.test.js.map +1 -0
- package/dist/runtime/query/__tests__/checkpoint.test.js +9 -3
- package/dist/runtime/query/__tests__/checkpoint.test.js.map +1 -1
- package/dist/runtime/query/__tests__/orphan-task-turn-end.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/orphan-task-turn-end.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/orphan-task-turn-end.test.js +110 -0
- package/dist/runtime/query/__tests__/orphan-task-turn-end.test.js.map +1 -0
- package/dist/runtime/query/checkpoint.d.ts +16 -2
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +21 -7
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/context.d.ts +7 -0
- package/dist/runtime/query/context.d.ts.map +1 -1
- package/dist/runtime/query/context.js +1 -0
- package/dist/runtime/query/context.js.map +1 -1
- package/dist/runtime/query/index.d.ts +20 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +75 -6
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +3 -57
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +246 -735
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/checkpoint.js +22 -0
- package/dist/runtime/query/iteration/phases/checkpoint.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +1 -2
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts +35 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -0
- package/dist/runtime/query/iteration/stream-turn.js +341 -0
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -0
- package/dist/runtime/query/replay/list.d.ts +17 -1
- package/dist/runtime/query/replay/list.d.ts.map +1 -1
- package/dist/runtime/query/replay/list.js +21 -4
- package/dist/runtime/query/replay/list.js.map +1 -1
- package/dist/runtime/query/replay/prepare.d.ts +10 -0
- package/dist/runtime/query/replay/prepare.d.ts.map +1 -1
- package/dist/runtime/query/replay/prepare.js +20 -4
- package/dist/runtime/query/replay/prepare.js.map +1 -1
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/run/checkpoint-disk.d.ts +28 -0
- package/dist/store/run/checkpoint-disk.d.ts.map +1 -0
- package/dist/store/run/checkpoint-disk.js +55 -0
- package/dist/store/run/checkpoint-disk.js.map +1 -0
- package/dist/types/provider/config.d.ts +51 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/provider/index.d.ts +1 -1
- package/dist/types/provider/index.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +10 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/run/checkpoint-store.d.ts +64 -0
- package/dist/types/run/checkpoint-store.d.ts.map +1 -0
- package/dist/types/run/checkpoint-store.js +12 -0
- package/dist/types/run/checkpoint-store.js.map +1 -0
- package/dist/types/run/config.d.ts +27 -0
- package/dist/types/run/config.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +6 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/run/index.d.ts +1 -0
- package/dist/types/run/index.d.ts.map +1 -1
- package/dist/types/run/index.js +1 -0
- package/dist/types/run/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bridge/a2a/mapper.ts +3 -0
- package/src/bridge/sse/mapper.ts +5 -0
- package/src/{runtime/query → constants}/continuation.ts +5 -0
- package/src/constants/index.ts +1 -0
- package/src/manager/run/persistence.ts +34 -1
- package/src/provider/__tests__/registry.test.ts +266 -1
- package/src/provider/capabilities.ts +47 -0
- package/src/provider/index.ts +12 -1
- package/src/provider/registry.ts +202 -7
- package/src/public-runtime.ts +6 -0
- package/src/run/reporter.ts +9 -0
- package/src/runtime/query/__tests__/capability-negotiation.test.ts +248 -0
- package/src/runtime/query/__tests__/checkpoint-store.test.ts +322 -0
- package/src/runtime/query/__tests__/checkpoint.test.ts +15 -6
- package/src/runtime/query/__tests__/orphan-task-turn-end.test.ts +127 -0
- package/src/runtime/query/checkpoint.ts +23 -9
- package/src/runtime/query/context.ts +9 -0
- package/src/runtime/query/index.ts +115 -28
- package/src/runtime/query/iteration/index.ts +298 -905
- package/src/runtime/query/iteration/phases/checkpoint.ts +25 -0
- package/src/runtime/query/iteration/phases/context.ts +1 -3
- package/src/runtime/query/iteration/stream-turn.ts +396 -0
- package/src/runtime/query/replay/list.ts +38 -4
- package/src/runtime/query/replay/prepare.ts +37 -4
- package/src/store/index.ts +1 -0
- package/src/store/run/checkpoint-disk.ts +67 -0
- package/src/types/provider/config.ts +54 -0
- package/src/types/provider/index.ts +3 -0
- package/src/types/provider/interface.ts +11 -0
- package/src/types/run/checkpoint-store.ts +72 -0
- package/src/types/run/config.ts +30 -0
- package/src/types/run/events.ts +13 -0
- package/src/types/run/index.ts +1 -0
- package/dist/runtime/query/continuation.d.ts.map +0 -1
- package/dist/runtime/query/continuation.js.map +0 -1
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
import type { RunPersistence } from '../../../manager/run/persistence.js'
|
|
8
|
+
import { ToolRegistry } from '../../../registry/tool/execute.js'
|
|
9
|
+
import type { CheckpointId, IterationCheckpoint } from '../../../types/hitl/index.js'
|
|
10
|
+
import type { RunId, SessionId, TenantId } from '../../../types/ids/index.js'
|
|
11
|
+
import { createUserMessage } from '../../../types/message/index.js'
|
|
12
|
+
import type { LLMProvider, StreamChunk } from '../../../types/provider/index.js'
|
|
13
|
+
import type { CheckpointRunScope, CheckpointStore } from '../../../types/run/checkpoint-store.js'
|
|
14
|
+
import type { RunEvent } from '../../../types/run/index.js'
|
|
15
|
+
import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
|
|
16
|
+
import { CheckpointManager } from '../checkpoint.js'
|
|
17
|
+
import { drainQuery } from '../index.js'
|
|
18
|
+
import { runIterationCheckpoint } from '../iteration/phases/checkpoint.js'
|
|
19
|
+
import type { IterationContext } from '../iteration/phases/context.js'
|
|
20
|
+
|
|
21
|
+
const ZERO_USAGE = {
|
|
22
|
+
promptTokens: 0,
|
|
23
|
+
completionTokens: 0,
|
|
24
|
+
totalTokens: 0,
|
|
25
|
+
cachedTokens: 0,
|
|
26
|
+
cacheWriteTokens: 0,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ZERO_COST = {
|
|
30
|
+
inputCostPer1M: 0,
|
|
31
|
+
outputCostPer1M: 0,
|
|
32
|
+
totalCost: 0,
|
|
33
|
+
cacheDiscount: 0,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SCOPE: CheckpointRunScope = {
|
|
37
|
+
tenantId: 'tnt_cp_store' as TenantId,
|
|
38
|
+
projectId: 'prj_cp_store' as ProjectId,
|
|
39
|
+
sessionId: 'ses_cp_store' as SessionId,
|
|
40
|
+
runId: 'run_cp_store' as RunId,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* In-memory {@link CheckpointStore} used both as the conformance fixture for
|
|
45
|
+
* `CheckpointManager` and as the injected store in the query()-level test.
|
|
46
|
+
* Keys rows by the full scope so the tests can assert the kernel passes the
|
|
47
|
+
* five-layer attribution through, not just the checkpoint payload.
|
|
48
|
+
*/
|
|
49
|
+
class InMemoryCheckpointStore implements CheckpointStore {
|
|
50
|
+
readonly rows = new Map<string, IterationCheckpoint>()
|
|
51
|
+
readonly seenScopes: CheckpointRunScope[] = []
|
|
52
|
+
|
|
53
|
+
private key(scope: CheckpointRunScope, checkpointId: CheckpointId): string {
|
|
54
|
+
return [scope.tenantId, scope.projectId, scope.sessionId, scope.runId, checkpointId].join('/')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private runPrefix(scope: CheckpointRunScope): string {
|
|
58
|
+
return `${[scope.tenantId, scope.projectId, scope.sessionId, scope.runId].join('/')}/`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async writeCheckpoint(scope: CheckpointRunScope, checkpoint: IterationCheckpoint): Promise<void> {
|
|
62
|
+
this.seenScopes.push(scope)
|
|
63
|
+
this.rows.set(this.key(scope, checkpoint.id), checkpoint)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async readCheckpoint(
|
|
67
|
+
scope: CheckpointRunScope,
|
|
68
|
+
checkpointId: CheckpointId,
|
|
69
|
+
): Promise<IterationCheckpoint | null> {
|
|
70
|
+
return this.rows.get(this.key(scope, checkpointId)) ?? null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async listCheckpoints(scope: CheckpointRunScope): Promise<IterationCheckpoint[]> {
|
|
74
|
+
const prefix = this.runPrefix(scope)
|
|
75
|
+
return [...this.rows.entries()]
|
|
76
|
+
.filter(([key]) => key.startsWith(prefix))
|
|
77
|
+
.map(([, cp]) => cp)
|
|
78
|
+
.sort((a, b) => a.createdAt - b.createdAt)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async deleteCheckpoint(scope: CheckpointRunScope, checkpointId: CheckpointId): Promise<void> {
|
|
82
|
+
this.rows.delete(this.key(scope, checkpointId))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function makeRunMgrStub(): RunPersistence {
|
|
87
|
+
return {
|
|
88
|
+
id: SCOPE.runId,
|
|
89
|
+
messages: [{ role: 'user', content: 'hello' }],
|
|
90
|
+
tokenUsage: { ...ZERO_USAGE },
|
|
91
|
+
costInfo: { ...ZERO_COST },
|
|
92
|
+
currentIteration: 1,
|
|
93
|
+
getSession: () => ({ startedAt: Date.now() }),
|
|
94
|
+
} as unknown as RunPersistence
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
describe('CheckpointManager against an injected CheckpointStore', () => {
|
|
98
|
+
it('round-trips create → restore → list → prune through the interface', async () => {
|
|
99
|
+
const store = new InMemoryCheckpointStore()
|
|
100
|
+
const mgr = new CheckpointManager(store, SCOPE)
|
|
101
|
+
const runMgr = makeRunMgrStub()
|
|
102
|
+
|
|
103
|
+
const first = await mgr.create(runMgr, 1)
|
|
104
|
+
const second = await mgr.create(runMgr, 2)
|
|
105
|
+
// Deterministic ordering for prune (createdAt can tie at ms resolution).
|
|
106
|
+
const storedFirst = store.rows.get(
|
|
107
|
+
[SCOPE.tenantId, SCOPE.projectId, SCOPE.sessionId, SCOPE.runId, first.id].join('/'),
|
|
108
|
+
)
|
|
109
|
+
const storedSecond = store.rows.get(
|
|
110
|
+
[SCOPE.tenantId, SCOPE.projectId, SCOPE.sessionId, SCOPE.runId, second.id].join('/'),
|
|
111
|
+
)
|
|
112
|
+
if (storedFirst) storedFirst.createdAt = 1_000
|
|
113
|
+
if (storedSecond) storedSecond.createdAt = 2_000
|
|
114
|
+
|
|
115
|
+
// restore reads back through the interface
|
|
116
|
+
const restored = await mgr.restore(first.id)
|
|
117
|
+
expect(restored.id).toBe(first.id)
|
|
118
|
+
expect(restored.iteration).toBe(1)
|
|
119
|
+
|
|
120
|
+
// list sees both, oldest first
|
|
121
|
+
const listed = await mgr.list()
|
|
122
|
+
expect(listed.map((cp) => cp.id)).toEqual([first.id, second.id])
|
|
123
|
+
|
|
124
|
+
// prune deletes oldest-first through deleteCheckpoint
|
|
125
|
+
await mgr.prune(1)
|
|
126
|
+
const remaining = await mgr.list()
|
|
127
|
+
expect(remaining.map((cp) => cp.id)).toEqual([second.id])
|
|
128
|
+
|
|
129
|
+
// every write carried the full five-layer scope
|
|
130
|
+
expect(store.seenScopes.every((s) => s.tenantId === SCOPE.tenantId)).toBe(true)
|
|
131
|
+
expect(store.seenScopes.every((s) => s.sessionId === SCOPE.sessionId)).toBe(true)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('restore throws a descriptive error for a missing checkpoint', async () => {
|
|
135
|
+
const mgr = new CheckpointManager(new InMemoryCheckpointStore(), SCOPE)
|
|
136
|
+
await expect(mgr.restore('cp_missing' as CheckpointId)).rejects.toThrow(
|
|
137
|
+
'Checkpoint not found: cp_missing',
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// ─── cadence + prune via the iteration-checkpoint phase ──────────────────────
|
|
143
|
+
|
|
144
|
+
function makePhaseContext(
|
|
145
|
+
store: InMemoryCheckpointStore,
|
|
146
|
+
runConfig: { checkpointEvery?: number; pruneKeepLast?: number },
|
|
147
|
+
): { ctx: IterationContext; events: RunEvent[] } {
|
|
148
|
+
const events: RunEvent[] = []
|
|
149
|
+
const ctx = {
|
|
150
|
+
runConfig: {
|
|
151
|
+
model: 'mock-model',
|
|
152
|
+
timeoutMs: 5_000,
|
|
153
|
+
tokenBudget: 100_000,
|
|
154
|
+
...runConfig,
|
|
155
|
+
},
|
|
156
|
+
runMgr: makeRunMgrStub(),
|
|
157
|
+
checkpointMgr: new CheckpointManager(store, SCOPE),
|
|
158
|
+
emitEvent: async (event: RunEvent) => {
|
|
159
|
+
events.push(event)
|
|
160
|
+
},
|
|
161
|
+
drainPending: function* (): Generator<RunEvent> {},
|
|
162
|
+
resumeHandler: async () => ({ action: 'continue' as const }),
|
|
163
|
+
} as unknown as IterationContext
|
|
164
|
+
return { ctx, events }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function drivePhase(ctx: IterationContext, iterationNum: number): Promise<void> {
|
|
168
|
+
const gen = runIterationCheckpoint(ctx, iterationNum)
|
|
169
|
+
let result = await gen.next()
|
|
170
|
+
while (!result.done) {
|
|
171
|
+
result = await gen.next()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
describe('iteration checkpoint cadence (checkpointEvery)', () => {
|
|
176
|
+
it('defaults to a checkpoint on every iteration', async () => {
|
|
177
|
+
const store = new InMemoryCheckpointStore()
|
|
178
|
+
const { ctx, events } = makePhaseContext(store, {})
|
|
179
|
+
|
|
180
|
+
await drivePhase(ctx, 1)
|
|
181
|
+
await drivePhase(ctx, 2)
|
|
182
|
+
await drivePhase(ctx, 3)
|
|
183
|
+
|
|
184
|
+
expect(store.rows.size).toBe(3)
|
|
185
|
+
expect(events.filter((e) => e.type === 'checkpoint_created')).toHaveLength(3)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('checkpointEvery: 2 checkpoints iterations 1, 3, 5 and skips the rest', async () => {
|
|
189
|
+
const store = new InMemoryCheckpointStore()
|
|
190
|
+
const { ctx, events } = makePhaseContext(store, { checkpointEvery: 2 })
|
|
191
|
+
|
|
192
|
+
for (const iteration of [1, 2, 3, 4, 5]) {
|
|
193
|
+
await drivePhase(ctx, iteration)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const created = events.filter(
|
|
197
|
+
(e): e is Extract<RunEvent, { type: 'checkpoint_created' }> =>
|
|
198
|
+
e.type === 'checkpoint_created',
|
|
199
|
+
)
|
|
200
|
+
expect(created.map((e) => e.iteration)).toEqual([1, 3, 5])
|
|
201
|
+
expect(store.rows.size).toBe(3)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('pruneKeepLast keeps only the newest N checkpoints after each create', async () => {
|
|
205
|
+
const store = new InMemoryCheckpointStore()
|
|
206
|
+
const { ctx } = makePhaseContext(store, { pruneKeepLast: 2 })
|
|
207
|
+
|
|
208
|
+
// Make createdAt strictly increasing so prune order is deterministic.
|
|
209
|
+
let tick = 0
|
|
210
|
+
const originalWrite = store.writeCheckpoint.bind(store)
|
|
211
|
+
store.writeCheckpoint = async (scope, checkpoint) => {
|
|
212
|
+
tick += 1
|
|
213
|
+
await originalWrite(scope, { ...checkpoint, createdAt: tick })
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await drivePhase(ctx, 1)
|
|
217
|
+
await drivePhase(ctx, 2)
|
|
218
|
+
await drivePhase(ctx, 3)
|
|
219
|
+
await drivePhase(ctx, 4)
|
|
220
|
+
|
|
221
|
+
const remaining = await store.listCheckpoints(SCOPE)
|
|
222
|
+
expect(remaining).toHaveLength(2)
|
|
223
|
+
expect(remaining.map((cp) => cp.iteration)).toEqual([3, 4])
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// ─── query()-level injection ─────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
class OneToolCallProvider implements LLMProvider {
|
|
230
|
+
readonly id = 'one-tool-call'
|
|
231
|
+
readonly name = 'One Tool Call Provider'
|
|
232
|
+
calls = 0
|
|
233
|
+
|
|
234
|
+
async *chatStream(): AsyncIterable<StreamChunk> {
|
|
235
|
+
this.calls += 1
|
|
236
|
+
|
|
237
|
+
if (this.calls === 1) {
|
|
238
|
+
yield {
|
|
239
|
+
id: 'msg_1',
|
|
240
|
+
delta: {
|
|
241
|
+
toolCalls: [
|
|
242
|
+
{
|
|
243
|
+
index: 0,
|
|
244
|
+
id: 'toolu_echo_1',
|
|
245
|
+
type: 'function',
|
|
246
|
+
function: { name: 'echo', arguments: '{"text":"hi"}' },
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
yield {
|
|
252
|
+
id: 'msg_1',
|
|
253
|
+
delta: { toolCallEnd: { index: 0, id: 'toolu_echo_1' } },
|
|
254
|
+
}
|
|
255
|
+
yield {
|
|
256
|
+
id: 'msg_1',
|
|
257
|
+
delta: {},
|
|
258
|
+
finishReason: 'tool_calls',
|
|
259
|
+
usage: ZERO_USAGE,
|
|
260
|
+
}
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
yield { id: 'msg_2', delta: { content: 'done' } }
|
|
265
|
+
yield { id: 'msg_2', delta: {}, finishReason: 'stop', usage: ZERO_USAGE }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
describe('query() with an injected checkpointStore', () => {
|
|
270
|
+
let workdirs: string[] = []
|
|
271
|
+
|
|
272
|
+
afterEach(async () => {
|
|
273
|
+
await Promise.all(workdirs.map((dir) => rm(dir, { recursive: true, force: true })))
|
|
274
|
+
workdirs = []
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('persists iteration checkpoints into the injected store, keyed by run scope', async () => {
|
|
278
|
+
const provider = new OneToolCallProvider()
|
|
279
|
+
const store = new InMemoryCheckpointStore()
|
|
280
|
+
const tools = new ToolRegistry()
|
|
281
|
+
tools.register({
|
|
282
|
+
name: 'echo',
|
|
283
|
+
description: 'echo the text back',
|
|
284
|
+
inputSchema: z.object({ text: z.string() }),
|
|
285
|
+
execute: async () => ({ success: true, output: 'hi' }),
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-checkpoint-store-'))
|
|
289
|
+
workdirs.push(workingDirectory)
|
|
290
|
+
|
|
291
|
+
const run = await drainQuery({
|
|
292
|
+
provider,
|
|
293
|
+
tools,
|
|
294
|
+
checkpointStore: store,
|
|
295
|
+
runConfig: {
|
|
296
|
+
model: 'mock-model',
|
|
297
|
+
timeoutMs: 5_000,
|
|
298
|
+
tokenBudget: 100_000,
|
|
299
|
+
maxIterations: 3,
|
|
300
|
+
maxResponseTokens: 256,
|
|
301
|
+
},
|
|
302
|
+
agentId: 'agent_test',
|
|
303
|
+
agentName: 'Test Agent',
|
|
304
|
+
messages: [createUserMessage('use the echo tool')],
|
|
305
|
+
workingDirectory,
|
|
306
|
+
sessionId: 'ses_cp_inject' as SessionId,
|
|
307
|
+
threadId: 'thd_cp_inject' as ThreadId,
|
|
308
|
+
projectId: 'prj_cp_inject' as ProjectId,
|
|
309
|
+
tenantId: 'tnt_cp_inject' as TenantId,
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
expect(run.status).toBe('completed')
|
|
313
|
+
// The tool-call iteration produced at least one checkpoint, and it
|
|
314
|
+
// landed in the injected store — not on disk.
|
|
315
|
+
expect(store.rows.size).toBeGreaterThan(0)
|
|
316
|
+
const scope = store.seenScopes[0]
|
|
317
|
+
expect(scope?.tenantId).toBe('tnt_cp_inject')
|
|
318
|
+
expect(scope?.projectId).toBe('prj_cp_inject')
|
|
319
|
+
expect(scope?.sessionId).toBe('ses_cp_inject')
|
|
320
|
+
expect(scope?.runId).toBe(run.id)
|
|
321
|
+
})
|
|
322
|
+
})
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import type { RunDiskStore } from '../../../store/run/disk.js'
|
|
3
2
|
import type { CheckpointId, IterationCheckpoint } from '../../../types/hitl/index.js'
|
|
4
|
-
import type { EmergencySaveId, RunId } from '../../../types/ids/index.js'
|
|
3
|
+
import type { EmergencySaveId, RunId, SessionId, TenantId } from '../../../types/ids/index.js'
|
|
4
|
+
import type { CheckpointRunScope, CheckpointStore } from '../../../types/run/checkpoint-store.js'
|
|
5
5
|
import type { EmergencySaveData } from '../../../types/run/emergency.js'
|
|
6
|
+
import type { ProjectId } from '../../../types/session/ids.js'
|
|
6
7
|
import { CheckpointManager, projectEmergencyToCheckpoint } from '../checkpoint.js'
|
|
7
8
|
|
|
9
|
+
const TEST_SCOPE: CheckpointRunScope = {
|
|
10
|
+
tenantId: 'tnt_test' as TenantId,
|
|
11
|
+
projectId: 'prj_test' as ProjectId,
|
|
12
|
+
sessionId: 'ses_test' as SessionId,
|
|
13
|
+
runId: 'run_test' as RunId,
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
function makeCheckpoint(overrides: Partial<IterationCheckpoint> = {}): IterationCheckpoint {
|
|
9
17
|
return {
|
|
10
18
|
id: 'cp_test_a' as CheckpointId,
|
|
@@ -30,10 +38,10 @@ function makeCheckpoint(overrides: Partial<IterationCheckpoint> = {}): Iteration
|
|
|
30
38
|
}
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
function makeStoreStub(checkpoints: IterationCheckpoint[]):
|
|
41
|
+
function makeStoreStub(checkpoints: IterationCheckpoint[]): CheckpointStore {
|
|
34
42
|
return {
|
|
35
43
|
listCheckpoints: async () => checkpoints,
|
|
36
|
-
} as unknown as
|
|
44
|
+
} as unknown as CheckpointStore
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
describe('CheckpointManager.listEntries', () => {
|
|
@@ -60,7 +68,7 @@ describe('CheckpointManager.listEntries', () => {
|
|
|
60
68
|
}),
|
|
61
69
|
])
|
|
62
70
|
|
|
63
|
-
const mgr = new CheckpointManager(store)
|
|
71
|
+
const mgr = new CheckpointManager(store, TEST_SCOPE)
|
|
64
72
|
const entries = await mgr.listEntries()
|
|
65
73
|
|
|
66
74
|
expect(entries).toHaveLength(2)
|
|
@@ -81,7 +89,7 @@ describe('CheckpointManager.listEntries', () => {
|
|
|
81
89
|
})
|
|
82
90
|
|
|
83
91
|
it('returns empty array when no checkpoints exist', async () => {
|
|
84
|
-
const mgr = new CheckpointManager(makeStoreStub([]))
|
|
92
|
+
const mgr = new CheckpointManager(makeStoreStub([]), TEST_SCOPE)
|
|
85
93
|
expect(await mgr.listEntries()).toEqual([])
|
|
86
94
|
})
|
|
87
95
|
|
|
@@ -93,6 +101,7 @@ describe('CheckpointManager.listEntries', () => {
|
|
|
93
101
|
branchStack: [{ agentId: 'a', decision: 'd', confidence: 1, timestamp: 0 }],
|
|
94
102
|
}),
|
|
95
103
|
]),
|
|
104
|
+
TEST_SCOPE,
|
|
96
105
|
)
|
|
97
106
|
const [entry] = await mgr.listEntries()
|
|
98
107
|
expect(entry).not.toHaveProperty('tokenUsage')
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises'
|
|
2
|
+
import { mkdtemp } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
6
|
+
|
|
7
|
+
import { ToolRegistry } from '../../../registry/tool/execute.js'
|
|
8
|
+
import type { TaskGateway, TaskHandle } from '../../../types/agent/gateway.js'
|
|
9
|
+
import type { SessionId, TaskId, TenantId } from '../../../types/ids/index.js'
|
|
10
|
+
import { createUserMessage } from '../../../types/message/index.js'
|
|
11
|
+
import type { LLMProvider, StreamChunk } from '../../../types/provider/index.js'
|
|
12
|
+
import type { RunEvent } from '../../../types/run/index.js'
|
|
13
|
+
import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
|
|
14
|
+
import { drainQuery } from '../index.js'
|
|
15
|
+
|
|
16
|
+
const ZERO_USAGE = {
|
|
17
|
+
promptTokens: 0,
|
|
18
|
+
completionTokens: 0,
|
|
19
|
+
totalTokens: 0,
|
|
20
|
+
cachedTokens: 0,
|
|
21
|
+
cacheWriteTokens: 0,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Ends its turn with plain text on the first (and only) call. */
|
|
25
|
+
class SingleTurnProvider implements LLMProvider {
|
|
26
|
+
readonly id = 'single-turn'
|
|
27
|
+
readonly name = 'Single Turn Provider'
|
|
28
|
+
calls = 0
|
|
29
|
+
|
|
30
|
+
async *chatStream(): AsyncIterable<StreamChunk> {
|
|
31
|
+
this.calls += 1
|
|
32
|
+
yield {
|
|
33
|
+
id: `msg_${this.calls}`,
|
|
34
|
+
delta: { content: 'Final answer.' },
|
|
35
|
+
}
|
|
36
|
+
yield {
|
|
37
|
+
id: `msg_${this.calls}`,
|
|
38
|
+
delta: {},
|
|
39
|
+
finishReason: 'stop',
|
|
40
|
+
usage: ZERO_USAGE,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Gateway that permanently reports one running task. Every dispatch
|
|
47
|
+
* tool is blocking, so a running task at end-of-turn is an orphan —
|
|
48
|
+
* there is no notification producer that could ever deliver its
|
|
49
|
+
* result (the listener was removed in dc16d58). The run must NOT
|
|
50
|
+
* busy-wait on it.
|
|
51
|
+
*/
|
|
52
|
+
function orphanTaskGateway(): TaskGateway {
|
|
53
|
+
const handle: TaskHandle = {
|
|
54
|
+
taskId: 'task_orphan' as TaskId,
|
|
55
|
+
agentId: 'agent_worker',
|
|
56
|
+
state: 'running',
|
|
57
|
+
createdAt: Date.now(),
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
createTask: async () => handle,
|
|
61
|
+
waitForTask: () => new Promise<TaskHandle>(() => {}),
|
|
62
|
+
continueTask: async () => {},
|
|
63
|
+
cancelTask: () => {},
|
|
64
|
+
getTask: () => handle,
|
|
65
|
+
listTasks: () => [handle],
|
|
66
|
+
onTaskCompleted: () => () => {},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('end of turn with running agent tasks', () => {
|
|
71
|
+
let workdirs: string[] = []
|
|
72
|
+
|
|
73
|
+
afterEach(async () => {
|
|
74
|
+
await Promise.all(workdirs.map((dir) => rm(dir, { recursive: true, force: true })))
|
|
75
|
+
workdirs = []
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Regression: the loop used to poll `pendingNotifications` every
|
|
79
|
+
// 250ms for up to `runConfig.timeoutMs` (120s default) whenever the
|
|
80
|
+
// turn ended while the gateway still listed a running task — but
|
|
81
|
+
// nothing has pushed onto that queue since dc16d58 removed the
|
|
82
|
+
// onTaskCompleted producer, so the wait always injected nothing and
|
|
83
|
+
// the run hung for minutes. The vitest per-test timeout is the
|
|
84
|
+
// hang detector here: with the busy-wait present this test times
|
|
85
|
+
// out instead of completing.
|
|
86
|
+
it('ends the run promptly instead of busy-waiting on orphan tasks', async () => {
|
|
87
|
+
const provider = new SingleTurnProvider()
|
|
88
|
+
const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-orphan-task-'))
|
|
89
|
+
workdirs.push(workingDirectory)
|
|
90
|
+
const events: RunEvent[] = []
|
|
91
|
+
|
|
92
|
+
const run = await drainQuery(
|
|
93
|
+
{
|
|
94
|
+
provider,
|
|
95
|
+
tools: new ToolRegistry(),
|
|
96
|
+
runConfig: {
|
|
97
|
+
model: 'mock-model',
|
|
98
|
+
// Deliberately longer than the vitest timeout: the old
|
|
99
|
+
// code waited min(timeoutMs, …) polling the dead queue.
|
|
100
|
+
timeoutMs: 120_000,
|
|
101
|
+
tokenBudget: 100_000,
|
|
102
|
+
maxIterations: 3,
|
|
103
|
+
maxResponseTokens: 256,
|
|
104
|
+
},
|
|
105
|
+
agentId: 'agent_test',
|
|
106
|
+
agentName: 'Test Agent',
|
|
107
|
+
messages: [createUserMessage('do the thing')],
|
|
108
|
+
workingDirectory,
|
|
109
|
+
taskGateway: orphanTaskGateway(),
|
|
110
|
+
sessionId: 'ses_orphan_task' as SessionId,
|
|
111
|
+
threadId: 'thd_orphan_task' as ThreadId,
|
|
112
|
+
projectId: 'prj_orphan_task' as ProjectId,
|
|
113
|
+
tenantId: 'tnt_orphan_task' as TenantId,
|
|
114
|
+
},
|
|
115
|
+
(event) => {
|
|
116
|
+
events.push(event)
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
expect(run.status).toBe('completed')
|
|
121
|
+
expect(run.stopReason).toBe('end_turn')
|
|
122
|
+
expect(run.result).toBe('Final answer.')
|
|
123
|
+
// One turn only — no futile re-invocation loop on the orphan.
|
|
124
|
+
expect(provider.calls).toBe(1)
|
|
125
|
+
expect(events.some((event) => event.type === 'run_failed')).toBe(false)
|
|
126
|
+
}, 10_000)
|
|
127
|
+
})
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { RunPersistence } from '../../manager/run/persistence.js'
|
|
2
|
-
import type { RunDiskStore } from '../../store/run/disk.js'
|
|
3
2
|
import type {
|
|
4
3
|
ActiveNodeInfo,
|
|
5
4
|
BranchStackEntry,
|
|
@@ -8,13 +7,19 @@ import type {
|
|
|
8
7
|
IterationCheckpoint,
|
|
9
8
|
} from '../../types/hitl/index.js'
|
|
10
9
|
import type { AssistantMessage } from '../../types/message/index.js'
|
|
10
|
+
import type { CheckpointRunScope, CheckpointStore } from '../../types/run/checkpoint-store.js'
|
|
11
11
|
import type { EmergencySaveData } from '../../types/run/emergency.js'
|
|
12
12
|
import type { CheckpointListEntry } from '../../types/run/replay.js'
|
|
13
13
|
import { ZERO_COST } from '../../utils/cost.js'
|
|
14
14
|
import { buildToolResultHashes } from '../../utils/hash.js'
|
|
15
15
|
import { generateCheckpointId } from '../../utils/id.js'
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Projection from a full checkpoint payload to the public listing entry.
|
|
19
|
+
* Exported for the replay `listCheckpoints` entry point, which projects
|
|
20
|
+
* store results without constructing a `CheckpointManager`.
|
|
21
|
+
*/
|
|
22
|
+
export function toCheckpointListEntry(cp: IterationCheckpoint): CheckpointListEntry {
|
|
18
23
|
return {
|
|
19
24
|
id: cp.id,
|
|
20
25
|
runId: cp.runId,
|
|
@@ -55,10 +60,19 @@ export function projectEmergencyToCheckpoint(dump: EmergencySaveData): Iteration
|
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
export class CheckpointManager {
|
|
58
|
-
private store:
|
|
63
|
+
private store: CheckpointStore
|
|
64
|
+
private scope: CheckpointRunScope
|
|
59
65
|
|
|
60
|
-
|
|
66
|
+
/**
|
|
67
|
+
* @param store scope-keyed checkpoint persistence. The default query
|
|
68
|
+
* pipeline passes the run's disk-backed store
|
|
69
|
+
* ({@link import('../../store/run/checkpoint-disk.js').DiskCheckpointStore});
|
|
70
|
+
* hosts inject their own via `QueryParams.checkpointStore`.
|
|
71
|
+
* @param scope the run every operation of this manager is keyed to.
|
|
72
|
+
*/
|
|
73
|
+
constructor(store: CheckpointStore, scope: CheckpointRunScope) {
|
|
61
74
|
this.store = store
|
|
75
|
+
this.scope = scope
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
async create(
|
|
@@ -87,12 +101,12 @@ export class CheckpointManager {
|
|
|
87
101
|
activeNode: extra?.activeNode,
|
|
88
102
|
}
|
|
89
103
|
|
|
90
|
-
await this.store.writeCheckpoint(checkpoint)
|
|
104
|
+
await this.store.writeCheckpoint(this.scope, checkpoint)
|
|
91
105
|
return checkpoint
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
async restore(checkpointId: CheckpointId): Promise<IterationCheckpoint> {
|
|
95
|
-
const checkpoint = await this.store.readCheckpoint(checkpointId)
|
|
109
|
+
const checkpoint = await this.store.readCheckpoint(this.scope, checkpointId)
|
|
96
110
|
if (!checkpoint) {
|
|
97
111
|
throw new Error(`Checkpoint not found: ${checkpointId}`)
|
|
98
112
|
}
|
|
@@ -100,7 +114,7 @@ export class CheckpointManager {
|
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
async list(): Promise<IterationCheckpoint[]> {
|
|
103
|
-
return this.store.listCheckpoints()
|
|
117
|
+
return this.store.listCheckpoints(this.scope)
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
/**
|
|
@@ -110,7 +124,7 @@ export class CheckpointManager {
|
|
|
110
124
|
* full checkpoint payload. See ses_005-deterministic-replay design §3.1.
|
|
111
125
|
*/
|
|
112
126
|
async listEntries(): Promise<CheckpointListEntry[]> {
|
|
113
|
-
const checkpoints = await this.store.listCheckpoints()
|
|
127
|
+
const checkpoints = await this.store.listCheckpoints(this.scope)
|
|
114
128
|
return checkpoints.map(toCheckpointListEntry)
|
|
115
129
|
}
|
|
116
130
|
|
|
@@ -121,7 +135,7 @@ export class CheckpointManager {
|
|
|
121
135
|
const toDelete = all.sort((a, b) => a.createdAt - b.createdAt).slice(0, all.length - keepLast)
|
|
122
136
|
|
|
123
137
|
for (const cp of toDelete) {
|
|
124
|
-
await this.store.deleteCheckpoint(cp.id)
|
|
138
|
+
await this.store.deleteCheckpoint(this.scope, cp.id)
|
|
125
139
|
}
|
|
126
140
|
}
|
|
127
141
|
|
|
@@ -15,6 +15,7 @@ import type { RunId, SessionId, TenantId } from '../../types/ids/index.js'
|
|
|
15
15
|
import type { Message } from '../../types/message/index.js'
|
|
16
16
|
import type { PermissionMode } from '../../types/permission/index.js'
|
|
17
17
|
import type { LLMProvider } from '../../types/provider/index.js'
|
|
18
|
+
import type { CheckpointStore } from '../../types/run/checkpoint-store.js'
|
|
18
19
|
import type { AgentRunConfig } from '../../types/run/index.js'
|
|
19
20
|
import type { ProjectId, ThreadId } from '../../types/session/ids.js'
|
|
20
21
|
import type { ModelPricing } from '../../utils/cost.js'
|
|
@@ -55,6 +56,13 @@ export interface RunContextConfig {
|
|
|
55
56
|
|
|
56
57
|
pathBuilder?: PathBuilder
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Optional checkpoint persistence override, threaded through to
|
|
61
|
+
* {@link RunPersistence}. Absent ⇒ disk default under the run's
|
|
62
|
+
* output directory.
|
|
63
|
+
*/
|
|
64
|
+
checkpointStore?: CheckpointStore
|
|
65
|
+
|
|
58
66
|
/**
|
|
59
67
|
* Optional injected migrator — tests pass a stub; production code relies
|
|
60
68
|
* on the {@link DefaultFilesystemMigrator}. See session-hierarchy.md
|
|
@@ -162,6 +170,7 @@ export class RunContextFactory {
|
|
|
162
170
|
projectId: config.projectId,
|
|
163
171
|
parentRunId: config.parentRunId,
|
|
164
172
|
depth: config.depth,
|
|
173
|
+
checkpointStore: config.checkpointStore,
|
|
165
174
|
})
|
|
166
175
|
|
|
167
176
|
const trackingConfig = resolveActivityTracking(permissionMode, config.enableActivityTracking)
|