@namzu/sdk 53.2.2 → 55.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/CHANGELOG.md +56 -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/context-breakdown.d.ts +85 -0
  6. package/dist/compaction/context-breakdown.d.ts.map +1 -0
  7. package/dist/compaction/context-breakdown.js +149 -0
  8. package/dist/compaction/context-breakdown.js.map +1 -0
  9. package/dist/compaction/extractor.d.ts.map +1 -1
  10. package/dist/compaction/extractor.js +5 -1
  11. package/dist/compaction/extractor.js.map +1 -1
  12. package/dist/compaction/managers/structured.d.ts.map +1 -1
  13. package/dist/compaction/managers/structured.js +5 -0
  14. package/dist/compaction/managers/structured.js.map +1 -1
  15. package/dist/compaction/manual.d.ts.map +1 -1
  16. package/dist/compaction/manual.js +6 -0
  17. package/dist/compaction/manual.js.map +1 -1
  18. package/dist/compaction/serializer.d.ts +6 -0
  19. package/dist/compaction/serializer.d.ts.map +1 -1
  20. package/dist/compaction/serializer.js +21 -3
  21. package/dist/compaction/serializer.js.map +1 -1
  22. package/dist/compaction/token-estimate.d.ts +2 -0
  23. package/dist/compaction/token-estimate.d.ts.map +1 -1
  24. package/dist/compaction/token-estimate.js +4 -0
  25. package/dist/compaction/token-estimate.js.map +1 -1
  26. package/dist/compaction/verifier.d.ts.map +1 -1
  27. package/dist/compaction/verifier.js +27 -6
  28. package/dist/compaction/verifier.js.map +1 -1
  29. package/dist/constants/compaction/index.d.ts +1 -0
  30. package/dist/constants/compaction/index.d.ts.map +1 -1
  31. package/dist/constants/compaction/index.js +1 -0
  32. package/dist/constants/compaction/index.js.map +1 -1
  33. package/dist/manager/resident/proposal.d.ts +2 -2
  34. package/dist/manager/resident/store.d.ts +4 -4
  35. package/dist/prompt/coding-agent-doctrine.d.ts +3 -3
  36. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  37. package/dist/prompt/coding-agent-doctrine.js +18 -20
  38. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  39. package/dist/public-runtime.d.ts +8 -3
  40. package/dist/public-runtime.d.ts.map +1 -1
  41. package/dist/public-runtime.js +7 -2
  42. package/dist/public-runtime.js.map +1 -1
  43. package/dist/runtime/cache-clock.d.ts +39 -0
  44. package/dist/runtime/cache-clock.d.ts.map +1 -0
  45. package/dist/runtime/cache-clock.js +48 -0
  46. package/dist/runtime/cache-clock.js.map +1 -0
  47. package/dist/runtime/query/checkpoint.d.ts +12 -2
  48. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  49. package/dist/runtime/query/checkpoint.js +42 -8
  50. package/dist/runtime/query/checkpoint.js.map +1 -1
  51. package/dist/runtime/query/compact-session.d.ts +37 -2
  52. package/dist/runtime/query/compact-session.d.ts.map +1 -1
  53. package/dist/runtime/query/compact-session.js +77 -3
  54. package/dist/runtime/query/compact-session.js.map +1 -1
  55. package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
  56. package/dist/runtime/query/executor/tool-call-admission.js +3 -0
  57. package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
  58. package/dist/runtime/query/executor.d.ts +39 -1
  59. package/dist/runtime/query/executor.d.ts.map +1 -1
  60. package/dist/runtime/query/executor.js +115 -7
  61. package/dist/runtime/query/executor.js.map +1 -1
  62. package/dist/runtime/query/index.d.ts +18 -0
  63. package/dist/runtime/query/index.d.ts.map +1 -1
  64. package/dist/runtime/query/index.js +20 -1
  65. package/dist/runtime/query/index.js.map +1 -1
  66. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  67. package/dist/runtime/query/iteration/index.js +25 -8
  68. package/dist/runtime/query/iteration/index.js.map +1 -1
  69. package/dist/runtime/query/iteration/phases/context.d.ts +24 -2
  70. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  71. package/dist/runtime/query/iteration/phases/context.js +22 -22
  72. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  73. package/dist/runtime/query/iteration/phases/per-call-review.d.ts +78 -0
  74. package/dist/runtime/query/iteration/phases/per-call-review.d.ts.map +1 -0
  75. package/dist/runtime/query/iteration/phases/per-call-review.js +494 -0
  76. package/dist/runtime/query/iteration/phases/per-call-review.js.map +1 -0
  77. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  78. package/dist/runtime/query/iteration/phases/tool-review.js +129 -62
  79. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  80. package/dist/runtime/query/iteration/step-context.d.ts +14 -0
  81. package/dist/runtime/query/iteration/step-context.d.ts.map +1 -1
  82. package/dist/runtime/query/iteration/step-context.js +29 -0
  83. package/dist/runtime/query/iteration/step-context.js.map +1 -1
  84. package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -1
  85. package/dist/runtime/query/iteration/step-shaping.js +6 -2
  86. package/dist/runtime/query/iteration/step-shaping.js.map +1 -1
  87. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  88. package/dist/runtime/query/prepare-turn.js +16 -3
  89. package/dist/runtime/query/prepare-turn.js.map +1 -1
  90. package/dist/runtime/query/project-instructions.d.ts +24 -5
  91. package/dist/runtime/query/project-instructions.d.ts.map +1 -1
  92. package/dist/runtime/query/project-instructions.js +107 -35
  93. package/dist/runtime/query/project-instructions.js.map +1 -1
  94. package/dist/runtime/query/prompt.d.ts +6 -0
  95. package/dist/runtime/query/prompt.d.ts.map +1 -1
  96. package/dist/runtime/query/prompt.js +10 -49
  97. package/dist/runtime/query/prompt.js.map +1 -1
  98. package/dist/runtime/query/resume-pending.d.ts +8 -0
  99. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  100. package/dist/runtime/query/resume-pending.js +101 -0
  101. package/dist/runtime/query/resume-pending.js.map +1 -1
  102. package/dist/runtime/query/review-policy.d.ts +18 -0
  103. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  104. package/dist/runtime/query/review-policy.js +10 -0
  105. package/dist/runtime/query/review-policy.js.map +1 -1
  106. package/dist/runtime/query/steering.d.ts +55 -56
  107. package/dist/runtime/query/steering.d.ts.map +1 -1
  108. package/dist/runtime/query/steering.js +78 -72
  109. package/dist/runtime/query/steering.js.map +1 -1
  110. package/dist/store/budget/disk.js +4 -1
  111. package/dist/store/budget/disk.js.map +1 -1
  112. package/dist/tools/admit-facts.d.ts.map +1 -1
  113. package/dist/tools/admit-facts.js +6 -0
  114. package/dist/tools/admit-facts.js.map +1 -1
  115. package/dist/tools/builtins/bash.d.ts +8 -0
  116. package/dist/tools/builtins/bash.d.ts.map +1 -1
  117. package/dist/tools/builtins/bash.js +36 -27
  118. package/dist/tools/builtins/bash.js.map +1 -1
  119. package/dist/tools/builtins/browser.d.ts.map +1 -1
  120. package/dist/tools/builtins/browser.js +38 -12
  121. package/dist/tools/builtins/browser.js.map +1 -1
  122. package/dist/tools/builtins/edit-apply.d.ts +7 -4
  123. package/dist/tools/builtins/edit-apply.d.ts.map +1 -1
  124. package/dist/tools/builtins/edit-apply.js +3 -0
  125. package/dist/tools/builtins/edit-apply.js.map +1 -1
  126. package/dist/tools/builtins/edit.d.ts.map +1 -1
  127. package/dist/tools/builtins/edit.js +31 -11
  128. package/dist/tools/builtins/edit.js.map +1 -1
  129. package/dist/tools/builtins/exit-semantics.d.ts +31 -0
  130. package/dist/tools/builtins/exit-semantics.d.ts.map +1 -0
  131. package/dist/tools/builtins/exit-semantics.js +198 -0
  132. package/dist/tools/builtins/exit-semantics.js.map +1 -0
  133. package/dist/tools/builtins/job.js +1 -1
  134. package/dist/tools/builtins/job.js.map +1 -1
  135. package/dist/tools/builtins/skill.js +1 -1
  136. package/dist/tools/builtins/skill.js.map +1 -1
  137. package/dist/tools/builtins/structuredOutput.js +1 -1
  138. package/dist/tools/builtins/structuredOutput.js.map +1 -1
  139. package/dist/tools/builtins/verify-outputs.js +1 -1
  140. package/dist/tools/builtins/verify-outputs.js.map +1 -1
  141. package/dist/tools/builtins/write-file.js +11 -6
  142. package/dist/tools/builtins/write-file.js.map +1 -1
  143. package/dist/tools/coordinator/ask-user-question.d.ts +6 -0
  144. package/dist/tools/coordinator/ask-user-question.d.ts.map +1 -1
  145. package/dist/tools/coordinator/ask-user-question.js +6 -1
  146. package/dist/tools/coordinator/ask-user-question.js.map +1 -1
  147. package/dist/tools/coordinator/index.d.ts.map +1 -1
  148. package/dist/tools/coordinator/index.js +10 -3
  149. package/dist/tools/coordinator/index.js.map +1 -1
  150. package/dist/tools/memory/delete.js +1 -1
  151. package/dist/tools/memory/delete.js.map +1 -1
  152. package/dist/tools/memory/read.js +1 -1
  153. package/dist/tools/memory/read.js.map +1 -1
  154. package/dist/tools/memory/save.d.ts.map +1 -1
  155. package/dist/tools/memory/save.js +4 -3
  156. package/dist/tools/memory/save.js.map +1 -1
  157. package/dist/tools/memory/search.js +1 -1
  158. package/dist/tools/memory/search.js.map +1 -1
  159. package/dist/tools/memory/update.js +1 -1
  160. package/dist/tools/memory/update.js.map +1 -1
  161. package/dist/tools/schedules/loop-tool.d.ts.map +1 -1
  162. package/dist/tools/schedules/loop-tool.js +4 -2
  163. package/dist/tools/schedules/loop-tool.js.map +1 -1
  164. package/dist/tools/schedules/schedule-tool.js +2 -2
  165. package/dist/tools/schedules/schedule-tool.js.map +1 -1
  166. package/dist/tools/task/create.js +1 -1
  167. package/dist/tools/task/create.js.map +1 -1
  168. package/dist/tools/untrusted-envelope.js +1 -1
  169. package/dist/tools/untrusted-envelope.js.map +1 -1
  170. package/dist/toolsets/manager.d.ts.map +1 -1
  171. package/dist/toolsets/manager.js +6 -1
  172. package/dist/toolsets/manager.js.map +1 -1
  173. package/dist/turn/evidence-query.d.ts.map +1 -1
  174. package/dist/turn/evidence-query.js +16 -1
  175. package/dist/turn/evidence-query.js.map +1 -1
  176. package/dist/turn/evidence-recall.js +1 -1
  177. package/dist/turn/evidence-recall.js.map +1 -1
  178. package/dist/types/agent/base.d.ts +7 -0
  179. package/dist/types/agent/base.d.ts.map +1 -1
  180. package/dist/types/common/index.d.ts +19 -0
  181. package/dist/types/common/index.d.ts.map +1 -1
  182. package/dist/types/common/index.js +9 -0
  183. package/dist/types/common/index.js.map +1 -1
  184. package/dist/types/hitl/index.d.ts +66 -0
  185. package/dist/types/hitl/index.d.ts.map +1 -1
  186. package/dist/types/hitl/index.js.map +1 -1
  187. package/dist/types/message/index.d.ts +8 -0
  188. package/dist/types/message/index.d.ts.map +1 -1
  189. package/dist/types/message/index.js +12 -2
  190. package/dist/types/message/index.js.map +1 -1
  191. package/dist/types/message/notice-frame.d.ts +41 -0
  192. package/dist/types/message/notice-frame.d.ts.map +1 -0
  193. package/dist/types/message/notice-frame.js +65 -0
  194. package/dist/types/message/notice-frame.js.map +1 -0
  195. package/dist/types/provider/chat.d.ts +8 -0
  196. package/dist/types/provider/chat.d.ts.map +1 -1
  197. package/dist/types/provider/config.d.ts +6 -0
  198. package/dist/types/provider/config.d.ts.map +1 -1
  199. package/dist/types/session/checkpoint.d.ts +52 -0
  200. package/dist/types/session/checkpoint.d.ts.map +1 -1
  201. package/dist/types/session/checkpoint.js +5 -0
  202. package/dist/types/session/checkpoint.js.map +1 -1
  203. package/dist/types/session/events.d.ts +9 -0
  204. package/dist/types/session/events.d.ts.map +1 -1
  205. package/dist/types/session/events.js.map +1 -1
  206. package/dist/types/session/records.d.ts +181 -6
  207. package/dist/types/session/records.d.ts.map +1 -1
  208. package/dist/types/session/records.js +1 -1
  209. package/dist/types/session/records.js.map +1 -1
  210. package/dist/types/tool/presentation.d.ts +17 -1
  211. package/dist/types/tool/presentation.d.ts.map +1 -1
  212. package/package.json +1 -1
  213. package/src/agents/QueryAgent.ts +1 -0
  214. package/src/compaction/context-breakdown.ts +228 -0
  215. package/src/compaction/extractor.ts +8 -1
  216. package/src/compaction/managers/structured.ts +3 -0
  217. package/src/compaction/manual.ts +5 -0
  218. package/src/compaction/serializer.ts +28 -3
  219. package/src/compaction/token-estimate.ts +5 -0
  220. package/src/compaction/verifier.ts +32 -6
  221. package/src/constants/compaction/index.ts +1 -0
  222. package/src/prompt/coding-agent-doctrine.ts +18 -20
  223. package/src/public-runtime.ts +24 -2
  224. package/src/runtime/cache-clock.ts +67 -0
  225. package/src/runtime/query/checkpoint.ts +49 -8
  226. package/src/runtime/query/compact-session.ts +101 -3
  227. package/src/runtime/query/executor/tool-call-admission.ts +3 -0
  228. package/src/runtime/query/executor.ts +165 -15
  229. package/src/runtime/query/index.ts +45 -1
  230. package/src/runtime/query/iteration/index.ts +27 -6
  231. package/src/runtime/query/iteration/phases/context.ts +37 -11
  232. package/src/runtime/query/iteration/phases/per-call-review.ts +598 -0
  233. package/src/runtime/query/iteration/phases/tool-review.ts +168 -83
  234. package/src/runtime/query/iteration/step-context.ts +29 -0
  235. package/src/runtime/query/iteration/step-shaping.ts +6 -2
  236. package/src/runtime/query/prepare-turn.ts +18 -1
  237. package/src/runtime/query/project-instructions.ts +119 -38
  238. package/src/runtime/query/prompt.ts +11 -54
  239. package/src/runtime/query/resume-pending.ts +131 -0
  240. package/src/runtime/query/review-policy.ts +28 -0
  241. package/src/runtime/query/steering.ts +100 -96
  242. package/src/store/budget/disk.ts +4 -1
  243. package/src/tools/admit-facts.ts +6 -0
  244. package/src/tools/builtins/bash.ts +38 -28
  245. package/src/tools/builtins/browser.ts +38 -12
  246. package/src/tools/builtins/edit-apply.ts +12 -2
  247. package/src/tools/builtins/edit.ts +38 -12
  248. package/src/tools/builtins/exit-semantics.ts +198 -0
  249. package/src/tools/builtins/job.ts +1 -1
  250. package/src/tools/builtins/skill.ts +1 -1
  251. package/src/tools/builtins/structuredOutput.ts +1 -1
  252. package/src/tools/builtins/verify-outputs.ts +1 -1
  253. package/src/tools/builtins/write-file.ts +11 -6
  254. package/src/tools/coordinator/ask-user-question.ts +12 -1
  255. package/src/tools/coordinator/index.ts +10 -3
  256. package/src/tools/memory/delete.ts +1 -1
  257. package/src/tools/memory/read.ts +1 -1
  258. package/src/tools/memory/save.ts +4 -3
  259. package/src/tools/memory/search.ts +1 -1
  260. package/src/tools/memory/update.ts +1 -1
  261. package/src/tools/schedules/loop-tool.ts +4 -2
  262. package/src/tools/schedules/schedule-tool.ts +2 -2
  263. package/src/tools/task/create.ts +1 -1
  264. package/src/tools/untrusted-envelope.ts +1 -1
  265. package/src/toolsets/manager.ts +6 -1
  266. package/src/turn/evidence-query.ts +19 -1
  267. package/src/turn/evidence-recall.ts +1 -1
  268. package/src/types/agent/base.ts +8 -0
  269. package/src/types/common/index.ts +27 -0
  270. package/src/types/hitl/index.ts +67 -0
  271. package/src/types/message/index.ts +20 -2
  272. package/src/types/message/notice-frame.ts +88 -0
  273. package/src/types/provider/chat.ts +8 -0
  274. package/src/types/provider/config.ts +6 -0
  275. package/src/types/session/checkpoint.ts +5 -0
  276. package/src/types/session/events.ts +6 -0
  277. package/src/types/session/records.ts +1 -1
  278. package/src/types/tool/presentation.ts +16 -0
