@iowarp/clio-coder 0.3.4 → 0.3.6

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 (265) hide show
  1. package/CHANGELOG.md +37 -2
  2. package/CONTRIBUTING.md +6 -6
  3. package/README.md +2 -2
  4. package/dist/{acp-S5R4RR5B.js → acp-2BEHC4DL.js} +4 -4
  5. package/dist/{agents-P6DMMVZY.js → agents-LNNFTM53.js} +13 -11
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-2XCZLPKS.js → auth-KXXFI2VS.js} +6 -6
  8. package/dist/{chunk-YCWGATWI.js → chunk-24I7BN55.js} +2 -2
  9. package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
  10. package/dist/chunk-3BPUFZDL.js +37 -0
  11. package/dist/{chunk-WPQLXFOZ.js → chunk-43AOLP7E.js} +2 -2
  12. package/dist/{chunk-N4CZJQRK.js → chunk-5JGRAMKL.js} +4 -4
  13. package/dist/{chunk-BRXQQJFP.js → chunk-6US73PDB.js} +568 -47
  14. package/dist/{chunk-K6WL7QZT.js → chunk-6XXKFVSN.js} +2 -2
  15. package/dist/{chunk-QQK64KLB.js → chunk-CJUB2JJ2.js} +138 -20
  16. package/dist/{chunk-HV5X7OR2.js → chunk-CKXWIANG.js} +12 -12
  17. package/dist/{chunk-UZHIZC5S.js → chunk-CYQKWTG3.js} +61 -76
  18. package/dist/{chunk-QWU7ZBO7.js → chunk-DJVECN66.js} +204 -45
  19. package/dist/{chunk-ZWMF7253.js → chunk-E2ER4LJF.js} +304 -9
  20. package/dist/{chunk-7RXG6QRZ.js → chunk-EKY57CSP.js} +2 -75
  21. package/dist/{chunk-EDRHSCIE.js → chunk-EYPA3EGJ.js} +10 -2
  22. package/dist/{chunk-TTNYS3EA.js → chunk-G7MUEIGA.js} +1 -1
  23. package/dist/{chunk-BPGS2WCQ.js → chunk-GEYXPTRF.js} +2 -1
  24. package/dist/{chunk-BEY543CS.js → chunk-GOXNB3AO.js} +5 -2
  25. package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
  26. package/dist/chunk-HWUFFB6L.js +83 -0
  27. package/dist/{chunk-35MKKU5R.js → chunk-K7T3E2SR.js} +15 -8
  28. package/dist/{chunk-VAWWTKDP.js → chunk-KHSFENX2.js} +2 -2
  29. package/dist/chunk-LCGCVYZ4.js +57 -0
  30. package/dist/{chunk-X6COSD2O.js → chunk-LYF7OHWH.js} +41 -14
  31. package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
  32. package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
  33. package/dist/{chunk-5M54SPOL.js → chunk-ODFEOB4F.js} +161 -5
  34. package/dist/{chunk-3JLKSKD7.js → chunk-OH3TOQTB.js} +5 -1
  35. package/dist/{chunk-MEQ45TQ4.js → chunk-PBTHKCPN.js} +18 -4
  36. package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
  37. package/dist/{chunk-QQL5RT5M.js → chunk-QM3F2GKX.js} +94 -36
  38. package/dist/{chunk-A2GZF7DC.js → chunk-QNQHSOLF.js} +4 -4
  39. package/dist/{chunk-KRPY7NTG.js → chunk-R46L2BIR.js} +3 -3
  40. package/dist/{chunk-BP4OYD6A.js → chunk-RY3LY4J5.js} +20 -2
  41. package/dist/{chunk-34475P3I.js → chunk-TSHXZTOQ.js} +5 -4
  42. package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
  43. package/dist/{chunk-2TZWSW76.js → chunk-WHGPSPT5.js} +2 -2
  44. package/dist/{chunk-TW3WDMVS.js → chunk-WHJYKASB.js} +2 -2
  45. package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
  46. package/dist/{chunk-HXG4IURW.js → chunk-XE2VEJHX.js} +2 -2
  47. package/dist/{chunk-3HZ5RWN2.js → chunk-XF5N4U5A.js} +7 -6
  48. package/dist/{chunk-ZYKPLLNQ.js → chunk-XXQNGV4M.js} +590 -32
  49. package/dist/{chunk-4JUF2NNX.js → chunk-XYDYPRZI.js} +4 -4
  50. package/dist/{chunk-VMNQ6OZA.js → chunk-ZRGEBJ4T.js} +971 -794
  51. package/dist/{chunk-2LZI5CAG.js → chunk-ZXF4XRKW.js} +75 -33
  52. package/dist/{chunk-VSNATDE6.js → chunk-ZZMN5OM4.js} +2 -2
  53. package/dist/cli/index.js +31 -31
  54. package/dist/{clio-J5JIOIDS.js → clio-M2KGYUFZ.js} +2 -2
  55. package/dist/{code-nav-AXCXSBHX.js → code-nav-GQNL7XA6.js} +5 -5
  56. package/dist/codewiki/build-worker.js +4 -4
  57. package/dist/{components-KELWS457.js → components-5TTYYX6G.js} +3 -3
  58. package/dist/{config-OEBMIN2U.js → config-XUUYQIWO.js} +27 -25
  59. package/dist/{configure-PUQOSIXQ.js → configure-IHJ7YOMV.js} +7 -7
  60. package/dist/{context-URSXPBCK.js → context-74JLXAWD.js} +12 -12
  61. package/dist/{context-MGSE4Z2T.js → context-75MIWW3U.js} +24 -22
  62. package/dist/{context-EKDCKUUZ.js → context-ZQ7SIFJV.js} +8 -7
  63. package/dist/{context-clear-KDAJRNUK.js → context-clear-GYKWNUML.js} +24 -22
  64. package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
  65. package/dist/{context-working-set-SBKMPPI2.js → context-working-set-UX5KEP4J.js} +11 -10
  66. package/dist/{dispatch-runner-MSWN72NK.js → dispatch-runner-GIJBHNFL.js} +21 -20
  67. package/dist/{docs-2C2LTVT2.js → docs-6FZSCG5B.js} +3 -3
  68. package/dist/{doctor-7BSE27PJ.js → doctor-SVJ5BZCW.js} +4 -4
  69. package/dist/{eval-IZGDOO4H.js → eval-CG6LLBLD.js} +47 -232
  70. package/dist/{evidence-SR7WXB5B.js → evidence-ZYFIEN42.js} +19 -18
  71. package/dist/{evolve-K7VE2CBX.js → evolve-QGEXEMDW.js} +19 -18
  72. package/dist/{extensions-QVDOHDGJ.js → extensions-ADGNCJJD.js} +3 -3
  73. package/dist/{fleet-7XMJNQNF.js → fleet-S5R4ZOQY.js} +49 -30
  74. package/dist/{fleet-preflight-AQNAH644.js → fleet-preflight-BHSNPBMH.js} +2 -2
  75. package/dist/{init-JGNPAYXT.js → init-5DRU55YR.js} +31 -29
  76. package/dist/memory-7YKKR6UC.js +467 -0
  77. package/dist/{models-ZMMLFJNN.js → models-ZPOLRU2C.js} +10 -10
  78. package/dist/{monitor-2F3T5KHP.js → monitor-US5F5YGZ.js} +33 -18
  79. package/dist/{orchestrator-ORHT43JB.js → orchestrator-E2AL4T5N.js} +1092 -659
  80. package/dist/{paths-UXLN5YYZ.js → paths-E7KYAQWE.js} +3 -3
  81. package/dist/{reset-NXGTYNUO.js → reset-KZ652EK6.js} +3 -3
  82. package/dist/{run-RF4WJGMT.js → run-SRNBKDWD.js} +52 -40
  83. package/dist/{share-UT3W6E4M.js → share-CGZE33UP.js} +3 -3
  84. package/dist/{skills-PSACKC5Q.js → skills-S2X4DLY5.js} +4 -4
  85. package/dist/{skills-eval-WJSI55RZ.js → skills-eval-W2GGIC4R.js} +19 -18
  86. package/dist/{targets-PIIRAOYS.js → targets-54SWINWB.js} +14 -12
  87. package/dist/{terminal-lease-ULWXWNVY.js → terminal-lease-SAIF2OGY.js} +5 -4
  88. package/dist/{uninstall-FZCQCDKC.js → uninstall-BVLWXKBT.js} +3 -3
  89. package/dist/{upgrade-346TZ6AV.js → upgrade-JKAR27XC.js} +8 -8
  90. package/dist/{usage-6KKXR32N.js → usage-MSAWCLX4.js} +60 -27
  91. package/dist/{verifiers-4UUM6TEE.js → verifiers-NCBTHHN2.js} +60 -54
  92. package/dist/{wiki-generate-7STOCIFZ.js → wiki-generate-GUSOQ6ZP.js} +30 -28
  93. package/dist/worker/entry.js +69 -58
  94. package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
  95. package/docs/README.md +3 -3
  96. package/docs/acp.md +1 -1
  97. package/docs/alcf-provider.md +1 -1
  98. package/docs/architecture.md +2 -2
  99. package/docs/artifact-placement.md +1 -2
  100. package/docs/artifact-versions.md +1 -1
  101. package/docs/built-in-agents.md +1 -1
  102. package/docs/capacity-and-scheduling.md +1 -1
  103. package/docs/commands-and-modes.md +9 -7
  104. package/docs/configuration-and-targets.md +12 -1
  105. package/docs/context-engine.md +4 -2
  106. package/docs/context-working-set.md +4 -4
  107. package/docs/development-pipeline.md +1 -1
  108. package/docs/documentation-coverage.md +3 -3
  109. package/docs/documentation-guide.md +2 -2
  110. package/docs/eval-runner.md +1 -1
  111. package/docs/evals-internal.md +4 -45
  112. package/docs/evidence-and-memory.md +67 -7
  113. package/docs/evolution.md +1 -1
  114. package/docs/exit-codes-and-output.md +1 -1
  115. package/docs/extensions-and-sharing.md +2 -2
  116. package/docs/fleet-dispatch.md +28 -2
  117. package/docs/installation-and-lifecycle.md +2 -2
  118. package/docs/middleware-and-components.md +19 -2
  119. package/docs/model-catalog.md +1 -1
  120. package/docs/observability.md +3 -3
  121. package/docs/proactive-memory.md +26 -16
  122. package/docs/prompt-envelope-and-tools.md +4 -2
  123. package/docs/provider-adapter-cookbook.md +1 -1
  124. package/docs/release-cut-checklist.md +38 -35
  125. package/docs/safety-model.md +29 -7
  126. package/docs/scientific-validation.md +3 -3
  127. package/docs/session-lifecycle.md +1 -1
  128. package/docs/skills-marketplace.md +1 -1
  129. package/docs/tool-usage.md +2 -2
  130. package/docs/trace-store.md +1 -1
  131. package/docs/troubleshooting.md +1 -1
  132. package/docs/tui-design.md +38 -4
  133. package/docs/worker-dispatch-mechanics.md +1 -1
  134. package/package.json +7 -4
  135. package/src/cli/agents.ts +2 -3
  136. package/src/cli/argv.ts +14 -1
  137. package/src/cli/fleet.ts +15 -0
  138. package/src/cli/index.ts +1 -1
  139. package/src/cli/memory.ts +272 -10
  140. package/src/cli/modes/json-stream.ts +2 -2
  141. package/src/cli/modes/print.ts +12 -1
  142. package/src/cli/run.ts +22 -2
  143. package/src/cli/targets.ts +12 -3
  144. package/src/cli/usage.ts +55 -7
  145. package/src/core/bus-events.ts +3 -0
  146. package/src/core/response-model-id.ts +134 -0
  147. package/src/core/toml.ts +62 -0
  148. package/src/core/workspace-files.ts +0 -1
  149. package/src/domains/agents/builtins/architect.md +1 -1
  150. package/src/domains/agents/catalog.ts +5 -4
  151. package/src/domains/agents/recipe.ts +54 -14
  152. package/src/domains/agents/result-contract.ts +7 -4
  153. package/src/domains/context/bootstrap.ts +36 -27
  154. package/src/domains/context/project-metadata.ts +19 -63
  155. package/src/domains/context/prompt-context.ts +8 -0
  156. package/src/domains/context/working-set/policies/index.ts +3 -4
  157. package/src/domains/dispatch/budget-envelope.ts +396 -0
  158. package/src/domains/dispatch/contract.ts +2 -0
  159. package/src/domains/dispatch/extension.ts +81 -27
  160. package/src/domains/dispatch/orphan-recovery.ts +1 -0
  161. package/src/domains/dispatch/receipt-integrity.ts +4 -0
  162. package/src/domains/dispatch/state.ts +1 -0
  163. package/src/domains/dispatch/types.ts +10 -3
  164. package/src/domains/dispatch/validation.ts +14 -0
  165. package/src/domains/dispatch/worker-spawn.ts +14 -3
  166. package/src/domains/eval/metrics/evidence.ts +0 -116
  167. package/src/domains/eval/metrics/invariants.ts +1 -1
  168. package/src/domains/eval/runners/clio-run.ts +1 -10
  169. package/src/domains/eval/runners/external-command.ts +2 -29
  170. package/src/domains/eval/schema/suite.ts +0 -7
  171. package/src/domains/eval/suites/run.ts +1 -7
  172. package/src/domains/memory/index.ts +22 -0
  173. package/src/domains/memory/operations.ts +58 -1
  174. package/src/domains/memory/promotion.ts +281 -0
  175. package/src/domains/memory/prompt-section.ts +25 -5
  176. package/src/domains/memory/proposal.ts +51 -7
  177. package/src/domains/memory/task-bank.ts +3 -2
  178. package/src/domains/memory/task-memory-handoff.ts +181 -24
  179. package/src/domains/memory/task-memory-policy.ts +3 -1
  180. package/src/domains/memory/types.ts +37 -0
  181. package/src/domains/memory/validate.ts +178 -0
  182. package/src/domains/middleware/memory-intervention.ts +35 -25
  183. package/src/domains/middleware/runtime.ts +6 -0
  184. package/src/domains/middleware/skills-reminder.ts +19 -4
  185. package/src/domains/middleware/stalled-turn.ts +43 -1
  186. package/src/domains/middleware/types.ts +10 -0
  187. package/src/domains/observability/contract.ts +6 -1
  188. package/src/domains/observability/cost.ts +20 -4
  189. package/src/domains/observability/extension.ts +2 -2
  190. package/src/domains/providers/index.ts +3 -0
  191. package/src/domains/providers/model-discovery.ts +9 -0
  192. package/src/domains/providers/runtime-resolution.ts +38 -1
  193. package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
  194. package/src/domains/providers/types/context-window-slots.ts +18 -0
  195. package/src/domains/providers/types/runtime-descriptor.ts +3 -1
  196. package/src/domains/safety/call-target.ts +211 -14
  197. package/src/domains/safety/decision-presentation.ts +268 -0
  198. package/src/domains/safety/redaction.ts +73 -0
  199. package/src/domains/session/context-ledger.ts +10 -1
  200. package/src/domains/session/decision-board.ts +4 -0
  201. package/src/domains/session/entries.ts +3 -0
  202. package/src/domains/session/history.ts +68 -19
  203. package/src/domains/session/usage.ts +24 -7
  204. package/src/engine/acp/event-mapper.ts +7 -0
  205. package/src/engine/acp/server.ts +29 -2
  206. package/src/engine/apis/lmstudio.ts +25 -4
  207. package/src/engine/apis/openai-completions.ts +147 -22
  208. package/src/engine/claude/sdk-runtime.ts +8 -2
  209. package/src/engine/claude/tool-safety.ts +13 -0
  210. package/src/engine/loop-guard.ts +27 -3
  211. package/src/engine/worker-events.ts +4 -3
  212. package/src/engine/worker-runtime.ts +59 -54
  213. package/src/entry/orchestrator.ts +18 -1
  214. package/src/interactive/chat-loop-messages.ts +22 -0
  215. package/src/interactive/chat-loop.ts +13 -0
  216. package/src/interactive/chat-renderer.ts +19 -3
  217. package/src/interactive/clio-editor.ts +44 -7
  218. package/src/interactive/context-overlay.ts +43 -5
  219. package/src/interactive/cost-overlay.ts +39 -8
  220. package/src/interactive/dispatch-board.ts +212 -35
  221. package/src/interactive/footer/widgets.ts +13 -0
  222. package/src/interactive/interactive-application.ts +6 -1
  223. package/src/interactive/interactive-input-runtime.ts +11 -1
  224. package/src/interactive/interactive-presentation.ts +11 -1
  225. package/src/interactive/memory-overlay.ts +89 -4
  226. package/src/interactive/overlay-ask-user-lifecycle.ts +1 -1
  227. package/src/interactive/overlay-frame.ts +5 -2
  228. package/src/interactive/overlay-general-openers.ts +40 -1
  229. package/src/interactive/overlay-key-routing.ts +41 -1
  230. package/src/interactive/overlay-lifecycle.ts +11 -4
  231. package/src/interactive/overlay-permission-lifecycle.ts +23 -8
  232. package/src/interactive/overlay-transitions.ts +11 -0
  233. package/src/interactive/overlays/ask-user.ts +74 -30
  234. package/src/interactive/overlays/decisions.ts +3 -1
  235. package/src/interactive/permission-hint.ts +35 -0
  236. package/src/interactive/permission-overlay.ts +95 -45
  237. package/src/interactive/renderers/tool-execution.ts +19 -49
  238. package/src/interactive/session-last-turn.ts +8 -1
  239. package/src/interactive/session-usage-reseed.ts +36 -10
  240. package/src/interactive/slash-commands.ts +2 -2
  241. package/src/interactive/status/summary.ts +5 -0
  242. package/src/interactive/status/types.ts +5 -0
  243. package/src/interactive/terminal-lease.ts +1 -0
  244. package/src/interactive/turn-context.ts +96 -23
  245. package/src/interactive/turn-middleware.ts +1 -0
  246. package/src/interactive/turn-runtime.ts +37 -8
  247. package/src/interactive/turn-state.ts +3 -0
  248. package/src/interactive/worker-progress.ts +440 -0
  249. package/src/interactive/worker-stream.ts +51 -110
  250. package/src/tools/agent-tools.ts +28 -3
  251. package/src/tools/ask-user.ts +21 -1
  252. package/src/tools/context/index.ts +2 -2
  253. package/src/tools/dispatch-arguments.ts +8 -0
  254. package/src/tools/dispatch-event-text.ts +19 -0
  255. package/src/tools/dispatch.ts +24 -1
  256. package/src/tools/monitor.ts +15 -0
  257. package/src/tools/registry.ts +15 -5
  258. package/src/tools/result-disposition.ts +156 -0
  259. package/src/tools/result-shaping.ts +59 -1
  260. package/src/tools/verify/authoring.ts +55 -54
  261. package/src/tools/worker-evidence.ts +19 -0
  262. package/src/worker/spec-contract.ts +43 -3
  263. package/dist/chunk-EFADSJET.js +0 -18
  264. package/dist/memory-4ALKDJ4Q.js +0 -246
  265. package/src/domains/eval/metrics/chaos-stream.ts +0 -93
