@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
@@ -25,6 +25,7 @@ import { EmergencySaveManager } from '../../manager/run/emergency.js'
25
25
  import type { RunPersistence } from '../../manager/run/persistence.js'
26
26
  import { resolveModelPricing } from '../../pricing/index.js'
27
27
  import { resolveProviderCapabilities } from '../../provider/capabilities.js'
28
+ import { isCallerAbortError } from '../../provider/errors.js'
28
29
  import {
29
30
  type ProviderChainMember,
30
31
  type ServingMember,
@@ -77,6 +78,7 @@ import type { AgentPersona } from '../../types/persona/index.js'
77
78
  import type { LLMProvider } from '../../types/provider/index.js'
78
79
  import type { TaskRouterConfig } from '../../types/router/index.js'
79
80
  import type { ReviewAnswer } from '../../types/run/answer-review.js'
81
+ import { cancelCauseOf } from '../../types/run/cancel-cause.js'
80
82
  import type { CheckpointStore, FencingToken } from '../../types/run/checkpoint-store.js'
81
83
  import type { RunEventCursor, RunEventReplay } from '../../types/run/event-cursor.js'
82
84
  import { resolveRunEventReplay } from '../../types/run/event-cursor.js'
@@ -1367,6 +1369,40 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1367
1369
  eventTranslator.wireActivityStore(ctx.activityStore, ctx.runId)
1368
1370
  eventTranslator.wirePlanManager(ctx.planManager, ctx.runId)
1369
1371
  eventTranslator.setGeneration(params.claimFence)
1372
+ let interruptHooksStarted = false
1373
+ const executeUserInterruptHooks = async (terminalError: unknown): Promise<void> => {
1374
+ if (
1375
+ interruptHooksStarted ||
1376
+ !params.pluginManager ||
1377
+ !isCallerAbortError(terminalError, ctx.abortController.signal) ||
1378
+ params.parentRunId !== undefined ||
1379
+ (params.depth ?? 0) !== 0 ||
1380
+ cancelCauseOf(ctx.abortController.signal.reason) !== 'user'
1381
+ ) {
1382
+ return
1383
+ }
1384
+
1385
+ interruptHooksStarted = true
1386
+ try {
1387
+ // Deliberately omit the already-aborted run signal. The lifecycle
1388
+ // manager still supplies each handler its own deadline signal, while
1389
+ // `run_interrupt`'s observational fan-out prevents one result from
1390
+ // suppressing the cleanup hooks that follow it.
1391
+ await params.pluginManager.executeHooks(
1392
+ 'run_interrupt',
1393
+ { runId: ctx.runId, cancelCause: 'user' },
1394
+ eventTranslator.emitEvent,
1395
+ )
1396
+ } catch (error) {
1397
+ // Cancellation is the terminal authority. A hook event sink or an
1398
+ // unexpected manager failure is reported, but cannot turn Stop into a
1399
+ // failed run or prevent the durable cancellation verdict.
1400
+ ctx.log.error('Run interrupt hooks did not settle cleanly', {
1401
+ [NAMZU.RUN_ID]: ctx.runId,
1402
+ ...errorAttributes(error),
1403
+ })
1404
+ }
1405
+ }
1370
1406
 
1371
1407
  if (attachmentResolutionCancelled) {
1372
1408
  // Attachment materialization happens before RunContext exists. Once it
@@ -1375,6 +1411,9 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1375
1411
  // and settle through the ordinary cancellation classifier. Prompt
1376
1412
  // contributions/cache, host callbacks, tools, plugins, sandbox, guardrails,
1377
1413
  // advisors, and providers are all authority-bearing work and stay out.
1414
+ // The dedicated root interrupt notification is the sole plugin exception:
1415
+ // it runs after cancellation under its own deadline and cannot regain model
1416
+ // or tool authority.
1378
1417
  if (params.resumeFromCheckpoint && !selectedResumeState) {
1379
1418
  // The canonical resume surface hands query the checkpoint state it
1380
1419
  // already selected. A raw resume query has no such snapshot; after
@@ -1460,6 +1499,8 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1460
1499
  const terminalError = ctx.abortController.signal.aborted
1461
1500
  ? ctx.abortController.signal.reason
1462
1501
  : error
1502
+ await executeUserInterruptHooks(terminalError)
1503
+ yield* eventTranslator.drainPending()
1463
1504
  yield* cancelledAssembler.handleError(terminalError, rootSpan)
1464
1505
  } finally {
1465
1506
  rootSpan.end()
@@ -1620,6 +1661,14 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1620
1661
  const questionParks = params.questionParks ?? new QuestionParkBinding()
1621
1662
  const pendingAnswers = params.pendingAnswers ?? new PendingAnswers()
1622
1663
 
1664
+ // One gate instance owns both model-issued calls and calls dispatched by
1665
+ // another tool. Constructing it only inside the iteration review left the
1666
+ // nested registry path outside the operator's policy entirely.
1667
+ const gateConfig = params.authorizationGate
1668
+ const verificationGate = gateConfig?.enabled
1669
+ ? new AuthorizationGate(gateConfig, ctx.log)
1670
+ : undefined
1671
+
1623
1672
  // is null only when the run has no disk layout (tests,
1624
1673
  // in-memory hosts); the budget then degrades to middle-elision.
1625
1674
  const runDirForTools = ctx.runMgr.getRunDir()
@@ -1660,6 +1709,8 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1660
1709
  // `read`/`grep` without a new affordance.
1661
1710
  ...(toolOutputDir ? { toolOutputDir } : {}),
1662
1711
  ...(params.repairToolCall ? { repairToolCall: params.repairToolCall } : {}),
1712
+ ...(verificationGate ? { authorizationGate: verificationGate } : {}),
1713
+ recordAudit: (input) => ctx.runMgr.recordAudit(input),
1663
1714
  // The durable pause, reachable from any tool rather than from the
1664
1715
  // four kernel-owned points that used to own it. Built here from
1665
1716
  // the machinery the run already holds; the recorder binds a few
@@ -1806,12 +1857,6 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1806
1857
  }
1807
1858
  }
1808
1859
 
1809
- const gateConfig = params.authorizationGate
1810
-
1811
- const verificationGate = gateConfig?.enabled
1812
- ? new AuthorizationGate(gateConfig, ctx.log)
1813
- : undefined
1814
-
1815
1860
  const iterationOrchestrator = new IterationOrchestrator({
1816
1861
  provider: resilientProvider,
1817
1862
  servingMember: () => serving.current,
@@ -2581,6 +2626,8 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
2581
2626
  } catch (err) {
2582
2627
  // A failed run still spent its steps; report them.
2583
2628
  ctx.runMgr.setSteps(iterationOrchestrator.getSteps())
2629
+ await executeUserInterruptHooks(err)
2630
+ yield* eventTranslator.drainPending()
2584
2631
  yield* resultAssembler.handleError(err, rootSpan)
2585
2632
  } finally {
2586
2633
  // Release the process's termination path as soon as this run is
@@ -1,7 +1,8 @@
1
1
  import type { AuthorizationGate } from '../../../../authorization/index.js'
2
+ import type { ToolCallSummary } from '../../../../types/hitl/index.js'
2
3
  import type { ChatCompletionResponse } from '../../../../types/provider/index.js'
3
4
  import type { RunEvent } from '../../../../types/run/index.js'
4
- import type { ToolCallDenials } from '../../executor.js'
5
+ import type { PreparedToolBatch, ToolCallDenials } from '../../executor.js'
5
6
  import {
6
7
  awaitProjectInstructionCallback,
7
8
  replaceProjectInstructionSnapshot,
@@ -64,28 +65,42 @@ export async function* runToolReview(
64
65
  return finish('executed')
65
66
  }
66
67
 
67
- const toolCallSummaries = toolCalls.map((tc) => {
68
- let input: unknown
69
- try {
70
- input = JSON.parse(tc.function.arguments)
71
- } catch {
72
- input = tc.function.arguments
73
- }
74
- const tool = ctx.tools.get(tc.function.name)
75
- const isDestructive = tool?.isDestructive ? tool.isDestructive(input) : false
76
-
77
- return {
78
- id: tc.id,
79
- name: tc.function.name,
80
- input,
81
- isDestructive,
82
- }
83
- })
68
+ const prepareForReview = async (): Promise<PreparedToolBatch | undefined> => {
69
+ const prepare = ctx.toolExecutor.prepareBatchForReview
70
+ return typeof prepare === 'function' ? prepare.call(ctx.toolExecutor, response) : undefined
71
+ }
72
+ let preparedBatch = await prepareForReview()
73
+ const summariesFor = (prepared: PreparedToolBatch | undefined): ToolCallSummary[] => {
74
+ const calls =
75
+ prepared?.reviewCalls ??
76
+ toolCalls.map((tc) => {
77
+ let input: unknown
78
+ try {
79
+ input = JSON.parse(tc.function.arguments)
80
+ } catch {
81
+ input = tc.function.arguments
82
+ }
83
+ return { id: tc.id, name: tc.function.name, input }
84
+ })
85
+ return calls.map((tc) => {
86
+ const tool = ctx.tools.get(tc.name)
87
+ const isDestructive = tool?.isDestructive ? tool.isDestructive(tc.input) : false
88
+
89
+ return {
90
+ id: tc.id,
91
+ name: tc.name,
92
+ input: tc.input,
93
+ isDestructive,
94
+ authorization: { decision: 'review' },
95
+ }
96
+ })
97
+ }
98
+ let toolCallSummaries = summariesFor(preparedBatch)
84
99
 
85
100
  /** Executes the batch, answering every call, and appends the results. */
86
101
  const settle = async (denials?: ToolCallDenials): Promise<void> => {
87
102
  const startedAt = Date.now()
88
- const batch = await ctx.toolExecutor.executeBatch(response, denials)
103
+ const batch = await ctx.toolExecutor.executeBatch(response, denials, undefined, preparedBatch)
89
104
  toolMs += Date.now() - startedAt
90
105
  executed = batch.results
91
106
  // Recorded AFTER execution and never before it: this advises, it does
@@ -130,6 +145,12 @@ export async function* runToolReview(
130
145
  }
131
146
  }
132
147
 
148
+ if (toolCallSummaries.length === 0) {
149
+ await settle()
150
+ yield* ctx.drainPending()
151
+ return finish('executed')
152
+ }
153
+
133
154
  /** Every call denied for the same reason (human rejection, gate stop). */
134
155
  const denyAll = (reason: string): ToolCallDenials =>
135
156
  new Map(toolCalls.map((tc) => [tc.id, reason]))
@@ -159,10 +180,16 @@ export async function* runToolReview(
159
180
  toolDef: ctx.tools.get(tc.name),
160
181
  }),
161
182
  }))
