@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
@@ -150,6 +150,8 @@ function buildShellContext(config: PromptBuilderConfig): string | undefined {
150
150
  return lines.length > 0 ? lines.join('\n') : undefined
151
151
  }
152
152
 
153
+ const SEGMENT_SEPARATOR = '\n\n---\n\n'
154
+
153
155
  export class PromptBuilder {
154
156
  private config: PromptBuilderConfig
155
157
 
@@ -232,67 +234,22 @@ export class PromptBuilder {
232
234
  )
233
235
  }
234
236
 
237
+ /**
238
+ * The whole prompt as one string: exactly the two segments a request
239
+ * sends, static then dynamic, joined by the separator. It is derived from
240
+ * `buildSegmented` rather than assembled a second time, so the order the
241
+ * model reads and the text the prompt-cache hash covers cannot drift apart.
242
+ */
235
243
  build(contextLevel: AgentContextLevel = 'full', workingDirectory?: string): string {
236
- const parts: string[] = []
237
-
238
- if (contextLevel === 'full' && this.config.basePrompt) {
239
- parts.push(this.config.basePrompt)
240
- }
241
-
242
- if (this.config.systemPrompt) {
243
- parts.push(this.config.systemPrompt)
244
- } else if (this.config.persona) {
245
- parts.push(assembleSystemPrompt(this.config.persona, this.config.skills))
246
- } else {
247
- const skillSection = this.renderSkillsInPlace()
248
- if (skillSection) parts.push(skillSection)
249
- }
250
-
251
- if (this.config.systemPrompt) {
252
- const skillSection = this.renderSkillsInPlace()
253
- if (skillSection) parts.push(skillSection)
254
- }
255
-
256
- if (contextLevel !== 'minimal') {
257
- const toolSection = this.config.tools.toPromptSection(this.config.allowedTools)
258
- if (toolSection) {
259
- parts.push(toolSection)
260
- }
261
-
262
- const tierGuidance = this.config.tools.toTierGuidance()
263
- if (tierGuidance) {
264
- parts.push(tierGuidance)
265
- }
266
- }
267
-
268
- if (contextLevel !== 'minimal' && workingDirectory) {
269
- const shouldIncludeEnv =
270
- hasFilesystemTools(this.config.tools, this.config.allowedTools) ||
271
- Boolean(this.config.runtimeContext)
272
- if (shouldIncludeEnv) {
273
- parts.push(buildEnvContext(workingDirectory, this.config))
274
- }
275
- }
276
-
277
- // Static then dynamic, in that order and nothing between them,
278
- // because that is exactly how `buildSegmented` is rejoined upstream:
279
- // `${static}\n\n---\n\n${dynamic}`. The two methods produce the same
280
- // prompt for the same input, and a turn that hits the prompt cache
281
- // must not be asking a different question from one that misses it.
282
- const shellContext = buildShellContext(this.config)
283
- if (shellContext) parts.push(shellContext)
284
-
285
- parts.push(...this.renderContributions('static', workingDirectory))
286
- parts.push(...this.renderContributions('dynamic', workingDirectory))
287
-
288
- return parts.join('\n\n---\n\n')
244
+ const { static: staticText, dynamic } = this.buildSegmented(contextLevel, workingDirectory)
245
+ return [staticText, dynamic].filter((part) => part.length > 0).join(SEGMENT_SEPARATOR)
289
246
  }
290
247
 