@@ -7,29 +7,25 @@ import {
7
7
  } from "./chunk-UOV2BYIW.js";
8
8
  import {
9
9
  describeDispatchPlan
10
- } from "./chunk-EDRHSCIE.js";
10
+ } from "./chunk-EYPA3EGJ.js";
11
11
  import {
12
12
  RESPONSE_SCHEMA_RUNTIME_ID,
13
13
  applyToolProfile,
14
14
  bashTool,
15
15
  builtin,
16
+ describeCallAction,
16
17
  networkToolsDisabled,
17
18
  readMaxBytes,
18
19
  readTool,
19
20
  renderAgentLedger
20
- } from "./chunk-ZYKPLLNQ.js";
21
+ } from "./chunk-XXQNGV4M.js";
21
22
  import {
22
23
  createTaskBoardStore,
23
24
  taskBoardCounts
24
- } from "./chunk-WPQLXFOZ.js";
25
+ } from "./chunk-43AOLP7E.js";
25
26
  import {
26
- DEFAULT_AUTONOMY_EXPOSURE,
27
- DEFAULT_AUTONOMY_LEVEL,
28
- autonomyAskRejection,
29
- autonomyDenyRejection,
30
- formatModelRejection,
31
- mapAutonomy
32
- } from "./chunk-7RXG6QRZ.js";
27
+ formatModelRejection
28
+ } from "./chunk-EKY57CSP.js";
33
29
  import {
34
30
  classify
35
31
  } from "./chunk-AOCYTWAV.js";
@@ -71,7 +67,14 @@ import {
71
67
  import {
72
68
  evaluateSkillToolSurface,
73
69
  skillActivationFromToolDetails
74
- } from "./chunk-BEY543CS.js";
70
+ } from "./chunk-GOXNB3AO.js";
71
+ import {
72
+ DEFAULT_AUTONOMY_EXPOSURE,
73
+ DEFAULT_AUTONOMY_LEVEL,
74
+ autonomyAskRejection,
75
+ autonomyDenyRejection,
76
+ mapAutonomy
77
+ } from "./chunk-HWUFFB6L.js";
75
78
  import {
76
79
  DEFAULT_TOOL_RESULT_MAX_BYTES,
77
80
  OBSERVATION_POLICY_SLACK_BYTES,
@@ -84,8 +87,9 @@ import {
84
87
  reserveObservation,
85
88
  shapeToolResult,
86
89
  toolResultContextText,
90
+ toolResultDigestFor,
87
91
  truncateHead
88
- } from "./chunk-5M54SPOL.js";
92
+ } from "./chunk-ODFEOB4F.js";
89
93
  import {
90
94
  GREP_MAX_LINE_LENGTH,
91
95
  truncateLine
@@ -124,7 +128,7 @@ import {
124
128
  } from "./chunk-BNAZZHFG.js";
125
129
  import {
126
130
  BusChannels
127
- } from "./chunk-TTNYS3EA.js";
131
+ } from "./chunk-G7MUEIGA.js";
128
132
  import {
129
133
  init_esm_shims
130
134
  } from "./chunk-3R73A4XB.js";
@@ -238,670 +242,327 @@ function cloneArtifact(artifact) {
238
242
  return clone;
239
243
  }
240
244
 
241
- // src/engine/loop-guard.ts
245
+ // src/tools/ask-user.ts
242
246
  init_esm_shims();
