@namzu/sdk 54.0.0 → 55.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +1 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/compaction/extractor.d.ts.map +1 -1
  6. package/dist/compaction/extractor.js +5 -1
  7. package/dist/compaction/extractor.js.map +1 -1
  8. package/dist/compaction/managers/structured.d.ts.map +1 -1
  9. package/dist/compaction/managers/structured.js +5 -0
  10. package/dist/compaction/managers/structured.js.map +1 -1
  11. package/dist/compaction/manual.d.ts.map +1 -1
  12. package/dist/compaction/manual.js +6 -0
  13. package/dist/compaction/manual.js.map +1 -1
  14. package/dist/compaction/serializer.d.ts +6 -0
  15. package/dist/compaction/serializer.d.ts.map +1 -1
  16. package/dist/compaction/serializer.js +21 -3
  17. package/dist/compaction/serializer.js.map +1 -1
  18. package/dist/compaction/verifier.d.ts.map +1 -1
  19. package/dist/compaction/verifier.js +27 -6
  20. package/dist/compaction/verifier.js.map +1 -1
  21. package/dist/constants/compaction/index.d.ts +1 -0
  22. package/dist/constants/compaction/index.d.ts.map +1 -1
  23. package/dist/constants/compaction/index.js +1 -0
  24. package/dist/constants/compaction/index.js.map +1 -1
  25. package/dist/{manager/resident/learning-cycle.d.ts → learning/cycle.d.ts} +45 -36
  26. package/dist/learning/cycle.d.ts.map +1 -0
  27. package/dist/{manager/resident/learning-cycle.js → learning/cycle.js} +52 -20
  28. package/dist/learning/cycle.js.map +1 -0
  29. package/dist/learning/deprecated.d.ts +86 -0
  30. package/dist/learning/deprecated.d.ts.map +1 -0
  31. package/dist/learning/deprecated.js +22 -0
  32. package/dist/learning/deprecated.js.map +1 -0
  33. package/dist/learning/experience-store.d.ts +35 -0
  34. package/dist/learning/experience-store.d.ts.map +1 -0
  35. package/dist/learning/experience-store.js +143 -0
  36. package/dist/learning/experience-store.js.map +1 -0
  37. package/dist/learning/experience.d.ts +150 -0
  38. package/dist/learning/experience.d.ts.map +1 -0
  39. package/dist/learning/experience.js +146 -0
  40. package/dist/learning/experience.js.map +1 -0
  41. package/dist/learning/generator.d.ts +118 -0
  42. package/dist/learning/generator.d.ts.map +1 -0
  43. package/dist/learning/generator.js +275 -0
  44. package/dist/learning/generator.js.map +1 -0
  45. package/dist/learning/leak-tripwire.d.ts +66 -0
  46. package/dist/learning/leak-tripwire.d.ts.map +1 -0
  47. package/dist/learning/leak-tripwire.js +208 -0
  48. package/dist/learning/leak-tripwire.js.map +1 -0
  49. package/dist/{manager/resident/learning-observation.d.ts → learning/observation.d.ts} +4 -4
  50. package/dist/learning/observation.d.ts.map +1 -0
  51. package/dist/{manager/resident/learning-observation.js → learning/observation.js} +3 -3
  52. package/dist/learning/observation.js.map +1 -0
  53. package/dist/{manager/resident/learning.d.ts → learning/skill.d.ts} +29 -29
  54. package/dist/learning/skill.d.ts.map +1 -0
  55. package/dist/{manager/resident/learning.js → learning/skill.js} +27 -27
  56. package/dist/learning/skill.js.map +1 -0
  57. package/dist/{manager/resident/learning-store.d.ts → learning/store.d.ts} +38 -31
  58. package/dist/learning/store.d.ts.map +1 -0
  59. package/dist/{manager/resident/learning-store.js → learning/store.js} +52 -20
  60. package/dist/learning/store.js.map +1 -0
  61. package/dist/manager/resident/activity.d.ts +30 -6
  62. package/dist/manager/resident/activity.d.ts.map +1 -1
  63. package/dist/manager/resident/activity.js.map +1 -1
  64. package/dist/manager/resident/agenda.d.ts +42 -12
  65. package/dist/manager/resident/agenda.d.ts.map +1 -1
  66. package/dist/manager/resident/agenda.js +9 -7
  67. package/dist/manager/resident/agenda.js.map +1 -1
  68. package/dist/manager/resident/consumption.d.ts +27 -5
  69. package/dist/manager/resident/consumption.d.ts.map +1 -1
  70. package/dist/manager/resident/consumption.js +2 -0
  71. package/dist/manager/resident/consumption.js.map +1 -1
  72. package/dist/manager/resident/delivery-window.d.ts +7 -1
  73. package/dist/manager/resident/delivery-window.d.ts.map +1 -1
  74. package/dist/manager/resident/delivery-window.js +2 -0
  75. package/dist/manager/resident/delivery-window.js.map +1 -1
  76. package/dist/manager/resident/evidence-recall.d.ts +7 -1
  77. package/dist/manager/resident/evidence-recall.d.ts.map +1 -1
  78. package/dist/manager/resident/evidence-recall.js +2 -0
  79. package/dist/manager/resident/evidence-recall.js.map +1 -1
  80. package/dist/manager/resident/history.d.ts +45 -9
  81. package/dist/manager/resident/history.d.ts.map +1 -1
  82. package/dist/manager/resident/history.js.map +1 -1
  83. package/dist/manager/resident/host.d.ts +44 -10
  84. package/dist/manager/resident/host.d.ts.map +1 -1
  85. package/dist/manager/resident/host.js +4 -2
  86. package/dist/manager/resident/host.js.map +1 -1
  87. package/dist/manager/resident/initiative.d.ts +32 -6
  88. package/dist/manager/resident/initiative.d.ts.map +1 -1
  89. package/dist/manager/resident/initiative.js +2 -0
  90. package/dist/manager/resident/initiative.js.map +1 -1
  91. package/dist/manager/resident/loop.d.ts +22 -4
  92. package/dist/manager/resident/loop.d.ts.map +1 -1
  93. package/dist/manager/resident/loop.js +7 -1
  94. package/dist/manager/resident/loop.js.map +1 -1
  95. package/dist/manager/resident/outbox.d.ts +42 -8
  96. package/dist/manager/resident/outbox.d.ts.map +1 -1
  97. package/dist/manager/resident/outbox.js +2 -0
  98. package/dist/manager/resident/outbox.js.map +1 -1
  99. package/dist/manager/resident/proposal.d.ts +19 -5
  100. package/dist/manager/resident/proposal.d.ts.map +1 -1
  101. package/dist/manager/resident/proposal.js +2 -0
  102. package/dist/manager/resident/proposal.js.map +1 -1
  103. package/dist/manager/resident/store.d.ts +31 -9
  104. package/dist/manager/resident/store.d.ts.map +1 -1
  105. package/dist/manager/resident/store.js +7 -1
  106. package/dist/manager/resident/store.js.map +1 -1
  107. package/dist/manager/resident/tool-evidence.d.ts +45 -9
  108. package/dist/manager/resident/tool-evidence.d.ts.map +1 -1
  109. package/dist/manager/resident/tool-evidence.js +5 -1
  110. package/dist/manager/resident/tool-evidence.js.map +1 -1
  111. package/dist/prompt/coding-agent-doctrine.d.ts +3 -3
  112. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  113. package/dist/prompt/coding-agent-doctrine.js +18 -20
  114. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  115. package/dist/prompt/resident-learning.d.ts +12 -2
  116. package/dist/prompt/resident-learning.d.ts.map +1 -1
  117. package/dist/prompt/resident-learning.js +6 -5
  118. package/dist/prompt/resident-learning.js.map +1 -1
  119. package/dist/prompt/resident-step.d.ts +10 -4
  120. package/dist/prompt/resident-step.d.ts.map +1 -1
  121. package/dist/prompt/resident-step.js +6 -4
  122. package/dist/prompt/resident-step.js.map +1 -1
  123. package/dist/public-runtime.d.ts +10 -4
  124. package/dist/public-runtime.d.ts.map +1 -1
  125. package/dist/public-runtime.js +11 -4
  126. package/dist/public-runtime.js.map +1 -1
  127. package/dist/public-types.d.ts +9 -4
  128. package/dist/public-types.d.ts.map +1 -1
  129. package/dist/runtime/query/checkpoint.d.ts +12 -2
  130. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  131. package/dist/runtime/query/checkpoint.js +42 -8
  132. package/dist/runtime/query/checkpoint.js.map +1 -1
  133. package/dist/runtime/query/executor.d.ts +39 -1
  134. package/dist/runtime/query/executor.d.ts.map +1 -1
  135. package/dist/runtime/query/executor.js +115 -7
  136. package/dist/runtime/query/executor.js.map +1 -1
  137. package/dist/runtime/query/index.d.ts +18 -0
  138. package/dist/runtime/query/index.d.ts.map +1 -1
  139. package/dist/runtime/query/index.js +20 -1
  140. package/dist/runtime/query/index.js.map +1 -1
  141. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  142. package/dist/runtime/query/iteration/index.js +17 -8
  143. package/dist/runtime/query/iteration/index.js.map +1 -1
  144. package/dist/runtime/query/iteration/phases/context.d.ts +24 -2
  145. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  146. package/dist/runtime/query/iteration/phases/context.js +22 -22
  147. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  148. package/dist/runtime/query/iteration/phases/per-call-review.d.ts +87 -0
  149. package/dist/runtime/query/iteration/phases/per-call-review.d.ts.map +1 -0
  150. package/dist/runtime/query/iteration/phases/per-call-review.js +513 -0
  151. package/dist/runtime/query/iteration/phases/per-call-review.js.map +1 -0
  152. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  153. package/dist/runtime/query/iteration/phases/tool-review.js +129 -62
  154. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  155. package/dist/runtime/query/iteration/step-context.d.ts +14 -0
  156. package/dist/runtime/query/iteration/step-context.d.ts.map +1 -1
  157. package/dist/runtime/query/iteration/step-context.js +29 -0
  158. package/dist/runtime/query/iteration/step-context.js.map +1 -1
  159. package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -1
  160. package/dist/runtime/query/iteration/step-shaping.js +6 -2
  161. package/dist/runtime/query/iteration/step-shaping.js.map +1 -1
  162. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  163. package/dist/runtime/query/prepare-turn.js +16 -3
  164. package/dist/runtime/query/prepare-turn.js.map +1 -1
  165. package/dist/runtime/query/project-instructions.d.ts +24 -5
  166. package/dist/runtime/query/project-instructions.d.ts.map +1 -1
  167. package/dist/runtime/query/project-instructions.js +107 -35
  168. package/dist/runtime/query/project-instructions.js.map +1 -1
  169. package/dist/runtime/query/prompt.d.ts +6 -0
  170. package/dist/runtime/query/prompt.d.ts.map +1 -1
  171. package/dist/runtime/query/prompt.js +10 -49
  172. package/dist/runtime/query/prompt.js.map +1 -1
  173. package/dist/runtime/query/resume-pending.d.ts +8 -0
  174. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  175. package/dist/runtime/query/resume-pending.js +101 -0
  176. package/dist/runtime/query/resume-pending.js.map +1 -1
  177. package/dist/runtime/query/review-policy.d.ts +18 -0
  178. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  179. package/dist/runtime/query/review-policy.js +10 -0
  180. package/dist/runtime/query/review-policy.js.map +1 -1
  181. package/dist/runtime/query/steering.d.ts +55 -56
  182. package/dist/runtime/query/steering.d.ts.map +1 -1
  183. package/dist/runtime/query/steering.js +78 -72
  184. package/dist/runtime/query/steering.js.map +1 -1
  185. package/dist/tools/builtins/bash.d.ts +8 -0
  186. package/dist/tools/builtins/bash.d.ts.map +1 -1
  187. package/dist/tools/builtins/bash.js +13 -18
  188. package/dist/tools/builtins/bash.js.map +1 -1
  189. package/dist/tools/builtins/browser.d.ts.map +1 -1
  190. package/dist/tools/builtins/browser.js +38 -12
  191. package/dist/tools/builtins/browser.js.map +1 -1
  192. package/dist/tools/builtins/edit.js +4 -4
  193. package/dist/tools/builtins/edit.js.map +1 -1
  194. package/dist/tools/builtins/job.js +1 -1
  195. package/dist/tools/builtins/job.js.map +1 -1
  196. package/dist/tools/builtins/skill.js +1 -1
  197. package/dist/tools/builtins/skill.js.map +1 -1
  198. package/dist/tools/builtins/structuredOutput.js +1 -1
  199. package/dist/tools/builtins/structuredOutput.js.map +1 -1
  200. package/dist/tools/builtins/verify-outputs.js +1 -1
  201. package/dist/tools/builtins/verify-outputs.js.map +1 -1
  202. package/dist/tools/builtins/write-file.js +4 -4
  203. package/dist/tools/builtins/write-file.js.map +1 -1
  204. package/dist/tools/coordinator/ask-user-question.d.ts +6 -0
  205. package/dist/tools/coordinator/ask-user-question.d.ts.map +1 -1
  206. package/dist/tools/coordinator/ask-user-question.js +6 -1
  207. package/dist/tools/coordinator/ask-user-question.js.map +1 -1
  208. package/dist/tools/coordinator/index.d.ts.map +1 -1
  209. package/dist/tools/coordinator/index.js +10 -3
  210. package/dist/tools/coordinator/index.js.map +1 -1
  211. package/dist/tools/resident-history.d.ts +2 -0
  212. package/dist/tools/resident-history.d.ts.map +1 -1
  213. package/dist/tools/resident-history.js +2 -0
  214. package/dist/tools/resident-history.js.map +1 -1
  215. package/dist/tools/resident-tool-evidence.d.ts +5 -1
  216. package/dist/tools/resident-tool-evidence.d.ts.map +1 -1
  217. package/dist/tools/resident-tool-evidence.js +5 -1
  218. package/dist/tools/resident-tool-evidence.js.map +1 -1
  219. package/dist/tools/schedules/loop-tool.d.ts.map +1 -1
  220. package/dist/tools/schedules/loop-tool.js +4 -2
  221. package/dist/tools/schedules/loop-tool.js.map +1 -1
  222. package/dist/tools/schedules/schedule-tool.js +2 -2
  223. package/dist/tools/schedules/schedule-tool.js.map +1 -1
  224. package/dist/tools/task/create.js +1 -1
  225. package/dist/tools/task/create.js.map +1 -1
  226. package/dist/tools/untrusted-envelope.js +1 -1
  227. package/dist/tools/untrusted-envelope.js.map +1 -1
  228. package/dist/turn/evidence-query.d.ts.map +1 -1
  229. package/dist/turn/evidence-query.js +16 -1
  230. package/dist/turn/evidence-query.js.map +1 -1
  231. package/dist/turn/evidence-recall.js +1 -1
  232. package/dist/turn/evidence-recall.js.map +1 -1
  233. package/dist/types/agent/base.d.ts +7 -0
  234. package/dist/types/agent/base.d.ts.map +1 -1
  235. package/dist/types/hitl/index.d.ts +66 -0
  236. package/dist/types/hitl/index.d.ts.map +1 -1
  237. package/dist/types/hitl/index.js.map +1 -1
  238. package/dist/types/message/index.d.ts +8 -0
  239. package/dist/types/message/index.d.ts.map +1 -1
  240. package/dist/types/message/index.js +12 -2
  241. package/dist/types/message/index.js.map +1 -1
  242. package/dist/types/message/notice-frame.d.ts +41 -0
  243. package/dist/types/message/notice-frame.d.ts.map +1 -0
  244. package/dist/types/message/notice-frame.js +65 -0
  245. package/dist/types/message/notice-frame.js.map +1 -0
  246. package/package.json +1 -1
  247. package/src/agents/QueryAgent.ts +1 -0
  248. package/src/compaction/extractor.ts +8 -1
  249. package/src/compaction/managers/structured.ts +3 -0
  250. package/src/compaction/manual.ts +5 -0
  251. package/src/compaction/serializer.ts +28 -3
  252. package/src/compaction/verifier.ts +32 -6
  253. package/src/constants/compaction/index.ts +1 -0
  254. package/src/{manager/resident/learning-cycle.ts → learning/cycle.ts} +130 -78
  255. package/src/learning/deprecated.ts +124 -0
  256. package/src/learning/experience-store.ts +202 -0
  257. package/src/learning/experience.ts +238 -0
  258. package/src/learning/generator.ts +388 -0
  259. package/src/learning/leak-tripwire.ts +280 -0
  260. package/src/{manager/resident/learning-observation.ts → learning/observation.ts} +5 -5
  261. package/src/{manager/resident/learning.ts → learning/skill.ts} +64 -67
  262. package/src/{manager/resident/learning-store.ts → learning/store.ts} +97 -67
  263. package/src/manager/resident/activity.ts +30 -6
  264. package/src/manager/resident/agenda.ts +63 -33
  265. package/src/manager/resident/consumption.ts +27 -5
  266. package/src/manager/resident/delivery-window.ts +7 -1
  267. package/src/manager/resident/evidence-recall.ts +7 -1
  268. package/src/manager/resident/history.ts +45 -9
  269. package/src/manager/resident/host.ts +45 -11
  270. package/src/manager/resident/initiative.ts +32 -6
  271. package/src/manager/resident/loop.ts +22 -4
  272. package/src/manager/resident/outbox.ts +42 -8
  273. package/src/manager/resident/proposal.ts +17 -3
  274. package/src/manager/resident/store.ts +27 -5
  275. package/src/manager/resident/tool-evidence.ts +45 -9
  276. package/src/prompt/coding-agent-doctrine.ts +18 -20
  277. package/src/prompt/resident-learning.ts +16 -7
  278. package/src/prompt/resident-step.ts +19 -13
  279. package/src/public-runtime.ts +36 -5
  280. package/src/public-types.ts +96 -34
  281. package/src/runtime/query/checkpoint.ts +49 -8
  282. package/src/runtime/query/executor.ts +165 -15
  283. package/src/runtime/query/index.ts +45 -1
  284. package/src/runtime/query/iteration/index.ts +19 -6
  285. package/src/runtime/query/iteration/phases/context.ts +37 -11
  286. package/src/runtime/query/iteration/phases/per-call-review.ts +617 -0
  287. package/src/runtime/query/iteration/phases/tool-review.ts +168 -83
  288. package/src/runtime/query/iteration/step-context.ts +29 -0
  289. package/src/runtime/query/iteration/step-shaping.ts +6 -2
  290. package/src/runtime/query/prepare-turn.ts +18 -1
  291. package/src/runtime/query/project-instructions.ts +119 -38
  292. package/src/runtime/query/prompt.ts +11 -54
  293. package/src/runtime/query/resume-pending.ts +131 -0
  294. package/src/runtime/query/review-policy.ts +28 -0
  295. package/src/runtime/query/steering.ts +100 -96
  296. package/src/tools/builtins/bash.ts +13 -19
  297. package/src/tools/builtins/browser.ts +38 -12
  298. package/src/tools/builtins/edit.ts +4 -4
  299. package/src/tools/builtins/job.ts +1 -1
  300. package/src/tools/builtins/skill.ts +1 -1
  301. package/src/tools/builtins/structuredOutput.ts +1 -1
  302. package/src/tools/builtins/verify-outputs.ts +1 -1
  303. package/src/tools/builtins/write-file.ts +4 -4
  304. package/src/tools/coordinator/ask-user-question.ts +12 -1
  305. package/src/tools/coordinator/index.ts +10 -3
  306. package/src/tools/resident-history.ts +2 -0
  307. package/src/tools/resident-tool-evidence.ts +5 -1
  308. package/src/tools/schedules/loop-tool.ts +4 -2
  309. package/src/tools/schedules/schedule-tool.ts +2 -2
  310. package/src/tools/task/create.ts +1 -1
  311. package/src/tools/untrusted-envelope.ts +1 -1
  312. package/src/turn/evidence-query.ts +19 -1
  313. package/src/turn/evidence-recall.ts +1 -1
  314. package/src/types/agent/base.ts +8 -0
  315. package/src/types/hitl/index.ts +67 -0
  316. package/src/types/message/index.ts +20 -2
  317. package/src/types/message/notice-frame.ts +88 -0
  318. package/dist/manager/resident/learning-cycle.d.ts.map +0 -1
  319. package/dist/manager/resident/learning-cycle.js.map +0 -1
  320. package/dist/manager/resident/learning-observation.d.ts.map +0 -1
  321. package/dist/manager/resident/learning-observation.js.map +0 -1
  322. package/dist/manager/resident/learning-store.d.ts.map +0 -1
  323. package/dist/manager/resident/learning-store.js.map +0 -1
  324. package/dist/manager/resident/learning.d.ts.map +0 -1
  325. package/dist/manager/resident/learning.js.map +0 -1