@@ -2,20 +2,35 @@ import { allowRuleScope } from '../../../../authorization/gate.js'
2
2
  import type { AuthorizationGate } from '../../../../authorization/index.js'
3
3
  import { isTrustedReadOnly } from '../../../../tools/trusted-read-only.js'
4
4
  import type { ToolSourceRef } from '../../../../toolsets/types.js'
5
- import type { ToolCallDeclined, ToolCallSummary } from '../../../../types/hitl/index.js'
5
+ import type {
6
+ HITLResumeDecision,
7
+ ToolCallDeclined,
8
+ ToolCallSummary,
9
+ } from '../../../../types/hitl/index.js'
6
10
  import type { ChatCompletionResponse } from '../../../../types/provider/index.js'
7
11
  import type { SessionEvent } from '../../../../types/session/index.js'
8
12
  import type { ShellDialect } from '../../../../types/tool/index.js'
9
13
  import { DECLINED_TOOL_CALL_FEEDBACK } from '../../declined.js'
10
- import type { PreparedToolBatch, ToolCallDeclines, ToolCallDenials } from '../../executor.js'
14
+ import type {
15
+ PreparedToolBatch,
16
+ ReviewedExecution,
17
+ ToolCallDeclines,
18
+ ToolCallDenials,
19
+ } from '../../executor.js'
11
20
  import {
12
21
  awaitProjectInstructionCallback,
13
22
  replaceProjectInstructionSnapshot,
14
23
  } from '../../project-instructions.js'