243
- var LOOP_GUARD_REGISTRATION_ID = "guard.loop";
244
- var INTERACTIVE_LOOP_BLOCK_BUDGET = 2;
245
- var WORKER_LOOP_BLOCK_CALLS_PER_BLOCK = 10;
246
- function workerLoopBlockBudget(toolCalls) {
247
- if (!Number.isSafeInteger(toolCalls) || toolCalls <= 0) return INTERACTIVE_LOOP_BLOCK_BUDGET;
248
- return Math.max(INTERACTIVE_LOOP_BLOCK_BUDGET, Math.ceil(toolCalls / WORKER_LOOP_BLOCK_CALLS_PER_BLOCK));
247
+ import { randomUUID } from "node:crypto";
248
+ import { mkdir, writeFile } from "node:fs/promises";
249
+ import path from "node:path";
250
+
251
+ // src/domains/safety/decision-presentation.ts
252
+ init_esm_shims();
253
+ var ACTION_CLASSES = /* @__PURE__ */ new Set([
254
+ "read",
255
+ "write",
256
+ "execute",
257
+ "dispatch",
258
+ "system_modify",
259
+ "git_destructive",
260
+ "unknown"
261
+ ]);
262
+ function decisionActionClass(value) {
263
+ return typeof value === "string" && ACTION_CLASSES.has(value) ? value : "unknown";
264
+ }
265
+ function actionConsequence(actionClass) {
266
+ switch (actionClass) {
267
+ case "read":
268
+ return { affectedScope: "workspace", reversibility: "reversible" };
269
+ case "write":
270
+ return { affectedScope: "workspace", reversibility: "reversible" };
271
+ case "execute":
272
+ case "dispatch":
273
+ return { affectedScope: "workspace", reversibility: "limited" };
274
+ case "system_modify":
275
+ case "git_destructive":
276
+ case "unknown":
277
+ return { affectedScope: "system", reversibility: "unknown" };
278
+ }
279
+ }
280
+ function decisionFactsForPermission(input) {
281
+ const exposure = input.exposure ?? "local";
282
+ const consequence = actionConsequence(input.actionClass);
283
+ return {
284
+ requestKind: "permission",
285
+ axis: input.axis,
286
+ exposure,
287
+ affectedScope: exposure === "outward" ? "outward" : consequence.affectedScope,
288
+ reversibility: exposure === "outward" ? "limited" : consequence.reversibility,
289
+ origin: input.origin,
290
+ authorityEffect: "grants-once",
291
+ tool: input.tool,
292
+ actionClass: input.actionClass
293
+ };
249
294
  }
250
- var LOOP_SYNTHESIS_BACKSTOP_DENIALS = 2;
251
- var WIDE_BATCH_DENIAL_FLOOR = 32;
252
- var DEFAULT_WORKER_TOOL_CALL_CAP = GUARDRAIL_DEFAULTS.workerToolCallCap;
253
- var DEFAULT_ORCH_TURN_TOOL_CALL_BUDGET = GUARDRAIL_DEFAULTS.turnToolCallBudget;
254
- var ORCH_TURN_TOOL_CALL_HARD_MARGIN = 15;
255
- var LOOP_GUARD_TURN_LIMIT = 32;
256
- var SUCCEEDED_FINGERPRINT_LIMIT = 128;
257
- var NO_TURN_BUCKET = "no-turn";
258
- var SIZE_ONLY_ARG_KEYS = /* @__PURE__ */ new Set(["limit", "offset", "max_bytes", "context"]);
259
- var RESULT_STAGNATION_THRESHOLD = 3;
260
- var CROSS_ARGUMENT_RESULT_MIN_BYTES = 64;
261
- function stagnationFingerprint(tool, args) {
262
- const reduced = {};
263
- for (const [key, value] of Object.entries(args ?? {})) {
264
- if (!SIZE_ONLY_ARG_KEYS.has(key)) reduced[key] = value;
265
- }
266
- return hashToolCall(tool, reduced);
295
+ function decisionFactsForAnswer(exposure) {
296
+ return {
297
+ requestKind: "question",
298
+ axis: { kind: "answer" },
299
+ exposure,
300
+ affectedScope: exposure === "outward" ? "outward" : "conversation",
301
+ reversibility: exposure === "outward" ? "limited" : "reversible",
302
+ origin: { kind: "main" },
303
+ authorityEffect: "records-answer"
304
+ };
267
305
  }
268
- function stagnationBlockReason(tool, identicalResults) {
269
- return `loop detected: the last ${identicalResults} ${tool} calls returned byte-identical results even though size arguments (limit/offset) changed. Raising them is not producing new information. Re-read the result above, change the query or tool, or answer from what you have.`;
306
+ function classifyTier(facts) {
307
+ if (facts.origin.kind === "worker") return "worker";
308
+ if (facts.affectedScope === "system") return "system";
309
+ if (facts.exposure === "outward" || facts.affectedScope === "outward") return "outward";
310
+ if (facts.axis.kind === "safety-net") return "safety-net";
311
+ if (facts.authorityEffect === "grants-once") return "workspace";
312
+ return "conversation";
313
+ }
314
+ function tierIdentity(tier) {
315
+ switch (tier) {
316
+ case "conversation":
317
+ return { tierLabel: "Conversational answer", title: "Answer a question", semanticToken: "accent" };
318
+ case "workspace":
319
+ return { tierLabel: "Workspace authority", title: "Approve workspace action", semanticToken: "action" };
320
+ case "outward":
321
+ return { tierLabel: "Outward consequence", title: "Confirm outward consequence", semanticToken: "warning" };
322
+ case "safety-net":
323
+ return { tierLabel: "Safety-net confirmation", title: "Safety-net confirmation", semanticToken: "warning" };
324
+ case "system":
325
+ return { tierLabel: "System change", title: "Approve system change", semanticToken: "warning" };
326
+ case "worker":
327
+ return { tierLabel: "Worker escalation", title: "Worker needs approval", semanticToken: "action" };
328
+ }
329
+ }
330
+ function requestedByCopy(facts) {
331
+ const requester = facts.origin.kind === "worker" ? `worker ${facts.origin.agentId} (run ${facts.origin.runId})` : "main agent";
332
+ if (facts.axis.kind === "answer") return requester;
333
+ if (facts.axis.kind === "safety-net") return `${requester} through safety-net rail ${facts.axis.ruleId}`;
334
+ return `${requester} through autonomy level (${facts.axis.level})`;
335
+ }
336
+ function authorizationCopy(facts) {
337
+ if (facts.authorityEffect === "records-answer") {
338
+ return facts.exposure === "outward" ? "Your response records an answer about an outward step. It does not publish or send anything by itself." : "Your response records an answer for this interview. It grants no tool authority.";
339
+ }
340
+ const tool = facts.tool ?? "the presented tool";
341
+ const actionClass = facts.actionClass ?? "unknown";
342
+ if (tool === "ask_user" && facts.exposure === "outward") {
343
+ return "Approval opens this one outward-decision interview. It does not publish or send anything by itself.";
344
+ }
345
+ return `Approval authorizes one ${actionClass} call to ${tool}. It does not change the autonomy level.`;
346
+ }
347
+ function consequenceCopy(facts, tier) {
348
+ switch (tier) {
349
+ case "conversation":
350
+ return "The answer stays in this session's decision record.";
351
+ case "workspace":
352
+ return facts.actionClass === "read" ? "The call reads within the workspace without mutating it." : "The call can affect files or processes inside the workspace.";
353
+ case "outward":
354
+ return "The resulting step can reach people or systems outside the workspace.";
355
+ case "safety-net":
356
+ return "An always-on safety-net rail requires a one-shot operator decision before this call can run.";
357
+ case "system":
358
+ return "The call can change state outside the workspace or state the classifier cannot safely bound.";
359
+ case "worker":
360
+ return "A dispatched worker is parked. Your answer returns only to that run's exact request.";
361
+ }
362
+ }
363
+ function reversibilityCopy(facts) {
364
+ if (facts.requestKind === "question") {
365
+ return facts.reversibility === "reversible" ? "Reversible: yes. The recorded answer can be superseded later." : "Reversible: not guaranteed. An outward step that later uses this answer may have lasting effects.";
366
+ }
367
+ if (facts.actionClass === "read" && facts.reversibility === "reversible") {
368
+ return "Reversible: yes. This call does not mutate the workspace.";
369
+ }
370
+ switch (facts.reversibility) {
371
+ case "reversible":
372
+ return "Reversible: yes. Workspace changes can be reviewed and reverted.";
373
+ case "limited":
374
+ return "Reversible: not guaranteed. Effects that already ran or reached an outward system may remain.";
375
+ case "unknown":
376
+ return "Reversible: unknown. Treat the effect as not safely reversible.";
377
+ }
378
+ }
379
+ function requiredActions(facts, tier) {
380
+ if (facts.authorityEffect === "records-answer") {
381
+ return [
382
+ {
383
+ id: "record-answer",
384
+ label: facts.exposure === "outward" ? "Record outward answer" : "Record answer",
385
+ consequence: "Records the selected or typed answer without granting tool authority."
386
+ },
387
+ { id: "cancel", label: "Cancel", consequence: "Closes the interview without recording an answer for this round." }
388
+ ];
389
+ }
390
+ const approveLabel = tier === "system" ? "Approve system change once" : tier === "safety-net" ? "Approve guarded action once" : tier === "worker" ? "Approve worker request once" : tier === "outward" ? "Approve outward decision once" : "Approve workspace action once";
391
+ const stopConsequence = facts.origin.kind === "worker" ? "Denies this worker request and ends the active main-agent turn. Other worker requests retain their timeout policy." : "Denies every parked request from this turn and ends the run.";
392
+ return [
393
+ {
394
+ id: "approve-once",
395
+ label: approveLabel,
396
+ consequence: "Runs only the presented request and does not change the autonomy level."
397
+ },
398
+ { id: "deny", label: "Deny this request", consequence: "Denies only the presented request and advances the queue." },
399
+ { id: "stop", label: "Deny and stop", consequence: stopConsequence }
400
+ ];
270
401
  }
271
- function crossArgumentResultMessage(tool, distinctArguments) {
272
- return `loop guard: ${distinctArguments} distinct ${tool} arguments returned the same substantial result this turn. The tool may be ignoring an argument; re-read the result, change tools, or answer from what you have.`;
402
+ function classifyDecisionPresentation(facts) {
403
+ const tier = classifyTier(facts);
404
+ return {
405
+ tier,
406
+ ...tierIdentity(tier),
407
+ authorizationCopy: authorizationCopy(facts),
408
+ consequenceCopy: consequenceCopy(facts, tier),
409
+ reversibilityCopy: reversibilityCopy(facts),
410
+ requestedByCopy: requestedByCopy(facts),
411
+ requiredActions: requiredActions(facts, tier)
412
+ };
273
413
  }
274
- function loopBlockBaseReason(tool, repeatCount, priorSuccesses) {
275
- const evidence = priorSuccesses > 0 ? `This exact call already succeeded ${priorSuccesses} ${priorSuccesses === 1 ? "time" : "times"} this run; its result is already in the conversation above \u2014 re-read that result before calling tools again. ` : "";
276
- return `loop detected: ${tool} was called ${repeatCount} times with identical arguments among this turn's recent tool calls. Repeating the exact call is blocked. ${evidence}Change strategy: vary the arguments, use a different tool, or explain what new information you expect before retrying.`;
414
+
415
+ // src/tools/ask-user.ts
416
+ var ASK_USER_OTHER_LABEL = "Other (type your answer)";
417
+ var MAX_DECISIONS = 24;
418
+ var DEFAULT_MAX_ROUNDS = 6;
419
+ var MAX_ROUNDS = 24;
420
+ var askUserParameters = typebox_exports.Object({
421
+ action: typebox_exports.Optional(
422
+ StringEnum(["ask", "complete"], {
423
+ description: "ask presents a round of questions; complete (exactly once) records decisions before final prose."
424
+ })
425
+ ),
426
+ mode: typebox_exports.Optional(
427
+ StringEnum(["round", "single_question"], {
428
+ description: "Caps how many questions this call may carry. single_question accepts exactly 1 and rejects the call otherwise (interviews); round accepts 1 to 4 tightly related confirmations. Default is round."
429
+ })
430
+ ),
431
+ questions: typebox_exports.Optional(
432
+ typebox_exports.Array(
433
+ typebox_exports.Object({
434
+ question: typebox_exports.String({ description: "Question for the operator." }),
435
+ header: typebox_exports.Optional(typebox_exports.String({ description: "Short header." })),
436
+ options: typebox_exports.Optional(
437
+ typebox_exports.Array(
438
+ typebox_exports.Object({
439
+ label: typebox_exports.String({ description: "Choice label." }),
440
+ description: typebox_exports.Optional(typebox_exports.String({ description: "Short explanation." }))
441
+ }),
442
+ { description: "Suggested choices, recommended first. The UI adds an implicit Other choice." }
443
+ )
444
+ ),
445
+ multi_select: typebox_exports.Optional(typebox_exports.Boolean({ description: "Allow multiple selections." }))
446
+ }),
447
+ { description: "For action=ask: 1 to 4 questions, or exactly 1 when mode=single_question." }
448
+ )
449
+ ),
450
+ max_rounds: typebox_exports.Optional(typebox_exports.Integer({ description: "Round limit for this turn (default 6, max 24)." })),
451
+ decisions: typebox_exports.Optional(
452
+ typebox_exports.Array(
453
+ typebox_exports.Object({
454
+ key: typebox_exports.String({ description: "Stable snake_case key." }),
455
+ value: typebox_exports.String({ description: "Decision value." }),
456
+ label: typebox_exports.Optional(typebox_exports.String({ description: "Human-readable label." }))
457
+ }),
458
+ { description: "For action=complete: compact decisions." }
459
+ )
460
+ ),
461
+ summary: typebox_exports.Optional(typebox_exports.String({ description: "Short closeout summary for action=complete." })),
462
+ exposure: typebox_exports.Optional(
463
+ StringEnum(["local", "outward"], {
464
+ description: "Blast radius of this gate. local (default) stays inside the workspace. outward means answering it publishes or sends something the operator cannot take back (filing an issue or PR, posting a comment, pushing, releasing); at autonomy auto-edit an outward gate parks for the operator instead of being answered automatically."
465
+ })
466
+ )
467
+ });
468
+ function isRecord(value) {
469
+ return typeof value === "object" && value !== null && !Array.isArray(value);
277
470
  }
278
- function synthesisLockoutDirective() {
279
- return "loop guard: this turn reached its tool-call limit after repeated identical calls, so tool calls are now disabled for the rest of this turn. Everything you retrieved is already in the conversation above. Answer the operator now, in plain prose, from what you have gathered. Do not write tool-call markup such as <tool_call> blocks; tool calls are disabled and will not run.";
471
+ function trimOptionalString(value) {
472
+ if (typeof value !== "string") return void 0;
473
+ const trimmed = value.trim();
474
+ return trimmed.length > 0 ? trimmed : void 0;
280
475
  }
281
- function synthesisBackstopReason(tool) {
282
- return `loop guard: tool calls stayed disabled and ${tool} was called again instead of answering, so the turn is being stopped. Summarize what you found for the operator.`;
476
+ function normalizeAskUserMode(value) {
477
+ if (value === void 0) return {};
478
+ const raw = trimOptionalString(value)?.toLowerCase().replace(/[-\s]+/g, "_");
479
+ if (!raw) return { error: "mode must be round or single_question" };
480
+ if (raw === "round" || raw === "batch") return { mode: "round" };
481
+ if (raw === "single" || raw === "single_question" || raw === "one_question") {
482
+ return { mode: "single_question" };
483
+ }
484
+ return { error: "mode must be round or single_question" };
283
485
  }
284
- function workerExplorationSynthesisDirective(limit) {
285
- return `worker exploration budget reached (${limit}); exploration tools are now disabled for this run. Answer in plain prose from the evidence already gathered, with live path:line citations. Do not retry or substitute another tool call.`;
486
+ function normalizeExposure(value) {
487
+ if (value === void 0) return { exposure: DEFAULT_AUTONOMY_EXPOSURE };
488
+ const raw = trimOptionalString(value)?.toLowerCase();
489
+ if (raw === "local" || raw === "outward") return { exposure: raw };
490
+ return { error: "exposure must be local or outward" };
286
491
  }
287
- function disallowedToolsText(deliveryTools) {
288
- const candidates = ["code_nav", "ls", "grep", "find", "context", "git", "shell commands"];
289
- const excluded = candidates.filter((tool) => !deliveryTools.includes(tool));
290
- return excluded.join(", ");
492
+ function askUserExposure(args) {
493
+ if (args?.exposure === void 0) return DEFAULT_AUTONOMY_EXPOSURE;
494
+ return normalizeExposure(args.exposure).exposure === "local" ? "local" : "outward";
291
495
  }
292
- function workerLiveReadReserveDirective(remaining, deliveryTools) {
293
- const admitted = deliveryTools.length > 0 ? `read and ${deliveryTools.join("/")}` : "the read tool";
294
- const finish = deliveryTools.length > 0 ? `Finish the work: re-read what you will cite and write it with ${deliveryTools.join(" or ")}.` : "Read a specific live file needed for the handoff.";
295
- return `worker live-read reserve: broad orientation is complete and ${remaining} ${remaining === 1 ? "call remains" : "calls remain"}. Only ${admitted} ${deliveryTools.length > 0 ? "are" : "is"} admitted now. ${finish} Do not substitute ${disallowedToolsText(deliveryTools)}.`;
496
+ function normalizeMaxRounds(value) {
497
+ if (value === void 0) return {};
498
+ if (typeof value !== "number" || !Number.isInteger(value)) {
499
+ return { error: `max_rounds must be an integer from 1 to ${MAX_ROUNDS}` };
500
+ }
501
+ if (value < 1 || value > MAX_ROUNDS) {
502
+ return { error: `max_rounds must be an integer from 1 to ${MAX_ROUNDS}` };
503
+ }
504
+ return { maxRounds: value };
296
505
  }
297
- function workerDeliveryOnlyDirective(limit, deliveryTools) {
298
- return `worker discovery budget reached (${limit}); exploration tools are disabled for the rest of this run. Only read and ${deliveryTools.join("/")} are admitted now. Write what you have with ${deliveryTools.join(" or ")} and finish. Do not substitute ${disallowedToolsText(deliveryTools)}.`;
506
+ function normalizeOptions(value, index) {
507
+ if (value === void 0) return {};
508
+ if (!Array.isArray(value)) return { error: `questions[${index}].options must be an array` };
509
+ const options = [];
510
+ for (let optionIndex = 0; optionIndex < value.length; optionIndex += 1) {
511
+ const raw = value[optionIndex];
512
+ if (!isRecord(raw)) return { error: `questions[${index}].options[${optionIndex}] must be an object` };
513
+ const label = trimOptionalString(raw.label);
514
+ if (!label) return { error: `questions[${index}].options[${optionIndex}].label is required` };
515
+ const option = { label };
516
+ const description = trimOptionalString(raw.description);
517
+ if (description) option.description = description;
518
+ options.push(option);
519
+ }
520
+ return options.length > 0 ? { options } : {};
299
521
  }
300
- var DEAD_TOOL_CALL_MARKUP_PATTERNS = [
301
- /<tool_call>[\s\S]*?<\/tool_call>/gi,
302
- /<function=[\w.-]+>[\s\S]*?<\/function>/gi,
303
- /<tool_call>\s*(?:<function=|[{[])[\s\S]*$/i,
304
- /<function=[\w.-]+>[\s\S]*$/i
305
- ];
306
- function stripDeadToolCallMarkup(text) {
307
- let out = text;
308
- for (const pattern of DEAD_TOOL_CALL_MARKUP_PATTERNS) {
309
- out = out.replace(pattern, "");
522
+ function normalizeAskUserQuestions(args) {
523
+ const rawQuestions = args.questions;
524
+ if (!Array.isArray(rawQuestions)) return { error: "questions must be an array" };
525
+ if (rawQuestions.length < 1) return { error: "questions must contain at least 1 item" };
526
+ if (rawQuestions.length > 4) return { error: "questions must contain at most 4 items" };
527
+ const questions = [];
528
+ for (let index = 0; index < rawQuestions.length; index += 1) {
529
+ const raw = rawQuestions[index];
530
+ if (!isRecord(raw)) return { error: `questions[${index}] must be an object` };
531
+ const questionText = trimOptionalString(raw.question);
532
+ if (!questionText) return { error: `questions[${index}].question is required` };
533
+ const normalized = { question: questionText };
534
+ const header = trimOptionalString(raw.header);
535
+ if (header) normalized.header = header;
536
+ const options = normalizeOptions(raw.options, index);
537
+ if (options.error) return { error: options.error };
538
+ if (options.options) normalized.options = options.options;
539
+ if (raw.multi_select === true) normalized.multi_select = true;
540
+ questions.push(normalized);
310
541
  }
311
- return out === text ? text : out.trim();
542
+ return { questions };
312
543
  }
313
- function lockedSynthesisFallbackText() {
314
- return "[Clio Coder] loop guard: tool calls were disabled for the rest of this turn, but the reply contained only tool-call markup, which cannot run and was removed. Ask me to continue with a different approach, or narrow the request.";
544
+ function normalizeDecision(raw, index) {
545
+ if (!isRecord(raw)) return { error: `decisions[${index}] must be an object` };
546
+ const key = trimOptionalString(raw.key);
547
+ if (!key) return { error: `decisions[${index}].key is required` };
548
+ const value = trimOptionalString(raw.value);
549
+ if (!value) return { error: `decisions[${index}].value is required` };
550
+ const decision = { key: toDecisionKey(key), value };
551
+ const label = trimOptionalString(raw.label);
552
+ if (label) decision.label = label;
553
+ return { decision };
315
554
  }
316
- function sanitizeLockedSynthesisMessage(message) {
317
- if (!message || typeof message !== "object" || message.role !== "assistant") return false;
318
- const record = message;
319
- const stopReason = record.stopReason;
320
- if (stopReason !== void 0 && stopReason !== "stop" && stopReason !== "length") return false;
321
- if (!Array.isArray(record.content)) return false;
322
- const blocks = record.content;
323
- if (blocks.some((block) => block?.type === "toolCall")) return false;
324
- let changed = false;
325
- for (const block of blocks) {
326
- if (block?.type !== "text" || typeof block.text !== "string") continue;
327
- const stripped = stripDeadToolCallMarkup(block.text);
328
- if (stripped !== block.text) {
329
- block.text = stripped;
330
- changed = true;
331
- }
332
- }
333
- if (!changed) return false;
334
- const remaining = blocks.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("").trim();
335
- if (remaining.length === 0) {
336
- const firstText = blocks.find(
337
- (block) => block?.type === "text" && typeof block.text === "string"
338
- );
339
- if (firstText) firstText.text = lockedSynthesisFallbackText();
340
- else blocks.push({ type: "text", text: lockedSynthesisFallbackText() });
555
+ function normalizeDecisions(value) {
556
+ if (value === void 0) return {};
557
+ if (!Array.isArray(value)) return { error: "decisions must be an array" };
558
+ if (value.length > MAX_DECISIONS) return { error: `decisions must contain at most ${MAX_DECISIONS} items` };
559
+ const decisions = [];
560
+ for (let index = 0; index < value.length; index += 1) {
561
+ const normalized = normalizeDecision(value[index], index);
562
+ if (normalized.error) return { error: normalized.error };
563
+ if (normalized.decision) decisions.push(normalized.decision);
341
564
  }
342
- return true;
343
- }
344
- function isLoopGuardSynthesisBackstopReason(reason) {
345
- return /^loop guard: tool calls stayed disabled and .+ was called again instead of answering/.test(reason);
346
- }
347
- function readOrchTurnToolCallBudget(env = process.env) {
348
- const soft = resolveGuardrail("turnToolCallBudget", env);
349
- return { soft, hard: soft + ORCH_TURN_TOOL_CALL_HARD_MARGIN };
350
- }
351
- function createLoopGuardRegistration(options) {
352
- const budget = options.turnBlockBudget ?? INTERACTIVE_LOOP_BLOCK_BUDGET;
353
- const cap = options.toolCallCap;
354
- const softLimit = options.toolCallSoftLimit !== void 0 && Number.isSafeInteger(options.toolCallSoftLimit) && options.toolCallSoftLimit > 0 && (cap === void 0 || options.toolCallSoftLimit <= cap) ? options.toolCallSoftLimit : void 0;
355
- const turnBudget = options.turnToolCallBudget;
356
- const synthesisLockout = options.turnSynthesisLockout === true;
357
- const softReadReserve = options.toolCallSoftReadReserve ?? 0;
358
- const softReadReserveThreshold = softLimit !== void 0 && softReadReserve > 0 && softLimit > softReadReserve ? softLimit - softReadReserve : null;
359
- const deliveryTools = [...new Set(options.deliveryTools ?? [])];
360
- const reserveAdmits = (tool) => isReserveAdmittedTool(tool, deliveryTools);
361
- let softAdmittedCount = 0;
362
- let softReadReserveEntered = false;
363
- let reserveDenials = { denials: 0 };
364
- let deniedAttempts = 0;
365
- const denialBudget = cap === void 0 ? void 0 : Math.max(cap, WIDE_BATCH_DENIAL_FLOOR);
366
- const boundRunDenials = (effects, input) => {
367
- if (denialBudget === void 0 || !effects.some((effect) => effect.kind === "block_tool")) return effects;
368
- deniedAttempts += 1;
369
- if (deniedAttempts <= denialBudget) return effects;
370
- return [{ kind: "block_tool", reason: synthesisBackstopReason(input.toolName ?? "unknown"), severity: "hard-block" }];
371
- };
372
- const reserve = options.toolCallReserve ?? 0;
373
- const reserveThreshold = cap !== void 0 && reserve > 0 && cap > reserve ? cap - reserve : null;
374
- let reserveDirectiveEmitted = false;
375
- const blocksByTurn = /* @__PURE__ */ new Map();
376
- const callsByTurn = /* @__PURE__ */ new Map();
377
- const lockoutByTurn = /* @__PURE__ */ new Map();
378
- let capLockout = null;
379
- let softLockout = null;
380
- const succeededFingerprints = /* @__PURE__ */ new Map();
381
- const stagnationByTurn = /* @__PURE__ */ new Map();
382
- const crossArgumentResultsByTurn = /* @__PURE__ */ new Map();
383
- let count = 0;
384
- const enterSoftReadReserve = () => {
385
- if (softReadReserveEntered) return;
386
- softReadReserveEntered = true;
387
- options.onSoftReadReserve?.();
388
- };
389
- const bumpBoundedCounter = (store, key, limit = LOOP_GUARD_TURN_LIMIT) => {
390
- if (!store.has(key)) {
391
- while (store.size >= limit) {
392
- const oldest = store.keys().next().value;
393
- if (typeof oldest !== "string") break;
394
- store.delete(oldest);
395
- }
396
- }
397
- const next = (store.get(key) ?? 0) + 1;
398
- store.set(key, next);
399
- return next;
400
- };
401
- const resultCarriesEvidence = (details) => {
402
- const observation = details?.observation;
403
- if (observation === null || typeof observation !== "object" || Array.isArray(observation)) return true;
404
- const record = observation;
405
- return !(record.truncated === true && record.shownCount === 0);
406
- };
407
- const recordSuccessfulResult = (input) => {
408
- if (input.metadata?.resultKind !== "ok") return;
409
- if (!resultCarriesEvidence(input.toolResultDetails)) return;
410
- const tool = input.toolName;
411
- if (typeof tool !== "string" || tool.length === 0) return;
412
- bumpBoundedCounter(succeededFingerprints, hashToolCall(tool, input.toolArgs ?? {}), SUCCEEDED_FINGERPRINT_LIMIT);
413
- };
414
- const recordResultForStagnation = (input) => {
415
- const turnKey = input.turnId ?? NO_TURN_BUCKET;
416
- const tool = input.toolName;
417
- const resultFingerprint = input.metadata?.resultFingerprint;
418
- if (typeof tool !== "string" || tool.length === 0 || input.metadata?.resultKind !== "ok" || typeof resultFingerprint !== "string") {
419
- stagnationByTurn.delete(turnKey);
420
- return;
421
- }
422
- const reducedFingerprint = stagnationFingerprint(tool, input.toolArgs);
423
- const previous = stagnationByTurn.get(turnKey);
424
- if (previous !== void 0 && previous.reducedFingerprint === reducedFingerprint && previous.resultFingerprint === resultFingerprint) {
425
- previous.streak += 1;
426
- return;
427
- }
428
- if (!stagnationByTurn.has(turnKey)) {
429
- while (stagnationByTurn.size >= LOOP_GUARD_TURN_LIMIT) {
430
- const oldest = stagnationByTurn.keys().next().value;
431
- if (typeof oldest !== "string") break;
432
- stagnationByTurn.delete(oldest);
433
- }
434
- }
435
- stagnationByTurn.set(turnKey, { reducedFingerprint, resultFingerprint, streak: 1 });
436
- };
437
- const crossArgumentResultEffects = (input) => {
438
- const turnKey = input.turnId ?? NO_TURN_BUCKET;
439
- const tool = input.toolName;
440
- const resultFingerprint = input.metadata?.resultFingerprint;
441
- const resultBytes = input.metadata?.resultBytes;
442
- if (typeof tool !== "string" || tool.length === 0 || input.metadata?.actionClass !== "read" || input.metadata?.resultKind !== "ok" || typeof resultFingerprint !== "string" || typeof resultBytes !== "number" || resultBytes < CROSS_ARGUMENT_RESULT_MIN_BYTES || !resultCarriesEvidence(input.toolResultDetails)) {
443
- crossArgumentResultsByTurn.delete(turnKey);
444
- return [];
445
- }
446
- const argumentFingerprint = stagnationFingerprint(tool, input.toolArgs);
447
- const previous = crossArgumentResultsByTurn.get(turnKey);
448
- if (previous?.tool === tool && previous.resultFingerprint === resultFingerprint) {
449
- if (previous.warned) return [];
450
- previous.argumentFingerprints.add(argumentFingerprint);
451
- if (previous.argumentFingerprints.size >= RESULT_STAGNATION_THRESHOLD) {
452
- previous.warned = true;
453
- return [
454
- {
455
- kind: "annotate_tool_result",
456
- message: crossArgumentResultMessage(tool, previous.argumentFingerprints.size),
457
- severity: "warn"
458
- }
459
- ];
460
- }
461
- return [];
462
- }
463
- if (!crossArgumentResultsByTurn.has(turnKey)) {
464
- while (crossArgumentResultsByTurn.size >= LOOP_GUARD_TURN_LIMIT) {
465
- const oldest = crossArgumentResultsByTurn.keys().next().value;
466
- if (typeof oldest !== "string") break;
467
- crossArgumentResultsByTurn.delete(oldest);
468
- }
469
- }
470
- crossArgumentResultsByTurn.set(turnKey, {
471
- tool,
472
- resultFingerprint,
473
- argumentFingerprints: /* @__PURE__ */ new Set([argumentFingerprint]),
474
- warned: false
475
- });
476
- return [];
477
- };
478
- const bumpTurnBlocks = (turnId) => bumpBoundedCounter(blocksByTurn, turnId);
479
- const enterLockout = (input, turnKey, state) => {
480
- if (!lockoutByTurn.has(turnKey)) {
481
- while (lockoutByTurn.size >= LOOP_GUARD_TURN_LIMIT) {
482
- const oldest = lockoutByTurn.keys().next().value;
483
- if (typeof oldest !== "string") break;
484
- lockoutByTurn.delete(oldest);
485
- }
486
- }
487
- lockoutByTurn.set(turnKey, {
488
- ...state,
489
- denials: 0,
490
- ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
491
- });
492
- };
493
- const reachesLockoutBackstop = (state, input) => {
494
- if (input.correlationId !== void 0 && state.correlationId === input.correlationId) return false;
495
- state.denials += 1;
496
- if (input.correlationId !== void 0) state.correlationId = input.correlationId;
497
- else delete state.correlationId;
498
- return state.denials > LOOP_SYNTHESIS_BACKSTOP_DENIALS;
499
- };
500
- const emitLoopBlocked = (input, info, at) => {
501
- const payload = {
502
- tool: info.tool,
503
- repeatCount: info.repeatCount,
504
- blocksThisTurn: info.blocksThisTurn,
505
- budget,
506
- interrupted: info.disposition === "stop",
507
- disposition: info.disposition,
508
- at,
509
- ...input.turnId !== void 0 ? { turnId: input.turnId } : {}
510
- };
511
- options.bus?.emit(BusChannels.LoopBlocked, payload);
512
- };
513
- const emitBudgetEvent = (input, callsThisTurn, interrupted, at) => {
514
- if (turnBudget === void 0) return;
515
- const payload = {
516
- tool: input.toolName ?? "unknown",
517
- callsThisTurn,
518
- softBudget: turnBudget.soft,
519
- hardCeiling: turnBudget.hard,
520
- interrupted,
521
- at,
522
- ...input.turnId !== void 0 ? { turnId: input.turnId } : {}
523
- };
524
- options.bus?.emit(BusChannels.ToolBudgetExceeded, payload);
525
- };
526
- const evaluateTurnBudget = (input, now) => {
527
- if (turnBudget === void 0) return null;
528
- const turnKey = input.turnId ?? NO_TURN_BUCKET;
529
- const callsThisTurn = bumpBoundedCounter(callsByTurn, turnKey);
530
- if (callsThisTurn >= turnBudget.hard) {
531
- if (callsThisTurn === turnBudget.hard) emitBudgetEvent(input, callsThisTurn, true, now);
532
- return [
533
- {
534
- kind: "block_tool",
535
- reason: `tool-call budget exhausted: ${callsThisTurn} tool calls in this turn reached the hard ceiling (${turnBudget.hard}); the turn is being stopped. Summarize what you found and wait for the operator.`,
536
- severity: "hard-block"
537
- }
538
- ];
539
- }
540
- if (callsThisTurn >= turnBudget.soft) {
541
- if (callsThisTurn === turnBudget.soft) emitBudgetEvent(input, callsThisTurn, false, now);
542
- return [
543
- {
544
- kind: "block_tool",
545
- reason: `tool-call budget: you have made ${callsThisTurn} tool calls in this turn (soft budget ${turnBudget.soft}). Every further tool call this turn will be blocked, so do not retry this call and do not substitute another one. Summarize what you have found so far in plain text, state the single next step you propose, and wait for the operator.`,
546
- severity: "hard-block"
547
- }
548
- ];
549
- }
550
- return null;
551
- };
552
- const blockAsLoop = (input, turnKey, tool, repeatCount, baseReason, now) => {
553
- const blocksThisTurn = bumpTurnBlocks(turnKey);
554
- const reachedBudget = blocksThisTurn >= budget;
555
- if (synthesisLockout && reachedBudget) {
556
- enterLockout(input, turnKey, { tool, repeatCount, blocksThisTurn });
557
- options.onSynthesisLockout?.();
558
- emitLoopBlocked(input, { tool, repeatCount, blocksThisTurn, disposition: "lockout" }, now);
559
- return [{ kind: "block_tool", reason: synthesisLockoutDirective(), severity: "hard-block" }];
560
- }
561
- emitLoopBlocked(input, { tool, repeatCount, blocksThisTurn, disposition: reachedBudget ? "stop" : "block" }, now);
562
- const reason = reachedBudget ? `${baseReason} Loop budget exhausted (${blocksThisTurn} blocks this turn); the agent is being stopped.` : baseReason;
563
- return [{ kind: "block_tool", reason, severity: "hard-block" }];
564
- };
565
- return {
566
- id: LOOP_GUARD_REGISTRATION_ID,
567
- description: "blocks repeated tool calls, enforces tool-call caps, and annotates substantial identical read results across distinct arguments",
568
- hooks: ["before_tool", "after_tool"],
569
- callCount: () => count,
570
- evaluate(input) {
571
- if (input.hook === "after_tool") {
572
- recordSuccessfulResult(input);
573
- recordResultForStagnation(input);
574
- const effects = [...crossArgumentResultEffects(input)];
575
- if (softReadReserveEntered && softLockout === null && deliveryTools.length === 0 && input.toolName === ToolNames.Read) {
576
- effects.push({ kind: "require_tool", toolName: ToolNames.Read });
577
- }
578
- if (reserveThreshold !== null && cap !== void 0 && !reserveDirectiveEmitted && count > reserveThreshold && count <= cap) {
579
- reserveDirectiveEmitted = true;
580
- effects.push({
581
- kind: "annotate_tool_result",
582
- message: workerSynthesisReserveDirective(Math.max(0, cap - count), cap),
583
- severity: "warn"
584
- });
585
- }
586
- return effects;
587
- }
588
- const now = options.now?.() ?? Date.now();
589
- const turnKey = input.turnId ?? NO_TURN_BUCKET;
590
- const decide = () => {
591
- if (capLockout !== null && cap !== void 0) {
592
- if (reachesLockoutBackstop(capLockout, input)) {
593
- return [
594
- { kind: "block_tool", reason: synthesisBackstopReason(input.toolName ?? "unknown"), severity: "hard-block" }
595
- ];
596
- }
597
- return [{ kind: "block_tool", reason: workerToolCallCapSynthesisReason(cap), severity: "hard-block" }];
598
- }
599
- if (softLockout !== null && softLimit !== void 0) {
600
- if (reachesLockoutBackstop(softLockout, input)) {
601
- return [
602
- { kind: "block_tool", reason: synthesisBackstopReason(input.toolName ?? "unknown"), severity: "hard-block" }
603
- ];
604
- }
605
- return [{ kind: "block_tool", reason: workerExplorationSynthesisDirective(softLimit), severity: "hard-block" }];
606
- }
607
- const lockout = synthesisLockout ? lockoutByTurn.get(turnKey) : void 0;
608
- if (lockout !== void 0) {
609
- if (reachesLockoutBackstop(lockout, input)) {
610
- emitLoopBlocked(
611
- input,
612
- {
613
- tool: lockout.tool,
614
- repeatCount: lockout.repeatCount,
615
- blocksThisTurn: lockout.blocksThisTurn,
616
- disposition: "stop"
617
- },
618
- now
619
- );
620
- return [{ kind: "block_tool", reason: synthesisBackstopReason(lockout.tool), severity: "hard-block" }];
621
- }
622
- return [{ kind: "block_tool", reason: synthesisLockoutDirective(), severity: "hard-block" }];
623
- }
624
- if (softLimit !== void 0) {
625
- const spentSoftLimit = softAdmittedCount >= softLimit;
626
- if (spentSoftLimit && deliveryTools.length === 0) {
627
- softLockout = {
628
- denials: 0,
629
- ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
630
- };
631
- options.onSynthesisLockout?.();
632
- return [{ kind: "block_tool", reason: workerExplorationSynthesisDirective(softLimit), severity: "hard-block" }];
633
- }
634
- const inReserve = softReadReserveThreshold !== null && softAdmittedCount >= softReadReserveThreshold;
635
- if (spentSoftLimit || inReserve) {
636
- enterSoftReadReserve();
637
- if (!reserveAdmits(input.toolName)) {
638
- if (reachesLockoutBackstop(reserveDenials, input)) {
639
- softLockout = {
640
- denials: 0,
641
- ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
642
- };
643
- options.onSynthesisLockout?.();
644
- return [{ kind: "block_tool", reason: workerExplorationSynthesisDirective(softLimit), severity: "hard-block" }];
645
- }
646
- return [
647
- {
648
- kind: "block_tool",
649
- reason: spentSoftLimit ? workerDeliveryOnlyDirective(softLimit, deliveryTools) : workerLiveReadReserveDirective(softLimit - softAdmittedCount, deliveryTools),
650
- severity: "hard-block"
651
- }
652
- ];
653
- }
654
- }
655
- }
656
- if (reserveThreshold !== null && cap !== void 0 && count >= reserveThreshold && !reserveAdmits(input.toolName)) {
657
- if (reachesLockoutBackstop(reserveDenials, input)) {
658
- if (synthesisLockout) {
659
- capLockout = {
660
- denials: 0,
661
- ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
662
- };
663
- options.onSynthesisLockout?.();
664
- }
665
- return [{ kind: "block_tool", reason: workerToolCallCapSynthesisReason(cap), severity: "hard-block" }];
666
- }
667
- return [
668
- {
669
- kind: "block_tool",
670
- reason: workerSynthesisReserveBlockReason(input.toolName ?? "unknown", Math.max(1, cap - count), cap),
671
- severity: "hard-block"
672
- }
673
- ];
674
- }
675
- count += 1;
676
- reserveDenials = { denials: 0 };
677
- if (softLimit !== void 0) {
678
- softAdmittedCount += 1;
679
- if (softReadReserveThreshold !== null && softAdmittedCount >= softReadReserveThreshold) enterSoftReadReserve();
680
- if (softAdmittedCount >= softLimit) {
681
- if (deliveryTools.length === 0) {
682
- softLockout = {
683
- denials: 0,
684
- ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
685
- };
686
- options.onSynthesisLockout?.();
687
- }
688
- options.onSoftLimitFinalCallAdmitted?.(input.toolCallId);
689
- }
690
- }
691
- if (cap !== void 0 && count > cap) {
692
- if (!synthesisLockout) {
693
- return [{ kind: "block_tool", reason: workerToolCallCapExceededReason(cap), severity: "hard-block" }];
694
- }
695
- capLockout = {
696
- denials: 0,
697
- ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
698
- };
699
- options.onSynthesisLockout?.();
700
- return [{ kind: "block_tool", reason: workerToolCallCapSynthesisReason(cap), severity: "hard-block" }];
701
- }
702
- const fingerprint = input.metadata?.callFingerprint;
703
- if (typeof fingerprint !== "string" || fingerprint.length === 0) {
704
- return evaluateTurnBudget(input, now) ?? [];
705
- }
706
- const verdict = options.safety.observeLoop(`${turnKey}|${fingerprint}`, now);
707
- if (verdict.looping) {
708
- const tool = input.toolName ?? "unknown";
709
- const priorSuccesses = succeededFingerprints.get(fingerprint) ?? 0;
710
- return blockAsLoop(
711
- input,
712
- turnKey,
713
- tool,
714
- verdict.count,
715
- loopBlockBaseReason(tool, verdict.count, priorSuccesses),
716
- now
717
- );
718
- }
719
- const toolName = input.toolName;
720
- if (typeof toolName === "string" && toolName.length > 0) {
721
- const entry = stagnationByTurn.get(turnKey);
722
- if (entry !== void 0 && entry.streak >= RESULT_STAGNATION_THRESHOLD - 1 && entry.reducedFingerprint === stagnationFingerprint(toolName, input.toolArgs)) {
723
- return blockAsLoop(
724
- input,
725
- turnKey,
726
- toolName,
727
- entry.streak + 1,
728
- stagnationBlockReason(toolName, entry.streak),
729
- now
730
- );
731
- }
732
- }
733
- return evaluateTurnBudget(input, now) ?? [];
734
- };
735
- return boundRunDenials(decide(), input);
736
- }
737
- };
738
- }
739
- function isReserveAdmittedTool(tool, deliveryTools) {
740
- return tool === ToolNames.Read || tool !== void 0 && deliveryTools.includes(tool);
741
- }
742
- function resolveDeliveryTools(allowedTools, product) {
743
- const candidates = product === "orientation" ? [ToolNames.Write, ToolNames.Edit, ToolNames.CodeNav] : [ToolNames.Write, ToolNames.Edit];
744
- if (allowedTools instanceof Set) {
745
- return candidates.filter((tool) => allowedTools.has(tool));
746
- }
747
- return candidates.filter((tool) => allowedTools.includes(tool));
748
- }
749
-
750
- // src/tools/ask-user.ts
751
- init_esm_shims();
752
- import { randomUUID } from "node:crypto";
753
- import { mkdir, writeFile } from "node:fs/promises";
754
- import path from "node:path";
755
- var ASK_USER_OTHER_LABEL = "Other (type your answer)";
756
- var MAX_DECISIONS = 24;
757
- var DEFAULT_MAX_ROUNDS = 6;
758
- var MAX_ROUNDS = 24;
759
- var askUserParameters = typebox_exports.Object({
760
- action: typebox_exports.Optional(
761
- StringEnum(["ask", "complete"], {
762
- description: "ask presents a round of questions; complete (exactly once) records decisions before final prose."
763
- })
764
- ),
765
- mode: typebox_exports.Optional(
766
- StringEnum(["round", "single_question"], {
767
- description: "Caps how many questions this call may carry. single_question accepts exactly 1 and rejects the call otherwise (interviews); round accepts 1 to 4 tightly related confirmations. Default is round."
768
- })
769
- ),
770
- questions: typebox_exports.Optional(
771
- typebox_exports.Array(
772
- typebox_exports.Object({
773
- question: typebox_exports.String({ description: "Question for the operator." }),
774
- header: typebox_exports.Optional(typebox_exports.String({ description: "Short header." })),
775
- options: typebox_exports.Optional(
776
- typebox_exports.Array(
777
- typebox_exports.Object({
778
- label: typebox_exports.String({ description: "Choice label." }),
779
- description: typebox_exports.Optional(typebox_exports.String({ description: "Short explanation." }))
780
- }),
781
- { description: "Suggested choices, recommended first. The UI adds an implicit Other choice." }
782
- )
783
- ),
784
- multi_select: typebox_exports.Optional(typebox_exports.Boolean({ description: "Allow multiple selections." }))
785
- }),
786
- { description: "For action=ask: 1 to 4 questions, or exactly 1 when mode=single_question." }
787
- )
788
- ),
789
- max_rounds: typebox_exports.Optional(typebox_exports.Integer({ description: "Round limit for this turn (default 6, max 24)." })),
790
- decisions: typebox_exports.Optional(
791
- typebox_exports.Array(
792
- typebox_exports.Object({
793
- key: typebox_exports.String({ description: "Stable snake_case key." }),
794
- value: typebox_exports.String({ description: "Decision value." }),
795
- label: typebox_exports.Optional(typebox_exports.String({ description: "Human-readable label." }))
796
- }),
797
- { description: "For action=complete: compact decisions." }
798
- )
799
- ),
800
- summary: typebox_exports.Optional(typebox_exports.String({ description: "Short closeout summary for action=complete." })),
801
- exposure: typebox_exports.Optional(
802
- StringEnum(["local", "outward"], {
803
- description: "Blast radius of this gate. local (default) stays inside the workspace. outward means answering it publishes or sends something the operator cannot take back (filing an issue or PR, posting a comment, pushing, releasing); at autonomy auto-edit an outward gate parks for the operator instead of being answered automatically."
804
- })
805
- )
806
- });
807
- function isRecord(value) {
808
- return typeof value === "object" && value !== null && !Array.isArray(value);
809
- }
810
- function trimOptionalString(value) {
811
- if (typeof value !== "string") return void 0;
812
- const trimmed = value.trim();
813
- return trimmed.length > 0 ? trimmed : void 0;
814
- }
815
- function normalizeAskUserMode(value) {
816
- if (value === void 0) return {};
817
- const raw = trimOptionalString(value)?.toLowerCase().replace(/[-\s]+/g, "_");
818
- if (!raw) return { error: "mode must be round or single_question" };
819
- if (raw === "round" || raw === "batch") return { mode: "round" };
820
- if (raw === "single" || raw === "single_question" || raw === "one_question") {
821
- return { mode: "single_question" };
822
- }
823
- return { error: "mode must be round or single_question" };
824
- }
825
- function normalizeExposure(value) {
826
- if (value === void 0) return { exposure: DEFAULT_AUTONOMY_EXPOSURE };
827
- const raw = trimOptionalString(value)?.toLowerCase();
828
- if (raw === "local" || raw === "outward") return { exposure: raw };
829
- return { error: "exposure must be local or outward" };
830
- }
831
- function askUserExposure(args) {
832
- if (args?.exposure === void 0) return DEFAULT_AUTONOMY_EXPOSURE;
833
- return normalizeExposure(args.exposure).exposure === "local" ? "local" : "outward";
834
- }
835
- function normalizeMaxRounds(value) {
836
- if (value === void 0) return {};
837
- if (typeof value !== "number" || !Number.isInteger(value)) {
838
- return { error: `max_rounds must be an integer from 1 to ${MAX_ROUNDS}` };
839
- }
840
- if (value < 1 || value > MAX_ROUNDS) {
841
- return { error: `max_rounds must be an integer from 1 to ${MAX_ROUNDS}` };
842
- }
843
- return { maxRounds: value };
844
- }
845
- function normalizeOptions(value, index) {
846
- if (value === void 0) return {};
847
- if (!Array.isArray(value)) return { error: `questions[${index}].options must be an array` };
848
- const options = [];
849
- for (let optionIndex = 0; optionIndex < value.length; optionIndex += 1) {
850
- const raw = value[optionIndex];
851
- if (!isRecord(raw)) return { error: `questions[${index}].options[${optionIndex}] must be an object` };
852
- const label = trimOptionalString(raw.label);
853
- if (!label) return { error: `questions[${index}].options[${optionIndex}].label is required` };
854
- const option = { label };
855
- const description = trimOptionalString(raw.description);
856
- if (description) option.description = description;
857
- options.push(option);
858
- }
859
- return options.length > 0 ? { options } : {};
860
- }
861
- function normalizeAskUserQuestions(args) {
862
- const rawQuestions = args.questions;
863
- if (!Array.isArray(rawQuestions)) return { error: "questions must be an array" };
864
- if (rawQuestions.length < 1) return { error: "questions must contain at least 1 item" };
865
- if (rawQuestions.length > 4) return { error: "questions must contain at most 4 items" };
866
- const questions = [];
867
- for (let index = 0; index < rawQuestions.length; index += 1) {
868
- const raw = rawQuestions[index];
869
- if (!isRecord(raw)) return { error: `questions[${index}] must be an object` };
870
- const questionText = trimOptionalString(raw.question);
871
- if (!questionText) return { error: `questions[${index}].question is required` };
872
- const normalized = { question: questionText };
873
- const header = trimOptionalString(raw.header);
874
- if (header) normalized.header = header;
875
- const options = normalizeOptions(raw.options, index);
876
- if (options.error) return { error: options.error };
877
- if (options.options) normalized.options = options.options;
878
- if (raw.multi_select === true) normalized.multi_select = true;
879
- questions.push(normalized);
880
- }
881
- return { questions };
882
- }
883
- function normalizeDecision(raw, index) {
884
- if (!isRecord(raw)) return { error: `decisions[${index}] must be an object` };
885
- const key = trimOptionalString(raw.key);
886
- if (!key) return { error: `decisions[${index}].key is required` };
887
- const value = trimOptionalString(raw.value);
888
- if (!value) return { error: `decisions[${index}].value is required` };
889
- const decision = { key: toDecisionKey(key), value };
890
- const label = trimOptionalString(raw.label);
891
- if (label) decision.label = label;
892
- return { decision };
893
- }
894
- function normalizeDecisions(value) {
895
- if (value === void 0) return {};
896
- if (!Array.isArray(value)) return { error: "decisions must be an array" };
897
- if (value.length > MAX_DECISIONS) return { error: `decisions must contain at most ${MAX_DECISIONS} items` };
898
- const decisions = [];
899
- for (let index = 0; index < value.length; index += 1) {
900
- const normalized = normalizeDecision(value[index], index);
901
- if (normalized.error) return { error: normalized.error };
902
- if (normalized.decision) decisions.push(normalized.decision);
903
- }
904
- return { decisions };
565
+ return { decisions };
905
566
  }
906
567
  function normalizeAskUserCall(args) {
907
568
  const rawAction = trimOptionalString(args.action)?.toLowerCase() ?? "ask";
@@ -939,6 +600,7 @@ function normalizeAskUserCall(args) {
939
600
  return {
940
601
  call: {
941
602
  action: "ask",
603
+ exposure: exposure.exposure ?? DEFAULT_AUTONOMY_EXPOSURE,
942
604
  ...mode.mode ? { mode: mode.mode } : {},
943
605
  questions: questions.questions,
944
606
  ...summary ? { summary } : {},
@@ -971,6 +633,7 @@ function createStandalonePolicy(options) {
971
633
  updatedAt: now,
972
634
  ...options?.sessionId ? { sessionId: options.sessionId } : {},
973
635
  ...options?.turnId ? { turnId: options.turnId } : {},
636
+ exposure: DEFAULT_AUTONOMY_EXPOSURE,
974
637
  rounds: [],
975
638
  decisions: [],
976
639
  inFlight: false,
@@ -985,6 +648,11 @@ function hydratePolicy(policy, options) {
985
648
  if (options?.sessionId && !policy.sessionId) policy.sessionId = options.sessionId;
986
649
  if (options?.turnId && !policy.turnId) policy.turnId = options.turnId;
987
650
  }
651
+ function mergePolicyExposure(policy, exposure) {
652
+ const merged = policy.exposure === "outward" || exposure === "outward" ? "outward" : "local";
653
+ policy.exposure = merged;
654
+ return merged;
655
+ }
988
656
  function transcriptQuestions(questions) {
989
657
  return questions.map((question) => ({
990
658
  question: question.question,
@@ -1077,6 +745,7 @@ async function persistAskUserTranscript(policy, options) {
1077
745
  ...policy.endedAt ? { endedAt: policy.endedAt } : {},
1078
746
  ...policy.sessionId ? { sessionId: policy.sessionId } : {},
1079
747
  ...policy.turnId ? { turnId: policy.turnId } : {},
748
+ exposure: policy.exposure ?? DEFAULT_AUTONOMY_EXPOSURE,
1080
749
  ...policy.summary ? { summary: policy.summary } : {},
1081
750
  decisions: policy.decisions,
1082
751
  rounds: policy.rounds
@@ -1096,6 +765,7 @@ function compactInterview(policy, event, latestAnswers = []) {
1096
765
  rounds: policy.rounds.length,
1097
766
  max_rounds: policy.maxCalls,
1098
767
  transcript_path: policy.transcriptPath ?? null,
768
+ exposure: policy.exposure ?? DEFAULT_AUTONOMY_EXPOSURE,
1099
769
  latest_answers: latestAnswers,
1100
770
  decisions: policy.decisions,
1101
771
  ...policy.summary ? { summary: policy.summary } : {},
@@ -1142,104 +812,632 @@ async function finalizeAskUserInterview(policy, reason, options) {
1142
812
  policy.endedAt = now;
1143
813
  policy.summary = policy.summary ?? `Interview closed by host: ${reason}.`;
1144
814
  }
1145
- await persistAskUserTranscript(policy, options);
815
+ await persistAskUserTranscript(policy, options);
816
+ }
817
+ async function finalizeAskUserInterviewForHost(policy, reason, options, onFinalized) {
818
+ await finalizeAskUserInterview(policy, reason, options);
819
+ onFinalized?.(policy);
820
+ }
821
+ async function defaultAskUserHandler() {
822
+ return cancelledAskUserResult();
823
+ }
824
+ function createAskUserTool(deps = {}) {
825
+ return {
826
+ name: ToolNames.AskUser,
827
+ description: "Run a host-owned operator interview: action=ask presents questions, action=complete records compact decisions before final prose. If cancelled, proceed with defaults and do not ask again.",
828
+ parameters: askUserParameters,
829
+ baseActionClass: "read",
830
+ executionMode: "sequential",
831
+ async run(args, options) {
832
+ const normalized = normalizeAskUserCall(args);
833
+ if (!normalized.call) return { kind: "error", message: `ask_user: ${normalized.error ?? "invalid input"}` };
834
+ const policy = options?.askUserPolicy ?? createStandalonePolicy(options);
835
+ hydratePolicy(policy, options);
836
+ const call = normalized.call;
837
+ if (call.action === "complete") {
838
+ return completeInterview(policy, "complete", options, call.summary, call.decisions ?? []);
839
+ }
840
+ const questions = call.questions ?? [];
841
+ const exposure = mergePolicyExposure(policy, call.exposure ?? DEFAULT_AUTONOMY_EXPOSURE);
842
+ if (call.max_rounds !== void 0) {
843
+ const nextLimit = policy.callCount === 0 ? call.max_rounds : Math.max(policy.maxCalls, call.max_rounds);
844
+ policy.maxCalls = Math.max(nextLimit, policy.callCount + 1);
845
+ }
846
+ if (policy.inFlight === true) {
847
+ return {
848
+ kind: "error",
849
+ message: "ask_user: an operator interview round is already in progress. Wait for that answer."
850
+ };
851
+ }
852
+ if (policy.status === "complete") {
853
+ await persistAskUserTranscript(policy, options);
854
+ return okInterviewResult(policy, "already_complete");
855
+ }
856
+ if (policy.status === "cancelled" || policy.cancelled === true) {
857
+ policy.status = "cancelled";
858
+ policy.cancelled = true;
859
+ await persistAskUserTranscript(policy, options);
860
+ return okInterviewResult(policy, "already_cancelled");
861
+ }
862
+ const key = questionKey(questions);
863
+ if (policy.askedQuestionKeys.has(key)) {
864
+ await persistAskUserTranscript(policy, options);
865
+ return okInterviewResult(policy, "duplicate_round_ignored");
866
+ }
867
+ if (policy.callCount >= policy.maxCalls) {
868
+ return completeInterview(
869
+ policy,
870
+ "round_limit_reached",
871
+ options,
872
+ `Interview closed after reaching the ${policy.maxCalls}-round limit.`
873
+ );
874
+ }
875
+ const requestedAt = (/* @__PURE__ */ new Date()).toISOString();
876
+ policy.inFlight = true;
877
+ policy.status = "active";
878
+ policy.updatedAt = requestedAt;
879
+ if (call.summary) policy.summary = call.summary;
880
+ const handler = deps.askUser ?? defaultAskUserHandler;
881
+ try {
882
+ const result = normalizeAskUserResult(
883
+ questions,
884
+ await handler(questions, {
885
+ ...options,
886
+ decisionPresentation: classifyDecisionPresentation(decisionFactsForAnswer(exposure))
887
+ })
888
+ );
889
+ const answeredAt = (/* @__PURE__ */ new Date()).toISOString();
890
+ policy.callCount += 1;
891
+ policy.askedQuestionKeys.add(key);
892
+ policy.answerCount += result.answers.length;
893
+ policy.updatedAt = answeredAt;
894
+ const round = {
895
+ round: policy.rounds.length + 1,
896
+ requestedAt,
897
+ answeredAt,
898
+ questions: transcriptQuestions(questions),
899
+ answers: transcriptAnswers(result.answers),
900
+ ...result.cancelled === true ? { cancelled: true } : {}
901
+ };
902
+ policy.rounds.push(round);
903
+ if (result.cancelled === true) {
904
+ policy.status = "cancelled";
905
+ policy.cancelled = true;
906
+ policy.endedAt = answeredAt;
907
+ await persistAskUserTranscript(policy, options);
908
+ return okInterviewResult(policy, "cancelled", []);
909
+ }
910
+ const derived = deriveAnswerDecisions(questions, result.answers, policy);
911
+ upsertDecisions(policy, derived);
912
+ await persistAskUserTranscript(policy, options);
913
+ return okInterviewResult(policy, "round_answered", result.answers);
914
+ } finally {
915
+ policy.inFlight = false;
916
+ }
917
+ }
918
+ };
919
+ }
920
+
921
+ // src/engine/loop-guard.ts
922
+ init_esm_shims();
923
+ var LOOP_GUARD_REGISTRATION_ID = "guard.loop";
924
+ var INTERACTIVE_LOOP_BLOCK_BUDGET = 2;
925
+ var WORKER_LOOP_BLOCK_CALLS_PER_BLOCK = 10;
926
+ function workerLoopBlockBudget(toolCalls) {
927
+ if (!Number.isSafeInteger(toolCalls) || toolCalls <= 0) return INTERACTIVE_LOOP_BLOCK_BUDGET;
928
+ return Math.max(INTERACTIVE_LOOP_BLOCK_BUDGET, Math.ceil(toolCalls / WORKER_LOOP_BLOCK_CALLS_PER_BLOCK));
929
+ }
930
+ var LOOP_SYNTHESIS_BACKSTOP_DENIALS = 2;
931
+ var WIDE_BATCH_DENIAL_FLOOR = 32;
932
+ var DEFAULT_WORKER_TOOL_CALL_CAP = GUARDRAIL_DEFAULTS.workerToolCallCap;
933
+ var DEFAULT_ORCH_TURN_TOOL_CALL_BUDGET = GUARDRAIL_DEFAULTS.turnToolCallBudget;
934
+ var ORCH_TURN_TOOL_CALL_HARD_MARGIN = 15;
935
+ var LOOP_GUARD_TURN_LIMIT = 32;
936
+ var SUCCEEDED_FINGERPRINT_LIMIT = 128;
937
+ var NO_TURN_BUCKET = "no-turn";
938
+ var SIZE_ONLY_ARG_KEYS = /* @__PURE__ */ new Set(["limit", "offset", "max_bytes", "context"]);
939
+ var RESULT_STAGNATION_THRESHOLD = 3;
940
+ var CROSS_ARGUMENT_RESULT_MIN_BYTES = 64;
941
+ function stagnationFingerprint(tool, args) {
942
+ const reduced = {};
943
+ for (const [key, value] of Object.entries(args ?? {})) {
944
+ if (!SIZE_ONLY_ARG_KEYS.has(key)) reduced[key] = value;
945
+ }
946
+ return hashToolCall(tool, reduced);
947
+ }
948
+ function stagnationBlockReason(tool, identicalResults) {
949
+ return `loop detected: the last ${identicalResults} ${tool} calls returned byte-identical results even though size arguments (limit/offset) changed. Raising them is not producing new information. Re-read the result above, change the query or tool, or answer from what you have.`;
950
+ }
951
+ function crossArgumentResultMessage(tool, distinctArguments) {
952
+ return `loop guard: ${distinctArguments} distinct ${tool} arguments returned the same substantial result this turn. The tool may be ignoring an argument; re-read the result, change tools, or answer from what you have.`;
953
+ }
954
+ function loopBlockBaseReason(tool, repeatCount, priorSuccesses) {
955
+ const evidence = priorSuccesses > 0 ? `This exact call already succeeded ${priorSuccesses} ${priorSuccesses === 1 ? "time" : "times"} this run; its result is already in the conversation above \u2014 re-read that result before calling tools again. ` : "";
956
+ return `loop detected: ${tool} was called ${repeatCount} times with identical arguments among this turn's recent tool calls. Repeating the exact call is blocked. ${evidence}Change strategy: vary the arguments, use a different tool, or explain what new information you expect before retrying.`;
957
+ }
958
+ function synthesisLockoutDirective() {
959
+ return "loop guard: this turn reached its tool-call limit after repeated identical calls, so tool calls are now disabled for the rest of this turn. Everything you retrieved is already in the conversation above. Answer the operator now, in plain prose, from what you have gathered. Do not write tool-call markup such as <tool_call> blocks; tool calls are disabled and will not run.";
960
+ }
961
+ function synthesisBackstopReason(tool) {
962
+ return `loop guard: tool calls stayed disabled and ${tool} was called again instead of answering, so the turn is being stopped. Summarize what you found for the operator.`;
963
+ }
964
+ function workerExplorationSynthesisDirective(limit) {
965
+ return `worker exploration budget reached (${limit}); exploration tools are now disabled for this run. Answer in plain prose from the evidence already gathered, with live path:line citations. Do not retry or substitute another tool call.`;
966
+ }
967
+ function disallowedToolsText(deliveryTools) {
968
+ const candidates = ["code_nav", "ls", "grep", "find", "context", "git", "shell commands"];
969
+ const excluded = candidates.filter((tool) => !deliveryTools.includes(tool));
970
+ return excluded.join(", ");
971
+ }
972
+ function workerLiveReadReserveDirective(remaining, deliveryTools) {
973
+ const admitted = deliveryTools.length > 0 ? `read and ${deliveryTools.join("/")}` : "the read tool";
974
+ const finish = deliveryTools.length > 0 ? `Finish the work: re-read what you will cite and write it with ${deliveryTools.join(" or ")}.` : "Read a specific live file needed for the handoff.";
975
+ return `worker live-read reserve: broad orientation is complete and ${remaining} ${remaining === 1 ? "call remains" : "calls remain"}. Only ${admitted} ${deliveryTools.length > 0 ? "are" : "is"} admitted now. ${finish} Do not substitute ${disallowedToolsText(deliveryTools)}.`;
976
+ }
977
+ function workerDeliveryOnlyDirective(limit, deliveryTools) {
978
+ return `worker discovery budget reached (${limit}); exploration tools are disabled for the rest of this run. Only read and ${deliveryTools.join("/")} are admitted now. Write what you have with ${deliveryTools.join(" or ")} and finish. Do not substitute ${disallowedToolsText(deliveryTools)}.`;
979
+ }
980
+ var DEAD_TOOL_CALL_MARKUP_PATTERNS = [
981
+ /<tool_call>[\s\S]*?<\/tool_call>/gi,
982
+ /<function=[\w.-]+>[\s\S]*?<\/function>/gi,
983
+ /<tool_call>\s*(?:<function=|[{[])[\s\S]*$/i,
984
+ /<function=[\w.-]+>[\s\S]*$/i
985
+ ];
986
+ function stripDeadToolCallMarkup(text) {
987
+ let out = text;
988
+ for (const pattern of DEAD_TOOL_CALL_MARKUP_PATTERNS) {
989
+ out = out.replace(pattern, "");
990
+ }
991
+ return out === text ? text : out.trim();
992
+ }
993
+ function lockedSynthesisFallbackText() {
994
+ return "[Clio Coder] loop guard: tool calls were disabled for the rest of this turn, but the reply contained only tool-call markup, which cannot run and was removed. Ask me to continue with a different approach, or narrow the request.";
995
+ }
996
+ function sanitizeLockedSynthesisMessage(message) {
997
+ if (!message || typeof message !== "object" || message.role !== "assistant") return false;
998
+ const record = message;
999
+ const stopReason = record.stopReason;
1000
+ if (stopReason !== void 0 && stopReason !== "stop" && stopReason !== "length") return false;
1001
+ if (!Array.isArray(record.content)) return false;
1002
+ const blocks = record.content;
1003
+ if (blocks.some((block) => block?.type === "toolCall")) return false;
1004
+ let changed = false;
1005
+ for (const block of blocks) {
1006
+ if (block?.type !== "text" || typeof block.text !== "string") continue;
1007
+ const stripped = stripDeadToolCallMarkup(block.text);
1008
+ if (stripped !== block.text) {
1009
+ block.text = stripped;
1010
+ changed = true;
1011
+ }
1012
+ }
1013
+ if (!changed) return false;
1014
+ const remaining = blocks.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("").trim();
1015
+ if (remaining.length === 0) {
1016
+ const firstText = blocks.find(
1017
+ (block) => block?.type === "text" && typeof block.text === "string"
1018
+ );
1019
+ if (firstText) firstText.text = lockedSynthesisFallbackText();
1020
+ else blocks.push({ type: "text", text: lockedSynthesisFallbackText() });
1021
+ }
1022
+ return true;
1146
1023
  }
1147
- async function finalizeAskUserInterviewForHost(policy, reason, options, onFinalized) {
1148
- await finalizeAskUserInterview(policy, reason, options);
1149
- onFinalized?.(policy);
1024
+ function isLoopGuardSynthesisBackstopReason(reason) {
1025
+ return /^loop guard: tool calls stayed disabled and .+ was called again instead of answering/.test(reason);
1150
1026
  }
1151
- async function defaultAskUserHandler() {
1152
- return cancelledAskUserResult();
1027
+ function readOrchTurnToolCallBudget(env = process.env) {
1028
+ const soft = resolveGuardrail("turnToolCallBudget", env);
1029
+ return { soft, hard: soft + ORCH_TURN_TOOL_CALL_HARD_MARGIN };
1153
1030
  }
1154
- function createAskUserTool(deps = {}) {
1155
- return {
1156
- name: ToolNames.AskUser,
1157
- description: "Run a host-owned operator interview: action=ask presents questions, action=complete records compact decisions before final prose. If cancelled, proceed with defaults and do not ask again.",
1158
- parameters: askUserParameters,
1159
- baseActionClass: "read",
1160
- executionMode: "sequential",
1161
- async run(args, options) {
1162
- const normalized = normalizeAskUserCall(args);
1163
- if (!normalized.call) return { kind: "error", message: `ask_user: ${normalized.error ?? "invalid input"}` };
1164
- const policy = options?.askUserPolicy ?? createStandalonePolicy(options);
1165
- hydratePolicy(policy, options);
1166
- const call = normalized.call;
1167
- if (call.action === "complete") {
1168
- return completeInterview(policy, "complete", options, call.summary, call.decisions ?? []);
1169
- }
1170
- const questions = call.questions ?? [];
1171
- if (call.max_rounds !== void 0) {
1172
- const nextLimit = policy.callCount === 0 ? call.max_rounds : Math.max(policy.maxCalls, call.max_rounds);
1173
- policy.maxCalls = Math.max(nextLimit, policy.callCount + 1);
1031
+ function createLoopGuardRegistration(options) {
1032
+ const budget = options.turnBlockBudget ?? INTERACTIVE_LOOP_BLOCK_BUDGET;
1033
+ const cap = options.toolCallCap;
1034
+ let softLimit = options.toolCallSoftLimit !== void 0 && Number.isSafeInteger(options.toolCallSoftLimit) && options.toolCallSoftLimit > 0 && (cap === void 0 || options.toolCallSoftLimit <= cap) ? options.toolCallSoftLimit : void 0;
1035
+ const turnBudget = options.turnToolCallBudget;
1036
+ const synthesisLockout = options.turnSynthesisLockout === true;
1037
+ let softReadReserve = options.toolCallSoftReadReserve ?? 0;
1038
+ let softReadReserveThreshold = softLimit !== void 0 && softReadReserve > 0 && softLimit > softReadReserve ? softLimit - softReadReserve : null;
1039
+ const deliveryTools = [...new Set(options.deliveryTools ?? [])];
1040
+ const reserveAdmits = (tool) => isReserveAdmittedTool(tool, deliveryTools);
1041
+ let softAdmittedCount = 0;
1042
+ let softReadReserveEntered = false;
1043
+ let reserveDenials = { denials: 0 };
1044
+ let deniedAttempts = 0;
1045
+ const denialBudget = cap === void 0 ? void 0 : Math.max(cap, WIDE_BATCH_DENIAL_FLOOR);
1046
+ const boundRunDenials = (effects, input) => {
1047
+ if (denialBudget === void 0 || !effects.some((effect) => effect.kind === "block_tool")) return effects;
1048
+ deniedAttempts += 1;
1049
+ if (deniedAttempts <= denialBudget) return effects;
1050
+ return [{ kind: "block_tool", reason: synthesisBackstopReason(input.toolName ?? "unknown"), severity: "hard-block" }];
1051
+ };
1052
+ const reserve = options.toolCallReserve ?? 0;
1053
+ const reserveThreshold = cap !== void 0 && reserve > 0 && cap > reserve ? cap - reserve : null;
1054
+ let reserveDirectiveEmitted = false;
1055
+ const blocksByTurn = /* @__PURE__ */ new Map();
1056
+ const callsByTurn = /* @__PURE__ */ new Map();
1057
+ const lockoutByTurn = /* @__PURE__ */ new Map();
1058
+ let capLockout = null;
1059
+ let softLockout = null;
1060
+ const succeededFingerprints = /* @__PURE__ */ new Map();
1061
+ const stagnationByTurn = /* @__PURE__ */ new Map();
1062
+ const crossArgumentResultsByTurn = /* @__PURE__ */ new Map();
1063
+ let count = 0;
1064
+ const enterSoftReadReserve = () => {
1065
+ if (softReadReserveEntered) return;
1066
+ softReadReserveEntered = true;
1067
+ options.onSoftReadReserve?.();
1068
+ };
1069
+ const bumpBoundedCounter = (store, key, limit = LOOP_GUARD_TURN_LIMIT) => {
1070
+ if (!store.has(key)) {
1071
+ while (store.size >= limit) {
1072
+ const oldest = store.keys().next().value;
1073
+ if (typeof oldest !== "string") break;
1074
+ store.delete(oldest);
1174
1075
  }
1175
- if (policy.inFlight === true) {
1176
- return {
1177
- kind: "error",
1178
- message: "ask_user: an operator interview round is already in progress. Wait for that answer."
1179
- };
1076
+ }
1077
+ const next = (store.get(key) ?? 0) + 1;
1078
+ store.set(key, next);
1079
+ return next;
1080
+ };
1081
+ const resultCarriesEvidence = (details) => {
1082
+ const observation = details?.observation;
1083
+ if (observation === null || typeof observation !== "object" || Array.isArray(observation)) return true;
1084
+ const record = observation;
1085
+ return !(record.truncated === true && record.shownCount === 0);
1086
+ };
1087
+ const recordSuccessfulResult = (input) => {
1088
+ if (input.metadata?.resultKind !== "ok") return;
1089
+ if (!resultCarriesEvidence(input.toolResultDetails)) return;
1090
+ const tool = input.toolName;
1091
+ if (typeof tool !== "string" || tool.length === 0) return;
1092
+ bumpBoundedCounter(succeededFingerprints, hashToolCall(tool, input.toolArgs ?? {}), SUCCEEDED_FINGERPRINT_LIMIT);
1093
+ };
1094
+ const recordResultForStagnation = (input) => {
1095
+ const turnKey = input.turnId ?? NO_TURN_BUCKET;
1096
+ const tool = input.toolName;
1097
+ const resultFingerprint = input.metadata?.resultFingerprint;
1098
+ if (typeof tool !== "string" || tool.length === 0 || input.metadata?.resultKind !== "ok" || typeof resultFingerprint !== "string") {
1099
+ stagnationByTurn.delete(turnKey);
1100
+ return;
1101
+ }
1102
+ const reducedFingerprint = stagnationFingerprint(tool, input.toolArgs);
1103
+ const previous = stagnationByTurn.get(turnKey);
1104
+ if (previous !== void 0 && previous.reducedFingerprint === reducedFingerprint && previous.resultFingerprint === resultFingerprint) {
1105
+ previous.streak += 1;
1106
+ return;
1107
+ }
1108
+ if (!stagnationByTurn.has(turnKey)) {
1109
+ while (stagnationByTurn.size >= LOOP_GUARD_TURN_LIMIT) {
1110
+ const oldest = stagnationByTurn.keys().next().value;
1111
+ if (typeof oldest !== "string") break;
1112
+ stagnationByTurn.delete(oldest);
1180
1113
  }
1181
- if (policy.status === "complete") {
1182
- await persistAskUserTranscript(policy, options);
1183
- return okInterviewResult(policy, "already_complete");
1114
+ }
1115
+ stagnationByTurn.set(turnKey, { reducedFingerprint, resultFingerprint, streak: 1 });
1116
+ };
1117
+ const crossArgumentResultEffects = (input) => {
1118
+ const turnKey = input.turnId ?? NO_TURN_BUCKET;
1119
+ const tool = input.toolName;
1120
+ const resultFingerprint = input.metadata?.resultFingerprint;
1121
+ const resultBytes = input.metadata?.resultBytes;
1122
+ if (typeof tool !== "string" || tool.length === 0 || input.metadata?.actionClass !== "read" || input.metadata?.resultKind !== "ok" || typeof resultFingerprint !== "string" || typeof resultBytes !== "number" || resultBytes < CROSS_ARGUMENT_RESULT_MIN_BYTES || !resultCarriesEvidence(input.toolResultDetails)) {
1123
+ crossArgumentResultsByTurn.delete(turnKey);
1124
+ return [];
1125
+ }
1126
+ const argumentFingerprint = stagnationFingerprint(tool, input.toolArgs);
1127
+ const previous = crossArgumentResultsByTurn.get(turnKey);
1128
+ if (previous?.tool === tool && previous.resultFingerprint === resultFingerprint) {
1129
+ if (previous.warned) return [];
1130
+ previous.argumentFingerprints.add(argumentFingerprint);
1131
+ if (previous.argumentFingerprints.size >= RESULT_STAGNATION_THRESHOLD) {
1132
+ previous.warned = true;
1133
+ return [
1134
+ {
1135
+ kind: "annotate_tool_result",
1136
+ message: crossArgumentResultMessage(tool, previous.argumentFingerprints.size),
1137
+ severity: "warn"
1138
+ }
1139
+ ];
1184
1140
  }
1185
- if (policy.status === "cancelled" || policy.cancelled === true) {
1186
- policy.status = "cancelled";
1187
- policy.cancelled = true;
1188
- await persistAskUserTranscript(policy, options);
1189
- return okInterviewResult(policy, "already_cancelled");
1141
+ return [];
1142
+ }
1143
+ if (!crossArgumentResultsByTurn.has(turnKey)) {
1144
+ while (crossArgumentResultsByTurn.size >= LOOP_GUARD_TURN_LIMIT) {
1145
+ const oldest = crossArgumentResultsByTurn.keys().next().value;
1146
+ if (typeof oldest !== "string") break;
1147
+ crossArgumentResultsByTurn.delete(oldest);
1190
1148
  }
1191
- const key = questionKey(questions);
1192
- if (policy.askedQuestionKeys.has(key)) {
1193
- await persistAskUserTranscript(policy, options);
1194
- return okInterviewResult(policy, "duplicate_round_ignored");
1149
+ }
1150
+ crossArgumentResultsByTurn.set(turnKey, {
1151
+ tool,
1152
+ resultFingerprint,
1153
+ argumentFingerprints: /* @__PURE__ */ new Set([argumentFingerprint]),
1154
+ warned: false
1155
+ });
1156
+ return [];
1157
+ };
1158
+ const bumpTurnBlocks = (turnId) => bumpBoundedCounter(blocksByTurn, turnId);
1159
+ const enterLockout = (input, turnKey, state) => {
1160
+ if (!lockoutByTurn.has(turnKey)) {
1161
+ while (lockoutByTurn.size >= LOOP_GUARD_TURN_LIMIT) {
1162
+ const oldest = lockoutByTurn.keys().next().value;
1163
+ if (typeof oldest !== "string") break;
1164
+ lockoutByTurn.delete(oldest);
1195
1165
  }
1196
- if (policy.callCount >= policy.maxCalls) {
1197
- return completeInterview(
1198
- policy,
1199
- "round_limit_reached",
1200
- options,
1201
- `Interview closed after reaching the ${policy.maxCalls}-round limit.`
1202
- );
1166
+ }
1167
+ lockoutByTurn.set(turnKey, {
1168
+ ...state,
1169
+ denials: 0,
1170
+ ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
1171
+ });
1172
+ };
1173
+ const reachesLockoutBackstop = (state, input) => {
1174
+ if (input.correlationId !== void 0 && state.correlationId === input.correlationId) return false;
1175
+ state.denials += 1;
1176
+ if (input.correlationId !== void 0) state.correlationId = input.correlationId;
1177
+ else delete state.correlationId;
1178
+ return state.denials > LOOP_SYNTHESIS_BACKSTOP_DENIALS;
1179
+ };
1180
+ const emitLoopBlocked = (input, info, at) => {
1181
+ const payload = {
1182
+ tool: info.tool,
1183
+ repeatCount: info.repeatCount,
1184
+ blocksThisTurn: info.blocksThisTurn,
1185
+ budget,
1186
+ interrupted: info.disposition === "stop",
1187
+ disposition: info.disposition,
1188
+ at,
1189
+ ...input.turnId !== void 0 ? { turnId: input.turnId } : {}
1190
+ };
1191
+ options.bus?.emit(BusChannels.LoopBlocked, payload);
1192
+ };
1193
+ const emitBudgetEvent = (input, callsThisTurn, interrupted, at) => {
1194
+ if (turnBudget === void 0) return;
1195
+ const payload = {
1196
+ tool: input.toolName ?? "unknown",
1197
+ callsThisTurn,
1198
+ softBudget: turnBudget.soft,
1199
+ hardCeiling: turnBudget.hard,
1200
+ interrupted,
1201
+ at,
1202
+ ...input.turnId !== void 0 ? { turnId: input.turnId } : {}
1203
+ };
1204
+ options.bus?.emit(BusChannels.ToolBudgetExceeded, payload);
1205
+ };
1206
+ const evaluateTurnBudget = (input, now) => {
1207
+ if (turnBudget === void 0) return null;
1208
+ const turnKey = input.turnId ?? NO_TURN_BUCKET;
1209
+ const callsThisTurn = bumpBoundedCounter(callsByTurn, turnKey);
1210
+ if (callsThisTurn >= turnBudget.hard) {
1211
+ if (callsThisTurn === turnBudget.hard) emitBudgetEvent(input, callsThisTurn, true, now);
1212
+ return [
1213
+ {
1214
+ kind: "block_tool",
1215
+ reason: `tool-call budget exhausted: ${callsThisTurn} tool calls in this turn reached the hard ceiling (${turnBudget.hard}); the turn is being stopped. Summarize what you found and wait for the operator.`,
1216
+ severity: "hard-block"
1217
+ }
1218
+ ];
1219
+ }
1220
+ if (callsThisTurn >= turnBudget.soft) {
1221
+ if (callsThisTurn === turnBudget.soft) emitBudgetEvent(input, callsThisTurn, false, now);
1222
+ return [
1223
+ {
1224
+ kind: "block_tool",
1225
+ reason: `tool-call budget: you have made ${callsThisTurn} tool calls in this turn (soft budget ${turnBudget.soft}). Every further tool call this turn will be blocked, so do not retry this call and do not substitute another one. Summarize what you have found so far in plain text, state the single next step you propose, and wait for the operator.`,
1226
+ severity: "hard-block"
1227
+ }
1228
+ ];
1229
+ }
1230
+ return null;
1231
+ };
1232
+ const blockAsLoop = (input, turnKey, tool, repeatCount, baseReason, now) => {
1233
+ const blocksThisTurn = bumpTurnBlocks(turnKey);
1234
+ const reachedBudget = blocksThisTurn >= budget;
1235
+ if (synthesisLockout && reachedBudget) {
1236
+ enterLockout(input, turnKey, { tool, repeatCount, blocksThisTurn });
1237
+ options.onSynthesisLockout?.();
1238
+ emitLoopBlocked(input, { tool, repeatCount, blocksThisTurn, disposition: "lockout" }, now);
1239
+ return [{ kind: "block_tool", reason: synthesisLockoutDirective(), severity: "hard-block" }];
1240
+ }
1241
+ emitLoopBlocked(input, { tool, repeatCount, blocksThisTurn, disposition: reachedBudget ? "stop" : "block" }, now);
1242
+ const reason = reachedBudget ? `${baseReason} Loop budget exhausted (${blocksThisTurn} blocks this turn); the agent is being stopped.` : baseReason;
1243
+ return [{ kind: "block_tool", reason, severity: "hard-block" }];
1244
+ };
1245
+ return {
1246
+ id: LOOP_GUARD_REGISTRATION_ID,
1247
+ description: "blocks repeated tool calls, enforces tool-call caps, and annotates substantial identical read results across distinct arguments",
1248
+ hooks: ["before_tool", "after_tool"],
1249
+ callCount: () => count,
1250
+ extendWorkerToolCallPhase(phase) {
1251
+ if (softLimit === void 0 || !Number.isSafeInteger(phase.toolCalls) || !Number.isSafeInteger(phase.readReserve) || phase.toolCalls <= 0 || phase.readReserve < 0 || phase.readReserve >= phase.toolCalls || cap !== void 0 && phase.toolCalls > cap || phase.toolCalls < softLimit || phase.toolCalls === softLimit && phase.readReserve <= softReadReserve) {
1252
+ return false;
1203
1253
  }
1204
- const requestedAt = (/* @__PURE__ */ new Date()).toISOString();
1205
- policy.inFlight = true;
1206
- policy.status = "active";
1207
- policy.updatedAt = requestedAt;
1208
- if (call.summary) policy.summary = call.summary;
1209
- const handler = deps.askUser ?? defaultAskUserHandler;
1210
- try {
1211
- const result = normalizeAskUserResult(questions, await handler(questions, options));
1212
- const answeredAt = (/* @__PURE__ */ new Date()).toISOString();
1213
- policy.callCount += 1;
1214
- policy.askedQuestionKeys.add(key);
1215
- policy.answerCount += result.answers.length;
1216
- policy.updatedAt = answeredAt;
1217
- const round = {
1218
- round: policy.rounds.length + 1,
1219
- requestedAt,
1220
- answeredAt,
1221
- questions: transcriptQuestions(questions),
1222
- answers: transcriptAnswers(result.answers),
1223
- ...result.cancelled === true ? { cancelled: true } : {}
1224
- };
1225
- policy.rounds.push(round);
1226
- if (result.cancelled === true) {
1227
- policy.status = "cancelled";
1228
- policy.cancelled = true;
1229
- policy.endedAt = answeredAt;
1230
- await persistAskUserTranscript(policy, options);
1231
- return okInterviewResult(policy, "cancelled", []);
1254
+ softLimit = phase.toolCalls;
1255
+ softReadReserve = phase.readReserve;
1256
+ softReadReserveThreshold = softReadReserve > 0 && softLimit > softReadReserve ? softLimit - softReadReserve : null;
1257
+ softReadReserveEntered = false;
1258
+ softLockout = null;
1259
+ reserveDenials = { denials: 0 };
1260
+ return true;
1261
+ },
1262
+ evaluate(input) {
1263
+ if (input.hook === "after_tool") {
1264
+ recordSuccessfulResult(input);
1265
+ recordResultForStagnation(input);
1266
+ const effects = [...crossArgumentResultEffects(input)];
1267
+ if (softReadReserveEntered && softLockout === null && deliveryTools.length === 0 && input.toolName === ToolNames.Read) {
1268
+ effects.push({ kind: "require_tool", toolName: ToolNames.Read });
1232
1269
  }
1233
- const derived = deriveAnswerDecisions(questions, result.answers, policy);
1234
- upsertDecisions(policy, derived);
1235
- await persistAskUserTranscript(policy, options);
1236
- return okInterviewResult(policy, "round_answered", result.answers);
1237
- } finally {
1238
- policy.inFlight = false;
1270
+ if (reserveThreshold !== null && cap !== void 0 && !reserveDirectiveEmitted && count > reserveThreshold && count <= cap) {
1271
+ reserveDirectiveEmitted = true;
1272
+ effects.push({
1273
+ kind: "annotate_tool_result",
1274
+ message: workerSynthesisReserveDirective(Math.max(0, cap - count), cap),
1275
+ severity: "warn"
1276
+ });
1277
+ }
1278
+ return effects;
1239
1279
  }
1280
+ const now = options.now?.() ?? Date.now();
1281
+ const turnKey = input.turnId ?? NO_TURN_BUCKET;
1282
+ const decide = () => {
1283
+ if (capLockout !== null && cap !== void 0) {
1284
+ if (reachesLockoutBackstop(capLockout, input)) {
1285
+ return [
1286
+ { kind: "block_tool", reason: synthesisBackstopReason(input.toolName ?? "unknown"), severity: "hard-block" }
1287
+ ];
1288
+ }
1289
+ return [{ kind: "block_tool", reason: workerToolCallCapSynthesisReason(cap), severity: "hard-block" }];
1290
+ }
1291
+ if (softLockout !== null && softLimit !== void 0) {
1292
+ if (reachesLockoutBackstop(softLockout, input)) {
1293
+ return [
1294
+ { kind: "block_tool", reason: synthesisBackstopReason(input.toolName ?? "unknown"), severity: "hard-block" }
1295
+ ];
1296
+ }
1297
+ return [{ kind: "block_tool", reason: workerExplorationSynthesisDirective(softLimit), severity: "hard-block" }];
1298
+ }
1299
+ const lockout = synthesisLockout ? lockoutByTurn.get(turnKey) : void 0;
1300
+ if (lockout !== void 0) {
1301
+ if (reachesLockoutBackstop(lockout, input)) {
1302
+ emitLoopBlocked(
1303
+ input,
1304
+ {
1305
+ tool: lockout.tool,
1306
+ repeatCount: lockout.repeatCount,
1307
+ blocksThisTurn: lockout.blocksThisTurn,
1308
+ disposition: "stop"
1309
+ },
1310
+ now
1311
+ );
1312
+ return [{ kind: "block_tool", reason: synthesisBackstopReason(lockout.tool), severity: "hard-block" }];
1313
+ }
1314
+ return [{ kind: "block_tool", reason: synthesisLockoutDirective(), severity: "hard-block" }];
1315
+ }
1316
+ if (softLimit !== void 0) {
1317
+ const spentSoftLimit = softAdmittedCount >= softLimit;
1318
+ if (spentSoftLimit && deliveryTools.length === 0) {
1319
+ softLockout = {
1320
+ denials: 0,
1321
+ ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
1322
+ };
1323
+ options.onSynthesisLockout?.();
1324
+ return [{ kind: "block_tool", reason: workerExplorationSynthesisDirective(softLimit), severity: "hard-block" }];
1325
+ }
1326
+ const inReserve = softReadReserveThreshold !== null && softAdmittedCount >= softReadReserveThreshold;
1327
+ if (spentSoftLimit || inReserve) {
1328
+ enterSoftReadReserve();
1329
+ if (!reserveAdmits(input.toolName)) {
1330
+ if (reachesLockoutBackstop(reserveDenials, input)) {
1331
+ softLockout = {
1332
+ denials: 0,
1333
+ ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
1334
+ };
1335
+ options.onSynthesisLockout?.();
1336
+ return [{ kind: "block_tool", reason: workerExplorationSynthesisDirective(softLimit), severity: "hard-block" }];
1337
+ }
1338
+ return [
1339
+ {
1340
+ kind: "block_tool",
1341
+ reason: spentSoftLimit ? workerDeliveryOnlyDirective(softLimit, deliveryTools) : workerLiveReadReserveDirective(softLimit - softAdmittedCount, deliveryTools),
1342
+ severity: "hard-block"
1343
+ }
1344
+ ];
1345
+ }
1346
+ }
1347
+ }
1348
+ if (reserveThreshold !== null && cap !== void 0 && count >= reserveThreshold && !reserveAdmits(input.toolName)) {
1349
+ if (reachesLockoutBackstop(reserveDenials, input)) {
1350
+ if (synthesisLockout) {
1351
+ capLockout = {
1352
+ denials: 0,
1353
+ ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
1354
+ };
1355
+ options.onSynthesisLockout?.();
1356
+ }
1357
+ return [{ kind: "block_tool", reason: workerToolCallCapSynthesisReason(cap), severity: "hard-block" }];
1358
+ }
1359
+ return [
1360
+ {
1361
+ kind: "block_tool",
1362
+ reason: workerSynthesisReserveBlockReason(input.toolName ?? "unknown", Math.max(1, cap - count), cap),
1363
+ severity: "hard-block"
1364
+ }
1365
+ ];
1366
+ }
1367
+ count += 1;
1368
+ reserveDenials = { denials: 0 };
1369
+ if (softLimit !== void 0) {
1370
+ softAdmittedCount += 1;
1371
+ if (softReadReserveThreshold !== null && softAdmittedCount >= softReadReserveThreshold) enterSoftReadReserve();
1372
+ if (softAdmittedCount >= softLimit) {
1373
+ if (deliveryTools.length === 0) {
1374
+ softLockout = {
1375
+ denials: 0,
1376
+ ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
1377
+ };
1378
+ options.onSynthesisLockout?.();
1379
+ }
1380
+ options.onSoftLimitFinalCallAdmitted?.(input.toolCallId);
1381
+ }
1382
+ }
1383
+ if (cap !== void 0 && count > cap) {
1384
+ if (!synthesisLockout) {
1385
+ return [{ kind: "block_tool", reason: workerToolCallCapExceededReason(cap), severity: "hard-block" }];
1386
+ }
1387
+ capLockout = {
1388
+ denials: 0,
1389
+ ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
1390
+ };
1391
+ options.onSynthesisLockout?.();
1392
+ return [{ kind: "block_tool", reason: workerToolCallCapSynthesisReason(cap), severity: "hard-block" }];
1393
+ }
1394
+ const fingerprint = input.metadata?.callFingerprint;
1395
+ if (typeof fingerprint !== "string" || fingerprint.length === 0) {
1396
+ return evaluateTurnBudget(input, now) ?? [];
1397
+ }
1398
+ const verdict = options.safety.observeLoop(`${turnKey}|${fingerprint}`, now);
1399
+ if (verdict.looping) {
1400
+ const tool = input.toolName ?? "unknown";
1401
+ const priorSuccesses = succeededFingerprints.get(fingerprint) ?? 0;
1402
+ return blockAsLoop(
1403
+ input,
1404
+ turnKey,
1405
+ tool,
1406
+ verdict.count,
1407
+ loopBlockBaseReason(tool, verdict.count, priorSuccesses),
1408
+ now
1409
+ );
1410
+ }
1411
+ const toolName = input.toolName;
1412
+ if (typeof toolName === "string" && toolName.length > 0) {
1413
+ const entry = stagnationByTurn.get(turnKey);
1414
+ if (entry !== void 0 && entry.streak >= RESULT_STAGNATION_THRESHOLD - 1 && entry.reducedFingerprint === stagnationFingerprint(toolName, input.toolArgs)) {
1415
+ return blockAsLoop(
1416
+ input,
1417
+ turnKey,
1418
+ toolName,
1419
+ entry.streak + 1,
1420
+ stagnationBlockReason(toolName, entry.streak),
1421
+ now
1422
+ );
1423
+ }
1424
+ }
1425
+ return evaluateTurnBudget(input, now) ?? [];
1426
+ };
1427
+ return boundRunDenials(decide(), input);
1240
1428
  }
1241
1429
  };
1242
1430
  }
1431
+ function isReserveAdmittedTool(tool, deliveryTools) {
1432
+ return tool === ToolNames.Read || tool !== void 0 && deliveryTools.includes(tool);
1433
+ }
1434
+ function resolveDeliveryTools(allowedTools, product) {
1435
+ const candidates = product === "orientation" ? [ToolNames.Write, ToolNames.Edit, ToolNames.CodeNav] : [ToolNames.Write, ToolNames.Edit];
1436
+ if (allowedTools instanceof Set) {
1437
+ return candidates.filter((tool) => allowedTools.has(tool));
1438
+ }
1439
+ return candidates.filter((tool) => allowedTools.includes(tool));
1440
+ }
1243
1441
 
1244
1442
  // src/tools/registry.ts
1245
1443
  init_esm_shims();
@@ -1286,13 +1484,15 @@ function createRegistry(deps) {
1286
1484
  const preparedArgs = prepareToolArgs(spec, call.args ?? {});
1287
1485
  resultDisposition = resolveToolResultDisposition(spec, preparedArgs);
1288
1486
  const result = await spec.run(preparedArgs, options);
1289
- const afterEffects = runToolHook("after_tool", spec, call, decision, options, result);
1487
+ const digest = toolResultDigestFor(spec, result, resultDisposition);
1488
+ const afterEffects = runToolHook("after_tool", spec, call, decision, options, result, digest);
1290
1489
  const finalResult = shapeToolResult(spec, applyToolResultEffects(result, afterEffects), options, resultDisposition);
1291
1490
  return { kind: "ok", result: finalResult, decision };
1292
1491
  } catch (err) {
1293
1492
  const message = err instanceof Error ? err.message : String(err);
1294
1493
  const result = { kind: "error", message };
1295
- const afterEffects = runToolHook("after_tool", spec, call, decision, options, result);
1494
+ const digest = toolResultDigestFor(spec, result, resultDisposition);
1495
+ const afterEffects = runToolHook("after_tool", spec, call, decision, options, result, digest);
1296
1496
  return {
1297
1497
  kind: "ok",
1298
1498
  result: shapeToolResult(spec, applyToolResultEffects(result, afterEffects), options, resultDisposition),
@@ -1303,9 +1503,9 @@ function createRegistry(deps) {
1303
1503
  disposeAdmissionArgs(spec, call.args ?? {});
1304
1504
  }
1305
1505
  };
1306
- const runToolHook = (hook, spec, call, decision, options, result) => {
1506
+ const runToolHook = (hook, spec, call, decision, options, result, resultDigest) => {
1307
1507
  if (!deps.middleware) return [];
1308
- const input = buildToolHookInput(hook, spec, call, decision, "operating", options, result);
1508
+ const input = buildToolHookInput(hook, spec, call, decision, "operating", options, result, resultDigest);
1309
1509
  const effects = deps.middleware.runHook(input).effects;
1310
1510
  try {
1311
1511
  deps.onMiddlewareEffects?.(effects, input);
@@ -1740,7 +1940,7 @@ ${plan.text}`,
1740
1940
  ...decision.policy !== void 0 ? { policy: decision.policy } : {}
1741
1941
  };
1742
1942
  }
1743
- function buildToolHookInput(hook, spec, call, decision, posture, options, result) {
1943
+ function buildToolHookInput(hook, spec, call, decision, posture, options, result, resultDigest) {
1744
1944
  const metadata = {
1745
1945
  posture,
1746
1946
  actionClass: decision.classification.actionClass,
@@ -1769,6 +1969,7 @@ function buildToolHookInput(hook, spec, call, decision, posture, options, result
1769
1969
  };
1770
1970
  if (call.args !== void 0) input.toolArgs = call.args;
1771
1971
  if (result?.details !== void 0) input.toolResultDetails = result.details;
1972
+ if (resultDigest !== void 0) input.toolResultDigest = resultDigest;
1772
1973
  if (options?.runId !== void 0) input.runId = options.runId;
1773
1974
  if (options?.sessionId !== void 0) input.sessionId = options.sessionId;
1774
1975
  if (options?.turnId !== void 0) input.turnId = options.turnId;
@@ -1863,9 +2064,16 @@ async function runValidatedToolCall(input) {
1863
2064
  const { spec, args, registry, signal, telemetry } = input;
1864
2065
  const startedAt = Date.now();
1865
2066
  const startedAtClock = performance2.now();
1866
- telemetry?.onStart?.({ tool: spec.name, posture: "operating", startedAt });
1867
2067
  const callId = input.invokeOptions?.toolCallId;
1868
2068
  const withCallId = callId === void 0 ? {} : { toolCallId: callId };
2069
+ const action = describeCallAction(spec.name, args);
2070
+ telemetry?.onStart?.({
2071
+ tool: spec.name,
2072
+ ...withCallId,
2073
+ posture: "operating",
2074
+ startedAt,
2075
+ ...action !== null ? { action } : {}
2076
+ });
1869
2077
  const invokeOpts = {};
1870
2078
  if (input.invokeOptions) Object.assign(invokeOpts, input.invokeOptions);
1871
2079
  if (signal) invokeOpts.signal = signal;
@@ -2023,40 +2231,6 @@ function resolveSessionTools(runtime, toolRegistry, invokeOptions, telemetry) {
2023
2231
  return resolveAgentTools(input);
2024
2232
  }
2025
2233
 
2026
- // src/domains/safety/call-target.ts
2027
- init_esm_shims();
2028
- var ESC_CHAR = String.fromCharCode(27);
2029
- var BEL_CHAR = String.fromCharCode(7);
2030
- var OSC_PATTERN = new RegExp(`${ESC_CHAR}\\][\\s\\S]*?(?:${BEL_CHAR}|${ESC_CHAR}\\\\|$)`, "g");
2031
- var CSI_PATTERN = new RegExp(`${ESC_CHAR}\\[[0-9;?]*[0-9A-Za-z]`, "g");
2032
- function sanitizeForDisplay(value) {
2033
- const stripped = value.replace(OSC_PATTERN, "").replace(CSI_PATTERN, "");
2034
- let out = "";
2035
- for (const ch of stripped) {
2036
- const code = ch.codePointAt(0) ?? 0;
2037
- out += code < 32 || code === 127 ? " " : ch;
2038
- }
2039
- return out;
2040
- }
2041
- function oneLine(value) {
2042
- return value.replace(/\s+/g, " ").trim();
2043
- }
2044
- function sanitizeCallTargetText(value) {
2045
- return oneLine(sanitizeForDisplay(value));
2046
- }
2047
- function describeCallTarget(args) {
2048
- if (!args) return "";
2049
- const str = (value) => typeof value === "string" && value.trim().length > 0 ? oneLine(sanitizeForDisplay(value)).trim() || null : null;
2050
- const candidate = str(args.command) ?? str(args.path) ?? str(args.file_path) ?? str(args.name) ?? str(args.pattern);
2051
- if (candidate) return candidate;
2052
- try {
2053
- const json = JSON.stringify(args);
2054
- return json === "{}" || json === void 0 ? "" : oneLine(sanitizeForDisplay(json)).slice(0, 120);
2055
- } catch {
2056
- return "";
2057
- }
2058
- }
2059
-
2060
2234
  // src/engine/provider-payload.ts
2061
2235
  init_esm_shims();
2062
2236
  function reasoningSummaryForLevel(level) {
@@ -4908,7 +5082,7 @@ function registerCoreTools(registry, deps = {}) {
4908
5082
  });
4909
5083
  registry.register({
4910
5084
  ...builtin(
4911
- lazyTool(codeNavToolSurface, async () => (await import("./code-nav-AXCXSBHX.js")).codeNavTool),
5085
+ lazyTool(codeNavToolSurface, async () => (await import("./code-nav-GQNL7XA6.js")).codeNavTool),
4912
5086
  { path: "src/tools/codewiki/code-nav.ts", scope: "core" }
4913
5087
  )
4914
5088
  });
@@ -4933,9 +5107,9 @@ function registerCoreTools(registry, deps = {}) {
4933
5107
  registry.register({
4934
5108
  ...builtin(
4935
5109
  lazyTool(contextToolSurface, async () => {
4936
- const { createContextTool } = await import("./context-EKDCKUUZ.js");
5110
+ const { createContextTool } = await import("./context-ZQ7SIFJV.js");
4937
5111
  if (!session) return createContextTool(skillToolDeps);
4938
- const { probeWorkspace } = await import("./workspace-G4ZWUIPR.js");
5112
+ const { probeWorkspace } = await import("./workspace-ZJ6BFM3Q.js");
4939
5113
  return createContextTool({
4940
5114
  ...skillToolDeps,
4941
5115
  ...readSessionEntries ? {
@@ -4998,6 +5172,14 @@ function assertRegisteredBuiltinTools(registry, registration, includeDispatchToo
4998
5172
 
4999
5173
  export {
5000
5174
  createProtectedArtifactsRegistration,
5175
+ decisionActionClass,
5176
+ decisionFactsForPermission,
5177
+ decisionFactsForAnswer,
5178
+ classifyDecisionPresentation,
5179
+ ASK_USER_OTHER_LABEL,
5180
+ askUserExposure,
5181
+ cancelledAskUserResult,
5182
+ finalizeAskUserInterviewForHost,
5001
5183
  INTERACTIVE_LOOP_BLOCK_BUDGET,
5002
5184
  workerLoopBlockBudget,
5003
5185
  lockedSynthesisFallbackText,
@@ -5011,11 +5193,6 @@ export {
5011
5193
  resolveAgentTools,
5012
5194
  effectiveToolNames,
5013
5195
  resolveSessionTools,
5014
- ASK_USER_OTHER_LABEL,
5015
- cancelledAskUserResult,
5016
- finalizeAskUserInterviewForHost,
5017
- sanitizeCallTargetText,
5018
- describeCallTarget,
5019
5196
  diffWords,
5020
5197
  patchProviderThinkingPayload,
5021
5198
  patchToolChoiceNonePayload,
@@ -5028,4 +5205,4 @@ export {
5028
5205
  assertRegisteredBuiltinTools,
5029
5206
  createRegistry
5030
5207
  };
5031
- //# sourceMappingURL=chunk-VMNQ6OZA.js.map
5208
+ //# sourceMappingURL=chunk-ZRGEBJ4T.js.map