@namzu/sdk 32.0.0 → 33.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 (260) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/dist/authorization/gate.d.ts +1 -1
  3. package/dist/authorization/gate.js +1 -1
  4. package/dist/connector/index.d.ts +1 -1
  5. package/dist/connector/index.d.ts.map +1 -1
  6. package/dist/connector/index.js +1 -1
  7. package/dist/connector/index.js.map +1 -1
  8. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  9. package/dist/connector/mcp/adapter.js +71 -16
  10. package/dist/connector/mcp/adapter.js.map +1 -1
  11. package/dist/connector/mcp/client.d.ts.map +1 -1
  12. package/dist/connector/mcp/client.js +3 -2
  13. package/dist/connector/mcp/client.js.map +1 -1
  14. package/dist/connector/mcp/http-redirect.d.ts +17 -0
  15. package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
  16. package/dist/connector/mcp/http-redirect.js +31 -0
  17. package/dist/connector/mcp/http-redirect.js.map +1 -0
  18. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  19. package/dist/connector/mcp/http-sse.js +5 -0
  20. package/dist/connector/mcp/http-sse.js.map +1 -1
  21. package/dist/connector/mcp/image-admission.d.ts +16 -0
  22. package/dist/connector/mcp/image-admission.d.ts.map +1 -0
  23. package/dist/connector/mcp/image-admission.js +288 -0
  24. package/dist/connector/mcp/image-admission.js.map +1 -0
  25. package/dist/connector/mcp/stdio.d.ts +13 -0
  26. package/dist/connector/mcp/stdio.d.ts.map +1 -1
  27. package/dist/connector/mcp/stdio.js +66 -23
  28. package/dist/connector/mcp/stdio.js.map +1 -1
  29. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  30. package/dist/connector/mcp/streamable-http.js +3 -0
  31. package/dist/connector/mcp/streamable-http.js.map +1 -1
  32. package/dist/constants/tools/index.js +1 -1
  33. package/dist/constants/tools/index.js.map +1 -1
  34. package/dist/execution/base.d.ts +13 -0
  35. package/dist/execution/base.d.ts.map +1 -1
  36. package/dist/execution/base.js +103 -8
  37. package/dist/execution/base.js.map +1 -1
  38. package/dist/execution/errors.d.ts +12 -0
  39. package/dist/execution/errors.d.ts.map +1 -0
  40. package/dist/execution/errors.js +21 -0
  41. package/dist/execution/errors.js.map +1 -0
  42. package/dist/execution/factory.d.ts.map +1 -1
  43. package/dist/execution/factory.js +1 -0
  44. package/dist/execution/factory.js.map +1 -1
  45. package/dist/execution/hybrid.d.ts +5 -0
  46. package/dist/execution/hybrid.d.ts.map +1 -1
  47. package/dist/execution/hybrid.js +36 -16
  48. package/dist/execution/hybrid.js.map +1 -1
  49. package/dist/execution/index.d.ts +1 -0
  50. package/dist/execution/index.d.ts.map +1 -1
  51. package/dist/execution/index.js +1 -0
  52. package/dist/execution/index.js.map +1 -1
  53. package/dist/execution/local.d.ts +13 -0
  54. package/dist/execution/local.d.ts.map +1 -1
  55. package/dist/execution/local.js +145 -19
  56. package/dist/execution/local.js.map +1 -1
  57. package/dist/execution/output.d.ts +27 -0
  58. package/dist/execution/output.d.ts.map +1 -0
  59. package/dist/execution/output.js +105 -0
  60. package/dist/execution/output.js.map +1 -0
  61. package/dist/execution/remote.d.ts +26 -2
  62. package/dist/execution/remote.d.ts.map +1 -1
  63. package/dist/execution/remote.js +86 -15
  64. package/dist/execution/remote.js.map +1 -1
  65. package/dist/plugin/lifecycle.d.ts.map +1 -1
  66. package/dist/plugin/lifecycle.js +9 -3
  67. package/dist/plugin/lifecycle.js.map +1 -1
  68. package/dist/process/kill-tree.d.ts +11 -13
  69. package/dist/process/kill-tree.d.ts.map +1 -1
  70. package/dist/process/kill-tree.js +11 -13
  71. package/dist/process/kill-tree.js.map +1 -1
  72. package/dist/provider/fallback.d.ts.map +1 -1
  73. package/dist/provider/fallback.js +31 -0
  74. package/dist/provider/fallback.js.map +1 -1
  75. package/dist/provider/idle-timeout.d.ts.map +1 -1
  76. package/dist/provider/idle-timeout.js +5 -0
  77. package/dist/provider/idle-timeout.js.map +1 -1
  78. package/dist/provider/retry.d.ts.map +1 -1
  79. package/dist/provider/retry.js +5 -0
  80. package/dist/provider/retry.js.map +1 -1
  81. package/dist/public-runtime.d.ts +2 -2
  82. package/dist/public-runtime.d.ts.map +1 -1
  83. package/dist/public-runtime.js +2 -2
  84. package/dist/public-runtime.js.map +1 -1
  85. package/dist/registry/tool/execute.d.ts +7 -1
  86. package/dist/registry/tool/execute.d.ts.map +1 -1
  87. package/dist/registry/tool/execute.js +201 -54
  88. package/dist/registry/tool/execute.js.map +1 -1
  89. package/dist/run/command-gate.d.ts.map +1 -1
  90. package/dist/run/command-gate.js +11 -0
  91. package/dist/run/command-gate.js.map +1 -1
  92. package/dist/run/workspace-fingerprint.d.ts +4 -4
  93. package/dist/run/workspace-fingerprint.d.ts.map +1 -1
  94. package/dist/run/workspace-fingerprint.js +6 -4
  95. package/dist/run/workspace-fingerprint.js.map +1 -1
  96. package/dist/runtime/bidi/index.d.ts +1 -1
  97. package/dist/runtime/bidi/index.d.ts.map +1 -1
  98. package/dist/runtime/bidi/index.js +1 -1
  99. package/dist/runtime/bidi/index.js.map +1 -1
  100. package/dist/runtime/bidi/session.d.ts +19 -0
  101. package/dist/runtime/bidi/session.d.ts.map +1 -1
  102. package/dist/runtime/bidi/session.js +150 -26
  103. package/dist/runtime/bidi/session.js.map +1 -1
  104. package/dist/runtime/query/executor.d.ts +43 -1
  105. package/dist/runtime/query/executor.d.ts.map +1 -1
  106. package/dist/runtime/query/executor.js +626 -161
  107. package/dist/runtime/query/executor.js.map +1 -1
  108. package/dist/runtime/query/index.d.ts.map +1 -1
  109. package/dist/runtime/query/index.js +46 -4
  110. package/dist/runtime/query/index.js.map +1 -1
  111. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  112. package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
  113. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  114. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  115. package/dist/runtime/query/request-rich-content.js +33 -13
  116. package/dist/runtime/query/request-rich-content.js.map +1 -1
  117. package/dist/runtime/query/resume-pending.d.ts +5 -1
  118. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  119. package/dist/runtime/query/resume-pending.js +66 -1
  120. package/dist/runtime/query/resume-pending.js.map +1 -1
  121. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  122. package/dist/runtime/query/tool-output-budget.js +45 -16
  123. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  124. package/dist/runtime/query/tooling.d.ts +6 -0
  125. package/dist/runtime/query/tooling.d.ts.map +1 -1
  126. package/dist/runtime/query/tooling.js +4 -0
  127. package/dist/runtime/query/tooling.js.map +1 -1
  128. package/dist/sandbox/provider/local.d.ts.map +1 -1
  129. package/dist/sandbox/provider/local.js +193 -41
  130. package/dist/sandbox/provider/local.js.map +1 -1
  131. package/dist/scheduler/delegating.d.ts +2 -1
  132. package/dist/scheduler/delegating.d.ts.map +1 -1
  133. package/dist/scheduler/delegating.js +6 -5
  134. package/dist/scheduler/delegating.js.map +1 -1
  135. package/dist/scheduler/local.d.ts +2 -1
  136. package/dist/scheduler/local.d.ts.map +1 -1
  137. package/dist/scheduler/local.js +2 -2
  138. package/dist/scheduler/local.js.map +1 -1
  139. package/dist/session/workspace/driver.d.ts +12 -4
  140. package/dist/session/workspace/driver.d.ts.map +1 -1
  141. package/dist/session/workspace/git-worktree.d.ts +17 -3
  142. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  143. package/dist/session/workspace/git-worktree.js +153 -34
  144. package/dist/session/workspace/git-worktree.js.map +1 -1
  145. package/dist/skills/registry.d.ts +15 -0
  146. package/dist/skills/registry.d.ts.map +1 -1
  147. package/dist/skills/registry.js +33 -0
  148. package/dist/skills/registry.js.map +1 -1
  149. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  150. package/dist/tools/builtins/computer-use.js +39 -2
  151. package/dist/tools/builtins/computer-use.js.map +1 -1
  152. package/dist/tools/builtins/run-code.d.ts.map +1 -1
  153. package/dist/tools/builtins/run-code.js +9 -11
  154. package/dist/tools/builtins/run-code.js.map +1 -1
  155. package/dist/tools/builtins/skill.d.ts +2 -1
  156. package/dist/tools/builtins/skill.d.ts.map +1 -1
  157. package/dist/tools/builtins/skill.js +250 -6
  158. package/dist/tools/builtins/skill.js.map +1 -1
  159. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  160. package/dist/tools/coordinator/agent.js +100 -30
  161. package/dist/tools/coordinator/agent.js.map +1 -1
  162. package/dist/types/agent/manager.d.ts +1 -1
  163. package/dist/types/agent/manager.d.ts.map +1 -1
  164. package/dist/types/agent/scheduler.d.ts +10 -1
  165. package/dist/types/agent/scheduler.d.ts.map +1 -1
  166. package/dist/types/authorization/index.d.ts +12 -12
  167. package/dist/types/bidi/index.d.ts +19 -2
  168. package/dist/types/bidi/index.d.ts.map +1 -1
  169. package/dist/types/computer-use/index.d.ts +18 -0
  170. package/dist/types/computer-use/index.d.ts.map +1 -1
  171. package/dist/types/connector/execution-context.d.ts +3 -1
  172. package/dist/types/connector/execution-context.d.ts.map +1 -1
  173. package/dist/types/execution/index.d.ts +43 -1
  174. package/dist/types/execution/index.d.ts.map +1 -1
  175. package/dist/types/hitl/index.d.ts +11 -0
  176. package/dist/types/hitl/index.d.ts.map +1 -1
  177. package/dist/types/hitl/index.js.map +1 -1
  178. package/dist/types/message/index.d.ts +1 -1
  179. package/dist/types/message/index.d.ts.map +1 -1
  180. package/dist/types/message/index.js +2 -1
  181. package/dist/types/message/index.js.map +1 -1
  182. package/dist/types/plugin/index.d.ts +14 -1
  183. package/dist/types/plugin/index.d.ts.map +1 -1
  184. package/dist/types/plugin/index.js +1 -0
  185. package/dist/types/plugin/index.js.map +1 -1
  186. package/dist/types/provider/interface.d.ts +14 -0
  187. package/dist/types/provider/interface.d.ts.map +1 -1
  188. package/dist/types/sandbox/index.d.ts +15 -10
  189. package/dist/types/sandbox/index.d.ts.map +1 -1
  190. package/dist/types/sandbox/index.js.map +1 -1
  191. package/dist/types/tool/index.d.ts +77 -6
  192. package/dist/types/tool/index.d.ts.map +1 -1
  193. package/dist/types/tool/index.js.map +1 -1
  194. package/dist/types/workspace/ref.d.ts +5 -0
  195. package/dist/types/workspace/ref.d.ts.map +1 -1
  196. package/dist/utils/abort.d.ts +8 -0
  197. package/dist/utils/abort.d.ts.map +1 -1
  198. package/dist/utils/abort.js +16 -0
  199. package/dist/utils/abort.js.map +1 -1
  200. package/package.json +4 -4
  201. package/src/authorization/gate.ts +1 -1
  202. package/src/connector/index.ts +2 -0
  203. package/src/connector/mcp/adapter.ts +82 -21
  204. package/src/connector/mcp/client.ts +2 -1
  205. package/src/connector/mcp/http-redirect.ts +32 -0
  206. package/src/connector/mcp/http-sse.ts +5 -0
  207. package/src/connector/mcp/image-admission.ts +272 -0
  208. package/src/connector/mcp/stdio.ts +63 -21
  209. package/src/connector/mcp/streamable-http.ts +3 -0
  210. package/src/constants/tools/index.ts +1 -1
  211. package/src/execution/base.ts +124 -7
  212. package/src/execution/errors.ts +24 -0
  213. package/src/execution/factory.ts +1 -0
  214. package/src/execution/hybrid.ts +47 -16
  215. package/src/execution/index.ts +1 -0
  216. package/src/execution/local.ts +164 -19
  217. package/src/execution/output.ts +104 -0
  218. package/src/execution/remote.ts +122 -17
  219. package/src/plugin/lifecycle.ts +9 -3
  220. package/src/process/kill-tree.ts +11 -13
  221. package/src/provider/fallback.ts +39 -0
  222. package/src/provider/idle-timeout.ts +8 -0
  223. package/src/provider/retry.ts +8 -0
  224. package/src/public-runtime.ts +7 -1
  225. package/src/registry/tool/execute.ts +230 -64
  226. package/src/run/command-gate.ts +12 -0
  227. package/src/run/workspace-fingerprint.ts +5 -4
  228. package/src/runtime/bidi/index.ts +1 -1
  229. package/src/runtime/bidi/session.ts +177 -28
  230. package/src/runtime/query/executor.ts +769 -163
  231. package/src/runtime/query/index.ts +53 -6
  232. package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
  233. package/src/runtime/query/request-rich-content.ts +39 -14
  234. package/src/runtime/query/resume-pending.ts +83 -1
  235. package/src/runtime/query/tool-output-budget.ts +52 -17
  236. package/src/runtime/query/tooling.ts +10 -0
  237. package/src/sandbox/provider/local.ts +213 -45
  238. package/src/scheduler/delegating.ts +6 -5
  239. package/src/scheduler/local.ts +3 -2
  240. package/src/session/workspace/driver.ts +12 -4
  241. package/src/session/workspace/git-worktree.ts +173 -34
  242. package/src/skills/registry.ts +47 -0
  243. package/src/tools/builtins/computer-use.ts +45 -2
  244. package/src/tools/builtins/run-code.ts +9 -11
  245. package/src/tools/builtins/skill.ts +325 -7
  246. package/src/tools/coordinator/agent.ts +108 -33
  247. package/src/types/agent/manager.ts +1 -1
  248. package/src/types/agent/scheduler.ts +10 -1
  249. package/src/types/bidi/index.ts +19 -2
  250. package/src/types/computer-use/index.ts +19 -0
  251. package/src/types/connector/execution-context.ts +3 -0
  252. package/src/types/execution/index.ts +46 -1
  253. package/src/types/hitl/index.ts +11 -0
  254. package/src/types/message/index.ts +3 -2
  255. package/src/types/plugin/index.ts +15 -0
  256. package/src/types/provider/interface.ts +18 -0
  257. package/src/types/sandbox/index.ts +15 -10
  258. package/src/types/tool/index.ts +85 -6
  259. package/src/types/workspace/ref.ts +5 -0
  260. package/src/utils/abort.ts +18 -0