15
24
  import { attachRepeatNotice } from '../../repeat-call.js'
16
25
  import { isReviewExempt } from '../../review-policy.js'
17
- import { attachNotice, attachSteering, formatJobNote, formatMonitorNote } from '../../steering.js'
26
+ import {
27
+ formatJobNote,
28
+ formatMonitorNote,
29
+ takeNoticeMessage,
30
+ takeSteeringMessage,
31
+ } from '../../steering.js'
18
32
  import { type IterationContext, awaitDecisionDurably } from './context.js'
33
+ import { PerCallReview } from './per-call-review.js'
19
34
 
20
35
  interface VerificationAwareContext extends IterationContext {
21
36
  readonly verificationGate?: AuthorizationGate
@@ -197,7 +212,10 @@ export async function* runToolReview(
197
212
  let declines: ToolCallDeclines | undefined
198
213
 
199
214
  /** Executes the batch, answering every call, and appends the results. */
200
- const settleBatch = async (denials?: ToolCallDenials): Promise<void> => {
215
+ const settleBatch = async (
216
+ denials?: ToolCallDenials,
217
+ review?: ReviewedExecution,
218
+ ): Promise<void> => {
201
219
  const startedAt = Date.now()
202
220
  const batch = await ctx.toolExecutor.executeBatch(
203
221
  response,
@@ -205,6 +223,7 @@ export async function* runToolReview(
205
223
  undefined,
206
224
  preparedBatch,
207
225
  declines,
226
+ review,
208
227
  )
209
228
  toolMs += Date.now() - startedAt
210
229
  executed = batch.results
@@ -240,40 +259,42 @@ export async function* runToolReview(
240
259
  const notice = ctx.repeatCalls?.record(summary.name, summary.input)
241
260
  if (notice) notices.push(notice)
242
261
  }
243
- // Guidance the host queued while this batch was running rides out on
244
- // the last result. This is the only legal slot for it: a `tool_use`
245
- // block must be answered by a `tool_result` with the same id, so a
246
- // user message wedged between them is rejected by the provider. Same
247
- // delivery a denial already uses, without the refusal.
262
+ // What the host or the kernel queued while this batch was running reaches
263
+ // the model as messages of their own right after the batch. A tool result
264
+ // stays what the tool returned: instructions written into it read as tool
265
+ // output, and a `tool_use` block still gets its `tool_result` first.
248
266
  let jobNoticeDelivered = false
249
267
  let monitorNoticeDelivered = false
250
- for (const msg of attachRepeatNotice(
251
- attachNotice(
252
- attachNotice(
253
- attachSteering(batch.messages, ctx.steering, ctx.onSteeringDelivered),
254
- ctx.jobNotices,
255
- formatJobNote,
256
- // The exits that text accounts for have now been read, so the
257
- // record of them stops being pending work. Left standing, it
258
- // buys the model a turn the next time any job queues a notice.
259
- () => {
260
- ctx.awaitedJobs?.noticesDelivered()
261
- jobNoticeDelivered = true
262
- },
263
- ),
264
- ctx.monitorNotices,
265
- formatMonitorNote,
266
- // Same rule for what a monitor said: read here, so a turn holding
267
- // itself open for the monitor does not spend a turn re-reading it.
268
- () => {
269
- ctx.awaitedJobs?.monitorNoticesDelivered()
270
- monitorNoticeDelivered = true
271
- },
272
- ),
273
- notices,
274
- )) {
268
+ for (const msg of attachRepeatNotice(batch.messages, notices)) {
275
269
  ctx.recorder.pushMessage(msg)
276
270
  }
271
+ const steering = takeSteeringMessage(batch.messages, ctx.steering, ctx.onSteeringDelivered)
272
+ if (steering) ctx.recorder.pushMessage(steering)
273
+ const jobNotice = takeNoticeMessage(
274
+ batch.messages,
275
+ ctx.jobNotices,
276
+ formatJobNote,
277
+ // The exits that text accounts for have now been read, so the
278
+ // record of them stops being pending work. Left standing, it
279
+ // buys the model a turn the next time any job queues a notice.
280
+ () => {
281
+ ctx.awaitedJobs?.noticesDelivered()
282
+ jobNoticeDelivered = true
283
+ },
284
+ )
285
+ if (jobNotice) ctx.recorder.pushMessage(jobNotice)
286
+ const monitorNotice = takeNoticeMessage(
287
+ batch.messages,
288
+ ctx.monitorNotices,
289
+ formatMonitorNote,
290
+ // Same rule for what a monitor said: read here, so a turn holding
291
+ // itself open for the monitor does not spend a turn re-reading it.
292
+ () => {
293
+ ctx.awaitedJobs?.monitorNoticesDelivered()
294
+ monitorNoticeDelivered = true
295
+ },
296
+ )
297
+ if (monitorNotice) ctx.recorder.pushMessage(monitorNotice)
277
298
  if (jobNoticeDelivered) ctx.onJobNoticeDelivered?.()
278
299
  if (monitorNoticeDelivered) ctx.onMonitorNoticeDelivered?.()
279
300
  // The complete tool-result batch is already in history before host policy
@@ -305,9 +326,10 @@ export async function* runToolReview(
305
326
  /** Stream recorded starts and progress while execution itself remains pending. */
306
327
  const settle = async function* (
307
328
  denials?: ToolCallDenials,
329
+ review?: ReviewedExecution,
308
330
  ): AsyncGenerator<SessionEvent, void, unknown> {
309
331
  if (!ctx.onPendingEvents) {
310
- await settleBatch(denials)
332
+ await settleBatch(denials, review)
311
333
  return
312
334
  }
313
335
  const wakeup = () => {
@@ -322,7 +344,7 @@ export async function* runToolReview(
322
344
  const unsubscribe = ctx.onPendingEvents(() => wake.notify())
323
345
  // Start the captured batch before yielding. Live observation must not
324
346
  // create a new mutation window before execution validates its payload.
325
- const execution = settleBatch(denials).then(
347
+ const execution = settleBatch(denials, review).then(
326
348
  () => {
327
349
  settled = { ok: true }
328
350
  wake.notify()
@@ -711,24 +733,6 @@ export async function* runToolReview(
711
733
  }
712
734
  }
713
735
 
714
- const reviewCheckpoint = await ctx.checkpointMgr.create(ctx.recorder, iterationNum)
715
-
716
- await ctx.emitEvent({
717
- type: 'tool_review_requested',
718
- turnId: ctx.recorder.turnId,
719
- toolCalls: toolCallSummaries,
720
- iteration: iterationNum,
721
- })
722
- yield* ctx.drainPending()
723
-
724
- const reviewDecision = await awaitDecisionDurably(ctx, reviewCheckpoint, {
725
- type: 'tool_review',
726
- sessionId: ctx.recorder.sessionId,
727
- turnId: ctx.recorder.turnId,
728
- checkpointId: reviewCheckpoint.id,
729
- toolCalls: toolCallSummaries,
730
- })
731
-
732
736
  /**
733
737
  * The escalations this decision lets through, on the record, and the
734
738
  * escapes it did not confirm, refused.
@@ -742,8 +746,9 @@ export async function* runToolReview(
742
746
  const settleEscalations = async (
743
747
  denials: Map<string, string>,
744
748
  confirmed: readonly string[] | undefined,
749
+ only?: readonly ToolCallSummary[],
745
750
  ): Promise<void> => {
746
- for (const tc of escalated()) {
751
+ for (const tc of only ? only.filter((call) => call.escalation !== undefined) : escalated()) {
747
752
  const denied = denials.get(tc.id)
748
753
  if (denied !== undefined) {
749
754
  // A crossing that was asked about and refused is on the record
@@ -825,6 +830,105 @@ export async function* runToolReview(
825
830
  }
826
831
  }
827
832
 
833
+ /**
834
+ * The record of calls approved without anyone being asked: by a skill's
835
+ * `allowed-tools`, or by a `pre_tool_use` hook's `allow`. Only a call that
836
+ * carried the mark counts: a policy that lists an unmarked id has not been
837
+ * given a skill's or a hook's word for it.
838
+ */
839
+ const recordGrantAudits = async (
840
+ decision: HITLResumeDecision,
841
+ calls: readonly ToolCallSummary[],
842
+ ): Promise<void> => {
843
+ if (decision.action !== 'approve_tools') return
844
+ if (decision.skillGranted) {
845
+ const listed = new Set(decision.skillGranted)
846
+ for (const tc of calls) {
847
+ if (!listed.has(tc.id) || !tc.skillGrant || gateDenied.has(tc.id)) continue
848
+ await ctx.recorder.recordAudit({
849
+ what: { action: 'tool_call', tool: tc.name },
850
+ outcome: 'approved',
851
+ reason: `pre-approved by the allowed-tools of skill "${tc.skillGrant.skill}" for this turn; nobody was asked`,
852
+ })
853
+ }
854
+ }
855
+ if (decision.hookGranted) {
856
+ const listed = new Set(decision.hookGranted)
857
+ for (const tc of calls) {
858
+ if (!listed.has(tc.id) || !tc.hookGrant || gateDenied.has(tc.id)) continue
859
+ await ctx.recorder.recordAudit({
860
+ what: { action: 'tool_call', tool: tc.name },
861
+ outcome: 'approved',
862
+ reason: `pre-approved by a pre_tool_use hook${tc.hookGrant.reason ? ` (${tc.hookGrant.reason})` : ''}; nobody was asked`,
863
+ })
864
+ }
865
+ }
866
+ }
867
+
868
+ // Opt-in: ask about one call at a time while the batch runs. Everything
869
+ // above has already run on the batch as a whole, because those checks are
870
+ // about the calls and not about who answers.
871
+ if (ctx.perCallReview) {
872
+ const reviewedIds = new Set(toolCallSummaries.map((tc) => tc.id))
873
+ const walk = new PerCallReview({
874
+ ctx,
875
+ iteration: iterationNum,
876
+ toolCalls,
877
+ summaries: () => toolCallSummaries,
878
+ gateDenied,
879
+ reviewAllowed,
880
+ unreviewedIds: new Set(
881
+ preparedBatch ? toolCalls.filter((tc) => !reviewedIds.has(tc.id)).map((tc) => tc.id) : [],
882
+ ),
883
+ settleEscalations,
884
+ recordGrantAudits,
885
+ evaluateGate: (tc) =>
886
+ ctx.verificationGate?.evaluate({
887
+ toolName: tc.name,
888
+ toolInput: tc.input,
889
+ toolDef: ctx.tools.get(tc.name),
890
+ ...dialectFor(tc.name),
891
+ ...sourceFor(tc.name),
892
+ }).decision,
893
+ })
894
+ try {
895
+ await walk.askTogetherFirst()
896
+ const denials = new Map([...gateDenied, ...walk.denials])
897
+ if (walk.declines.size > 0) declines = walk.declines
898
+ yield* settle(denials, walk)
899
+ yield* ctx.drainPending()
900
+ if (walk.endsTurn) {
901
+ // Every call is answered by now. The turn stops the way an
902
+ // interrupt stops it, so nothing downstream reads the stop as a
903
+ // result the model should react to.
904
+ ctx.recorder.setStopReason('cancelled')
905
+ ctx.recorder.markCancelled()
906
+ return finish('stop')
907
+ }
908
+ return finish(denials.size + walk.refusedCount >= toolCalls.length ? 'rejected' : 'executed')
909
+ } finally {
910
+ walk.dispose()
911
+ }
912
+ }
913
+
914
+ const reviewCheckpoint = await ctx.checkpointMgr.create(ctx.recorder, iterationNum)
915
+
916
+ await ctx.emitEvent({
917
+ type: 'tool_review_requested',
918
+ turnId: ctx.recorder.turnId,
919
+ toolCalls: toolCallSummaries,
920
+ iteration: iterationNum,
921
+ })
922
+ yield* ctx.drainPending()
923
+
924
+ const reviewDecision = await awaitDecisionDurably(ctx, reviewCheckpoint, {
925
+ type: 'tool_review',
926
+ sessionId: ctx.recorder.sessionId,
927
+ turnId: ctx.recorder.turnId,
928
+ checkpointId: reviewCheckpoint.id,
929
+ toolCalls: toolCallSummaries,
930
+ })
931
+
828
932
  switch (reviewDecision.action) {
829
933
  case 'reject_tools': {
830
934
  await ctx.emitEvent({
@@ -846,6 +950,12 @@ export async function* runToolReview(
846
950
  await settleEscalations(denials, undefined)
847
951
  yield* settle(denials)
848
952
  yield* ctx.drainPending()
953
+ if (reviewDecision.endTurn) {
954
+ // Every call is answered above; the turn then stops as an interrupt does.
955
+ ctx.recorder.setStopReason('cancelled')
956
+ ctx.recorder.markCancelled()
957
+ return finish('stop')
958
+ }
849
959
  return finish('rejected')
850
960
  }
851
961
 
@@ -961,34 +1071,9 @@ export async function* runToolReview(
961
1071
  if (reviewDecision.action === 'approve_tools' && reviewDecision.remember) {
962
1072
  ctx.toolGrants?.grant(reviewDecision.remember)
963
1073
  }
964
- // A call nobody was asked about, approved because a skill said so,
965
- // is on the record naming that skill. Only a call that carried the
966
- // mark counts: a policy that lists an unmarked id has not been
967
- // given a skill's word for it.
968
- if (reviewDecision.action === 'approve_tools' && reviewDecision.skillGranted) {
969
- const listed = new Set(reviewDecision.skillGranted)
970
- for (const tc of toolCallSummaries) {
971
- if (!listed.has(tc.id) || !tc.skillGrant || gateDenied.has(tc.id)) continue
972
- await ctx.recorder.recordAudit({
973
- what: { action: 'tool_call', tool: tc.name },
974
- outcome: 'approved',
975
- reason: `pre-approved by the allowed-tools of skill "${tc.skillGrant.skill}" for this turn; nobody was asked`,
976
- })
977
- }
978
- }
979
-
980
- // The same for a call a hook's `allow` approved.
981
- if (reviewDecision.action === 'approve_tools' && reviewDecision.hookGranted) {
982
- const listed = new Set(reviewDecision.hookGranted)
983
- for (const tc of toolCallSummaries) {
984
- if (!listed.has(tc.id) || !tc.hookGrant || gateDenied.has(tc.id)) continue
985
- await ctx.recorder.recordAudit({
986
- what: { action: 'tool_call', tool: tc.name },
987
- outcome: 'approved',
988
- reason: `pre-approved by a pre_tool_use hook${tc.hookGrant.reason ? ` (${tc.hookGrant.reason})` : ''}; nobody was asked`,
989
- })
990
- }
991
- }
1074
+ // A call nobody was asked about, approved because a skill or a hook
1075
+ // said so, is on the record naming which.
1076
+ await recordGrantAudits(reviewDecision, toolCallSummaries)
992
1077
 
993
1078
  await ctx.emitEvent({
994
1079
  type: 'tool_review_completed',
@@ -1,4 +1,6 @@
1
1
  import { createRuntimeContextMessage } from '../../../types/message/index.js'
2
+ import type { Message } from '../../../types/message/index.js'
3
+ import { isOperatorUserMessage } from '../steering.js'
2
4
 
3
5
  /**
4
6
  * Request-only context, as the model reads it: a runtime-context user
@@ -15,3 +17,30 @@ export function stepContextMessage(content: string) {
15
17
  'step-context',
16
18
  )
17
19
  }
20
+
21
+ /**
22
+ * Put `blocks` in front of the operator's new message when that message is
23
+ * what the model is about to answer, so the request is the last thing it
24
+ * reads and the longest data comes before it. Only the reminders and other
25
+ * request-only context may follow that message: once the turn has an
26
+ * assistant or tool message after it, the blocks stay at the end of the
27
+ * history, where a caching driver ends its breakpoint before them, so the
28
+ * tool loop of a long turn is not re-read at full price on every step.
29
+ *
30
+ * Reminders and notices that follow the operator's message stay where they
31
+ * are: they are short, and the edge after the request is where they are read.
32
+ */
33
+ export function placeBeforeLatestOperatorMessage(history: Message[], blocks: readonly Message[]) {
34
+ if (blocks.length === 0) return
35
+ let at = history.length
36
+ for (let i = history.length - 1; i >= 0; i--) {
37
+ const message = history[i] as Message
38
+ if (isOperatorUserMessage(message)) {
39
+ at = i
40
+ break
41
+ }
42
+ // A reply or a tool result after the message: the model has moved on.
43
+ if (message.role === 'assistant' || message.role === 'tool') break
44
+ }
45
+ history.splice(at, 0, ...blocks)
46
+ }
@@ -20,7 +20,7 @@ import { toErrorMessage } from '../../../utils/error.js'
20
20
  import { createCallbackInference } from '../callback-inference.js'
21
21
  import { measureContext } from './phases/compaction.js'
22
22
  import type { IterationContext } from './phases/index.js'
23
- import { stepContextMessage } from './step-context.js'
23
+ import { placeBeforeLatestOperatorMessage, stepContextMessage } from './step-context.js'
24
24
 
25
25
  /**
26
26
  * How the next request is shaped: admission, preparation, and the context
@@ -58,6 +58,7 @@ export function appendWorkContext(
58
58
  ctx.toolExecutor.describeFileEvidence(messages),
59
59
  ].filter((content): content is string => Boolean(content))
60
60
  if (contributions.length === 0) return
61
+ const placed: Message[] = []
61
62
  let room = stepContext(shaping, stepNumber, prepared).contextBudget?.remainingTokens ?? 0
62
63
  // Leave room for the actual task; admit whole contributions, never dangling partial references.
63
64
  if (room < 1_500) return
@@ -66,9 +67,12 @@ export function appendWorkContext(
66
67
  const message = stepContextMessage(content)
67
68
  const tokens = estimateMessageTokens(message)
68
69
  if (tokens > Math.min(2_000, room - 1_000)) continue
69
- messages.push(message)
70
+ placed.push(message)
70
71
  room -= tokens
71
72
  }
73
+ // Per-turn data goes in front of the operator's new message while that is
74
+ // what the model is answering, like the other blocks of request-only data.
75
+ placeBeforeLatestOperatorMessage(messages, placed)
72
76
  }
73
77
 
74
78
  export function stepContext(
@@ -53,7 +53,9 @@ import {
53
53
  awaitProjectInstructionCallback,
54
54
  collapseProjectInstructionSnapshots,
55
55
  isProjectInstructionMessage,
56
+ latestProjectInstructionSnapshot,
56
57
  replaceProjectInstructionSnapshot,
58
+ sameProjectInstructionSnapshot,
57
59
  } from './project-instructions.js'
58
60
  import type { PromptCache } from './prompt-cache.js'
59
61
  import { resolveMaxRequestRichContentBytes } from './request-rich-content.js'
@@ -640,11 +642,26 @@ export async function prepareTurn(params: QueryParams): Promise<PreparedTurn> {
640
642
  // be erased merely because cancellation arrived before this catch ran.
641
643
  if (!preparationSignal.aborted || error !== preparationSignal.reason) throw error
642
644
  }
643
- if (snapshot !== undefined) {
645
+ // The snapshot a conversation already holds stays where it is. Moving an
646
+ // unchanged copy to this turn's front would rewrite everything after its
647
+ // old position on every request, so the provider's cache would reuse
648
+ // almost none of the history. Only a changed snapshot is sent again, at
649
+ // the tail just before the new message, and it replaces the old copy.
650
+ const standing = latestProjectInstructionSnapshot([
651
+ ...history.map((entry) => entry.message),
652
+ ...resolvedInitialMessages,
653
+ ])
654
+ const unchanged =
655
+ snapshot !== undefined &&
656
+ snapshot !== null &&
657
+ standing !== undefined &&
658
+ sameProjectInstructionSnapshot(standing, snapshot)
659
+ if (snapshot !== undefined && !unchanged) {
644
660
  resolvedInitialMessages = replaceProjectInstructionSnapshot(
645
661
  resolvedInitialMessages,
646
662
  snapshot,
647
663
  'before-latest-user',
664
+ history.map((entry) => entry.message),
648
665
  )
649
666
  }
650
667
  }
@@ -126,10 +126,31 @@ function assertsProjectInstructionMessage(message: UserMessage): asserts message
126
126
  }
127
127
 
128
128
  /**
129
- * Keep only the latest structurally valid project snapshot, at its own
130
- * chronological position. Invalid tagged provenance refuses before a provider
131
- * can be called; an untrusted persisted object cannot acquire policy status by
132
- * spelling one discriminator.
129
+ * The line that opens a changed copy of the project instructions. The copy is
130
+ * complete, so the model can drop the earlier one outright.
131
+ */
132
+ export const PROJECT_INSTRUCTION_UPDATE_HEADER =
133
+ 'Updated project instructions. These replace the project instructions given earlier in this conversation in full; a rule that appears only in the earlier copy no longer applies.\n\n'
134
+
135
+ function isUpdate(message: UserMessage): boolean {
136
+ return message.source?.type === 'project-instructions' && message.source.update === true
137
+ }
138
+
139
+ /** The policy text of a snapshot, without the header an update carries. */
140
+ export function projectInstructionBody(message: UserMessage): string {
141
+ const content = typeof message.content === 'string' ? message.content : ''
142
+ return isUpdate(message) && content.startsWith(PROJECT_INSTRUCTION_UPDATE_HEADER)
143
+ ? content.slice(PROJECT_INSTRUCTION_UPDATE_HEADER.length)
144
+ : content
145
+ }
146
+
147
+ /**
148
+ * Keep the project policy a request needs, each at its own chronological
149
+ * position. A plain snapshot supersedes every earlier one. A changed copy
150
+ * (`source.update`) supersedes the copies before it but leaves the first one
151
+ * in place, so the request keeps its shape up to the change. Invalid tagged
152
+ * provenance refuses before a provider can be called; an untrusted persisted
153
+ * object cannot acquire policy status by spelling one discriminator.
133
154
  */
134
155
  export function collapseProjectInstructionSnapshots(messages: readonly Message[]): Message[] {
135
156
  let latest = -1
@@ -140,9 +161,21 @@ export function collapseProjectInstructionSnapshots(messages: readonly Message[]
140
161
  latest = index
141
162
  }
142
163
  if (latest < 0) return [...messages]
164
+ const latestMessage = messages[latest] as UserMessage
165
+ // The base is the earliest copy, kept only while the latest is an update of it.
166
+ let base = -1
167
+ if (isUpdate(latestMessage)) {
168
+ for (let index = 0; index < latest; index += 1) {
169
+ const message = messages[index]
170
+ if (message && isProjectInstructionMessage(message)) {
171
+ base = index
172
+ break
173
+ }
174
+ }
175
+ }
143
176
  return messages.flatMap((message, index) => {
144
177
  if (message.role === 'user' && message.source?.type === 'project-instructions') {
145
- return index === latest
178
+ return index === latest || index === base
146
179
  ? [message.retain === true ? message : { ...message, retain: true }]
147
180
  : []
148
181
  }
@@ -150,9 +183,42 @@ export function collapseProjectInstructionSnapshots(messages: readonly Message[]
150
183
  })
151
184
  }
152
185
 
186
+ function canonical(message: UserMessage): unknown {
187
+ // Through JSON, as the durable record went: an explicitly undefined field and
188
+ // a missing one are the same message.
189
+ return JSON.parse(
190
+ JSON.stringify({
191
+ content: projectInstructionBody(message),
192
+ attachments: message.attachments,
193
+ source: message.source ? { ...message.source, update: undefined } : message.source,
194
+ cacheHint: message.cacheHint,
195
+ }),
196
+ )
197
+ }
198
+
199
+ /** Whether two snapshots say the same thing, ignoring ids, timestamps and `retain`. */
200
+ export function sameProjectInstructionSnapshot(a: UserMessage, b: UserMessage): boolean {
201
+ return isDeepStrictEqual(canonical(a), canonical(b))
202
+ }
203
+
204
+ /** The latest project-instruction snapshot in `messages`, if there is one. */
205
+ export function latestProjectInstructionSnapshot(
206
+ messages: readonly Message[],
207
+ ): UserMessage | undefined {
208
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
209
+ const message = messages[index]
210
+ if (message && isProjectInstructionMessage(message)) return message
211
+ }
212
+ return undefined
213
+ }
214
+
153
215
  /**
154
216
  * Replace project policy without mutating the caller's history.
155
217
  *
218
+ * The first copy a conversation holds keeps its place. A changed snapshot is
219
+ * sent as a complete update at the tail (`source.update`), which replaces the
220
+ * earlier updates and tells the model it supersedes the first copy; the
221
+ * request is unchanged up to the update, so a provider's cache still covers it.
156
222
  * Runtime updates append after the finalized tool batch. A host seeding a new
157
223
  * human turn uses `before-latest-user`, so the policy is context for that
158
224
  * request rather than a reply after it.
@@ -161,53 +227,68 @@ export function replaceProjectInstructionSnapshot(
161
227
  messages: readonly Message[],
162
228
  snapshot: UserMessage | null,
163
229
  placement: 'append' | 'before-latest-user' = 'append',
230
+ /** Messages the conversation already holds ahead of `messages`; read for the first copy, never changed. */
231
+ held: readonly Message[] = [],
164
232
  ): Message[] {
165
233
  if (snapshot) assertsProjectInstructionMessage(snapshot)
166
234
  // Validate every tagged predecessor before removing it. A host-authoritative
167
235
  // replacement must not turn malformed persisted provenance into a silent
168
236
  // success merely because the bad record happens to be superseded.
169
237
  const validated = collapseProjectInstructionSnapshots(messages)
170
- let current: UserMessage | undefined
171
- for (let index = validated.length - 1; index >= 0; index -= 1) {
172
- const message = validated[index]
173
- if (message && isProjectInstructionMessage(message)) {
174
- current = message
175
- break
176
- }
177
- }
238
+ const all = collapseProjectInstructionSnapshots([...held, ...messages])
239
+ const current = latestProjectInstructionSnapshot(all)
178
240
  if (
179
241
  snapshot !== null &&
180
242
  current !== undefined &&
181
- isDeepStrictEqual(
182
- {
183
- content: current.content,
184
- attachments: current.attachments,
185
- source: current.source,
186
- cacheHint: current.cacheHint,
187
- },
188
- {
189
- content: snapshot.content,
190
- attachments: snapshot.attachments,
191
- source: snapshot.source,
192
- cacheHint: snapshot.cacheHint,
193
- },
194
- )
243
+ sameProjectInstructionSnapshot(current, snapshot)
195
244
  ) {
196
245
  return validated
197
246
  }
198
247
  if (snapshot === null && current === undefined) return validated
199
- const without = validated.filter(
200
- (message) => !(message.role === 'user' && message.source?.type === 'project-instructions'),
201
- )
248
+ const isPolicy = (message: Message): boolean =>
249
+ message.role === 'user' && message.source?.type === 'project-instructions'
250
+ const without = validated.filter((message) => !isPolicy(message))
202
251
  if (!snapshot) return without
203
- const retained = snapshot.retain === true ? snapshot : { ...snapshot, retain: true }
204
- if (placement === 'append') return [...without, retained]
205
- let insertion = without.length
206
- for (let index = without.length - 1; index >= 0; index -= 1) {
207
- if (without[index]?.role === 'user') {
208
- insertion = index
209
- break
252
+ const place = (list: readonly Message[], message: UserMessage): Message[] => {
253
+ if (placement === 'append') return [...list, message]
254
+ let insertion = list.length
255
+ for (let index = list.length - 1; index >= 0; index -= 1) {
256
+ if (list[index]?.role === 'user') {
257
+ insertion = index
258
+ break
259
+ }
210
260
  }
261
+ return [...list.slice(0, insertion), message, ...list.slice(insertion)]
262
+ }
263
+ const base = all.find(
264
+ (message): message is UserMessage => isProjectInstructionMessage(message) && !isUpdate(message),
265
+ )
266
+ const body = projectInstructionBody(snapshot)
267
+ if (base === undefined) {
268
+ const first: UserMessage =
269
+ snapshot.retain === true && !isUpdate(snapshot)
270
+ ? snapshot
271
+ : {
272
+ ...snapshot,
273
+ content: body,
274
+ source: {
275
+ type: 'project-instructions',
276
+ files: snapshot.source.files,
277
+ },
278
+ retain: true,
279
+ }
280
+ return place(without, first)
281
+ }
282
+ const update: UserMessage = {
283
+ ...snapshot,
284
+ content: PROJECT_INSTRUCTION_UPDATE_HEADER + body,
285
+ source: {
286
+ type: 'project-instructions',
287
+ files: snapshot.source.files,
288
+ update: true,
289
+ },
290
+ retain: true,
211
291
  }
212
- return [...without.slice(0, insertion), retained, ...without.slice(insertion)]
292
+ const kept = validated.filter((message) => !isPolicy(message) || message === base)
293
+ return place(kept, update)
213
294
  }