@namzu/sdk 7.0.0 → 9.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/CHANGELOG.md +652 -0
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +186 -163
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/__tests__/a-policy-nobody-can-select.test.d.ts +2 -0
- package/dist/agents/__tests__/a-policy-nobody-can-select.test.d.ts.map +1 -0
- package/dist/agents/__tests__/a-policy-nobody-can-select.test.js +177 -0
- package/dist/agents/__tests__/a-policy-nobody-can-select.test.js.map +1 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts +2 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts.map +1 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.js +125 -0
- package/dist/agents/__tests__/supervisor-inbox-scope.test.js.map +1 -0
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +4 -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 +24 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/contracts/api.d.ts +1 -1
- package/dist/contracts/api.d.ts.map +1 -1
- package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.d.ts +2 -0
- package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.d.ts.map +1 -0
- package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.js +114 -0
- package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.js.map +1 -0
- package/dist/gateway/__tests__/completion-inbox.test.js +292 -2
- package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -1
- package/dist/gateway/completion-inbox.d.ts +94 -6
- package/dist/gateway/completion-inbox.d.ts.map +1 -1
- package/dist/gateway/completion-inbox.js +235 -15
- package/dist/gateway/completion-inbox.js.map +1 -1
- package/dist/gateway/local.d.ts.map +1 -1
- package/dist/gateway/local.js +19 -6
- package/dist/gateway/local.js.map +1 -1
- package/dist/manager/agent/__tests__/lifecycle.test.js +56 -0
- package/dist/manager/agent/__tests__/lifecycle.test.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +62 -33
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.d.ts +2 -0
- package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.d.ts.map +1 -0
- package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.js +73 -0
- package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.js.map +1 -0
- package/dist/manager/plan/lifecycle.d.ts +48 -0
- package/dist/manager/plan/lifecycle.d.ts.map +1 -1
- package/dist/manager/plan/lifecycle.js +56 -0
- package/dist/manager/plan/lifecycle.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +8 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +12 -0
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +11 -0
- package/dist/run/reporter.js.map +1 -1
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js +142 -0
- package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js.map +1 -0
- package/dist/runtime/query/__tests__/completion-notification.test.js +414 -32
- package/dist/runtime/query/__tests__/completion-notification.test.js.map +1 -1
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js +100 -0
- package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js.map +1 -0
- package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.js +62 -0
- package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.js.map +1 -0
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +5 -0
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/guard.d.ts +28 -0
- package/dist/runtime/query/guard.d.ts.map +1 -1
- package/dist/runtime/query/guard.js +31 -0
- package/dist/runtime/query/guard.js.map +1 -1
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts +2 -0
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.js +226 -0
- package/dist/runtime/query/iteration/__tests__/settle-grace.test.js.map +1 -0
- package/dist/runtime/query/iteration/index.d.ts +92 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +814 -566
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js +95 -0
- package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts +34 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +61 -4
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/store/task/disk.d.ts.map +1 -1
- package/dist/store/task/disk.js +14 -0
- package/dist/store/task/disk.js.map +1 -1
- package/dist/store/task/memory.d.ts.map +1 -1
- package/dist/store/task/memory.js +14 -0
- package/dist/store/task/memory.js.map +1 -1
- package/dist/tools/__tests__/untrusted-envelope.test.js +16 -0
- package/dist/tools/__tests__/untrusted-envelope.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.js +101 -0
- package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.js.map +1 -0
- package/dist/tools/coordinator/__tests__/completion-delivery.test.js +117 -0
- package/dist/tools/coordinator/__tests__/completion-delivery.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/task-list.test.js +57 -0
- package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.js +99 -0
- package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.js.map +1 -0
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +17 -23
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +142 -14
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/coordinator/outcome.d.ts +60 -0
- package/dist/tools/coordinator/outcome.d.ts.map +1 -0
- package/dist/tools/coordinator/outcome.js +68 -0
- package/dist/tools/coordinator/outcome.js.map +1 -0
- package/dist/tools/untrusted-envelope.d.ts.map +1 -1
- package/dist/tools/untrusted-envelope.js +9 -1
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/types/agent/gateway.d.ts +37 -2
- package/dist/types/agent/gateway.d.ts.map +1 -1
- package/dist/types/agent/supervisor.d.ts +38 -1
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/plan/index.d.ts +26 -0
- package/dist/types/plan/index.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +35 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/run/entity.d.ts +16 -0
- package/dist/types/run/entity.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +92 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/run/prepare-step.d.ts +17 -2
- package/dist/types/run/prepare-step.d.ts.map +1 -1
- package/dist/types/task/index.d.ts +19 -1
- package/dist/types/task/index.d.ts.map +1 -1
- package/dist/types/task/index.js +10 -1
- package/dist/types/task/index.js.map +1 -1
- package/dist/types/verification/index.d.ts +98 -0
- package/dist/types/verification/index.d.ts.map +1 -1
- package/dist/types/verification/index.js +10 -0
- package/dist/types/verification/index.js.map +1 -1
- package/dist/verification/__tests__/argument-pattern.test.d.ts +2 -0
- package/dist/verification/__tests__/argument-pattern.test.d.ts.map +1 -0
- package/dist/verification/__tests__/argument-pattern.test.js +122 -0
- package/dist/verification/__tests__/argument-pattern.test.js.map +1 -0
- package/dist/verification/gate.d.ts.map +1 -1
- package/dist/verification/gate.js +44 -0
- package/dist/verification/gate.js.map +1 -1
- package/dist/verification/rules.d.ts.map +1 -1
- package/dist/verification/rules.js +27 -0
- package/dist/verification/rules.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/SupervisorAgent.ts +189 -167
- package/src/agents/__tests__/a-policy-nobody-can-select.test.ts +202 -0
- package/src/agents/__tests__/supervisor-inbox-scope.test.ts +149 -0
- package/src/bridge/a2a/mapper.ts +4 -0
- package/src/bridge/sse/mapper.ts +25 -0
- package/src/contracts/api.ts +1 -0
- package/src/gateway/__tests__/a-knob-nobody-can-turn.test.ts +139 -0
- package/src/gateway/__tests__/completion-inbox.test.ts +348 -2
- package/src/gateway/completion-inbox.ts +248 -16
- package/src/gateway/local.ts +19 -7
- package/src/manager/agent/__tests__/lifecycle.test.ts +74 -0
- package/src/manager/agent/lifecycle.ts +72 -42
- package/src/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.ts +88 -0
- package/src/manager/plan/lifecycle.ts +61 -0
- package/src/manager/run/persistence.ts +12 -0
- package/src/run/reporter.ts +12 -0
- package/src/runtime/query/__tests__/completion-does-not-erase-the-answer.test.ts +163 -0
- package/src/runtime/query/__tests__/completion-notification.test.ts +486 -34
- package/src/runtime/query/__tests__/context-size-on-the-wire.test.ts +122 -0
- package/src/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.ts +79 -0
- package/src/runtime/query/events.ts +5 -0
- package/src/runtime/query/guard.ts +32 -0
- package/src/runtime/query/iteration/__tests__/settle-grace.test.ts +265 -0
- package/src/runtime/query/iteration/index.ts +902 -636
- package/src/runtime/query/iteration/phases/__tests__/compaction-declined.test.ts +124 -0
- package/src/runtime/query/iteration/phases/compaction.ts +83 -10
- package/src/store/task/disk.ts +15 -0
- package/src/store/task/memory.ts +15 -0
- package/src/tools/__tests__/untrusted-envelope.test.ts +23 -0
- package/src/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.ts +133 -0
- package/src/tools/coordinator/__tests__/completion-delivery.test.ts +147 -0
- package/src/tools/coordinator/__tests__/task-list.test.ts +72 -0
- package/src/tools/coordinator/__tests__/the-plan-a-human-approves.test.ts +121 -0
- package/src/tools/coordinator/agent.ts +17 -25
- package/src/tools/coordinator/index.ts +155 -18
- package/src/tools/coordinator/outcome.ts +70 -0
- package/src/tools/untrusted-envelope.ts +9 -1
- package/src/types/agent/gateway.ts +37 -2
- package/src/types/agent/supervisor.ts +40 -1
- package/src/types/plan/index.ts +29 -0
- package/src/types/provider/interface.ts +39 -0
- package/src/types/run/entity.ts +17 -0
- package/src/types/run/events.ts +92 -0
- package/src/types/run/prepare-step.ts +17 -2
- package/src/types/task/index.ts +22 -3
- package/src/types/verification/index.ts +61 -0
- package/src/verification/__tests__/argument-pattern.test.ts +158 -0
- package/src/verification/gate.ts +45 -0
- package/src/verification/rules.ts +28 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { RunId } from '../../../types/ids/index.js'
|
|
4
|
+
import { PlanManager } from '../lifecycle.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `completePlan` scored an unreported step as a failure.
|
|
8
|
+
*
|
|
9
|
+
* The test was "is every step completed or skipped", and everything else fell
|
|
10
|
+
* to the same branch — so a step still `pending` produced `failed`. Since
|
|
11
|
+
* `addStep` defaults every step to `pending`, a caller that added steps, did
|
|
12
|
+
* the work, and settled the plan without reporting each one got `failed` for a
|
|
13
|
+
* plan that had fully succeeded. That is the path of least effort, not an
|
|
14
|
+
* unusual one.
|
|
15
|
+
*
|
|
16
|
+
* A step that FAILED is an outcome. A step nobody reported on is not — it says
|
|
17
|
+
* the caller and the plan disagree about whether the work is over, and
|
|
18
|
+
* answering "failed" settles that by inventing a result.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const RUN = 'run_plan_outcome' as RunId
|
|
22
|
+
|
|
23
|
+
function planWithSteps(count: number): PlanManager {
|
|
24
|
+
const manager = new PlanManager(RUN)
|
|
25
|
+
manager.startGenerating('a plan')
|
|
26
|
+
for (let i = 0; i < count; i += 1) {
|
|
27
|
+
manager.addStep({
|
|
28
|
+
id: `step-${i + 1}`,
|
|
29
|
+
description: `step ${i + 1}`,
|
|
30
|
+
dependsOn: [],
|
|
31
|
+
order: i,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
manager.markReady()
|
|
35
|
+
return manager
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('a plan settles on what its steps actually reported', () => {
|
|
39
|
+
it('completes when every step reported success', () => {
|
|
40
|
+
const manager = planWithSteps(2)
|
|
41
|
+
for (const step of manager.active?.steps ?? []) {
|
|
42
|
+
manager.updateStepStatus(step.id, 'completed')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
expect(manager.completePlan()?.status).toBe('completed')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('counts a skipped step as settled, not as a failure', () => {
|
|
49
|
+
const manager = planWithSteps(2)
|
|
50
|
+
const steps = manager.active?.steps ?? []
|
|
51
|
+
manager.updateStepStatus(steps[0]?.id as string, 'completed')
|
|
52
|
+
manager.updateStepStatus(steps[1]?.id as string, 'skipped')
|
|
53
|
+
|
|
54
|
+
expect(manager.completePlan()?.status).toBe('completed')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('fails when a step actually failed', () => {
|
|
58
|
+
const manager = planWithSteps(2)
|
|
59
|
+
const steps = manager.active?.steps ?? []
|
|
60
|
+
manager.updateStepStatus(steps[0]?.id as string, 'completed')
|
|
61
|
+
manager.updateStepStatus(steps[1]?.id as string, 'failed')
|
|
62
|
+
|
|
63
|
+
expect(manager.completePlan()?.status).toBe('failed')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('refuses rather than scoring a step nobody reported on', () => {
|
|
67
|
+
// The defect, in the shape a caller reaches it: steps added, work done,
|
|
68
|
+
// nothing reported. Answering `failed` here is the invented result.
|
|
69
|
+
const manager = planWithSteps(2)
|
|
70
|
+
manager.updateStepStatus(manager.active?.steps[0]?.id as string, 'completed')
|
|
71
|
+
|
|
72
|
+
expect(() => manager.completePlan()).toThrow(/have not reported an outcome/)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('names the way out rather than only the refusal', () => {
|
|
76
|
+
// A caller in this position either forgot to report progress or called
|
|
77
|
+
// too early, and only they know which — so the message has to carry
|
|
78
|
+
// both moves, not just the complaint.
|
|
79
|
+
const manager = planWithSteps(1)
|
|
80
|
+
|
|
81
|
+
expect(() => manager.completePlan()).toThrow(/updateStepStatus/)
|
|
82
|
+
expect(() => manager.completePlan()).toThrow(/failPlan/)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('still returns null when there is no plan at all', () => {
|
|
86
|
+
expect(new PlanManager(RUN).completePlan()).toBeNull()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -26,6 +26,33 @@ export type PlanEventListener = (event: PlanEvent) => void
|
|
|
26
26
|
|
|
27
27
|
export type PlanApprovalHandler = (request: PlanApprovalRequest) => Promise<PlanApprovalResponse>
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* The plan a run declares, and the gate a host approves it through.
|
|
31
|
+
*
|
|
32
|
+
* **The kernel deliberately drives only part of this class.** It builds a plan
|
|
33
|
+
* (`approve_plan` calls `startGenerating` / `addStep` / `markReady`), gates it
|
|
34
|
+
* (`iteration/phases/context.ts` calls `approve` and `startExecution`),
|
|
35
|
+
* translates its events onto the run stream (`EventTranslator.wirePlanManager`),
|
|
36
|
+
* and settles it on failure (`runtime/query/result.ts` calls `failPlan`). It
|
|
37
|
+
* never reports a step outcome and never settles a plan that succeeded.
|
|
38
|
+
*
|
|
39
|
+
* That is a split, not an omission — `drainQuery` hands the manager to the host
|
|
40
|
+
* through `onContextCreated({ planManager })` BEFORE the iteration loop starts,
|
|
41
|
+
* precisely so a host can drive the half the kernel does not. So a grep for
|
|
42
|
+
* callers of `updateStepStatus` or `completePlan` inside this package finds
|
|
43
|
+
* none, and that is not evidence the methods are dead: the callers are hosts,
|
|
44
|
+
* and they are outside the repository. `PlanManager` is exported from
|
|
45
|
+
* `public-runtime.ts` for this reason.
|
|
46
|
+
*
|
|
47
|
+
* Recorded here because the absence has already been read once as a dead layer
|
|
48
|
+
* and proposed for deletion. What it would have deleted is a working
|
|
49
|
+
* human-in-the-loop approval gate.
|
|
50
|
+
*
|
|
51
|
+
* The one genuine gap in the split is tracked separately: nothing settles a
|
|
52
|
+
* plan that SUCCEEDED, so its status can reach `failed` or stay `executing`
|
|
53
|
+
* but never `completed`. Fixing that needs a decision about what a
|
|
54
|
+
* kernel-built plan's steps mean, not a guessed status — see `completePlan`.
|
|
55
|
+
*/
|
|
29
56
|
export class PlanManager {
|
|
30
57
|
private currentPlan: Plan | null = null
|
|
31
58
|
private runId: RunId
|
|
@@ -185,9 +212,43 @@ export class PlanManager {
|
|
|
185
212
|
return step
|
|
186
213
|
}
|
|
187
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Settle the plan, computing its outcome from its steps.
|
|
217
|
+
*
|
|
218
|
+
* A step that is still `pending` or `running` used to land here as
|
|
219
|
+
* **`failed`**, because the test was "is every step completed or skipped"
|
|
220
|
+
* and anything else fell to the same branch. So a caller that added steps,
|
|
221
|
+
* did the work, and settled the plan without reporting each step got
|
|
222
|
+
* `failed` for a plan that fully succeeded — and `addStep` defaults every
|
|
223
|
+
* step to `pending`, so that is the path of least effort, not an unusual
|
|
224
|
+
* one.
|
|
225
|
+
*
|
|
226
|
+
* The two cases are different facts and want different responses. A step
|
|
227
|
+
* that FAILED is an outcome: the plan failed, report it. A step nobody
|
|
228
|
+
* reported on is not an outcome at all — it says the caller and this plan
|
|
229
|
+
* disagree about whether the work is over, and answering "failed" resolves
|
|
230
|
+
* that disagreement by inventing a result.
|
|
231
|
+
*
|
|
232
|
+
* So an unfinished step is refused rather than scored. The message names
|
|
233
|
+
* the steps and the two ways out, because a caller in this position either
|
|
234
|
+
* forgot to report progress or called too early, and only they know which.
|
|
235
|
+
*/
|
|
188
236
|
completePlan(): Plan | null {
|
|
189
237
|
if (!this.currentPlan) return null
|
|
190
238
|
|
|
239
|
+
const unfinished = this.currentPlan.steps.filter(
|
|
240
|
+
(s) => s.status === 'pending' || s.status === 'running',
|
|
241
|
+
)
|
|
242
|
+
if (unfinished.length > 0) {
|
|
243
|
+
const named = unfinished.slice(0, 3).map((s) => s.description)
|
|
244
|
+
const rest = unfinished.length - named.length
|
|
245
|
+
const listed = rest > 0 ? `${named.join('; ')}, and ${rest} more` : named.join('; ')
|
|
246
|
+
const counted = `${unfinished.length} of ${this.currentPlan.steps.length} steps`
|
|
247
|
+
throw new Error(
|
|
248
|
+
`Cannot complete plan "${this.currentPlan.title}": ${counted} have not reported an outcome (${listed}). Report each step with updateStepStatus — 'skipped' is a valid outcome — or call failPlan if the plan is being abandoned. Scoring an unreported step as a failure would report a plan that succeeded as one that did not.`,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
191
252
|
const allDone = this.currentPlan.steps.every(
|
|
192
253
|
(s) => s.status === 'completed' || s.status === 'skipped',
|
|
193
254
|
)
|
|
@@ -270,6 +270,18 @@ export class RunPersistence {
|
|
|
270
270
|
this.run.structuredOutput = value
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Name the delegated work this run ended without waiting for.
|
|
275
|
+
*
|
|
276
|
+
* See {@link Run.abandonedTaskIds}. Recording rather than cancelling is
|
|
277
|
+
* the point: the kernel owes the caller the truth about what it walked
|
|
278
|
+
* away from, and nothing more.
|
|
279
|
+
*/
|
|
280
|
+
setAbandonedTaskIds(taskIds: readonly string[]): void {
|
|
281
|
+
if (taskIds.length === 0) return
|
|
282
|
+
this.run.abandonedTaskIds = [...taskIds]
|
|
283
|
+
}
|
|
284
|
+
|
|
273
285
|
setSteps(steps: readonly StepResult[]): void {
|
|
274
286
|
this.run.steps = steps
|
|
275
287
|
}
|
package/src/run/reporter.ts
CHANGED
|
@@ -270,6 +270,18 @@ export function createRunReporter(parentLogger?: Logger): RunReporter {
|
|
|
270
270
|
})
|
|
271
271
|
break
|
|
272
272
|
|
|
273
|
+
case 'compaction_failed':
|
|
274
|
+
// warn rather than info: the run is now continuing at a context
|
|
275
|
+
// size it had already decided was too large.
|
|
276
|
+
log.warn('Context compaction shed nothing', {
|
|
277
|
+
runId: event.runId,
|
|
278
|
+
iteration: event.iteration,
|
|
279
|
+
cause: event.cause,
|
|
280
|
+
messages: event.messages,
|
|
281
|
+
...(event.error !== undefined ? { error: event.error } : {}),
|
|
282
|
+
})
|
|
283
|
+
break
|
|
284
|
+
|
|
273
285
|
case 'capability_warning':
|
|
274
286
|
log.warn('Provider capability mismatch', {
|
|
275
287
|
runId: event.runId,
|
|
@@ -0,0 +1,163 @@
|
|
|
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 { CompletionInbox } from '../../../gateway/completion-inbox.js'
|
|
8
|
+
import { ToolRegistry } from '../../../registry/tool/execute.js'
|
|
9
|
+
import { defineTool } from '../../../tools/defineTool.js'
|
|
10
|
+
import type { TaskHandle } from '../../../types/agent/gateway.js'
|
|
11
|
+
import type { SessionId, TaskId, TenantId } from '../../../types/ids/index.js'
|
|
12
|
+
import { createUserMessage } from '../../../types/message/index.js'
|
|
13
|
+
import type { LLMProvider, StreamChunk } from '../../../types/provider/index.js'
|
|
14
|
+
import type { ProjectId, ThreadId } from '../../../types/session/ids.js'
|
|
15
|
+
import { drainQuery } from '../index.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A notification appended after the answer must not become the answer's grave.
|
|
19
|
+
*
|
|
20
|
+
* `RunPersistence.resolveResult` assembles `Run.result` by walking the message
|
|
21
|
+
* tail BACKWARDS and stopping at the first non-assistant message, and it runs
|
|
22
|
+
* at `markCompleted` — after the loop has finished. So a task notification
|
|
23
|
+
* pushed after the final assistant turn hides that turn from the assembler
|
|
24
|
+
* entirely.
|
|
25
|
+
*
|
|
26
|
+
* This is not hypothetical. It was introduced by the change that made every
|
|
27
|
+
* exit hand over a finished worker's output, and measured here: a run whose
|
|
28
|
+
* model had just said "THIS IS THE RUN ANSWER." returned `run.result ===
|
|
29
|
+
* undefined`. Trading a lost worker result for a lost RUN result is strictly
|
|
30
|
+
* worse than the defect the delivery exists to fix, and every one of the
|
|
31
|
+
* suite's 2,600 tests passed while it was true, because none of them asserted
|
|
32
|
+
* `run.result` on a path where a completion could land last.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const ZERO_USAGE = {
|
|
36
|
+
promptTokens: 0,
|
|
37
|
+
completionTokens: 0,
|
|
38
|
+
totalTokens: 0,
|
|
39
|
+
cachedTokens: 0,
|
|
40
|
+
cacheWriteTokens: 0,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ANSWER = 'THIS IS THE RUN ANSWER.'
|
|
44
|
+
|
|
45
|
+
function completed(): TaskHandle {
|
|
46
|
+
return {
|
|
47
|
+
taskId: 'tsk_bg' as TaskId,
|
|
48
|
+
agentId: 'reviewer',
|
|
49
|
+
state: 'completed',
|
|
50
|
+
createdAt: 0,
|
|
51
|
+
completedAt: 1,
|
|
52
|
+
result: { status: 'completed', result: 'THE WORKER RESULT' },
|
|
53
|
+
} as TaskHandle
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const noop = defineTool({
|
|
57
|
+
name: 'noop',
|
|
58
|
+
description: 'does nothing',
|
|
59
|
+
inputSchema: z.object({}),
|
|
60
|
+
category: 'analysis',
|
|
61
|
+
permissions: [],
|
|
62
|
+
readOnly: true,
|
|
63
|
+
destructive: false,
|
|
64
|
+
concurrencySafe: true,
|
|
65
|
+
async execute() {
|
|
66
|
+
return { success: true, output: 'ok' }
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const workdirs: string[] = []
|
|
71
|
+
afterEach(async () => {
|
|
72
|
+
await Promise.all(workdirs.map((dir) => rm(dir, { recursive: true, force: true })))
|
|
73
|
+
workdirs.length = 0
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('a completion delivered on the way out leaves the answer readable', () => {
|
|
77
|
+
it('keeps run.result when the worker lands during the closing turn', async () => {
|
|
78
|
+
const workingDirectory = await mkdtemp(join(tmpdir(), 'namzu-answer-'))
|
|
79
|
+
workdirs.push(workingDirectory)
|
|
80
|
+
|
|
81
|
+
const inbox = new CompletionInbox()
|
|
82
|
+
let announce: ((h: TaskHandle) => void) | undefined
|
|
83
|
+
inbox.launched('tsk_bg' as TaskId)
|
|
84
|
+
inbox.attach({
|
|
85
|
+
onTaskCompleted: (cb: (h: TaskHandle) => void) => {
|
|
86
|
+
announce = cb
|
|
87
|
+
return () => {
|
|
88
|
+
announce = undefined
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
getTask: () => undefined,
|
|
92
|
+
} as never)
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* One tool call, then the iteration ceiling forces a closing turn. The
|
|
96
|
+
* worker settles DURING that closing turn — after the last in-loop
|
|
97
|
+
* drain, so the only thing left to deliver it is the exit path.
|
|
98
|
+
*/
|
|
99
|
+
class ClosingTurnProvider implements LLMProvider {
|
|
100
|
+
readonly id = 'closing-turn'
|
|
101
|
+
readonly name = 'Closing Turn Provider'
|
|
102
|
+
calls = 0
|
|
103
|
+
async *chatStream(): AsyncIterable<StreamChunk> {
|
|
104
|
+
this.calls += 1
|
|
105
|
+
if (this.calls === 1) {
|
|
106
|
+
yield {
|
|
107
|
+
id: 'm1',
|
|
108
|
+
delta: {
|
|
109
|
+
toolCalls: [
|
|
110
|
+
{
|
|
111
|
+
index: 0,
|
|
112
|
+
id: 'toolu_1',
|
|
113
|
+
type: 'function',
|
|
114
|
+
function: { name: 'noop', arguments: '{}' },
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
yield { id: 'm1', delta: {}, finishReason: 'tool_calls', usage: ZERO_USAGE }
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
announce?.(completed())
|
|
123
|
+
yield { id: 'm2', delta: { content: ANSWER } }
|
|
124
|
+
yield { id: 'm2', delta: {}, finishReason: 'stop', usage: ZERO_USAGE }
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const tools = new ToolRegistry()
|
|
129
|
+
tools.register(noop)
|
|
130
|
+
|
|
131
|
+
const run = await drainQuery({
|
|
132
|
+
provider: new ClosingTurnProvider(),
|
|
133
|
+
tools,
|
|
134
|
+
completionInbox: inbox,
|
|
135
|
+
agentId: 'agent_test',
|
|
136
|
+
agentName: 'Test Agent',
|
|
137
|
+
messages: [createUserMessage('go')],
|
|
138
|
+
workingDirectory,
|
|
139
|
+
runConfig: {
|
|
140
|
+
model: 'mock-model',
|
|
141
|
+
timeoutMs: 20_000,
|
|
142
|
+
tokenBudget: 100_000,
|
|
143
|
+
maxIterations: 1,
|
|
144
|
+
maxResponseTokens: 256,
|
|
145
|
+
},
|
|
146
|
+
sessionId: 'ses_answer' as SessionId,
|
|
147
|
+
threadId: 'thd_answer' as ThreadId,
|
|
148
|
+
projectId: 'prj_answer' as ProjectId,
|
|
149
|
+
tenantId: 'tnt_answer' as TenantId,
|
|
150
|
+
} as never)
|
|
151
|
+
|
|
152
|
+
// Both halves, because either one alone is satisfied by a broken fix:
|
|
153
|
+
// dropping the delivery keeps the answer, and dropping the answer fix
|
|
154
|
+
// keeps the notification.
|
|
155
|
+
expect(run.result, 'the notification buried the run answer').toBe(ANSWER)
|
|
156
|
+
expect(
|
|
157
|
+
(run.messages as { content: unknown }[]).some(
|
|
158
|
+
(m) => typeof m.content === 'string' && m.content.includes('THE WORKER RESULT'),
|
|
159
|
+
),
|
|
160
|
+
'the worker result was not delivered',
|
|
161
|
+
).toBe(true)
|
|
162
|
+
}, 60_000)
|
|
163
|
+
})
|