291
248
  buildSegmented(
292
249
  contextLevel: AgentContextLevel = 'full',
293
250
  workingDirectory?: string,
294
251
  ): PromptSegments {
295
- const separator = '\n\n---\n\n'
252
+ const separator = SEGMENT_SEPARATOR
296
253
  const staticParts: string[] = []
297
254
  const dynamicParts: string[] = []
298
255
 
@@ -75,6 +75,12 @@ export interface PendingResumePlan {
75
75
  readonly modifiedCallIds?: ReadonlySet<string>
76
76
  /** Calls whose sandbox escape the durable decision confirmed; see `HITLResumeDecision`. */
77
77
  readonly confirmedEscalations?: readonly string[]
78
+ /**
79
+ * The park asked about some calls of the batch (the per-call review), so the
80
+ * decision speaks for those and no others. Every other call that has no
81
+ * recovered result is not run; see {@link applyPendingResume}.
82
+ */
83
+ readonly partial?: true
78
84
  /**
79
85
  * Answers to deliver to tools that parked on a question, keyed by the
80
86
  * asking call's id. Present only on a question resume.
@@ -136,6 +142,9 @@ export function planPendingResume(
136
142
  // describe this state and applying it would be consent-by-coincidence.
137
143
  const recordedIds = new Set(pending.request.toolCalls.map((tc) => tc.id))
138
144
  const actualIds = assistant.toolCalls.map((tc) => tc.id)
145
+ if (pending.request.perCall === true) {
146
+ return planPartialResume(checkpoint, pending.request.toolCalls, assistant, decision, log)
147
+ }
139
148
  const mismatched = actualIds.filter((id) => !recordedIds.has(id))
140
149
  if (mismatched.length > 0 || recordedIds.size !== actualIds.length) {
141
150
  log.error('Tool calls in the checkpoint do not match the ones the decision was made about', {
@@ -166,6 +175,118 @@ export function planPendingResume(
166
175
  }
167
176
  }
168
177
 
178
+ /** What a call is told when it had not run, and nobody had answered it, when the session stopped. */
179
+ export const NOT_RUN_BEFORE_RESTART =
180
+ 'This call had not run when the session stopped, and an earlier approval does not carry over. Ask again if it is still needed.'
181
+
182
+ /**
183
+ * Resume a park that asked about some calls of a batch, not all of them.
184
+ *
185
+ * The decision speaks for the calls it was shown, and only those: the id check
186
+ * is that every one of them is still an unanswered call of the batch. Calls
187
+ * that already ran are answered from the record when the plan is applied, and
188
+ * a call that had not run and was not asked about is not run: a yes given to
189
+ * one call is not a yes to another.
190
+ */
191
+ function planPartialResume(
192
+ checkpoint: RestoredCheckpoint,
193
+ shown: readonly ToolCallSummary[],
194
+ assistant: AssistantMessage,
195
+ decision: HITLResumeDecision,
196
+ log: Logger,
197
+ ): PendingResumePlan | null {
198
+ const calls = assistant.toolCalls ?? []
199
+ const byId = new Map(calls.map((call) => [call.id, call]))
200
+ const shownCalls = shown.map((summary) => byId.get(summary.id))
201
+ if (shown.length === 0 || shownCalls.some((call) => call === undefined)) {
202
+ log.error('The call a decision was made about is not among the unanswered tool calls', {
203
+ 'namzu.checkpoint.id': checkpoint.id,
204
+ 'namzu.runtime.recorded': shown.map((summary) => summary.id),
205
+ 'namzu.runtime.actual': calls.map((call) => call.id),
206
+ })
207
+ return null
208
+ }
209
+ const asked = shownCalls as ToolCall[]
210
+ const denials = derivePartialDenials(asked, decision)
211
+ if (!denials) return null
212
+ const declines = derivePartialDeclines(asked, decision, shown)
213
+ return {
214
+ source: 'decision',
215
+ checkpointId: checkpoint.id,
216
+ assistant,
217
+ response: synthesizeResponse(assistant),
218
+ denials,
219
+ ...(declines ? { declines } : {}),
220
+ reviewedCalls: shown,
221
+ modifiedCallIds: new Set(),
222
+ partial: true,
223
+ ...((decision.action === 'approve_tools' || decision.action === 'modify_tools') &&
224
+ decision.confirmedEscalations
225
+ ? { confirmedEscalations: decision.confirmedEscalations }
226
+ : {}),
227
+ }
228
+ }
229
+
230
+ /**
231
+ * The same reading of a decision the live per-call review makes: nothing the
232
+ * decision does not name is approved, and an edit is not offered.
233
+ */
234
+ function derivePartialDenials(
235
+ asked: readonly ToolCall[],
236
+ decision: HITLResumeDecision,
237
+ ): ToolCallDenials | null {
238
+ switch (decision.action) {
239
+ case 'approve_tools':
240
+ case 'continue':
241
+ return new Map()
242
+ case 'reject_tools': {
243
+ const reason = decision.feedback || DECLINED_TOOL_CALL_FEEDBACK
244
+ return new Map(asked.map((call) => [call.id, reason]))
245
+ }
246
+ case 'modify_tools': {
247
+ const denials = new Map<string, string>()
248
+ for (const call of asked) {
249
+ const mods = decision.modifications.filter((mod) => mod.toolCallId === call.id)
250
+ if (mods.some((mod) => mod.action === 'deny'))
251
+ denials.set(call.id, DECLINED_TOOL_CALL_FEEDBACK)
252
+ else if (!mods.some((mod) => mod.action === 'approve')) {
253
+ denials.set(
254
+ call.id,
255
+ 'No answer was given for this call, so it was not run. Ask again if it is still needed.',
256
+ )
257
+ }
258
+ }
259
+ return denials
260
+ }
261
+ default:
262
+ return null
263
+ }
264
+ }
265
+
266
+ function derivePartialDeclines(
267
+ asked: readonly ToolCall[],
268
+ decision: HITLResumeDecision,
269
+ shown: readonly ToolCallSummary[],
270
+ ): ToolCallDeclines | undefined {
271
+ const gateDenied = new Set(
272
+ shown.filter((call) => call.authorization?.decision === 'deny').map((call) => call.id),
273
+ )
274
+ if (decision.action === 'reject_tools' && decision.declined) {
275
+ const declined = decision.declined
276
+ const calls = asked.filter((call) => !gateDenied.has(call.id))
277
+ return calls.length > 0 ? new Map(calls.map((call) => [call.id, declined])) : undefined
278
+ }
279
+ if (decision.action === 'modify_tools') {
280
+ const denied = asked.filter(
281
+ (call) =>
282
+ !gateDenied.has(call.id) &&
283
+ decision.modifications.some((mod) => mod.toolCallId === call.id && mod.action === 'deny'),
284
+ )
285
+ if (denied.length > 0) return new Map(denied.map((call) => [call.id, {}]))
286
+ }
287
+ return undefined
288
+ }
289
+
169
290
  /**
170
291
  * The stable marker `supersededByRecovery` puts at the head of its reason.
171
292
  *
@@ -432,6 +553,16 @@ export async function applyPendingResume(
432
553
  const denials = new Map(plan.denials)
433
554
  const reviewedById = new Map(plan.reviewedCalls?.map((call) => [call.id, call]))
434
555
 
556
+ // A decision about some calls is not a decision about the rest. A call that
557
+ // already ran is answered from the record (`prior`); one that had not run and
558
+ // was not asked about is not run, and the model is told so.
559
+ if (plan.partial) {
560
+ for (const call of plan.response.message.toolCalls ?? []) {
561
+ if (prior?.has(call.id) || reviewedById.has(call.id) || denials.has(call.id)) continue
562
+ denials.set(call.id, NOT_RUN_BEFORE_RESTART)
563
+ }
564
+ }
565
+
435
566
  // A gate denial belongs to the turn, not to the process that first evaluated
436
567
  // it. Restore it before preparation so denied calls do not even reach a
437
568
  // pre-tool hook after restart.
@@ -664,6 +664,18 @@ export interface ToolReviewRequest {
664
664
  * in the session run without asking.
665
665
  */
666
666
  readonly screenConsent?: true
667
+ /**
668
+ * Set when the request is the per-call review's question about one call (or
669
+ * the few asked together), not the whole batch. See `PerCallReviewConfig`.
670
+ */
671
+ readonly perCall?: true
672
+ /**
673
+ * Aborts when the question stops being open: the turn was cancelled, a
674
+ * rule or the mode now covers the call, or an earlier refusal ended the
675
+ * turn. A host that shows the question closes it then; its answer is
676
+ * ignored. Present only with `perCall`.
677
+ */
678
+ readonly signal?: AbortSignal
667
679
  }
668
680
 
669
681
  export type ToolReviewAnswer =
@@ -680,6 +692,12 @@ export type ToolReviewAnswer =
680
692
  * cannot carry this. `note` is the person's own words.
681
693
  */
682
694
  readonly declined?: ToolCallDeclined
695
+ /**
696
+ * The refusal also ends the turn: see `endTurn` on the `reject_tools`
697
+ * decision. A plain No sets it; a No that carries the person's reason
698
+ * leaves it out so the model reads the reason and carries on.
699
+ */
700
+ readonly endTurn?: true
683
701
  }
