@iowarp/clio-coder 0.3.3 → 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 (370) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/CONTRIBUTING.md +7 -7
  3. package/README.md +3 -3
  4. package/dist/{acp-P2AQILE2.js → acp-2BEHC4DL.js} +9 -8
  5. package/dist/{agents-72W3BI7I.js → agents-LNNFTM53.js} +29 -24
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-5TWEIYDN.js → auth-KXXFI2VS.js} +14 -10
  8. package/dist/{chunk-GGXXDWE4.js → chunk-22NAGB7X.js} +2 -2
  9. package/dist/{chunk-YCWGATWI.js → chunk-24I7BN55.js} +2 -2
  10. package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
  11. package/dist/chunk-3BPUFZDL.js +37 -0
  12. package/dist/chunk-43AOLP7E.js +375 -0
  13. package/dist/{chunk-ZDOOVTXZ.js → chunk-4OC57DA6.js} +27 -4
  14. package/dist/{chunk-6SGHMWE3.js → chunk-5JGRAMKL.js} +5 -5
  15. package/dist/{chunk-V6RTAOC2.js → chunk-6US73PDB.js} +572 -51
  16. package/dist/{chunk-5UFT4SUX.js → chunk-6XXKFVSN.js} +3 -3
  17. package/dist/{chunk-A3CYT5EX.js → chunk-AD2SYQYC.js} +55 -2
  18. package/dist/chunk-AOCYTWAV.js +449 -0
  19. package/dist/chunk-CFGTUFWB.js +67 -0
  20. package/dist/chunk-CJUB2JJ2.js +1478 -0
  21. package/dist/{chunk-FNTMWMX5.js → chunk-CKXWIANG.js} +14 -12
  22. package/dist/{chunk-PIWWS5BL.js → chunk-CYQKWTG3.js} +63 -78
  23. package/dist/{chunk-LZSJBIVT.js → chunk-DJVECN66.js} +271 -762
  24. package/dist/{chunk-CBCAPZAA.js → chunk-E25LMLRW.js} +2 -2
  25. package/dist/{chunk-ZWMF7253.js → chunk-E2ER4LJF.js} +304 -9
  26. package/dist/{chunk-STBPMHSX.js → chunk-EKY57CSP.js} +51 -84
  27. package/dist/{chunk-DUYJ5IO6.js → chunk-EYPA3EGJ.js} +12 -4
  28. package/dist/{chunk-M6SHUN7Q.js → chunk-FO5ZOVUY.js} +2 -2
  29. package/dist/chunk-FYYLNIL5.js +313 -0
  30. package/dist/{chunk-OQ33BKR3.js → chunk-G7MUEIGA.js} +3 -60
  31. package/dist/chunk-GEYXPTRF.js +613 -0
  32. package/dist/chunk-GOXNB3AO.js +261 -0
  33. package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
  34. package/dist/chunk-HWUFFB6L.js +83 -0
  35. package/dist/{chunk-4XUGQOHA.js → chunk-K7T3E2SR.js} +15 -8
  36. package/dist/chunk-K7VKOLQQ.js +15 -0
  37. package/dist/{chunk-UFIIWP2H.js → chunk-KHSFENX2.js} +8 -8
  38. package/dist/{chunk-BMEMKKIT.js → chunk-KOHPCX4K.js} +2 -2
  39. package/dist/chunk-LCGCVYZ4.js +57 -0
  40. package/dist/chunk-LL4KHSZI.js +22 -0
  41. package/dist/{chunk-PAJK6MAQ.js → chunk-LYF7OHWH.js} +42 -15
  42. package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
  43. package/dist/{chunk-5UUP6MWO.js → chunk-MV3K5QF2.js} +5 -436
  44. package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
  45. package/dist/{chunk-TZK7PACC.js → chunk-NILBFAPG.js} +14 -8
  46. package/dist/chunk-ODFEOB4F.js +1082 -0
  47. package/dist/{chunk-AGYYIBLL.js → chunk-OH3TOQTB.js} +6 -2
  48. package/dist/chunk-OZNBF4L3.js +23 -0
  49. package/dist/{chunk-DSELYM6W.js → chunk-PBTHKCPN.js} +30 -10
  50. package/dist/{verify-375KUB3Y.js → chunk-PCZJO5TI.js} +127 -42
  51. package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
  52. package/dist/{chunk-SRDMMSEP.js → chunk-QM3F2GKX.js} +1063 -1645
  53. package/dist/{chunk-X6IAEBZR.js → chunk-QNQHSOLF.js} +7 -7
  54. package/dist/{chunk-OC7FIQPC.js → chunk-R46L2BIR.js} +10 -7
  55. package/dist/{chunk-2TLUCQVG.js → chunk-RD5U66HV.js} +3 -3
  56. package/dist/{chunk-6N5PTWMY.js → chunk-RY3LY4J5.js} +50 -13
  57. package/dist/{chunk-OKGUZO2U.js → chunk-SPULKLCF.js} +4 -3
  58. package/dist/{chunk-OOJYHWRB.js → chunk-TSHXZTOQ.js} +6 -5
  59. package/dist/chunk-TZSKNMZG.js +434 -0
  60. package/dist/{chunk-7MNJORFF.js → chunk-UL3WSD3F.js} +6 -1
  61. package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
  62. package/dist/{chunk-COU2UHX6.js → chunk-VEZEGCGW.js} +170 -2
  63. package/dist/chunk-W6GROXXM.js +69 -0
  64. package/dist/{chunk-OAO4GE4M.js → chunk-WHGPSPT5.js} +2 -2
  65. package/dist/chunk-WHJYKASB.js +677 -0
  66. package/dist/{chunk-ORBHGJC5.js → chunk-WR67VIZY.js} +3 -3
  67. package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
  68. package/dist/{chunk-TZTZS7QK.js → chunk-XE2VEJHX.js} +5 -3
  69. package/dist/{chunk-LM5TQCJZ.js → chunk-XF5N4U5A.js} +8 -7
  70. package/dist/{chunk-LWLEKMDQ.js → chunk-XXQNGV4M.js} +1073 -552
  71. package/dist/{chunk-KZWTDYJF.js → chunk-XYDYPRZI.js} +7 -7
  72. package/dist/chunk-ZGVHUX3M.js +66 -0
  73. package/dist/{chunk-LW6DSM3M.js → chunk-ZRGEBJ4T.js} +1192 -1119
  74. package/dist/{chunk-2DJ2KNFG.js → chunk-ZXF4XRKW.js} +202 -40
  75. package/dist/chunk-ZZMN5OM4.js +122 -0
  76. package/dist/cli/index.js +34 -30
  77. package/dist/{clio-JOU4FXVA.js → clio-M2KGYUFZ.js} +7 -6
  78. package/dist/{code-nav-7AX6FYE6.js → code-nav-GQNL7XA6.js} +8 -6
  79. package/dist/codewiki/build-worker.js +4 -4
  80. package/dist/{components-KELWS457.js → components-5TTYYX6G.js} +3 -3
  81. package/dist/{config-XCDVKR23.js → config-XUUYQIWO.js} +47 -35
  82. package/dist/{configure-4GAP54ZW.js → configure-IHJ7YOMV.js} +18 -15
  83. package/dist/{context-77FM5DV5.js → context-74JLXAWD.js} +18 -10
  84. package/dist/{context-4UOGGLQ5.js → context-75MIWW3U.js} +41 -29
  85. package/dist/{context-5VKGUVJJ.js → context-ZQ7SIFJV.js} +85 -9
  86. package/dist/{context-clear-XXJRLCJJ.js → context-clear-GYKWNUML.js} +41 -29
  87. package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
  88. package/dist/context-working-set-UX5KEP4J.js +1553 -0
  89. package/dist/{dispatch-runner-QPRDDBDX.js → dispatch-runner-GIJBHNFL.js} +47 -32
  90. package/dist/{docs-2C2LTVT2.js → docs-6FZSCG5B.js} +3 -3
  91. package/dist/{doctor-HR46URBJ.js → doctor-SVJ5BZCW.js} +12 -12
  92. package/dist/{eval-XSSNATB4.js → eval-CG6LLBLD.js} +54 -238
  93. package/dist/{evidence-6HG2PY2B.js → evidence-ZYFIEN42.js} +57 -28
  94. package/dist/{evolve-K7YU3NCY.js → evolve-QGEXEMDW.js} +36 -25
  95. package/dist/{extensions-QVDOHDGJ.js → extensions-ADGNCJJD.js} +3 -3
  96. package/dist/{fleet-VY3HHKN6.js → fleet-S5R4ZOQY.js} +73 -44
  97. package/dist/{fleet-preflight-DDN536IT.js → fleet-preflight-BHSNPBMH.js} +3 -3
  98. package/dist/{init-JYGXI3FK.js → init-5DRU55YR.js} +49 -37
  99. package/dist/memory-7YKKR6UC.js +467 -0
  100. package/dist/{models-I5QWSEOM.js → models-ZPOLRU2C.js} +24 -21
  101. package/dist/{monitor-GE4ID3IA.js → monitor-US5F5YGZ.js} +73 -46
  102. package/dist/{orchestrator-EM5MC3HM.js → orchestrator-E2AL4T5N.js} +1624 -1007
  103. package/dist/{paths-UXLN5YYZ.js → paths-E7KYAQWE.js} +3 -3
  104. package/dist/{reset-L2FQEE3E.js → reset-KZ652EK6.js} +6 -5
  105. package/dist/{run-ZU3QMZPZ.js → run-SRNBKDWD.js} +76 -54
  106. package/dist/{share-S5BZQC5I.js → share-CGZE33UP.js} +7 -6
  107. package/dist/{skills-X5VXCRNQ.js → skills-S2X4DLY5.js} +4 -4
  108. package/dist/{skills-eval-WKIHWTHR.js → skills-eval-W2GGIC4R.js} +40 -29
  109. package/dist/{targets-SNCPI2NR.js → targets-54SWINWB.js} +28 -23
  110. package/dist/{terminal-lease-BNAHVHBS.js → terminal-lease-SAIF2OGY.js} +6 -4
  111. package/dist/{uninstall-FZCQCDKC.js → uninstall-BVLWXKBT.js} +3 -3
  112. package/dist/{upgrade-JQHHPQ4K.js → upgrade-JKAR27XC.js} +20 -19
  113. package/dist/{usage-OR4O5SMZ.js → usage-MSAWCLX4.js} +79 -36
  114. package/dist/verifiers-NCBTHHN2.js +1220 -0
  115. package/dist/verify-X5HDROLA.js +25 -0
  116. package/dist/{wiki-generate-UEXP2ARI.js → wiki-generate-GUSOQ6ZP.js} +50 -37
  117. package/dist/worker/entry.js +90 -70
  118. package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
  119. package/docs/README.md +8 -7
  120. package/docs/acp.md +1 -1
  121. package/docs/alcf-provider.md +1 -1
  122. package/docs/architecture.md +2 -2
  123. package/docs/artifact-placement.md +1 -2
  124. package/docs/artifact-versions.md +1 -1
  125. package/docs/built-in-agents.md +1 -1
  126. package/docs/capacity-and-scheduling.md +1 -1
  127. package/docs/commands-and-modes.md +60 -26
  128. package/docs/config-knobs-audit.md +1 -2
  129. package/docs/configuration-and-targets.md +26 -1
  130. package/docs/context-engine.md +67 -13
  131. package/docs/context-working-set.md +194 -0
  132. package/docs/development-pipeline.md +1 -1
  133. package/docs/documentation-coverage.md +6 -6
  134. package/docs/documentation-guide.md +7 -6
  135. package/docs/environment-variables.md +2 -1
  136. package/docs/eval-runner.md +1 -1
  137. package/docs/evals-internal.md +4 -32
  138. package/docs/evidence-and-memory.md +139 -7
  139. package/docs/evolution.md +1 -1
  140. package/docs/exit-codes-and-output.md +1 -1
  141. package/docs/extensions-and-sharing.md +2 -2
  142. package/docs/fleet-dispatch.md +49 -8
  143. package/docs/glossary.md +21 -1
  144. package/docs/installation-and-lifecycle.md +2 -2
  145. package/docs/middleware-and-components.md +19 -2
  146. package/docs/model-catalog.md +7 -9
  147. package/docs/observability.md +4 -4
  148. package/docs/proactive-memory.md +26 -16
  149. package/docs/prompt-envelope-and-tools.md +7 -5
  150. package/docs/provider-adapter-cookbook.md +1 -1
  151. package/docs/release-cut-checklist.md +43 -40
  152. package/docs/safety-model.md +49 -8
  153. package/docs/scientific-validation.md +21 -3
  154. package/docs/session-lifecycle.md +3 -3
  155. package/docs/skills-marketplace.md +1 -1
  156. package/docs/tool-usage.md +79 -12
  157. package/docs/trace-store.md +1 -1
  158. package/docs/troubleshooting.md +1 -1
  159. package/docs/tui-design.md +38 -4
  160. package/docs/worker-dispatch-mechanics.md +11 -1
  161. package/package.json +13 -13
  162. package/skills/meta/clio-test/SKILL.md +20 -17
  163. package/skills/meta/clio-test/evals.md +3 -3
  164. package/skills/meta/clio-test/references/harness.md +35 -6
  165. package/skills/meta/clio-test/references/test-map.md +20 -10
  166. package/skills/registry.yaml +2 -2
  167. package/skills/skill-marketplace.json +1 -1
  168. package/src/cli/agents.ts +2 -3
  169. package/src/cli/argv.ts +14 -1
  170. package/src/cli/context-working-set.ts +513 -0
  171. package/src/cli/context.ts +8 -0
  172. package/src/cli/evidence.ts +20 -2
  173. package/src/cli/fleet.ts +15 -0
  174. package/src/cli/index.ts +5 -1
  175. package/src/cli/memory.ts +272 -10
  176. package/src/cli/modes/json-stream.ts +2 -2
  177. package/src/cli/modes/print.ts +12 -1
  178. package/src/cli/run.ts +22 -2
  179. package/src/cli/targets.ts +12 -3
  180. package/src/cli/usage.ts +55 -7
  181. package/src/cli/verifiers.ts +325 -0
  182. package/src/core/bash-exec.ts +39 -14
  183. package/src/core/bus-events.ts +22 -4
  184. package/src/core/config.ts +54 -0
  185. package/src/core/defaults.ts +50 -3
  186. package/src/core/response-model-id.ts +134 -0
  187. package/src/core/toml.ts +62 -0
  188. package/src/core/verification-scripts.ts +6 -0
  189. package/src/core/workspace-files.ts +0 -1
  190. package/src/domains/agents/builtins/architect.md +1 -1
  191. package/src/domains/agents/builtins/verifier.md +3 -0
  192. package/src/domains/agents/catalog.ts +5 -4
  193. package/src/domains/agents/recipe.ts +54 -14
  194. package/src/domains/agents/result-contract.ts +7 -4
  195. package/src/domains/config/classify.ts +1 -0
  196. package/src/domains/context/bootstrap.ts +36 -27
  197. package/src/domains/context/project-metadata.ts +19 -63
  198. package/src/domains/context/prompt-context.ts +8 -0
  199. package/src/domains/context/working-set/contract.ts +161 -0
  200. package/src/domains/context/working-set/defaults.ts +28 -0
  201. package/src/domains/context/working-set/engine.ts +203 -0
  202. package/src/domains/context/working-set/fold.ts +62 -0
  203. package/src/domains/context/working-set/horizon.ts +38 -0
  204. package/src/domains/context/working-set/marker.ts +103 -0
  205. package/src/domains/context/working-set/path-index.ts +436 -0
  206. package/src/domains/context/working-set/payload.ts +152 -0
  207. package/src/domains/context/working-set/policies/age-horizon.ts +55 -0
  208. package/src/domains/context/working-set/policies/index.ts +20 -0
  209. package/src/domains/context/working-set/policies/structural.ts +160 -0
  210. package/src/domains/context/working-set/project.ts +132 -0
  211. package/src/domains/context/working-set/protect.ts +109 -0
  212. package/src/domains/context/working-set/recall.ts +177 -0
  213. package/src/domains/context/working-set/replay/controls.ts +112 -0
  214. package/src/domains/context/working-set/replay/load-clio.ts +199 -0
  215. package/src/domains/context/working-set/replay/metrics.ts +185 -0
  216. package/src/domains/context/working-set/replay/reference-graph.ts +79 -0
  217. package/src/domains/context/working-set/replay/report.ts +139 -0
  218. package/src/domains/context/working-set/replay/runner.ts +325 -0
  219. package/src/domains/context/working-set/replay/synthetic.ts +422 -0
  220. package/src/domains/context/working-set/replay/trace.ts +21 -0
  221. package/src/domains/context/working-set/visible.ts +54 -0
  222. package/src/domains/dispatch/budget-envelope.ts +396 -0
  223. package/src/domains/dispatch/contract.ts +2 -0
  224. package/src/domains/dispatch/extension.ts +81 -27
  225. package/src/domains/dispatch/orphan-recovery.ts +1 -0
  226. package/src/domains/dispatch/receipt-integrity.ts +4 -0
  227. package/src/domains/dispatch/state.ts +1 -0
  228. package/src/domains/dispatch/types.ts +10 -3
  229. package/src/domains/dispatch/validation.ts +14 -0
  230. package/src/domains/dispatch/worker-spawn.ts +14 -3
  231. package/src/domains/eval/metrics/evidence.ts +0 -116
  232. package/src/domains/eval/metrics/invariants.ts +1 -1
  233. package/src/domains/eval/runners/clio-run.ts +1 -10
  234. package/src/domains/eval/runners/external-command.ts +2 -29
  235. package/src/domains/eval/schema/suite.ts +0 -7
  236. package/src/domains/eval/suites/run.ts +1 -7
  237. package/src/domains/evidence/build.ts +112 -45
  238. package/src/domains/evidence/eval.ts +24 -7
  239. package/src/domains/evidence/index.ts +53 -0
  240. package/src/domains/evidence/ordering.ts +12 -0
  241. package/src/domains/evidence/run-trust.ts +221 -0
  242. package/src/domains/evidence/store.ts +46 -6
  243. package/src/domains/evidence/trust-status.ts +854 -0
  244. package/src/domains/evidence/types.ts +26 -0
  245. package/src/domains/memory/index.ts +22 -0
  246. package/src/domains/memory/operations.ts +58 -1
  247. package/src/domains/memory/promotion.ts +281 -0
  248. package/src/domains/memory/prompt-section.ts +25 -5
  249. package/src/domains/memory/proposal.ts +51 -7
  250. package/src/domains/memory/task-bank.ts +3 -2
  251. package/src/domains/memory/task-memory-handoff.ts +181 -24
  252. package/src/domains/memory/task-memory-policy.ts +3 -1
  253. package/src/domains/memory/types.ts +37 -0
  254. package/src/domains/memory/validate.ts +178 -0
  255. package/src/domains/middleware/memory-intervention.ts +38 -25
  256. package/src/domains/middleware/runtime.ts +6 -0
  257. package/src/domains/middleware/skills-reminder.ts +19 -4
  258. package/src/domains/middleware/stalled-turn.ts +208 -5
  259. package/src/domains/middleware/types.ts +10 -0
  260. package/src/domains/observability/contract.ts +6 -1
  261. package/src/domains/observability/cost.ts +20 -4
  262. package/src/domains/observability/extension.ts +2 -2
  263. package/src/domains/providers/index.ts +3 -0
  264. package/src/domains/providers/model-discovery.ts +9 -0
  265. package/src/domains/providers/runtime-resolution.ts +38 -1
  266. package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
  267. package/src/domains/providers/types/context-window-slots.ts +18 -0
  268. package/src/domains/providers/types/runtime-descriptor.ts +3 -1
  269. package/src/domains/safety/autonomy.ts +1 -1
  270. package/src/domains/safety/call-target.ts +211 -14
  271. package/src/domains/safety/decision-presentation.ts +268 -0
  272. package/src/domains/safety/default-path-policy.ts +8 -0
  273. package/src/domains/safety/finish-contract.ts +4 -3
  274. package/src/domains/safety/policy-engine.ts +48 -6
  275. package/src/domains/safety/redaction.ts +73 -0
  276. package/src/domains/session/compaction/compact.ts +23 -1
  277. package/src/domains/session/compaction/cut-point.ts +2 -0
  278. package/src/domains/session/compaction/tokens.ts +16 -1
  279. package/src/domains/session/context-ledger.ts +12 -1
  280. package/src/domains/session/decision-board.ts +4 -0
  281. package/src/domains/session/entries.ts +110 -1
  282. package/src/domains/session/history.ts +68 -19
  283. package/src/domains/session/manager.ts +9 -2
  284. package/src/domains/session/migrations/index.ts +22 -3
  285. package/src/domains/session/usage.ts +24 -7
  286. package/src/engine/acp/event-mapper.ts +7 -0
  287. package/src/engine/acp/server.ts +32 -2
  288. package/src/engine/agent.ts +18 -1
  289. package/src/engine/apis/lmstudio.ts +25 -4
  290. package/src/engine/apis/openai-completions.ts +147 -22
  291. package/src/engine/claude/sdk-runtime.ts +8 -2
  292. package/src/engine/claude/tool-safety.ts +13 -0
  293. package/src/engine/loop-guard.ts +27 -3
  294. package/src/engine/session.ts +9 -3
  295. package/src/engine/worker-events.ts +4 -3
  296. package/src/engine/worker-runtime.ts +59 -54
  297. package/src/entry/orchestrator.ts +34 -5
  298. package/src/interactive/chat-loop-messages.ts +40 -6
  299. package/src/interactive/chat-loop.ts +13 -0
  300. package/src/interactive/chat-panel.ts +17 -1
  301. package/src/interactive/chat-renderer.ts +49 -24
  302. package/src/interactive/clio-editor.ts +44 -7
  303. package/src/interactive/context-meter.ts +10 -0
  304. package/src/interactive/context-overlay.ts +120 -7
  305. package/src/interactive/context-recall-command.ts +110 -0
  306. package/src/interactive/cost-overlay.ts +39 -8
  307. package/src/interactive/dispatch-board.ts +212 -35
  308. package/src/interactive/footer/widgets.ts +13 -0
  309. package/src/interactive/interactive-application.ts +6 -1
  310. package/src/interactive/interactive-input-runtime.ts +11 -1
  311. package/src/interactive/interactive-presentation.ts +11 -1
  312. package/src/interactive/interactive-slash-runtime.ts +37 -1
  313. package/src/interactive/memory-overlay.ts +89 -4
  314. package/src/interactive/model-session-replay.ts +21 -0
  315. package/src/interactive/overlay-ask-user-lifecycle.ts +1 -1
  316. package/src/interactive/overlay-frame.ts +5 -2
  317. package/src/interactive/overlay-general-openers.ts +46 -1
  318. package/src/interactive/overlay-key-routing.ts +41 -1
  319. package/src/interactive/overlay-lifecycle.ts +11 -4
  320. package/src/interactive/overlay-permission-lifecycle.ts +23 -8
  321. package/src/interactive/overlay-session-lifecycle.ts +8 -4
  322. package/src/interactive/overlay-transitions.ts +11 -0
  323. package/src/interactive/overlays/ask-user.ts +74 -30
  324. package/src/interactive/overlays/decisions.ts +3 -1
  325. package/src/interactive/permission-hint.ts +35 -0
  326. package/src/interactive/permission-overlay.ts +95 -45
  327. package/src/interactive/renderers/tool-execution.ts +37 -51
  328. package/src/interactive/session-last-turn.ts +8 -1
  329. package/src/interactive/session-transcript.ts +2 -2
  330. package/src/interactive/session-usage-reseed.ts +36 -10
  331. package/src/interactive/slash-commands.ts +31 -4
  332. package/src/interactive/status/summary.ts +5 -0
  333. package/src/interactive/status/types.ts +5 -0
  334. package/src/interactive/terminal-lease.ts +1 -0
  335. package/src/interactive/turn-context.ts +333 -110
  336. package/src/interactive/turn-middleware.ts +7 -6
  337. package/src/interactive/turn-runtime.ts +37 -8
  338. package/src/interactive/turn-state.ts +3 -0
  339. package/src/interactive/worker-progress.ts +440 -0
  340. package/src/interactive/worker-stream.ts +51 -110
  341. package/src/tools/agent-tools.ts +39 -7
  342. package/src/tools/ask-user.ts +21 -1
  343. package/src/tools/bash.ts +144 -82
  344. package/src/tools/builtin-tool-catalog.ts +11 -5
  345. package/src/tools/context/index.ts +107 -5
  346. package/src/tools/context/surface.ts +3 -2
  347. package/src/tools/core-bootstrap.ts +21 -0
  348. package/src/tools/dispatch-arguments.ts +8 -0
  349. package/src/tools/dispatch-event-text.ts +19 -0
  350. package/src/tools/dispatch-runner.ts +9 -7
  351. package/src/tools/dispatch.ts +24 -1
  352. package/src/tools/monitor.ts +43 -20
  353. package/src/tools/registry.ts +72 -10
  354. package/src/tools/result-disposition.ts +706 -0
  355. package/src/tools/result-shaping.ts +321 -20
  356. package/src/tools/safe-exec.ts +2 -0
  357. package/src/tools/verify/authoring.ts +1120 -0
  358. package/src/tools/verify/catalog.ts +346 -0
  359. package/src/tools/verify/index.ts +13 -3
  360. package/src/tools/verify/scripts.ts +135 -37
  361. package/src/tools/verify/surface.ts +9 -5
  362. package/src/tools/worker-evidence.ts +54 -12
  363. package/src/worker/spec-contract.ts +43 -3
  364. package/dist/chunk-J7CWMCQD.js +0 -255
  365. package/dist/chunk-T6YILFSB.js +0 -80
  366. package/dist/chunk-VAKQQHWR.js +0 -434
  367. package/dist/chunk-VPAYEGVX.js +0 -184
  368. package/dist/chunk-XBXAASKX.js +0 -18
  369. package/dist/memory-WFZMGYHX.js +0 -236
  370. package/src/domains/eval/metrics/chaos-stream.ts +0 -93
