@namzu/sdk 9.0.0 → 11.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 +110 -0
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +0 -5
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/manager/plan/lifecycle.d.ts +10 -0
- package/dist/manager/plan/lifecycle.d.ts.map +1 -1
- package/dist/manager/plan/lifecycle.js +14 -0
- package/dist/manager/plan/lifecycle.js.map +1 -1
- package/dist/runtime/query/__tests__/a-plan-that-succeeded-says-so.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/a-plan-that-succeeded-says-so.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/a-plan-that-succeeded-says-so.test.js +90 -0
- package/dist/runtime/query/__tests__/a-plan-that-succeeded-says-so.test.js.map +1 -0
- package/dist/runtime/query/index.d.ts +0 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +0 -1
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +0 -17
- 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/result.d.ts.map +1 -1
- package/dist/runtime/query/result.js +17 -1
- package/dist/runtime/query/result.js.map +1 -1
- package/dist/tools/coordinator/__tests__/a-listing-is-not-a-back-door.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/a-listing-is-not-a-back-door.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/a-listing-is-not-a-back-door.test.js +139 -0
- package/dist/tools/coordinator/__tests__/a-listing-is-not-a-back-door.test.js.map +1 -0
- package/dist/tools/coordinator/__tests__/a-plan-step-reports-its-own-outcome.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/a-plan-step-reports-its-own-outcome.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/a-plan-step-reports-its-own-outcome.test.js +160 -0
- package/dist/tools/coordinator/__tests__/a-plan-step-reports-its-own-outcome.test.js.map +1 -0
- package/dist/tools/coordinator/__tests__/approve-plan.test.js +21 -6
- package/dist/tools/coordinator/__tests__/approve-plan.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/completion-delivery.test.js +4 -0
- package/dist/tools/coordinator/__tests__/completion-delivery.test.js.map +1 -1
- package/dist/tools/coordinator/__tests__/task-list.test.js +44 -15
- package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +159 -8
- package/dist/tools/coordinator/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/SupervisorAgent.ts +1 -8
- package/src/manager/plan/lifecycle.ts +14 -0
- package/src/runtime/query/__tests__/a-plan-that-succeeded-says-so.test.ts +109 -0
- package/src/runtime/query/index.ts +0 -6
- package/src/runtime/query/iteration/phases/context.ts +0 -19
- package/src/runtime/query/result.ts +18 -1
- package/src/tools/coordinator/__tests__/a-listing-is-not-a-back-door.test.ts +171 -0
- package/src/tools/coordinator/__tests__/a-plan-step-reports-its-own-outcome.test.ts +215 -0
- package/src/tools/coordinator/__tests__/approve-plan.test.ts +32 -11
- package/src/tools/coordinator/__tests__/completion-delivery.test.ts +7 -0
- package/src/tools/coordinator/__tests__/task-list.test.ts +47 -20
- package/src/tools/coordinator/index.ts +177 -8
|
@@ -88,14 +88,30 @@ describe('coordinator approve_plan tool', () => {
|
|
|
88
88
|
])
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
-
it('
|
|
91
|
+
it('opens with the historical approval sentence, then names the steps', async () => {
|
|
92
|
+
// This assertion used to be `toBe` on the whole string, guarding the
|
|
93
|
+
// approve-with-edits change against disturbing the bare-approve path.
|
|
94
|
+
// It was never a promise that the output would carry nothing more, and
|
|
95
|
+
// it now carries the step roster — without which `plan_step_id` and
|
|
96
|
+
// `update_plan_step` name ids the model has never been told.
|
|
92
97
|
const result = await executeApprovePlan({ approved: true })
|
|
93
98
|
|
|
94
99
|
expect(result.success).toBe(true)
|
|
95
|
-
expect(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
expect(
|
|
101
|
+
result.output.startsWith(
|
|
102
|
+
'Plan approved by user. Proceed with execution — launch workers via create_task.',
|
|
103
|
+
),
|
|
104
|
+
).toBe(true)
|
|
105
|
+
expect(result.output).toContain('step_1 — Extract uploaded DOCX files')
|
|
106
|
+
expect(result.data).toMatchObject({ approved: true, feedback: undefined })
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('carries the step ids in data, so a host does not have to parse prose', async () => {
|
|
110
|
+
const result = await executeApprovePlan({ approved: true })
|
|
111
|
+
|
|
112
|
+
expect((result.data as { steps: unknown }).steps).toEqual([
|
|
113
|
+
{ step_id: 'step_1', description: 'Extract uploaded DOCX files', agent_id: undefined },
|
|
114
|
+
])
|
|
99
115
|
})
|
|
100
116
|
|
|
101
117
|
it('embeds approve-with-edits feedback in the output and data', async () => {
|
|
@@ -105,12 +121,17 @@ describe('coordinator approve_plan tool', () => {
|
|
|
105
121
|
})
|
|
106
122
|
|
|
107
123
|
expect(result.success).toBe(true)
|
|
108
|
-
expect(
|
|
109
|
-
|
|
110
|
-
'
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
expect(
|
|
125
|
+
result.output.startsWith(
|
|
126
|
+
'Plan approved by user with required edits — apply them during execution:\n' +
|
|
127
|
+
'Skip step 2 and use the staging database instead.\n' +
|
|
128
|
+
'Proceed with execution — launch workers via create_task.',
|
|
129
|
+
),
|
|
130
|
+
).toBe(true)
|
|
131
|
+
// The edits stay ahead of the roster: what the user demanded is the
|
|
132
|
+
// first thing read, and the step list is reference material after it.
|
|
133
|
+
expect(result.output.indexOf('staging database')).toBeLessThan(result.output.indexOf('step_1'))
|
|
134
|
+
expect(result.data).toMatchObject({
|
|
114
135
|
approved: true,
|
|
115
136
|
feedback: 'Skip step 2 and use the staging database instead.',
|
|
116
137
|
})
|
|
@@ -298,6 +298,13 @@ describe('waiting explicitly beats listing in a loop', () => {
|
|
|
298
298
|
describe('the task listing carries the output it always had', () => {
|
|
299
299
|
async function listWith(result: string): Promise<string> {
|
|
300
300
|
const h = harness()
|
|
301
|
+
// Launch it first. The listing is scoped to what this run launched, so
|
|
302
|
+
// settling a task the tools never created describes a sibling run's
|
|
303
|
+
// work — which the listing now declines to show, correctly.
|
|
304
|
+
await toolNamed(h.tools, 'create_task').execute(
|
|
305
|
+
{ agent_id: 'reviewer', prompt: 'go', description: 'review', background: true },
|
|
306
|
+
{} as never,
|
|
307
|
+
)
|
|
301
308
|
h.settle({
|
|
302
309
|
taskId: 'tsk_1' as TaskId,
|
|
303
310
|
agentId: 'reviewer',
|
|
@@ -27,13 +27,27 @@ function makeContext(): ToolContext {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Hands back the seeded handles in order, one per `createTask`.
|
|
32
|
+
*
|
|
33
|
+
* The listing is scoped to what this tool set launched, so a fixture that
|
|
34
|
+
* only stuffed `listTasks()` would now list nothing — and a gateway holding
|
|
35
|
+
* tasks these tools never launched is precisely the sibling-run case the
|
|
36
|
+
* scope exists to refuse. So the tests launch through the front door and the
|
|
37
|
+
* fixture plays along.
|
|
38
|
+
*/
|
|
30
39
|
function gatewayWith(handles: TaskHandle[]): TaskGateway {
|
|
40
|
+
let nextLaunch = 0
|
|
31
41
|
return {
|
|
32
42
|
async createTask() {
|
|
33
|
-
|
|
43
|
+
const h = handles[nextLaunch++]
|
|
44
|
+
if (!h) throw new Error('fixture ran out of seeded handles')
|
|
45
|
+
return h
|
|
34
46
|
},
|
|
35
|
-
async waitForTask() {
|
|
36
|
-
|
|
47
|
+
async waitForTask(id) {
|
|
48
|
+
const h = handles.find((x) => x.taskId === id)
|
|
49
|
+
if (!h) throw new Error(`fixture has no handle ${id}`)
|
|
50
|
+
return h
|
|
37
51
|
},
|
|
38
52
|
async continueTask() {},
|
|
39
53
|
cancelTask() {},
|
|
@@ -79,12 +93,29 @@ function handle(input: {
|
|
|
79
93
|
}
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Build the coordinator surface, launch each seeded handle through
|
|
98
|
+
* `create_task`, and return `agent_task_list`.
|
|
99
|
+
*
|
|
100
|
+
* Launching is what puts the tasks in this run's scope. Reaching past it to
|
|
101
|
+
* seed the gateway directly would test a listing nobody can produce.
|
|
102
|
+
*/
|
|
103
|
+
async function agentTaskListOver(seeded: TaskHandle[]) {
|
|
83
104
|
const tools = buildCoordinatorTools({
|
|
84
|
-
gateway,
|
|
105
|
+
gateway: gatewayWith(seeded),
|
|
85
106
|
workingDirectory: '/tmp/test',
|
|
86
107
|
allowedAgentIds: ['solution-architecture', 'enterprise-architecture'],
|
|
87
108
|
})
|
|
109
|
+
|
|
110
|
+
const createTask = tools.find((tool) => tool.name === 'create_task')
|
|
111
|
+
if (!createTask) throw new Error('create_task tool missing from coordinator builder')
|
|
112
|
+
for (const h of seeded) {
|
|
113
|
+
await createTask.execute(
|
|
114
|
+
{ agent_id: h.agentId, prompt: 'work', description: `launch ${h.taskId}` },
|
|
115
|
+
makeContext(),
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
88
119
|
const t = tools.find((tool) => tool.name === 'agent_task_list')
|
|
89
120
|
if (!t) throw new Error('agent_task_list tool missing from coordinator builder')
|
|
90
121
|
return t
|
|
@@ -92,7 +123,7 @@ function findAgentTaskList(gateway: TaskGateway) {
|
|
|
92
123
|
|
|
93
124
|
describe('coordinator agent_task_list tool', () => {
|
|
94
125
|
it('lists every task with state, agent, and timing', async () => {
|
|
95
|
-
const
|
|
126
|
+
const seeded = [
|
|
96
127
|
handle({
|
|
97
128
|
id: 'task_a',
|
|
98
129
|
agentId: 'solution-architecture',
|
|
@@ -114,9 +145,8 @@ describe('coordinator agent_task_list tool', () => {
|
|
|
114
145
|
completedAt: 4000,
|
|
115
146
|
lastError: 'bash exit 1',
|
|
116
147
|
}),
|
|
117
|
-
]
|
|
118
|
-
|
|
119
|
-
const tool = findAgentTaskList(gateway)
|
|
148
|
+
]
|
|
149
|
+
const tool = await agentTaskListOver(seeded)
|
|
120
150
|
const result = await tool.execute({}, makeContext())
|
|
121
151
|
expect(result.success).toBe(true)
|
|
122
152
|
expect(result.output).toMatch(/Tasks: 3 total/)
|
|
@@ -131,7 +161,7 @@ describe('coordinator agent_task_list tool', () => {
|
|
|
131
161
|
})
|
|
132
162
|
|
|
133
163
|
it('filters by state', async () => {
|
|
134
|
-
const
|
|
164
|
+
const seeded = [
|
|
135
165
|
handle({
|
|
136
166
|
id: 'task_a',
|
|
137
167
|
agentId: 'solution-architecture',
|
|
@@ -145,9 +175,8 @@ describe('coordinator agent_task_list tool', () => {
|
|
|
145
175
|
state: 'running',
|
|
146
176
|
createdAt: 1000,
|
|
147
177
|
}),
|
|
148
|
-
]
|
|
149
|
-
|
|
150
|
-
const tool = findAgentTaskList(gateway)
|
|
178
|
+
]
|
|
179
|
+
const tool = await agentTaskListOver(seeded)
|
|
151
180
|
const result = await tool.execute({ state: 'running' }, makeContext())
|
|
152
181
|
expect(result.success).toBe(true)
|
|
153
182
|
const data = result.data as { items: Array<{ task_id: string }> }
|
|
@@ -157,7 +186,7 @@ describe('coordinator agent_task_list tool', () => {
|
|
|
157
186
|
})
|
|
158
187
|
|
|
159
188
|
it('handles an empty gateway', async () => {
|
|
160
|
-
const tool =
|
|
189
|
+
const tool = await agentTaskListOver([])
|
|
161
190
|
const result = await tool.execute({}, makeContext())
|
|
162
191
|
expect(result.success).toBe(true)
|
|
163
192
|
expect(result.output).toMatch(/Tasks: 0 total/)
|
|
@@ -224,7 +253,7 @@ describe('agent_task_list frames what a worker said', () => {
|
|
|
224
253
|
}
|
|
225
254
|
|
|
226
255
|
async function render(text: string): Promise<string> {
|
|
227
|
-
const tool =
|
|
256
|
+
const tool = await agentTaskListOver([withResult(text)])
|
|
228
257
|
const out = await tool.execute({}, makeContext())
|
|
229
258
|
return out.output
|
|
230
259
|
}
|
|
@@ -263,11 +292,9 @@ describe('agent_task_list frames what a worker said', () => {
|
|
|
263
292
|
})
|
|
264
293
|
|
|
265
294
|
it('says nothing extra for a task that produced no output', async () => {
|
|
266
|
-
const tool =
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
]),
|
|
270
|
-
)
|
|
295
|
+
const tool = await agentTaskListOver([
|
|
296
|
+
handle({ id: 'task_none', agentId: 'reviewer', state: 'running', createdAt: 0 }),
|
|
297
|
+
])
|
|
271
298
|
|
|
272
299
|
const out = await tool.execute({}, makeContext())
|
|
273
300
|
|
|
@@ -308,6 +308,33 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
308
308
|
// compatibility (Agent tool consumes it from its own path).
|
|
309
309
|
} = opts
|
|
310
310
|
const cwd = opts.workingDirectory
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The tasks THIS surface launched — the scope of everything it will read back.
|
|
314
|
+
*
|
|
315
|
+
* A `TaskGateway` is shared on purpose: `SupervisorAgentConfig.gateway`
|
|
316
|
+
* exists so a host can hand the same one to several runs. `listTasks()` is
|
|
317
|
+
* therefore gateway-wide by design, and `agent_task_list` used to hand that
|
|
318
|
+
* straight to the model — so a supervisor could read a sibling run's worker
|
|
319
|
+
* output, including the `result` field, by listing. `wait_for_task` had the
|
|
320
|
+
* same reach through `getTask`.
|
|
321
|
+
*
|
|
322
|
+
* That is the leak `CompletionInbox` closed on the push side, through a
|
|
323
|
+
* different door: the inbox refuses a completion for a task it was not told
|
|
324
|
+
* about, precisely because `onTaskCompleted` is a broadcast. The pull side
|
|
325
|
+
* kept no such record and asked the gateway directly.
|
|
326
|
+
*
|
|
327
|
+
* The scope lives here rather than in `listTasks()` because the two answer
|
|
328
|
+
* different questions. A host calling `listTasks()` is the operator and may
|
|
329
|
+
* legitimately want everything on its gateway; a model calling
|
|
330
|
+
* `agent_task_list` is one run asking about its own work. Narrowing the
|
|
331
|
+
* gateway method would take the operator's view away to fix the model's.
|
|
332
|
+
*
|
|
333
|
+
* Consequence worth stating: a task launched through a DIFFERENT surface on
|
|
334
|
+
* the same gateway — `buildAgentTool`, or the host itself — is not listed
|
|
335
|
+
* here. That is the same rule, not an exception to it.
|
|
336
|
+
*/
|
|
337
|
+
const launchedHere = new Set<TaskId>()
|
|
311
338
|
void opts.onTaskLaunched
|
|
312
339
|
|
|
313
340
|
const agentIdEnum = delegateSchema(agentIds)
|
|
@@ -386,6 +413,12 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
386
413
|
.describe(
|
|
387
414
|
'Existing planning task ID to link. If omitted, a planning task is auto-created.',
|
|
388
415
|
),
|
|
416
|
+
plan_step_id: z
|
|
417
|
+
.string()
|
|
418
|
+
.optional()
|
|
419
|
+
.describe(
|
|
420
|
+
'The approve_plan step this launch carries out (e.g. "step_2"). Pass it and the step reports its own outcome — running on launch, completed or failed when the worker settles — so the plan can say how it went. Omit it only when this launch is not part of the approved plan.',
|
|
421
|
+
),
|
|
389
422
|
...(canLaunchInBackground
|
|
390
423
|
? {
|
|
391
424
|
background: z
|
|
@@ -407,9 +440,24 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
407
440
|
// children were observed at 8m04s, which it would have survived by
|
|
408
441
|
// under two minutes.
|
|
409
442
|
timeoutMs: DELEGATION_TIMEOUT_MS,
|
|
410
|
-
async execute(
|
|
443
|
+
async execute(
|
|
444
|
+
{ agent_id, prompt, description, plan_task_id, plan_step_id, background },
|
|
445
|
+
_context,
|
|
446
|
+
) {
|
|
411
447
|
let resolvedPlanTaskId = plan_task_id
|
|
412
448
|
|
|
449
|
+
// The binding between a plan step and the work that carries it out.
|
|
450
|
+
// Without it a plan's steps had no relationship to any tool call, so
|
|
451
|
+
// nothing could ever observe how a step went — which is why a plan
|
|
452
|
+
// could report `failed` or stay `executing` forever but never
|
|
453
|
+
// `completed`.
|
|
454
|
+
const planStepId = plan_step_id
|
|
455
|
+
const reportStep = (status: 'running' | 'completed' | 'failed', error?: string): void => {
|
|
456
|
+
if (!planStepId) return
|
|
457
|
+
getPlanManager?.()?.updateStepStatus(planStepId, status, error)
|
|
458
|
+
}
|
|
459
|
+
reportStep('running')
|
|
460
|
+
|
|
413
461
|
if (taskStore) {
|
|
414
462
|
if (resolvedPlanTaskId) {
|
|
415
463
|
await taskStore.update(resolvedPlanTaskId as `task_${string}`, {
|
|
@@ -446,6 +494,9 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
446
494
|
// is exactly the blocking launch whose wait was abandoned.
|
|
447
495
|
completionInbox?.launched(handle.taskId)
|
|
448
496
|
|
|
497
|
+
// ...and whose it is for the READ side too. See `launchedHere`.
|
|
498
|
+
launchedHere.add(handle.taskId)
|
|
499
|
+
|
|
449
500
|
// A background launch asked for with nowhere to deliver it is
|
|
450
501
|
// REFUSED, not quietly turned into a blocking one.
|
|
451
502
|
//
|
|
@@ -469,6 +520,10 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
469
520
|
description: 'Failed: the launch was refused before any worker started',
|
|
470
521
|
})
|
|
471
522
|
}
|
|
523
|
+
// Same reason the plan task is closed here: nothing is running,
|
|
524
|
+
// so a step left `running` would show work underway with no
|
|
525
|
+
// worker behind it, and the plan could never settle.
|
|
526
|
+
reportStep('failed', 'the launch was refused before any worker started')
|
|
472
527
|
return {
|
|
473
528
|
success: false,
|
|
474
529
|
output: '',
|
|
@@ -569,6 +624,11 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
569
624
|
})
|
|
570
625
|
}
|
|
571
626
|
|
|
627
|
+
// The step reports the same outcome, from the same two authorities.
|
|
628
|
+
// This is the only point in a delegated launch where the answer is
|
|
629
|
+
// actually known.
|
|
630
|
+
reportStep(success ? 'completed' : 'failed', success ? undefined : resultText.slice(0, 200))
|
|
631
|
+
|
|
572
632
|
return {
|
|
573
633
|
success,
|
|
574
634
|
// Framed, because a delegated worker is the component MOST
|
|
@@ -631,6 +691,21 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
631
691
|
// waiting. Same bound as the launch it is waiting on.
|
|
632
692
|
timeoutMs: DELEGATION_TIMEOUT_MS,
|
|
633
693
|
async execute({ task_id }, _context) {
|
|
694
|
+
// Same scope as the listing — see `launchedHere`. Asked FIRST, so a
|
|
695
|
+
// task belonging to a sibling run on a shared gateway is refused
|
|
696
|
+
// here rather than waited on and then read.
|
|
697
|
+
if (!launchedHere.has(task_id as TaskId)) {
|
|
698
|
+
// Deliberately does not distinguish "never existed" from
|
|
699
|
+
// "belongs to someone else". The second answer is itself the
|
|
700
|
+
// leak in miniature: it confirms a task id a run was not
|
|
701
|
+
// supposed to know about.
|
|
702
|
+
return {
|
|
703
|
+
success: false,
|
|
704
|
+
output: `No task ${task_id} was launched by this run. Call agent_task_list to see the tasks you can wait on.`,
|
|
705
|
+
data: { task_id },
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
634
709
|
const known = gateway.getTask(task_id as TaskId)
|
|
635
710
|
if (!known) {
|
|
636
711
|
return {
|
|
@@ -716,7 +791,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
716
791
|
|
|
717
792
|
const agentTaskList = defineTool({
|
|
718
793
|
name: 'agent_task_list',
|
|
719
|
-
description: `Inspect the live state of
|
|
794
|
+
description: `Inspect the live state of the agent tasks YOU launched with create_task: returns each task's id, agent, state (pending/running/completed/failed/canceled), and timing. Tasks launched by another run are not listed, even when it shares this gateway. Distinct from the plan-task store's \`task_list\` (which lists planning tasks): this tool lists running/completed worker invocations. ${listingAdvice}`,
|
|
720
795
|
inputSchema: z.object({
|
|
721
796
|
state: z
|
|
722
797
|
.enum(['pending', 'running', 'completed', 'failed', 'canceled'])
|
|
@@ -729,7 +804,10 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
729
804
|
destructive: false,
|
|
730
805
|
concurrencySafe: true,
|
|
731
806
|
async execute({ state }) {
|
|
732
|
-
|
|
807
|
+
// Scoped to this run's own launches — see `launchedHere`. The
|
|
808
|
+
// gateway may hold a sibling run's tasks, and this listing is not
|
|
809
|
+
// the door to them.
|
|
810
|
+
const handles = gateway.listTasks().filter((h) => launchedHere.has(h.taskId))
|
|
733
811
|
const filtered = state ? handles.filter((h) => h.state === state) : handles
|
|
734
812
|
const items = filtered.map((h) => {
|
|
735
813
|
const runStatus = h.result?.status
|
|
@@ -965,17 +1043,38 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
965
1043
|
|
|
966
1044
|
if (response.approved) {
|
|
967
1045
|
pm.startExecution()
|
|
1046
|
+
|
|
1047
|
+
// The step ids, because nothing else tells the model what
|
|
1048
|
+
// they are. `plan_step_id` on create_task and `step_id` on
|
|
1049
|
+
// update_plan_step are both unusable without them — a
|
|
1050
|
+
// binding the caller cannot name is a binding that does not
|
|
1051
|
+
// exist. Rendered from the plan as approved, so a plan the
|
|
1052
|
+
// user edited lists the steps they actually approved.
|
|
1053
|
+
const roster = (pm.active?.steps ?? [])
|
|
1054
|
+
.map((s) => ` ${s.id} — ${s.description}${s.agentId ? ` (${s.agentId})` : ''}`)
|
|
1055
|
+
.join('\n')
|
|
1056
|
+
const howToReport = roster
|
|
1057
|
+
? `\nSteps, and how each reports its outcome:\n${roster}\nPass plan_step_id to create_task for a delegated step; call update_plan_step for one you do yourself. The plan cannot report success until every step has reported.`
|
|
1058
|
+
: ''
|
|
1059
|
+
|
|
968
1060
|
// Approve-with-edits: when the user attached feedback to an
|
|
969
1061
|
// approval, embed it in the model-visible output so the
|
|
970
|
-
// supervisor applies the edits during execution.
|
|
971
|
-
// approve keeps the historical output byte-identical.
|
|
1062
|
+
// supervisor applies the edits during execution.
|
|
972
1063
|
const output = response.feedback
|
|
973
|
-
? `Plan approved by user with required edits — apply them during execution:\n${response.feedback}\nProceed with execution — launch workers via create_task
|
|
974
|
-
:
|
|
1064
|
+
? `Plan approved by user with required edits — apply them during execution:\n${response.feedback}\nProceed with execution — launch workers via create_task.${howToReport}`
|
|
1065
|
+
: `Plan approved by user. Proceed with execution — launch workers via create_task.${howToReport}`
|
|
975
1066
|
return {
|
|
976
1067
|
success: true,
|
|
977
1068
|
output,
|
|
978
|
-
data: {
|
|
1069
|
+
data: {
|
|
1070
|
+
approved: true,
|
|
1071
|
+
feedback: response.feedback,
|
|
1072
|
+
steps: (pm.active?.steps ?? []).map((s) => ({
|
|
1073
|
+
step_id: s.id,
|
|
1074
|
+
description: s.description,
|
|
1075
|
+
agent_id: s.agentId,
|
|
1076
|
+
})),
|
|
1077
|
+
},
|
|
979
1078
|
}
|
|
980
1079
|
}
|
|
981
1080
|
|
|
@@ -991,6 +1090,76 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
991
1090
|
},
|
|
992
1091
|
})
|
|
993
1092
|
tools.push(approvePlan)
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* How a step the ORCHESTRATOR did reports its own outcome.
|
|
1096
|
+
*
|
|
1097
|
+
* `create_task` reports the steps it carries out, which covers every
|
|
1098
|
+
* delegated step. A step with no `agent_id` is the orchestrator's own
|
|
1099
|
+
* work and has no tool call to bind to — so without this there is no
|
|
1100
|
+
* way for it to ever report, and a plan containing one could never
|
|
1101
|
+
* settle no matter how well it went.
|
|
1102
|
+
*
|
|
1103
|
+
* `skipped` is a first-class outcome and not a euphemism for failure:
|
|
1104
|
+
* a plan that turned out not to need a step went right, and forcing
|
|
1105
|
+
* that into `completed` or `failed` would make the plan lie in one
|
|
1106
|
+
* direction or the other.
|
|
1107
|
+
*/
|
|
1108
|
+
const updatePlanStep = defineTool({
|
|
1109
|
+
name: 'update_plan_step',
|
|
1110
|
+
description:
|
|
1111
|
+
'Report how a step of the approved plan went. Use it for steps YOU carried out — steps delegated with create_task report themselves when you pass plan_step_id. Call it as each step settles, not in a batch at the end: the plan cannot say it succeeded until every step has reported, and an unreported step is not scored as a failure, it simply leaves the plan unsettled. Use "skipped" for a step that turned out not to be needed; that is a successful outcome, not a failure.',
|
|
1112
|
+
inputSchema: z.object({
|
|
1113
|
+
step_id: z.string().describe('The plan step id, e.g. "step_2".'),
|
|
1114
|
+
status: z
|
|
1115
|
+
.enum(['completed', 'skipped', 'failed'])
|
|
1116
|
+
.describe(
|
|
1117
|
+
'How it went. "skipped" means the step was not needed and the plan is still on track.',
|
|
1118
|
+
),
|
|
1119
|
+
error: z
|
|
1120
|
+
.string()
|
|
1121
|
+
.optional()
|
|
1122
|
+
.describe('What went wrong. Only meaningful with status "failed".'),
|
|
1123
|
+
}),
|
|
1124
|
+
category: 'custom',
|
|
1125
|
+
permissions: [],
|
|
1126
|
+
readOnly: false,
|
|
1127
|
+
destructive: false,
|
|
1128
|
+
concurrencySafe: true,
|
|
1129
|
+
async execute({ step_id, status, error }) {
|
|
1130
|
+
const pm = getPlanManager?.()
|
|
1131
|
+
if (!pm?.active) {
|
|
1132
|
+
return {
|
|
1133
|
+
success: false,
|
|
1134
|
+
output: '',
|
|
1135
|
+
error: 'There is no active plan to report against. Call approve_plan first.',
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const step = pm.updateStepStatus(step_id, status, error)
|
|
1140
|
+
if (!step) {
|
|
1141
|
+
const known = pm.active.steps.map((s) => s.id).join(', ')
|
|
1142
|
+
return {
|
|
1143
|
+
success: false,
|
|
1144
|
+
output: '',
|
|
1145
|
+
error: `No plan step "${step_id}". This plan's steps are: ${known || '(none)'}.`,
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const outstanding = pm.unreportedSteps
|
|
1150
|
+
return {
|
|
1151
|
+
success: true,
|
|
1152
|
+
output:
|
|
1153
|
+
outstanding.length === 0
|
|
1154
|
+
? `Step ${step_id} reported as ${status}. Every step has now reported.`
|
|
1155
|
+
: `Step ${step_id} reported as ${status}. Still unreported: ${outstanding
|
|
1156
|
+
.map((s) => s.id)
|
|
1157
|
+
.join(', ')}.`,
|
|
1158
|
+
data: { step_id, status, unreported: outstanding.map((s) => s.id) },
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
})
|
|
1162
|
+
tools.push(updatePlanStep)
|
|
994
1163
|
}
|
|
995
1164
|
|
|
996
1165
|
if (resumeHandler && runId) {
|