684
702
 
685
703
  /** How a host asks a person. The one thing the kernel cannot supply. */
@@ -955,6 +973,8 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
955
973
  turnId: request.turnId,
956
974
  toolCalls: request.toolCalls,
957
975
  ...(screenConsent ? { screenConsent } : {}),
976
+ ...(request.perCall ? { perCall: true as const } : {}),
977
+ ...(request.perCall && context?.signal ? { signal: context.signal } : {}),
958
978
  })
959
979
  // The first look at the screen in a session is the operator's to allow:
960
980
  // what is on it goes to the model provider, and a screenshot reads as
@@ -982,6 +1002,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
982
1002
  action: 'reject_tools',
983
1003
  feedback: answer.feedback ?? SCREEN_CONSENT_DECLINED_FEEDBACK,
984
1004
  ...(answer.declined ? { declined: answer.declined } : {}),
1005
+ ...(answer.endTurn ? { endTurn: true as const } : {}),
985
1006
  }
986
1007
  }
987
1008
  consent.sessions.add(sessionKey)
@@ -1038,6 +1059,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
1038
1059
  action: 'reject_tools',
1039
1060
  feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
1040
1061
  ...(answer.declined ? { declined: answer.declined } : {}),
1062
+ ...(answer.endTurn ? { endTurn: true as const } : {}),
1041
1063
  }