@@ -1,43 +1,41 @@
1
1
  import { createRequire as __clioCreateRequire } from "node:module"; const require = __clioCreateRequire(import.meta.url);
2
2
  import {
3
- BASH_HARD_CAP_BYTES,
4
- HEADLESS_PERMISSION_DENIED_MARKER,
5
- combineBashOutput,
6
- runBashCommand
7
- } from "./chunk-J7CWMCQD.js";
3
+ HEADLESS_PERMISSION_DENIED_MARKER
4
+ } from "./chunk-K7VKOLQQ.js";
8
5
  import {
9
6
  hashToolCall
10
7
  } from "./chunk-UOV2BYIW.js";
11
8
  import {
12
9
  describeDispatchPlan
13
- } from "./chunk-DUYJ5IO6.js";
10
+ } from "./chunk-EYPA3EGJ.js";
14
11
  import {
15
12
  RESPONSE_SCHEMA_RUNTIME_ID,
16
13
  applyToolProfile,
14
+ bashTool,
17
15
  builtin,
18
- createTaskBoardStore,
16
+ describeCallAction,
19
17
  networkToolsDisabled,
20
18
  readMaxBytes,
21
19
  readTool,
22
- renderAgentLedger,
20
+ renderAgentLedger
21
+ } from "./chunk-XXQNGV4M.js";
22
+ import {
23
+ createTaskBoardStore,
23
24
  taskBoardCounts
24
- } from "./chunk-LWLEKMDQ.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-STBPMHSX.js";
27
+ formatModelRejection
28
+ } from "./chunk-EKY57CSP.js";
29
+ import {
30
+ classify
31
+ } from "./chunk-AOCYTWAV.js";
33
32
  import {
34
33
  CLIO_ARTIFACT_DIR,
35
34
  artifactDefaultPath,
36
- classify,
37
35
  detectValidationCommand,
38
36
  protectArtifact,
39
37
  protectedArtifactMutationBlockReason
40
- } from "./chunk-5UUP6MWO.js";
38
+ } from "./chunk-MV3K5QF2.js";
41
39
  import {
42
40
  parseAgentLedgerBody
43
41
  } from "./chunk-CMZWFGD2.js";
