@namzu/sdk 7.0.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/CHANGELOG.md +652 -0
  2. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  3. package/dist/agents/SupervisorAgent.js +186 -163
  4. package/dist/agents/SupervisorAgent.js.map +1 -1
  5. package/dist/agents/__tests__/a-policy-nobody-can-select.test.d.ts +2 -0
  6. package/dist/agents/__tests__/a-policy-nobody-can-select.test.d.ts.map +1 -0
  7. package/dist/agents/__tests__/a-policy-nobody-can-select.test.js +177 -0
  8. package/dist/agents/__tests__/a-policy-nobody-can-select.test.js.map +1 -0
  9. package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts +2 -0
  10. package/dist/agents/__tests__/supervisor-inbox-scope.test.d.ts.map +1 -0
  11. package/dist/agents/__tests__/supervisor-inbox-scope.test.js +125 -0
  12. package/dist/agents/__tests__/supervisor-inbox-scope.test.js.map +1 -0
  13. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  14. package/dist/bridge/a2a/mapper.js +4 -0
  15. package/dist/bridge/a2a/mapper.js.map +1 -1
  16. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  17. package/dist/bridge/sse/mapper.js +24 -0
  18. package/dist/bridge/sse/mapper.js.map +1 -1
  19. package/dist/contracts/api.d.ts +1 -1
  20. package/dist/contracts/api.d.ts.map +1 -1
  21. package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.d.ts +2 -0
  22. package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.d.ts.map +1 -0
  23. package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.js +114 -0
  24. package/dist/gateway/__tests__/a-knob-nobody-can-turn.test.js.map +1 -0
  25. package/dist/gateway/__tests__/completion-inbox.test.js +292 -2
  26. package/dist/gateway/__tests__/completion-inbox.test.js.map +1 -1
  27. package/dist/gateway/completion-inbox.d.ts +94 -6
  28. package/dist/gateway/completion-inbox.d.ts.map +1 -1
  29. package/dist/gateway/completion-inbox.js +235 -15
  30. package/dist/gateway/completion-inbox.js.map +1 -1
  31. package/dist/gateway/local.d.ts.map +1 -1
  32. package/dist/gateway/local.js +19 -6
  33. package/dist/gateway/local.js.map +1 -1
  34. package/dist/manager/agent/__tests__/lifecycle.test.js +56 -0
  35. package/dist/manager/agent/__tests__/lifecycle.test.js.map +1 -1
  36. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  37. package/dist/manager/agent/lifecycle.js +62 -33
  38. package/dist/manager/agent/lifecycle.js.map +1 -1
  39. package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.d.ts +2 -0
  40. package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.d.ts.map +1 -0
  41. package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.js +73 -0
  42. package/dist/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.js.map +1 -0
  43. package/dist/manager/plan/lifecycle.d.ts +48 -0
  44. package/dist/manager/plan/lifecycle.d.ts.map +1 -1
  45. package/dist/manager/plan/lifecycle.js +56 -0
  46. package/dist/manager/plan/lifecycle.js.map +1 -1
  47. package/dist/manager/run/persistence.d.ts +8 -0
  48. package/dist/manager/run/persistence.d.ts.map +1 -1
  49. package/dist/manager/run/persistence.js +12 -0
  50. package/dist/manager/run/persistence.js.map +1 -1
  51. package/dist/run/reporter.d.ts.map +1 -1
  52. package/dist/run/reporter.js +11 -0
  53. package/dist/run/reporter.js.map +1 -1
  54. package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts +2 -0
  55. package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.d.ts.map +1 -0
  56. package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js +142 -0
  57. package/dist/runtime/query/__tests__/completion-does-not-erase-the-answer.test.js.map +1 -0
  58. package/dist/runtime/query/__tests__/completion-notification.test.js +414 -32
  59. package/dist/runtime/query/__tests__/completion-notification.test.js.map +1 -1
  60. package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts +2 -0
  61. package/dist/runtime/query/__tests__/context-size-on-the-wire.test.d.ts.map +1 -0
  62. package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js +100 -0
  63. package/dist/runtime/query/__tests__/context-size-on-the-wire.test.js.map +1 -0
  64. package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.d.ts +2 -0
  65. package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.d.ts.map +1 -0
  66. package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.js +62 -0
  67. package/dist/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.js.map +1 -0
  68. package/dist/runtime/query/events.d.ts.map +1 -1
  69. package/dist/runtime/query/events.js +5 -0
  70. package/dist/runtime/query/events.js.map +1 -1
  71. package/dist/runtime/query/guard.d.ts +28 -0
  72. package/dist/runtime/query/guard.d.ts.map +1 -1
  73. package/dist/runtime/query/guard.js +31 -0
  74. package/dist/runtime/query/guard.js.map +1 -1
  75. package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts +2 -0
  76. package/dist/runtime/query/iteration/__tests__/settle-grace.test.d.ts.map +1 -0
  77. package/dist/runtime/query/iteration/__tests__/settle-grace.test.js +226 -0
  78. package/dist/runtime/query/iteration/__tests__/settle-grace.test.js.map +1 -0
  79. package/dist/runtime/query/iteration/index.d.ts +92 -0
  80. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  81. package/dist/runtime/query/iteration/index.js +814 -566
  82. package/dist/runtime/query/iteration/index.js.map +1 -1
  83. package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts +2 -0
  84. package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.d.ts.map +1 -0
  85. package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js +95 -0
  86. package/dist/runtime/query/iteration/phases/__tests__/compaction-declined.test.js.map +1 -0
  87. package/dist/runtime/query/iteration/phases/compaction.d.ts +34 -0
  88. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  89. package/dist/runtime/query/iteration/phases/compaction.js +61 -4
  90. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  91. package/dist/store/task/disk.d.ts.map +1 -1
  92. package/dist/store/task/disk.js +14 -0
  93. package/dist/store/task/disk.js.map +1 -1
  94. package/dist/store/task/memory.d.ts.map +1 -1
  95. package/dist/store/task/memory.js +14 -0
  96. package/dist/store/task/memory.js.map +1 -1
  97. package/dist/tools/__tests__/untrusted-envelope.test.js +16 -0
  98. package/dist/tools/__tests__/untrusted-envelope.test.js.map +1 -1
  99. package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.d.ts +2 -0
  100. package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.d.ts.map +1 -0
  101. package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.js +101 -0
  102. package/dist/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.js.map +1 -0
  103. package/dist/tools/coordinator/__tests__/completion-delivery.test.js +117 -0
  104. package/dist/tools/coordinator/__tests__/completion-delivery.test.js.map +1 -1
  105. package/dist/tools/coordinator/__tests__/task-list.test.js +57 -0
  106. package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -1
  107. package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.d.ts +2 -0
  108. package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.d.ts.map +1 -0
  109. package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.js +99 -0
  110. package/dist/tools/coordinator/__tests__/the-plan-a-human-approves.test.js.map +1 -0
  111. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  112. package/dist/tools/coordinator/agent.js +17 -23
  113. package/dist/tools/coordinator/agent.js.map +1 -1
  114. package/dist/tools/coordinator/index.d.ts.map +1 -1
  115. package/dist/tools/coordinator/index.js +142 -14
  116. package/dist/tools/coordinator/index.js.map +1 -1
  117. package/dist/tools/coordinator/outcome.d.ts +60 -0
  118. package/dist/tools/coordinator/outcome.d.ts.map +1 -0
  119. package/dist/tools/coordinator/outcome.js +68 -0
  120. package/dist/tools/coordinator/outcome.js.map +1 -0
  121. package/dist/tools/untrusted-envelope.d.ts.map +1 -1
  122. package/dist/tools/untrusted-envelope.js +9 -1
  123. package/dist/tools/untrusted-envelope.js.map +1 -1
  124. package/dist/types/agent/gateway.d.ts +37 -2
  125. package/dist/types/agent/gateway.d.ts.map +1 -1
  126. package/dist/types/agent/supervisor.d.ts +38 -1
  127. package/dist/types/agent/supervisor.d.ts.map +1 -1
  128. package/dist/types/plan/index.d.ts +26 -0
  129. package/dist/types/plan/index.d.ts.map +1 -1
  130. package/dist/types/provider/interface.d.ts +35 -0
  131. package/dist/types/provider/interface.d.ts.map +1 -1
  132. package/dist/types/run/entity.d.ts +16 -0
  133. package/dist/types/run/entity.d.ts.map +1 -1
  134. package/dist/types/run/events.d.ts +92 -0
  135. package/dist/types/run/events.d.ts.map +1 -1
  136. package/dist/types/run/events.js.map +1 -1
  137. package/dist/types/run/prepare-step.d.ts +17 -2
  138. package/dist/types/run/prepare-step.d.ts.map +1 -1
  139. package/dist/types/task/index.d.ts +19 -1
  140. package/dist/types/task/index.d.ts.map +1 -1
  141. package/dist/types/task/index.js +10 -1
  142. package/dist/types/task/index.js.map +1 -1
  143. package/dist/types/verification/index.d.ts +98 -0
  144. package/dist/types/verification/index.d.ts.map +1 -1
  145. package/dist/types/verification/index.js +10 -0
  146. package/dist/types/verification/index.js.map +1 -1
  147. package/dist/verification/__tests__/argument-pattern.test.d.ts +2 -0
  148. package/dist/verification/__tests__/argument-pattern.test.d.ts.map +1 -0
  149. package/dist/verification/__tests__/argument-pattern.test.js +122 -0
  150. package/dist/verification/__tests__/argument-pattern.test.js.map +1 -0
  151. package/dist/verification/gate.d.ts.map +1 -1
  152. package/dist/verification/gate.js +44 -0
  153. package/dist/verification/gate.js.map +1 -1
  154. package/dist/verification/rules.d.ts.map +1 -1
  155. package/dist/verification/rules.js +27 -0
  156. package/dist/verification/rules.js.map +1 -1
  157. package/package.json +1 -1
  158. package/src/agents/SupervisorAgent.ts +189 -167
  159. package/src/agents/__tests__/a-policy-nobody-can-select.test.ts +202 -0
  160. package/src/agents/__tests__/supervisor-inbox-scope.test.ts +149 -0
  161. package/src/bridge/a2a/mapper.ts +4 -0
  162. package/src/bridge/sse/mapper.ts +25 -0
  163. package/src/contracts/api.ts +1 -0
  164. package/src/gateway/__tests__/a-knob-nobody-can-turn.test.ts +139 -0
  165. package/src/gateway/__tests__/completion-inbox.test.ts +348 -2
  166. package/src/gateway/completion-inbox.ts +248 -16
  167. package/src/gateway/local.ts +19 -7
  168. package/src/manager/agent/__tests__/lifecycle.test.ts +74 -0
  169. package/src/manager/agent/lifecycle.ts +72 -42
  170. package/src/manager/plan/__tests__/a-plan-that-succeeded-is-not-failed.test.ts +88 -0
  171. package/src/manager/plan/lifecycle.ts +61 -0
  172. package/src/manager/run/persistence.ts +12 -0
  173. package/src/run/reporter.ts +12 -0
  174. package/src/runtime/query/__tests__/completion-does-not-erase-the-answer.test.ts +163 -0
  175. package/src/runtime/query/__tests__/completion-notification.test.ts +486 -34
  176. package/src/runtime/query/__tests__/context-size-on-the-wire.test.ts +122 -0
  177. package/src/runtime/query/__tests__/the-plan-graph-reaches-a-host.test.ts +79 -0
  178. package/src/runtime/query/events.ts +5 -0
  179. package/src/runtime/query/guard.ts +32 -0
  180. package/src/runtime/query/iteration/__tests__/settle-grace.test.ts +265 -0
  181. package/src/runtime/query/iteration/index.ts +902 -636
  182. package/src/runtime/query/iteration/phases/__tests__/compaction-declined.test.ts +124 -0
  183. package/src/runtime/query/iteration/phases/compaction.ts +83 -10
  184. package/src/store/task/disk.ts +15 -0
  185. package/src/store/task/memory.ts +15 -0
  186. package/src/tools/__tests__/untrusted-envelope.test.ts +23 -0
  187. package/src/tools/coordinator/__tests__/a-failed-worker-is-not-an-answer.test.ts +133 -0
  188. package/src/tools/coordinator/__tests__/completion-delivery.test.ts +147 -0
  189. package/src/tools/coordinator/__tests__/task-list.test.ts +72 -0
  190. package/src/tools/coordinator/__tests__/the-plan-a-human-approves.test.ts +121 -0
  191. package/src/tools/coordinator/agent.ts +17 -25
  192. package/src/tools/coordinator/index.ts +155 -18
  193. package/src/tools/coordinator/outcome.ts +70 -0
  194. package/src/tools/untrusted-envelope.ts +9 -1
  195. package/src/types/agent/gateway.ts +37 -2
  196. package/src/types/agent/supervisor.ts +40 -1
  197. package/src/types/plan/index.ts +29 -0
  198. package/src/types/provider/interface.ts +39 -0
  199. package/src/types/run/entity.ts +17 -0
  200. package/src/types/run/events.ts +92 -0
  201. package/src/types/run/prepare-step.ts +17 -2
  202. package/src/types/task/index.ts +22 -3
  203. package/src/types/verification/index.ts +61 -0
  204. package/src/verification/__tests__/argument-pattern.test.ts +158 -0
  205. package/src/verification/gate.ts +45 -0
  206. package/src/verification/rules.ts +28 -0