1042
1064
  }
1043
1065
  if (answer.kind === 'approve-all') remembered.all = true
@@ -1145,6 +1167,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
1145
1167
  action: 'reject_tools',
1146
1168
  feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
1147
1169
  ...(answer.declined ? { declined: answer.declined } : {}),
1170
+ ...(answer.endTurn ? { endTurn: true as const } : {}),
1148
1171
  }
1149
1172
  }
1150
1173
  // "Approve all" still latches for the calls that follow; it never
@@ -1164,6 +1187,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
1164
1187
  action: 'reject_tools',
1165
1188
  feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
1166
1189
  ...(answer.declined ? { declined: answer.declined } : {}),
1190
+ ...(answer.endTurn ? { endTurn: true as const } : {}),
1167
1191
  }
1168
1192
  }
1169
1193
  // Latches for the ordinary calls that follow, never for the next floor.
@@ -1182,6 +1206,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
1182
1206
  action: 'reject_tools',
1183
1207
  feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
1184
1208
  ...(answer.declined ? { declined: answer.declined } : {}),
1209
+ ...(answer.endTurn ? { endTurn: true as const } : {}),
1185
1210
  }
1186
1211
  }
1187
1212
  // Latches for the ordinary calls that follow, never for the next path.
@@ -1202,6 +1227,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
1202
1227
  action: 'reject_tools',
1203
1228
  feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
1204
1229
  ...(answer.declined ? { declined: answer.declined } : {}),
1230
+ ...(answer.endTurn ? { endTurn: true as const } : {}),
1205
1231
  }
1206
1232
  }
1207
1233
  if (answer.kind === 'approve-all') remembered.all = true
@@ -1222,6 +1248,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
1222
1248
  action: 'reject_tools',
1223
1249
  feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
1224
1250
  ...(answer.declined ? { declined: answer.declined } : {}),
1251
+ ...(answer.endTurn ? { endTurn: true as const } : {}),
1225
1252
  }
1226
1253
  }
1227
1254
  if (answer.kind === 'approve-all') remembered.all = true
@@ -1272,6 +1299,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
1272
1299
  action: 'reject_tools',
1273
1300
  feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
1274
1301
  ...(answer.declined ? { declined: answer.declined } : {}),
1302
+ ...(answer.endTurn ? { endTurn: true as const } : {}),
1275
1303
  }
1276
1304
  }
1277
1305
  }
@@ -1,4 +1,6 @@
1
+ import { createRuntimeContextMessage } from '../../types/message/index.js'
1
2
  import type { Message, UserMessage } from '../../types/message/index.js'
