@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.
Files changed (107) hide show
  1. package/CHANGELOG.md +254 -0
  2. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  3. package/dist/agents/SupervisorAgent.js +18 -0
  4. package/dist/agents/SupervisorAgent.js.map +1 -1
  5. package/dist/agents/__tests__/supervisor-coordinator-registration.test.js +31 -0
  6. package/dist/agents/__tests__/supervisor-coordinator-registration.test.js.map +1 -1
  7. package/dist/gateway/__tests__/completion-inbox.test.d.ts +2 -0
  8. package/dist/gateway/__tests__/completion-inbox.test.d.ts.map +1 -0
  9. package/dist/gateway/__tests__/completion-inbox.test.js +275 -0
  10. package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -0
  11. package/dist/gateway/completion-inbox.d.ts +113 -0
  12. package/dist/gateway/completion-inbox.d.ts.map +1 -0
  13. package/dist/gateway/completion-inbox.js +225 -0
  14. package/dist/gateway/completion-inbox.js.map +1 -0
  15. package/dist/public-runtime.d.ts +1 -0
  16. package/dist/public-runtime.d.ts.map +1 -1
  17. package/dist/public-runtime.js +4 -0
  18. package/dist/public-runtime.js.map +1 -1
  19. package/dist/registry/tool/__tests__/dialect.test.js +8 -5
  20. package/dist/registry/tool/__tests__/dialect.test.js.map +1 -1
  21. package/dist/registry/tool/dialect.js +11 -4
  22. package/dist/registry/tool/dialect.js.map +1 -1
  23. package/dist/registry/tool/execute.d.ts.map +1 -1
  24. package/dist/registry/tool/execute.js +34 -0
  25. package/dist/registry/tool/execute.js.map +1 -1
  26. package/dist/runtime/query/__tests__/completion-notification.test.d.ts +2 -0
  27. package/dist/runtime/query/__tests__/completion-notification.test.d.ts.map +1 -0
  28. package/dist/runtime/query/__tests__/completion-notification.test.js +168 -0
  29. package/dist/runtime/query/__tests__/completion-notification.test.js.map +1 -0
  30. package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.d.ts +2 -0
  31. package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.d.ts.map +1 -0
  32. package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.js +122 -0
  33. package/dist/runtime/query/__tests__/run-survives-its-own-park.proc-test.js.map +1 -0
  34. package/dist/runtime/query/__tests__/step-allow-list.test.d.ts +2 -0
  35. package/dist/runtime/query/__tests__/step-allow-list.test.d.ts.map +1 -0
  36. package/dist/runtime/query/__tests__/step-allow-list.test.js +171 -0
  37. package/dist/runtime/query/__tests__/step-allow-list.test.js.map +1 -0
  38. package/dist/runtime/query/executor.d.ts +14 -0
  39. package/dist/runtime/query/executor.d.ts.map +1 -1
  40. package/dist/runtime/query/executor.js +20 -1
  41. package/dist/runtime/query/executor.js.map +1 -1
  42. package/dist/runtime/query/index.d.ts +9 -0
  43. package/dist/runtime/query/index.d.ts.map +1 -1
  44. package/dist/runtime/query/index.js +1 -0
  45. package/dist/runtime/query/index.js.map +1 -1
  46. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  47. package/dist/runtime/query/iteration/index.js +74 -1
  48. package/dist/runtime/query/iteration/index.js.map +1 -1
  49. package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
  50. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  51. package/dist/runtime/query/iteration/phases/context.js +43 -13
  52. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  53. package/dist/tools/builtins/__tests__/bash.proc-test.d.ts +2 -0
  54. package/dist/tools/builtins/__tests__/bash.proc-test.d.ts.map +1 -0
  55. package/dist/tools/builtins/__tests__/bash.proc-test.js +115 -0
  56. package/dist/tools/builtins/__tests__/bash.proc-test.js.map +1 -0
  57. package/dist/tools/builtins/__tests__/bash.test.d.ts +2 -0
  58. package/dist/tools/builtins/__tests__/bash.test.d.ts.map +1 -0
  59. package/dist/tools/builtins/__tests__/bash.test.js +65 -0
  60. package/dist/tools/builtins/__tests__/bash.test.js.map +1 -0
  61. package/dist/tools/builtins/bash.d.ts.map +1 -1
  62. package/dist/tools/builtins/bash.js +89 -17
  63. package/dist/tools/builtins/bash.js.map +1 -1
  64. package/dist/tools/coordinator/__tests__/allow-delegation.test.d.ts +2 -0
  65. package/dist/tools/coordinator/__tests__/allow-delegation.test.d.ts.map +1 -0
  66. package/dist/tools/coordinator/__tests__/allow-delegation.test.js +94 -0
  67. package/dist/tools/coordinator/__tests__/allow-delegation.test.js.map +1 -0
  68. package/dist/tools/coordinator/__tests__/completion-delivery.test.d.ts +2 -0
  69. package/dist/tools/coordinator/__tests__/completion-delivery.test.d.ts.map +1 -0
  70. package/dist/tools/coordinator/__tests__/completion-delivery.test.js +255 -0
  71. package/dist/tools/coordinator/__tests__/completion-delivery.test.js.map +1 -0
  72. package/dist/tools/coordinator/__tests__/task-list.test.js +13 -7
  73. package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
  74. package/dist/tools/coordinator/index.d.ts +44 -0
  75. package/dist/tools/coordinator/index.d.ts.map +1 -1
  76. package/dist/tools/coordinator/index.js +239 -16
  77. package/dist/tools/coordinator/index.js.map +1 -1
  78. package/dist/types/agent/supervisor.d.ts +27 -0
  79. package/dist/types/agent/supervisor.d.ts.map +1 -1
  80. package/dist/types/tool/index.d.ts +13 -0
  81. package/dist/types/tool/index.d.ts.map +1 -1
  82. package/dist/types/tool/index.js.map +1 -1
  83. package/package.json +3 -2
  84. package/src/agents/SupervisorAgent.ts +19 -0
  85. package/src/agents/__tests__/supervisor-coordinator-registration.test.ts +37 -0
  86. package/src/gateway/__tests__/completion-inbox.test.ts +343 -0
  87. package/src/gateway/completion-inbox.ts +241 -0
  88. package/src/public-runtime.ts +4 -0
  89. package/src/registry/tool/__tests__/dialect.test.ts +8 -5
  90. package/src/registry/tool/dialect.ts +11 -4
  91. package/src/registry/tool/execute.ts +35 -0
  92. package/src/runtime/query/__tests__/completion-notification.test.ts +196 -0
  93. package/src/runtime/query/__tests__/run-survives-its-own-park.proc-test.ts +127 -0
  94. package/src/runtime/query/__tests__/step-allow-list.test.ts +205 -0
  95. package/src/runtime/query/executor.ts +21 -1
  96. package/src/runtime/query/index.ts +11 -0
  97. package/src/runtime/query/iteration/index.ts +77 -1
  98. package/src/runtime/query/iteration/phases/context.ts +51 -15
  99. package/src/tools/builtins/__tests__/bash.proc-test.ts +142 -0
  100. package/src/tools/builtins/__tests__/bash.test.ts +79 -0
  101. package/src/tools/builtins/bash.ts +100 -18
  102. package/src/tools/coordinator/__tests__/allow-delegation.test.ts +120 -0
  103. package/src/tools/coordinator/__tests__/completion-delivery.test.ts +347 -0
  104. package/src/tools/coordinator/__tests__/task-list.test.ts +13 -7
  105. package/src/tools/coordinator/index.ts +278 -21
  106. package/src/types/agent/supervisor.ts +28 -0
  107. 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 and await its result. BLOCKING: returns the agent's final output as this call's tool_result. 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.`,
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
- async execute({ agent_id, prompt, description, plan_task_id }, _context) {
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 async envelope injection, and no
287
- // second `tool_result` for the same `tool_use_id` providers
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
- async execute({ task_id, message }) {
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. Use it BEFORE declaring multi-worker work done confirm every launched task reached `completed`, none still `running` or `failed`. Read-only and safe to call repeatedly.",
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
- (i) =>
439
- `- ${i.task_id} → ${i.agent_id} [${i.state}${i.run_status && i.run_status !== i.state ? ` / ${i.run_status}` : ''}]${
440
- i.duration_ms !== null ? ` (${Math.round(i.duration_ms / 1000)}s)` : ''
441
- }${i.last_error ? ` error: ${i.last_error.slice(0, 200)}` : ''}`,
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
- // Per-task cancellation belonged to the old non-blocking worker protocol.
466
- // `create_task` blocks and returns the worker output as its tool_result, so
467
- // every worker is terminal by the time a later turn learns its id — the tool
468
- // could only ever manufacture a "cancelled" for something already done.
469
- // Host-owned interruption still uses the gateway contract directly.
470
- void cancelTask
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
- const tools: ToolDefinition[] =
489
- agentIds.length > 0 ? [createTask, agentTaskList] : [agentTaskList]
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
@@ -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