@namzu/sdk 32.0.0 → 33.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 (305) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/authorization/gate.d.ts +1 -1
  3. package/dist/authorization/gate.js +1 -1
  4. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  5. package/dist/bridge/a2a/mapper.js +13 -0
  6. package/dist/bridge/a2a/mapper.js.map +1 -1
  7. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  8. package/dist/bridge/sse/mapper.js +3 -0
  9. package/dist/bridge/sse/mapper.js.map +1 -1
  10. package/dist/connector/index.d.ts +1 -1
  11. package/dist/connector/index.d.ts.map +1 -1
  12. package/dist/connector/index.js +1 -1
  13. package/dist/connector/index.js.map +1 -1
  14. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  15. package/dist/connector/mcp/adapter.js +71 -16
  16. package/dist/connector/mcp/adapter.js.map +1 -1
  17. package/dist/connector/mcp/client.d.ts.map +1 -1
  18. package/dist/connector/mcp/client.js +3 -2
  19. package/dist/connector/mcp/client.js.map +1 -1
  20. package/dist/connector/mcp/http-redirect.d.ts +17 -0
  21. package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
  22. package/dist/connector/mcp/http-redirect.js +31 -0
  23. package/dist/connector/mcp/http-redirect.js.map +1 -0
  24. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  25. package/dist/connector/mcp/http-sse.js +5 -0
  26. package/dist/connector/mcp/http-sse.js.map +1 -1
  27. package/dist/connector/mcp/image-admission.d.ts +16 -0
  28. package/dist/connector/mcp/image-admission.d.ts.map +1 -0
  29. package/dist/connector/mcp/image-admission.js +288 -0
  30. package/dist/connector/mcp/image-admission.js.map +1 -0
  31. package/dist/connector/mcp/stdio.d.ts +13 -0
  32. package/dist/connector/mcp/stdio.d.ts.map +1 -1
  33. package/dist/connector/mcp/stdio.js +66 -23
  34. package/dist/connector/mcp/stdio.js.map +1 -1
  35. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  36. package/dist/connector/mcp/streamable-http.js +3 -0
  37. package/dist/connector/mcp/streamable-http.js.map +1 -1
  38. package/dist/constants/tools/index.js +1 -1
  39. package/dist/constants/tools/index.js.map +1 -1
  40. package/dist/execution/base.d.ts +13 -0
  41. package/dist/execution/base.d.ts.map +1 -1
  42. package/dist/execution/base.js +103 -8
  43. package/dist/execution/base.js.map +1 -1
  44. package/dist/execution/errors.d.ts +12 -0
  45. package/dist/execution/errors.d.ts.map +1 -0
  46. package/dist/execution/errors.js +21 -0
  47. package/dist/execution/errors.js.map +1 -0
  48. package/dist/execution/factory.d.ts.map +1 -1
  49. package/dist/execution/factory.js +1 -0
  50. package/dist/execution/factory.js.map +1 -1
  51. package/dist/execution/hybrid.d.ts +5 -0
  52. package/dist/execution/hybrid.d.ts.map +1 -1
  53. package/dist/execution/hybrid.js +36 -16
  54. package/dist/execution/hybrid.js.map +1 -1
  55. package/dist/execution/index.d.ts +1 -0
  56. package/dist/execution/index.d.ts.map +1 -1
  57. package/dist/execution/index.js +1 -0
  58. package/dist/execution/index.js.map +1 -1
  59. package/dist/execution/local.d.ts +13 -0
  60. package/dist/execution/local.d.ts.map +1 -1
  61. package/dist/execution/local.js +145 -19
  62. package/dist/execution/local.js.map +1 -1
  63. package/dist/execution/output.d.ts +27 -0
  64. package/dist/execution/output.d.ts.map +1 -0
  65. package/dist/execution/output.js +105 -0
  66. package/dist/execution/output.js.map +1 -0
  67. package/dist/execution/remote.d.ts +26 -2
  68. package/dist/execution/remote.d.ts.map +1 -1
  69. package/dist/execution/remote.js +86 -15
  70. package/dist/execution/remote.js.map +1 -1
  71. package/dist/plugin/lifecycle.d.ts.map +1 -1
  72. package/dist/plugin/lifecycle.js +9 -3
  73. package/dist/plugin/lifecycle.js.map +1 -1
  74. package/dist/process/kill-tree.d.ts +11 -13
  75. package/dist/process/kill-tree.d.ts.map +1 -1
  76. package/dist/process/kill-tree.js +11 -13
  77. package/dist/process/kill-tree.js.map +1 -1
  78. package/dist/provider/capabilities.d.ts +2 -0
  79. package/dist/provider/capabilities.d.ts.map +1 -1
  80. package/dist/provider/capabilities.js +6 -0
  81. package/dist/provider/capabilities.js.map +1 -1
  82. package/dist/provider/fallback.d.ts.map +1 -1
  83. package/dist/provider/fallback.js +31 -0
  84. package/dist/provider/fallback.js.map +1 -1
  85. package/dist/provider/idle-timeout.d.ts.map +1 -1
  86. package/dist/provider/idle-timeout.js +5 -0
  87. package/dist/provider/idle-timeout.js.map +1 -1
  88. package/dist/provider/retry.d.ts.map +1 -1
  89. package/dist/provider/retry.js +5 -0
  90. package/dist/provider/retry.js.map +1 -1
  91. package/dist/public-runtime.d.ts +2 -2
  92. package/dist/public-runtime.d.ts.map +1 -1
  93. package/dist/public-runtime.js +2 -2
  94. package/dist/public-runtime.js.map +1 -1
  95. package/dist/registry/tool/execute.d.ts +7 -1
  96. package/dist/registry/tool/execute.d.ts.map +1 -1
  97. package/dist/registry/tool/execute.js +201 -54
  98. package/dist/registry/tool/execute.js.map +1 -1
  99. package/dist/run/command-gate.d.ts.map +1 -1
  100. package/dist/run/command-gate.js +11 -0
  101. package/dist/run/command-gate.js.map +1 -1
  102. package/dist/run/workspace-fingerprint.d.ts +4 -4
  103. package/dist/run/workspace-fingerprint.d.ts.map +1 -1
  104. package/dist/run/workspace-fingerprint.js +6 -4
  105. package/dist/run/workspace-fingerprint.js.map +1 -1
  106. package/dist/runtime/bidi/index.d.ts +1 -1
  107. package/dist/runtime/bidi/index.d.ts.map +1 -1
  108. package/dist/runtime/bidi/index.js +1 -1
  109. package/dist/runtime/bidi/index.js.map +1 -1
  110. package/dist/runtime/bidi/session.d.ts +19 -0
  111. package/dist/runtime/bidi/session.d.ts.map +1 -1
  112. package/dist/runtime/bidi/session.js +150 -26
  113. package/dist/runtime/bidi/session.js.map +1 -1
  114. package/dist/runtime/query/executor.d.ts +43 -1
  115. package/dist/runtime/query/executor.d.ts.map +1 -1
  116. package/dist/runtime/query/executor.js +626 -161
  117. package/dist/runtime/query/executor.js.map +1 -1
  118. package/dist/runtime/query/index.d.ts +3 -1
  119. package/dist/runtime/query/index.d.ts.map +1 -1
  120. package/dist/runtime/query/index.js +48 -4
  121. package/dist/runtime/query/index.js.map +1 -1
  122. package/dist/runtime/query/iteration/index.d.ts +10 -0
  123. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  124. package/dist/runtime/query/iteration/index.js +67 -0
  125. package/dist/runtime/query/iteration/index.js.map +1 -1
  126. package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
  127. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  128. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  129. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  130. package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
  131. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  132. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  133. package/dist/runtime/query/request-rich-content.js +33 -13
  134. package/dist/runtime/query/request-rich-content.js.map +1 -1
  135. package/dist/runtime/query/result.d.ts.map +1 -1
  136. package/dist/runtime/query/result.js +25 -22
  137. package/dist/runtime/query/result.js.map +1 -1
  138. package/dist/runtime/query/resume-pending.d.ts +5 -1
  139. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  140. package/dist/runtime/query/resume-pending.js +66 -1
  141. package/dist/runtime/query/resume-pending.js.map +1 -1
  142. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  143. package/dist/runtime/query/tool-output-budget.js +45 -16
  144. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  145. package/dist/runtime/query/tooling.d.ts +6 -0
  146. package/dist/runtime/query/tooling.d.ts.map +1 -1
  147. package/dist/runtime/query/tooling.js +4 -0
  148. package/dist/runtime/query/tooling.js.map +1 -1
  149. package/dist/sandbox/provider/local.d.ts.map +1 -1
  150. package/dist/sandbox/provider/local.js +193 -41
  151. package/dist/sandbox/provider/local.js.map +1 -1
  152. package/dist/scheduler/delegating.d.ts +2 -1
  153. package/dist/scheduler/delegating.d.ts.map +1 -1
  154. package/dist/scheduler/delegating.js +6 -5
  155. package/dist/scheduler/delegating.js.map +1 -1
  156. package/dist/scheduler/local.d.ts +2 -1
  157. package/dist/scheduler/local.d.ts.map +1 -1
  158. package/dist/scheduler/local.js +2 -2
  159. package/dist/scheduler/local.js.map +1 -1
  160. package/dist/session/workspace/driver.d.ts +12 -4
  161. package/dist/session/workspace/driver.d.ts.map +1 -1
  162. package/dist/session/workspace/git-worktree.d.ts +17 -3
  163. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  164. package/dist/session/workspace/git-worktree.js +153 -34
  165. package/dist/session/workspace/git-worktree.js.map +1 -1
  166. package/dist/skills/registry.d.ts +15 -0
  167. package/dist/skills/registry.d.ts.map +1 -1
  168. package/dist/skills/registry.js +33 -0
  169. package/dist/skills/registry.js.map +1 -1
  170. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  171. package/dist/tools/builtins/computer-use.js +76 -2
  172. package/dist/tools/builtins/computer-use.js.map +1 -1
  173. package/dist/tools/builtins/run-code.d.ts.map +1 -1
  174. package/dist/tools/builtins/run-code.js +9 -11
  175. package/dist/tools/builtins/run-code.js.map +1 -1
  176. package/dist/tools/builtins/skill.d.ts +2 -1
  177. package/dist/tools/builtins/skill.d.ts.map +1 -1
  178. package/dist/tools/builtins/skill.js +250 -6
  179. package/dist/tools/builtins/skill.js.map +1 -1
  180. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  181. package/dist/tools/coordinator/agent.js +100 -30
  182. package/dist/tools/coordinator/agent.js.map +1 -1
  183. package/dist/types/agent/manager.d.ts +1 -1
  184. package/dist/types/agent/manager.d.ts.map +1 -1
  185. package/dist/types/agent/scheduler.d.ts +10 -1
  186. package/dist/types/agent/scheduler.d.ts.map +1 -1
  187. package/dist/types/authorization/index.d.ts +12 -12
  188. package/dist/types/bidi/index.d.ts +19 -2
  189. package/dist/types/bidi/index.d.ts.map +1 -1
  190. package/dist/types/computer-use/index.d.ts +18 -0
  191. package/dist/types/computer-use/index.d.ts.map +1 -1
  192. package/dist/types/connector/execution-context.d.ts +3 -1
  193. package/dist/types/connector/execution-context.d.ts.map +1 -1
  194. package/dist/types/errors/catalog.d.ts.map +1 -1
  195. package/dist/types/errors/catalog.js +13 -6
  196. package/dist/types/errors/catalog.js.map +1 -1
  197. package/dist/types/errors/index.d.ts.map +1 -1
  198. package/dist/types/errors/index.js +21 -8
  199. package/dist/types/errors/index.js.map +1 -1
  200. package/dist/types/execution/index.d.ts +43 -1
  201. package/dist/types/execution/index.d.ts.map +1 -1
  202. package/dist/types/hitl/index.d.ts +11 -0
  203. package/dist/types/hitl/index.d.ts.map +1 -1
  204. package/dist/types/hitl/index.js.map +1 -1
  205. package/dist/types/message/index.d.ts +1 -1
  206. package/dist/types/message/index.d.ts.map +1 -1
  207. package/dist/types/message/index.js +2 -1
  208. package/dist/types/message/index.js.map +1 -1
  209. package/dist/types/plugin/index.d.ts +14 -1
  210. package/dist/types/plugin/index.d.ts.map +1 -1
  211. package/dist/types/plugin/index.js +1 -0
  212. package/dist/types/plugin/index.js.map +1 -1
  213. package/dist/types/provider/config.d.ts +9 -0
  214. package/dist/types/provider/config.d.ts.map +1 -1
  215. package/dist/types/provider/interface.d.ts +14 -0
  216. package/dist/types/provider/interface.d.ts.map +1 -1
  217. package/dist/types/run/events.d.ts +17 -0
  218. package/dist/types/run/events.d.ts.map +1 -1
  219. package/dist/types/run/events.js.map +1 -1
  220. package/dist/types/sandbox/index.d.ts +15 -10
  221. package/dist/types/sandbox/index.d.ts.map +1 -1
  222. package/dist/types/sandbox/index.js.map +1 -1
  223. package/dist/types/tool/index.d.ts +77 -6
  224. package/dist/types/tool/index.d.ts.map +1 -1
  225. package/dist/types/tool/index.js.map +1 -1
  226. package/dist/types/tool/presentation.d.ts +6 -2
  227. package/dist/types/tool/presentation.d.ts.map +1 -1
  228. package/dist/types/workspace/ref.d.ts +5 -0
  229. package/dist/types/workspace/ref.d.ts.map +1 -1
  230. package/dist/utils/abort.d.ts +8 -0
  231. package/dist/utils/abort.d.ts.map +1 -1
  232. package/dist/utils/abort.js +16 -0
  233. package/dist/utils/abort.js.map +1 -1
  234. package/package.json +4 -4
  235. package/src/authorization/gate.ts +1 -1
  236. package/src/bridge/a2a/mapper.ts +23 -4
  237. package/src/bridge/sse/mapper.ts +3 -0
  238. package/src/connector/index.ts +2 -0
  239. package/src/connector/mcp/adapter.ts +82 -21
  240. package/src/connector/mcp/client.ts +2 -1
  241. package/src/connector/mcp/http-redirect.ts +32 -0
  242. package/src/connector/mcp/http-sse.ts +5 -0
  243. package/src/connector/mcp/image-admission.ts +272 -0
  244. package/src/connector/mcp/stdio.ts +63 -21
  245. package/src/connector/mcp/streamable-http.ts +3 -0
  246. package/src/constants/tools/index.ts +1 -1
  247. package/src/execution/base.ts +124 -7
  248. package/src/execution/errors.ts +24 -0
  249. package/src/execution/factory.ts +1 -0
  250. package/src/execution/hybrid.ts +47 -16
  251. package/src/execution/index.ts +1 -0
  252. package/src/execution/local.ts +164 -19
  253. package/src/execution/output.ts +104 -0
  254. package/src/execution/remote.ts +122 -17
  255. package/src/plugin/lifecycle.ts +9 -3
  256. package/src/process/kill-tree.ts +11 -13
  257. package/src/provider/capabilities.ts +10 -0
  258. package/src/provider/fallback.ts +39 -0
  259. package/src/provider/idle-timeout.ts +8 -0
  260. package/src/provider/retry.ts +8 -0
  261. package/src/public-runtime.ts +7 -1
  262. package/src/registry/tool/execute.ts +230 -64
  263. package/src/run/command-gate.ts +12 -0
  264. package/src/run/workspace-fingerprint.ts +5 -4
  265. package/src/runtime/bidi/index.ts +1 -1
  266. package/src/runtime/bidi/session.ts +177 -28
  267. package/src/runtime/query/executor.ts +769 -163
  268. package/src/runtime/query/index.ts +58 -7
  269. package/src/runtime/query/iteration/index.ts +72 -0
  270. package/src/runtime/query/iteration/phases/context.ts +5 -0
  271. package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
  272. package/src/runtime/query/request-rich-content.ts +39 -14
  273. package/src/runtime/query/result.ts +25 -23
  274. package/src/runtime/query/resume-pending.ts +83 -1
  275. package/src/runtime/query/tool-output-budget.ts +52 -17
  276. package/src/runtime/query/tooling.ts +10 -0
  277. package/src/sandbox/provider/local.ts +213 -45
  278. package/src/scheduler/delegating.ts +6 -5
  279. package/src/scheduler/local.ts +3 -2
  280. package/src/session/workspace/driver.ts +12 -4
  281. package/src/session/workspace/git-worktree.ts +173 -34
  282. package/src/skills/registry.ts +47 -0
  283. package/src/tools/builtins/computer-use.ts +86 -2
  284. package/src/tools/builtins/run-code.ts +9 -11
  285. package/src/tools/builtins/skill.ts +325 -7
  286. package/src/tools/coordinator/agent.ts +108 -33
  287. package/src/types/agent/manager.ts +1 -1
  288. package/src/types/agent/scheduler.ts +10 -1
  289. package/src/types/bidi/index.ts +19 -2
  290. package/src/types/computer-use/index.ts +19 -0
  291. package/src/types/connector/execution-context.ts +3 -0
  292. package/src/types/errors/catalog.ts +14 -6
  293. package/src/types/errors/index.ts +22 -8
  294. package/src/types/execution/index.ts +46 -1
  295. package/src/types/hitl/index.ts +11 -0
  296. package/src/types/message/index.ts +3 -2
  297. package/src/types/plugin/index.ts +15 -0
  298. package/src/types/provider/config.ts +9 -0
  299. package/src/types/provider/interface.ts +18 -0
  300. package/src/types/run/events.ts +17 -3
  301. package/src/types/sandbox/index.ts +15 -10
  302. package/src/types/tool/index.ts +85 -6
  303. package/src/types/tool/presentation.ts +10 -3
  304. package/src/types/workspace/ref.ts +5 -0
  305. package/src/utils/abort.ts +18 -0