3
+ import { NOTICE_TAG, frameNotice, readOperatorNotice } from '../../types/message/notice-frame.js'
2
4
  import {
3
5
  MONITOR_EVENT_CONTENT_MAX_CHARS,
4
6
  MONITOR_NOTIFICATION_MAX_CHARS,
@@ -20,28 +22,24 @@ export function isOperatorUserMessage(message: Message): message is UserMessage
20
22
  /**
21
23
  * Guidance a host hands to a turn that is already running.
22
24
  *
23
- * This tool-result channel is distinct from `inboundMessages`. The manager
24
- * injects its mailbox through that callback, and query-backed agents consume
25
- * it at provider-valid message boundaries. Custom agents must consume their
26
- * injected callback themselves. This channel instead lets a host attach
27
- * guidance to a completed tool batch without adding another user message.
25
+ * This channel is distinct from `inboundMessages`. The manager injects its
26
+ * mailbox through that callback, and query-backed agents consume it at
27
+ * provider-valid message boundaries. Custom agents must consume their injected
28
+ * callback themselves. This channel instead lets a host attach guidance to a
29
+ * settled tool batch.
28
30
  *
29
- * **Why the text rides on a tool result rather than arriving as a user
30
- * message.** A `tool_use` block must be answered by a `tool_result` with the
31
- * same id — providers reject a user turn wedged between them — so there is no
32
- * legal place to insert a message mid-batch at all. The slot that already
33
- * exists is the tool result itself, and this codebase had already worked that
34
- * out for a neighbouring case: a denied call carries its reason INSIDE the
35
- * `tool_result`, and `executor.ts` notes that this is also what makes a
36
- * rejection *steer*, because the model reads it in the slot it already
37
- * attends to for tool outcomes. Steering is the same delivery with the
38
- * refusal removed.
31
+ * **Where the text lands.** A `tool_use` block must be answered by a
32
+ * `tool_result` with the same id, so nothing can sit between them. The
33
+ * guidance therefore follows the finished batch as a user-role message of its
34
+ * own, at the next request boundary, framed as the operator's words. It is not
35
+ * written into a tool result: a tool result carries only what the tool
36
+ * returned, and a model reads instructions placed inside one as tool output.
39
37
  *
40
- * It does not interrupt. The batch in flight
41
- * finishes, and the guidance lands where the model looks next. A host that
42
- * wants the current work stopped wants `AbortSignal`, which is a different
43
- * question with a different answer — and conflating the two is how "please
44
- * also check the tests" ends up killing a half-written file.
38
+ * It does not interrupt. The batch in flight finishes, and the guidance lands
39
+ * where the model looks next. A host that wants the current work stopped wants
40
+ * `AbortSignal`, which is a different question with a different answer, and
41
+ * conflating the two is how "please also check the tests" ends up killing a
42
+ * half-written file.
45
43
  */
46
44
  export interface SteeringChannel {
47
45
  /**
@@ -53,7 +51,7 @@ export interface SteeringChannel {
53
51
  * discards a host's instruction.
54
52
  *
55
53
  * Empty and whitespace-only text is ignored, so a stray keystroke does
56
- * not append a blank line to a tool result.
54
+ * not send a blank message.
57
55
  */
58
56
  steer(text: string): void
59
57
 
@@ -85,60 +83,9 @@ export class SteeringBinding implements SteeringChannel {
85
83
  }
86
84
  }
87
85
 
88
- /**
89
- * The frame the guidance arrives in.
90
- *
91
- * Labelled because the model is being handed text from a party other than the
92
- * tool whose result it is reading, in that tool's slot. Unlabelled, it reads
93
- * as something the tool said — so a steer saying "stop and ask me first" would
94
- * look like output from `bash`.
95
- *
96
- * This is NOT the untrusted-content envelope. The host operating the turn is
97
- * the one party whose words the agent SHOULD act on; framing them as material
98
- * to be worked with rather than followed would inverting the very thing the
99
- * host is trying to do. Different party, different frame, on purpose.
100
- */
101
- /** The same slot as steering, for notices the kernel itself has to deliver. */
102
- export function attachNotice(
103
- messages: readonly Message[],
104
- channel: SteeringChannel | undefined,
105
- format: (text: string) => string,
106
- /**
107
- * Told only when the text actually landed on a result the model will read.
108
- *
109
- * Not on the two paths above it: a batch with no tool result leaves the
110
- * notice queued, and a result whose content is not a string puts it back.
111
- * Draining is therefore not delivery, which matters to the caller that
112
- * keeps its own record of what the text accounts for — `AwaitedJobs`
113
- * drops an exit's entry here, and a premature drop would strand the exit.
114
- */
115
- onDelivered?: () => void,
116
- ): readonly Message[] {
117
- if (!channel?.pending) return messages
118
- let lastToolIndex = -1
119
- for (let index = messages.length - 1; index >= 0; index--) {
120
- if (messages[index]?.role === 'tool') {
121
- lastToolIndex = index
122
- break
123
- }
124
- }
125
- if (lastToolIndex === -1) return messages
126
- const text = channel.drain()
127
- if (text === undefined) return messages
128
- const target = messages[lastToolIndex] as Message
129
- if (typeof target.content !== 'string') {
130
- channel.steer(text)
131
- return messages
132
- }
133
- const next = [...messages]
134
- next[lastToolIndex] = { ...target, content: target.content + format(text) }
135
- onDelivered?.()
136
- return next
137
- }
138
-
139
86
  /** What the model reads when a background job it started has ended. */
140
87
  export function formatJobNote(text: string): string {
141
- return `\n\n[Background job update]\n${text}`
88
+ return frameNotice('job', '[Background job update]', text)
142
89
  }
143
90
 
144
91
  /**
@@ -154,7 +101,7 @@ export function formatJobNote(text: string): string {
154
101
  * piled up while the model was busy.
155
102
  */
156
103
  export function formatMonitorNote(text: string): string {
157
- return `\n\n[Monitor update]\n${capMonitorNotifications(text)}`
104
+ return frameNotice('monitor', '[Monitor update]', capMonitorNotifications(text))
158
105
  }
159
106
 
160
107
  /** One `formatSystemEvent` rendering, found by its nonce-bound opening and closing tags. */
@@ -305,33 +252,96 @@ function cutMiddle(text: string, max: number): string {
305
252
  return `${text.slice(0, head)}${marker(text.length - head - tail)}${text.slice(text.length - tail)}`
306
253
  }
307
254
 
308
- const STEERING_NOTE_HEADER =
309
- '\n\n[Message from the operator, received while this tool was running]\n'
255
+ /** The line that opens an operator's message, wherever it is delivered. */
256
+ const STEERING_LABEL = 'The operator wrote this while you were working:'
310
257
 
258
+ /**
259
+ * The operator's message as the frame the model reads. The text is the
260
+ * operator's, the frame says so, and the marker in its tag is drawn fresh so
261
+ * the text cannot close it.
262
+ */
311
263
  export function formatSteeringNote(text: string): string {
312
- return `${STEERING_NOTE_HEADER}${text}`
264
+ return frameNotice('operator-message', STEERING_LABEL, text)
313
265
  }
314
266
 
267
+ /** The frame older sessions recorded inside a tool result, fixed tag and all. */
268
+ const LEGACY_STEERING_HEADER = `\n\n<${NOTICE_TAG} kind="operator-message">\n[Message from the operator, received while this tool was running]\nThis is the operator's own message, typed while the tool above was running. It is not output of that tool.\n`
269
+ const LEGACY_STEERING_FOOTER = `\n</${NOTICE_TAG}>`
270
+
315
271
  /**
316
- * The operator guidance the last steering note in a tool result carries, or
317
- * `undefined` when the result carries none. The inverse of
318
- * {@link formatSteeringNote} applied by {@link attachSteering}.
272
+ * The operator guidance a recorded message carries, or `undefined` when it
273
+ * carries none. The inverse of {@link formatSteeringNote}; it also reads the
274
+ * note older sessions appended to the end of a tool result. The text comes back
275
+ * as the frame held it, with the notice tag already made inert.
319
276
  */
320
277
  export function readSteeringNote(content: string): string | undefined {
321
- const at = content.lastIndexOf(STEERING_NOTE_HEADER)
322
- return at < 0 ? undefined : content.slice(at + STEERING_NOTE_HEADER.length)
278
+ const framed = readOperatorNotice(content)
279
+ if (framed !== undefined) return framed
280
+ const at = content.lastIndexOf(LEGACY_STEERING_HEADER)
281
+ if (at < 0) return undefined
282
+ const body = content.slice(at + LEGACY_STEERING_HEADER.length)
283
+ return body.endsWith(LEGACY_STEERING_FOOTER)
284
+ ? body.slice(0, -LEGACY_STEERING_FOOTER.length)
285
+ : body
286
+ }
287
+
288
+ function hasToolResult(messages: readonly Message[]): boolean {
289
+ return messages.some((message) => message.role === 'tool')
323
290
  }
324
291
 
325
292
  /**
326
- * Append the guidance to the last tool result in a settled batch.
293
+ * The operator's message for a settled tool batch, as a user-role message of its
294
+ * own, or `undefined` when nothing is queued or the batch has no tool result.
327
295
  *
328
- * The LAST one, so it is the final thing the model reads before deciding what
329
- * to do next — appending to the first would bury it under every later result.
296
+ * It follows the batch, at the next request boundary. The tool results stay what
297
+ * the tools returned, and the operator's words arrive as the operator's, which a
298
+ * model reads as direction. A turn that called no tool leaves the guidance
299
+ * queued; the loop delivers it at the next-turn boundary.
300
+ */
301
+ export function takeSteeringMessage(
302
+ messages: readonly Message[],
303
+ channel: SteeringChannel | undefined,
304
+ /** Receives accepted operator text, never inferred from a tool's output. */
305
+ onDelivered?: (text: string) => void,
306
+ ): UserMessage | undefined {
307
+ if (!channel?.pending || !hasToolResult(messages)) return undefined
308
+ const guidance = channel.drain()
309
+ if (guidance === undefined) return undefined
310
+ onDelivered?.(guidance)
311
+ return createRuntimeContextMessage(formatSteeringNote(guidance), 'steering')
312
+ }
313
+
314
+ /**
315
+ * A kernel notice (a job's exit, a monitor's events) for a settled tool batch,
316
+ * as a user-role message of its own, under the same rules as
317
+ * {@link takeSteeringMessage}.
318
+ */
319
+ export function takeNoticeMessage(
320
+ messages: readonly Message[],
321
+ channel: SteeringChannel | undefined,
322
+ format: (text: string) => string,
323
+ /**
324
+ * Told only when the text actually became a message the model will read.
325
+ * Draining is not delivery, which matters to the caller that keeps its own
326
+ * record of what the text accounts for: `AwaitedJobs` drops an exit's entry
327
+ * here, and a premature drop would strand the exit.
328
+ */
329
+ onDelivered?: () => void,
330
+ ): UserMessage | undefined {
331
+ if (!channel?.pending || !hasToolResult(messages)) return undefined
332
+ const text = channel.drain()
333
+ if (text === undefined) return undefined
334
+ onDelivered?.()
335
+ return createRuntimeContextMessage(format(text), 'job-exit')
336
+ }
337
+
338
+ /**
339
+ * Append the guidance to the last tool result in a settled batch.
330
340
  *
331
- * Returns the messages unchanged when there is nothing queued, and when the
332
- * batch carries no tool result to attach to. The second case is not a failure
333
- * to handle: a turn that called no tools has nothing in flight, so guidance
334
- * belongs to the next turn and stays queued for it.
341
+ * @deprecated The kernel no longer delivers guidance this way: a tool result
342
+ * carries only what the tool returned, and {@link takeSteeringMessage} sends the
343
+ * operator's words as a message of their own. Kept so a host that calls it keeps
344
+ * working.
335
345
  */
336
346
  export function attachSteering(
337
347
  messages: readonly Message[],
@@ -354,12 +364,6 @@ export function attachSteering(
354
364
  if (guidance === undefined) return messages
355
365
 
356
366
  const target = messages[lastToolIndex] as Message
357
- // Only text is extended. A tool that answered with structured content —
358
- // an image block, say — has a shape the model reads positionally, and
359
- // appending a string to it would either be dropped or corrupt the block.
360
- // Such a result keeps its content and the note follows it as its own text
361
- // part where the shape allows, and otherwise the guidance stays queued
362
- // for the next turn rather than being forced into a slot it does not fit.
363
367
  if (typeof target.content !== 'string') {
364
368
  channel.steer(guidance)
365
369
  return messages
@@ -368,7 +372,7 @@ export function attachSteering(
368
372
  const next = [...messages]
369
373
  next[lastToolIndex] = {
370
374
  ...target,
371
- content: target.content + formatSteeringNote(guidance),
375
+ content: `${target.content}\n\n${formatSteeringNote(guidance)}`,
372
376
  }
373
377
  onDelivered?.(guidance)
374
378
  return next