@namzu/sdk 6.0.0 → 6.2.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 +254 -0
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +18 -0
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/__tests__/supervisor-coordinator-registration.test.js +31 -0
- package/dist/agents/__tests__/supervisor-coordinator-registration.test.js.map +1 -1
- package/dist/gateway/__tests__/completion-inbox.test.d.ts +2 -0
- package/dist/gateway/__tests__/completion-inbox.test.d.ts.map +1 -0
- package/dist/gateway/__tests__/completion-inbox.test.js +275 -0
- package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -0
- package/dist/gateway/completion-inbox.d.ts +113 -0
- package/dist/gateway/completion-inbox.d.ts.map +1 -0
- package/dist/gateway/completion-inbox.js +225 -0
- package/dist/gateway/completion-inbox.js.map +1 -0
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +4 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/registry/tool/__tests__/dialect.test.js +8 -5
- package/dist/registry/tool/__tests__/dialect.test.js.map +1 -1
- package/dist/registry/tool/dialect.js +11 -4
- package/dist/registry/tool/dialect.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +34 -0
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/runtime/query/__tests__/completion-notification.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/completion-notification.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/completion-notification.test.js +168 -0
- package/dist/runtime/query/__tests__/completion-notification.test.js.map +1 -0
- package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.d.ts +2 -0
- package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.js +122 -0
- package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.js.map +1 -0
- package/dist/runtime/query/__tests__/step-allow-list.test.d.ts +2 -0
- package/dist/runtime/query/__tests__/step-allow-list.test.d.ts.map +1 -0
- package/dist/runtime/query/__tests__/step-allow-list.test.js +171 -0
- package/dist/runtime/query/__tests__/step-allow-list.test.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +14 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +20 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +9 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +1 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +74 -1
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js +43 -13
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/tools/builtins/__tests__/bash.proc-test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/bash.proc-test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/bash.proc-test.js +115 -0
- package/dist/tools/builtins/__tests__/bash.proc-test.js.map +1 -0
- package/dist/tools/builtins/__tests__/bash.test.d.ts +2 -0
- package/dist/tools/builtins/__tests__/bash.test.d.ts.map +1 -0
- package/dist/tools/builtins/__tests__/bash.test.js +65 -0
- package/dist/tools/builtins/__tests__/bash.test.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +89 -17
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/coordinator/__tests__/allow-delegation.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/allow-delegation.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/allow-delegation.test.js +94 -0
- package/dist/tools/coordinator/__tests__/allow-delegation.test.js.map +1 -0
- package/dist/tools/coordinator/__tests__/completion-delivery.test.d.ts +2 -0
- package/dist/tools/coordinator/__tests__/completion-delivery.test.d.ts.map +1 -0
- package/dist/tools/coordinator/__tests__/completion-delivery.test.js +255 -0
- package/dist/tools/coordinator/__tests__/completion-delivery.test.js.map +1 -0
- package/dist/tools/coordinator/__tests__/task-list.test.js +13 -7
- package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts +44 -0
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +239 -16
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/types/agent/supervisor.d.ts +27 -0
- package/dist/types/agent/supervisor.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +13 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/package.json +3 -2
- package/src/agents/SupervisorAgent.ts +19 -0
- package/src/agents/__tests__/supervisor-coordinator-registration.test.ts +37 -0
- package/src/gateway/__tests__/completion-inbox.test.ts +343 -0
- package/src/gateway/completion-inbox.ts +241 -0
- package/src/public-runtime.ts +4 -0
- package/src/registry/tool/__tests__/dialect.test.ts +8 -5
- package/src/registry/tool/dialect.ts +11 -4
- package/src/registry/tool/execute.ts +35 -0
- package/src/runtime/query/__tests__/completion-notification.test.ts +196 -0
- package/src/runtime/query/__tests__/run-survives-its-own-park.proc-test.ts +127 -0
- package/src/runtime/query/__tests__/step-allow-list.test.ts +205 -0
- package/src/runtime/query/executor.ts +21 -1
- package/src/runtime/query/index.ts +11 -0
- package/src/runtime/query/iteration/index.ts +77 -1
- package/src/runtime/query/iteration/phases/context.ts +51 -15
- package/src/tools/builtins/__tests__/bash.proc-test.ts +142 -0
- package/src/tools/builtins/__tests__/bash.test.ts +79 -0
- package/src/tools/builtins/bash.ts +100 -18
- package/src/tools/coordinator/__tests__/allow-delegation.test.ts +120 -0
- package/src/tools/coordinator/__tests__/completion-delivery.test.ts +347 -0
- package/src/tools/coordinator/__tests__/task-list.test.ts +13 -7
- package/src/tools/coordinator/index.ts +278 -21
- package/src/types/agent/supervisor.ts +28 -0
- package/src/types/tool/index.ts +13 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
+
import type { CompletionInbox } from '../../gateway/completion-inbox.js'
|
|
2
3
|
import type { PlanManager } from '../../manager/plan/lifecycle.js'
|
|
3
4
|
import type { PendingAnswers, QuestionParkRecorder } from '../../runtime/query/question-park.js'
|
|
4
5
|
import type { AgentRuntimeContext } from '../../types/agent/base.js'
|
|
@@ -33,6 +34,16 @@ export interface CoordinatorToolsOptions {
|
|
|
33
34
|
runtimeContext?: AgentRuntimeContext
|
|
34
35
|
allowedAgentIds: string[]
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* May this run delegate at all? Defaults to true.
|
|
39
|
+
*
|
|
40
|
+
* Same field, same name, as SupervisorAgentConfig.allowDelegation — the
|
|
41
|
+
* name is kept identical deliberately. This options bag already renames
|
|
42
|
+
* agentIds to allowedAgentIds, and a second rename on the road between
|
|
43
|
+
* the config and the decision would make the road untraceable.
|
|
44
|
+
*/
|
|
45
|
+
allowDelegation?: boolean
|
|
46
|
+
|
|
36
47
|
taskStore?: TaskStore
|
|
37
48
|
|
|
38
49
|
runId?: RunId
|
|
@@ -41,6 +52,17 @@ export interface CoordinatorToolsOptions {
|
|
|
41
52
|
|
|
42
53
|
onTaskLaunched?: TaskLaunchedCallback
|
|
43
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Where a completion goes when no call is left waiting for it.
|
|
57
|
+
*
|
|
58
|
+
* These tools claim a completion the moment they hand it to the model as a
|
|
59
|
+
* `tool_result`; anything unclaimed is delivered to the transcript as a
|
|
60
|
+
* notification instead. Without an inbox the tools still work and the
|
|
61
|
+
* blocking path is unchanged — only the abandoned and background
|
|
62
|
+
* completions go unheard, which is the behaviour before this existed.
|
|
63
|
+
*/
|
|
64
|
+
completionInbox?: CompletionInbox
|
|
65
|
+
|
|
44
66
|
/**
|
|
45
67
|
* HITL park channel for `ask_user_question`. The tool is registered
|
|
46
68
|
* only when BOTH `resumeHandler` and `runId` are present — without a
|
|
@@ -203,16 +225,52 @@ function delegateSchema(agentIds: readonly string[]): z.ZodType<string> {
|
|
|
203
225
|
return z.enum(agentIds as [string, ...string[]])
|
|
204
226
|
}
|
|
205
227
|
|
|
228
|
+
/**
|
|
229
|
+
* How much of a finished worker's output the listing inlines per task.
|
|
230
|
+
*
|
|
231
|
+
* A listing is consulted when several tasks are in flight, so the whole of
|
|
232
|
+
* every result would be a wall. Enough to be usable, with `wait_for_task`
|
|
233
|
+
* named as the way to get the rest.
|
|
234
|
+
*/
|
|
235
|
+
const LISTED_RESULT_LIMIT = 2_000
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* How long a coordinator tool may wait on a delegated agent.
|
|
239
|
+
*
|
|
240
|
+
* The executor's own default is two minutes, sized for a file read or a
|
|
241
|
+
* test run, and its docstring says outright that a tool which legitimately
|
|
242
|
+
* runs longer declares its own. This one runs an entire agent, and did not.
|
|
243
|
+
*
|
|
244
|
+
* Measured on real traffic: three delegated children took 4m21s, 5m58s and
|
|
245
|
+
* 8m04s; all three parents timed out at 120s. The children were never
|
|
246
|
+
* killed — only the parent's wait was — so the blocking path was not
|
|
247
|
+
* occasionally missed, it was structurally unreachable, and the model was
|
|
248
|
+
* left polling a listing because that was the only move left to it.
|
|
249
|
+
*
|
|
250
|
+
* An hour rather than "a bit more than eight minutes" because a generic
|
|
251
|
+
* stopwatch is the wrong instrument for a child that is making progress:
|
|
252
|
+
* a failure should come from what the child is doing, not from the clock
|
|
253
|
+
* the parent happens to be holding. Peer runtimes agree — the ones that
|
|
254
|
+
* bound a delegated child at all land on an hour, and several impose no
|
|
255
|
+
* wall-clock bound whatsoever, bounding turns or depth instead.
|
|
256
|
+
*
|
|
257
|
+
* A wedged child is still caught, an hour later, and the run budget and
|
|
258
|
+
* iteration ceiling both still apply above this.
|
|
259
|
+
*/
|
|
260
|
+
export const DELEGATION_TIMEOUT_MS = 60 * 60 * 1000
|
|
261
|
+
|
|
206
262
|
export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefinition[] {
|
|
207
263
|
const {
|
|
208
264
|
gateway,
|
|
209
265
|
allowedAgentIds: agentIds,
|
|
266
|
+
allowDelegation,
|
|
210
267
|
taskStore,
|
|
211
268
|
runId,
|
|
212
269
|
getPlanManager,
|
|
213
270
|
resumeHandler,
|
|
214
271
|
questionParks,
|
|
215
272
|
pendingAnswers,
|
|
273
|
+
completionInbox,
|
|
216
274
|
// `onTaskLaunched` was the entry point for the old
|
|
217
275
|
// non-blocking + envelope-injection flow. create_task is now
|
|
218
276
|
// blocking, so the callback is no longer wired here.
|
|
@@ -227,7 +285,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
227
285
|
|
|
228
286
|
const createTask = defineTool({
|
|
229
287
|
name: 'create_task',
|
|
230
|
-
description: `Launch a task on a specialized agent
|
|
288
|
+
description: `Launch a task on a specialized agent. By default this BLOCKS and returns the agent's final output as this call's tool_result; pass background: true to get a task_id back immediately and receive the result later as a task notification. Available agents: ${agentIds.join(', ')}. Prefer compact assignments; for large context, write/read shared workspace files and pass filenames or references. To launch multiple tasks in parallel, call this tool multiple times in a single assistant turn — the runtime executes every tool_use block from one response concurrently and delivers all tool_results together, so 'fan out 8 specialists' is one assistant message with 8 create_task blocks. Do not race: until a worker's result reaches you, you know nothing about it — never fabricate, summarise or predict what it will say, in any form.`,
|
|
231
289
|
inputSchema: z.object({
|
|
232
290
|
agent_id: agentIdEnum.describe('Which agent to run'),
|
|
233
291
|
prompt: z
|
|
@@ -242,13 +300,24 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
242
300
|
.describe(
|
|
243
301
|
'Existing planning task ID to link. If omitted, a planning task is auto-created.',
|
|
244
302
|
),
|
|
303
|
+
background: z
|
|
304
|
+
.boolean()
|
|
305
|
+
.optional()
|
|
306
|
+
.describe(
|
|
307
|
+
'Return immediately with a task_id instead of waiting. The result arrives later as a task notification. Use this when you have other work to do meanwhile; leave it off when the next thing you do depends on this answer.',
|
|
308
|
+
),
|
|
245
309
|
}),
|
|
246
310
|
category: 'custom',
|
|
247
311
|
permissions: [],
|
|
248
312
|
readOnly: false,
|
|
249
313
|
destructive: false,
|
|
250
314
|
concurrencySafe: true,
|
|
251
|
-
|
|
315
|
+
// See DELEGATION_TIMEOUT_MS. Ten minutes was the first attempt at
|
|
316
|
+
// this and was still a guess dressed as a measurement — real
|
|
317
|
+
// children were observed at 8m04s, which it would have survived by
|
|
318
|
+
// under two minutes.
|
|
319
|
+
timeoutMs: DELEGATION_TIMEOUT_MS,
|
|
320
|
+
async execute({ agent_id, prompt, description, plan_task_id, background }, _context) {
|
|
252
321
|
let resolvedPlanTaskId = plan_task_id
|
|
253
322
|
|
|
254
323
|
if (taskStore) {
|
|
@@ -279,14 +348,58 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
279
348
|
...(_context.parentSpan ? { parentSpan: _context.parentSpan } : {}),
|
|
280
349
|
})
|
|
281
350
|
|
|
351
|
+
if (background) {
|
|
352
|
+
// Tell the inbox to hold the run open for this. Without it the
|
|
353
|
+
// supervisor could launch a worker, answer, and settle the run
|
|
354
|
+
// while the worker was still going — discarding the result the
|
|
355
|
+
// launch existed to produce.
|
|
356
|
+
completionInbox?.expect(handle.taskId)
|
|
357
|
+
// Launched to run alongside this turn. Nothing waits on it, so
|
|
358
|
+
// its completion reaches the supervisor as a notification in the
|
|
359
|
+
// transcript instead — see `CompletionInbox`. Returning the id
|
|
360
|
+
// here is what makes that notification correlatable, and what
|
|
361
|
+
// lets `wait_for_task` and `agent_task_list` reach the output.
|
|
362
|
+
return {
|
|
363
|
+
success: true,
|
|
364
|
+
output: `Launched ${agent_id} in the background as task ${handle.taskId}. You are not waiting on it: keep working, and its result will arrive as a task notification. To fetch it yourself, call wait_for_task with this task_id.`,
|
|
365
|
+
data: {
|
|
366
|
+
task_id: handle.taskId,
|
|
367
|
+
agent_id,
|
|
368
|
+
description,
|
|
369
|
+
state: handle.state,
|
|
370
|
+
plan_task_id: resolvedPlanTaskId,
|
|
371
|
+
background: true,
|
|
372
|
+
},
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
282
376
|
// The tool returns its real result as the `tool_result` for the
|
|
283
377
|
// dispatching `tool_use`. Parallel fan-out happens at the executor
|
|
284
378
|
// layer: when the supervisor emits N `create_task` blocks in one
|
|
285
379
|
// assistant turn, the runtime runs them together and delivers all
|
|
286
|
-
// N `tool_result`s at once. No
|
|
287
|
-
//
|
|
288
|
-
// reject a duplicated id outright.
|
|
380
|
+
// N `tool_result`s at once. No second `tool_result` for the
|
|
381
|
+
// same `tool_use_id` — providers reject a duplicated id outright.
|
|
289
382
|
const completed = await gateway.waitForTask(handle.taskId)
|
|
383
|
+
|
|
384
|
+
// Whether this call is still the live path decides who delivers the
|
|
385
|
+
// result. If the executor already gave up on us — its deadline
|
|
386
|
+
// passed and the model was told "timed out, it may still be
|
|
387
|
+
// running" — then NOT claiming is what routes this completion to the
|
|
388
|
+
// transcript as a notification. Claiming it would delete the output.
|
|
389
|
+
//
|
|
390
|
+
// The returned value below goes nowhere: the executor won its race
|
|
391
|
+
// and returned already, so this result is discarded. It is written
|
|
392
|
+
// out anyway because a bare `return` here would read as an oversight,
|
|
393
|
+
// and because a host reading tool outcomes off its own instrumentation
|
|
394
|
+
// should find the reason rather than an empty string.
|
|
395
|
+
if (_context.abortSignal?.aborted) {
|
|
396
|
+
return {
|
|
397
|
+
success: false,
|
|
398
|
+
output: `This wait was abandoned before ${agent_id} finished; its result will arrive separately as a task notification (task ${handle.taskId}).`,
|
|
399
|
+
data: { task_id: handle.taskId, agent_id, abandoned: true },
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
completionInbox?.claim(handle.taskId)
|
|
290
403
|
const success = completed.state === 'completed'
|
|
291
404
|
const resultText =
|
|
292
405
|
completed.result?.result ??
|
|
@@ -350,7 +463,10 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
350
463
|
readOnly: false,
|
|
351
464
|
destructive: false,
|
|
352
465
|
concurrencySafe: true,
|
|
353
|
-
|
|
466
|
+
// It waits on a child exactly as create_task does, so it inherits
|
|
467
|
+
// the same bound rather than the file-read default.
|
|
468
|
+
timeoutMs: DELEGATION_TIMEOUT_MS,
|
|
469
|
+
async execute({ task_id, message }, _context) {
|
|
354
470
|
await gateway.continueTask(task_id as TaskId, message)
|
|
355
471
|
// Mirror create_task's blocking pattern: await the new
|
|
356
472
|
// completion and return the agent's output inline. The
|
|
@@ -359,6 +475,17 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
359
475
|
// onTaskCompleted listener that the iteration loop
|
|
360
476
|
// no longer registers (envelope path is dead).
|
|
361
477
|
const completed = await gateway.waitForTask(task_id as TaskId)
|
|
478
|
+
// Same reasoning as create_task: the model already has a timeout
|
|
479
|
+
// for this call, so leaving the completion unclaimed is what sends
|
|
480
|
+
// it to the transcript as a notification.
|
|
481
|
+
if (_context.abortSignal?.aborted) {
|
|
482
|
+
return {
|
|
483
|
+
success: false,
|
|
484
|
+
output: `This wait was abandoned before task ${task_id} finished; its result will arrive separately as a task notification.`,
|
|
485
|
+
data: { task_id, abandoned: true },
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
completionInbox?.claim(task_id as TaskId)
|
|
362
489
|
const success = completed.state === 'completed'
|
|
363
490
|
const resultText =
|
|
364
491
|
completed.result?.result ??
|
|
@@ -372,6 +499,75 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
372
499
|
},
|
|
373
500
|
})
|
|
374
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Join a task already running, without sending it anything.
|
|
504
|
+
*
|
|
505
|
+
* `continue_task` blocks, but only as a side effect of sending a
|
|
506
|
+
* message — so a supervisor that merely wanted to wait had to invent
|
|
507
|
+
* something to say, and one that would not do that was left calling
|
|
508
|
+
* `agent_task_list` in a sleep loop. That polling was never the model
|
|
509
|
+
* misbehaving; it was the only move on the board.
|
|
510
|
+
*/
|
|
511
|
+
const waitForTaskTool = defineTool({
|
|
512
|
+
name: 'wait_for_task',
|
|
513
|
+
description:
|
|
514
|
+
'Block until an already-running task finishes and return its output. Use this instead of listing tasks in a loop: it costs one call and no waiting turns. Give it a task_id from a background create_task or from a task notification.',
|
|
515
|
+
inputSchema: z.object({
|
|
516
|
+
task_id: z.string().describe('Agent task ID to wait for'),
|
|
517
|
+
}),
|
|
518
|
+
category: 'custom',
|
|
519
|
+
permissions: [],
|
|
520
|
+
readOnly: true,
|
|
521
|
+
destructive: false,
|
|
522
|
+
concurrencySafe: true,
|
|
523
|
+
// A tool whose entire purpose is to wait must not be cut off for
|
|
524
|
+
// waiting. Same bound as the launch it is waiting on.
|
|
525
|
+
timeoutMs: DELEGATION_TIMEOUT_MS,
|
|
526
|
+
async execute({ task_id }, _context) {
|
|
527
|
+
const known = gateway.getTask(task_id as TaskId)
|
|
528
|
+
if (!known) {
|
|
529
|
+
return {
|
|
530
|
+
success: false,
|
|
531
|
+
output: `No task ${task_id}. Call agent_task_list to see which tasks exist.`,
|
|
532
|
+
data: { task_id },
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const completed = await gateway.waitForTask(task_id as TaskId)
|
|
537
|
+
if (_context.abortSignal?.aborted) {
|
|
538
|
+
return {
|
|
539
|
+
success: false,
|
|
540
|
+
output: `This wait was abandoned before task ${task_id} finished; its result will arrive separately as a task notification.`,
|
|
541
|
+
data: { task_id, abandoned: true },
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
completionInbox?.claim(task_id as TaskId)
|
|
545
|
+
|
|
546
|
+
const success = completed.state === 'completed'
|
|
547
|
+
const resultText =
|
|
548
|
+
completed.result?.result ??
|
|
549
|
+
completed.result?.lastError ??
|
|
550
|
+
`Task finished with state: ${completed.state}`
|
|
551
|
+
return {
|
|
552
|
+
success,
|
|
553
|
+
output: wrapUntrusted(
|
|
554
|
+
{
|
|
555
|
+
kind: 'agent-result',
|
|
556
|
+
attributes: { agent: completed.agentId, task: completed.taskId },
|
|
557
|
+
provenance: `This is the output of the delegated agent "${completed.agentId}", not this agent's own work.`,
|
|
558
|
+
},
|
|
559
|
+
resultText,
|
|
560
|
+
),
|
|
561
|
+
data: {
|
|
562
|
+
task_id,
|
|
563
|
+
agent_id: completed.agentId,
|
|
564
|
+
state: completed.state,
|
|
565
|
+
result: resultText,
|
|
566
|
+
},
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
})
|
|
570
|
+
|
|
375
571
|
const cancelTask = defineTool({
|
|
376
572
|
name: 'cancel_task',
|
|
377
573
|
description:
|
|
@@ -386,6 +582,12 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
386
582
|
concurrencySafe: true,
|
|
387
583
|
async execute({ task_id }) {
|
|
388
584
|
gateway.cancelTask(task_id as TaskId)
|
|
585
|
+
// Stop holding the run open for it. `expect` put this task on the
|
|
586
|
+
// inbox's outstanding list at launch and only a completion takes it
|
|
587
|
+
// off — so without this a cancelled worker kept `hasPendingWork`
|
|
588
|
+
// true and every attempt to settle paid the full grace period
|
|
589
|
+
// waiting for a result that had just been called off.
|
|
590
|
+
completionInbox?.forget(task_id as TaskId)
|
|
389
591
|
return {
|
|
390
592
|
success: true,
|
|
391
593
|
output: `Task ${task_id} cancelled`,
|
|
@@ -397,7 +599,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
397
599
|
const agentTaskList = defineTool({
|
|
398
600
|
name: 'agent_task_list',
|
|
399
601
|
description:
|
|
400
|
-
"Inspect the live state of every agent task launched on this gateway via create_task: returns each task's id, agent, state (pending/running/completed/failed/canceled), and timing. Distinct from the plan-task store's `task_list` (which lists planning tasks): this tool lists running/completed worker invocations.
|
|
602
|
+
"Inspect the live state of every agent task launched on this gateway via create_task: returns each task's id, agent, state (pending/running/completed/failed/canceled), and timing. Distinct from the plan-task store's `task_list` (which lists planning tasks): this tool lists running/completed worker invocations. Do NOT call this to find out whether work finished: a blocking create_task has already returned each worker's output, and a backgrounded one arrives as a task notification. Use it when you need to see what is still running, or to re-read the output of a task whose launch you stopped waiting for.",
|
|
401
603
|
inputSchema: z.object({
|
|
402
604
|
state: z
|
|
403
605
|
.enum(['pending', 'running', 'completed', 'failed', 'canceled'])
|
|
@@ -415,6 +617,14 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
415
617
|
const items = filtered.map((h) => {
|
|
416
618
|
const runStatus = h.result?.status
|
|
417
619
|
const lastError = h.result?.lastError ?? undefined
|
|
620
|
+
// The worker's actual output, which this listing used to drop. It
|
|
621
|
+
// read `h.result` for the status and the error and stopped one
|
|
622
|
+
// property short of the thing the task was launched to produce —
|
|
623
|
+
// so a supervisor that knew a task_id and knew it had completed
|
|
624
|
+
// still had no way to read what it said. That is the state a run
|
|
625
|
+
// lands in whenever the launching call was abandoned, which is
|
|
626
|
+
// exactly when this listing gets consulted.
|
|
627
|
+
const output = h.result?.result ?? undefined
|
|
418
628
|
return {
|
|
419
629
|
task_id: h.taskId,
|
|
420
630
|
agent_id: h.agentId,
|
|
@@ -424,6 +634,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
424
634
|
completed_at: h.completedAt ? new Date(h.completedAt).toISOString() : null,
|
|
425
635
|
duration_ms: h.completedAt ? h.completedAt - h.createdAt : null,
|
|
426
636
|
last_error: lastError,
|
|
637
|
+
...(output !== undefined ? { result: output } : {}),
|
|
427
638
|
}
|
|
428
639
|
})
|
|
429
640
|
const summary = {
|
|
@@ -434,12 +645,26 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
434
645
|
canceled: handles.filter((h) => h.state === 'canceled').length,
|
|
435
646
|
}
|
|
436
647
|
const lines = items.length
|
|
437
|
-
? items.map(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
648
|
+
? items.map((i) => {
|
|
649
|
+
const head = `- ${i.task_id} → ${i.agent_id} [${i.state}${i.run_status && i.run_status !== i.state ? ` / ${i.run_status}` : ''}]${
|
|
650
|
+
i.duration_ms !== null ? ` (${Math.round(i.duration_ms / 1000)}s)` : ''
|
|
651
|
+
}${i.last_error ? ` — error: ${i.last_error.slice(0, 200)}` : ''}`
|
|
652
|
+
// The output goes in the rendered TEXT, not only in `data`.
|
|
653
|
+
// Only `output` becomes the tool_result the model reads —
|
|
654
|
+
// the executor never looks at `data` — so a result added
|
|
655
|
+
// to the projection alone would have been added to a field the
|
|
656
|
+
// model cannot see, which is how this listing came to prove a
|
|
657
|
+
// task had finished while withholding what it said.
|
|
658
|
+
if (i.result === undefined) return head
|
|
659
|
+
const body =
|
|
660
|
+
i.result.length > LISTED_RESULT_LIMIT
|
|
661
|
+
? `${i.result.slice(0, LISTED_RESULT_LIMIT)}\n … truncated; call wait_for_task with "${i.task_id}" for the whole thing.`
|
|
662
|
+
: i.result
|
|
663
|
+
return `${head}\n${body
|
|
664
|
+
.split('\n')
|
|
665
|
+
.map((line) => ` ${line}`)
|
|
666
|
+
.join('\n')}`
|
|
667
|
+
})
|
|
443
668
|
: ['(no tasks launched yet)']
|
|
444
669
|
const header = `Tasks: ${summary.total} total — ${summary.running} running, ${summary.completed} completed, ${summary.failed} failed, ${summary.canceled} canceled`
|
|
445
670
|
return {
|
|
@@ -462,12 +687,20 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
462
687
|
// non-default gateway (one that keeps the worker process alive
|
|
463
688
|
// for follow-ups) wants to re-register it.
|
|
464
689
|
void continueTask
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
|
|
690
|
+
// `cancel_task` is registered again, and the reasoning that dropped it is
|
|
691
|
+
// worth keeping because it was sound at the time and is not any more.
|
|
692
|
+
//
|
|
693
|
+
// It read: per-task cancellation belonged to the old non-blocking worker
|
|
694
|
+
// protocol; since `create_task` blocks and returns the output as its
|
|
695
|
+
// tool_result, every worker is terminal by the time a later turn learns its
|
|
696
|
+
// id, so the tool could only manufacture a "cancelled" for something
|
|
697
|
+
// already finished.
|
|
698
|
+
//
|
|
699
|
+
// That held for exactly as long as blocking was the only way to launch.
|
|
700
|
+
// `background: true` brings back a worker that is running with nothing
|
|
701
|
+
// waiting on it and whose id the supervisor holds while it is still alive —
|
|
702
|
+
// the precondition the old rationale said had disappeared. A launch the
|
|
703
|
+
// model cannot stop is a hole, and this is the tool that closes it.
|
|
471
704
|
// An empty roster withholds `create_task` rather than mounting an
|
|
472
705
|
// unsatisfiable one. Mounting it and refusing at parse time reaches the
|
|
473
706
|
// same verdict, but it reaches it the expensive way: the model is shown a
|
|
@@ -485,8 +718,32 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
|
|
|
485
718
|
// and a human channel" stays a supported configuration, which is why this
|
|
486
719
|
// omits rather than refusing to build: a caller asking this builder for
|
|
487
720
|
// `ask_user_question` with no roster is doing something legitimate.
|
|
488
|
-
|
|
489
|
-
|
|
721
|
+
// `wait_for_task` and `cancel_task` ride with `create_task` because they
|
|
722
|
+
// are only meaningful once something has been launched.
|
|
723
|
+
//
|
|
724
|
+
// Waiting had no tool at all. `continue_task` blocks, but only as a side
|
|
725
|
+
// effect of sending a message, so a supervisor that merely wanted to wait
|
|
726
|
+
// had to invent something to say — and one that would not do that was left
|
|
727
|
+
// calling `agent_task_list` in a sleep loop. That was never the model
|
|
728
|
+
// misbehaving; it was the only move available.
|
|
729
|
+
// Two independent reasons not to mount the delegation surface, and the
|
|
730
|
+
// second one is not derivable from the first.
|
|
731
|
+
//
|
|
732
|
+
// An empty roster answers WHO may be called: nobody, so the tools have
|
|
733
|
+
// nothing to act on. `allowDelegation: false` answers WHETHER this run may
|
|
734
|
+
// call anyone, which a non-empty roster cannot settle — a host that runs a
|
|
735
|
+
// specialist by putting its persona into the supervisor shell and its id
|
|
736
|
+
// into the roster has a list of one and must still delegate to nobody.
|
|
737
|
+
// From inside this function that run is indistinguishable from a
|
|
738
|
+
// supervisor whose roster happens to hold a single specialist, so the
|
|
739
|
+
// caller states the fact rather than the SDK guessing it.
|
|
740
|
+
//
|
|
741
|
+
// `!== false` rather than truthiness, so an absent flag keeps today's
|
|
742
|
+
// behaviour exactly.
|
|
743
|
+
const canDelegate = agentIds.length > 0 && allowDelegation !== false
|
|
744
|
+
const tools: ToolDefinition[] = canDelegate
|
|
745
|
+
? [createTask, waitForTaskTool, cancelTask, agentTaskList]
|
|
746
|
+
: [agentTaskList]
|
|
490
747
|
|
|
491
748
|
if (getPlanManager) {
|
|
492
749
|
const approvePlan = defineTool({
|
|
@@ -19,6 +19,34 @@ export interface SupervisorAgentConfig extends BaseAgentConfig {
|
|
|
19
19
|
|
|
20
20
|
agentIds: string[]
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* May this run invoke subagents at all? Defaults to `true`.
|
|
24
|
+
*
|
|
25
|
+
* `agentIds` answers WHO may be called; this answers WHETHER, and they are
|
|
26
|
+
* different questions. A run whose own persona is the single agent on the
|
|
27
|
+
* list has a non-empty list and still must not call anyone.
|
|
28
|
+
*
|
|
29
|
+
* It cannot be derived. Comparing the list against the executing agent
|
|
30
|
+
* fails where a host substitutes a specialist's persona into the
|
|
31
|
+
* supervisor shell — the two ids differ, so the predicate says "can
|
|
32
|
+
* delegate" about a run that cannot. And no predicate over `agentIds`
|
|
33
|
+
* could work, because a supervisor whose list holds one specialist and a
|
|
34
|
+
* run that IS that specialist are indistinguishable in it. The fact lives
|
|
35
|
+
* with the caller, so the caller states it.
|
|
36
|
+
*
|
|
37
|
+
* Positive polarity on purpose: `allowDelegation: false` reads correctly
|
|
38
|
+
* the first time, where a `delegationDisabled` spelling inverts twice at
|
|
39
|
+
* every site that consults it.
|
|
40
|
+
*
|
|
41
|
+
* **Absolute.** `runtimeToolOverrides` cannot put the delegation tools
|
|
42
|
+
* back — the override pass runs over the tools this flag already declined
|
|
43
|
+
* to build, and both values come from the same caller in the same call, so
|
|
44
|
+
* "must not delegate" plus "give it `create_task`" is a contradiction
|
|
45
|
+
* rather than extra knowledge. This matches `agentIds: []`, which is
|
|
46
|
+
* absolute today for the same reason.
|
|
47
|
+
*/
|
|
48
|
+
allowDelegation?: boolean
|
|
49
|
+
|
|
22
50
|
gateway?: TaskGateway
|
|
23
51
|
agentManager?: AgentManagerContract
|
|
24
52
|
tools?: ToolRegistryContract
|
package/src/types/tool/index.ts
CHANGED
|
@@ -100,6 +100,19 @@ export interface ToolContext {
|
|
|
100
100
|
invocationState?: InvocationState
|
|
101
101
|
|
|
102
102
|
toolRegistry?: ToolRegistryRef
|
|
103
|
+
/**
|
|
104
|
+
* The names this turn may call, if the turn was narrowed.
|
|
105
|
+
*
|
|
106
|
+
* Enforced at dispatch, not only used to decide which schemas the model is
|
|
107
|
+
* shown. It was the latter alone for a while, which made the narrowing
|
|
108
|
+
* presentational: a step could withhold a tool from the request and the
|
|
109
|
+
* executor would still run it when the model named it anyway — from
|
|
110
|
+
* repeated context, from a gateway with its own tool memory, or from a
|
|
111
|
+
* replayed prefix.
|
|
112
|
+
*
|
|
113
|
+
* Absent means no narrowing, which is not the same as an empty list: an
|
|
114
|
+
* empty list is a turn that may call nothing.
|
|
115
|
+
*/
|
|
103
116
|
allowedTools?: readonly string[]
|
|
104
117
|
sandbox?: Sandbox
|
|
105
118
|
fileReadTracker?: FileReadTracker
|