183
+ for (const { toolCall, gateResult } of gateResults) {
184
+ toolCall.authorization = {
185
+ decision: gateResult.decision,
186
+ ...(gateResult.reason ? { reason: gateResult.reason } : {}),
187
+ }
188
+ }
162
189
 
163
190
  for (const gr of gateResults) {
164
191
  if (gr.gateResult.decision === 'deny') {
165
- const reason = `Blocked by the verification gate: ${gr.gateResult.reason}`
192
+ const reason = `Blocked by the authorization gate: ${gr.gateResult.reason}`
166
193
  gateDenied.set(gr.toolCall.id, reason)
167
194
  // A gate denial is a refusal — first-class in the audit trail, never
168
195
  // an absent record (LOG-14, design §5). Written here, once per
@@ -180,7 +207,7 @@ export async function* runToolReview(
180
207
  const allDenied = gateResults.every((gr) => gr.gateResult.decision === 'deny')
181
208
 
182
209
  if (allAllowed) {
183
- ctx.log.debug('Verification gate: all tool calls pre-approved', {
210
+ ctx.log.debug('Authorization gate: all tool calls pre-approved', {
184
211
  'namzu.tool.names': gateResults.map((gr) => gr.toolCall.name),
185
212
  })
186
213
  await settle()
@@ -189,7 +216,7 @@ export async function* runToolReview(
189
216
  }
190
217
 
191
218
  if (allDenied) {
192
- ctx.log.debug('Verification gate: all tool calls denied', {
219
+ ctx.log.debug('Authorization gate: all tool calls denied', {
193
220
  'namzu.tool.names': gateResults.map((gr) => gr.toolCall.name),
194
221
  })
195
222
  await settle(gateDenied)
@@ -197,7 +224,7 @@ export async function* runToolReview(
197
224
  return finish('rejected')
198
225
  }
199
226
 
200
- ctx.log.debug('Verification gate: mixed decisions, proceeding to review', {
227
+ ctx.log.debug('Authorization gate: mixed decisions, proceeding to review', {
201
228
  'namzu.runtime.decisions': gateResults.map((gr) => ({
202
229
  tool: gr.toolCall.name,
203
230
  decision: gr.gateResult.decision,
@@ -267,6 +294,7 @@ export async function* runToolReview(
267
294
 
268
295
  // Gate denials are the floor; per-call human denials add to them.
269
296
  const denials = new Map(gateDenied)
297
+ const modifiedCallIds = new Set<string>()
270
298
 
271
299
  for (const mod of reviewDecision.modifications) {
272
300
  if (mod.action === 'modify' && mod.modifiedInput !== undefined) {
@@ -274,6 +302,7 @@ export async function* runToolReview(
274
302
  // A modification cannot resurrect a gate-denied call.
275
303
  if (tc && !denials.has(tc.id)) {
276
304
  tc.function.arguments = JSON.stringify(mod.modifiedInput)
305
+ modifiedCallIds.add(tc.id)
277
306
  }
278
307
  }
279
308
  if (mod.action === 'deny' && !denials.has(mod.toolCallId)) {
@@ -281,6 +310,41 @@ export async function* runToolReview(
281
310
  }
282
311
  }
283
312
 
313
+ // A human modification changes the raw call after the first preparation.
314
+ // Decode it once again, then require policy to explicitly allow the new
315
+ // executable value. A second nested review would be ambiguous: the human
316
+ // edited raw JSON, not an unseen schema transform of it.
317
+ if (modifiedCallIds.size > 0) {
318
+ const reprepare = ctx.toolExecutor.reprepareBatchForReview
319
+ preparedBatch =
320
+ preparedBatch && typeof reprepare === 'function'
321
+ ? await reprepare.call(ctx.toolExecutor, response, preparedBatch, modifiedCallIds)
322
+ : await prepareForReview()
323
+ toolCallSummaries = summariesFor(preparedBatch)
324
+ }
325
+ if (ctx.verificationGate && modifiedCallIds.size > 0) {
326
+ for (const summary of toolCallSummaries) {
327
+ if (!modifiedCallIds.has(summary.id)) continue
328
+ if (denials.has(summary.id)) continue
329
+ const gateResult = ctx.verificationGate.evaluate({
330
+ toolName: summary.name,
331
+ toolInput: summary.input,
332
+ toolDef: ctx.tools.get(summary.name),
333
+ })
334
+ if (gateResult.decision === 'allow') continue
335
+ const reason =
336
+ gateResult.decision === 'deny'
337
+ ? `Blocked by the authorization gate after the tool input was modified: ${gateResult.reason}`
338
+ : `Blocked by the authorization gate after the tool input was modified: the prepared value requires a new explicit approval. ${gateResult.reason}`
339
+ denials.set(summary.id, reason)
340
+ await ctx.runMgr.recordAudit({
341
+ what: { action: 'tool_call', tool: summary.name },
342
+ outcome: 'refused',
343
+ reason,
344
+ })
345
+ }
346
+ }
347
+
284
348
  const everythingDenied = denials.size === toolCalls.length
285
349
  await settle(denials)
286
350
  yield* ctx.drainPending()
@@ -34,7 +34,7 @@ interface RichOccurrence {
34
34
  readonly source: 'user' | 'tool'
35
35
  readonly kind: RichKind
36
36
  readonly bytes: number
37
- readonly reason: 'budget' | 'provider-rejected'
37
+ readonly reason: 'budget' | 'provider-rejected' | 'invalid-image'
38
38
  }
39
39
 
40
40
  export interface RequestImageIdentity {
@@ -54,6 +54,12 @@ const rejectedUserImageMarker = (): string =>
54
54
  const rejectedToolImageMarker = (): string =>
55
55
  '[image omitted from this model request because the provider rejected this image; call the producing tool again after correcting its image source if it is still needed.]'
56
56
 
57
+ const invalidUserImageMarker = (): string =>
58
+ '[image omitted from this model request because its encoded bytes are not a complete supported raster matching the declared media type; attach a corrected image in a new message if it is still needed.]'
59
+
60
+ const invalidToolImageMarker = (): string =>
61
+ '[image omitted from this model request because the producing tool returned encoded bytes that are not a complete supported raster matching the declared media type; call the tool again after correcting its image source if it is still needed.]'
62
+
57
63
  function assertInlineAttachment(
58
64
  attachment: MessageAttachment,
59
65
  ): asserts attachment is Exclude<MessageAttachment, { readonly type: 'stored' }> {
@@ -78,10 +84,12 @@ function collectRichOccurrences(messages: readonly Message[]): RichOccurrence[]
78
84
  kind: attachment.type === 'document' ? 'document' : 'image',
79
85
  bytes: attachment.data.length,
80
86
  reason:
81
- attachment.type !== 'document' &&
82
- attachment.modelOmission?.reason === 'provider-rejected'
83
- ? 'provider-rejected'
84
- : 'budget',
87
+ attachment.type !== 'document' && attachment.modelOmission?.reason === 'invalid-image'
88
+ ? 'invalid-image'
89
+ : attachment.type !== 'document' &&
90
+ attachment.modelOmission?.reason === 'provider-rejected'
91
+ ? 'provider-rejected'
92
+ : 'budget',
85
93
  })
86
94
  }
87
95
  continue
@@ -97,9 +105,11 @@ function collectRichOccurrences(messages: readonly Message[]): RichOccurrence[]
97
105
  kind: block.type,
98
106
  bytes: block.data.length,
99
107
  reason:
100
- block.type === 'image' && block.modelOmission?.reason === 'provider-rejected'
101
- ? 'provider-rejected'
102
- : 'budget',
108
+ block.type === 'image' && block.modelOmission?.reason === 'invalid-image'
109
+ ? 'invalid-image'
110
+ : block.type === 'image' && block.modelOmission?.reason === 'provider-rejected'
111
+ ? 'provider-rejected'
112
+ : 'budget',
103
113
  })
104
114
  }
105
115
  }
@@ -117,7 +127,9 @@ function appendMarkers(content: string, occurrences: readonly RichOccurrence[]):
117
127
  const markers = occurrences.map((occurrence) =>
118
128
  occurrence.reason === 'provider-rejected'
119
129
  ? rejectedUserImageMarker()
120
- : userMarker(occurrence.kind),
130
+ : occurrence.reason === 'invalid-image'
131
+ ? invalidUserImageMarker()
132
+ : userMarker(occurrence.kind),
121
133
  )
122
134
  return appendTextMarkers(content, markers)
123
135
  }
@@ -146,7 +158,9 @@ export function projectRequestRichContent(messages: Message[], maxBytes: number)
146
158
  }
147
159
 
148
160
  for (const occurrence of occurrences) {
149
- if (occurrence.reason === 'provider-rejected') omit(occurrence)
161
+ if (occurrence.reason === 'provider-rejected' || occurrence.reason === 'invalid-image') {
162
+ omit(occurrence)
163
+ }
150
164
  }
151
165
 
152
166
  let total = occurrences.reduce(
@@ -189,7 +203,9 @@ export function projectRequestRichContent(messages: Message[], maxBytes: number)
189
203
  text:
190
204
  occurrence.reason === 'provider-rejected'
191
205
  ? rejectedToolImageMarker()
192
- : toolMarker(occurrence.kind),
206
+ : occurrence.reason === 'invalid-image'
207
+ ? invalidToolImageMarker()
208
+ : toolMarker(occurrence.kind),
193
209
  }
194
210
  : block
195
211
  })
@@ -220,7 +236,10 @@ export function findSingleRequestImage(messages: readonly Message[]): RequestIma
220
236
  }
221
237
  if (attachment.type === 'document') continue
222
238
  if (candidate === null) {
223
- candidate = { data: attachment.data, mediaType: attachment.mediaType }
239
+ candidate = {
240
+ data: attachment.data,
241
+ mediaType: attachment.mediaType,
242
+ }
224
243
  } else if (!sameImage(attachment, candidate)) {
225
244
  return null
226
245
  }
@@ -302,7 +321,10 @@ export function markProviderRejectedImage(
302
321
  }
303
322
  changed = true
304
323
  count += 1
305
- return { ...attachment, modelOmission: { reason: 'provider-rejected' as const } }
324
+ return {
325
+ ...attachment,
326
+ modelOmission: { reason: 'provider-rejected' as const },
327
+ }
306
328
  })
307
329
  return changed ? { ...message, attachments } : message
308
330
  }
@@ -318,7 +340,10 @@ export function markProviderRejectedImage(
318
340
  }
319
341
  changed = true
320
342
  count += 1
321
- return { ...block, modelOmission: { reason: 'provider-rejected' as const } }
343
+ return {
344
+ ...block,
345
+ modelOmission: { reason: 'provider-rejected' as const },
346
+ }
322
347
  })
323
348
  return changed ? { ...message, content } : message
324
349
  })
@@ -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,