@namzu/sdk 5.2.0 → 6.1.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 (111) hide show
  1. package/CHANGELOG.md +237 -0
  2. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  3. package/dist/agents/SupervisorAgent.js +13 -0
  4. package/dist/agents/SupervisorAgent.js.map +1 -1
  5. package/dist/gateway/__tests__/completion-inbox.test.d.ts +2 -0
  6. package/dist/gateway/__tests__/completion-inbox.test.d.ts.map +1 -0
  7. package/dist/gateway/__tests__/completion-inbox.test.js +258 -0
  8. package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -0
  9. package/dist/gateway/completion-inbox.d.ts +113 -0
  10. package/dist/gateway/completion-inbox.d.ts.map +1 -0
  11. package/dist/gateway/completion-inbox.js +211 -0
  12. package/dist/gateway/completion-inbox.js.map +1 -0
  13. package/dist/provider/__tests__/strict-schema.test.js +50 -2
  14. package/dist/provider/__tests__/strict-schema.test.js.map +1 -1
  15. package/dist/provider/__tests__/vendor-detail.test.d.ts +2 -0
  16. package/dist/provider/__tests__/vendor-detail.test.d.ts.map +1 -0
  17. package/dist/provider/__tests__/vendor-detail.test.js +89 -0
  18. package/dist/provider/__tests__/vendor-detail.test.js.map +1 -0
  19. package/dist/provider/errors.d.ts +38 -5
  20. package/dist/provider/errors.d.ts.map +1 -1
  21. package/dist/provider/errors.js +107 -5
  22. package/dist/provider/errors.js.map +1 -1
  23. package/dist/provider/strict-schema.d.ts.map +1 -1
  24. package/dist/provider/strict-schema.js +64 -8
  25. package/dist/provider/strict-schema.js.map +1 -1
  26. package/dist/public-runtime.d.ts +4 -0
  27. package/dist/public-runtime.d.ts.map +1 -1
  28. package/dist/public-runtime.js +10 -0
  29. package/dist/public-runtime.js.map +1 -1
  30. package/dist/registry/tool/__tests__/dialect.test.d.ts +2 -0
  31. package/dist/registry/tool/__tests__/dialect.test.d.ts.map +1 -0
  32. package/dist/registry/tool/__tests__/dialect.test.js +146 -0
  33. package/dist/registry/tool/__tests__/dialect.test.js.map +1 -0
  34. package/dist/registry/tool/dialect.d.ts +50 -0
  35. package/dist/registry/tool/dialect.d.ts.map +1 -0
  36. package/dist/registry/tool/dialect.js +138 -0
  37. package/dist/registry/tool/dialect.js.map +1 -0
  38. package/dist/registry/tool/execute.d.ts.map +1 -1
  39. package/dist/registry/tool/execute.js +34 -0
  40. package/dist/registry/tool/execute.js.map +1 -1
  41. package/dist/registry/toolset/catalog.d.ts.map +1 -1
  42. package/dist/registry/toolset/catalog.js +10 -5
  43. package/dist/registry/toolset/catalog.js.map +1 -1
  44. package/dist/runtime/query/__tests__/completion-notification.test.d.ts +2 -0
  45. package/dist/runtime/query/__tests__/completion-notification.test.d.ts.map +1 -0
  46. package/dist/runtime/query/__tests__/completion-notification.test.js +168 -0
  47. package/dist/runtime/query/__tests__/completion-notification.test.js.map +1 -0
  48. package/dist/runtime/query/__tests__/step-allow-list.test.d.ts +2 -0
  49. package/dist/runtime/query/__tests__/step-allow-list.test.d.ts.map +1 -0
  50. package/dist/runtime/query/__tests__/step-allow-list.test.js +171 -0
  51. package/dist/runtime/query/__tests__/step-allow-list.test.js.map +1 -0
  52. package/dist/runtime/query/__tests__/stream-recovery.test.js +6 -0
  53. package/dist/runtime/query/__tests__/stream-recovery.test.js.map +1 -1
  54. package/dist/runtime/query/executor.d.ts +14 -0
  55. package/dist/runtime/query/executor.d.ts.map +1 -1
  56. package/dist/runtime/query/executor.js +20 -1
  57. package/dist/runtime/query/executor.js.map +1 -1
  58. package/dist/runtime/query/index.d.ts +9 -0
  59. package/dist/runtime/query/index.d.ts.map +1 -1
  60. package/dist/runtime/query/index.js +1 -0
  61. package/dist/runtime/query/index.js.map +1 -1
  62. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  63. package/dist/runtime/query/iteration/index.js +74 -1
  64. package/dist/runtime/query/iteration/index.js.map +1 -1
  65. package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
  66. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  67. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  68. package/dist/runtime/query/result.d.ts.map +1 -1
  69. package/dist/runtime/query/result.js +6 -0
  70. package/dist/runtime/query/result.js.map +1 -1
  71. package/dist/tools/coordinator/__tests__/completion-delivery.test.d.ts +2 -0
  72. package/dist/tools/coordinator/__tests__/completion-delivery.test.d.ts.map +1 -0
  73. package/dist/tools/coordinator/__tests__/completion-delivery.test.js +255 -0
  74. package/dist/tools/coordinator/__tests__/completion-delivery.test.js.map +1 -0
  75. package/dist/tools/coordinator/__tests__/task-list.test.js +13 -7
  76. package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
  77. package/dist/tools/coordinator/index.d.ts +35 -0
  78. package/dist/tools/coordinator/index.d.ts.map +1 -1
  79. package/dist/tools/coordinator/index.js +222 -16
  80. package/dist/tools/coordinator/index.js.map +1 -1
  81. package/dist/types/provider/error.d.ts +20 -4
  82. package/dist/types/provider/error.d.ts.map +1 -1
  83. package/dist/types/tool/index.d.ts +13 -0
  84. package/dist/types/tool/index.d.ts.map +1 -1
  85. package/dist/types/tool/index.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/agents/SupervisorAgent.ts +14 -0
  88. package/src/gateway/__tests__/completion-inbox.test.ts +320 -0
  89. package/src/gateway/completion-inbox.ts +227 -0
  90. package/src/provider/__tests__/strict-schema.test.ts +58 -2
  91. package/src/provider/__tests__/vendor-detail.test.ts +107 -0
  92. package/src/provider/errors.ts +106 -5
  93. package/src/provider/strict-schema.ts +65 -8
  94. package/src/public-runtime.ts +11 -0
  95. package/src/registry/tool/__tests__/dialect.test.ts +200 -0
  96. package/src/registry/tool/dialect.ts +143 -0
  97. package/src/registry/tool/execute.ts +35 -0
  98. package/src/registry/toolset/catalog.ts +10 -5
  99. package/src/runtime/query/__tests__/completion-notification.test.ts +196 -0
  100. package/src/runtime/query/__tests__/step-allow-list.test.ts +205 -0
  101. package/src/runtime/query/__tests__/stream-recovery.test.ts +6 -0
  102. package/src/runtime/query/executor.ts +21 -1
  103. package/src/runtime/query/index.ts +11 -0
  104. package/src/runtime/query/iteration/index.ts +77 -1
  105. package/src/runtime/query/iteration/phases/context.ts +10 -0
  106. package/src/runtime/query/result.ts +6 -0
  107. package/src/tools/coordinator/__tests__/completion-delivery.test.ts +347 -0
  108. package/src/tools/coordinator/__tests__/task-list.test.ts +13 -7
  109. package/src/tools/coordinator/index.ts +250 -20
  110. package/src/types/provider/error.ts +20 -4
  111. 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'