@@ -159,7 +159,14 @@ export class SupervisorAgent extends AbstractAgent<SupervisorAgentConfig, Superv
159
159
  projectId,
160
160
  parentActor,
161
161
  }
162
- gateway = new LocalTaskGateway(config.agentManager, taskContext, listener, input)
162
+ // The only hop between the config and the gateway's policy. Omit it
163
+ // and the field is settable, documented, and read by nothing —
164
+ // which is exactly the state it was in before.
165
+ gateway = new LocalTaskGateway(config.agentManager, taskContext, listener, input, {
166
+ ...(config.siblingFailurePolicy
167
+ ? { siblingFailurePolicy: config.siblingFailurePolicy }
168
+ : {}),
169
+ })
163
170
  } else {
164
171
  throw new Error("SupervisorAgentConfig requires either 'gateway' or 'agentManager'")
165
172
  }
@@ -185,181 +192,196 @@ export class SupervisorAgent extends AbstractAgent<SupervisorAgentConfig, Superv
185
192
  const completionInbox = new CompletionInbox()
186
193
  completionInbox.attach(gateway)
187
194
 
188
- const coordinatorToolDefs = buildCoordinatorTools({
189
- gateway,
190
- completionInbox,
191
- workingDirectory: input.workingDirectory,
192
- runtimeContext: input.runtimeContext,
193
- allowedAgentIds: config.agentIds,
194
- // The only hop between the config and the decision. Omit it and
195
- // everything still compiles: the field is settable, documented, and
196
- // read by nothing — which is the shape of a declaration this repo
197
- // has had to go and delete before.
198
- allowDelegation: config.allowDelegation,
199
- taskStore: input.taskStore,
200
- runId,
201
- getPlanManager: () => planManagerRef,
202
- onTaskLaunched: (agentTaskId, meta) => {
203
- launchedTasks.set(agentTaskId, meta)
204
- },
205
- // With a resume handler present the coordinator surface gains
206
- // ask_user_question — the model can park the run on a question
207
- // routed through the same HITL channel as plan approvals.
208
- ...(config.resumeHandler ? { resumeHandler: config.resumeHandler } : {}),
209
- questionParks,
210
- pendingAnswers,
211
- })
195
+ // From here to the return in a try/finally, so the listener is released
196
+ // on every way out. The registration loop below can throw
197
+ // ToolNameCollisionError, and a host that hits that fixes its config and
198
+ // runs again — which is how a leak of one listener per run becomes a leak
199
+ // of one per ATTEMPT.
200
+ try {
201
+ const coordinatorToolDefs = buildCoordinatorTools({
202
+ gateway,
203
+ completionInbox,
204
+ workingDirectory: input.workingDirectory,
205
+ runtimeContext: input.runtimeContext,
206
+ allowedAgentIds: config.agentIds,
207
+ // The only hop between the config and the decision. Omit it and
208
+ // everything still compiles: the field is settable, documented, and
209
+ // read by nothing — which is the shape of a declaration this repo
210
+ // has had to go and delete before.
211
+ allowDelegation: config.allowDelegation,
212
+ taskStore: input.taskStore,
213
+ runId,
214
+ getPlanManager: () => planManagerRef,
215
+ onTaskLaunched: (agentTaskId, meta) => {
216
+ launchedTasks.set(agentTaskId, meta)
217
+ },
218
+ // With a resume handler present the coordinator surface gains
219
+ // ask_user_question — the model can park the run on a question
220
+ // routed through the same HITL channel as plan approvals.
221
+ ...(config.resumeHandler ? { resumeHandler: config.resumeHandler } : {}),
222
+ questionParks,
223
+ pendingAnswers,
224
+ })
212
225
 
213
- const tools = new ToolRegistry()
214
- if (config.tools) {
215
- for (const tool of config.tools.getAll()) {
216
- tools.register(tool, config.tools.getAvailability(tool.name))
226
+ const tools = new ToolRegistry()
227
+ if (config.tools) {
228
+ for (const tool of config.tools.getAll()) {
229
+ tools.register(tool, config.tools.getAvailability(tool.name))
230
+ }
217
231
  }
218
- }
219
- // Registered the way every other kernel-mounted tool in this SDK is
220
- // registered: honouring `runtimeToolOverrides`, and refusing to take a
221
- // name the host already used.
222
- //
223
- // Both halves were missing here and nowhere else. `runtimeToolOverrides`
224
- // is declared on `AgentInput`, is forwarded into this very `drainQuery`
225
- // call below, and is consulted for the task tools and for the advisory
226
- // tools but the coordinator tools were registered before that and
227
- // unconditionally, so `{ create_task: 'disabled' }` was honoured
228
- // everywhere except the one surface a host would most want to decline.
229
- // A run that must not delegate had prompt text and a gateway refusal as
230
- // its only defences.
231
- //
232
- // Collision REFUSES rather than overwrites, and the principle is
233
- // complete mediation rather than fail-safe defaults: "proposals to gain
234
- // performance by remembering the result of an authority check [must] be
235
- // examined skeptically. If a change in authority occurs, such remembered
236
- // results must be systematically updated" (Saltzer & Schroeder 1975,
237
- // §I.A.3(c)). A registry entry is a remembered binding of a name to an
238
- // authority, and a later write that rebinds the name leaves every
239
- // decision made about the old binding stale.
240
- //
241
- // The counter-argument is that today the host's tool merely loses
242
- // quietly and the run still works, so six reserved names is a real cost
243
- // on a name a consumer may have chosen long ago. It does not hold,
244
- // because "loses quietly" is not what happens. `registerOne` ends with
245
- // `availability.set(id, state)` and this call passes no state, so a tool
246
- // the host registered `deferred` or `suspended` is silently PROMOTED to
247
- // active under someone else's implementation; and because the store is a
248
- // Map, the replacement inherits the host's insertion position in the
249
- // prompt-cache prefix. That is a different authorization surface, not a
250
- // lost registration. CWE-390 is the shape `ManagedRegistry` has here
251
- // detection of an error condition without action and CWE-694's own
252
- // mitigation is nearly this fix: do not operate any resource with a
253
- // non-unique identifier, and report the error.
254
- //
255
- // Refusing is also what the peer set does. One runtime's registry
256
- // primitive throws on both duplicate and reserved names; another refuses
257
- // its injected delegation name in a pre-flight that tells the author to
258
- // rename. Closer to home, `ProviderRegistry.register` already throws
259
- // unless the caller passes `{ replace: true }` declared intent is what
260
- // separates a legitimate replacement from an accidental one, and no such
261
- // intent is expressible here.
262
- const overrides = input.runtimeToolOverrides
263
- for (const tool of coordinatorToolDefs) {
264
- const override = overrides?.[tool.name]
265
- if (override === 'disabled') continue
266
- if (config.tools?.has(tool.name)) {
267
- throw new ToolNameCollisionError(tool.name, 'the supervisor coordinator surface')
232
+ // Registered the way every other kernel-mounted tool in this SDK is
233
+ // registered: honouring `runtimeToolOverrides`, and refusing to take a
234
+ // name the host already used.
235
+ //
236
+ // Both halves were missing here and nowhere else. `runtimeToolOverrides`
237
+ // is declared on `AgentInput`, is forwarded into this very `drainQuery`
238
+ // call below, and is consulted for the task tools and for the advisory
239
+ // tools but the coordinator tools were registered before that and
240
+ // unconditionally, so `{ create_task: 'disabled' }` was honoured
241
+ // everywhere except the one surface a host would most want to decline.
242
+ // A run that must not delegate had prompt text and a gateway refusal as
243
+ // its only defences.
244
+ //
245
+ // Collision REFUSES rather than overwrites, and the principle is
246
+ // complete mediation rather than fail-safe defaults: "proposals to gain
247
+ // performance by remembering the result of an authority check [must] be
248
+ // examined skeptically. If a change in authority occurs, such remembered
249
+ // results must be systematically updated" (Saltzer & Schroeder 1975,
250
+ // §I.A.3(c)). A registry entry is a remembered binding of a name to an
251
+ // authority, and a later write that rebinds the name leaves every
252
+ // decision made about the old binding stale.
253
+ //
254
+ // The counter-argument is that today the host's tool merely loses
255
+ // quietly and the run still works, so six reserved names is a real cost
256
+ // on a name a consumer may have chosen long ago. It does not hold,
257
+ // because "loses quietly" is not what happens. `registerOne` ends with
258
+ // `availability.set(id, state)` and this call passes no state, so a tool
259
+ // the host registered `deferred` or `suspended` is silently PROMOTED to
260
+ // active under someone else's implementation; and because the store is a
261
+ // Map, the replacement inherits the host's insertion position in the
262
+ // prompt-cache prefix. That is a different authorization surface, not a
263
+ // lost registration. CWE-390 is the shape `ManagedRegistry` has here
264
+ // detection of an error condition without action and CWE-694's own
265
+ // mitigation is nearly this fix: do not operate any resource with a
266
+ // non-unique identifier, and report the error.
267
+ //
268
+ // Refusing is also what the peer set does. One runtime's registry
269
+ // primitive throws on both duplicate and reserved names; another refuses
270
+ // its injected delegation name in a pre-flight that tells the author to
271
+ // rename. Closer to home, `ProviderRegistry.register` already throws
272
+ // unless the caller passes `{ replace: true }` declared intent is what
273
+ // separates a legitimate replacement from an accidental one, and no such
274
+ // intent is expressible here.
275
+ const overrides = input.runtimeToolOverrides
276
+ for (const tool of coordinatorToolDefs) {
277
+ const override = overrides?.[tool.name]
278
+ if (override === 'disabled') continue
279
+ if (config.tools?.has(tool.name)) {
280
+ throw new ToolNameCollisionError(tool.name, 'the supervisor coordinator surface')
281
+ }
282
+ tools.register(tool, override ?? 'active')
268
283
  }
269
- tools.register(tool, override ?? 'active')
270
- }
271
284
 
272
- const childInvocationState = deriveChildState(
273
- config.invocationState ?? { tenantId },
274
- this.metadata.id,
275
- )
285
+ const childInvocationState = deriveChildState(
286
+ config.invocationState ?? { tenantId },
287
+ this.metadata.id,
288
+ )
276
289
 
277
- const run = await drainQuery(
278
- {
279
- systemPrompt: config.systemPrompt,
280
- skills: config.skills,
281
- provider: config.provider,
282
- tools,
283
- runConfig: {
284
- model: config.model,
285
- tokenBudget: config.tokenBudget,
286
- timeoutMs: config.timeoutMs,
287
- maxIterations: config.maxIterations,
288
- temperature: config.temperature,
289
- env: config.env,
290
- // See ReactiveAgent: a hand-listed literal drops what nobody
291
- // remembered to add, and reports nothing when it does.
292
- ...(config.thinking ? { thinking: config.thinking } : {}),
293
- ...(config.effort ? { effort: config.effort } : {}),
294
- },
295
- questionParks,
296
- pendingAnswers,
297
- agentId: this.metadata.id,
298
- agentName: this.metadata.name,
299
- workingDirectory: input.workingDirectory,
300
- messages: input.messages,
301
- signal: input.signal,
302
- sessionId,
303
- threadId,
304
- projectId,
305
- tenantId,
306
- runId,
307
- parentRunId: config.parentRunId,
308
- depth: config.depth,
309
- contextLevel: 'full',
310
- onContextCreated: ({ planManager }) => {
311
- planManagerRef = planManager
290
+ const run = await drainQuery(
291
+ {
292
+ systemPrompt: config.systemPrompt,
293
+ skills: config.skills,
294
+ provider: config.provider,
295
+ tools,
296
+ runConfig: {
297
+ model: config.model,
298
+ tokenBudget: config.tokenBudget,
299
+ timeoutMs: config.timeoutMs,
300
+ maxIterations: config.maxIterations,
301
+ temperature: config.temperature,
302
+ env: config.env,
303
+ // See ReactiveAgent: a hand-listed literal drops what nobody
304
+ // remembered to add, and reports nothing when it does.
305
+ ...(config.thinking ? { thinking: config.thinking } : {}),
306
+ ...(config.effort ? { effort: config.effort } : {}),
307
+ },
308
+ questionParks,
309
+ pendingAnswers,
310
+ // How wide a fan-out actually runs. Absent leaves the kernel
311
+ // default — the same forwarding ReactiveAgent has always done,
312
+ // missing from the one agent whose job is delegation.
313
+ ...(config.maxToolConcurrency !== undefined
314
+ ? { maxToolConcurrency: config.maxToolConcurrency }
315
+ : {}),
316
+ agentId: this.metadata.id,
317
+ agentName: this.metadata.name,
318
+ workingDirectory: input.workingDirectory,
319
+ messages: input.messages,
320
+ signal: input.signal,
321
+ sessionId,
322
+ threadId,
323
+ projectId,
324
+ tenantId,
325
+ runId,
326
+ parentRunId: config.parentRunId,
327
+ depth: config.depth,
328
+ contextLevel: 'full',
329
+ onContextCreated: ({ planManager }) => {
330
+ planManagerRef = planManager
331
+ },
332
+ taskStore: input.taskStore,
333
+ runtimeToolOverrides: input.runtimeToolOverrides,
334
+ runtimeContext: input.runtimeContext,
335
+ taskGateway: gateway,
336
+ completionInbox,
337
+ launchedTasks,
338
+ advisory: config.advisory,
339
+ invocationState: childInvocationState,
340
+ // HITL surface: forward optional review-time hooks so hosts can
341
+ // run "Ask before acting" supervisors instead of the default
342
+ // auto-approve. drainQuery falls back to autoApproveHandler
343
+ // when resumeHandler is omitted (= same behaviour as before).
344
+ ...(config.resumeHandler ? { resumeHandler: config.resumeHandler } : {}),
345
+ // Forwarded for the same reason the handler is. A capability the
346
+ // kernel honours in `drainQuery` but that never reaches the
347
+ // surface a host actually constructs is a capability nobody can
348
+ // use — which is the shape of defect this file has already been
349
+ // corrected for twice.
350
+ ...(config.steering ? { steering: config.steering } : {}),
351
+ ...(config.verificationGate ? { verificationGate: config.verificationGate } : {}),
352
+ ...(config.sandboxProvider ? { sandboxProvider: config.sandboxProvider } : {}),
353
+ // Working-memory / compaction seam (optional; absent => unchanged
354
+ // run path, byte-identical for every existing consumer).
355
+ ...(config.compactionConfig ? { compactionConfig: config.compactionConfig } : {}),
356
+ ...(config.workingMemoryProvider
357
+ ? { workingMemoryProvider: config.workingMemoryProvider }
358
+ : {}),
312
359
  },
313
- taskStore: input.taskStore,
314
- runtimeToolOverrides: input.runtimeToolOverrides,
315
- runtimeContext: input.runtimeContext,
316
- taskGateway: gateway,
317
- completionInbox,
318
- launchedTasks,
319
- advisory: config.advisory,
320
- invocationState: childInvocationState,
321
- // HITL surface: forward optional review-time hooks so hosts can
322
- // run "Ask before acting" supervisors instead of the default
323
- // auto-approve. drainQuery falls back to autoApproveHandler
324
- // when resumeHandler is omitted (= same behaviour as before).
325
- ...(config.resumeHandler ? { resumeHandler: config.resumeHandler } : {}),
326
- // Forwarded for the same reason the handler is. A capability the
327
- // kernel honours in `drainQuery` but that never reaches the
328
- // surface a host actually constructs is a capability nobody can
329
- // use — which is the shape of defect this file has already been
330
- // corrected for twice.
331
- ...(config.steering ? { steering: config.steering } : {}),
332
- ...(config.verificationGate ? { verificationGate: config.verificationGate } : {}),
333
- ...(config.sandboxProvider ? { sandboxProvider: config.sandboxProvider } : {}),
334
- // Working-memory / compaction seam (optional; absent => unchanged
335
- // run path, byte-identical for every existing consumer).
336
- ...(config.compactionConfig ? { compactionConfig: config.compactionConfig } : {}),
337
- ...(config.workingMemoryProvider
338
- ? { workingMemoryProvider: config.workingMemoryProvider }
339
- : {}),
340
- },
341
- listener,
342
- )
360
+ listener,
361
+ )
343
362
 
344
- const taskHandles = gateway.listTasks()
345
- const taskResults = synthesizeTaskResults(taskHandles, runId)
363
+ const taskHandles = gateway.listTasks()
364
+ const taskResults = synthesizeTaskResults(taskHandles, runId)
346
365
 
347
- const completedTasks = countCompletedTasks(taskResults)
366
+ const completedTasks = countCompletedTasks(taskResults)
348
367
 
349
- return {
350
- runId: run.id,
351
- status: run.status === 'completed' ? 'completed' : 'failed',
352
- stopReason: run.stopReason,
353
- usage: run.tokenUsage,
354
- cost: run.costInfo,
355
- iterations: run.currentIteration,
356
- durationMs: Date.now() - startTime,
357
- messages: run.messages,
358
- result: run.result,
359
- lastError: run.lastError,
360
- taskResults,
361
- completedTasks,
362
- totalTasks: taskResults.length,
368
+ return {
369
+ runId: run.id,
370
+ status: run.status === 'completed' ? 'completed' : 'failed',
371
+ stopReason: run.stopReason,
372
+ usage: run.tokenUsage,
373
+ cost: run.costInfo,
374
+ iterations: run.currentIteration,
375
+ durationMs: Date.now() - startTime,
376
+ messages: run.messages,
377
+ result: run.result,
378
+ lastError: run.lastError,
379
+ taskResults,
380
+ completedTasks,
381
+ totalTasks: taskResults.length,
382
+ }
383
+ } finally {
384
+ completionInbox.close()
363
385
  }
364
386
  }
365
387
  }
@@ -0,0 +1,202 @@
1
+ import { mkdtemp } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { describe, expect, it } from 'vitest'
5
+
6
+ import { MockLLMProvider, registerMock } from '../../provider/index.js'
7
+ import { ToolRegistry } from '../../registry/index.js'
8
+ import type { Agent } from '../../types/agent/core.js'
9
+ import type { SiblingFailurePolicy } from '../../types/agent/gateway.js'
10
+ import type { AgentManagerContract } from '../../types/agent/manager.js'
11
+ import type {
12
+ AgentTask,
13
+ AgentTaskContext,
14
+ AgentTaskState,
15
+ SendMessageOptions,
16
+ } from '../../types/agent/task.js'
17
+ import type { RunId, TaskId } from '../../types/ids/index.js'
18
+ import { SupervisorAgent } from '../SupervisorAgent.js'
19
+
20
+ /**
21
+ * `'cancel-siblings'` was unreachable from every entry point.
22
+ *
23
+ * `LocalTaskGateway` has honoured the policy since it was written and the
24
+ * cancellation machinery behind it is complete — but the policy was the fifth
25
+ * constructor argument of a gateway the supervisor builds itself, and the
26
+ * supervisor passed four. So every host in existence ran `'continue'`, and the
27
+ * only way to reach the other value was to construct the gateway by hand and
28
+ * hand it in through `config.gateway`. A policy nobody can select is not a
29
+ * policy, and the tests it had all constructed the gateway directly, so they
30
+ * passed while nothing upstream could turn it on.
31
+ *
32
+ * The assertion is deliberately the cancellation itself. Asserting that the run
33
+ * completed would pass with the forwarding deleted — a fan-out under
34
+ * `'continue'` completes too — and prove nothing.
35
+ */
36
+
37
+ registerMock()
38
+
39
+ const FAILING = 'doomed' as const
40
+ const SLOW = 'patient' as const
41
+
42
+ /**
43
+ * A manager whose slow child only settles when someone cancels it — which is
44
+ * what an abort actually does, and what lets this run terminate at all.
45
+ */
46
+ class FanOutManager implements AgentManagerContract {
47
+ readonly cancelled: TaskId[] = []
48
+ private readonly tasks = new Map<TaskId, AgentTask>()
49
+ private releaseSlow?: () => void
50
+ private readonly slowSettled = new Promise<void>((resolve) => {
51
+ this.releaseSlow = resolve
52
+ })
53
+ private markLaunched?: () => void
54
+ /**
55
+ * The doomed child does not settle until BOTH have launched.
56
+ *
57
+ * Without this the test is a race it usually wins: the sibling policy only
58
+ * cancels tasks the gateway is already tracking, so if the failure lands
59
+ * before the slow child finishes registering, nothing is cancelled, nothing
60
+ * releases the slow child, and the run hangs until the delegation timeout.
61
+ * That happened once here. A fan-out where one leg dies while another is
62
+ * genuinely in flight is the scenario being tested, so the harness states
63
+ * it instead of hoping for it.
64
+ */
65
+ private readonly bothLaunched = new Promise<void>((resolve) => {
66
+ this.markLaunched = resolve
67
+ })
68
+
69
+ async sendMessage(options: SendMessageOptions): Promise<AgentTask> {
70
+ const failing = options.agentId === FAILING
71
+ const taskId = (failing ? 'task_doomed' : 'task_patient') as TaskId
72
+ const task = {
73
+ taskId,
74
+ agentId: options.agentId,
75
+ agent: {} as Agent<never, never>,
76
+ childAbortController: new AbortController(),
77
+ context: {} as AgentTaskContext,
78
+ // The failing child is `completed` at the gateway layer and
79
+ // `failed` in its own result — the two-authority split that makes
80
+ // this worth checking at all.
81
+ state: (failing ? 'completed' : 'running') as AgentTaskState,
82
+ pendingMessages: [],
83
+ createdAt: 1,
84
+ ...(failing
85
+ ? { result: { runId: 'run_child' as RunId, status: 'failed', result: 'it broke' } }
86
+ : {}),
87
+ } as AgentTask
88
+
89
+ this.tasks.set(taskId, task)
90
+ if (this.tasks.size === 2) this.markLaunched?.()
91
+ return task
92
+ }
93
+
94
+ cancel(taskId: TaskId): void {
95
+ this.cancelled.push(taskId)
96
+ const task = this.tasks.get(taskId)
97
+ if (task) task.state = 'failed' as AgentTaskState
98
+ this.releaseSlow?.()
99
+ }
100
+
101
+ async waitForCompletion(taskId: TaskId): Promise<void> {
102
+ if (taskId === ('task_patient' as TaskId)) await this.slowSettled
103
+ // The doomed child fails only once its sibling is actually in flight —
104
+ // see `bothLaunched`. Registration happens inside `createTask` right
105
+ // after `sendMessage` returns, so waiting a further microtask turn is
106
+ // what puts this after it rather than racing it.
107
+ else {
108
+ await this.bothLaunched.then(() => undefined)
109
+ // Safety valve, and it is what makes a REGRESSION legible. If the
110
+ // policy never reaches the gateway, nothing cancels the slow child
111
+ // and this run hangs until the delegation timeout — so the test
112
+ // would report "timed out after 60s" instead of "the sibling was
113
+ // not cancelled". Releasing it here means the assertion below is
114
+ // what fails, and it fails in a second.
115
+ setTimeout(() => this.releaseSlow?.(), 250).unref?.()
116
+ }
117
+ }
118
+
119
+ getInstance(taskId: TaskId): AgentTask | undefined {
120
+ return this.tasks.get(taskId)
121
+ }
122
+
123
+ cancelAll(): void {}
124
+ async continueTask(): Promise<void> {}
125
+ queueMessage(): void {}
126
+ drainMessages() {
127
+ return []
128
+ }
129
+ listByParent(): AgentTask[] {
130
+ return []
131
+ }
132
+ listActive(): AgentTask[] {
133
+ return []
134
+ }
135
+ getState(): AgentTaskState | undefined {
136
+ return undefined
137
+ }
138
+ on(): void {}
139
+ off(): void {}
140
+ cleanup(): void {}
141
+ dispose(): void {}
142
+ }
143
+
144
+ async function fanOut(policy?: SiblingFailurePolicy): Promise<FanOutManager> {
145
+ const agentManager = new FanOutManager()
146
+ const agent = new SupervisorAgent({
147
+ id: 'sup_policy',
148
+ name: 'Supervisor',
149
+ version: '1',
150
+ category: 'test',
151
+ description: 'coordinates workers',
152
+ })
153
+
154
+ await agent.run(
155
+ {
156
+ messages: [{ role: 'user', content: 'go', timestamp: 1 }],
157
+ workingDirectory: await mkdtemp(join(tmpdir(), 'namzu-sibling-policy-')),
158
+ } as never,
159
+ {
160
+ provider: new MockLLMProvider({
161
+ turns: [
162
+ {
163
+ toolCalls: [
164
+ {
165
+ name: 'create_task',
166
+ args: { agent_id: SLOW, prompt: 'long work', description: 'the patient one' },
167
+ },
168
+ {
169
+ name: 'create_task',
170
+ args: { agent_id: FAILING, prompt: 'doomed work', description: 'the doomed one' },
171
+ },
172
+ ],
173
+ },
174
+ { text: 'done' },
175
+ ],
176
+ }),
177
+ agentIds: [SLOW, FAILING],
178
+ agentManager,
179
+ tools: new ToolRegistry(),
180
+ systemPrompt: 'You coordinate.',
181
+ model: 'mock-model',
182
+ tokenBudget: 100_000,
183
+ timeoutMs: 30_000,
184
+ maxIterations: 3,
185
+ sessionId: 'ses_policy',
186
+ threadId: 'thd_policy',
187
+ projectId: 'prj_policy',
188
+ tenantId: 'tnt_policy',
189
+ ...(policy ? { siblingFailurePolicy: policy } : {}),
190
+ } as never,
191
+ )
192
+
193
+ return agentManager
194
+ }
195
+
196
+ describe('a supervisor can say what a failed child means for its siblings', () => {
197
+ it('reaches the gateway, so cancel-siblings actually stops the rest', async () => {
198
+ const manager = await fanOut('cancel-siblings')
199
+
200
+ expect(manager.cancelled).toContain('task_patient' as TaskId)
201
+ }, 60_000)
202
+ })