@@ -14,6 +14,15 @@ import { generateToolCallId } from '../../utils/id.js';
14
14
  import { compressShellOutput } from '../../utils/shell-compress.js';
15
15
  import { bindOwner } from '../jobs/registry.js';
16
16
  import { DEFAULT_MAX_TOOL_OUTPUT_CHARS, applyToolOutputBudget, describeDroppedContent, measureContentBytes, } from './tool-output-budget.js';
17
+ function assertUniqueToolCallIds(toolCalls) {
18
+ const seen = new Set();
19
+ for (const [index, toolCall] of toolCalls.entries()) {
20
+ if (seen.has(toolCall.id)) {
21
+ throw new Error(`Provider returned duplicate tool call id "${toolCall.id}" at batch index ${index}; the batch is refused because review, denial and result ownership require one unique id per call.`);
22
+ }
23
+ seen.add(toolCall.id);
24
+ }
25
+ }
17
26
  /**
18
27
  * Default per-tool deadline. Long enough for a real build or test run,
19
28
  * short enough that a wedged tool does not hold a turn open indefinitely.
@@ -183,6 +192,7 @@ export class ToolExecutor {
183
192
  workingStateManager;
184
193
  probes;
185
194
  parentSpan;
195
+ preparedBatches = new WeakSet();
186
196
  /** Set per turn by the orchestrator; see {@link setStepAllowedTools}. */