@@ -0,0 +1,617 @@
1
+ import type { AuthorizationGate } from '../../../../authorization/index.js'
2
+ import type {
3
+ HITLResumeDecision,
4
+ ToolCallDeclined,
5
+ ToolCallSummary,
6
+ } from '../../../../types/hitl/index.js'
7
+ import type { ToolCall } from '../../../../types/message/index.js'
8
+ import { DECLINED_TOOL_CALL_FEEDBACK } from '../../declined.js'
9
+ import type { CallAdmission, ReviewedExecution } from '../../executor.js'
10
+ import { DECLINED_NOTE_MAX_CHARS } from '../../executor.js'
11
+ import { supersededByRecovery } from '../../resume-pending.js'
12
+ import { type IterationContext, awaitDecisionDurably } from './context.js'
13
+
14
+ /** What a call is told when a refusal elsewhere in its batch ended the turn before it started. */
15
+ export const REFUSED_WITH_THE_TURN =
16
+ 'An earlier call in this batch was refused and the turn was stopped, so this call was not run. Wait for the user.'
17
+
18
+ /** What a call is told when the answer to it could not be taken. */
19
+ export const UNANSWERED_CALL =
20
+ 'No answer was given for this call, so it was not run. Ask again if it is still needed.'
21
+
22
+ const EDIT_NOT_OFFERED =
23
+ 'Editing a call while it is asked about is not offered, so this call was not run. Ask again with the input you want.'
24
+
25
+ const PAUSED_CALL =
26
+ 'The turn was asked to wait before this call could be answered, so it was not run. Ask again if it is still needed.'
27
+
28
+ const NOT_THIS_QUESTION =
29
+ 'That answer was not an answer to a question about a tool call, so this call was not run.'
30
+
31
+ /** The audit trail keeps a person's reason, cut short and without control characters. */
32
+ /**
33
+ * Whether a tool result is the answer to a call that never ran because the
34
+ * review did not let it: the person's No, a refusal that ended the turn, or a
35
+ * question that went unanswered. Such a result records a choice or an
36
+ * interruption, not a tool that failed, so a host counting failures leaves it
37
+ * out. Policy refusals and failed runs are not matched.
38
+ */
39
+ export function isReviewRefusalResult(output: string): boolean {
40
+ if (!output.startsWith('Error: Tool "')) return false
41
+ return [
42
+ DECLINED_TOOL_CALL_FEEDBACK,
43
+ REFUSED_WITH_THE_TURN,
44
+ UNANSWERED_CALL,
45
+ EDIT_NOT_OFFERED,
46
+ PAUSED_CALL,
47
+ NOT_THIS_QUESTION,
48
+ ].some((reason) => output.endsWith(` was not executed. ${reason}`) || output.endsWith(reason))
49
+ }
50
+
51
+ const AUDIT_NOTE_MAX_CHARS = 500
52
+
53
+ /**
54
+ * A person's words, made safe to print: control characters (an escape
55
+ * sequence among them) become spaces, line breaks and tabs stay, and the
56
+ * length is capped by code point.
57
+ */
58
+ export function cleanPersonNote(note: string | undefined): string {
59
+ if (note === undefined) return ''
60
+ let out = ''
61
+ let inGap = false
62
+ for (const ch of note) {
63
+ const code = ch.codePointAt(0) as number
64
+ const control = code <= 0x08 || (code >= 0x0b && code <= 0x1f) || (code >= 0x7f && code <= 0x9f)
65
+ if (control) {
66
+ if (!inGap) out += ' '
67
+ inGap = true
68
+ continue
69
+ }
70
+ inGap = false
71
+ out += ch
72
+ }
73
+ return Array.from(out.trim()).slice(0, DECLINED_NOTE_MAX_CHARS).join('')
74
+ }
75
+
76
+ interface Refusal {
77
+ readonly run: false
78
+ readonly reason: string
79
+ readonly declined?: ToolCallDeclined
80
+ }
81
+
82
+ type Verdict = { readonly run: true } | Refusal
83
+
84
+ /** What a decision about some calls means for each of them. */
85
+ interface Interpreted {
86
+ readonly verdicts: Map<string, Verdict>
87
+ /** The calls were refused and the refusal ends the turn. */
88
+ readonly endsTurn: boolean
89
+ /** The decision was the host stopping the turn outright. */
90
+ readonly cancelled: boolean
91
+ /** The decision was a request to wait: the park it recorded must not stay open. */
92
+ readonly paused: boolean
93
+ }
94
+
95
+ interface OpenAsk {
96
+ readonly calls: readonly ToolCallSummary[]
97
+ readonly controller: AbortController
98
+ /** A question about one call can be closed by a re-check; a question about several cannot. */
99
+ readonly recheckable: boolean
100
+ /** Why the question was closed from here, when it was. */
101
+ closedBy?: 'covered' | 'ended'
102
+ /** The question's answer was taken from the close itself, not from the host. */
103
+ closedHere?: true
104
+ }
105
+
106
+ /** What the walk needs from the review phase; keeps this file free of that file's closures. */
107
+ export interface PerCallReviewDeps {
108
+ readonly ctx: IterationContext & {
109
+ readonly verificationGate?: AuthorizationGate
110
+ }
111
+ readonly iteration: number
112
+ readonly toolCalls: readonly ToolCall[]
113
+ readonly summaries: () => readonly ToolCallSummary[]
114
+ /** Ids the review may not release: the operator's policy denied them. */
115
+ readonly gateDenied: ReadonlyMap<string, string>
116
+ readonly reviewAllowed: () => boolean
117
+ /**
118
+ * Calls that carry no summary because their preparation failed: they run
119
+ * nothing, the executor answers them with the reason, so they need no yes.
120
+ */
121
+ readonly unreviewedIds: ReadonlySet<string>
122
+ /** The shared checks that follow an approval or a refusal; see `runToolReview`. */
123
+ readonly settleEscalations: (
124
+ denials: Map<string, string>,
125
+ confirmed: readonly string[] | undefined,
126
+ calls: readonly ToolCallSummary[],
127
+ ) => Promise<void>
128
+ readonly recordGrantAudits: (
129
+ decision: HITLResumeDecision,
130
+ calls: readonly ToolCallSummary[],
131
+ ) => Promise<void>
132
+ readonly evaluateGate: (call: ToolCallSummary) => 'allow' | 'deny' | 'review' | undefined
133
+ }
134
+
135
+ /**
136
+ * The per-call review of one batch: it answers the executor's question "may this
137
+ * call start" by asking about that call alone, and keeps every question that is
138
+ * open at once consistent with the others.
139
+ *
140
+ * - An answer that grants something (a remembered approval, a changed rule or
141
+ * mode on the host's side) re-checks the questions still open, and any call
142
+ * the new state covers is let through unasked. A re-check never refuses.
143
+ * - A plain refusal ends the turn: the questions still open close, calls not
144
+ * yet started are refused without being asked, calls in flight are cancelled.
145
+ * - Every refusal and every call that could not be matched to a reviewed
146
+ * summary is a refusal. Nothing is approved by the absence of an answer.
147
+ */
148
+ export class PerCallReview implements ReviewedExecution {
149
+ readonly #deps: PerCallReviewDeps
150
+ readonly #endController = new AbortController()
151
+ readonly #open = new Set<OpenAsk>()
152
+ readonly #approvedTogether = new Set<string>()
153
+ /** Refusals settled before the walk, from the question asked of several calls at once. */
154
+ readonly denials = new Map<string, string>()
155
+ readonly declines = new Map<string, ToolCallDeclined>()
156
+ #endsTurn = false
157
+ #cancelled = false
158
+ #refused = 0
159
+ #checking: Promise<void> | undefined
160
+ #checkAgain = false
161
+ #unsubscribe: (() => void) | undefined
162
+ /**
163
+ * The end of the line of calls waiting to open their question. Questions open
164
+ * one after another in the order the calls were admitted, which is the order
165
+ * the model wrote them, whatever each call's own checks take to finish.
166
+ */
167
+ #line: Promise<void> = Promise.resolve()
168
+
169
+ constructor(deps: PerCallReviewDeps) {
170
+ this.#deps = deps
171
+ this.#unsubscribe = deps.ctx.perCallReview?.onRulesChanged?.(() => {
172
+ void this.recheck()
173
+ })
174
+ }
175
+
176
+ get ended(): AbortSignal {
177
+ return this.#endController.signal
178
+ }
179
+
180
+ /** A refusal (or the host) ended the turn. */
181
+ get endsTurn(): boolean {
182
+ return this.#endsTurn
183
+ }
184
+
185
+ /** How many calls the walk refused, asked or not. */
186
+ get refusedCount(): number {
187
+ return this.#refused
188
+ }
189
+
190
+ /** The host stopped the turn rather than a person refusing a call. */
191
+ get cancelled(): boolean {
192
+ return this.#cancelled
193
+ }
194
+
195
+ /** Stop listening for changes. Called when the walk is over, whichever way it ended. */
196
+ dispose(): void {
197
+ this.#unsubscribe?.()
198
+ this.#unsubscribe = undefined
199
+ }
200
+
201
+ /**
202
+ * The questions that are about the batch rather than about one call, asked
203
+ * first and together. Their answers are settled here, before anything starts.
204
+ */
205
+ async askTogetherFirst(): Promise<void> {
206
+ const config = this.#deps.ctx.perCallReview
207
+ if (!config?.askTogether) return
208
+ const askable = this.#deps
209
+ .summaries()
210
+ .filter((call) => !this.#deps.gateDenied.has(call.id) && !this.#skipsAsk(call))
211
+ if (askable.length === 0) return
212
+ const wanted = new Set(config.askTogether(askable))
213
+ const together = askable.filter((call) => wanted.has(call.id))
214
+ if (together.length === 0) return
215
+ const interpreted = await this.#ask(together)
216
+ for (const call of together) {
217
+ const verdict = interpreted.verdicts.get(call.id) ?? {
218
+ run: false,
219
+ reason: UNANSWERED_CALL,
220
+ }
221
+ if (verdict.run) {
222
+ this.#approvedTogether.add(call.id)
223
+ } else {
224
+ this.denials.set(call.id, verdict.reason)
225
+ if (verdict.declined) this.declines.set(call.id, verdict.declined)
226
+ }
227
+ }
228
+ this.#noteEnd(interpreted)
229
+ }
230
+
231
+ /** Answers the executor: may this call start. */
232
+ async admit(call: ToolCall): Promise<CallAdmission> {
233
+ // Taken synchronously, so the place in line is the order of the asking.
234
+ const before = this.#line
235
+ let leave!: () => void
236
+ this.#line = new Promise<void>((resolve) => {
237
+ leave = resolve
238
+ })
239
+ try {
240
+ const verdict = await this.#decide(call, before, leave)
241
+ if (!verdict.run) this.#refused += 1
242
+ return verdict
243
+ } finally {
244
+ leave()
245
+ }
246
+ }
247
+
248
+ async #decide(call: ToolCall, before: Promise<void>, leave: () => void): Promise<CallAdmission> {
249
+ await before
250
+ if (this.ended.aborted) return { run: false, reason: REFUSED_WITH_THE_TURN }
251
+ if (this.#approvedTogether.has(call.id)) return { run: true }
252
+ const summary = this.#deps.summaries().find((candidate) => candidate.id === call.id)
253
+ if (summary === undefined) {
254
+ // A call whose preparation failed runs nothing; the executor answers it
255
+ // with the reason. Any other call that has no summary was never shown,
256
+ // and a call nobody saw is not approved.
257
+ return this.#deps.unreviewedIds.has(call.id)
258
+ ? { run: true }
259
+ : { run: false, reason: UNANSWERED_CALL }
260
+ }
261
+ if (this.#skipsAsk(summary)) return { run: true }
262
+ // A call that waited for its turn is checked against the rules, grants and
263
+ // mode as they are now: an earlier answer may have remembered a rule that
264
+ // covers it, and nobody is asked about what is already settled.
265
+ if (await this.#coveredNow(summary)) {
266
+ await this.#deps.ctx.recorder.recordAudit({
267
+ what: { action: 'tool_call', tool: summary.name },
268
+ outcome: 'approved',
269
+ reason:
270
+ 'a rule, an earlier approval or the permission mode changed while this call waited for its turn and now covers it; it was approved without being shown',
271
+ })
272
+ return { run: true }
273
+ }
274
+ const interpreted = await this.#ask([summary], leave)
275
+ this.#noteEnd(interpreted)
276
+ const verdict = interpreted.verdicts.get(summary.id) ?? {
277
+ run: false,
278
+ reason: UNANSWERED_CALL,
279
+ }
280
+ if (!verdict.run) {
281
+ this.#deps.ctx.log.info('A tool call was refused', {
282
+ 'namzu.tool.names': [summary.name],
283
+ })
284
+ return verdict
285
+ }
286
+ return { run: true }
287
+ }
288
+
289
+ /**
290
+ * Re-check every question still open against the rules and approvals as they
291
+ * are now. Only ever approves.
292
+ */
293
+ recheck(): Promise<void> {
294
+ if (this.#checking) {
295
+ this.#checkAgain = true
296
+ return this.#checking
297
+ }
298
+ this.#checking = (async () => {
299
+ try {
300
+ do {
301
+ this.#checkAgain = false
302
+ for (const ask of [...this.#open]) {
303
+ if (!ask.recheckable || ask.closedBy !== undefined || this.ended.aborted) continue
304
+ if (await this.#coveredNow(ask.calls[0] as ToolCallSummary)) {
305
+ // The person may have answered while this was being worked out.
306
+ if (!this.#open.has(ask) || ask.closedBy !== undefined) continue
307
+ ask.closedBy = 'covered'
308
+ ask.controller.abort()
309
+ }
310
+ }
311
+ } while (this.#checkAgain)
312
+ } finally {
313
+ this.#checking = undefined
314
+ }
315
+ })()
316
+ return this.#checking
317
+ }
318
+
319
+ /** Calls a re-check must never wave through: they exist to put a question to a person. */
320
+ static #alwaysAsked(call: ToolCallSummary): boolean {
321
+ return (
322
+ call.escalation !== undefined ||
323
+ call.requiresApproval === true ||
324
+ call.authorization?.askRule === true ||
325
+ call.authorization?.explicitReview === true ||
326
+ call.hookPermission?.decision === 'ask'
327
+ )
328
+ }
329
+
330
+ async #coveredNow(call: ToolCallSummary): Promise<boolean> {
331
+ if (PerCallReview.#alwaysAsked(call) || this.#deps.gateDenied.has(call.id)) return false
332
+ const { ctx } = this.#deps
333
+ // The host's veto comes before every approval path, the turn's own included.
334
+ if (await this.#vetoed(ctx.perCallReview?.neverCover, call)) return false
335
+ if (!this.#deps.reviewAllowed()) {
336
+ if (this.#deps.evaluateGate(call) === 'allow') return true
337
+ if (ctx.toolGrants?.covers(call)) return true
338
+ }
339
+ try {
340
+ return (await ctx.perCallReview?.covers?.(call)) === true
341
+ } catch {
342
+ // A host predicate that fails is not a yes.
343
+ return false
344
+ }
345
+ }
346
+
347
+ /** A host predicate that throws vetoes: a failure is never a yes. */
348
+ async #vetoed(
349
+ veto: ((call: ToolCallSummary) => boolean | Promise<boolean>) | undefined,
350
+ call: ToolCallSummary,
351
+ ): Promise<boolean> {
352
+ if (!veto) return false
353
+ try {
354
+ return (await veto(call)) !== false
355
+ } catch {
356
+ return true
357
+ }
358
+ }
359
+
360
+ /** Whether the turn's own rules and earlier approvals let this call run unasked. */
361
+ #skipsAsk(call: ToolCallSummary): boolean {
362
+ if (this.#deps.reviewAllowed()) return false
363
+ if (call.authorization?.decision === 'allow') return !PerCallReview.#alwaysAsked(call)
364
+ return !PerCallReview.#alwaysAsked(call) && this.#deps.ctx.toolGrants?.covers(call) === true
365
+ }
366
+
367
+ #noteEnd(interpreted: Interpreted): void {
368
+ if (!interpreted.endsTurn && !interpreted.cancelled) return
369
+ this.#end(interpreted.cancelled)
370
+ }
371
+
372
+ #end(cancelled: boolean): void {
373
+ if (this.#endsTurn) return
374
+ this.#endsTurn = true
375
+ this.#cancelled = cancelled
376
+ for (const ask of this.#open) {
377
+ ask.closedBy ??= 'ended'
378
+ ask.controller.abort()
379
+ }
380
+ this.#endController.abort(new Error('the user stopped the turn'))
381
+ }
382
+
383
+ /** One request to the handler about `calls`, and what its answer means for each. */
384
+ async #ask(calls: readonly ToolCallSummary[], opened?: () => void): Promise<Interpreted> {
385
+ const { ctx, iteration } = this.#deps
386
+ const checkpoint = await ctx.checkpointMgr.create(ctx.recorder, iteration)
387
+ await ctx.emitEvent({
388
+ type: 'tool_review_requested',
389
+ turnId: ctx.recorder.turnId,
390
+ toolCalls: [...calls],
391
+ iteration,
392
+ })
393
+ const asked: OpenAsk = {
394
+ calls,
395
+ controller: new AbortController(),
396
+ recheckable: calls.length === 1,
397
+ }
398
+ this.#open.add(asked)
399
+ let decision: HITLResumeDecision
400
+ try {
401
+ const answer = awaitDecisionDurably(
402
+ ctx,
403
+ checkpoint,
404
+ {
405
+ type: 'tool_review',
406
+ sessionId: ctx.recorder.sessionId,
407
+ turnId: ctx.recorder.turnId,
408
+ checkpointId: checkpoint.id,
409
+ toolCalls: [...calls],
410
+ perCall: true,
411
+ },
412
+ {
413
+ signal: asked.controller.signal,
414
+ closedAs: () => {
415
+ asked.closedHere = true
416
+ return asked.closedBy === 'covered'
417
+ ? { action: 'approve_tools' }
418
+ : { action: 'reject_tools', feedback: REFUSED_WITH_THE_TURN }
419
+ },
420
+ },
421
+ )
422
+ // The handler has been given the question: the next call may open its own.
423
+ opened?.()
424
+ decision = await answer
425
+ } finally {
426
+ this.#open.delete(asked)
427
+ }
428
+
429
+ // Only an answer the close itself supplied counts as the close: a person
430
+ // who answered first has answered, and a re-check that landed a moment
431
+ // later does not turn their No into a yes.
432
+ if (asked.closedHere && asked.closedBy === 'covered') {
433
+ await ctx.emitEvent({
434
+ type: 'tool_review_completed',
435
+ turnId: ctx.recorder.turnId,
436
+ decision: 'approved',
437
+ })
438
+ for (const call of calls) {
439
+ await ctx.recorder.recordAudit({
440
+ what: { action: 'tool_call', tool: call.name },
441
+ outcome: 'approved',
442
+ reason:
443
+ 'a rule, an earlier approval or the permission mode changed while this call waited and now covers it; it was approved without being shown',
444
+ })
445
+ }
446
+ return this.#settle(
447
+ { action: 'approve_tools' },
448
+ calls,
449
+ new Map(calls.map((call) => [call.id, { run: true } as Verdict])),
450
+ false,
451
+ false,
452
+ false,
453
+ )
454
+ }
455
+ if (asked.closedHere && asked.closedBy === 'ended') {
456
+ await ctx.emitEvent({
457
+ type: 'tool_review_completed',
458
+ turnId: ctx.recorder.turnId,
459
+ decision: 'rejected',
460
+ })
461
+ return {
462
+ verdicts: new Map(
463
+ calls.map((call) => [call.id, { run: false, reason: REFUSED_WITH_THE_TURN } as Verdict]),
464
+ ),
465
+ endsTurn: false,
466
+ cancelled: false,
467
+ paused: false,
468
+ }
469
+ }
470
+
471
+ const interpreted = this.#interpret(decision, calls)
472
+ await ctx.emitEvent({
473
+ type: 'tool_review_completed',
474
+ turnId: ctx.recorder.turnId,
475
+ decision: decisionLabel(decision, interpreted),
476
+ })
477
+ if (interpreted.paused) {
478
+ await ctx.checkpointMgr
479
+ .unpark(checkpoint.id, supersededByRecovery(decision))
480
+ .catch(() => null)
481
+ }
482
+ return this.#settle(
483
+ decision,
484
+ calls,
485
+ interpreted.verdicts,
486
+ interpreted.endsTurn,
487
+ interpreted.cancelled,
488
+ interpreted.paused,
489
+ )
490
+ }
491
+
492
+ /** The checks that follow an answer, then the answer's side effects. */
493
+ async #settle(
494
+ decision: HITLResumeDecision,
495
+ calls: readonly ToolCallSummary[],
496
+ verdicts: Map<string, Verdict>,
497
+ endsTurn: boolean,
498
+ cancelled: boolean,
499
+ paused: boolean,
500
+ ): Promise<Interpreted> {
501
+ const { ctx } = this.#deps
502
+ const denials = new Map<string, string>()
503
+ for (const call of calls) {
504
+ const verdict = verdicts.get(call.id)
505
+ if (verdict && !verdict.run) denials.set(call.id, verdict.reason)
506
+ }
507
+ const confirmed =
508
+ decision.action === 'approve_tools' || decision.action === 'modify_tools'
509
+ ? decision.confirmedEscalations
510
+ : undefined
511
+ await this.#deps.settleEscalations(denials, confirmed, calls)
512
+ const result = new Map<string, Verdict>()
513
+ for (const call of calls) {
514
+ const prior = verdicts.get(call.id) ?? {
515
+ run: false,
516
+ reason: UNANSWERED_CALL,
517
+ }
518
+ const reason = denials.get(call.id)
519
+ // An escape the answer did not confirm turns a yes into a refusal here.
520
+ result.set(call.id, prior.run && reason !== undefined ? { run: false, reason } : prior)
521
+ }
522
+ const approvedCalls = calls.filter((call) => result.get(call.id)?.run === true)
523
+ if (decision.action === 'approve_tools' && approvedCalls.length > 0) {
524
+ if (decision.remember) ctx.toolGrants?.grant(decision.remember)
525
+ await this.#deps.recordGrantAudits(decision, approvedCalls)
526
+ // What was just remembered may cover a question still open.
527
+ if (decision.remember) void this.recheck()
528
+ }
529
+ for (const call of calls) {
530
+ const verdict = result.get(call.id)
531
+ if (verdict && !verdict.run && verdict.declined) {
532
+ const note = cleanPersonNote(verdict.declined.note)
533
+ await ctx.recorder.recordAudit({
534
+ what: { action: 'tool_call', tool: call.name },
535
+ outcome: 'refused',
536
+ reason: `declined by the person${
537
+ note ? `: ${Array.from(note).slice(0, AUDIT_NOTE_MAX_CHARS).join('')}` : ''
538
+ }`,
539
+ })
540
+ }
541
+ }
542
+ return { verdicts: result, endsTurn, cancelled, paused }
543
+ }
544
+
545
+ #interpret(decision: HITLResumeDecision, calls: readonly ToolCallSummary[]): Interpreted {
546
+ const verdicts = new Map<string, Verdict>()
547
+ const refuseAll = (verdict: Refusal) => {
548
+ for (const call of calls) verdicts.set(call.id, verdict)
549
+ }
550
+ let endsTurn = false
551
+ let cancelled = false
552
+ let paused = false
553
+ switch (decision.action) {
554
+ case 'approve_tools':
555
+ case 'continue':
556
+ for (const call of calls) verdicts.set(call.id, { run: true })
557
+ break
558
+ case 'reject_tools': {
559
+ const note = cleanPersonNote(decision.declined?.note)
560
+ refuseAll({
561
+ run: false,
562
+ reason: decision.feedback
563
+ ? Array.from(decision.feedback)
564
+ .slice(0, DECLINED_NOTE_MAX_CHARS * 2)
565
+ .join('')
566
+ : DECLINED_TOOL_CALL_FEEDBACK,
567
+ ...(decision.declined ? { declined: note ? { note } : {} } : {}),
568
+ })
569
+ endsTurn = decision.endTurn === true
570
+ break
571
+ }
572
+ case 'modify_tools': {
573
+ for (const call of calls) {
574
+ const mods = decision.modifications.filter((mod) => mod.toolCallId === call.id)
575
+ if (mods.some((mod) => mod.action === 'deny')) {
576
+ verdicts.set(call.id, {
577
+ run: false,
578
+ reason: DECLINED_TOOL_CALL_FEEDBACK,
579
+ declined: {},
580
+ })
581
+ } else if (mods.some((mod) => mod.action === 'modify')) {
582
+ verdicts.set(call.id, { run: false, reason: EDIT_NOT_OFFERED })
583
+ } else if (mods.some((mod) => mod.action === 'approve')) {
584
+ verdicts.set(call.id, { run: true })
585
+ } else {
586
+ verdicts.set(call.id, { run: false, reason: UNANSWERED_CALL })
587
+ }
588
+ }
589
+ break
590
+ }
591
+ case 'pause':
592
+ refuseAll({ run: false, reason: PAUSED_CALL })
593
+ paused = true
594
+ break
595
+ case 'abort':
596
+ refuseAll({ run: false, reason: REFUSED_WITH_THE_TURN })
597
+ cancelled = true
598
+ break
599
+ default:
600
+ // A plan or question answer to a tool question: not an answer to it.
601
+ this.#deps.ctx.log.warn('Unexpected decision during a per-call tool review', {
602
+ 'namzu.runtime.action': decision.action,
603
+ })
604
+ refuseAll({ run: false, reason: NOT_THIS_QUESTION })
605
+ }
606
+ return { verdicts, endsTurn, cancelled, paused }
607
+ }
608
+ }
609
+
610
+ function decisionLabel(
611
+ decision: HITLResumeDecision,
612
+ interpreted: Interpreted,
613
+ ): 'approved' | 'modified' | 'rejected' {
614
+ if (decision.action === 'modify_tools') return 'modified'
615
+ const refused = [...interpreted.verdicts.values()].some((verdict) => !verdict.run)
616
+ return refused ? 'rejected' : 'approved'
617
+ }