@@ -47,7 +45,7 @@ import {
47
45
  import {
48
46
  gitTool,
49
47
  verifyToolSurface
50
- } from "./chunk-TZK7PACC.js";
48
+ } from "./chunk-NILBFAPG.js";
51
49
  import {
52
50
  codeNavToolSurface
53
51
  } from "./chunk-XN3L4EYL.js";
@@ -59,35 +57,43 @@ import {
59
57
  rgIgnoreArgs
60
58
  } from "./chunk-VG7TBQIY.js";
61
59
  import {
62
- expandPath,
63
60
  resolveReadPath,
64
61
  resolveToCwd,
65
62
  toPosixPath
66
63
  } from "./chunk-OQE5J4C6.js";
67
64
  import {
68
65
  contextToolSurface
69
- } from "./chunk-OKGUZO2U.js";
66
+ } from "./chunk-SPULKLCF.js";
70
67
  import {
71
68
  evaluateSkillToolSurface,
72
69
  skillActivationFromToolDetails
73
- } from "./chunk-T6YILFSB.js";
70
+ } from "./chunk-GOXNB3AO.js";
74
71
  import {
75
- DEFAULT_MAX_LINES,
76
- GREP_MAX_LINE_LENGTH,
72
+ DEFAULT_AUTONOMY_EXPOSURE,
73
+ DEFAULT_AUTONOMY_LEVEL,
74
+ autonomyAskRejection,
75
+ autonomyDenyRejection,
76
+ mapAutonomy
77
+ } from "./chunk-HWUFFB6L.js";
78
+ import {
79
+ DEFAULT_TOOL_RESULT_MAX_BYTES,
77
80
  OBSERVATION_POLICY_SLACK_BYTES,
78
81
  OBSERVE_SELF_CAPS,
79
82
  commitObservationReservation,
80
83
  finalizeObservation,
81
- formatSize,
84
+ isDispositionedToolResultError,
82
85
  observationBudgetExhausted,
83
86
  releaseObservation,
84
87
  reserveObservation,
85
88
  shapeToolResult,
86
- truncateHead,
87
- truncateLine,
88
- truncateTail,
89
- writeToolOffload
90
- } from "./chunk-VAKQQHWR.js";
89
+ toolResultContextText,
90
+ toolResultDigestFor,
91
+ truncateHead
92
+ } from "./chunk-ODFEOB4F.js";
93
+ import {
94
+ GREP_MAX_LINE_LENGTH,
95
+ truncateLine
96
+ } from "./chunk-OZNBF4L3.js";
91
97
  import {
92
98
  truncateUtf8
93
99
  } from "./chunk-6XLNIQDB.js";
@@ -102,9 +108,8 @@ import {
102
108
  incompleteInstallationAdvice
103
109
  } from "./chunk-65DEGPJ6.js";
104
110
  import {
105
- buildSafeToolEnv,
106
- resolveSafeCwd
107
- } from "./chunk-VPAYEGVX.js";
111
+ buildSafeToolEnv
112
+ } from "./chunk-VEZEGCGW.js";
108
113
  import {
109
114
  GUARDRAIL_DEFAULTS,
110
115
  isWorkerToolCallCapExceededReason,
@@ -123,7 +128,7 @@ import {
123
128
  } from "./chunk-BNAZZHFG.js";
124
129
  import {
125
130
  BusChannels
126
- } from "./chunk-OQ33BKR3.js";
131
+ } from "./chunk-G7MUEIGA.js";
127
132
  import {
128
133
  init_esm_shims
129
134
  } from "./chunk-3R73A4XB.js";
@@ -237,781 +242,445 @@ function cloneArtifact(artifact) {
237
242
  return clone;
238
243
  }
239
244
 
240
- // src/engine/loop-guard.ts
245
+ // src/tools/ask-user.ts
241
246
  init_esm_shims();
242
- var LOOP_GUARD_REGISTRATION_ID = "guard.loop";
243
- var INTERACTIVE_LOOP_BLOCK_BUDGET = 2;
244
- var WORKER_LOOP_BLOCK_CALLS_PER_BLOCK = 10;
245
- function workerLoopBlockBudget(toolCalls) {
246
- if (!Number.isSafeInteger(toolCalls) || toolCalls <= 0) return INTERACTIVE_LOOP_BLOCK_BUDGET;
247
- return Math.max(INTERACTIVE_LOOP_BLOCK_BUDGET, Math.ceil(toolCalls / WORKER_LOOP_BLOCK_CALLS_PER_BLOCK));
248
- }
249
- var LOOP_SYNTHESIS_BACKSTOP_DENIALS = 2;
250
- var WIDE_BATCH_DENIAL_FLOOR = 32;
251
- var DEFAULT_WORKER_TOOL_CALL_CAP = GUARDRAIL_DEFAULTS.workerToolCallCap;
252
- var DEFAULT_ORCH_TURN_TOOL_CALL_BUDGET = GUARDRAIL_DEFAULTS.turnToolCallBudget;
253
- var ORCH_TURN_TOOL_CALL_HARD_MARGIN = 15;
254
- var LOOP_GUARD_TURN_LIMIT = 32;
255
- var SUCCEEDED_FINGERPRINT_LIMIT = 128;
256
- var NO_TURN_BUCKET = "no-turn";
257
- var SIZE_ONLY_ARG_KEYS = /* @__PURE__ */ new Set(["limit", "offset", "max_bytes", "context"]);
258
- var RESULT_STAGNATION_THRESHOLD = 3;
259
- var CROSS_ARGUMENT_RESULT_MIN_BYTES = 64;
260
- function stagnationFingerprint(tool, args) {
261
- const reduced = {};
262
- for (const [key, value] of Object.entries(args ?? {})) {
263
- if (!SIZE_ONLY_ARG_KEYS.has(key)) reduced[key] = value;
264
- }
265
- return hashToolCall(tool, reduced);
266
- }
267
- function stagnationBlockReason(tool, identicalResults) {
268
- 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.`;
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
+ };
269
294
  }
270
- function crossArgumentResultMessage(tool, distinctArguments) {
271
- 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.`;
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
+ };
272
305
  }
273
- function loopBlockBaseReason(tool, repeatCount, priorSuccesses) {
274
- 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. ` : "";
275
- 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.`;
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
+ ];
276
401
  }
277
- function synthesisLockoutDirective() {
278
- 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.";
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
+ };
279
413
  }
280
- function synthesisBackstopReason(tool) {
281
- 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.`;
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);
282
470
  }
283
- function workerExplorationSynthesisDirective(limit) {
284
- 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.`;
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;
285
475
  }
286
- function disallowedToolsText(deliveryTools) {
287
- const candidates = ["code_nav", "ls", "grep", "find", "context", "git", "shell commands"];
288
- const excluded = candidates.filter((tool) => !deliveryTools.includes(tool));
289
- return excluded.join(", ");
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" };
290
485
  }
291
- function workerLiveReadReserveDirective(remaining, deliveryTools) {
292
- const admitted = deliveryTools.length > 0 ? `read and ${deliveryTools.join("/")}` : "the read tool";
293
- 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.";
294
- 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)}.`;
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" };
295
491
  }
296
- function workerDeliveryOnlyDirective(limit, deliveryTools) {
297
- 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)}.`;
492
+ function askUserExposure(args) {
493
+ if (args?.exposure === void 0) return DEFAULT_AUTONOMY_EXPOSURE;
494
+ return normalizeExposure(args.exposure).exposure === "local" ? "local" : "outward";
298
495
  }