187
197
  stepAllowedTools;
188
198
  readPaths = new Set();
@@ -319,17 +329,72 @@ export class ToolExecutor {
319
329
  const configured = this.config.permissionMode;
320
330
  return typeof configured === 'function' ? configured() : configured;
321
331
  }
322
- async executeBatch(response, denials, prior) {
332
+ /** Evaluate the run's operator policy against one already-prepared value. */
333
+ evaluatePreparedAuthorization(toolName, input) {
334
+ return this.config.authorizationGate?.evaluate({
335
+ toolName,
336
+ toolInput: input,
337
+ toolDef: this.config.tools.get(toolName),
338
+ });
339
+ }
340
+ /**
341
+ * Resolve repairs and pre-tool hooks, then decode each call exactly once.
342
+ * The returned projection is what policy and a human review; execution later
343
+ * consumes the registry-owned preparations rather than parsing again.
344
+ */
345
+ async prepareBatchForReview(response) {
346
+ assertUniqueToolCallIds(response.message.toolCalls ?? []);
347
+ const calls = new Map();
348
+ for (const toolCall of response.message.toolCalls ?? []) {
349
+ calls.set(toolCall.id, await this.prepareDirectCall(toolCall));
350
+ }
351
+ return this.publishPreparedBatch(calls);
352
+ }
353
+ /** Re-prepare only calls whose raw input a reviewer actually changed. */
354
+ async reprepareBatchForReview(response, previous, changedCallIds) {
355
+ assertUniqueToolCallIds(response.message.toolCalls ?? []);
356
+ if (!this.preparedBatches.has(previous)) {
357
+ throw new Error('Prepared tool batch is not owned by this executor.');
358
+ }
359
+ const calls = new Map(previous.calls);
360
+ for (const toolCall of response.message.toolCalls ?? []) {
361
+ if (changedCallIds.has(toolCall.id)) {
362
+ calls.set(toolCall.id, await this.prepareDirectCall(toolCall));
363
+ }
364
+ }
365
+ return this.publishPreparedBatch(calls);
366
+ }
367
+ publishPreparedBatch(calls) {
368
+ const reviewCalls = [...calls.values()]
369
+ .filter((call) => call.kind !== 'synthetic')
370
+ .map((call) => ({
371
+ id: call.toolCall.id,
372
+ name: call.toolName,
373
+ input: call.input,
374
+ }));
375
+ const batch = Object.freeze({
376
+ reviewCalls: Object.freeze(reviewCalls),
377
+ calls: new Map(calls),
378
+ });
379
+ this.preparedBatches.add(batch);
380
+ return batch;
381
+ }
382
+ async executeBatch(response, denials, prior, preparedBatch) {
323
383
  const toolCalls = response.message.toolCalls;
324
384
  if (!toolCalls) {
325
385
  return { messages: [], results: [], observations: [] };
326
386
  }
387
+ assertUniqueToolCallIds(toolCalls);
327
388
  this.batchCounter += 1;
328
389
  // Sampled here, once, and held for every call below. See the note on
329
390
  // `permissionMode` in the config type.
330
391
  this.batchMode = this.resolvePermissionMode();
331
392
  try {
332
- return await this.runBatch(toolCalls, denials, prior);
393
+ const owned = preparedBatch;
394
+ if (owned && !this.preparedBatches.has(owned)) {
395
+ throw new Error('Prepared tool batch is not owned by this executor.');
396
+ }
397
+ return await this.runBatch(toolCalls, denials, prior, owned);
333
398
  }
334
399
  finally {
335
400
  // Cleared so a later single execution outside a batch resolves
@@ -337,7 +402,7 @@ export class ToolExecutor {
337
402
  this.batchMode = undefined;
338
403
  }
339
404
  }
340
- async runBatch(toolCalls, denials, prior) {
405
+ async runBatch(toolCalls, denials, prior, preparedBatch) {
341
406
  this.log.debug('Executing tool batch', {
342
407
  [NAMZU.RUN_ID]: this.config.runId,
343
408
  'namzu.runtime.tool_count': toolCalls.length,
@@ -369,6 +434,7 @@ export class ToolExecutor {
369
434
  // order independence.
370
435
  const gate = new Semaphore(this.config.maxToolConcurrency ?? DEFAULT_TOOL_CONCURRENCY);
371
436
  toolCalls.forEach((toolCall, i) => {
437
+ const preparedCall = preparedBatch?.calls.get(toolCall.id);
372
438
  const recovered = prior?.get(toolCall.id);
373
439
  if (recovered !== undefined) {
374
440
  // This call already ran, in a process that died before the
@@ -389,7 +455,7 @@ export class ToolExecutor {
389
455
  // message so the assistant turn stays fully answered. Run them
390
456
  // on the parallel branch — they perform no side effects, so
391
457
  // serialization would only add latency.
392
- parallel.push(this.recordDenial(toolCall, denialReason).then((r) => {
458
+ parallel.push(this.recordDenial(toolCall, denialReason, preparedCall).then((r) => {
393
459
  results[i] = r;
394
460
  }));
395
461
  return;
@@ -419,7 +485,7 @@ export class ToolExecutor {
419
485
  };
420
486
  const run = async () => {
421
487
  try {
422
- results[i] = await this.executeSingle(toolCall, ctx, recordObservation, () => progress.close());
488
+ results[i] = await this.executeSingle(toolCall, ctx, recordObservation, () => progress.close(), preparedCall);
423
489
  }
424
490
  finally {
425
491
  await progress.close();
@@ -434,14 +500,19 @@ export class ToolExecutor {
434
500
  gate.release();
435
501
  }
436
502
  };
437
- let input = {};
503
+ let input = preparedCall?.input ?? {};
438
504
  try {
439
- input = JSON.parse(toolCall.function.arguments || '{}');
505
+ if (!preparedBatch?.calls.has(toolCall.id)) {
506
+ input = JSON.parse(toolCall.function.arguments || '{}');
507
+ }
440
508
  }
441
509
  catch {
442
510
  // non-JSON args → treat as unsafe (serialize), the conservative path
443
511
  }
444
- const safe = this.config.tools.get(toolCall.function.name)?.isConcurrencySafe?.(input) === true;
512
+ const preparedToolName = preparedCall?.toolName;
513
+ const safe = this.config.tools
514
+ .get(preparedToolName ?? toolCall.function.name)
515
+ ?.isConcurrencySafe?.(input) === true;
445
516
  if (safe)
446
517
  parallel.push(gated());
447
518
  else
@@ -494,6 +565,16 @@ export class ToolExecutor {
494
565
  * eleven children.
495
566
  */
496
567
  async dispatchNested(name, input, context, recordObservation, parentToolName, options) {
568
+ const signal = options?.signal
569
+ ? AbortSignal.any([context.abortSignal, options.signal])
570
+ : context.abortSignal;
571
+ // Authority is checked before an id, activity or event is created. A
572
+ // retained closure must be observationally inert after its invocation
573
+ // ends, not merely unable to finish the registry call it already started.
574
+ signal.throwIfAborted();
575
+ const preparedCall = await this.prepareNestedCall(name, input, signal);
576
+ signal.throwIfAborted();
577
+ const preparedInput = preparedCall.input;
497
578
  const parent = context.toolUseId;
498
579
  const via = parent && parentToolName
499
580
  ? {
@@ -508,9 +589,6 @@ export class ToolExecutor {
508
589
  // it rather than to itself.
509
590
  const nestedId = generateToolCallId();
510
591
  const startedAt = Date.now();
511
- const signal = options?.signal
512
- ? AbortSignal.any([context.abortSignal, options.signal])
513
- : context.abortSignal;
514
592
  const source = parent
515
593
  ? options?.runtimeToolCallId
516
594
  ? {
@@ -526,6 +604,74 @@ export class ToolExecutor {
526
604
  toolUseId: nestedId,
527
605
  toolName: name,
528
606
  });
607
+ if (preparedCall.kind === 'synthetic') {
608
+ await this.emitEvent({
609
+ type: 'tool_executing',
610
+ runId: this.config.runId,
611
+ toolUseId: nestedId,
612
+ toolName: name,
613
+ input: preparedInput,
614
+ ...(via ? { via } : {}),
615
+ });
616
+ await progress.close();
617
+ await this.emitEvent({
618
+ type: 'tool_completed',
619
+ runId: this.config.runId,
620
+ toolUseId: nestedId,
621
+ toolName: name,
622
+ result: preparedCall.message,
623
+ isError: preparedCall.isError,
624
+ durationMs: Date.now() - startedAt,
625
+ outputLength: preparedCall.message.length,
626
+ ...(via ? { via } : {}),
627
+ });
628
+ return preparedCall.isError
629
+ ? { success: false, output: '', error: preparedCall.message }
630
+ : { success: true, output: preparedCall.message };
631
+ }
632
+ const gateResult = this.config.authorizationGate?.evaluate({
633
+ toolName: name,
634
+ toolInput: preparedInput,
635
+ toolDef: this.config.tools.get(name),
636
+ });
637
+ if (gateResult && gateResult.decision !== 'allow') {
638
+ const reason = gateResult.decision === 'deny'
639
+ ? `Blocked by the authorization gate: ${gateResult.reason}`
640
+ : `Blocked by the authorization gate: this nested call requires an explicit allow rule because an operator review cannot be opened from inside another tool. ${gateResult.reason}`;
641
+ const output = deniedToolOutput(name, reason);
642
+ // Same fail-closed durability rule as a direct gate denial: if the
643
+ // configured run store cannot record the refusal, do not quietly carry
644
+ // on with an unaudited execution.
645
+ if (!this.config.recordAudit) {
646
+ throw new Error(`Nested tool "${name}" was refused, but no durable audit recorder is configured.`);
647
+ }
648
+ await this.config.recordAudit({
649
+ what: { action: 'tool_call', tool: name },
650
+ outcome: 'refused',
651
+ reason,
652
+ });
653
+ await progress.close();
654
+ await this.emitEvent({
655
+ type: 'tool_executing',
656
+ runId: this.config.runId,
657
+ toolUseId: nestedId,
658
+ toolName: name,
659
+ input: preparedInput,
660
+ ...(via ? { via } : {}),
661
+ });
662
+ await this.emitEvent({
663
+ type: 'tool_completed',
664
+ runId: this.config.runId,
665
+ toolUseId: nestedId,
666
+ toolName: name,
667
+ result: output,
668
+ isError: true,
669
+ durationMs: Date.now() - startedAt,
670
+ outputLength: output.length,
671
+ ...(via ? { via } : {}),
672
+ });
673
+ return { success: false, output: '', error: reason };
674
+ }
529
675
  const childContext = {
530
676
  ...context,
531
677
  abortSignal: signal,
@@ -545,15 +691,41 @@ export class ToolExecutor {
545
691
  runId: this.config.runId,
546
692
  toolUseId: nestedId,
547
693
  toolName: name,
548
- input,
694
+ input: preparedInput,
549
695
  ...(via ? { via } : {}),
550
696
  });
697
+ const vetoOutcome = this.probes.queryVeto({
698
+ type: 'tool_executing',
699
+ runId: this.config.runId,
700
+ toolUseId: nestedId,
701
+ toolName: name,
702
+ input: preparedInput,
703
+ ...(via ? { via } : {}),
704
+ }, buildProbeContext({ runId: this.config.runId }));
705
+ if (vetoOutcome.action === 'deny') {
706
+ const probeName = vetoOutcome.probeName ?? 'unnamed';
707
+ const reason = vetoOutcome.reason ?? 'no reason provided';
708
+ const message = new ProbeVetoError(probeName, reason, 'tool_executing').message;
709
+ await progress.close();
710
+ await this.emitEvent({
711
+ type: 'tool_completed',
712
+ runId: this.config.runId,
713
+ toolUseId: nestedId,
714
+ toolName: name,
715
+ result: `Error: ${message}`,
716
+ isError: true,
717
+ durationMs: Date.now() - startedAt,
718
+ outputLength: message.length + 7,
719
+ ...(via ? { via } : {}),
720
+ });
721
+ return { success: false, output: '', error: message };
722
+ }
551
723
  let result;
552
724
  try {
553
725
  // Use the same deadline layer as a model-issued call. Calling the
554
726
  // registry directly made nested tools the only tools whose own
555
727
  // `timeoutMs` declaration was ignored.
556
- result = await this.runOnce(name, input, childContext);
728
+ result = await this.runOnce(name, preparedInput, childContext, preparedCall.kind === 'ready' ? preparedCall.prepared : undefined);
557
729
  }
558
730
  finally {
559
731
  await progress.close();
@@ -600,7 +772,7 @@ export class ToolExecutor {
600
772
  runId: this.config.runId,
601
773
  toolUseId: nestedId,
602
774
  toolName: name,
603
- input,
775
+ input: preparedInput,
604
776
  result,
605
777
  ...(parent ? { parentToolUseId: parent } : {}),
606
778
  });
@@ -629,6 +801,7 @@ export class ToolExecutor {
629
801
  adoptSkillScope: (scope) => {
630
802
  this.skillScope = { ...scope, adoptedInBatch: this.batchCounter };
631
803
  },
804
+ maxToolOutputChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
632
805
  ...(this.config.skills ? { skills: this.config.skills } : {}),
633
806
  ...(this.config.web ? { web: this.config.web } : {}),
634
807
  // The SAME registry and the SAME context a model-issued call
@@ -664,90 +837,101 @@ export class ToolExecutor {
664
837
  };
665
838
  return context;
666
839
  }
667
- async executeSingle(toolCall, toolContext, recordObservation, settleProgress) {
668
- let toolName = toolCall.function.name;
669
- // A stream that cut off mid-JSON is the case `repairToolCall` exists
670
- // for, and it used to be the one case that never reached it: this
671
- // branch returned before `resolveCall` ran, so the motivating failure
672
- // was answered with a generic hint while the configured repairer sat
673
- // unused. Offer it the partial buffer first.
674
- const truncationRepair = toolCall.metadata?.inputTruncated === true
675
- ? await this.repairTruncatedCall(toolCall, toolName)
676
- : null;
677
- if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
678
- const message = truncatedToolInputMessage(toolName);
679
- await this.emitEvent({
680
- type: 'tool_executing',
681
- runId: this.config.runId,
682
- toolUseId: toolCall.id,
683
- toolName,
684
- input: {},
685
- });
686
- await this.emitEvent({
687
- type: 'tool_completed',
688
- runId: this.config.runId,
689
- toolUseId: toolCall.id,
690
- toolName,
691
- result: message,
692
- isError: true,
693
- });
694
- return {
695
- toolCallId: toolCall.id,
696
- toolName,
697
- output: message,
698
- isError: true,
699
- };
840
+ async executeSingle(toolCall, toolContext, recordObservation, settleProgress, preparedCall) {
841
+ if (preparedCall?.kind === 'synthetic') {
842
+ return this.recordSyntheticPreparation(preparedCall);
700
843
  }
701
- // A malformed call used to cost a full model round trip to fix: the
702
- // error went back as a `tool_result`, the model re-read the whole
703
- // context and tried again. A host that can repair it locally turns
704
- // that into nothing. No-op when no repairer is configured.
705
- const resolved = await this.resolveCall(truncationRepair
706
- ? {
707
- ...toolCall,
708
- function: {
709
- ...toolCall.function,
710
- name: truncationRepair.toolName ?? toolName,
711
- arguments: truncationRepair.arguments,
712
- },
713
- metadata: {},
714
- }
715
- : toolCall);
716
- toolName = resolved.toolName;
717
- if (!resolved.ok) {
718
- // malformed JSON args used to return without ever
719
- // emitting tool_executing or tool_completed, leaving UI cards
720
- // orphaned in `streaming_input`. Emit the executing→completed
721
- // terminal pair so the card lifecycle closes.
722
- const message = resolved.message;
723
- await this.emitEvent({
724
- type: 'tool_executing',
725
- runId: this.config.runId,
726
- toolUseId: toolCall.id,
727
- toolName,
728
- input: {},
729
- });
730
- await this.emitEvent({
731
- type: 'tool_completed',
732
- runId: this.config.runId,
733
- toolUseId: toolCall.id,
734
- toolName,
735
- result: message,
736
- isError: true,
737
- });
738
- return {
739
- toolCallId: toolCall.id,
740
- toolName,
741
- output: message,
742
- isError: true,
743
- };
844
+ let toolName = preparedCall?.toolName ?? toolCall.function.name;
845
+ let input;
846
+ let prepared;
847
+ if (preparedCall?.kind === 'ready' || preparedCall?.kind === 'legacy') {
848
+ input = preparedCall.input;
849
+ prepared = preparedCall.kind === 'ready' ? preparedCall.prepared : undefined;
744
850
  }
745
- let input = resolved.input;
746
- const preOutcome = await this.runPreToolHook(toolName, input);
747
- if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
748
- return this.recordSyntheticHookOutcome(toolCall.id, toolName, preOutcome.input, preOutcome);
851
+ else {
852
+ // A stream that cut off mid-JSON is the case `repairToolCall` exists
853
+ // for, and it used to be the one case that never reached it: this
854
+ // branch returned before `resolveCall` ran, so the motivating failure
855
+ // was answered with a generic hint while the configured repairer sat
856
+ // unused. Offer it the partial buffer first.
857
+ const truncationRepair = toolCall.metadata?.inputTruncated === true
858
+ ? await this.repairTruncatedCall(toolCall, toolName)
859
+ : null;
860
+ if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
861
+ const message = truncatedToolInputMessage(toolName);
862
+ await this.emitEvent({
863
+ type: 'tool_executing',
864
+ runId: this.config.runId,
865
+ toolUseId: toolCall.id,
866
+ toolName,
867
+ input: {},
868
+ });
869
+ await this.emitEvent({
870
+ type: 'tool_completed',
871
+ runId: this.config.runId,
872
+ toolUseId: toolCall.id,
873
+ toolName,
874
+ result: message,
875
+ isError: true,
876
+ });
877
+ return {
878
+ toolCallId: toolCall.id,
879
+ toolName,
880
+ output: message,
881
+ isError: true,
882
+ };
883
+ }
884
+ // A malformed call used to cost a full model round trip to fix: the
885
+ // error went back as a `tool_result`, the model re-read the whole
886
+ // context and tried again. A host that can repair it locally turns
887
+ // that into nothing. No-op when no repairer is configured.
888
+ const resolved = await this.resolveCall(truncationRepair
889
+ ? {
890
+ ...toolCall,
891
+ function: {
892
+ ...toolCall.function,
893
+ name: truncationRepair.toolName ?? toolName,
894
+ arguments: truncationRepair.arguments,
895
+ },
896
+ metadata: {},
897
+ }
898
+ : toolCall);
899
+ toolName = resolved.toolName;
900
+ if (!resolved.ok) {
901
+ // malformed JSON args used to return without ever
902
+ // emitting tool_executing or tool_completed, leaving UI cards
903
+ // orphaned in `streaming_input`. Emit the executing→completed
904
+ // terminal pair so the card lifecycle closes.
905
+ const message = resolved.message;
906
+ await this.emitEvent({
907
+ type: 'tool_executing',
908
+ runId: this.config.runId,
909
+ toolUseId: toolCall.id,
910
+ toolName,
911
+ input: {},
912
+ });
913
+ await this.emitEvent({
914
+ type: 'tool_completed',
915
+ runId: this.config.runId,
916
+ toolUseId: toolCall.id,
917
+ toolName,
918
+ result: message,
919
+ isError: true,
920
+ });
921
+ return {
922
+ toolCallId: toolCall.id,
923
+ toolName,
924
+ output: message,
925
+ isError: true,
926
+ };
927
+ }
928
+ input = resolved.input;
929
+ const preOutcome = await this.runPreToolHook(toolName, input);
930
+ if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
931
+ return this.recordSyntheticHookOutcome(toolCall.id, toolName, preOutcome.input, preOutcome);
932
+ }
933
+ input = preOutcome.input;
749
934
  }
750
- input = preOutcome.input;
751
935
  const activity = this.activityStore.create({
752
936
  type: 'tool_call',
753
937
  description: toolName,
@@ -827,7 +1011,7 @@ export class ToolExecutor {
827
1011
  // version silently DROPPED `content`, so a tool returning an image
828
1012
  // block had it discarded here — before the wire mapper that was
829
1013
  // built to carry it ever saw it.
830
- let result = await this.runOnce(toolName, input, toolContext);
1014
+ let result = await this.runOnce(toolName, input, toolContext, prepared);
831
1015
  let post = await this.runPostToolHook(toolName, input, result);
832
1016
  // In-loop retry. A transient failure used to cost a full model round
833
1017
  // trip: the error went back as a `tool_result`, the model read it and
@@ -894,22 +1078,38 @@ export class ToolExecutor {
894
1078
  // resume to trip over.
895
1079
  break;
896
1080
  }
897
- result = await this.runOnce(toolName, input, toolContext);
1081
+ result = await this.runOnce(toolName, input, toolContext, prepared);
898
1082
  post = await this.runPostToolHook(toolName, input, result);
899
1083
  }
900
1084
  const durationMs = Date.now() - startMs;
901
1085
  const rawOutput = result.success
902
1086
  ? result.output
903
1087
  : formatFailedToolOutput(result.output, result.error);
904
- let output = result.success ? this.maybeCompress(toolName, rawOutput) : rawOutput;
1088
+ const postOverride = post.override;
1089
+ let output = postOverride?.output ?? (result.success ? this.maybeCompress(toolName, rawOutput) : rawOutput);
1090
+ const selectedContent = postOverride?.isError
1091
+ ? undefined
1092
+ : (postOverride?.content ?? result.content);
1093
+ const maxToolOutputChars = this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS;
1094
+ // If the text preview forces rich content out, say so inside the SAME
1095
+ // budget. Appending this after truncation made the diagnostic itself a
1096
+ // cap bypass; selecting a post-hook override after truncation was a
1097
+ // larger bypass that could replace a bounded result with anything.
1098
+ if (maxToolOutputChars > 0 &&
1099
+ output.length > maxToolOutputChars &&
1100
+ selectedContent !== undefined) {
1101
+ const dropped = describeDroppedContent(selectedContent);
1102
+ if (dropped)
1103
+ output = `${output}\n\n${dropped}`;
1104
+ }
905
1105
  // Compression is opportunistic and shell-only; the budget is the
906
- // hard bound that applies to every tool. Runs after compression so
907
- // a result that shrinks under the cap is never spilled needlessly.
1106
+ // hard bound that applies to every final tool result, including a
1107
+ // post-tool hook's replacement and the rich-content omission notice.
908
1108
  const budgeted = applyToolOutputBudget({
909
1109
  toolName,
910
1110
  toolUseId: toolCall.id,
911
1111
  output,
912
- maxChars: this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS,
1112
+ maxChars: maxToolOutputChars,
913
1113
  spillDir: this.config.toolOutputDir,
914
1114
  onError: (message) => this.log.warn('Failed to spill oversized tool output', {
915
1115
  [NAMZU.RUN_ID]: this.config.runId,
@@ -926,21 +1126,6 @@ export class ToolExecutor {
926
1126
  });
927
1127
  }
928
1128
  output = budgeted.output;
929
- // A truncated text half drops the rich half with it — the preview is
930
- // no longer the tool's own payload, so an image alongside it would be
931
- // illustrating something the model can no longer read. Dropping is
932
- // right; doing it SILENTLY is not. The model saw a preview and had no
933
- // way to know an image existed at all, so it reasoned as though the
934
- // tool had returned text only.
935
- if (budgeted.truncated && result.content !== undefined) {
936
- const dropped = describeDroppedContent(result.content);
937
- if (dropped)
938
- output = `${output}\n\n${dropped}`;
939
- }
940
- const postOverride = post.override;
941
- if (postOverride !== null) {
942
- output = postOverride.output;
943
- }
944
1129
  // A failed call, or an override that says the call failed. A `replace`
945
1130
  // says the opposite, and reading it as a failure is what made redaction
946
1131
  // unusable: the model was told a successful call had gone wrong, and
@@ -1000,14 +1185,9 @@ export class ToolExecutor {
1000
1185
  result,
1001
1186
  });
1002
1187
  const resolveContent = () => {
1003
- if (postOverride?.content !== undefined) {
1004
- return { content: this.budgetContent(postOverride.content, toolName) };
1005
- }
1006
- if (postOverride?.isError)
1188
+ if (budgeted.truncated || selectedContent === undefined)
1007
1189
  return {};
1008
- if (budgeted.truncated || result.content === undefined)
1009
- return {};
1010
- return { content: this.budgetContent(result.content, toolName) };
1190
+ return { content: this.budgetContent(selectedContent, toolName) };
1011
1191
  };
1012
1192
  return {
1013
1193
  toolCallId: toolCall.id,
@@ -1047,13 +1227,10 @@ export class ToolExecutor {
1047
1227
  * model sees "this timed out" as a `tool_result` and can route around
1048
1228
  * it. A throw would end the run over one slow tool.
1049
1229
  */
1050
- async executeWithDeadline(toolName, input, toolContext) {
1230
+ async executeWithDeadline(toolName, input, toolContext, prepared) {
1051
1231
  const timeoutMs = this.config.tools.get(toolName)?.timeoutMs ??
1052
1232
  this.config.toolTimeoutMs ??
1053
1233
  DEFAULT_TOOL_TIMEOUT_MS;
1054
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
1055
- return this.config.tools.execute(toolName, input, toolContext);
1056
- }
1057
1234
  const controller = new AbortController();
1058
1235
  const parentSignal = toolContext.abortSignal;
1059
1236
  const onParentAbort = () => controller.abort(parentSignal.reason);
@@ -1063,14 +1240,18 @@ export class ToolExecutor {
1063
1240
  parentSignal.addEventListener('abort', onParentAbort, { once: true });
1064
1241
  let timer;
1065
1242
  let timedOut = false;
1243
+ let invocationOpen = true;
1244
+ const nestedDispatches = new Set();
1066
1245
  try {
1067
- const expired = new Promise((resolve) => {
1068
- timer = setTimeout(() => {
1069
- timedOut = true;
1070
- controller.abort(new Error(`Tool "${toolName}" exceeded ${timeoutMs}ms`));
1071
- resolve('timeout');
1072
- }, timeoutMs);
1073
- });
1246
+ const expired = Number.isFinite(timeoutMs) && timeoutMs > 0
1247
+ ? new Promise((resolve) => {
1248
+ timer = setTimeout(() => {
1249
+ timedOut = true;
1250
+ controller.abort(new Error(`Tool "${toolName}" exceeded ${timeoutMs}ms`));
1251
+ resolve('timeout');
1252
+ }, timeoutMs);
1253
+ })
1254
+ : undefined;
1074
1255
  const aborted = new Promise((resolve) => {
1075
1256
  if (controller.signal.aborted && !timedOut) {
1076
1257
  resolve('aborted');
@@ -1082,24 +1263,46 @@ export class ToolExecutor {
1082
1263
  }, { once: true });
1083
1264
  });
1084
1265
  const inheritedDispatch = toolContext.dispatchTool;
1085
- const execution = this.config.tools.execute(toolName, input, {
1266
+ const scopedDispatch = inheritedDispatch
1267
+ ? (name, nestedInput, options) => {
1268
+ if (!invocationOpen) {
1269
+ return Promise.reject(new Error(`Tool "${toolName}" invocation has settled; nested dispatch is closed.`));
1270
+ }
1271
+ const nested = inheritedDispatch(name, nestedInput, {
1272
+ ...options,
1273
+ signal: options?.signal
1274
+ ? AbortSignal.any([controller.signal, options.signal])
1275
+ : controller.signal,
1276
+ });
1277
+ nestedDispatches.add(nested);
1278
+ // `finally()` creates a second promise. Observe that promise too, or a
1279
+ // rejected nested call which its owner intentionally awaits later would
1280
+ // also create an unhandled cleanup rejection here.
1281
+ void nested.finally(() => nestedDispatches.delete(nested)).catch(() => { });
1282
+ return nested;
1283
+ }
1284
+ : undefined;
1285
+ if (controller.signal.aborted) {
1286
+ return {
1287
+ success: false,
1288
+ output: '',
1289
+ error: abortReasonText(controller.signal.reason)
1290
+ ? `Tool "${toolName}" was cancelled: ${abortReasonText(controller.signal.reason)}`
1291
+ : `Tool "${toolName}" was cancelled.`,
1292
+ };
1293
+ }
1294
+ const context = {
1086
1295
  ...toolContext,
1087
1296
  abortSignal: controller.signal,
1088
- ...(inheritedDispatch
1089
- ? {
1090
- dispatchTool: (name, nestedInput, options) => inheritedDispatch(name, nestedInput, {
1091
- ...options,
1092
- signal: options?.signal
1093
- ? AbortSignal.any([controller.signal, options.signal])
1094
- : controller.signal,
1095
- }),
1096
- }
1097
- : {}),
1098
- });
1297
+ ...(scopedDispatch ? { dispatchTool: scopedDispatch } : {}),
1298
+ };
1299
+ const execution = prepared
1300
+ ? this.config.tools.executePrepared(prepared, context)
1301
+ : this.config.tools.execute(toolName, input, context);
1099
1302
  // The loser of the race may still reject later; neutralize it so
1100
1303
  // it is never an unhandled rejection.
1101
1304
  execution.catch(() => { });
1102
- const outcome = await Promise.race([execution, expired, aborted]);
1305
+ const outcome = await Promise.race(expired ? [execution, expired, aborted] : [execution, aborted]);
1103
1306
  if (outcome === 'timeout') {
1104
1307
  this.log.warn('Tool timed out', {
1105
1308
  [NAMZU.RUN_ID]: this.config.runId,
@@ -1131,30 +1334,284 @@ export class ToolExecutor {
1131
1334
  return outcome;
1132
1335
  }
1133
1336
  finally {
1337
+ // Revoke synchronously before awaiting anything. A retained closure
1338
+ // called from another microtask is refused here; the aborted signal is
1339
+ // the structural backstop inside dispatchNested itself.
1340
+ invocationOpen = false;
1341
+ if (!controller.signal.aborted) {
1342
+ controller.abort(new Error(`Tool "${toolName}" invocation has settled.`));
1343
+ }
1344
+ // Calls already admitted before closure own event rows and registry
1345
+ // work. Their executor races observe the abort, emit a terminal result,
1346
+ // and settle before the parent is allowed to report completion.
1347
+ while (nestedDispatches.size > 0) {
1348
+ await Promise.allSettled([...nestedDispatches]);
1349
+ }
1134
1350
  if (timer !== undefined)
1135
1351
  clearTimeout(timer);
1136
1352
  parentSignal.removeEventListener('abort', onParentAbort);
1137
1353
  }
1138
1354
  }
1139
- async runPreToolHook(toolName, input) {
1355
+ async runPreToolHook(toolName, input, signal = this.config.abortSignal) {
1140
1356
  if (!this.config.pluginManager)
1141
- return { kind: 'continue', input };
1357
+ return { kind: 'continue', input, modified: false };
1142
1358
  const results = await this.config.pluginManager.executeHooks('pre_tool_use', {
1143
1359
  runId: this.config.runId,
1144
1360
  toolName,
1145
1361
  toolInput: input,
1146
- signal: this.config.abortSignal,
1362
+ signal,
1147
1363
  }, this.emitEvent);
1148
1364
  return this.interpretPreToolResults(toolName, input, results);
1149
1365
  }
1366
+ async prepareNestedCall(toolName, input, signal) {
1367
+ const prepare = this.config.tools.prepareExecution;
1368
+ const executePrepared = this.config.tools.executePrepared;
1369
+ if (typeof prepare !== 'function' || typeof executePrepared !== 'function') {
1370
+ if (this.config.authorizationGate) {
1371
+ return {
1372
+ kind: 'synthetic',
1373
+ input,
1374
+ message: `Tool "${toolName}" was not executed because its registry cannot bind authorization to one prepared input.`,
1375
+ isError: true,
1376
+ };
1377
+ }
1378
+ const preOutcome = await this.runPreToolHook(toolName, input, signal);
1379
+ if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
1380
+ return {
1381
+ kind: 'synthetic',
1382
+ input: preOutcome.input,
1383
+ message: preOutcome.output,
1384
+ isError: preOutcome.kind === 'error',
1385
+ };
1386
+ }
1387
+ return { kind: 'legacy', input: preOutcome.input };
1388
+ }
1389
+ let preparation;
1390
+ try {
1391
+ preparation = prepare.call(this.config.tools, toolName, input);
1392
+ }
1393
+ catch (err) {
1394
+ return {
1395
+ kind: 'synthetic',
1396
+ input,
1397
+ message: `Tool "${toolName}" could not be prepared: ${toErrorMessage(err)}`,
1398
+ isError: true,
1399
+ };
1400
+ }
1401
+ if (!preparation.success) {
1402
+ return {
1403
+ kind: 'synthetic',
1404
+ input,
1405
+ message: formatFailedToolOutput(preparation.result.output, preparation.result.error),
1406
+ isError: true,
1407
+ };
1408
+ }
1409
+ const preOutcome = await this.runPreToolHook(toolName, preparation.prepared.input, signal);
1410
+ if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
1411
+ return {
1412
+ kind: 'synthetic',
1413
+ input: preOutcome.input,
1414
+ message: preOutcome.output,
1415
+ isError: preOutcome.kind === 'error',
1416
+ };
1417
+ }
1418
+ if (!preOutcome.modified) {
1419
+ return {
1420
+ kind: 'ready',
1421
+ input: preparation.prepared.input,
1422
+ prepared: preparation.prepared,
1423
+ };
1424
+ }
1425
+ const modified = prepare.call(this.config.tools, toolName, preOutcome.input);
1426
+ if (!modified.success) {
1427
+ return {
1428
+ kind: 'synthetic',
1429
+ input: preOutcome.input,
1430
+ message: formatFailedToolOutput(modified.result.output, modified.result.error),
1431
+ isError: true,
1432
+ };
1433
+ }
1434
+ return { kind: 'ready', input: modified.prepared.input, prepared: modified.prepared };
1435
+ }
1436
+ async prepareDirectCall(toolCall) {
1437
+ let toolName = toolCall.function.name;
1438
+ const truncationRepair = toolCall.metadata?.inputTruncated === true
1439
+ ? await this.repairTruncatedCall(toolCall, toolName)
1440
+ : null;
1441
+ if (toolCall.metadata?.inputTruncated === true && !truncationRepair) {
1442
+ return {
1443
+ kind: 'synthetic',
1444
+ toolCall,
1445
+ toolName,
1446
+ input: {},
1447
+ message: truncatedToolInputMessage(toolName),
1448
+ isError: true,
1449
+ };
1450
+ }
1451
+ const prepare = this.config.tools.prepareExecution;
1452
+ const executePrepared = this.config.tools.executePrepared;
1453
+ if (typeof prepare !== 'function' || typeof executePrepared !== 'function') {
1454
+ const resolved = await this.resolveCall(truncationRepair
1455
+ ? {
1456
+ ...toolCall,
1457
+ function: {
1458
+ ...toolCall.function,
1459
+ name: truncationRepair.toolName ?? toolName,
1460
+ arguments: truncationRepair.arguments,
1461
+ },
1462
+ metadata: {},
1463
+ }
1464
+ : toolCall);
1465
+ toolName = resolved.toolName;
1466
+ if (!resolved.ok) {
1467
+ return {
1468
+ kind: 'synthetic',
1469
+ toolCall,
1470
+ toolName,
1471
+ input: {},
1472
+ message: resolved.message,
1473
+ isError: true,
1474
+ };
1475
+ }
1476
+ const preOutcome = await this.runPreToolHook(toolName, resolved.input);
1477
+ if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
1478
+ return {
1479
+ kind: 'synthetic',
1480
+ toolCall,
1481
+ toolName,
1482
+ input: preOutcome.input,
1483
+ message: preOutcome.output,
1484
+ isError: preOutcome.kind === 'error',
1485
+ };
1486
+ }
1487
+ if (!this.config.authorizationGate) {
1488
+ return {
1489
+ kind: 'legacy',
1490
+ toolCall,
1491
+ toolName,
1492
+ input: preOutcome.input,
1493
+ };
1494
+ }
1495
+ return {
1496
+ kind: 'synthetic',
1497
+ toolCall,
1498
+ toolName,
1499
+ input: preOutcome.input,
1500
+ message: `Tool "${toolName}" was not executed because its registry cannot bind authorization to one prepared input.`,
1501
+ isError: true,
1502
+ };
1503
+ }
1504
+ let raw = truncationRepair?.arguments ?? toolCall.function.arguments;
1505
+ toolName = truncationRepair?.toolName ?? toolName;
1506
+ let repairUsed = truncationRepair !== null;
1507
+ let preparation;
1508
+ for (;;) {
1509
+ let parsed;
1510
+ try {
1511
+ parsed = parseArguments(raw);
1512
+ }
1513
+ catch {
1514
+ const message = `Error: Invalid JSON in tool arguments for "${toolName}"`;
1515
+ const repair = !repairUsed && this.config.repairToolCall
1516
+ ? await this.requestRepair(toolCall, toolName, {
1517
+ reason: 'invalid_json',
1518
+ message,
1519
+ })
1520
+ : null;
1521
+ if (repair) {
1522
+ repairUsed = true;
1523
+ toolName = repair.toolName ?? toolName;
1524
+ raw = repair.arguments;
1525
+ continue;
1526
+ }
1527
+ return { kind: 'synthetic', toolCall, toolName, input: {}, message, isError: true };
1528
+ }
1529
+ try {
1530
+ preparation = prepare.call(this.config.tools, toolName, parsed);
1531
+ }
1532
+ catch (err) {
1533
+ const message = `Error: Unknown or unavailable tool "${toolName}": ${toErrorMessage(err)}`;
1534
+ const repair = !repairUsed && this.config.repairToolCall
1535
+ ? await this.requestRepair(toolCall, toolName, {
1536
+ reason: 'unknown_tool',
1537
+ message,
1538
+ })
1539
+ : null;
1540
+ if (repair) {
1541
+ repairUsed = true;
1542
+ toolName = repair.toolName ?? toolName;
1543
+ raw = repair.arguments;
1544
+ continue;
1545
+ }
1546
+ return { kind: 'synthetic', toolCall, toolName, input: parsed, message, isError: true };
1547
+ }
1548
+ if (preparation.success)
1549
+ break;
1550
+ const message = formatFailedToolOutput(preparation.result.output, preparation.result.error);
1551
+ const repair = !repairUsed && this.config.repairToolCall
1552
+ ? await this.requestRepair(toolCall, toolName, {
1553
+ reason: 'schema_validation',
1554
+ message,
1555
+ })
1556
+ : null;
1557
+ if (repair) {
1558
+ repairUsed = true;
1559
+ toolName = repair.toolName ?? toolName;
1560
+ raw = repair.arguments;
1561
+ continue;
1562
+ }
1563
+ return {
1564
+ kind: 'synthetic',
1565
+ toolCall,
1566
+ toolName,
1567
+ input: parsed,
1568
+ message,
1569
+ isError: true,
1570
+ };
1571
+ }
1572
+ const preOutcome = await this.runPreToolHook(toolName, preparation.prepared.input);
1573
+ if (preOutcome.kind === 'skip' || preOutcome.kind === 'error') {
1574
+ return {
1575
+ kind: 'synthetic',
1576
+ toolCall,
1577
+ toolName,
1578
+ input: preOutcome.input,
1579
+ message: preOutcome.output,
1580
+ isError: preOutcome.kind === 'error',
1581
+ };
1582
+ }
1583
+ if (preOutcome.modified) {
1584
+ const modified = prepare.call(this.config.tools, toolName, preOutcome.input);
1585
+ if (!modified.success) {
1586
+ return {
1587
+ kind: 'synthetic',
1588
+ toolCall,
1589
+ toolName,
1590
+ input: preOutcome.input,
1591
+ message: formatFailedToolOutput(modified.result.output, modified.result.error),
1592
+ isError: true,
1593
+ };
1594
+ }
1595
+ preparation = modified;
1596
+ }
1597
+ return {
1598
+ kind: 'ready',
1599
+ toolCall,
1600
+ toolName,
1601
+ input: preparation.prepared.input,
1602
+ prepared: preparation.prepared,
1603
+ };
1604
+ }
1150
1605
  interpretPreToolResults(toolName, initialInput, results) {
1151
1606
  let currentInput = initialInput;
1607
+ let modified = false;
1152
1608
  for (const result of results) {
1153
1609
  switch (result.action) {
1154
1610
  case 'continue':
1155
1611
  continue;
1156
1612
  case 'modify':
1157
1613
  currentInput = result.input;
1614
+ modified = true;
1158
1615
  continue;
1159
1616
  case 'skip':
1160
1617
  return {
@@ -1181,7 +1638,7 @@ export class ToolExecutor {
1181
1638
  }
1182
1639
  }
1183
1640
  }
1184
- return { kind: 'continue', input: currentInput };
1641
+ return { kind: 'continue', input: currentInput, modified };
1185
1642
  }
1186
1643
  /**
1187
1644
  * Turn the call the model issued into a name and a parsed input, giving
@@ -1328,9 +1785,9 @@ export class ToolExecutor {
1328
1785
  * image block had it discarded here — before the wire mapper built to
1329
1786
  * carry it ever saw it.
1330
1787
  */
1331
- async runOnce(toolName, input, toolContext) {
1788
+ async runOnce(toolName, input, toolContext, prepared) {
1332
1789
  try {
1333
- return await this.executeWithDeadline(toolName, input, toolContext);
1790
+ return await this.executeWithDeadline(toolName, input, toolContext, prepared);
1334
1791
  }
1335
1792
  catch (err) {
1336
1793
  const message = toErrorMessage(err);
@@ -1401,14 +1858,16 @@ export class ToolExecutor {
1401
1858
  * execution so UI cards reach a terminal state instead of hanging in
1402
1859
  * `executing`, and records a failed activity for the trace.
1403
1860
  */
1404
- async recordDenial(toolCall, reason) {
1405
- const toolName = toolCall.function.name;
1406
- let input = {};
1407
- try {
1408
- input = JSON.parse(toolCall.function.arguments || '{}');
1409
- }
1410
- catch {
1411
- input = toolCall.function.arguments;
1861
+ async recordDenial(toolCall, reason, preparedCall) {
1862
+ const toolName = preparedCall?.toolName ?? toolCall.function.name;
1863
+ let input = preparedCall?.input ?? {};
1864
+ if (!preparedCall) {
1865
+ try {
1866
+ input = JSON.parse(toolCall.function.arguments || '{}');
1867
+ }
1868
+ catch {
1869
+ input = toolCall.function.arguments;
1870
+ }
1412
1871
  }
1413
1872
  const output = deniedToolOutput(toolName, reason);
1414
1873
  this.log.info('Tool call denied — synthesizing tool_result', {
@@ -1484,6 +1943,12 @@ export class ToolExecutor {
1484
1943
  isError: outcome.kind === 'error',
1485
1944
  };
1486
1945
  }
1946
+ recordSyntheticPreparation(call) {
1947
+ return this.recordSyntheticHookOutcome(call.toolCall.id, call.toolName, call.input, {
1948
+ kind: call.isError ? 'error' : 'skip',
1949
+ output: call.message,
1950
+ });
1951
+ }
1487
1952
  /**
1488
1953
  * Bound the rich channel, or leave it alone when no cap is configured.
1489
1954
  *