@@ -1,8 +1,11 @@
1
+ import { isDeepStrictEqual } from 'node:util'
2
+
1
3
  import type { RunPersistence } from '../../manager/run/persistence.js'
2
4
  import type {
3
5
  CheckpointId,
4
6
  HITLResumeDecision,
5
7
  IterationCheckpoint,
8
+ ToolCallSummary,
6
9
  } from '../../types/hitl/index.js'
7
10
  import type { AssistantMessage, Message, ToolCall } from '../../types/message/index.js'
8
11
  import type { ChatCompletionResponse } from '../../types/provider/index.js'
@@ -38,6 +41,10 @@ export interface PendingResumePlan {
38
41
  readonly response: ChatCompletionResponse
39
42
  /** Per-call refusals derived from the decision. */
40
43
  readonly denials: ToolCallDenials
44
+ /** Exact projections and gate decisions persisted with a tool review. */
45
+ readonly reviewedCalls?: readonly ToolCallSummary[]
46
+ /** Calls whose raw input the human replaced in the durable decision. */
47
+ readonly modifiedCallIds?: ReadonlySet<string>
41
48
  /**
42
49
  * Answers to deliver to tools that parked on a question, keyed by the
43
50
  * asking call's id. Present only on a question resume.
@@ -117,6 +124,8 @@ export function planPendingResume(
117
124
  assistant,
118
125
  response: synthesizeResponse(assistant),
119
126
  denials,
127
+ reviewedCalls: pending.request.toolCalls,
128
+ modifiedCallIds: modifiedCallIds(decision),
120
129
  }
121
130
  }
122
131
 
@@ -246,13 +255,86 @@ export async function applyPendingResume(
246
255
  executor: ToolExecutor,
247
256
  prior?: PriorToolResults,
248
257
  ): Promise<void> {
258
+ const denials = new Map(plan.denials)
259
+ const reviewedById = new Map(plan.reviewedCalls?.map((call) => [call.id, call]))
260
+
261
+ // A gate denial belongs to the run, not to the process that first evaluated
262
+ // it. Restore it before preparation so denied calls do not even reach a
263
+ // pre-tool hook after restart.
264
+ for (const call of plan.reviewedCalls ?? []) {
265
+ if (call.authorization?.decision !== 'deny' || denials.has(call.id)) continue
266
+ denials.set(
267
+ call.id,
268
+ `Blocked by the authorization gate: ${call.authorization.reason ?? 'the persisted operator policy denied this call'}`,
269
+ )
270
+ }
271
+
272
+ const callsToPrepare = (plan.response.message.toolCalls ?? []).filter(
273
+ (call) => !prior?.has(call.id) && !denials.has(call.id),
274
+ )
275
+ const responseToPrepare: ChatCompletionResponse = {
276
+ ...plan.response,
277
+ message: { ...plan.response.message, toolCalls: callsToPrepare },
278
+ }
279
+ const preparedBatch = await executor.prepareBatchForReview(responseToPrepare)
280
+
281
+ for (const call of preparedBatch.reviewCalls) {
282
+ const reviewed = reviewedById.get(call.id)
283
+ const wasModified = plan.modifiedCallIds?.has(call.id) === true
284
+ if (reviewed && !wasModified) {
285
+ const unchanged = reviewed.name === call.name && isDeepStrictEqual(reviewed.input, call.input)
286
+ if (!unchanged) {
287
+ const reason =
288
+ 'The tool input changed after its durable review; the earlier approval cannot be reused.'
289
+ denials.set(call.id, reason)
290
+ await runMgr.recordAudit({
291
+ what: { action: 'tool_call', tool: call.name },
292
+ outcome: 'refused',
293
+ reason,
294
+ })
295
+ continue
296
+ }
297
+ }
298
+
299
+ const current = executor.evaluatePreparedAuthorization(call.name, call.input)
300
+ let reason: string | undefined
301
+ if (current?.decision === 'deny') {
302
+ reason = `Blocked by the authorization gate after resume: ${current.reason}`
303
+ } else if (current?.decision === 'review' && (wasModified || !reviewed)) {
304
+ reason = wasModified
305
+ ? `Blocked by the authorization gate after resume: the modified prepared value requires a new explicit approval. ${current.reason}`
306
+ : `Blocked by the authorization gate after resume: this recovered call requires operator review. ${current.reason}`
307
+ } else if (!current && reviewed && reviewed.authorization === undefined) {
308
+ reason =
309
+ 'The durable review predates bound authorization metadata; review this call again before execution.'
310
+ }
311
+
312
+ if (reason) {
313
+ denials.set(call.id, reason)
314
+ await runMgr.recordAudit({
315
+ what: { action: 'tool_call', tool: call.name },
316
+ outcome: 'refused',
317
+ reason,
318
+ })
319
+ }
320
+ }
321
+
249
322
  runMgr.pushMessage(plan.assistant)
250
- const batch = await executor.executeBatch(plan.response, plan.denials, prior)
323
+ const batch = await executor.executeBatch(plan.response, denials, prior, preparedBatch)
251
324
  for (const msg of batch.messages) {
252
325
  runMgr.pushMessage(msg)
253
326
  }
254
327
  }
255
328
 
329
+ function modifiedCallIds(decision: HITLResumeDecision): ReadonlySet<string> {
330
+ if (decision.action !== 'modify_tools') return new Set()
331
+ return new Set(
332
+ decision.modifications
333
+ .filter((modification) => modification.action === 'modify')
334
+ .map((modification) => modification.toolCallId),
335
+ )
336
+ }
337
+
256
338
  /**
257
339
  * Results the run already produced for calls in `toolCalls`.
258
340
  *
@@ -37,6 +37,57 @@ export const SPILL_MARKER = 'The full output was written to:'
37
37
  */
38
38
  const HEAD_SHARE = 0.75
39
39
 
40
+ function safeHead(text: string, maxChars: number): string {
41
+ let end = Math.min(text.length, Math.max(0, maxChars))
42
+ if (end > 0) {
43
+ const last = text.charCodeAt(end - 1)
44
+ if (last >= 0xd800 && last <= 0xdbff) end--
45
+ }
46
+ return text.slice(0, end)
47
+ }
48
+
49
+ function safeTail(text: string, maxChars: number): string {
50
+ let start = Math.max(0, text.length - Math.max(0, maxChars))
51
+ if (start < text.length) {
52
+ const first = text.charCodeAt(start)
53
+ if (first >= 0xdc00 && first <= 0xdfff) start++
54
+ }
55
+ return text.slice(start)
56
+ }
57
+
58
+ /**
59
+ * Fit a recoverable head+tail preview inside the actual model-visible cap.
60
+ *
61
+ * The old implementation allocated the entire cap to source text and then
62
+ * appended its diagnostic and recovery instructions. A configured 1,000
63
+ * character cap therefore emitted 1,200–1,300 characters; at very small
64
+ * limits the explanation could be several times larger than the limit.
65
+ */
66
+ function boundedPreview(
67
+ output: string,
68
+ maxChars: number,
69
+ toolName: string,
70
+ recovery: string,
71
+ ): string {
72
+ const detailedMiddle = [
73
+ '',
74
+ `[... characters omitted — "${toolName}" returned ${output.length.toLocaleString()} characters, over the ${maxChars.toLocaleString()}-character budget ...]`,
75
+ recovery,
76
+ '',
77
+ ].join('\n')
78
+ const compactMiddle = '\n[... omitted ...]\n'
79
+ const middle = detailedMiddle.length < maxChars ? detailedMiddle : compactMiddle
80
+
81
+ // A host may deliberately set a tiny positive cap. At that point no
82
+ // truthful recovery sentence fits; the hard bound still wins.
83
+ if (middle.length >= maxChars) return safeHead(middle, maxChars)
84
+
85
+ const sourceChars = maxChars - middle.length
86
+ const headChars = Math.max(1, Math.floor(sourceChars * HEAD_SHARE))
87
+ const tailChars = Math.max(0, sourceChars - headChars)
88
+ return `${safeHead(output, headChars)}${middle}${safeTail(output, tailChars)}`
89
+ }
90
+
40
91
  export interface ToolOutputBudgetResult {
41
92
  /** What the model sees. */
42
93
  readonly output: string
@@ -127,15 +178,6 @@ export function applyToolOutputBudget(opts: ApplyToolOutputBudgetOptions): ToolO
127
178
  return { output, originalLength, truncated: false }
128
179
  }
129
180
 
130
- // Both slices are derived from `maxChars` so the preview honours the
131
- // budget it is enforcing. `slice(-0)` returns the WHOLE string, so the
132
- // zero-tail case must be branched, not computed.
133
- const headChars = Math.max(1, Math.floor(maxChars * HEAD_SHARE))
134
- const tailChars = Math.max(0, maxChars - headChars)
135
- const head = output.slice(0, headChars)
136
- const tail = tailChars > 0 ? output.slice(-tailChars) : ''
137
- const omitted = originalLength - head.length - tail.length
138
-
139
181
  const spillPath = opts.spillDir
140
182
  ? spill(opts.spillDir, opts.toolUseId, output, opts.onError)
141
183
  : undefined
@@ -148,14 +190,7 @@ export function applyToolOutputBudget(opts: ApplyToolOutputBudgetOptions): ToolO
148
190
  : 'The full output was not retained. Re-run with a narrower query, a line range, or a filter.'
149
191
 
150
192
  return {
151
- output: [
152
- head,
153
- '',
154
- `[... ${omitted.toLocaleString()} characters omitted — "${opts.toolName}" returned ${originalLength.toLocaleString()} characters, over the ${maxChars.toLocaleString()}-character budget ...]`,
155
- recovery,
156
- '',
157
- tail,
158
- ].join('\n'),
193
+ output: boundedPreview(output, maxChars, opts.toolName, recovery),
159
194
  originalLength,
160
195
  truncated: true,
161
196
  ...(spillPath ? { spillPath } : {}),
@@ -1,8 +1,10 @@
1
+ import type { AuthorizationGate } from '../../authorization/gate.js'
1
2
  import type { PluginLifecycleManager } from '../../plugin/lifecycle.js'
2
3
  import type { ActivityStore } from '../../store/activity/memory.js'
3
4
  import type { RunId } from '../../types/ids/index.js'
4
5
  import type { InvocationState } from '../../types/invocation/index.js'
5
6
  import type { PermissionMode } from '../../types/permission/index.js'
7
+ import type { AuditEventInput } from '../../types/run/audit.js'
6
8
  import type { RunEvent } from '../../types/run/index.js'
7
9
  import type {
8
10
  RequestToolPause,
@@ -41,6 +43,10 @@ export interface ToolingBootstrapConfig {
41
43
  maxToolContentBytes?: number
42
44
  toolOutputDir?: string
43
45
  repairToolCall?: RepairToolCall
46
+ /** Operator authorization shared with the direct-call review path. */
47
+ authorizationGate?: AuthorizationGate
48
+ /** Durable refusal recorder for nested authorization decisions. */
49
+ recordAudit?: (input: AuditEventInput) => Promise<unknown>
44
50
  /** Builds the durable-pause seam for one tool call; see ToolContext.requestPause. */
45
51
  toolPause?: (toolUseId: string) => RequestToolPause
46
52
  }
@@ -81,6 +87,10 @@ export class ToolingBootstrap {
81
87
  : {}),
82
88
  ...(config.toolOutputDir !== undefined ? { toolOutputDir: config.toolOutputDir } : {}),
83
89
  ...(config.repairToolCall !== undefined ? { repairToolCall: config.repairToolCall } : {}),
90
+ ...(config.authorizationGate !== undefined
91
+ ? { authorizationGate: config.authorizationGate }
92
+ : {}),
93
+ ...(config.recordAudit !== undefined ? { recordAudit: config.recordAudit } : {}),
84
94
  ...(config.toolPause !== undefined ? { toolPause: config.toolPause } : {}),
85
95
  },
86
96
  activityStore,
@@ -38,6 +38,7 @@ import type {
38
38
  SandboxProvider,
39
39
  SandboxStatus,
40
40
  } from '../../types/sandbox/index.js'
41
+ import { subscribeToAbort } from '../../utils/abort.js'
41
42
  import { generateSandboxId } from '../../utils/id.js'
42
43
  import type { Logger } from '../../utils/logger.js'
43
44
  import {
@@ -72,6 +73,8 @@ function assertInsideSandbox(sandboxRoot: string, targetPath: string): string {
72
73
  const LINUX_UNSHARE_FLAGS = ['--mount', '--pid', '--fork', '--map-root-user', '--net']
73
74
 
74
75
  const SPAWN_PROBE_SENTINEL = 'namzu-sandbox-spawn-probe'
76
+ const BWRAP_INFO_FD = 3
77
+ const BWRAP_BLOCK_FD = 4
75
78
 
76
79
  interface SpawnProbeObservation {
77
80
  readonly error?: unknown
@@ -117,6 +120,48 @@ function probeSandboxSpawn(command: string, wrapperArgs: readonly string[]): boo
117
120
  return acceptsSandboxSpawnProbe(observation)
118
121
  }
119
122
 
123
+ function readBwrapChildPid(value: string): number | undefined {
124
+ try {
125
+ const record = JSON.parse(value) as { readonly 'child-pid'?: unknown }
126
+ const pid = record['child-pid']
127
+ return typeof pid === 'number' && Number.isSafeInteger(pid) && pid > 0 ? pid : undefined
128
+ } catch {
129
+ return undefined
130
+ }
131
+ }
132
+
133
+ /** The bwrap tier is usable only if its child can be addressed after startup. */
134
+ function probeBwrapSpawn(command: string, wrapperArgs: readonly string[]): boolean {
135
+ const observation = spawnSync(
136
+ command,
137
+ [
138
+ ...wrapperArgs,
139
+ '--info-fd',
140
+ String(BWRAP_INFO_FD),
141
+ '--block-fd',
142
+ String(BWRAP_BLOCK_FD),
143
+ '--',
144
+ process.execPath,
145
+ '-e',
146
+ `process.stdout.write(${JSON.stringify(SPAWN_PROBE_SENTINEL)})`,
147
+ ],
148
+ {
149
+ encoding: 'utf8',
150
+ timeout: 5_000,
151
+ // `/dev/null` proves the option is accepted and lets the synchronous
152
+ // probe proceed. Production supplies a live pipe and releases it only
153
+ // after the info record establishes ownership of the inner namespace.
154
+ stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'ignore'],
155
+ },
156
+ )
157
+ const status = observation.output?.[BWRAP_INFO_FD]
158
+ return (
159
+ acceptsSandboxSpawnProbe(observation) &&
160
+ typeof status === 'string' &&
161
+ readBwrapChildPid(status) !== undefined
162
+ )
163
+ }
164
+
120
165
  const TRUSTED_WRAPPER_CANDIDATES = {
121
166
  bwrap: ['/usr/bin/bwrap', '/bin/bwrap', '/usr/sbin/bwrap'],
122
167
  unshare: ['/usr/bin/unshare', '/bin/unshare', '/usr/sbin/unshare'],
@@ -275,7 +320,7 @@ function detectEnvironment(): DetectedEnvironment {
275
320
  // hosts that mediate namespace helpers and can claim a tier production
276
321
  // cannot actually drive.
277
322
  const bwrap = resolveTrustedWrapper('bwrap')
278
- if (bwrap !== undefined && probeSandboxSpawn(bwrap, buildBwrapArgs(tmpdir()))) {
323
+ if (bwrap !== undefined && probeBwrapSpawn(bwrap, buildBwrapArgs(tmpdir()))) {
279
324
  return { environment: 'linux-bwrap', wrapperCommand: bwrap }
280
325
  }
281
326
 
@@ -522,6 +567,27 @@ function buildSafeEnv(
522
567
  return env
523
568
  }
524
569
 
570
+ /**
571
+ * Reach bwrap's PID-namespace reaper as well as its outer host wrapper.
572
+ *
573
+ * `--new-session` deliberately moves the inner reaper out of the outer
574
+ * detached process group. `--die-with-parent` normally bridges that boundary,
575
+ * but there is a startup interval before the inner reaper has armed its parent
576
+ * death signal. `--info-fd` gives us the host pid once it exists. Address both
577
+ * its eventual process group and the pid itself: before `setsid` only the pid
578
+ * is valid; afterwards the negative pid reaches the complete inner session.
579
+ */
580
+ function killBwrapChild(pid: number | undefined, signal: NodeJS.Signals): void {
581
+ if (pid === undefined || process.platform === 'win32') return
582
+ for (const target of [-pid, pid]) {
583
+ try {
584
+ process.kill(target, signal)
585
+ } catch {
586
+ // The other address may be the valid one, or the namespace is gone.
587
+ }
588
+ }
589
+ }
590
+
525
591
  // ---------------------------------------------------------------------------
526
592
  // LocalSandbox
527
593
  // ---------------------------------------------------------------------------
@@ -577,7 +643,10 @@ class LocalSandbox implements Sandbox {
577
643
 
578
644
  const cwd = opts?.cwd ? assertInsideSandbox(this.rootDir, opts.cwd) : this.rootDir
579
645
 
580
- const { spawnCommand, spawnArgs } = this.buildSpawnArgs(command, args)
646
+ const { spawnCommand, spawnArgs, bwrapInfoFd, bwrapBlockFd } = this.buildSpawnArgs(
647
+ command,
648
+ args,
649
+ )
581
650
 
582
651
  this.log.debug('Executing command', {
583
652
  'namzu.sandbox.command': command,
@@ -586,23 +655,27 @@ class LocalSandbox implements Sandbox {
586
655
  'namzu.execution.environment': this.environment,
587
656
  })
588
657
 
589
- // The caller's cancellation and this call's own deadline both have to
590
- // reach `spawn`, and `spawn` takes exactly one signal.
591
- //
592
- // Only the deadline used to. `SandboxExecOptions.signal` is declared,
593
- // documented, and exported, and every backend dropped it so a Stop
594
- // abandoned the *wait* and left the sandboxed process running, which is
595
- // verbatim the failure the option's own docstring says it exists to
596
- // prevent. `AbortSignal.any` is what makes both reach the child: it
597
- // aborts as soon as either does, and — unlike an `addEventListener`
598
- // bridge — it does not retain a listener on the caller's long-lived
599
- // signal after this call settles.
658
+ // The caller's cancellation and this call's own deadline are separate
659
+ // causes. `spawn({ signal })` cannot own either one here: Node sends that
660
+ // signal only to the direct wrapper pid and removes its abort listener on
661
+ // `exit`, while this promise must wait for `close` so inherited stdout and
662
+ // stderr have drained. A wrapper can therefore exit before a descendant;
663
+ // an abort in that interval would reach nobody and the descendant would
664
+ // keep both pipes and this call alive.
600
665
  const ac = new AbortController()
601
666
  const timeoutId = setTimeout(() => ac.abort(), timeout)
602
- const spawnSignal = opts?.signal ? AbortSignal.any([ac.signal, opts.signal]) : ac.signal
603
667
 
604
668
  try {
605
- const result = await this.spawnProcess(spawnCommand, spawnArgs, cwd, env, ac, spawnSignal)
669
+ const result = await this.spawnProcess(
670
+ spawnCommand,
671
+ spawnArgs,
672
+ cwd,
673
+ env,
674
+ ac.signal,
675
+ opts?.signal,
676
+ bwrapInfoFd,
677
+ bwrapBlockFd,
678
+ )
606
679
  return { ...result, durationMs: Date.now() - startTime }
607
680
  } finally {
608
681
  clearTimeout(timeoutId)
@@ -685,8 +758,13 @@ class LocalSandbox implements Sandbox {
685
758
  private buildSpawnArgs(
686
759
  command: string,
687
760
  args: string[],
688
- ): { spawnCommand: string; spawnArgs: string[] } {
689
- return buildLimitedSpawn({
761
+ ): {
762
+ spawnCommand: string
763
+ spawnArgs: string[]
764
+ bwrapInfoFd?: number
765
+ bwrapBlockFd?: number
766
+ } {
767
+ const built = buildLimitedSpawn({
690
768
  environment: this.environment,
691
769
  ...(this.wrapperCommand !== undefined ? { wrapperCommand: this.wrapperCommand } : {}),
692
770
  command,
@@ -697,24 +775,40 @@ class LocalSandbox implements Sandbox {
697
775
  : {}),
698
776
  ...(this.config.maxProcesses !== undefined ? { maxProcesses: this.config.maxProcesses } : {}),
699
777
  })
778
+ if (this.environment !== 'linux-bwrap') return built
779
+
780
+ const separator = built.spawnArgs.indexOf('--')
781
+ if (separator < 0) throw new Error('bwrap invocation has no command separator')
782
+ return {
783
+ spawnCommand: built.spawnCommand,
784
+ spawnArgs: [
785
+ ...built.spawnArgs.slice(0, separator),
786
+ '--info-fd',
787
+ String(BWRAP_INFO_FD),
788
+ '--block-fd',
789
+ String(BWRAP_BLOCK_FD),
790
+ ...built.spawnArgs.slice(separator),
791
+ ],
792
+ bwrapInfoFd: BWRAP_INFO_FD,
793
+ bwrapBlockFd: BWRAP_BLOCK_FD,
794
+ }
700
795
  }
701
796
 
702
797
  /**
703
- * @param ac this call's own deadline still the thing that decides
704
- * whether a termination is reported as `timedOut`.
705
- * @param signal what actually reaches `spawn`: the deadline, merged with
706
- * the caller's cancellation when one was passed. Two
707
- * parameters because they answer different questions —
708
- * "should this process die" and "did it die because it ran
709
- * too long" — and a cancelled run did not time out.
798
+ * The two signals stay distinct so the first accepted cause is latched.
799
+ * Reading `deadline.aborted` later would let a deadline that fired while a
800
+ * caller cancellation was draining retroactively turn that cancellation
801
+ * into a timeout.
710
802
  */
711
803
  private spawnProcess(
712
804
  command: string,
713
805
  args: string[],
714
806
  cwd: string,
715
807
  env: Record<string, string>,
716
- ac: AbortController,
717
- signal: AbortSignal = ac.signal,
808
+ deadlineSignal: AbortSignal,
809
+ callerSignal?: AbortSignal,
810
+ bwrapInfoFd?: number,
811
+ bwrapBlockFd?: number,
718
812
  ): Promise<Omit<SandboxExecResult, 'durationMs'>> {
719
813
  return new Promise((resolvePromise, rejectPromise) => {
720
814
  let child: ReturnType<typeof spawn>
@@ -722,8 +816,10 @@ class LocalSandbox implements Sandbox {
722
816
  child = spawn(command, args, {
723
817
  cwd,
724
818
  env,
725
- stdio: ['pipe', 'pipe', 'pipe'],
726
- signal,
819
+ stdio:
820
+ bwrapInfoFd === undefined
821
+ ? ['pipe', 'pipe', 'pipe']
822
+ : ['pipe', 'pipe', 'pipe', 'pipe', 'pipe'],
727
823
  // Leader of its own process group (POSIX only — Windows has
728
824
  // nothing to opt into here), not a member of this Node
729
825
  // process's. That is what lets `killTree` below reach `cmd`
@@ -738,30 +834,92 @@ class LocalSandbox implements Sandbox {
738
834
 
739
835
  const stdout = new CappedStream(SANDBOX_MAX_OUTPUT_BYTES)
740
836
  const stderr = new CappedStream(SANDBOX_MAX_OUTPUT_BYTES)
741
- let timedOut = false
837
+ let cancellation: 'caller' | 'deadline' | undefined
838
+ let bwrapChildPid: number | undefined
839
+ let terminationSignal: NodeJS.Signals | undefined
840
+ let settled = false
841
+ let escalation: ReturnType<typeof setTimeout> | undefined
842
+ const disposeAbortListeners: Array<() => void> = []
843
+
844
+ const finishSubscriptions = () => {
845
+ for (const dispose of disposeAbortListeners.splice(0)) dispose()
846
+ }
847
+ const terminate = (signal: NodeJS.Signals) => {
848
+ terminationSignal = signal
849
+ // Before bwrap publishes its inner reaper, killing only the outer
850
+ // wrapper can strand startup between fork() and PR_SET_PDEATHSIG. Its
851
+ // block fd guarantees the authored command is not admitted meanwhile;
852
+ // retain the wrapper until the addressable inner pid arrives.
853
+ if (bwrapBlockFd !== undefined && bwrapChildPid === undefined) return
854
+ killTree(child, signal)
855
+ killBwrapChild(bwrapChildPid, signal)
856
+ }
857
+ const cancel = (origin: 'caller' | 'deadline') => {
858
+ if (settled || cancellation !== undefined) return
859
+ cancellation = origin
860
+ // Own the group while its leader is still ours. Deferring this to
861
+ // Node's AbortError loses the wrapper-exit/stdio-close interval.
862
+ terminate('SIGTERM')
863
+ escalation = setTimeout(() => {
864
+ escalation = undefined
865
+ terminate('SIGKILL')
866
+ }, SANDBOX_KILL_GRACE_MS)
867
+ // A command that honoured SIGTERM must not leave the host alive just
868
+ // to deliver a no-op escalation. If shared stdio closes first, the
869
+ // close handler forces any remaining group before releasing its pid.
870
+ escalation.unref?.()
871
+ }
742
872
 
743
873
  child.stdout?.on('data', (chunk: Buffer) => stdout.push(chunk))
744
874
  child.stderr?.on('data', (chunk: Buffer) => stderr.push(chunk))
745
-
746
- child.on('error', (err: NodeJS.ErrnoException) => {
747
- if (err.code === 'ABORT_ERR' || signal.aborted) {
748
- // `timedOut` means the DEADLINE fired. A caller-cancelled
749
- // run is aborted but not late, and reporting it as a
750
- // timeout would tell the model to retry with a longer
751
- // budget for something a human just stopped.
752
- timedOut = ac.signal.aborted
753
- // `spawn`'s own `signal` handling already reached `child.pid`
754
- // the outermost wrapper — but that was never the process
755
- // this call exists to stop. Reach the whole tree, then give
756
- // it a grace period before forcing it.
757
- killTree(child, 'SIGTERM')
758
- setTimeout(() => killTree(child, 'SIGKILL'), SANDBOX_KILL_GRACE_MS)
759
- return
875
+ const bwrapInfo = bwrapInfoFd === undefined ? undefined : child.stdio[bwrapInfoFd]
876
+ const bwrapBlock = bwrapBlockFd === undefined ? undefined : child.stdio[bwrapBlockFd]
877
+ let bwrapInfoBuffer = ''
878
+ bwrapInfo?.on('data', (chunk: Buffer) => {
879
+ if (bwrapChildPid !== undefined) return
880
+ bwrapInfoBuffer += chunk.toString('utf8')
881
+ // `--info-fd` writes one JSON document, often pretty-printed across
882
+ // several lines and potentially split across stream chunks. Parse the
883
+ // whole accumulated document whenever more arrives.
884
+ const observed = readBwrapChildPid(bwrapInfoBuffer)
885
+ if (observed === undefined) return
886
+ bwrapChildPid = observed
887
+ // Cancellation may have won before bwrap published the inner reaper.
888
+ // Nothing authored has been admitted yet, so there is no graceful
889
+ // shutdown to preserve: kill startup atomically before it can cross the
890
+ // gate. Otherwise one byte admits the command now that it has an owner.
891
+ if (terminationSignal !== undefined) {
892
+ if (escalation !== undefined) {
893
+ clearTimeout(escalation)
894
+ escalation = undefined
895
+ }
896
+ terminate('SIGKILL')
897
+ } else if (bwrapBlock !== undefined && bwrapBlock !== null && 'end' in bwrapBlock) {
898
+ bwrapBlock.end(Buffer.from([1]))
760
899
  }
900
+ })
901
+
902
+ child.once('error', (err: NodeJS.ErrnoException) => {
903
+ if (settled) return
904
+ settled = true
905
+ finishSubscriptions()
906
+ if (escalation !== undefined) clearTimeout(escalation)
761
907
  rejectPromise(err)
762
908
  })
763
909
 
764
- child.on('close', (code, signal) => {
910
+ child.once('close', (code, signal) => {
911
+ if (settled) return
912
+ settled = true
913
+ finishSubscriptions()
914
+ if (escalation !== undefined) {
915
+ clearTimeout(escalation)
916
+ escalation = undefined
917
+ // `close` proves the wrapper and shared pipes are gone, not that a
918
+ // descendant which closed its copies has stopped. Force the old group
919
+ // now: retaining its numeric pid for a later timer risks pid reuse.
920
+ terminate('SIGKILL')
921
+ }
922
+ const timedOut = cancellation === 'deadline'
765
923
  resolvePromise({
766
924
  exitCode: code ?? (timedOut ? 124 : 1),
767
925
  stdout: stdout.text,
@@ -776,6 +934,16 @@ class LocalSandbox implements Sandbox {
776
934
  stderrTruncated: stderr.truncated,
777
935
  })
778
936
  })
937
+
938
+ // Register only after every terminal handler exists, then re-check.
939
+ // JavaScript cannot interleave an abort callback inside this synchronous
940
+ // block, and the check closes the already-aborted admission edge.
941
+ disposeAbortListeners.push(subscribeToAbort(deadlineSignal, () => cancel('deadline')))
942
+ if (callerSignal !== undefined) {
943
+ disposeAbortListeners.push(subscribeToAbort(callerSignal, () => cancel('caller')))
944
+ }
945
+ if (deadlineSignal.aborted) cancel('deadline')
946
+ else if (callerSignal?.aborted) cancel('caller')
779
947
  })
780
948
  }
781
949
  }
@@ -4,6 +4,7 @@ import type { CreateTaskOptions, TaskHandle, TaskScheduler } from '../types/agen
4
4
  import type { AgentTaskState } from '../types/agent/task.js'
5
5
  import type { RunExecutionStatus } from '../types/common/index.js'
6
6
  import type { TaskId } from '../types/ids/index.js'
7
+ import { type CancelCause, RunCancelled } from '../types/run/cancel-cause.js'
7
8
  import { asRunId, generateTaskId } from '../utils/id.js'
8
9
 
9
10
  /**
@@ -248,16 +249,16 @@ export class DelegatingTaskScheduler implements TaskScheduler {
248
249
  await entry.delegate.continue(message)
249
250
  }
250
251
 
251
- cancelTask(taskId: TaskId): void {
252
+ cancelTask(taskId: TaskId, cause?: CancelCause): void {
252
253
  const entry = this.tasks.get(taskId)
253
254
  if (!entry) {
254
- this.config.local?.cancelTask(taskId)
255
+ this.config.local?.cancelTask(taskId, cause)
255
256
  return
256
257
  }
257
258
  if (!entry.delegate.capabilities.cancel) {
258
259
  throw new DelegateCapabilityError({ id: entry.delegate.id, capability: 'cancel' })
259
260
  }
260
- controllerAbort(entry)
261
+ controllerAbort(entry, cause)
261
262
  }
262
263
 
263
264
  getTask(taskId: TaskId): TaskHandle | undefined {
@@ -292,6 +293,6 @@ export class DelegatingTaskScheduler implements TaskScheduler {
292
293
  * race that: a delegate that completed a microsecond before the abort would
293
294
  * be recorded as cancelled while its answer sat unread.
294
295
  */
295
- function controllerAbort(entry: Entry): void {
296
- entry.controller.abort()
296
+ function controllerAbort(entry: Entry, cause?: CancelCause): void {
297
+ entry.controller.abort(cause ? new RunCancelled(cause) : undefined)
297
298
  }
@@ -11,6 +11,7 @@ import type {
11
11
  import type { AgentTaskContext } from '../types/agent/task.js'
12
12
  import type { TaskId } from '../types/ids/index.js'
13
13
  import { createUserMessage } from '../types/message/index.js'
14
+ import type { CancelCause } from '../types/run/cancel-cause.js'
14
15
  import type { RunEventListener } from '../types/run/events.js'
15
16
  import { toErrorMessage } from '../utils/error.js'
16
17
  import { SCOPE_ATTRIBUTE } from '../utils/log/types.js'
@@ -244,8 +245,8 @@ export class LocalTaskScheduler implements TaskScheduler {
244
245
  await this.agentManager.continueTask(taskId, message)
245
246
  }
246
247
 
247
- cancelTask(taskId: TaskId): void {
248
- this.agentManager.cancel(taskId)
248
+ cancelTask(taskId: TaskId, cause?: CancelCause): void {
249
+ this.agentManager.cancel(taskId, cause)
249
250
  }
250
251
 
251
252
  getTask(taskId: TaskId): TaskHandle | undefined {