@@ -41,6 +42,17 @@ export interface CoordinatorToolsOptions {
41
42
 
42
43
  onTaskLaunched?: TaskLaunchedCallback
43
44
 
45
+ /**
46
+ * Where a completion goes when no call is left waiting for it.
47
+ *
48
+ * These tools claim a completion the moment they hand it to the model as a
49
+ * `tool_result`; anything unclaimed is delivered to the transcript as a
50
+ * notification instead. Without an inbox the tools still work and the
51
+ * blocking path is unchanged — only the abandoned and background
52
+ * completions go unheard, which is the behaviour before this existed.
53
+ */
54
+ completionInbox?: CompletionInbox
55
+
44
56
  /**
45
57
  * HITL park channel for `ask_user_question`. The tool is registered
46
58
  * only when BOTH `resumeHandler` and `runId` are present — without a
@@ -203,6 +215,40 @@ function delegateSchema(agentIds: readonly string[]): z.ZodType<string> {
203
215
  return z.enum(agentIds as [string, ...string[]])
204
216
  }
205
217
 
218
+ /**
219
+ * How much of a finished worker's output the listing inlines per task.
220
+ *
221
+ * A listing is consulted when several tasks are in flight, so the whole of
222
+ * every result would be a wall. Enough to be usable, with `wait_for_task`
223
+ * named as the way to get the rest.
224
+ */
225
+ const LISTED_RESULT_LIMIT = 2_000
226
+
227
+ /**
228
+ * How long a coordinator tool may wait on a delegated agent.
229
+ *
230
+ * The executor's own default is two minutes, sized for a file read or a
231
+ * test run, and its docstring says outright that a tool which legitimately
232
+ * runs longer declares its own. This one runs an entire agent, and did not.
233
+ *
234
+ * Measured on real traffic: three delegated children took 4m21s, 5m58s and
235
+ * 8m04s; all three parents timed out at 120s. The children were never
236
+ * killed — only the parent's wait was — so the blocking path was not
237
+ * occasionally missed, it was structurally unreachable, and the model was
238
+ * left polling a listing because that was the only move left to it.
239
+ *
240
+ * An hour rather than "a bit more than eight minutes" because a generic
241
+ * stopwatch is the wrong instrument for a child that is making progress:
242
+ * a failure should come from what the child is doing, not from the clock
243
+ * the parent happens to be holding. Peer runtimes agree — the ones that
244
+ * bound a delegated child at all land on an hour, and several impose no
245
+ * wall-clock bound whatsoever, bounding turns or depth instead.
246
+ *
247
+ * A wedged child is still caught, an hour later, and the run budget and
248
+ * iteration ceiling both still apply above this.
249
+ */
250
+ export const DELEGATION_TIMEOUT_MS = 60 * 60 * 1000
251
+
206
252
  export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefinition[] {
207
253
  const {
208
254
  gateway,
@@ -213,6 +259,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
213
259
  resumeHandler,
214
260
  questionParks,
215
261
  pendingAnswers,
262
+ completionInbox,
216
263
  // `onTaskLaunched` was the entry point for the old
217
264
  // non-blocking + envelope-injection flow. create_task is now
218
265
  // blocking, so the callback is no longer wired here.
@@ -227,7 +274,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
227
274
 
228
275
  const createTask = defineTool({
229
276
  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.`,
277
+ 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
278
  inputSchema: z.object({
232
279
  agent_id: agentIdEnum.describe('Which agent to run'),
233
280
  prompt: z
@@ -242,13 +289,24 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
242
289
  .describe(
243
290
  'Existing planning task ID to link. If omitted, a planning task is auto-created.',
244
291
  ),
292
+ background: z
293
+ .boolean()
294
+ .optional()
295
+ .describe(
296
+ '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.',
297
+ ),
245
298
  }),
246
299
  category: 'custom',
247
300
  permissions: [],
248
301
  readOnly: false,
249
302
  destructive: false,
250
303
  concurrencySafe: true,
251
- async execute({ agent_id, prompt, description, plan_task_id }, _context) {
304
+ // See DELEGATION_TIMEOUT_MS. Ten minutes was the first attempt at
305
+ // this and was still a guess dressed as a measurement — real
306
+ // children were observed at 8m04s, which it would have survived by
307
+ // under two minutes.
308
+ timeoutMs: DELEGATION_TIMEOUT_MS,
309
+ async execute({ agent_id, prompt, description, plan_task_id, background }, _context) {
252
310
  let resolvedPlanTaskId = plan_task_id
253
311
 
254
312
  if (taskStore) {
@@ -279,14 +337,58 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
279
337
  ...(_context.parentSpan ? { parentSpan: _context.parentSpan } : {}),
280
338
  })
281
339
 
340
+ if (background) {
341
+ // Tell the inbox to hold the run open for this. Without it the
342
+ // supervisor could launch a worker, answer, and settle the run
343
+ // while the worker was still going — discarding the result the
344
+ // launch existed to produce.
345
+ completionInbox?.expect(handle.taskId)
346
+ // Launched to run alongside this turn. Nothing waits on it, so
347
+ // its completion reaches the supervisor as a notification in the
348
+ // transcript instead — see `CompletionInbox`. Returning the id
349
+ // here is what makes that notification correlatable, and what
350
+ // lets `wait_for_task` and `agent_task_list` reach the output.
351
+ return {
352
+ success: true,
353
+ 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.`,
354
+ data: {
355
+ task_id: handle.taskId,
356
+ agent_id,
357
+ description,
358
+ state: handle.state,
359
+ plan_task_id: resolvedPlanTaskId,
360
+ background: true,
361
+ },
362
+ }
363
+ }
364
+
282
365
  // The tool returns its real result as the `tool_result` for the
283
366
  // dispatching `tool_use`. Parallel fan-out happens at the executor
284
367
  // layer: when the supervisor emits N `create_task` blocks in one
285
368
  // 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.
369
+ // N `tool_result`s at once. No second `tool_result` for the
370
+ // same `tool_use_id` providers reject a duplicated id outright.
289
371
  const completed = await gateway.waitForTask(handle.taskId)
372
+
373
+ // Whether this call is still the live path decides who delivers the
374
+ // result. If the executor already gave up on us — its deadline
375
+ // passed and the model was told "timed out, it may still be
376
+ // running" — then NOT claiming is what routes this completion to the
377
+ // transcript as a notification. Claiming it would delete the output.
378
+ //
379
+ // The returned value below goes nowhere: the executor won its race
380
+ // and returned already, so this result is discarded. It is written
381
+ // out anyway because a bare `return` here would read as an oversight,
382
+ // and because a host reading tool outcomes off its own instrumentation
383
+ // should find the reason rather than an empty string.
384
+ if (_context.abortSignal?.aborted) {
385
+ return {
386
+ success: false,
387
+ output: `This wait was abandoned before ${agent_id} finished; its result will arrive separately as a task notification (task ${handle.taskId}).`,
388
+ data: { task_id: handle.taskId, agent_id, abandoned: true },
389
+ }
390
+ }
391
+ completionInbox?.claim(handle.taskId)
290
392
  const success = completed.state === 'completed'
291
393
  const resultText =
292
394
  completed.result?.result ??
@@ -350,7 +452,10 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
350
452
  readOnly: false,
351
453
  destructive: false,
352
454
  concurrencySafe: true,
353
- async execute({ task_id, message }) {
455
+ // It waits on a child exactly as create_task does, so it inherits
456
+ // the same bound rather than the file-read default.
457
+ timeoutMs: DELEGATION_TIMEOUT_MS,
458
+ async execute({ task_id, message }, _context) {
354
459
  await gateway.continueTask(task_id as TaskId, message)
355
460
  // Mirror create_task's blocking pattern: await the new
356
461
  // completion and return the agent's output inline. The
@@ -359,6 +464,17 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
359
464
  // onTaskCompleted listener that the iteration loop
360
465
  // no longer registers (envelope path is dead).
361
466
  const completed = await gateway.waitForTask(task_id as TaskId)
467
+ // Same reasoning as create_task: the model already has a timeout
468
+ // for this call, so leaving the completion unclaimed is what sends
469
+ // it to the transcript as a notification.
470
+ if (_context.abortSignal?.aborted) {
471
+ return {
472
+ success: false,
473
+ output: `This wait was abandoned before task ${task_id} finished; its result will arrive separately as a task notification.`,
474
+ data: { task_id, abandoned: true },
475
+ }
476
+ }
477
+ completionInbox?.claim(task_id as TaskId)
362
478
  const success = completed.state === 'completed'
363
479
  const resultText =
364
480
  completed.result?.result ??
@@ -372,6 +488,75 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
372
488
  },
373
489
  })
374
490
 
491
+ /**
492
+ * Join a task already running, without sending it anything.
493
+ *
494
+ * `continue_task` blocks, but only as a side effect of sending a
495
+ * message — so a supervisor that merely wanted to wait had to invent
496
+ * something to say, and one that would not do that was left calling
497
+ * `agent_task_list` in a sleep loop. That polling was never the model
498
+ * misbehaving; it was the only move on the board.
499
+ */
500
+ const waitForTaskTool = defineTool({
501
+ name: 'wait_for_task',
502
+ description:
503
+ '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.',
504
+ inputSchema: z.object({
505
+ task_id: z.string().describe('Agent task ID to wait for'),
506
+ }),
507
+ category: 'custom',
508
+ permissions: [],
509
+ readOnly: true,
510
+ destructive: false,
511
+ concurrencySafe: true,
512
+ // A tool whose entire purpose is to wait must not be cut off for
513
+ // waiting. Same bound as the launch it is waiting on.
514
+ timeoutMs: DELEGATION_TIMEOUT_MS,
515
+ async execute({ task_id }, _context) {
516
+ const known = gateway.getTask(task_id as TaskId)
517
+ if (!known) {
518
+ return {
519
+ success: false,
520
+ output: `No task ${task_id}. Call agent_task_list to see which tasks exist.`,
521
+ data: { task_id },
522
+ }
523
+ }
524
+
525
+ const completed = await gateway.waitForTask(task_id as TaskId)
526
+ if (_context.abortSignal?.aborted) {
527
+ return {
528
+ success: false,
529
+ output: `This wait was abandoned before task ${task_id} finished; its result will arrive separately as a task notification.`,
530
+ data: { task_id, abandoned: true },
531
+ }
532
+ }
533
+ completionInbox?.claim(task_id as TaskId)
534
+
535
+ const success = completed.state === 'completed'
536
+ const resultText =
537
+ completed.result?.result ??
538
+ completed.result?.lastError ??
539
+ `Task finished with state: ${completed.state}`
540
+ return {
541
+ success,
542
+ output: wrapUntrusted(
543
+ {
544
+ kind: 'agent-result',
545
+ attributes: { agent: completed.agentId, task: completed.taskId },
546
+ provenance: `This is the output of the delegated agent "${completed.agentId}", not this agent's own work.`,
547
+ },
548
+ resultText,
549
+ ),
550
+ data: {
551
+ task_id,
552
+ agent_id: completed.agentId,
553
+ state: completed.state,
554
+ result: resultText,
555
+ },
556
+ }
557
+ },
558
+ })
559
+
375
560
  const cancelTask = defineTool({
376
561
  name: 'cancel_task',
377
562
  description:
@@ -386,6 +571,12 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
386
571
  concurrencySafe: true,
387
572
  async execute({ task_id }) {
388
573
  gateway.cancelTask(task_id as TaskId)
574
+ // Stop holding the run open for it. `expect` put this task on the
575
+ // inbox's outstanding list at launch and only a completion takes it
576
+ // off — so without this a cancelled worker kept `hasPendingWork`
577
+ // true and every attempt to settle paid the full grace period
578
+ // waiting for a result that had just been called off.
579
+ completionInbox?.forget(task_id as TaskId)
389
580
  return {
390
581
  success: true,
391
582
  output: `Task ${task_id} cancelled`,
@@ -397,7 +588,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
397
588
  const agentTaskList = defineTool({
398
589
  name: 'agent_task_list',
399
590
  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.",
591
+ "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
592
  inputSchema: z.object({
402
593
  state: z
403
594
  .enum(['pending', 'running', 'completed', 'failed', 'canceled'])
@@ -415,6 +606,14 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
415
606
  const items = filtered.map((h) => {
416
607
  const runStatus = h.result?.status
417
608
  const lastError = h.result?.lastError ?? undefined
609
+ // The worker's actual output, which this listing used to drop. It
610
+ // read `h.result` for the status and the error and stopped one
611
+ // property short of the thing the task was launched to produce —
612
+ // so a supervisor that knew a task_id and knew it had completed
613
+ // still had no way to read what it said. That is the state a run
614
+ // lands in whenever the launching call was abandoned, which is
615
+ // exactly when this listing gets consulted.
616
+ const output = h.result?.result ?? undefined
418
617
  return {
419
618
  task_id: h.taskId,
420
619
  agent_id: h.agentId,
@@ -424,6 +623,7 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
424
623
  completed_at: h.completedAt ? new Date(h.completedAt).toISOString() : null,
425
624
  duration_ms: h.completedAt ? h.completedAt - h.createdAt : null,
426
625
  last_error: lastError,
626
+ ...(output !== undefined ? { result: output } : {}),
427
627
  }
428
628
  })
429
629
  const summary = {
@@ -434,12 +634,26 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
434
634
  canceled: handles.filter((h) => h.state === 'canceled').length,
435
635
  }
436
636
  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
- )
637
+ ? items.map((i) => {
638
+ const head = `- ${i.task_id} → ${i.agent_id} [${i.state}${i.run_status && i.run_status !== i.state ? ` / ${i.run_status}` : ''}]${
639
+ i.duration_ms !== null ? ` (${Math.round(i.duration_ms / 1000)}s)` : ''
640
+ }${i.last_error ? ` — error: ${i.last_error.slice(0, 200)}` : ''}`
641
+ // The output goes in the rendered TEXT, not only in `data`.
642
+ // Only `output` becomes the tool_result the model reads —
643
+ // the executor never looks at `data` — so a result added
644
+ // to the projection alone would have been added to a field the
645
+ // model cannot see, which is how this listing came to prove a
646
+ // task had finished while withholding what it said.
647
+ if (i.result === undefined) return head
648
+ const body =
649
+ i.result.length > LISTED_RESULT_LIMIT
650
+ ? `${i.result.slice(0, LISTED_RESULT_LIMIT)}\n … truncated; call wait_for_task with "${i.task_id}" for the whole thing.`
651
+ : i.result
652
+ return `${head}\n${body
653
+ .split('\n')
654
+ .map((line) => ` ${line}`)
655
+ .join('\n')}`
656
+ })
443
657
  : ['(no tasks launched yet)']
444
658
  const header = `Tasks: ${summary.total} total — ${summary.running} running, ${summary.completed} completed, ${summary.failed} failed, ${summary.canceled} canceled`
445
659
  return {
@@ -462,12 +676,20 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
462
676
  // non-default gateway (one that keeps the worker process alive
463
677
  // for follow-ups) wants to re-register it.
464
678
  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
679
+ // `cancel_task` is registered again, and the reasoning that dropped it is
680
+ // worth keeping because it was sound at the time and is not any more.
681
+ //
682
+ // It read: per-task cancellation belonged to the old non-blocking worker
683
+ // protocol; since `create_task` blocks and returns the output as its
684
+ // tool_result, every worker is terminal by the time a later turn learns its
685
+ // id, so the tool could only manufacture a "cancelled" for something
686
+ // already finished.
687
+ //
688
+ // That held for exactly as long as blocking was the only way to launch.
689
+ // `background: true` brings back a worker that is running with nothing
690
+ // waiting on it and whose id the supervisor holds while it is still alive —
691
+ // the precondition the old rationale said had disappeared. A launch the
692
+ // model cannot stop is a hole, and this is the tool that closes it.
471
693
  // An empty roster withholds `create_task` rather than mounting an
472
694
  // unsatisfiable one. Mounting it and refusing at parse time reaches the
473
695
  // same verdict, but it reaches it the expensive way: the model is shown a
@@ -485,8 +707,16 @@ export function buildCoordinatorTools(opts: CoordinatorToolsOptions): ToolDefini
485
707
  // and a human channel" stays a supported configuration, which is why this
486
708
  // omits rather than refusing to build: a caller asking this builder for
487
709
  // `ask_user_question` with no roster is doing something legitimate.
710
+ // `wait_for_task` and `cancel_task` ride with `create_task` because they
711
+ // are only meaningful once something has been launched.
712
+ //
713
+ // Waiting had no tool at all. `continue_task` blocks, but only as a side
714
+ // effect of sending a message, so a supervisor that merely wanted to wait
715
+ // had to invent something to say — and one that would not do that was left
716
+ // calling `agent_task_list` in a sleep loop. That was never the model
717
+ // misbehaving; it was the only move available.
488
718
  const tools: ToolDefinition[] =
489
- agentIds.length > 0 ? [createTask, agentTaskList] : [agentTaskList]
719
+ agentIds.length > 0 ? [createTask, waitForTaskTool, cancelTask, agentTaskList] : [agentTaskList]
490
720
 
491
721
  if (getPlanManager) {
492
722
  const approvePlan = defineTool({
@@ -10,20 +10,36 @@ export type ProviderErrorKind =
10
10
  /**
11
11
  * Serializable provider-failure metadata carried by failed runs and events.
12
12
  *
13
- * No response body, vendor message, URL, or `cause` belongs here.
13
+ * No response body, URL, or `cause` belongs here. `detail` is the one thing
14
+ * the provider itself said, and it arrives scrubbed — see below.
14
15
  */
15
16
  export interface ProviderErrorInfo {
16
17
  readonly kind: ProviderErrorKind
17
18
  readonly providerId: string
18
19
  readonly status?: number
19
20
  readonly retryAfterMs?: number
21
+ /**
22
+ * What the provider said was wrong, truncated and scrubbed of anything
23
+ * credential-shaped.
24
+ *
25
+ * Carried here and not only on the error's `message` for the same reason
26
+ * `kind` is: a host rendering a failure should not have to parse a
27
+ * sentence to show one. It is the field that names the offending
28
+ * parameter, which is usually the whole diagnosis.
29
+ */
30
+ readonly detail?: string
20
31
  }
21
32
 
22
33
  export interface ProviderRequestErrorInit extends ProviderErrorInfo {
23
34
  /**
24
- * Optional extra clause for the message. MUST be text this codebase
25
- * authored — never a fragment of a response body, a header, or a vendor
26
- * error.
35
+ * Optional extra clause for the message.
36
+ *
37
+ * This used to be required to be text the codebase authored, never a
38
+ * fragment of a vendor error — and the constructor did not read the field
39
+ * at all, so nothing carried either kind of text. Providers may now pass
40
+ * their own complaint through `vendorDetail`, which truncates it and
41
+ * replaces anything credential-shaped. Text this codebase authored is
42
+ * still welcome; what is not welcome is a raw body passed straight in.
27
43
  */
28
44
  readonly detail?: string
29
45
  }
@@ -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