299
- var DEAD_TOOL_CALL_MARKUP_PATTERNS = [
300
- /<tool_call>[\s\S]*?<\/tool_call>/gi,
301
- /<function=[\w.-]+>[\s\S]*?<\/function>/gi,
302
- /<tool_call>\s*(?:<function=|[{[])[\s\S]*$/i,
303
- /<function=[\w.-]+>[\s\S]*$/i
304
- ];
305
- function stripDeadToolCallMarkup(text) {
306
- let out = text;
307
- for (const pattern of DEAD_TOOL_CALL_MARKUP_PATTERNS) {
308
- out = out.replace(pattern, "");
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}` };
309
500
  }
310
- return out === text ? text : out.trim();
311
- }
312
- function lockedSynthesisFallbackText() {
313
- 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.";
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 };
314
505
  }
315
- function sanitizeLockedSynthesisMessage(message) {
316
- if (!message || typeof message !== "object" || message.role !== "assistant") return false;
317
- const record = message;
318
- const stopReason = record.stopReason;
319
- if (stopReason !== void 0 && stopReason !== "stop" && stopReason !== "length") return false;
320
- if (!Array.isArray(record.content)) return false;
321
- const blocks = record.content;
322
- if (blocks.some((block) => block?.type === "toolCall")) return false;
323
- let changed = false;
324
- for (const block of blocks) {
325
- if (block?.type !== "text" || typeof block.text !== "string") continue;
326
- const stripped = stripDeadToolCallMarkup(block.text);
327
- if (stripped !== block.text) {
328
- block.text = stripped;
329
- changed = true;
330
- }
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);
331
519
  }
332
- if (!changed) return false;
333
- const remaining = blocks.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("").trim();
334
- if (remaining.length === 0) {
335
- const firstText = blocks.find(
336
- (block) => block?.type === "text" && typeof block.text === "string"
337
- );
338
- if (firstText) firstText.text = lockedSynthesisFallbackText();
339
- else blocks.push({ type: "text", text: lockedSynthesisFallbackText() });
520
+ return options.length > 0 ? { options } : {};
521
+ }
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);
340
541
  }
341
- return true;
542
+ return { questions };
342
543
  }
343
- function isLoopGuardSynthesisBackstopReason(reason) {
344
- return /^loop guard: tool calls stayed disabled and .+ was called again instead of answering/.test(reason);
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 };
345
554
  }
346
- function readOrchTurnToolCallBudget(env = process.env) {
347
- const soft = resolveGuardrail("turnToolCallBudget", env);
348
- return { soft, hard: soft + ORCH_TURN_TOOL_CALL_HARD_MARGIN };
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);
564
+ }
565
+ return { decisions };
349
566
  }
350
- function createLoopGuardRegistration(options) {
351
- const budget = options.turnBlockBudget ?? INTERACTIVE_LOOP_BLOCK_BUDGET;
352
- const cap = options.toolCallCap;
353
- const softLimit = options.toolCallSoftLimit !== void 0 && Number.isSafeInteger(options.toolCallSoftLimit) && options.toolCallSoftLimit > 0 && (cap === void 0 || options.toolCallSoftLimit <= cap) ? options.toolCallSoftLimit : void 0;
354
- const turnBudget = options.turnToolCallBudget;
355
- const synthesisLockout = options.turnSynthesisLockout === true;
356
- const softReadReserve = options.toolCallSoftReadReserve ?? 0;
357
- const softReadReserveThreshold = softLimit !== void 0 && softReadReserve > 0 && softLimit > softReadReserve ? softLimit - softReadReserve : null;
358
- const deliveryTools = [...new Set(options.deliveryTools ?? [])];
359
- const reserveAdmits = (tool) => isReserveAdmittedTool(tool, deliveryTools);
360
- let softAdmittedCount = 0;
361
- let softReadReserveEntered = false;
362
- let reserveDenials = { denials: 0 };
363
- let deniedAttempts = 0;
364
- const denialBudget = cap === void 0 ? void 0 : Math.max(cap, WIDE_BATCH_DENIAL_FLOOR);
365
- const boundRunDenials = (effects, input) => {
366
- if (denialBudget === void 0 || !effects.some((effect) => effect.kind === "block_tool")) return effects;
367
- deniedAttempts += 1;
368
- if (deniedAttempts <= denialBudget) return effects;
369
- return [{ kind: "block_tool", reason: synthesisBackstopReason(input.toolName ?? "unknown"), severity: "hard-block" }];
370
- };
371
- const reserve = options.toolCallReserve ?? 0;
372
- const reserveThreshold = cap !== void 0 && reserve > 0 && cap > reserve ? cap - reserve : null;
373
- let reserveDirectiveEmitted = false;
374
- const blocksByTurn = /* @__PURE__ */ new Map();
375
- const callsByTurn = /* @__PURE__ */ new Map();
376
- const lockoutByTurn = /* @__PURE__ */ new Map();
377
- let capLockout = null;
378
- let softLockout = null;
379
- const succeededFingerprints = /* @__PURE__ */ new Map();
380
- const stagnationByTurn = /* @__PURE__ */ new Map();
381
- const crossArgumentResultsByTurn = /* @__PURE__ */ new Map();
382
- let count = 0;
383
- const enterSoftReadReserve = () => {
384
- if (softReadReserveEntered) return;
385
- softReadReserveEntered = true;
386
- options.onSoftReadReserve?.();
387
- };
388
- const bumpBoundedCounter = (store, key, limit = LOOP_GUARD_TURN_LIMIT) => {
389
- if (!store.has(key)) {
390
- while (store.size >= limit) {
391
- const oldest = store.keys().next().value;
392
- if (typeof oldest !== "string") break;
393
- store.delete(oldest);
394
- }
395
- }
396
- const next = (store.get(key) ?? 0) + 1;
397
- store.set(key, next);
398
- return next;
399
- };
400
- const resultCarriesEvidence = (details) => {
401
- const observation = details?.observation;
402
- if (observation === null || typeof observation !== "object" || Array.isArray(observation)) return true;
403
- const record = observation;
404
- return !(record.truncated === true && record.shownCount === 0);
405
- };
406
- const recordSuccessfulResult = (input) => {
407
- if (input.metadata?.resultKind !== "ok") return;
408
- if (!resultCarriesEvidence(input.toolResultDetails)) return;
409
- const tool = input.toolName;
410
- if (typeof tool !== "string" || tool.length === 0) return;
411
- bumpBoundedCounter(succeededFingerprints, hashToolCall(tool, input.toolArgs ?? {}), SUCCEEDED_FINGERPRINT_LIMIT);
412
- };
413
- const recordResultForStagnation = (input) => {
414
- const turnKey = input.turnId ?? NO_TURN_BUCKET;
415
- const tool = input.toolName;
416
- const resultFingerprint = input.metadata?.resultFingerprint;
417
- if (typeof tool !== "string" || tool.length === 0 || input.metadata?.resultKind !== "ok" || typeof resultFingerprint !== "string") {
418
- stagnationByTurn.delete(turnKey);
419
- return;
420
- }
421
- const reducedFingerprint = stagnationFingerprint(tool, input.toolArgs);
422
- const previous = stagnationByTurn.get(turnKey);
423
- if (previous !== void 0 && previous.reducedFingerprint === reducedFingerprint && previous.resultFingerprint === resultFingerprint) {
424
- previous.streak += 1;
425
- return;
426
- }
427
- if (!stagnationByTurn.has(turnKey)) {
428
- while (stagnationByTurn.size >= LOOP_GUARD_TURN_LIMIT) {
429
- const oldest = stagnationByTurn.keys().next().value;
430
- if (typeof oldest !== "string") break;
431
- stagnationByTurn.delete(oldest);
432
- }
433
- }
434
- stagnationByTurn.set(turnKey, { reducedFingerprint, resultFingerprint, streak: 1 });
435
- };
436
- const crossArgumentResultEffects = (input) => {
437
- const turnKey = input.turnId ?? NO_TURN_BUCKET;
438
- const tool = input.toolName;
439
- const resultFingerprint = input.metadata?.resultFingerprint;
440
- const resultBytes = input.metadata?.resultBytes;
441
- 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)) {
442
- crossArgumentResultsByTurn.delete(turnKey);
443
- return [];
444
- }
445
- const argumentFingerprint = stagnationFingerprint(tool, input.toolArgs);
446
- const previous = crossArgumentResultsByTurn.get(turnKey);
447
- if (previous?.tool === tool && previous.resultFingerprint === resultFingerprint) {
448
- if (previous.warned) return [];
449
- previous.argumentFingerprints.add(argumentFingerprint);
450
- if (previous.argumentFingerprints.size >= RESULT_STAGNATION_THRESHOLD) {
451
- previous.warned = true;
452
- return [
453
- {
454
- kind: "annotate_tool_result",
455
- message: crossArgumentResultMessage(tool, previous.argumentFingerprints.size),
456
- severity: "warn"
457
- }
458
- ];
459
- }
460
- return [];
461
- }
462
- if (!crossArgumentResultsByTurn.has(turnKey)) {
463
- while (crossArgumentResultsByTurn.size >= LOOP_GUARD_TURN_LIMIT) {
464
- const oldest = crossArgumentResultsByTurn.keys().next().value;
465
- if (typeof oldest !== "string") break;
466
- crossArgumentResultsByTurn.delete(oldest);
467
- }
567
+ function normalizeAskUserCall(args) {
568
+ const rawAction = trimOptionalString(args.action)?.toLowerCase() ?? "ask";
569
+ if (rawAction !== "ask" && rawAction !== "complete") {
570
+ return { error: "action must be ask or complete" };
571
+ }
572
+ const mode = normalizeAskUserMode(args.mode);
573
+ if (mode.error) return { error: mode.error };
574
+ const exposure = normalizeExposure(args.exposure);
575
+ if (exposure.error) return { error: exposure.error };
576
+ const maxRounds = normalizeMaxRounds(args.max_rounds);
577
+ if (maxRounds.error) return { error: maxRounds.error };
578
+ const decisions = normalizeDecisions(args.decisions);
579
+ if (decisions.error) return { error: decisions.error };
580
+ const summary = trimOptionalString(args.summary);
581
+ if (rawAction === "complete") {
582
+ if (Array.isArray(args.questions) && args.questions.length > 0) {
583
+ return { error: "action=complete must not include questions" };
468
584
  }
469
- crossArgumentResultsByTurn.set(turnKey, {
470
- tool,
471
- resultFingerprint,
472
- argumentFingerprints: /* @__PURE__ */ new Set([argumentFingerprint]),
473
- warned: false
474
- });
475
- return [];
476
- };
477
- const bumpTurnBlocks = (turnId) => bumpBoundedCounter(blocksByTurn, turnId);
478
- const enterLockout = (input, turnKey, state) => {
479
- if (!lockoutByTurn.has(turnKey)) {
480
- while (lockoutByTurn.size >= LOOP_GUARD_TURN_LIMIT) {
481
- const oldest = lockoutByTurn.keys().next().value;
482
- if (typeof oldest !== "string") break;
483
- lockoutByTurn.delete(oldest);
585
+ return {
586
+ call: {
587
+ action: "complete",
588
+ ...decisions.decisions ? { decisions: decisions.decisions } : {},
589
+ ...summary ? { summary } : {}
484
590
  }
485
- }
486
- lockoutByTurn.set(turnKey, {
487
- ...state,
488
- denials: 0,
489
- ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
490
- });
491
- };
492
- const reachesLockoutBackstop = (state, input) => {
493
- if (input.correlationId !== void 0 && state.correlationId === input.correlationId) return false;
494
- state.denials += 1;
495
- if (input.correlationId !== void 0) state.correlationId = input.correlationId;
496
- else delete state.correlationId;
497
- return state.denials > LOOP_SYNTHESIS_BACKSTOP_DENIALS;
498
- };
499
- const emitLoopBlocked = (input, info, at) => {
500
- const payload = {
501
- tool: info.tool,
502
- repeatCount: info.repeatCount,
503
- blocksThisTurn: info.blocksThisTurn,
504
- budget,
505
- interrupted: info.disposition === "stop",
506
- disposition: info.disposition,
507
- at,
508
- ...input.turnId !== void 0 ? { turnId: input.turnId } : {}
509
591
  };
510
- options.bus?.emit(BusChannels.LoopBlocked, payload);
511
- };
512
- const emitBudgetEvent = (input, callsThisTurn, interrupted, at) => {
513
- if (turnBudget === void 0) return;
514
- const payload = {
515
- tool: input.toolName ?? "unknown",
516
- callsThisTurn,
517
- softBudget: turnBudget.soft,
518
- hardCeiling: turnBudget.hard,
519
- interrupted,
520
- at,
521
- ...input.turnId !== void 0 ? { turnId: input.turnId } : {}
592
+ }
593
+ const questions = normalizeAskUserQuestions(args);
594
+ if (!questions.questions) return { error: questions.error ?? "invalid questions" };
595
+ if (mode.mode === "single_question" && questions.questions.length !== 1) {
596
+ return {
597
+ error: `mode=single_question carries exactly 1 question and this call carried ${questions.questions.length}; send them one call at a time, or use mode=round to ask up to 4 together`
522
598
  };
523
- options.bus?.emit(BusChannels.ToolBudgetExceeded, payload);
524
- };
525
- const evaluateTurnBudget = (input, now) => {
526
- if (turnBudget === void 0) return null;
527
- const turnKey = input.turnId ?? NO_TURN_BUCKET;
528
- const callsThisTurn = bumpBoundedCounter(callsByTurn, turnKey);
529
- if (callsThisTurn >= turnBudget.hard) {
530
- if (callsThisTurn === turnBudget.hard) emitBudgetEvent(input, callsThisTurn, true, now);
531
- return [
532
- {
533
- kind: "block_tool",
534
- 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.`,
535
- severity: "hard-block"
536
- }
537
- ];
538
- }
539
- if (callsThisTurn >= turnBudget.soft) {
540
- if (callsThisTurn === turnBudget.soft) emitBudgetEvent(input, callsThisTurn, false, now);
541
- return [
542
- {
543
- kind: "block_tool",
544
- 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.`,
545
- severity: "hard-block"
546
- }
547
- ];
548
- }
549
- return null;
550
- };
551
- const blockAsLoop = (input, turnKey, tool, repeatCount, baseReason, now) => {
552
- const blocksThisTurn = bumpTurnBlocks(turnKey);
553
- const reachedBudget = blocksThisTurn >= budget;
554
- if (synthesisLockout && reachedBudget) {
555
- enterLockout(input, turnKey, { tool, repeatCount, blocksThisTurn });
556
- options.onSynthesisLockout?.();
557
- emitLoopBlocked(input, { tool, repeatCount, blocksThisTurn, disposition: "lockout" }, now);
558
- return [{ kind: "block_tool", reason: synthesisLockoutDirective(), severity: "hard-block" }];
599
+ }
600
+ return {
601
+ call: {
602
+ action: "ask",
603
+ exposure: exposure.exposure ?? DEFAULT_AUTONOMY_EXPOSURE,
604
+ ...mode.mode ? { mode: mode.mode } : {},
605
+ questions: questions.questions,
606
+ ...summary ? { summary } : {},
607
+ ...maxRounds.maxRounds ? { max_rounds: maxRounds.maxRounds } : {}
559
608
  }
560
- emitLoopBlocked(input, { tool, repeatCount, blocksThisTurn, disposition: reachedBudget ? "stop" : "block" }, now);
561
- const reason = reachedBudget ? `${baseReason} Loop budget exhausted (${blocksThisTurn} blocks this turn); the agent is being stopped.` : baseReason;
562
- return [{ kind: "block_tool", reason, severity: "hard-block" }];
563
609
  };
564
- return {
565
- id: LOOP_GUARD_REGISTRATION_ID,
566
- description: "blocks repeated tool calls, enforces tool-call caps, and annotates substantial identical read results across distinct arguments",
567
- hooks: ["before_tool", "after_tool"],
568
- callCount: () => count,
569
- evaluate(input) {
570
- if (input.hook === "after_tool") {
571
- recordSuccessfulResult(input);
572
- recordResultForStagnation(input);
573
- const effects = [...crossArgumentResultEffects(input)];
574
- if (softReadReserveEntered && softLockout === null && deliveryTools.length === 0 && input.toolName === ToolNames.Read) {
575
- effects.push({ kind: "require_tool", toolName: ToolNames.Read });
576
- }
577
- if (reserveThreshold !== null && cap !== void 0 && !reserveDirectiveEmitted && count > reserveThreshold && count <= cap) {
578
- reserveDirectiveEmitted = true;
579
- effects.push({
580
- kind: "annotate_tool_result",
581
- message: workerSynthesisReserveDirective(Math.max(0, cap - count), cap),
582
- severity: "warn"
583
- });
584
- }
585
- return effects;
586
- }
587
- const now = options.now?.() ?? Date.now();
588
- const turnKey = input.turnId ?? NO_TURN_BUCKET;
589
- const decide = () => {
590
- if (capLockout !== null && cap !== void 0) {
591
- if (reachesLockoutBackstop(capLockout, input)) {
592
- return [
593
- { kind: "block_tool", reason: synthesisBackstopReason(input.toolName ?? "unknown"), severity: "hard-block" }
594
- ];
595
- }
596
- return [{ kind: "block_tool", reason: workerToolCallCapSynthesisReason(cap), severity: "hard-block" }];
597
- }
598
- if (softLockout !== null && softLimit !== void 0) {
599
- if (reachesLockoutBackstop(softLockout, input)) {
600
- return [
601
- { kind: "block_tool", reason: synthesisBackstopReason(input.toolName ?? "unknown"), severity: "hard-block" }
602
- ];
603
- }
604
- return [{ kind: "block_tool", reason: workerExplorationSynthesisDirective(softLimit), severity: "hard-block" }];
605
- }
606
- const lockout = synthesisLockout ? lockoutByTurn.get(turnKey) : void 0;
607
- if (lockout !== void 0) {
608
- if (reachesLockoutBackstop(lockout, input)) {
609
- emitLoopBlocked(
610
- input,
611
- {
612
- tool: lockout.tool,
613
- repeatCount: lockout.repeatCount,
614
- blocksThisTurn: lockout.blocksThisTurn,
615
- disposition: "stop"
616
- },
617
- now
618
- );
619
- return [{ kind: "block_tool", reason: synthesisBackstopReason(lockout.tool), severity: "hard-block" }];
620
- }
621
- return [{ kind: "block_tool", reason: synthesisLockoutDirective(), severity: "hard-block" }];
622
- }
623
- if (softLimit !== void 0) {
624
- const spentSoftLimit = softAdmittedCount >= softLimit;
625
- if (spentSoftLimit && deliveryTools.length === 0) {
626
- softLockout = {
627
- denials: 0,
628
- ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
629
- };
630
- options.onSynthesisLockout?.();
631
- return [{ kind: "block_tool", reason: workerExplorationSynthesisDirective(softLimit), severity: "hard-block" }];
632
- }
633
- const inReserve = softReadReserveThreshold !== null && softAdmittedCount >= softReadReserveThreshold;
634
- if (spentSoftLimit || inReserve) {
635
- enterSoftReadReserve();
636
- if (!reserveAdmits(input.toolName)) {
637
- if (reachesLockoutBackstop(reserveDenials, input)) {
638
- softLockout = {
639
- denials: 0,
640
- ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
641
- };
642
- options.onSynthesisLockout?.();
643
- return [{ kind: "block_tool", reason: workerExplorationSynthesisDirective(softLimit), severity: "hard-block" }];
644
- }
645
- return [
646
- {
647
- kind: "block_tool",
648
- reason: spentSoftLimit ? workerDeliveryOnlyDirective(softLimit, deliveryTools) : workerLiveReadReserveDirective(softLimit - softAdmittedCount, deliveryTools),
649
- severity: "hard-block"
650
- }
651
- ];
652
- }
653
- }
654
- }
655
- if (reserveThreshold !== null && cap !== void 0 && count >= reserveThreshold && !reserveAdmits(input.toolName)) {
656
- if (reachesLockoutBackstop(reserveDenials, input)) {
657
- if (synthesisLockout) {
658
- capLockout = {
659
- denials: 0,
660
- ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
661
- };
662
- options.onSynthesisLockout?.();
663
- }
664
- return [{ kind: "block_tool", reason: workerToolCallCapSynthesisReason(cap), severity: "hard-block" }];
665
- }
666
- return [
667
- {
668
- kind: "block_tool",
669
- reason: workerSynthesisReserveBlockReason(input.toolName ?? "unknown", Math.max(1, cap - count), cap),
670
- severity: "hard-block"
671
- }
672
- ];
673
- }
674
- count += 1;
675
- reserveDenials = { denials: 0 };
676
- if (softLimit !== void 0) {
677
- softAdmittedCount += 1;
678
- if (softReadReserveThreshold !== null && softAdmittedCount >= softReadReserveThreshold) enterSoftReadReserve();
679
- if (softAdmittedCount >= softLimit) {
680
- if (deliveryTools.length === 0) {
681
- softLockout = {
682
- denials: 0,
683
- ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
684
- };
685
- options.onSynthesisLockout?.();
686
- }
687
- options.onSoftLimitFinalCallAdmitted?.(input.toolCallId);
688
- }
689
- }
690
- if (cap !== void 0 && count > cap) {
691
- if (!synthesisLockout) {
692
- return [{ kind: "block_tool", reason: workerToolCallCapExceededReason(cap), severity: "hard-block" }];
693
- }
694
- capLockout = {
695
- denials: 0,
696
- ...input.correlationId !== void 0 ? { correlationId: input.correlationId } : {}
697
- };
698
- options.onSynthesisLockout?.();
699
- return [{ kind: "block_tool", reason: workerToolCallCapSynthesisReason(cap), severity: "hard-block" }];
700
- }
701
- const fingerprint = input.metadata?.callFingerprint;
702
- if (typeof fingerprint !== "string" || fingerprint.length === 0) {
703
- return evaluateTurnBudget(input, now) ?? [];
704
- }
705
- const verdict = options.safety.observeLoop(`${turnKey}|${fingerprint}`, now);
706
- if (verdict.looping) {
707
- const tool = input.toolName ?? "unknown";
708
- const priorSuccesses = succeededFingerprints.get(fingerprint) ?? 0;
709
- return blockAsLoop(
710
- input,
711
- turnKey,
712
- tool,
713
- verdict.count,
714
- loopBlockBaseReason(tool, verdict.count, priorSuccesses),
715
- now
716
- );
717
- }
718
- const toolName = input.toolName;
719
- if (typeof toolName === "string" && toolName.length > 0) {
720
- const entry = stagnationByTurn.get(turnKey);
721
- if (entry !== void 0 && entry.streak >= RESULT_STAGNATION_THRESHOLD - 1 && entry.reducedFingerprint === stagnationFingerprint(toolName, input.toolArgs)) {
722
- return blockAsLoop(
723
- input,
724
- turnKey,
725
- toolName,
726
- entry.streak + 1,
727
- stagnationBlockReason(toolName, entry.streak),
728
- now
729
- );
730
- }
731
- }
732
- return evaluateTurnBudget(input, now) ?? [];
733
- };
734
- return boundRunDenials(decide(), input);
735
- }
610
+ }
611
+ function cancelledAskUserResult() {
612
+ return { answers: [], cancelled: true };
613
+ }
614
+ function normalizeAskUserResult(questions, result) {
615
+ if (result.cancelled === true) return cancelledAskUserResult();
616
+ const answers = [];
617
+ for (let index = 0; index < questions.length; index += 1) {
618
+ const fallbackQuestion = questions[index]?.question ?? "";
619
+ const raw = result.answers[index];
620
+ if (!raw) continue;
621
+ const question = typeof raw.question === "string" && raw.question.trim().length > 0 ? raw.question : fallbackQuestion;
622
+ const answer = typeof raw.answer === "string" ? raw.answer.trim() : "";
623
+ if (question.length > 0 && answer.length > 0) answers.push({ question, answer });
624
+ }
625
+ return { answers };
626
+ }
627
+ function createStandalonePolicy(options) {
628
+ const now = (/* @__PURE__ */ new Date()).toISOString();
629
+ return {
630
+ id: randomUUID(),
631
+ status: "idle",
632
+ startedAt: now,
633
+ updatedAt: now,
634
+ ...options?.sessionId ? { sessionId: options.sessionId } : {},
635
+ ...options?.turnId ? { turnId: options.turnId } : {},
636
+ exposure: DEFAULT_AUTONOMY_EXPOSURE,
637
+ rounds: [],
638
+ decisions: [],
639
+ inFlight: false,
640
+ cancelled: false,
641
+ answerCount: 0,
642
+ callCount: 0,
643
+ maxCalls: DEFAULT_MAX_ROUNDS,
644
+ askedQuestionKeys: /* @__PURE__ */ new Set()
736
645
  };
737
646
  }
738
- function isReserveAdmittedTool(tool, deliveryTools) {
739
- return tool === ToolNames.Read || tool !== void 0 && deliveryTools.includes(tool);
647
+ function hydratePolicy(policy, options) {
648
+ if (options?.sessionId && !policy.sessionId) policy.sessionId = options.sessionId;
649
+ if (options?.turnId && !policy.turnId) policy.turnId = options.turnId;
740
650
  }
741
- function resolveDeliveryTools(allowedTools, product) {
742
- const candidates = product === "orientation" ? [ToolNames.Write, ToolNames.Edit, ToolNames.CodeNav] : [ToolNames.Write, ToolNames.Edit];
743
- if (allowedTools instanceof Set) {
744
- return candidates.filter((tool) => allowedTools.has(tool));
651
+ function mergePolicyExposure(policy, exposure) {
652
+ const merged = policy.exposure === "outward" || exposure === "outward" ? "outward" : "local";
653
+ policy.exposure = merged;
654
+ return merged;
655
+ }
656
+ function transcriptQuestions(questions) {
657
+ return questions.map((question) => ({
658
+ question: question.question,
659
+ ...question.header ? { header: question.header } : {},
660
+ ...question.options ? {
661
+ options: question.options.map((option) => ({
662
+ label: option.label,
663
+ ...option.description ? { description: option.description } : {}
664
+ }))
665
+ } : {},
666
+ ...question.multi_select === true ? { multi_select: true } : {}
667
+ }));
668
+ }
669
+ function transcriptAnswers(answers) {
670
+ return answers.map((answer) => ({ question: answer.question, answer: answer.answer }));
671
+ }
672
+ function toDecisionKey(value) {
673
+ const key = value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 56);
674
+ return key.length > 0 ? key : "decision";
675
+ }
676
+ function uniqueDecisionKey(base, existing) {
677
+ const key = toDecisionKey(base);
678
+ if (!existing.has(key)) return key;
679
+ for (let index = 2; index < 100; index += 1) {
680
+ const candidate = `${key}_${index}`;
681
+ if (!existing.has(candidate)) return candidate;
745
682
  }
746
- return candidates.filter((tool) => allowedTools.includes(tool));
747
- }
748
-
749
- // src/tools/ask-user.ts
750
- init_esm_shims();
751
- import { randomUUID } from "node:crypto";
752
- import { mkdir, writeFile } from "node:fs/promises";
753
- import path from "node:path";
754
- var ASK_USER_OTHER_LABEL = "Other (type your answer)";
755
- var MAX_DECISIONS = 24;
756
- var DEFAULT_MAX_ROUNDS = 6;
757
- var MAX_ROUNDS = 24;
758
- var askUserParameters = typebox_exports.Object({
759
- action: typebox_exports.Optional(
760
- StringEnum(["ask", "complete"], {
761
- description: "ask presents a round of questions; complete (exactly once) records decisions before final prose."
762
- })
763
- ),
764
- mode: typebox_exports.Optional(
765
- StringEnum(["round", "single_question"], {
766
- 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."
767
- })
768
- ),
769
- questions: typebox_exports.Optional(
770
- typebox_exports.Array(
771
- typebox_exports.Object({
772
- question: typebox_exports.String({ description: "Question for the operator." }),
773
- header: typebox_exports.Optional(typebox_exports.String({ description: "Short header." })),
774
- options: typebox_exports.Optional(
775
- typebox_exports.Array(
776
- typebox_exports.Object({
777
- label: typebox_exports.String({ description: "Choice label." }),
778
- description: typebox_exports.Optional(typebox_exports.String({ description: "Short explanation." }))
779
- }),
780
- { description: "Suggested choices, recommended first. The UI adds an implicit Other choice." }
781
- )
782
- ),
783
- multi_select: typebox_exports.Optional(typebox_exports.Boolean({ description: "Allow multiple selections." }))
784
- }),
785
- { description: "For action=ask: 1 to 4 questions, or exactly 1 when mode=single_question." }
786
- )
787
- ),
788
- max_rounds: typebox_exports.Optional(typebox_exports.Integer({ description: "Round limit for this turn (default 6, max 24)." })),
789
- decisions: typebox_exports.Optional(
790
- typebox_exports.Array(
791
- typebox_exports.Object({
792
- key: typebox_exports.String({ description: "Stable snake_case key." }),
793
- value: typebox_exports.String({ description: "Decision value." }),
794
- label: typebox_exports.Optional(typebox_exports.String({ description: "Human-readable label." }))
795
- }),
796
- { description: "For action=complete: compact decisions." }
797
- )
798
- ),
799
- summary: typebox_exports.Optional(typebox_exports.String({ description: "Short closeout summary for action=complete." })),
800
- exposure: typebox_exports.Optional(
801
- StringEnum(["local", "outward"], {
802
- 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."
803
- })
804
- )
805
- });
806
- function isRecord(value) {
807
- return typeof value === "object" && value !== null && !Array.isArray(value);
808
- }
809
- function trimOptionalString(value) {
810
- if (typeof value !== "string") return void 0;
811
- const trimmed = value.trim();
812
- return trimmed.length > 0 ? trimmed : void 0;
813
- }
814
- function normalizeAskUserMode(value) {
815
- if (value === void 0) return {};
816
- const raw = trimOptionalString(value)?.toLowerCase().replace(/[-\s]+/g, "_");
817
- if (!raw) return { error: "mode must be round or single_question" };
818
- if (raw === "round" || raw === "batch") return { mode: "round" };
819
- if (raw === "single" || raw === "single_question" || raw === "one_question") {
820
- return { mode: "single_question" };
821
- }
822
- return { error: "mode must be round or single_question" };
823
- }
824
- function normalizeExposure(value) {
825
- if (value === void 0) return { exposure: DEFAULT_AUTONOMY_EXPOSURE };
826
- const raw = trimOptionalString(value)?.toLowerCase();
827
- if (raw === "local" || raw === "outward") return { exposure: raw };
828
- return { error: "exposure must be local or outward" };
829
- }
830
- function askUserExposure(args) {
831
- if (args?.exposure === void 0) return DEFAULT_AUTONOMY_EXPOSURE;
832
- return normalizeExposure(args.exposure).exposure === "local" ? "local" : "outward";
833
- }
834
- function normalizeMaxRounds(value) {
835
- if (value === void 0) return {};
836
- if (typeof value !== "number" || !Number.isInteger(value)) {
837
- return { error: `max_rounds must be an integer from 1 to ${MAX_ROUNDS}` };
838
- }
839
- if (value < 1 || value > MAX_ROUNDS) {
840
- return { error: `max_rounds must be an integer from 1 to ${MAX_ROUNDS}` };
841
- }
842
- return { maxRounds: value };
843
- }
844
- function normalizeOptions(value, index) {
845
- if (value === void 0) return {};
846
- if (!Array.isArray(value)) return { error: `questions[${index}].options must be an array` };
847
- const options = [];
848
- for (let optionIndex = 0; optionIndex < value.length; optionIndex += 1) {
849
- const raw = value[optionIndex];
850
- if (!isRecord(raw)) return { error: `questions[${index}].options[${optionIndex}] must be an object` };
851
- const label = trimOptionalString(raw.label);
852
- if (!label) return { error: `questions[${index}].options[${optionIndex}].label is required` };
853
- const option = { label };
854
- const description = trimOptionalString(raw.description);
855
- if (description) option.description = description;
856
- options.push(option);
857
- }
858
- return options.length > 0 ? { options } : {};
859
- }
860
- function normalizeAskUserQuestions(args) {
861
- const rawQuestions = args.questions;
862
- if (!Array.isArray(rawQuestions)) return { error: "questions must be an array" };
863
- if (rawQuestions.length < 1) return { error: "questions must contain at least 1 item" };
864
- if (rawQuestions.length > 4) return { error: "questions must contain at most 4 items" };
865
- const questions = [];
866
- for (let index = 0; index < rawQuestions.length; index += 1) {
867
- const raw = rawQuestions[index];
868
- if (!isRecord(raw)) return { error: `questions[${index}] must be an object` };
869
- const questionText = trimOptionalString(raw.question);
870
- if (!questionText) return { error: `questions[${index}].question is required` };
871
- const normalized = { question: questionText };
872
- const header = trimOptionalString(raw.header);
873
- if (header) normalized.header = header;
874
- const options = normalizeOptions(raw.options, index);
875
- if (options.error) return { error: options.error };
876
- if (options.options) normalized.options = options.options;
877
- if (raw.multi_select === true) normalized.multi_select = true;
878
- questions.push(normalized);
879
- }
880
- return { questions };
881
- }
882
- function normalizeDecision(raw, index) {
883
- if (!isRecord(raw)) return { error: `decisions[${index}] must be an object` };
884
- const key = trimOptionalString(raw.key);
885
- if (!key) return { error: `decisions[${index}].key is required` };
886
- const value = trimOptionalString(raw.value);
887
- if (!value) return { error: `decisions[${index}].value is required` };
888
- const decision = { key: toDecisionKey(key), value };
889
- const label = trimOptionalString(raw.label);
890
- if (label) decision.label = label;
891
- return { decision };
892
- }
893
- function normalizeDecisions(value) {
894
- if (value === void 0) return {};
895
- if (!Array.isArray(value)) return { error: "decisions must be an array" };
896
- if (value.length > MAX_DECISIONS) return { error: `decisions must contain at most ${MAX_DECISIONS} items` };
897
- const decisions = [];
898
- for (let index = 0; index < value.length; index += 1) {
899
- const normalized = normalizeDecision(value[index], index);
900
- if (normalized.error) return { error: normalized.error };
901
- if (normalized.decision) decisions.push(normalized.decision);
902
- }
903
- return { decisions };
904
- }
905
- function normalizeAskUserCall(args) {
906
- const rawAction = trimOptionalString(args.action)?.toLowerCase() ?? "ask";
907
- if (rawAction !== "ask" && rawAction !== "complete") {
908
- return { error: "action must be ask or complete" };
909
- }
910
- const mode = normalizeAskUserMode(args.mode);
911
- if (mode.error) return { error: mode.error };
912
- const exposure = normalizeExposure(args.exposure);
913
- if (exposure.error) return { error: exposure.error };
914
- const maxRounds = normalizeMaxRounds(args.max_rounds);
915
- if (maxRounds.error) return { error: maxRounds.error };
916
- const decisions = normalizeDecisions(args.decisions);
917
- if (decisions.error) return { error: decisions.error };
918
- const summary = trimOptionalString(args.summary);
919
- if (rawAction === "complete") {
920
- if (Array.isArray(args.questions) && args.questions.length > 0) {
921
- return { error: "action=complete must not include questions" };
922
- }
923
- return {
924
- call: {
925
- action: "complete",
926
- ...decisions.decisions ? { decisions: decisions.decisions } : {},
927
- ...summary ? { summary } : {}
928
- }
929
- };
930
- }
931
- const questions = normalizeAskUserQuestions(args);
932
- if (!questions.questions) return { error: questions.error ?? "invalid questions" };
933
- if (mode.mode === "single_question" && questions.questions.length !== 1) {
934
- return {
935
- error: `mode=single_question carries exactly 1 question and this call carried ${questions.questions.length}; send them one call at a time, or use mode=round to ask up to 4 together`
936
- };
937
- }
938
- return {
939
- call: {
940
- action: "ask",
941
- ...mode.mode ? { mode: mode.mode } : {},
942
- questions: questions.questions,
943
- ...summary ? { summary } : {},
944
- ...maxRounds.maxRounds ? { max_rounds: maxRounds.maxRounds } : {}
945
- }
946
- };
947
- }
948
- function cancelledAskUserResult() {
949
- return { answers: [], cancelled: true };
950
- }
951
- function normalizeAskUserResult(questions, result) {
952
- if (result.cancelled === true) return cancelledAskUserResult();
953
- const answers = [];
954
- for (let index = 0; index < questions.length; index += 1) {
955
- const fallbackQuestion = questions[index]?.question ?? "";
956
- const raw = result.answers[index];
957
- if (!raw) continue;
958
- const question = typeof raw.question === "string" && raw.question.trim().length > 0 ? raw.question : fallbackQuestion;
959
- const answer = typeof raw.answer === "string" ? raw.answer.trim() : "";
960
- if (question.length > 0 && answer.length > 0) answers.push({ question, answer });
961
- }
962
- return { answers };
963
- }
964
- function createStandalonePolicy(options) {
965
- const now = (/* @__PURE__ */ new Date()).toISOString();
966
- return {
967
- id: randomUUID(),
968
- status: "idle",
969
- startedAt: now,
970
- updatedAt: now,
971
- ...options?.sessionId ? { sessionId: options.sessionId } : {},
972
- ...options?.turnId ? { turnId: options.turnId } : {},
973
- rounds: [],
974
- decisions: [],
975
- inFlight: false,
976
- cancelled: false,
977
- answerCount: 0,
978
- callCount: 0,
979
- maxCalls: DEFAULT_MAX_ROUNDS,
980
- askedQuestionKeys: /* @__PURE__ */ new Set()
981
- };
982
- }
983
- function hydratePolicy(policy, options) {
984
- if (options?.sessionId && !policy.sessionId) policy.sessionId = options.sessionId;
985
- if (options?.turnId && !policy.turnId) policy.turnId = options.turnId;
986
- }
987
- function transcriptQuestions(questions) {
988
- return questions.map((question) => ({
989
- question: question.question,
990
- ...question.header ? { header: question.header } : {},
991
- ...question.options ? {
992
- options: question.options.map((option) => ({
993
- label: option.label,
994
- ...option.description ? { description: option.description } : {}
995
- }))
996
- } : {},
997
- ...question.multi_select === true ? { multi_select: true } : {}
998
- }));
999
- }
1000
- function transcriptAnswers(answers) {
1001
- return answers.map((answer) => ({ question: answer.question, answer: answer.answer }));
1002
- }
1003
- function toDecisionKey(value) {
1004
- const key = value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 56);
1005
- return key.length > 0 ? key : "decision";
1006
- }
1007
- function uniqueDecisionKey(base, existing) {
1008
- const key = toDecisionKey(base);
1009
- if (!existing.has(key)) return key;
1010
- for (let index = 2; index < 100; index += 1) {
1011
- const candidate = `${key}_${index}`;
1012
- if (!existing.has(candidate)) return candidate;
1013
- }
1014
- return `${key}_${Date.now()}`;
683
+ return `${key}_${Date.now()}`;
1015
684
  }
1016
685
  function decisionFromAnswer(answer, questions, existing) {
1017
686
  const question = questions.find((candidate) => candidate.question === answer.question);
@@ -1076,6 +745,7 @@ async function persistAskUserTranscript(policy, options) {
1076
745
  ...policy.endedAt ? { endedAt: policy.endedAt } : {},
1077
746
  ...policy.sessionId ? { sessionId: policy.sessionId } : {},
1078
747
  ...policy.turnId ? { turnId: policy.turnId } : {},
748
+ exposure: policy.exposure ?? DEFAULT_AUTONOMY_EXPOSURE,
1079
749
  ...policy.summary ? { summary: policy.summary } : {},
1080
750
  decisions: policy.decisions,
1081
751
  rounds: policy.rounds
@@ -1095,6 +765,7 @@ function compactInterview(policy, event, latestAnswers = []) {
1095
765
  rounds: policy.rounds.length,
1096
766
  max_rounds: policy.maxCalls,
1097
767
  transcript_path: policy.transcriptPath ?? null,
768
+ exposure: policy.exposure ?? DEFAULT_AUTONOMY_EXPOSURE,
1098
769
  latest_answers: latestAnswers,
1099
770
  decisions: policy.decisions,
1100
771
  ...policy.summary ? { summary: policy.summary } : {},
@@ -1119,125 +790,653 @@ function okInterviewResult(policy, event, latestAnswers = []) {
1119
790
  }
1120
791
  };
1121
792
  }
1122
- async function completeInterview(policy, event, options, summary, decisions = []) {
1123
- const now = (/* @__PURE__ */ new Date()).toISOString();
1124
- if (decisions.length > 0) upsertDecisions(policy, decisions);
1125
- if (summary) policy.summary = summary;
1126
- if (policy.status !== "cancelled") {
1127
- policy.status = "complete";
1128
- policy.cancelled = false;
1129
- }
1130
- policy.endedAt = policy.endedAt ?? now;
1131
- policy.updatedAt = now;
1132
- await persistAskUserTranscript(policy, options);
1133
- return okInterviewResult(policy, event);
793
+ async function completeInterview(policy, event, options, summary, decisions = []) {
794
+ const now = (/* @__PURE__ */ new Date()).toISOString();
795
+ if (decisions.length > 0) upsertDecisions(policy, decisions);
796
+ if (summary) policy.summary = summary;
797
+ if (policy.status !== "cancelled") {
798
+ policy.status = "complete";
799
+ policy.cancelled = false;
800
+ }
801
+ policy.endedAt = policy.endedAt ?? now;
802
+ policy.updatedAt = now;
803
+ await persistAskUserTranscript(policy, options);
804
+ return okInterviewResult(policy, event);
805
+ }
806
+ async function finalizeAskUserInterview(policy, reason, options) {
807
+ if (policy.status === "idle" && policy.rounds.length === 0) return;
808
+ if (policy.status !== "complete" && policy.status !== "cancelled") {
809
+ const now = (/* @__PURE__ */ new Date()).toISOString();
810
+ policy.status = "complete";
811
+ policy.updatedAt = now;
812
+ policy.endedAt = now;
813
+ policy.summary = policy.summary ?? `Interview closed by host: ${reason}.`;
814
+ }
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;
1023
+ }
1024
+ function isLoopGuardSynthesisBackstopReason(reason) {
1025
+ return /^loop guard: tool calls stayed disabled and .+ was called again instead of answering/.test(reason);
1026
+ }
1027
+ function readOrchTurnToolCallBudget(env = process.env) {
1028
+ const soft = resolveGuardrail("turnToolCallBudget", env);
1029
+ return { soft, hard: soft + ORCH_TURN_TOOL_CALL_HARD_MARGIN };
1030
+ }
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);
1075
+ }
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);
1113
+ }
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
+ ];
1140
+ }
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);
1148
+ }
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);
1165
+ }
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;
1253
+ }
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 });
1269
+ }
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;
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);
1428
+ }
1429
+ };
1430
+ }
1431
+ function isReserveAdmittedTool(tool, deliveryTools) {
1432
+ return tool === ToolNames.Read || tool !== void 0 && deliveryTools.includes(tool);
1134
1433
  }
1135
- async function finalizeAskUserInterview(policy, reason, options) {
1136
- if (policy.status === "idle" && policy.rounds.length === 0) return;
1137
- if (policy.status !== "complete" && policy.status !== "cancelled") {
1138
- const now = (/* @__PURE__ */ new Date()).toISOString();
1139
- policy.status = "complete";
1140
- policy.updatedAt = now;
1141
- policy.endedAt = now;
1142
- policy.summary = policy.summary ?? `Interview closed by host: ${reason}.`;
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));
1143
1438
  }
1144
- await persistAskUserTranscript(policy, options);
1145
- }
1146
- async function finalizeAskUserInterviewForHost(policy, reason, options, onFinalized) {
1147
- await finalizeAskUserInterview(policy, reason, options);
1148
- onFinalized?.(policy);
1149
- }
1150
- async function defaultAskUserHandler() {
1151
- return cancelledAskUserResult();
1152
- }
1153
- function createAskUserTool(deps = {}) {
1154
- return {
1155
- name: ToolNames.AskUser,
1156
- 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.",
1157
- parameters: askUserParameters,
1158
- baseActionClass: "read",
1159
- executionMode: "sequential",
1160
- async run(args, options) {
1161
- const normalized = normalizeAskUserCall(args);
1162
- if (!normalized.call) return { kind: "error", message: `ask_user: ${normalized.error ?? "invalid input"}` };
1163
- const policy = options?.askUserPolicy ?? createStandalonePolicy(options);
1164
- hydratePolicy(policy, options);
1165
- const call = normalized.call;
1166
- if (call.action === "complete") {
1167
- return completeInterview(policy, "complete", options, call.summary, call.decisions ?? []);
1168
- }
1169
- const questions = call.questions ?? [];
1170
- if (call.max_rounds !== void 0) {
1171
- const nextLimit = policy.callCount === 0 ? call.max_rounds : Math.max(policy.maxCalls, call.max_rounds);
1172
- policy.maxCalls = Math.max(nextLimit, policy.callCount + 1);
1173
- }
1174
- if (policy.inFlight === true) {
1175
- return {
1176
- kind: "error",
1177
- message: "ask_user: an operator interview round is already in progress. Wait for that answer."
1178
- };
1179
- }
1180
- if (policy.status === "complete") {
1181
- await persistAskUserTranscript(policy, options);
1182
- return okInterviewResult(policy, "already_complete");
1183
- }
1184
- if (policy.status === "cancelled" || policy.cancelled === true) {
1185
- policy.status = "cancelled";
1186
- policy.cancelled = true;
1187
- await persistAskUserTranscript(policy, options);
1188
- return okInterviewResult(policy, "already_cancelled");
1189
- }
1190
- const key = questionKey(questions);
1191
- if (policy.askedQuestionKeys.has(key)) {
1192
- await persistAskUserTranscript(policy, options);
1193
- return okInterviewResult(policy, "duplicate_round_ignored");
1194
- }
1195
- if (policy.callCount >= policy.maxCalls) {
1196
- return completeInterview(
1197
- policy,
1198
- "round_limit_reached",
1199
- options,
1200
- `Interview closed after reaching the ${policy.maxCalls}-round limit.`
1201
- );
1202
- }
1203
- const requestedAt = (/* @__PURE__ */ new Date()).toISOString();
1204
- policy.inFlight = true;
1205
- policy.status = "active";
1206
- policy.updatedAt = requestedAt;
1207
- if (call.summary) policy.summary = call.summary;
1208
- const handler = deps.askUser ?? defaultAskUserHandler;
1209
- try {
1210
- const result = normalizeAskUserResult(questions, await handler(questions, options));
1211
- const answeredAt = (/* @__PURE__ */ new Date()).toISOString();
1212
- policy.callCount += 1;
1213
- policy.askedQuestionKeys.add(key);
1214
- policy.answerCount += result.answers.length;
1215
- policy.updatedAt = answeredAt;
1216
- const round = {
1217
- round: policy.rounds.length + 1,
1218
- requestedAt,
1219
- answeredAt,
1220
- questions: transcriptQuestions(questions),
1221
- answers: transcriptAnswers(result.answers),
1222
- ...result.cancelled === true ? { cancelled: true } : {}
1223
- };
1224
- policy.rounds.push(round);
1225
- if (result.cancelled === true) {
1226
- policy.status = "cancelled";
1227
- policy.cancelled = true;
1228
- policy.endedAt = answeredAt;
1229
- await persistAskUserTranscript(policy, options);
1230
- return okInterviewResult(policy, "cancelled", []);
1231
- }
1232
- const derived = deriveAnswerDecisions(questions, result.answers, policy);
1233
- upsertDecisions(policy, derived);
1234
- await persistAskUserTranscript(policy, options);
1235
- return okInterviewResult(policy, "round_answered", result.answers);
1236
- } finally {
1237
- policy.inFlight = false;
1238
- }
1239
- }
1240
- };
1439
+ return candidates.filter((tool) => allowedTools.includes(tool));
1241
1440
  }
1242
1441
 
1243
1442
  // src/tools/registry.ts
@@ -1269,6 +1468,7 @@ function createRegistry(deps) {
1269
1468
  let approvalRequestCounter = 0;
1270
1469
  const approvalRequestToken = randomBytes(4).toString("hex");
1271
1470
  const runSpec = async (spec, call, decision, options) => {
1471
+ let resultDisposition = spec.metadata?.resultDisposition;
1272
1472
  try {
1273
1473
  const beforeEffects = runToolHook("before_tool", spec, call, decision, options);
1274
1474
  const block = firstBlockToolEffect(beforeEffects);
@@ -1282,17 +1482,20 @@ function createRegistry(deps) {
1282
1482
  }
1283
1483
  try {
1284
1484
  const preparedArgs = prepareToolArgs(spec, call.args ?? {});
1285
- const result = shapeToolResult(spec, await spec.run(preparedArgs, options), options);
1286
- const afterEffects = runToolHook("after_tool", spec, call, decision, options, result);
1287
- const finalResult = shapeToolResult(spec, applyToolResultEffects(result, afterEffects), options);
1485
+ resultDisposition = resolveToolResultDisposition(spec, preparedArgs);
1486
+ const result = await spec.run(preparedArgs, options);
1487
+ const digest = toolResultDigestFor(spec, result, resultDisposition);
1488
+ const afterEffects = runToolHook("after_tool", spec, call, decision, options, result, digest);
1489
+ const finalResult = shapeToolResult(spec, applyToolResultEffects(result, afterEffects), options, resultDisposition);
1288
1490
  return { kind: "ok", result: finalResult, decision };
1289
1491
  } catch (err) {
1290
1492
  const message = err instanceof Error ? err.message : String(err);
1291
- const result = shapeToolResult(spec, { kind: "error", message }, options);
1292
- const afterEffects = runToolHook("after_tool", spec, call, decision, options, result);
1493
+ const result = { kind: "error", message };
1494
+ const digest = toolResultDigestFor(spec, result, resultDisposition);
1495
+ const afterEffects = runToolHook("after_tool", spec, call, decision, options, result, digest);
1293
1496
  return {
1294
1497
  kind: "ok",
1295
- result: shapeToolResult(spec, applyToolResultEffects(result, afterEffects), options),
1498
+ result: shapeToolResult(spec, applyToolResultEffects(result, afterEffects), options, resultDisposition),
1296
1499
  decision
1297
1500
  };
1298
1501
  }
@@ -1300,9 +1503,9 @@ function createRegistry(deps) {
1300
1503
  disposeAdmissionArgs(spec, call.args ?? {});
1301
1504
  }
1302
1505
  };
1303
- const runToolHook = (hook, spec, call, decision, options, result) => {
1506
+ const runToolHook = (hook, spec, call, decision, options, result, resultDigest) => {
1304
1507
  if (!deps.middleware) return [];
1305
- const input = buildToolHookInput(hook, spec, call, decision, "operating", options, result);
1508
+ const input = buildToolHookInput(hook, spec, call, decision, "operating", options, result, resultDigest);
1306
1509
  const effects = deps.middleware.runHook(input).effects;
1307
1510
  try {
1308
1511
  deps.onMiddlewareEffects?.(effects, input);
@@ -1609,6 +1812,23 @@ function prepareToolArgs(spec, args) {
1609
1812
  return args;
1610
1813
  }
1611
1814
  }
1815
+ function resolveToolResultDisposition(spec, args) {
1816
+ const declared = spec.metadata?.resultDisposition;
1817
+ if (!spec.resolveResultDisposition) return declared;
1818
+ try {
1819
+ return spec.resolveResultDisposition(args, declared);
1820
+ } catch (error) {
1821
+ const maxBytes = declared !== void 0 && declared.context.maxBytes !== void 0 ? declared.context.maxBytes : DEFAULT_TOOL_RESULT_MAX_BYTES;
1822
+ return {
1823
+ presentation: declared?.presentation ?? { foldDefault: "folded", showDiffWhenFolded: false, failureExcerpt: true },
1824
+ context: { mode: "metadata-only", maxBytes },
1825
+ // The narrowing is recorded with the result, so a resolver bug shows up
1826
+ // on the transcript row and in the model's header instead of reading as
1827
+ // a deliberate metadata-only request.
1828
+ fallback: { reason: "resolver-error", message: error instanceof Error ? error.message : String(error) }
1829
+ };
1830
+ }
1831
+ }
1612
1832
  function disposeAdmissionArgs(spec, args) {
1613
1833
  try {
1614
1834
  spec?.disposeAdmissionArguments?.(args);
@@ -1720,7 +1940,7 @@ ${plan.text}`,
1720
1940
  ...decision.policy !== void 0 ? { policy: decision.policy } : {}
1721
1941
  };
1722
1942
  }
1723
- function buildToolHookInput(hook, spec, call, decision, posture, options, result) {
1943
+ function buildToolHookInput(hook, spec, call, decision, posture, options, result, resultDigest) {
1724
1944
  const metadata = {
1725
1945
  posture,
1726
1946
  actionClass: decision.classification.actionClass,
@@ -1749,6 +1969,7 @@ function buildToolHookInput(hook, spec, call, decision, posture, options, result
1749
1969
  };
1750
1970
  if (call.args !== void 0) input.toolArgs = call.args;
1751
1971
  if (result?.details !== void 0) input.toolResultDetails = result.details;
1972
+ if (resultDigest !== void 0) input.toolResultDigest = resultDigest;
1752
1973
  if (options?.runId !== void 0) input.runId = options.runId;
1753
1974
  if (options?.sessionId !== void 0) input.sessionId = options.sessionId;
1754
1975
  if (options?.turnId !== void 0) input.turnId = options.turnId;
@@ -1780,12 +2001,14 @@ function applyToolResultEffects(result, effects) {
1780
2001
 
1781
2002
  ${annotations.join("\n")}`;
1782
2003
  if (result.kind === "ok") {
1783
- const annotated = { kind: "ok", output: `${result.output}${suffix}` };
1784
- if (result.details !== void 0) annotated.details = result.details;
1785
- if (result.terminate === true) annotated.terminate = true;
1786
- return annotated;
2004
+ const annotated2 = { kind: "ok", output: `${result.output}${suffix}` };
2005
+ if (result.details !== void 0) annotated2.details = result.details;
2006
+ if (result.terminate === true) annotated2.terminate = true;
2007
+ return annotated2;
1787
2008
  }
1788
- return { kind: "error", message: `${result.message}${suffix}` };
2009
+ const annotated = { kind: "error", message: `${result.message}${suffix}` };
2010
+ if (result.details !== void 0) annotated.details = result.details;
2011
+ return annotated;
1789
2012
  }
1790
2013
  function annotationMessages(effects) {
1791
2014
  const messages = [];
@@ -1800,8 +2023,20 @@ function annotationMessages(effects) {
1800
2023
  // src/engine/agent.ts
1801
2024
  init_esm_shims();
1802
2025
  import { Agent } from "@earendil-works/pi-agent-core";
2026
+ function dispositionAwareAfterToolCall(delegate) {
2027
+ return async (context, signal) => {
2028
+ const override = await delegate?.(context, signal);
2029
+ const effectiveResult = override?.details === void 0 ? context.result : { ...context.result, details: override.details };
2030
+ if (isDispositionedToolResultError(effectiveResult)) return { ...override, isError: true };
2031
+ return override;
2032
+ };
2033
+ }
1803
2034
  function createEngineAgent(options = {}) {
1804
- const agent = new Agent({ streamFn: engineStreamSimple, ...options });
2035
+ const agent = new Agent({
2036
+ streamFn: engineStreamSimple,
2037
+ ...options,
2038
+ afterToolCall: dispositionAwareAfterToolCall(options.afterToolCall)
2039
+ });
1805
2040
  return {
1806
2041
  agent,
1807
2042
  state: () => agent.state
@@ -1814,10 +2049,10 @@ import { performance as performance2 } from "node:perf_hooks";
1814
2049
  function projectToolResult(result) {
1815
2050
  const details = { ...result.details ?? {}, kind: result.kind };
1816
2051
  if (result.kind === "error") {
1817
- return { content: [{ type: "text", text: result.message }], details };
2052
+ return { content: [{ type: "text", text: toolResultContextText(result) }], details };
1818
2053
  }
1819
2054
  return {
1820
- content: [{ type: "text", text: result.output }],
2055
+ content: [{ type: "text", text: toolResultContextText(result) }],
1821
2056
  details,
1822
2057
  ...result.terminate === true ? { terminate: true } : {}
1823
2058
  };
@@ -1829,9 +2064,16 @@ async function runValidatedToolCall(input) {
1829
2064
  const { spec, args, registry, signal, telemetry } = input;
1830
2065
  const startedAt = Date.now();
1831
2066
  const startedAtClock = performance2.now();
1832
- telemetry?.onStart?.({ tool: spec.name, posture: "operating", startedAt });
1833
2067
  const callId = input.invokeOptions?.toolCallId;
1834
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
+ });
1835
2077
  const invokeOpts = {};
1836
2078
  if (input.invokeOptions) Object.assign(invokeOpts, input.invokeOptions);
1837
2079
  if (signal) invokeOpts.signal = signal;
@@ -1872,12 +2114,15 @@ async function runValidatedToolCall(input) {
1872
2114
  reason: verdict.result.message,
1873
2115
  decision: verdict.decision
1874
2116
  });
1875
- throw new Error(verdict.result.message);
2117
+ if (input.returnDispositionedErrors === true && isDispositionedToolResultError(verdict.result)) {
2118
+ return projectToolResult(verdict.result);
2119
+ }
2120
+ throw new Error(toolResultContextText(verdict.result));
1876
2121
  }
1877
2122
  const toolDetails = isRecord2(verdict.result.details) ? verdict.result.details : {};
1878
2123
  const skillActivation = spec.name === ToolNames.Context ? skillActivationFromToolDetails(toolDetails, input.invokeOptions?.turnId) : null;
1879
2124
  const result = {
1880
- content: [{ type: "text", text: verdict.result.output }],
2125
+ content: [{ type: "text", text: toolResultContextText(verdict.result) }],
1881
2126
  details: { ...toolDetails, kind: "ok" }
1882
2127
  };
1883
2128
  if (verdict.result.terminate === true) {
@@ -1941,7 +2186,8 @@ function toAgentTool(spec, registry, telemetry, invokeOptions) {
1941
2186
  const callInput = {
1942
2187
  spec,
1943
2188
  args: params,
1944
- registry
2189
+ registry,
2190
+ returnDispositionedErrors: true
1945
2191
  };
1946
2192
  if (signal) callInput.signal = signal;
1947
2193
  if (telemetry) callInput.telemetry = telemetry;
@@ -1985,40 +2231,6 @@ function resolveSessionTools(runtime, toolRegistry, invokeOptions, telemetry) {
1985
2231
  return resolveAgentTools(input);
1986
2232
  }
1987
2233
 
1988
- // src/domains/safety/call-target.ts
1989
- init_esm_shims();
1990
- var ESC_CHAR = String.fromCharCode(27);
1991
- var BEL_CHAR = String.fromCharCode(7);
1992
- var OSC_PATTERN = new RegExp(`${ESC_CHAR}\\][\\s\\S]*?(?:${BEL_CHAR}|${ESC_CHAR}\\\\|$)`, "g");
1993
- var CSI_PATTERN = new RegExp(`${ESC_CHAR}\\[[0-9;?]*[0-9A-Za-z]`, "g");
1994
- function sanitizeForDisplay(value) {
1995
- const stripped = value.replace(OSC_PATTERN, "").replace(CSI_PATTERN, "");
1996
- let out = "";
1997
- for (const ch of stripped) {
1998
- const code = ch.codePointAt(0) ?? 0;
1999
- out += code < 32 || code === 127 ? " " : ch;
2000
- }
2001
- return out;
2002
- }
2003
- function oneLine(value) {
2004
- return value.replace(/\s+/g, " ").trim();
2005
- }
2006
- function sanitizeCallTargetText(value) {
2007
- return oneLine(sanitizeForDisplay(value));
2008
- }
2009
- function describeCallTarget(args) {
2010
- if (!args) return "";
2011
- const str = (value) => typeof value === "string" && value.trim().length > 0 ? oneLine(sanitizeForDisplay(value)).trim() || null : null;
2012
- const candidate = str(args.command) ?? str(args.path) ?? str(args.file_path) ?? str(args.name) ?? str(args.pattern);
2013
- if (candidate) return candidate;
2014
- try {
2015
- const json = JSON.stringify(args);
2016
- return json === "{}" || json === void 0 ? "" : oneLine(sanitizeForDisplay(json)).slice(0, 120);
2017
- } catch {
2018
- return "";
2019
- }
2020
- }
2021
-
2022
2234
  // src/engine/provider-payload.ts
2023
2235
  init_esm_shims();
2024
2236
  function reasoningSummaryForLevel(level) {
@@ -2345,161 +2557,6 @@ function createArtifactTool(deps = {}) {
2345
2557
  };
2346
2558
  }
2347
2559
 
2348
- // src/tools/bash.ts
2349
- init_esm_shims();
2350
- var BASH_DISPLAY_MAX_BYTES = 16 * 1024;
2351
- var BASH_TAIL_NOTE_RESERVE = 512;
2352
- function shapeBashProgress(rawOutput) {
2353
- const truncation = truncateTail(rawOutput, {
2354
- maxLines: DEFAULT_MAX_LINES,
2355
- maxBytes: BASH_DISPLAY_MAX_BYTES - BASH_TAIL_NOTE_RESERVE
2356
- });
2357
- const bytes = Buffer.byteLength(rawOutput, "utf8");
2358
- return {
2359
- kind: "ok",
2360
- output: truncation.content,
2361
- details: {
2362
- resultSize: {
2363
- bytes,
2364
- shownBytes: truncation.outputBytes,
2365
- maxBytes: BASH_DISPLAY_MAX_BYTES,
2366
- truncated: truncation.truncated,
2367
- policy: "tail"
2368
- }
2369
- }
2370
- };
2371
- }
2372
- function shapeBashOutput(rawOutput, context) {
2373
- const truncation = truncateTail(rawOutput, {
2374
- maxLines: DEFAULT_MAX_LINES,
2375
- maxBytes: BASH_DISPLAY_MAX_BYTES - BASH_TAIL_NOTE_RESERVE
2376
- });
2377
- if (!truncation.truncated) return { text: rawOutput };
2378
- const totalBytes = Buffer.byteLength(rawOutput, "utf8");
2379
- const offloadPath = writeToolOffload(rawOutput, context, BASH_HARD_CAP_BYTES);
2380
- const startLine = truncation.totalLines - truncation.outputLines + 1;
2381
- const scope = truncation.lastLinePartial ? `last ${formatSize(truncation.outputBytes)} of line ${truncation.totalLines} (line is large)` : `lines ${startLine}-${truncation.totalLines} of ${truncation.totalLines}`;
2382
- const location = offloadPath !== null ? ` Full output saved to ${offloadPath}; read it with offset/limit.` : "";
2383
- const note = `[Output tail-truncated: showing ${scope} (${formatSize(BASH_DISPLAY_MAX_BYTES)} display limit).${location}]`;
2384
- const details = {
2385
- resultSize: {
2386
- bytes: totalBytes,
2387
- shownBytes: truncation.outputBytes,
2388
- maxBytes: BASH_DISPLAY_MAX_BYTES,
2389
- truncated: true,
2390
- policy: "tail",
2391
- followUpHint: "Read the full-output offload file with offset/limit, or re-run a narrower command.",
2392
- ...offloadPath !== null ? { offloadPath } : {}
2393
- }
2394
- };
2395
- return { text: `${truncation.content}
2396
-
2397
- ${note}`, details };
2398
- }
2399
- function withDetails(base, details) {
2400
- if (details === void 0) return base;
2401
- if (base.kind === "ok") return { ...base, details };
2402
- return { ...base, details };
2403
- }
2404
- var OBSERVE_COMMAND_PATTERN = /^\s*(?:cat|head|tail|ls|grep|rg|find)\b/;
2405
- var OBSERVE_NUDGE_SESSION_LIMIT = 64;
2406
- var observeNudgeSeenSessions = /* @__PURE__ */ new Set();
2407
- function observeToolsNudge(command, sessionId) {
2408
- if (!OBSERVE_COMMAND_PATTERN.test(command)) return "";
2409
- const key = sessionId ?? "no-session";
2410
- if (observeNudgeSeenSessions.has(key)) return "";
2411
- if (observeNudgeSeenSessions.size >= OBSERVE_NUDGE_SESSION_LIMIT) {
2412
- const oldest = observeNudgeSeenSessions.values().next().value;
2413
- if (oldest !== void 0) observeNudgeSeenSessions.delete(oldest);
2414
- }
2415
- observeNudgeSeenSessions.add(key);
2416
- return "\n\n[note: prefer the structured observe tools over shell file inspection: read pages files with offset/limit, ls lists directories, grep and find search, code_nav maps symbols. Their results are capped and continuable; keep bash for builds, git, and scripts.]";
2417
- }
2418
- var bashTool = {
2419
- name: ToolNames.Bash,
2420
- description: "Execute a bash command and return stdout and stderr. Runs inside the session workspace. Default timeout 300000 ms.",
2421
- parameters: typebox_exports.Object({
2422
- command: typebox_exports.String({ description: "Bash command to execute." }),
2423
- cwd: typebox_exports.Optional(
2424
- typebox_exports.String({
2425
- description: "Omit this. The command already runs at the session workspace root. Set it only to run in a subdirectory of that root, as a relative path; an absolute path outside the root is blocked."
2426
- })
2427
- ),
2428
- timeout_ms: typebox_exports.Optional(typebox_exports.Number({ description: "Timeout in milliseconds." }))
2429
- }),
2430
- baseActionClass: "execute",
2431
- executionMode: "sequential",
2432
- async run(args, options) {
2433
- if (typeof args.command !== "string" || args.command.length === 0) {
2434
- return { kind: "error", message: "bash: missing command argument" };
2435
- }
2436
- const cwdArg = typeof args.cwd === "string" && args.cwd.length > 0 ? args.cwd : void 0;
2437
- let cwd;
2438
- try {
2439
- cwd = resolveSafeCwd(cwdArg === void 0 ? void 0 : expandPath(cwdArg), process.cwd());
2440
- } catch {
2441
- return {
2442
- kind: "error",
2443
- message: `bash: cwd '${cwdArg}' escapes workspace root '${process.cwd()}'; use a typed tool or a project policy entry with explicit cwd`
2444
- };
2445
- }
2446
- const timeout = typeof args.timeout_ms === "number" && args.timeout_ms > 0 ? args.timeout_ms : 3e5;
2447
- try {
2448
- const runOptions = {
2449
- cwd,
2450
- timeoutMs: timeout,
2451
- ...options?.signal === void 0 ? {} : { signal: options.signal }
2452
- };
2453
- if (options?.onUpdate !== void 0) {
2454
- runOptions.onUpdate = (progress) => {
2455
- options.onUpdate?.(shapeBashProgress(combineBashOutput(progress)));
2456
- };
2457
- }
2458
- const result = await runBashCommand(args.command, runOptions);
2459
- const { error, aborted, timedOut, outputCapped } = result;
2460
- if (aborted) {
2461
- return { kind: "error", message: "bash: command aborted" };
2462
- }
2463
- const shaped = shapeBashOutput(combineBashOutput(result), options);
2464
- const output = shaped.text.trim();
2465
- if (timedOut) {
2466
- const status = `bash: command timed out after ${timeout}ms`;
2467
- return withDetails(
2468
- { kind: "error", message: output.length > 0 ? `${output}
2469
-
2470
- ${status}` : status },
2471
- shaped.details
2472
- );
2473
- }
2474
- if (outputCapped) {
2475
- const status = `bash: command output exceeded ${BASH_HARD_CAP_BYTES} bytes and was stopped`;
2476
- return withDetails(
2477
- { kind: "error", message: output.length > 0 ? `${output}
2478
-
2479
- ${status}` : status },
2480
- shaped.details
2481
- );
2482
- }
2483
- if (error) {
2484
- const code = typeof error.code === "number" ? error.code : error.code;
2485
- const status = `bash: command failed (exit ${code ?? "?"})`;
2486
- const message = output.length > 0 ? `${output}
2487
-
2488
- ${status}` : `${status}: ${error.message}`;
2489
- return withDetails({ kind: "error", message }, shaped.details);
2490
- }
2491
- const body = shaped.text.length > 0 ? shaped.text : "(no output)";
2492
- return withDetails(
2493
- { kind: "ok", output: `${body}${observeToolsNudge(args.command, options?.sessionId)}` },
2494
- shaped.details
2495
- );
2496
- } catch (err) {
2497
- const msg = err instanceof Error ? err.message : String(err);
2498
- return { kind: "error", message: `bash: ${msg}` };
2499
- }
2500
- }
2501
- };
2502
-
2503
2560
  // src/tools/credential-present.ts
2504
2561
  init_esm_shims();
2505
2562
  import { createReadStream } from "node:fs";
@@ -5019,13 +5076,13 @@ function registerCoreTools(registry, deps = {}) {
5019
5076
  });
5020
5077
  registry.register({
5021
5078
  ...builtin(
5022
- lazyTool(verifyToolSurface, async () => (await import("./verify-375KUB3Y.js")).verifyTool),
5079
+ lazyTool(verifyToolSurface, async () => (await import("./verify-X5HDROLA.js")).verifyTool),
5023
5080
  { path: "src/tools/verify/index.ts", scope: "core" }
5024
5081
  )
5025
5082
  });
5026
5083
  registry.register({
5027
5084
  ...builtin(
5028
- lazyTool(codeNavToolSurface, async () => (await import("./code-nav-7AX6FYE6.js")).codeNavTool),
5085
+ lazyTool(codeNavToolSurface, async () => (await import("./code-nav-GQNL7XA6.js")).codeNavTool),
5029
5086
  { path: "src/tools/codewiki/code-nav.ts", scope: "core" }
5030
5087
  )
5031
5088
  });
@@ -5046,14 +5103,27 @@ function registerCoreTools(registry, deps = {}) {
5046
5103
  ...builtin(credentialPresentTool, { path: "src/tools/credential-present.ts", scope: "core" })
5047
5104
  });
5048
5105
  const session = deps.session;
5106
+ const readSessionEntries = deps.readSessionEntries;
5049
5107
  registry.register({
5050
5108
  ...builtin(
5051
5109
  lazyTool(contextToolSurface, async () => {
5052
- const { createContextTool } = await import("./context-5VKGUVJJ.js");
5110
+ const { createContextTool } = await import("./context-ZQ7SIFJV.js");
5053
5111
  if (!session) return createContextTool(skillToolDeps);
5054
- const { probeWorkspace } = await import("./workspace-G4ZWUIPR.js");
5112
+ const { probeWorkspace } = await import("./workspace-ZJ6BFM3Q.js");
5055
5113
  return createContextTool({
5056
5114
  ...skillToolDeps,
5115
+ ...readSessionEntries ? {
5116
+ session: {
5117
+ hasSession: () => session.current() !== null,
5118
+ readEntries: readSessionEntries,
5119
+ activeLeafTurnId: () => {
5120
+ const meta = session.current();
5121
+ return meta ? session.tree(meta.id).leafId ?? void 0 : void 0;
5122
+ },
5123
+ appendEntry: (entry) => session.appendEntry(entry),
5124
+ ...deps.onContextRecalled ? { onRecalled: deps.onContextRecalled } : {}
5125
+ }
5126
+ } : {},
5057
5127
  workspace: {
5058
5128
  hasSession: () => session.current() !== null,
5059
5129
  getSnapshot: () => session.current()?.workspace ?? null,
@@ -5102,6 +5172,14 @@ function assertRegisteredBuiltinTools(registry, registration, includeDispatchToo
5102
5172
 
5103
5173
  export {
5104
5174
  createProtectedArtifactsRegistration,
5175
+ decisionActionClass,
5176
+ decisionFactsForPermission,
5177
+ decisionFactsForAnswer,
5178
+ classifyDecisionPresentation,
5179
+ ASK_USER_OTHER_LABEL,
5180
+ askUserExposure,
5181
+ cancelledAskUserResult,
5182
+ finalizeAskUserInterviewForHost,
5105
5183
  INTERACTIVE_LOOP_BLOCK_BUDGET,
5106
5184
  workerLoopBlockBudget,
5107
5185
  lockedSynthesisFallbackText,
@@ -5115,11 +5193,6 @@ export {
5115
5193
  resolveAgentTools,
5116
5194
  effectiveToolNames,
5117
5195
  resolveSessionTools,
5118
- ASK_USER_OTHER_LABEL,
5119
- cancelledAskUserResult,
5120
- finalizeAskUserInterviewForHost,
5121
- sanitizeCallTargetText,
5122
- describeCallTarget,
5123
5196
  diffWords,
5124
5197
  patchProviderThinkingPayload,
5125
5198
  patchToolChoiceNonePayload,
@@ -5132,4 +5205,4 @@ export {
5132
5205
  assertRegisteredBuiltinTools,
5133
5206
  createRegistry
5134
5207
  };
5135
- //# sourceMappingURL=chunk-LW6DSM3M.js.map
5208
+ //# sourceMappingURL=chunk-ZRGEBJ4T.js.map