@namzu/sdk 1.0.0 → 1.2.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 (330) hide show
  1. package/CHANGELOG.md +253 -0
  2. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  3. package/dist/agents/ReactiveAgent.js +5 -0
  4. package/dist/agents/ReactiveAgent.js.map +1 -1
  5. package/dist/agents/SupervisorAgent.d.ts +22 -1
  6. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  7. package/dist/agents/SupervisorAgent.js +47 -14
  8. package/dist/agents/SupervisorAgent.js.map +1 -1
  9. package/dist/agents/__tests__/supervisor-ledger.test.d.ts +2 -0
  10. package/dist/agents/__tests__/supervisor-ledger.test.d.ts.map +1 -0
  11. package/dist/agents/__tests__/supervisor-ledger.test.js +131 -0
  12. package/dist/agents/__tests__/supervisor-ledger.test.js.map +1 -0
  13. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  14. package/dist/bridge/a2a/mapper.js +2 -0
  15. package/dist/bridge/a2a/mapper.js.map +1 -1
  16. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  17. package/dist/bridge/sse/mapper.js +4 -0
  18. package/dist/bridge/sse/mapper.js.map +1 -1
  19. package/dist/config/runtime.d.ts +24 -0
  20. package/dist/config/runtime.d.ts.map +1 -1
  21. package/dist/config/runtime.js +9 -0
  22. package/dist/config/runtime.js.map +1 -1
  23. package/dist/connector/builtins/webhook.test.js +17 -0
  24. package/dist/connector/builtins/webhook.test.js.map +1 -1
  25. package/dist/connector/index.d.ts +1 -0
  26. package/dist/connector/index.d.ts.map +1 -1
  27. package/dist/connector/index.js +1 -0
  28. package/dist/connector/index.js.map +1 -1
  29. package/dist/connector/mcp/client.d.ts.map +1 -1
  30. package/dist/connector/mcp/client.js +4 -0
  31. package/dist/connector/mcp/client.js.map +1 -1
  32. package/dist/connector/mcp/index.d.ts +1 -0
  33. package/dist/connector/mcp/index.d.ts.map +1 -1
  34. package/dist/connector/mcp/index.js +1 -0
  35. package/dist/connector/mcp/index.js.map +1 -1
  36. package/dist/connector/mcp/streamable-http.d.ts +22 -0
  37. package/dist/connector/mcp/streamable-http.d.ts.map +1 -0
  38. package/dist/connector/mcp/streamable-http.js +128 -0
  39. package/dist/connector/mcp/streamable-http.js.map +1 -0
  40. package/dist/connector/mcp/streamable-http.test.d.ts +2 -0
  41. package/dist/connector/mcp/streamable-http.test.d.ts.map +1 -0
  42. package/dist/connector/mcp/streamable-http.test.js +132 -0
  43. package/dist/connector/mcp/streamable-http.test.js.map +1 -0
  44. package/dist/{runtime/query → constants}/continuation.d.ts +5 -0
  45. package/dist/constants/continuation.d.ts.map +1 -0
  46. package/dist/{runtime/query → constants}/continuation.js +5 -0
  47. package/dist/constants/continuation.js.map +1 -0
  48. package/dist/constants/index.d.ts +1 -0
  49. package/dist/constants/index.d.ts.map +1 -1
  50. package/dist/constants/index.js +1 -0
  51. package/dist/constants/index.js.map +1 -1
  52. package/dist/manager/run/persistence.d.ts +11 -0
  53. package/dist/manager/run/persistence.d.ts.map +1 -1
  54. package/dist/manager/run/persistence.js +30 -1
  55. package/dist/manager/run/persistence.js.map +1 -1
  56. package/dist/persona/assembler.d.ts.map +1 -1
  57. package/dist/persona/assembler.js +18 -9
  58. package/dist/persona/assembler.js.map +1 -1
  59. package/dist/provider/capabilities.d.ts +27 -0
  60. package/dist/provider/capabilities.d.ts.map +1 -0
  61. package/dist/provider/capabilities.js +29 -0
  62. package/dist/provider/capabilities.js.map +1 -0
  63. package/dist/provider/collect.d.ts.map +1 -1
  64. package/dist/provider/collect.js +4 -1
  65. package/dist/provider/collect.js.map +1 -1
  66. package/dist/provider/index.d.ts +2 -0
  67. package/dist/provider/index.d.ts.map +1 -1
  68. package/dist/provider/index.js +1 -0
  69. package/dist/provider/index.js.map +1 -1
  70. package/dist/public-runtime.d.ts +4 -3
  71. package/dist/public-runtime.d.ts.map +1 -1
  72. package/dist/public-runtime.js +3 -3
  73. package/dist/public-runtime.js.map +1 -1
  74. package/dist/registry/tool/execute.d.ts +27 -0
  75. package/dist/registry/tool/execute.d.ts.map +1 -1
  76. package/dist/registry/tool/execute.js +124 -21
  77. package/dist/registry/tool/execute.js.map +1 -1
  78. package/dist/registry/tool/execute.test.d.ts +9 -6
  79. package/dist/registry/tool/execute.test.d.ts.map +1 -1
  80. package/dist/registry/tool/execute.test.js +72 -10
  81. package/dist/registry/tool/execute.test.js.map +1 -1
  82. package/dist/registry/toolset/catalog.test.js +62 -0
  83. package/dist/registry/toolset/catalog.test.js.map +1 -1
  84. package/dist/run/reporter.d.ts.map +1 -1
  85. package/dist/run/reporter.js +8 -0
  86. package/dist/run/reporter.js.map +1 -1
  87. package/dist/runtime/query/__tests__/capability-negotiation.test.d.ts +2 -0
  88. package/dist/runtime/query/__tests__/capability-negotiation.test.d.ts.map +1 -0
  89. package/dist/runtime/query/__tests__/capability-negotiation.test.js +184 -0
  90. package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -0
  91. package/dist/runtime/query/__tests__/checkpoint-store.test.d.ts +2 -0
  92. package/dist/runtime/query/__tests__/checkpoint-store.test.d.ts.map +1 -0
  93. package/dist/runtime/query/__tests__/checkpoint-store.test.js +259 -0
  94. package/dist/runtime/query/__tests__/checkpoint-store.test.js.map +1 -0
  95. package/dist/runtime/query/__tests__/checkpoint.test.js +9 -3
  96. package/dist/runtime/query/__tests__/checkpoint.test.js.map +1 -1
  97. package/dist/runtime/query/__tests__/deferred-tools.test.js +43 -5
  98. package/dist/runtime/query/__tests__/deferred-tools.test.js.map +1 -1
  99. package/dist/runtime/query/__tests__/long-document-flow.test.d.ts +2 -0
  100. package/dist/runtime/query/__tests__/long-document-flow.test.d.ts.map +1 -0
  101. package/dist/runtime/query/__tests__/long-document-flow.test.js +154 -0
  102. package/dist/runtime/query/__tests__/long-document-flow.test.js.map +1 -0
  103. package/dist/runtime/query/__tests__/orphan-task-turn-end.test.d.ts +2 -0
  104. package/dist/runtime/query/__tests__/orphan-task-turn-end.test.d.ts.map +1 -0
  105. package/dist/runtime/query/__tests__/orphan-task-turn-end.test.js +110 -0
  106. package/dist/runtime/query/__tests__/orphan-task-turn-end.test.js.map +1 -0
  107. package/dist/runtime/query/__tests__/prompt.test.js +15 -11
  108. package/dist/runtime/query/__tests__/prompt.test.js.map +1 -1
  109. package/dist/runtime/query/checkpoint.d.ts +16 -2
  110. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  111. package/dist/runtime/query/checkpoint.js +21 -7
  112. package/dist/runtime/query/checkpoint.js.map +1 -1
  113. package/dist/runtime/query/context.d.ts +7 -0
  114. package/dist/runtime/query/context.d.ts.map +1 -1
  115. package/dist/runtime/query/context.js +1 -0
  116. package/dist/runtime/query/context.js.map +1 -1
  117. package/dist/runtime/query/index.d.ts +29 -0
  118. package/dist/runtime/query/index.d.ts.map +1 -1
  119. package/dist/runtime/query/index.js +98 -8
  120. package/dist/runtime/query/index.js.map +1 -1
  121. package/dist/runtime/query/iteration/index.d.ts +3 -55
  122. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  123. package/dist/runtime/query/iteration/index.js +260 -656
  124. package/dist/runtime/query/iteration/index.js.map +1 -1
  125. package/dist/runtime/query/iteration/phases/checkpoint.d.ts.map +1 -1
  126. package/dist/runtime/query/iteration/phases/checkpoint.js +24 -2
  127. package/dist/runtime/query/iteration/phases/checkpoint.js.map +1 -1
  128. package/dist/runtime/query/iteration/phases/compaction.d.ts +8 -0
  129. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  130. package/dist/runtime/query/iteration/phases/compaction.js +84 -3
  131. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  132. package/dist/runtime/query/iteration/phases/context.d.ts +13 -2
  133. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  134. package/dist/runtime/query/iteration/phases/context.js +53 -0
  135. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  136. package/dist/runtime/query/iteration/phases/hitl-abort-race.test.d.ts +2 -0
  137. package/dist/runtime/query/iteration/phases/hitl-abort-race.test.d.ts.map +1 -0
  138. package/dist/runtime/query/iteration/phases/hitl-abort-race.test.js +73 -0
  139. package/dist/runtime/query/iteration/phases/hitl-abort-race.test.js.map +1 -0
  140. package/dist/runtime/query/iteration/phases/hitl-answer-question.test.d.ts +2 -0
  141. package/dist/runtime/query/iteration/phases/hitl-answer-question.test.d.ts.map +1 -0
  142. package/dist/runtime/query/iteration/phases/hitl-answer-question.test.js +120 -0
  143. package/dist/runtime/query/iteration/phases/hitl-answer-question.test.js.map +1 -0
  144. package/dist/runtime/query/iteration/phases/tool-review.d.ts +1 -1
  145. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  146. package/dist/runtime/query/iteration/phases/tool-review.js +7 -2
  147. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  148. package/dist/runtime/query/iteration/phases/working-memory-compaction.test.d.ts +19 -0
  149. package/dist/runtime/query/iteration/phases/working-memory-compaction.test.d.ts.map +1 -0
  150. package/dist/runtime/query/iteration/phases/working-memory-compaction.test.js +177 -0
  151. package/dist/runtime/query/iteration/phases/working-memory-compaction.test.js.map +1 -0
  152. package/dist/runtime/query/iteration/phases/working-memory.d.ts +31 -0
  153. package/dist/runtime/query/iteration/phases/working-memory.d.ts.map +1 -0
  154. package/dist/runtime/query/iteration/phases/working-memory.js +81 -0
  155. package/dist/runtime/query/iteration/phases/working-memory.js.map +1 -0
  156. package/dist/runtime/query/iteration/stream-turn.d.ts +35 -0
  157. package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -0
  158. package/dist/runtime/query/iteration/stream-turn.js +341 -0
  159. package/dist/runtime/query/iteration/stream-turn.js.map +1 -0
  160. package/dist/runtime/query/replay/list.d.ts +17 -1
  161. package/dist/runtime/query/replay/list.d.ts.map +1 -1
  162. package/dist/runtime/query/replay/list.js +21 -4
  163. package/dist/runtime/query/replay/list.js.map +1 -1
  164. package/dist/runtime/query/replay/prepare.d.ts +10 -0
  165. package/dist/runtime/query/replay/prepare.d.ts.map +1 -1
  166. package/dist/runtime/query/replay/prepare.js +20 -4
  167. package/dist/runtime/query/replay/prepare.js.map +1 -1
  168. package/dist/session/workspace/__tests__/shared-run.test.js +1 -1
  169. package/dist/session/workspace/__tests__/shared-run.test.js.map +1 -1
  170. package/dist/session/workspace/shared-run.js +1 -0
  171. package/dist/session/workspace/shared-run.js.map +1 -1
  172. package/dist/skills/loader.test.js +8 -8
  173. package/dist/skills/loader.test.js.map +1 -1
  174. package/dist/store/index.d.ts +1 -0
  175. package/dist/store/index.d.ts.map +1 -1
  176. package/dist/store/index.js +1 -0
  177. package/dist/store/index.js.map +1 -1
  178. package/dist/store/run/checkpoint-disk.d.ts +28 -0
  179. package/dist/store/run/checkpoint-disk.d.ts.map +1 -0
  180. package/dist/store/run/checkpoint-disk.js +55 -0
  181. package/dist/store/run/checkpoint-disk.js.map +1 -0
  182. package/dist/streaming/coalesce.test.js.map +1 -1
  183. package/dist/tools/builtins/__tests__/payload-budget.test.js +48 -0
  184. package/dist/tools/builtins/__tests__/payload-budget.test.js.map +1 -1
  185. package/dist/tools/builtins/__tests__/read-file.test.js +9 -0
  186. package/dist/tools/builtins/__tests__/read-file.test.js.map +1 -1
  187. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  188. package/dist/tools/builtins/read-file.js +50 -2
  189. package/dist/tools/builtins/read-file.js.map +1 -1
  190. package/dist/tools/builtins/search-tools.d.ts.map +1 -1
  191. package/dist/tools/builtins/search-tools.js +32 -7
  192. package/dist/tools/builtins/search-tools.js.map +1 -1
  193. package/dist/tools/coordinator/__tests__/approve-plan.test.d.ts +2 -0
  194. package/dist/tools/coordinator/__tests__/approve-plan.test.d.ts.map +1 -0
  195. package/dist/tools/coordinator/__tests__/approve-plan.test.js +114 -0
  196. package/dist/tools/coordinator/__tests__/approve-plan.test.js.map +1 -0
  197. package/dist/tools/coordinator/__tests__/ask-user-question.test.d.ts +2 -0
  198. package/dist/tools/coordinator/__tests__/ask-user-question.test.d.ts.map +1 -0
  199. package/dist/tools/coordinator/__tests__/ask-user-question.test.js +309 -0
  200. package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -0
  201. package/dist/tools/coordinator/index.d.ts +8 -0
  202. package/dist/tools/coordinator/index.d.ts.map +1 -1
  203. package/dist/tools/coordinator/index.js +195 -15
  204. package/dist/tools/coordinator/index.js.map +1 -1
  205. package/dist/types/agent/index.d.ts +1 -0
  206. package/dist/types/agent/index.d.ts.map +1 -1
  207. package/dist/types/agent/index.js +1 -0
  208. package/dist/types/agent/index.js.map +1 -1
  209. package/dist/types/agent/reactive.d.ts +15 -0
  210. package/dist/types/agent/reactive.d.ts.map +1 -1
  211. package/dist/types/agent/supervisor.d.ts +18 -0
  212. package/dist/types/agent/supervisor.d.ts.map +1 -1
  213. package/dist/types/agent/working-memory.d.ts +29 -0
  214. package/dist/types/agent/working-memory.d.ts.map +1 -0
  215. package/dist/types/agent/working-memory.js +2 -0
  216. package/dist/types/agent/working-memory.js.map +1 -0
  217. package/dist/types/common/index.d.ts +10 -0
  218. package/dist/types/common/index.d.ts.map +1 -1
  219. package/dist/types/common/index.js +18 -0
  220. package/dist/types/common/index.js.map +1 -1
  221. package/dist/types/common/usage-merge.test.d.ts +2 -0
  222. package/dist/types/common/usage-merge.test.d.ts.map +1 -0
  223. package/dist/types/common/usage-merge.test.js +48 -0
  224. package/dist/types/common/usage-merge.test.js.map +1 -0
  225. package/dist/types/connector/mcp.d.ts +9 -2
  226. package/dist/types/connector/mcp.d.ts.map +1 -1
  227. package/dist/types/hitl/index.d.ts +42 -0
  228. package/dist/types/hitl/index.d.ts.map +1 -1
  229. package/dist/types/hitl/index.js +11 -0
  230. package/dist/types/hitl/index.js.map +1 -1
  231. package/dist/types/provider/chat.d.ts +9 -0
  232. package/dist/types/provider/chat.d.ts.map +1 -1
  233. package/dist/types/provider/config.d.ts +18 -0
  234. package/dist/types/provider/config.d.ts.map +1 -1
  235. package/dist/types/provider/interface.d.ts +10 -0
  236. package/dist/types/provider/interface.d.ts.map +1 -1
  237. package/dist/types/run/checkpoint-store.d.ts +64 -0
  238. package/dist/types/run/checkpoint-store.d.ts.map +1 -0
  239. package/dist/types/run/checkpoint-store.js +12 -0
  240. package/dist/types/run/checkpoint-store.js.map +1 -0
  241. package/dist/types/run/config.d.ts +27 -0
  242. package/dist/types/run/config.d.ts.map +1 -1
  243. package/dist/types/run/events.d.ts +6 -0
  244. package/dist/types/run/events.d.ts.map +1 -1
  245. package/dist/types/run/events.js.map +1 -1
  246. package/dist/types/run/index.d.ts +1 -0
  247. package/dist/types/run/index.d.ts.map +1 -1
  248. package/dist/types/run/index.js +1 -0
  249. package/dist/types/run/index.js.map +1 -1
  250. package/dist/version.d.ts.map +1 -1
  251. package/dist/version.js +25 -4
  252. package/dist/version.js.map +1 -1
  253. package/package.json +1 -1
  254. package/src/agents/ReactiveAgent.ts +5 -0
  255. package/src/agents/SupervisorAgent.ts +56 -16
  256. package/src/agents/__tests__/supervisor-ledger.test.ts +185 -0
  257. package/src/bridge/a2a/mapper.ts +3 -0
  258. package/src/bridge/sse/mapper.ts +5 -0
  259. package/src/config/runtime.ts +9 -0
  260. package/src/connector/builtins/webhook.test.ts +21 -0
  261. package/src/connector/index.ts +1 -0
  262. package/src/connector/mcp/client.ts +4 -0
  263. package/src/connector/mcp/index.ts +1 -0
  264. package/src/connector/mcp/streamable-http.test.ts +169 -0
  265. package/src/connector/mcp/streamable-http.ts +152 -0
  266. package/src/{runtime/query → constants}/continuation.ts +5 -0
  267. package/src/constants/index.ts +1 -0
  268. package/src/manager/run/persistence.ts +34 -1
  269. package/src/persona/assembler.ts +18 -6
  270. package/src/provider/capabilities.ts +47 -0
  271. package/src/provider/collect.ts +4 -1
  272. package/src/provider/index.ts +5 -0
  273. package/src/public-runtime.ts +5 -0
  274. package/src/registry/tool/execute.test.ts +85 -10
  275. package/src/registry/tool/execute.ts +124 -21
  276. package/src/registry/toolset/catalog.test.ts +68 -0
  277. package/src/run/reporter.ts +9 -0
  278. package/src/runtime/query/__tests__/capability-negotiation.test.ts +248 -0
  279. package/src/runtime/query/__tests__/checkpoint-store.test.ts +322 -0
  280. package/src/runtime/query/__tests__/checkpoint.test.ts +15 -6
  281. package/src/runtime/query/__tests__/deferred-tools.test.ts +52 -5
  282. package/src/runtime/query/__tests__/long-document-flow.test.ts +180 -0
  283. package/src/runtime/query/__tests__/orphan-task-turn-end.test.ts +127 -0
  284. package/src/runtime/query/__tests__/prompt.test.ts +15 -11
  285. package/src/runtime/query/checkpoint.ts +23 -9
  286. package/src/runtime/query/context.ts +9 -0
  287. package/src/runtime/query/index.ts +148 -29
  288. package/src/runtime/query/iteration/index.ts +316 -831
  289. package/src/runtime/query/iteration/phases/checkpoint.ts +32 -2
  290. package/src/runtime/query/iteration/phases/compaction.ts +89 -3
  291. package/src/runtime/query/iteration/phases/context.ts +59 -3
  292. package/src/runtime/query/iteration/phases/hitl-abort-race.test.ts +86 -0
  293. package/src/runtime/query/iteration/phases/hitl-answer-question.test.ts +139 -0
  294. package/src/runtime/query/iteration/phases/tool-review.ts +7 -3
  295. package/src/runtime/query/iteration/phases/working-memory-compaction.test.ts +215 -0
  296. package/src/runtime/query/iteration/phases/working-memory.ts +86 -0
  297. package/src/runtime/query/iteration/stream-turn.ts +396 -0
  298. package/src/runtime/query/replay/list.ts +38 -4
  299. package/src/runtime/query/replay/prepare.ts +37 -4
  300. package/src/session/workspace/__tests__/shared-run.test.ts +1 -1
  301. package/src/session/workspace/shared-run.ts +1 -0
  302. package/src/skills/loader.test.ts +8 -8
  303. package/src/store/index.ts +1 -0
  304. package/src/store/run/checkpoint-disk.ts +67 -0
  305. package/src/streaming/coalesce.test.ts +3 -3
  306. package/src/tools/builtins/__tests__/payload-budget.test.ts +63 -0
  307. package/src/tools/builtins/__tests__/read-file.test.ts +12 -0
  308. package/src/tools/builtins/read-file.ts +50 -2
  309. package/src/tools/builtins/search-tools.ts +38 -7
  310. package/src/tools/coordinator/__tests__/approve-plan.test.ts +139 -0
  311. package/src/tools/coordinator/__tests__/ask-user-question.test.ts +370 -0
  312. package/src/tools/coordinator/index.ts +227 -17
  313. package/src/types/agent/index.ts +1 -0
  314. package/src/types/agent/reactive.ts +17 -0
  315. package/src/types/agent/supervisor.ts +20 -0
  316. package/src/types/agent/working-memory.ts +29 -0
  317. package/src/types/common/index.ts +19 -0
  318. package/src/types/common/usage-merge.test.ts +53 -0
  319. package/src/types/connector/mcp.ts +14 -2
  320. package/src/types/hitl/index.ts +52 -1
  321. package/src/types/provider/chat.ts +10 -0
  322. package/src/types/provider/config.ts +18 -0
  323. package/src/types/provider/interface.ts +11 -0
  324. package/src/types/run/checkpoint-store.ts +72 -0
  325. package/src/types/run/config.ts +30 -0
  326. package/src/types/run/events.ts +13 -0
  327. package/src/types/run/index.ts +1 -0
  328. package/src/version.ts +26 -4
  329. package/dist/runtime/query/continuation.d.ts.map +0 -1
  330. package/dist/runtime/query/continuation.js.map +0 -1
@@ -1,33 +1,13 @@
1
1
  import { SpanStatusCode } from '@opentelemetry/api'
2
- import type { AdvisoryContext } from '../../../advisory/context.js'
3
2
  import { extractFromAssistantMessage } from '../../../compaction/extractor.js'
4
- import type { WorkingStateManager } from '../../../compaction/manager.js'
5
- import type { CompactionConfig } from '../../../config/runtime.js'
6
- import type { PlanManager } from '../../../manager/plan/lifecycle.js'
7
- import type { RunPersistence } from '../../../manager/run/persistence.js'
3
+ import { AUTO_CONTINUATION_USER_MESSAGE } from '../../../constants/continuation.js'
8
4
  import { collect } from '../../../provider/collect.js'
9
- import type { ActivityStore } from '../../../store/activity/memory.js'
10
5
  import { GENAI, NAMZU, agentIterationSpanName } from '../../../telemetry/attributes.js'
11
6
  import { getTracer } from '../../../telemetry/runtime-accessors.js'
12
- import type { ResumeHandler } from '../../../types/hitl/index.js'
13
- import type { ToolUseId } from '../../../types/ids/index.js'
14
7
  import { createAssistantMessage, createUserMessage } from '../../../types/message/index.js'
15
- import type {
16
- ChatCompletionResponse,
17
- LLMProvider,
18
- StreamChunk,
19
- } from '../../../types/provider/index.js'
20
- import type { AgentRunConfig, RunEvent, StopReason } from '../../../types/run/index.js'
21
- import type { MessageStopReason } from '../../../types/run/stop-reason.js'
22
- import type { ToolRegistryContract } from '../../../types/tool/index.js'
8
+ import type { RunEvent, StopReason } from '../../../types/run/index.js'
23
9
  import { toErrorMessage } from '../../../utils/error.js'
24
10
  import { generateMessageId } from '../../../utils/id.js'
25
- import type { Logger } from '../../../utils/logger.js'
26
- import type { CheckpointManager } from '../checkpoint.js'
27
- import { AUTO_CONTINUATION_USER_MESSAGE } from '../continuation.js'
28
- import type { EmitEvent } from '../events.js'
29
- import type { ToolExecutor } from '../executor.js'
30
- import type { GuardCoordinator } from '../guard.js'
31
11
  import { applyLifecycleHookResults } from '../plugin-hooks.js'
32
12
  import { runAdvisoryPhase } from './phases/advisory.js'
33
13
  import { runIterationCheckpoint } from './phases/checkpoint.js'
@@ -35,735 +15,274 @@ import { runCompactionCheck } from './phases/compaction.js'
35
15
  import type { IterationContext } from './phases/index.js'
36
16
  import { runPlanGate } from './phases/plan.js'
37
17
  import { runToolReview } from './phases/tool-review.js'
18
+ import { refreshWorkingMemory } from './phases/working-memory.js'
19
+ import { streamProviderTurn } from './stream-turn.js'
38
20
 
39
21
  export type { IterationContext } from './phases/index.js'
40
22
  export type { PhaseSignal } from './phases/index.js'
41
23
  export type { ToolReviewOutcome } from './phases/index.js'
42
24
 
43
- export interface IterationConfig {
44
- provider: LLMProvider
45
- runConfig: AgentRunConfig
46
- tools: ToolRegistryContract
47
- allowedTools?: string[]
48
- taskGateway?: import('../../../types/agent/gateway.js').TaskGateway
49
- taskStore?: import('../../../types/task/index.js').TaskStore
50
- launchedTasks?: Map<
51
- import('../../../types/ids/index.js').TaskId,
52
- import('./phases/context.js').LaunchedTaskMeta
53
- >
54
- compactionConfig?: CompactionConfig
55
- workingStateManager?: WorkingStateManager
56
- advisoryCtx?: AdvisoryContext
57
- agentBus?: import('../../../bus/index.js').AgentBus
58
- verificationGate?: import('../../../verification/gate.js').VerificationGate
59
- pluginManager?: import('../../../plugin/lifecycle.js').PluginLifecycleManager
60
- }
61
-
62
- /**
63
- * Escape the five XML metacharacters so an interpolated value cannot
64
- * break out of a tag. Used for the simple identifier fields in the
65
- * `<task-notification>` envelope (taskId, agentId, status) — values
66
- * here are controlled enums / opaque ids in practice, but escaping
67
- * keeps the envelope robust against any future producer that lets a
68
- * `<` or `&` leak in.
69
- */
70
- function xmlEscape(value: string): string {
71
- return value
72
- .replace(/&/g, '&amp;')
73
- .replace(/</g, '&lt;')
74
- .replace(/>/g, '&gt;')
75
- .replace(/"/g, '&quot;')
76
- .replace(/'/g, '&apos;')
77
- }
78
-
79
- /**
80
- * Wrap free-form worker output in a CDATA section. CDATA preserves
81
- * the raw text — code, markdown angle brackets, ampersands — so the
82
- * supervisor sees what the worker actually produced instead of an
83
- * escape-encoded approximation. The only termination CDATA forbids
84
- * is the literal `]]>` sequence; we split-and-rejoin around it to
85
- * keep the section well-formed regardless of payload.
86
- */
87
- function cdataWrap(value: string): string {
88
- return `<![CDATA[${value.replace(/]]>/g, ']]]]><![CDATA[>')}]]>`
89
- }
90
-
91
- /**
92
- * Map a provider's coarse `finishReason` plus the orchestrator's
93
- * `forceFinalize` flag onto the per-message {@link MessageStopReason}
94
- * union the v3 `message_completed` event surfaces.
95
- */
96
- function synthesizeMessageStopReason(
97
- finishReason: 'stop' | 'tool_calls' | 'length' | 'content_filter',
98
- forceFinalize: boolean,
99
- ): MessageStopReason {
100
- if (forceFinalize) return 'forced_finalize'
101
- switch (finishReason) {
102
- case 'tool_calls':
103
- return 'tool_use'
104
- case 'length':
105
- return 'max_tokens'
106
- case 'content_filter':
107
- return 'refusal'
108
- default:
109
- return 'end_turn'
110
- }
111
- }
112
-
113
- interface StreamingTurnResult {
114
- response: ChatCompletionResponse
115
- messageId: import('../../../types/ids/index.js').MessageId
116
- }
25
+ export class IterationOrchestrator {
26
+ private ctx: IterationContext
117
27
 
118
- /**
119
- * Consume a provider's streaming response and emit the v3 RunEvent
120
- * lifecycle natively (message_started → text_delta* + tool_input_*
121
- * → message_completed). Returns the aggregated `ChatCompletionResponse`
122
- * for downstream code that still expects the legacy shape (assistant
123
- * message construction, working-state extraction, telemetry attribute
124
- * stamping).
125
- *
126
- * Per-delta `emitEvent` calls are followed by a `drainPending()`
127
- * yield so SSE consumers see live progress instead of a burst at
128
- * end-of-message. The bus's ephemeral filter (D1) ensures these
129
- * deltas never hit transcript.jsonl.
130
- *
131
- * Edge cases (codex A3, A4, A5):
132
- * - Stream ends without `finishReason` (anthropic-sdk-typescript#842
133
- * dropped message_stop): we still emit `message_completed` from a
134
- * finally-style fall-through path with `stopReason: 'refusal'`.
135
- * - `tool_input_delta` with no `toolUseId` registered yet: we drop
136
- * the fragment and log a warning (proxies seen to misorder events).
137
- * - `chunk.error`: when no tool input is recoverable, we surface as
138
- * a thrown error after emitting the message_completed terminator so
139
- * consumer cards still close. If a tool-use block was already open,
140
- * we instead synthesize a tool call with runtime truncation metadata
141
- * so the executor can return a model-readable retry hint.
142
- */
143
- async function* streamProviderTurn(
144
- provider: LLMProvider,
145
- params: import('../../../types/provider/index.js').ChatCompletionParams,
146
- emitEvent: EmitEvent,
147
- drainPending: () => Generator<RunEvent>,
148
- runId: import('../../../types/ids/index.js').RunId,
149
- iteration: number,
150
- forceFinalize: boolean,
151
- log: Logger,
152
- ): AsyncGenerator<RunEvent, StreamingTurnResult> {
153
- const messageId = generateMessageId()
154
- await emitEvent({ type: 'message_started', runId, iteration, messageId })
155
- yield* drainPending()
156
-
157
- let id = ''
158
- const model = ''
159
- let textBuf = ''
160
- let finishReason: ChatCompletionResponse['finishReason'] = 'stop'
161
- let usage: ChatCompletionResponse['usage'] = {
162
- promptTokens: 0,
163
- completionTokens: 0,
164
- totalTokens: 0,
165
- cachedTokens: 0,
166
- cacheWriteTokens: 0,
28
+ constructor(ctx: IterationContext) {
29
+ this.ctx = ctx
167
30
  }
168
- const toolBuckets = new Map<
169
- number,
170
- {
171
- id: string
172
- name: string
173
- argsBuf: string
174
- started: boolean
175
- completed: boolean
176
- /**
177
- * Parsed input. `null` while the bucket is still streaming.
178
- * The synthesized
179
- * `ChatCompletionResponse.toolCalls[].function.arguments` is
180
- * derived from this — never from the raw buffer — so the
181
- * downstream executor (`runtime/query/executor.ts`) never has
182
- * to re-parse a truncated string. A truncated tool call is
183
- * surfaced as `arguments: "{}"` plus `metadata.inputTruncated`
184
- * so tool args remain clean while the executor can still
185
- * return a specific retry hint.
186
- */
187
- parsed: unknown | null
188
- inputTruncated: boolean
189
- }
190
- >()
191
- let streamError: string | undefined
192
31
 
193
- const stream = provider.chatStream({ ...params, stream: true }) as AsyncIterable<StreamChunk>
194
-
195
- try {
196
- for await (const chunk of stream) {
197
- if (chunk.error) {
198
- streamError = chunk.error
199
- break
200
- }
201
- if (!id && chunk.id) id = chunk.id
202
-
203
- if (chunk.delta.content) {
204
- textBuf += chunk.delta.content
205
- await emitEvent({
206
- type: 'text_delta',
207
- runId,
208
- iteration,
209
- messageId,
210
- text: chunk.delta.content,
211
- })
212
- yield* drainPending()
213
- }
32
+ async *runLoop(): AsyncGenerator<RunEvent> {
33
+ const { runConfig, runMgr } = this.ctx
34
+ const { model } = runConfig
35
+ const tracer = getTracer()
214
36
 
215
- for (const tc of chunk.delta.toolCalls ?? []) {
216
- let bucket = toolBuckets.get(tc.index)
217
- if (!bucket) {
218
- bucket = {
219
- id: tc.id ?? '',
220
- name: tc.function?.name ?? '',
221
- argsBuf: '',
222
- started: false,
223
- completed: false,
224
- parsed: null,
225
- inputTruncated: false,
226
- }
227
- toolBuckets.set(tc.index, bucket)
228
- }
229
- if (tc.id && !bucket.id) bucket.id = tc.id
230
- if (tc.function?.name && !bucket.name) bucket.name = tc.function.name
231
-
232
- if (!bucket.started && bucket.id && bucket.name) {
233
- bucket.started = true
234
- await emitEvent({
235
- type: 'tool_input_started',
236
- runId,
237
- iteration,
238
- messageId,
239
- toolUseId: bucket.id as ToolUseId,
240
- toolName: bucket.name,
241
- })
242
- yield* drainPending()
243
- }
37
+ const planSignal = yield* runPlanGate(this.ctx)
38
+ if (planSignal === 'stop') return
244
39
 
245
- const fragment = tc.function?.arguments
246
- if (fragment) {
247
- if (!bucket.id) {
248
- log.warn('tool_input_delta arrived before tool id was known; dropping fragment', {
249
- runId,
250
- index: tc.index,
251
- length: fragment.length,
252
- })
253
- } else {
254
- bucket.argsBuf += fragment
255
- await emitEvent({
256
- type: 'tool_input_delta',
257
- runId,
258
- toolUseId: bucket.id as ToolUseId,
259
- partialJson: fragment,
260
- })
261
- yield* drainPending()
262
- }
263
- }
264
- }
40
+ while (true) {
41
+ const guardResult = this.ctx.guard.beforeIteration(runMgr, this.ctx.abortController.signal)
265
42
 
266
- if (chunk.delta.toolCallEnd) {
267
- const { index, id: endId } = chunk.delta.toolCallEnd
268
- const bucket = toolBuckets.get(index)
269
- if (bucket && !bucket.completed) {
270
- bucket.completed = true
271
- let parsed: unknown = {}
272
- try {
273
- parsed = bucket.argsBuf ? JSON.parse(bucket.argsBuf) : {}
274
- } catch (err) {
275
- bucket.inputTruncated = true
276
- log.warn('tool input JSON parse failed at content_block_stop', {
277
- runId,
278
- toolUseId: endId,
279
- error: err instanceof Error ? err.message : String(err),
280
- })
281
- }
282
- bucket.parsed = parsed
283
- await emitEvent({
284
- type: 'tool_input_completed',
285
- runId,
286
- toolUseId: endId as ToolUseId,
287
- input: parsed,
288
- ...(bucket.inputTruncated ? { inputTruncated: true } : {}),
289
- })
290
- yield* drainPending()
43
+ if (guardResult.shouldStop) {
44
+ if (guardResult.isCancelled) {
45
+ this.ctx.log.info('Run cancelled by signal', { runId: runMgr.id })
46
+ runMgr.setStopReason('cancelled')
47
+ runMgr.markCancelled()
48
+ break
291
49
  }
292
- }
293
-
294
- if (chunk.finishReason) finishReason = chunk.finishReason
295
- if (chunk.usage) usage = chunk.usage
296
- }
297
- } catch (err) {
298
- streamError = err instanceof Error ? err.message : String(err)
299
- }
300
50
 
301
- // Flush any tool buckets the provider failed to close (no toolCallEnd
302
- // arrived — defensive against providers that don't yet emit it, and
303
- // the load-bearing path when the provider stream ends with
304
- // `stop_reason: "max_tokens"` mid-`input_json_delta`. In that case
305
- // Anthropic's SSE never sends `content_block_stop` for the open
306
- // tool_use block: the upstream model ran out of completion tokens
307
- // before it could close the JSON literal, so the buffered
308
- // `argsBuf` ends with something like `"content":"…some prefix` —
309
- // not parseable.
310
- //
311
- // Two cases coalesce here:
312
- // 1. The buffer parses cleanly (the provider just forgot to emit
313
- // `content_block_stop` but the args are intact) — keep parsed.
314
- // 2. The buffer is truncated mid-literal — `parsed = {}` is the
315
- // safe fallback so the executor's `JSON.parse(arguments)`
316
- // succeeds and downstream consumers don't crash. The PRICE
317
- // we used to pay was the model getting back a generic
318
- // "<field> is required" Zod error and not realising its
319
- // previous tool call was truncated server-side, so it would
320
- // retry with the SAME long input and hit the same cutoff in
321
- // a loop. Detect the truncation case and mark the tool call
322
- // with runtime metadata; the executor surfaces a specific
323
- // "your tool call was cut off by max_tokens — retry with
324
- // shorter input or split into smaller calls" message that the
325
- // model can act on.
326
- for (const bucket of toolBuckets.values()) {
327
- if (bucket.started && !bucket.completed) {
328
- bucket.completed = true
329
- let parsed: unknown = {}
330
- let truncated = false
331
- if (bucket.argsBuf) {
332
- try {
333
- parsed = JSON.parse(bucket.argsBuf)
334
- } catch {
335
- // argsBuf had content but didn't parse — almost
336
- // certainly the max_tokens-mid-literal cutoff. Mark
337
- // the bucket so the executor can return a model-
338
- // readable hint instead of a generic Zod error.
339
- truncated = true
340
- parsed = {}
341
- }
342
- }
343
- bucket.parsed = parsed
344
- bucket.inputTruncated = truncated
345
- if (truncated) {
346
- log.warn('tool input truncated by upstream cutoff (no toolCallEnd, argsBuf unparsable)', {
347
- runId,
348
- toolUseId: bucket.id,
349
- toolName: bucket.name,
350
- bufferLength: bucket.argsBuf.length,
51
+ const stopReason = guardResult.stopReason ?? 'end_turn'
52
+ this.ctx.log.info('Guard enforcing stop', {
53
+ runId: runMgr.id,
54
+ stopReason,
55
+ iteration: runMgr.currentIteration,
56
+ inputTokens: runMgr.tokenUsage.promptTokens,
57
+ outputTokens: runMgr.tokenUsage.completionTokens,
351
58
  })
59
+ await this.requestFinalResponse(model, stopReason)
60
+ yield* this.ctx.drainPending()
61
+ runMgr.setStopReason(stopReason)
62
+ break
352
63
  }
353
- await emitEvent({
354
- type: 'tool_input_completed',
355
- runId,
356
- toolUseId: bucket.id as ToolUseId,
357
- input: parsed,
358
- ...(truncated ? { inputTruncated: true } : {}),
359
- })
360
- yield* drainPending()
361
- }
362
- }
363
-
364
- // `arguments` MUST be valid JSON for the executor's `JSON.parse`
365
- // (`runtime/query/executor.ts:executeSingle`) to succeed. We
366
- // always serialise from the bucket's `parsed` object (filled by
367
- // either the `toolCallEnd` branch above or the post-stream flush
368
- // loop) instead of re-emitting `argsBuf`. When the provider
369
- // stream truncated mid-input, `metadata.inputTruncated` carries that
370
- // state; the executor parses cleanly and returns a specific
371
- // model-readable retry hint instead of the generic "Invalid JSON in
372
- // tool arguments" intercept.
373
- const toolCalls = [...toolBuckets.entries()]
374
- .sort(([a], [b]) => a - b)
375
- .map(([, b]) => ({
376
- id: b.id,
377
- type: 'function' as const,
378
- function: {
379
- name: b.name,
380
- arguments: JSON.stringify(b.parsed ?? {}),
381
- },
382
- ...(b.inputTruncated ? { metadata: { inputTruncated: true } } : {}),
383
- }))
384
-
385
- const recoveredToolInputFromStreamError =
386
- streamError !== undefined && toolCalls.some((tc) => tc.id && tc.function.name)
387
- const effectiveFinishReason: ChatCompletionResponse['finishReason'] =
388
- recoveredToolInputFromStreamError ? 'tool_calls' : finishReason
389
-
390
- if (recoveredToolInputFromStreamError) {
391
- log.warn('provider stream failed after tool input; surfacing tool call to executor', {
392
- runId,
393
- iteration,
394
- error: streamError,
395
- toolCallCount: toolCalls.length,
396
- })
397
- }
398
-
399
- const stopReason: MessageStopReason = streamError
400
- ? recoveredToolInputFromStreamError
401
- ? 'tool_use'
402
- : 'refusal'
403
- : synthesizeMessageStopReason(effectiveFinishReason, forceFinalize)
404
-
405
- await emitEvent({
406
- type: 'message_completed',
407
- runId,
408
- iteration,
409
- messageId,
410
- stopReason,
411
- usage,
412
- content: textBuf || undefined,
413
- })
414
- yield* drainPending()
415
-
416
- if (streamError && !recoveredToolInputFromStreamError) {
417
- throw new Error(`Provider stream error: ${streamError}`)
418
- }
419
64
 
420
- const response: ChatCompletionResponse = {
421
- id: id || messageId,
422
- model: model || params.model,
423
- message: {
424
- role: 'assistant',
425
- content: textBuf.length > 0 ? textBuf : null,
426
- toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
427
- },
428
- finishReason: effectiveFinishReason,
429
- usage,
430
- }
431
- return { response, messageId }
432
- }
65
+ const forceFinalize = guardResult.forceFinalize
66
+ const iterationNum = runMgr.incrementIteration()
67
+ this.ctx.log.debug('Iteration started', {
68
+ runId: runMgr.id,
69
+ iteration: iterationNum,
70
+ model,
71
+ forceFinalize,
72
+ messageCount: runMgr.messages.length,
73
+ })
433
74
 
434
- export class IterationOrchestrator {
435
- private ctx: IterationContext
75
+ const iterationActivity = this.ctx.activityStore.create({
76
+ type: 'llm_turn',
77
+ description: `LLM iteration ${iterationNum}`,
78
+ })
79
+ if (iterationActivity) {
80
+ this.ctx.activityStore.start(iterationActivity.id)
81
+ }
436
82
 
437
- constructor(
438
- config: IterationConfig,
439
- runMgr: RunPersistence,
440
- toolExecutor: ToolExecutor,
441
- guard: GuardCoordinator,
442
- activityStore: ActivityStore,
443
- emitEvent: EmitEvent,
444
- drainPending: () => Generator<RunEvent>,
445
- abortController: AbortController,
446
- log: Logger,
447
- resumeHandler: ResumeHandler,
448
- checkpointMgr: CheckpointManager,
449
- planManager: PlanManager,
450
- ) {
451
- this.ctx = {
452
- provider: config.provider,
453
- runConfig: config.runConfig,
454
- tools: config.tools,
455
- allowedTools: config.allowedTools,
456
- runMgr,
457
- toolExecutor,
458
- guard,
459
- activityStore,
460
- emitEvent,
461
- drainPending,
462
- abortController,
463
- log,
464
- resumeHandler,
465
- checkpointMgr,
466
- planManager,
467
- taskGateway: config.taskGateway,
468
- taskStore: config.taskStore,
469
- pendingNotifications: [],
470
- launchedTasks: config.launchedTasks ?? new Map(),
471
- compactionConfig: config.compactionConfig,
472
- workingStateManager: config.workingStateManager,
473
- advisoryCtx: config.advisoryCtx,
474
- agentBus: config.agentBus,
475
- verificationGate: config.verificationGate,
476
- pluginManager: config.pluginManager,
477
- }
478
- }
83
+ const iterSpan = tracer.startSpan(agentIterationSpanName(iterationNum))
84
+ iterSpan.setAttributes({
85
+ [NAMZU.ITERATION]: iterationNum,
86
+ [NAMZU.RUN_ID]: runMgr.id,
87
+ [GENAI.REQUEST_MODEL]: model,
88
+ })
479
89
 
480
- async *runLoop(): AsyncGenerator<RunEvent> {
481
- const { runConfig, runMgr } = this.ctx
482
- const { model } = runConfig
483
- const tracer = getTracer()
90
+ await this.ctx.emitEvent({
91
+ type: 'iteration_started',
92
+ runId: runMgr.id,
93
+ iteration: iterationNum,
94
+ })
95
+ yield* this.ctx.drainPending()
484
96
 
485
- // Worker-completion delivery used to fan out through a global
486
- // onTaskCompleted listener that pushed handles onto
487
- // `pendingNotifications`; the iteration loop then drained
488
- // them as <task-notification> envelopes. Both `create_task`
489
- // and the `Agent` tool are now blocking and return their
490
- // worker output as the dispatching tool_use's canonical
491
- // tool_result, so the listener path would only DUPLICATE
492
- // every completion (once as tool_result, once as injected
493
- // envelope user-message). Leaving the binding out closes
494
- // the duplicate notification surface entirely; the dormant
495
- // drain stays as a no-op until a follow-up tears it out.
496
- let unsubscribeTaskListener: (() => void) | undefined
497
- void unsubscribeTaskListener
97
+ try {
98
+ if (this.ctx.pluginManager) {
99
+ const hookResults = await this.ctx.pluginManager.executeHooks(
100
+ 'iteration_start',
101
+ { runId: runMgr.id, iteration: iterationNum },
102
+ this.ctx.emitEvent,
103
+ )
104
+ applyLifecycleHookResults('iteration_start', hookResults)
105
+ yield* this.ctx.drainPending()
106
+ }
498
107
 
499
- try {
500
- const planSignal = yield* runPlanGate(this.ctx)
501
- if (planSignal === 'stop') return
108
+ // Re-pin the working-memory block from ground truth at the primacy
109
+ // edge BEFORE compaction runs (so the refreshed slot is what
110
+ // compaction preserves). No-op when no provider is configured.
111
+ await refreshWorkingMemory(this.ctx)
112
+ await runCompactionCheck(this.ctx)
113
+
114
+ // Cache discipline: keep the tools param byte-stable even on the
115
+ // forced-final iteration and forbid tool use via tool_choice
116
+ // 'none' instead. Dropping the tools array would invalidate the
117
+ // entire prompt-cache prefix (tools render at position 0) and
118
+ // risks a 400 because the history still carries
119
+ // tool_use/tool_result blocks.
120
+ const openAITools = this.ctx.tools.toLLMTools(this.ctx.allowedTools)
121
+
122
+ const messages = forceFinalize
123
+ ? [
124
+ ...runMgr.messages,
125
+ createUserMessage(
126
+ '[SYSTEM] You are approaching your resource limits. Provide your final, comprehensive response now based on everything you have gathered so far. Do not request any more tool calls.',
127
+ ),
128
+ ]
129
+ : runMgr.messages
130
+
131
+ if (this.ctx.pluginManager) {
132
+ const hookResults = await this.ctx.pluginManager.executeHooks(
133
+ 'pre_llm_call',
134
+ { runId: runMgr.id, iteration: iterationNum },
135
+ this.ctx.emitEvent,
136
+ )
137
+ applyLifecycleHookResults('pre_llm_call', hookResults)
138
+ yield* this.ctx.drainPending()
139
+ }
502
140
 
503
- while (true) {
504
- const guardResult = this.ctx.guard.beforeIteration(runMgr, this.ctx.abortController.signal)
141
+ // Phase 4 (ses_001-tool-stream-events): consume the
142
+ // streaming response natively, emitting message and
143
+ // tool-input lifecycle events as deltas arrive. The
144
+ // helper yields RunEvents through drainPending() so SSE
145
+ // consumers see live progress; its return value is the
146
+ // aggregated `ChatCompletionResponse` for the legacy
147
+ // downstream paths (assistantMsg construction, working
148
+ // state extraction, telemetry attribute stamping).
149
+ const { response } = yield* streamProviderTurn(
150
+ this.ctx.provider,
151
+ {
152
+ model,
153
+ messages,
154
+ tools: openAITools.length > 0 ? openAITools : undefined,
155
+ toolChoice: forceFinalize && openAITools.length > 0 ? 'none' : undefined,
156
+ temperature: runConfig.temperature,
157
+ maxTokens: runConfig.maxResponseTokens,
158
+ cacheControl: { type: 'auto' },
159
+ // Thread the run abort into the model call so a Stop tears the
160
+ // in-flight turn down (provider passes it to fetch; the consumer
161
+ // also races it). Inert when never aborted.
162
+ signal: this.ctx.abortController.signal,
163
+ },
164
+ this.ctx.emitEvent,
165
+ this.ctx.drainPending,
166
+ runMgr.id,
167
+ iterationNum,
168
+ forceFinalize,
169
+ this.ctx.log,
170
+ )
505
171
 
506
- if (guardResult.shouldStop) {
507
- if (guardResult.isCancelled) {
508
- this.ctx.log.info('Run cancelled by signal', { runId: runMgr.id })
509
- runMgr.setStopReason('cancelled')
510
- runMgr.markCancelled()
511
- break
512
- }
172
+ runMgr.accumulateUsage(response.usage)
513
173
 
514
- const stopReason = guardResult.stopReason ?? 'end_turn'
515
- this.ctx.log.info('Guard enforcing stop', {
516
- runId: runMgr.id,
517
- stopReason,
518
- iteration: runMgr.currentIteration,
519
- inputTokens: runMgr.tokenUsage.promptTokens,
520
- outputTokens: runMgr.tokenUsage.completionTokens,
521
- })
522
- await this.requestFinalResponse(model, stopReason)
174
+ if (this.ctx.pluginManager) {
175
+ const hookResults = await this.ctx.pluginManager.executeHooks(
176
+ 'post_llm_call',
177
+ { runId: runMgr.id, iteration: iterationNum },
178
+ this.ctx.emitEvent,
179
+ )
180
+ applyLifecycleHookResults('post_llm_call', hookResults)
523
181
  yield* this.ctx.drainPending()
524
- runMgr.setStopReason(stopReason)
525
- break
526
182
  }
527
183
 
528
- const forceFinalize = guardResult.forceFinalize
529
- const iterationNum = runMgr.incrementIteration()
530
- this.ctx.log.debug('Iteration started', {
184
+ this.ctx.log.debug('LLM response received', {
531
185
  runId: runMgr.id,
532
186
  iteration: iterationNum,
533
- model,
534
- forceFinalize,
535
- messageCount: runMgr.messages.length,
536
- })
537
-
538
- const iterationActivity = this.ctx.activityStore.create({
539
- type: 'llm_turn',
540
- description: `LLM iteration ${iterationNum}`,
541
- })
542
- if (iterationActivity) {
543
- this.ctx.activityStore.start(iterationActivity.id)
544
- }
545
-
546
- const iterSpan = tracer.startSpan(agentIterationSpanName(iterationNum))
547
- iterSpan.setAttributes({
548
- [NAMZU.ITERATION]: iterationNum,
549
- [NAMZU.RUN_ID]: runMgr.id,
550
- [GENAI.REQUEST_MODEL]: model,
187
+ finishReason: response.finishReason,
188
+ hasContent: response.message.content !== null && response.message.content.length > 0,
189
+ toolCallCount: response.message.toolCalls?.length ?? 0,
190
+ promptTokens: response.usage.promptTokens,
191
+ completionTokens: response.usage.completionTokens,
192
+ totalTokens: runMgr.tokenUsage.totalTokens,
193
+ totalCost: runMgr.costInfo.totalCost,
551
194
  })
552
195
 
553
196
  await this.ctx.emitEvent({
554
- type: 'iteration_started',
197
+ type: 'token_usage_updated',
555
198
  runId: runMgr.id,
556
- iteration: iterationNum,
199
+ usage: runMgr.tokenUsage,
200
+ cost: runMgr.costInfo,
557
201
  })
558
- yield* this.ctx.drainPending()
559
-
560
- try {
561
- if (this.ctx.pluginManager) {
562
- const hookResults = await this.ctx.pluginManager.executeHooks(
563
- 'iteration_start',
564
- { runId: runMgr.id, iteration: iterationNum },
565
- this.ctx.emitEvent,
566
- )
567
- applyLifecycleHookResults('iteration_start', hookResults)
568
- yield* this.ctx.drainPending()
569
- }
570
-
571
- if (this.ctx.pendingNotifications.length > 0) {
572
- await this.injectOneTaskNotification()
573
- }
574
202
 
575
- await runCompactionCheck(this.ctx)
576
-
577
- const openAITools = forceFinalize
578
- ? undefined
579
- : this.ctx.tools.toLLMTools(this.ctx.allowedTools)
580
-
581
- const messages = forceFinalize
582
- ? [
583
- ...runMgr.messages,
584
- createUserMessage(
585
- '[SYSTEM] You are approaching your resource limits. Provide your final, comprehensive response now based on everything you have gathered so far. Do not request any more tool calls.',
586
- ),
587
- ]
588
- : runMgr.messages
589
-
590
- if (this.ctx.pluginManager) {
591
- const hookResults = await this.ctx.pluginManager.executeHooks(
592
- 'pre_llm_call',
593
- { runId: runMgr.id, iteration: iterationNum },
594
- this.ctx.emitEvent,
595
- )
596
- applyLifecycleHookResults('pre_llm_call', hookResults)
597
- yield* this.ctx.drainPending()
598
- }
599
-
600
- // Phase 4 (ses_001-tool-stream-events): consume the
601
- // streaming response natively, emitting message and
602
- // tool-input lifecycle events as deltas arrive. The
603
- // helper yields RunEvents through drainPending() so SSE
604
- // consumers see live progress; its return value is the
605
- // aggregated `ChatCompletionResponse` for the legacy
606
- // downstream paths (assistantMsg construction, working
607
- // state extraction, telemetry attribute stamping).
608
- const { response } = yield* streamProviderTurn(
609
- this.ctx.provider,
610
- {
611
- model,
612
- messages,
613
- tools: openAITools && openAITools.length > 0 ? openAITools : undefined,
614
- temperature: runConfig.temperature,
615
- maxTokens: runConfig.maxResponseTokens,
616
- cacheControl: { type: 'auto' },
617
- },
618
- this.ctx.emitEvent,
619
- this.ctx.drainPending,
620
- runMgr.id,
621
- iterationNum,
622
- forceFinalize,
623
- this.ctx.log,
203
+ const assistantMsg = createAssistantMessage(
204
+ response.message.content,
205
+ forceFinalize ? undefined : response.message.toolCalls,
206
+ )
207
+ runMgr.pushMessage(assistantMsg)
208
+
209
+ if (this.ctx.workingStateManager && this.ctx.compactionConfig && assistantMsg.content) {
210
+ extractFromAssistantMessage(
211
+ this.ctx.workingStateManager,
212
+ assistantMsg.content,
213
+ this.ctx.compactionConfig,
624
214
  )
215
+ }
625
216
 
626
- runMgr.accumulateUsage(response.usage)
627
-
628
- if (this.ctx.pluginManager) {
629
- const hookResults = await this.ctx.pluginManager.executeHooks(
630
- 'post_llm_call',
631
- { runId: runMgr.id, iteration: iterationNum },
632
- this.ctx.emitEvent,
633
- )
634
- applyLifecycleHookResults('post_llm_call', hookResults)
635
- yield* this.ctx.drainPending()
636
- }
217
+ yield* this.ctx.drainPending()
637
218
 
638
- this.ctx.log.debug('LLM response received', {
639
- runId: runMgr.id,
640
- iteration: iterationNum,
641
- finishReason: response.finishReason,
642
- hasContent: response.message.content !== null && response.message.content.length > 0,
643
- toolCallCount: response.message.toolCalls?.length ?? 0,
644
- promptTokens: response.usage.promptTokens,
645
- completionTokens: response.usage.completionTokens,
646
- totalTokens: runMgr.tokenUsage.totalTokens,
647
- totalCost: runMgr.costInfo.totalCost,
648
- })
219
+ iterSpan.setAttributes({
220
+ [GENAI.USAGE_INPUT_TOKENS]: response.usage.promptTokens,
221
+ [GENAI.USAGE_OUTPUT_TOKENS]: response.usage.completionTokens,
222
+ })
223
+ iterSpan.setStatus({ code: SpanStatusCode.OK })
649
224
 
650
- await this.ctx.emitEvent({
651
- type: 'token_usage_updated',
652
- runId: runMgr.id,
653
- usage: runMgr.tokenUsage,
654
- cost: runMgr.costInfo,
225
+ if (iterationActivity) {
226
+ this.ctx.activityStore.complete(iterationActivity.id, {
227
+ content: response.message.content,
228
+ hasToolCalls: forceFinalize ? false : !!response.message.toolCalls?.length,
655
229
  })
230
+ }
656
231
 
657
- const assistantMsg = createAssistantMessage(
658
- response.message.content,
659
- forceFinalize ? undefined : response.message.toolCalls,
660
- )
661
- runMgr.pushMessage(assistantMsg)
662
-
663
- if (this.ctx.workingStateManager && this.ctx.compactionConfig && assistantMsg.content) {
664
- extractFromAssistantMessage(
665
- this.ctx.workingStateManager,
666
- assistantMsg.content,
667
- this.ctx.compactionConfig,
232
+ if (
233
+ forceFinalize ||
234
+ response.finishReason === 'stop' ||
235
+ !response.message.toolCalls ||
236
+ response.message.toolCalls.length === 0
237
+ ) {
238
+ // Every task-dispatch tool (create_task, continue_task, Agent)
239
+ // is BLOCKING: the worker's output returns as the dispatching
240
+ // tool_use's canonical tool_result, so by the time the model
241
+ // ends its turn nothing launched by this run should still be
242
+ // in flight. A running task here is an orphan (interrupted
243
+ // tool execution, cancel race) with no delivery path back to
244
+ // the parent — the <task-notification> producer was removed
245
+ // in dc16d58, so waiting on the queue could only ever time
246
+ // out. Log the orphans honestly and end the turn normally.
247
+ if (!forceFinalize && this.hasRunningAgentTasks()) {
248
+ this.ctx.log.warn(
249
+ 'LLM ended turn with agent tasks still running — ending run without waiting (orphan tasks have no delivery path)',
250
+ {
251
+ runId: runMgr.id,
252
+ iteration: iterationNum,
253
+ },
668
254
  )
669
255
  }
670
256
 
671
- yield* this.ctx.drainPending()
672
-
673
- iterSpan.setAttributes({
674
- [GENAI.USAGE_INPUT_TOKENS]: response.usage.promptTokens,
675
- [GENAI.USAGE_OUTPUT_TOKENS]: response.usage.completionTokens,
676
- })
677
- iterSpan.setStatus({ code: SpanStatusCode.OK })
678
-
679
- if (iterationActivity) {
680
- this.ctx.activityStore.complete(iterationActivity.id, {
681
- content: response.message.content,
682
- hasToolCalls: forceFinalize ? false : !!response.message.toolCalls?.length,
257
+ const hasContent =
258
+ response.message.content !== null && response.message.content.length > 0
259
+
260
+ // Auto-continuation on `stop_reason: max_tokens`. The
261
+ // model hit its per-call output cap mid-text (NOT
262
+ // mid-tool-use — that path is handled separately
263
+ // below via `inputTruncated`). Push a synthetic
264
+ // "continue" user message and let the loop fire
265
+ // another turn. The provider receives the partial
266
+ // assistant content + the continue prompt and
267
+ // resumes from where it left off, mirroring the
268
+ // Claude.ai "Continue" affordance.
269
+ //
270
+ // Guards:
271
+ // - `hasContent` so we don't loop forever on an
272
+ // empty cutoff (Anthropic occasionally emits
273
+ // `stop_reason: max_tokens` with no content
274
+ // when an injected pre-fill blocks the model).
275
+ // - `!forceFinalize` so the forced-finalize path
276
+ // never auto-continues — that path is invoked
277
+ // specifically to extract a closing summary.
278
+ // - max_iterations bounds the loop in any case.
279
+ if (!forceFinalize && response.finishReason === 'length' && hasContent) {
280
+ this.ctx.log.info('LLM hit max_tokens mid-text — auto-continuing', {
281
+ runId: runMgr.id,
282
+ iteration: iterationNum,
283
+ completionTokens: response.usage.completionTokens,
683
284
  })
684
- }
685
-
686
- if (
687
- forceFinalize ||
688
- response.finishReason === 'stop' ||
689
- !response.message.toolCalls ||
690
- response.message.toolCalls.length === 0
691
- ) {
692
- const hasRunningTasks = this.hasRunningAgentTasks()
693
- const hasPendingNotifications = this.ctx.pendingNotifications.length > 0
694
-
695
- if (!forceFinalize && (hasRunningTasks || hasPendingNotifications)) {
696
- this.ctx.log.info(
697
- 'LLM ended turn but agent tasks still running — waiting for notifications',
698
- {
699
- runId: runMgr.id,
700
- runningTasks: hasRunningTasks,
701
- pendingNotifications: hasPendingNotifications,
702
- },
703
- )
704
-
705
- await this.ctx.emitEvent({
706
- type: 'iteration_completed',
707
- runId: runMgr.id,
708
- iteration: iterationNum,
709
- hasToolCalls: false,
710
- })
711
- yield* this.ctx.drainPending()
712
- iterSpan.end()
713
-
714
- yield* this.waitAndInjectNotifications()
715
- continue
716
- }
717
-
718
- const hasContent =
719
- response.message.content !== null && response.message.content.length > 0
720
-
721
- // Auto-continuation on `stop_reason: max_tokens`. The
722
- // model hit its per-call output cap mid-text (NOT
723
- // mid-tool-use — that path is handled separately
724
- // below via `inputTruncated`). Push a synthetic
725
- // "continue" user message and let the loop fire
726
- // another turn. The provider receives the partial
727
- // assistant content + the continue prompt and
728
- // resumes from where it left off, mirroring the
729
- // Claude.ai "Continue" affordance.
730
- //
731
- // Guards:
732
- // - `hasContent` so we don't loop forever on an
733
- // empty cutoff (Anthropic occasionally emits
734
- // `stop_reason: max_tokens` with no content
735
- // when an injected pre-fill blocks the model).
736
- // - `!forceFinalize` so the forced-finalize path
737
- // never auto-continues — that path is invoked
738
- // specifically to extract a closing summary.
739
- // - max_iterations bounds the loop in any case.
740
- if (!forceFinalize && response.finishReason === 'length' && hasContent) {
741
- this.ctx.log.info('LLM hit max_tokens mid-text — auto-continuing', {
742
- runId: runMgr.id,
743
- iteration: iterationNum,
744
- completionTokens: response.usage.completionTokens,
745
- })
746
- runMgr.pushMessage(createUserMessage(AUTO_CONTINUATION_USER_MESSAGE))
747
- await this.ctx.emitEvent({
748
- type: 'iteration_completed',
749
- runId: runMgr.id,
750
- iteration: iterationNum,
751
- hasToolCalls: false,
752
- })
753
- yield* this.ctx.drainPending()
754
- iterSpan.end()
755
- continue
756
- }
757
-
758
- if (!hasContent && !forceFinalize) {
759
- this.ctx.log.warn('Empty completion detected — requesting final summary', {
760
- iteration: iterationNum,
761
- finishReason: response.finishReason,
762
- })
763
- await this.requestFinalResponse(model, 'end_turn')
764
- yield* this.ctx.drainPending()
765
- }
766
-
285
+ runMgr.pushMessage(createUserMessage(AUTO_CONTINUATION_USER_MESSAGE))
767
286
  await this.ctx.emitEvent({
768
287
  type: 'iteration_completed',
769
288
  runId: runMgr.id,
@@ -771,38 +290,16 @@ export class IterationOrchestrator {
771
290
  hasToolCalls: false,
772
291
  })
773
292
  yield* this.ctx.drainPending()
774
- runMgr.setStopReason('end_turn')
775
- iterSpan.end()
776
- break
777
- }
778
-
779
- const reviewOutcome = yield* runToolReview(this.ctx, response, iterationNum)
780
-
781
- if (reviewOutcome === 'stop') {
782
- iterSpan.end()
783
- return
784
- }
785
-
786
- if (reviewOutcome === 'rejected') {
787
293
  iterSpan.end()
788
294
  continue
789
295
  }
790
296
 
791
- const checkpointSignal = yield* runIterationCheckpoint(this.ctx, iterationNum)
792
- if (checkpointSignal === 'stop') {
793
- iterSpan.end()
794
- return
795
- }
796
-
797
- await runAdvisoryPhase(this.ctx, iterationNum, response)
798
-
799
- if (this.ctx.pluginManager) {
800
- const hookResults = await this.ctx.pluginManager.executeHooks(
801
- 'iteration_end',
802
- { runId: runMgr.id, iteration: iterationNum },
803
- this.ctx.emitEvent,
804
- )
805
- applyLifecycleHookResults('iteration_end', hookResults)
297
+ if (!hasContent && !forceFinalize) {
298
+ this.ctx.log.warn('Empty completion detected — requesting final summary', {
299
+ iteration: iterationNum,
300
+ finishReason: response.finishReason,
301
+ })
302
+ await this.requestFinalResponse(model, 'end_turn')
806
303
  yield* this.ctx.drainPending()
807
304
  }
808
305
 
@@ -810,26 +307,89 @@ export class IterationOrchestrator {
810
307
  type: 'iteration_completed',
811
308
  runId: runMgr.id,
812
309
  iteration: iterationNum,
813
- hasToolCalls: true,
310
+ hasToolCalls: false,
814
311
  })
815
312
  yield* this.ctx.drainPending()
816
- iterSpan.end()
817
- } catch (err) {
818
- if (iterationActivity) {
819
- this.ctx.activityStore.fail(iterationActivity.id, toErrorMessage(err))
313
+ // A Stop that lands AFTER the final turn streamed but before
314
+ // this break must settle the run as cancelled, not end_turn —
315
+ // otherwise the just-produced answer is recorded as a clean
316
+ // completion. Mirrors the between-iteration cancel at :511.
317
+ if (this.ctx.abortController.signal.aborted) {
318
+ runMgr.setStopReason('cancelled')
319
+ runMgr.markCancelled()
320
+ iterSpan.end()
321
+ break
820
322
  }
323
+ runMgr.setStopReason('end_turn')
324
+ iterSpan.end()
325
+ break
326
+ }
821
327
 
822
- iterSpan.setStatus({
823
- code: SpanStatusCode.ERROR,
824
- message: toErrorMessage(err),
825
- })
826
- iterSpan.recordException(err instanceof Error ? err : new Error(String(err)))
328
+ const reviewOutcome = yield* runToolReview(this.ctx, response, iterationNum)
329
+
330
+ if (reviewOutcome === 'stop') {
331
+ iterSpan.end()
332
+ return
333
+ }
334
+
335
+ if (reviewOutcome === 'rejected') {
336
+ iterSpan.end()
337
+ continue
338
+ }
339
+
340
+ const checkpointSignal = yield* runIterationCheckpoint(this.ctx, iterationNum)
341
+ if (checkpointSignal === 'stop') {
827
342
  iterSpan.end()
828
- throw err
343
+ return
344
+ }
345
+
346
+ await runAdvisoryPhase(this.ctx, iterationNum, response)
347
+
348
+ if (this.ctx.pluginManager) {
349
+ const hookResults = await this.ctx.pluginManager.executeHooks(
350
+ 'iteration_end',
351
+ { runId: runMgr.id, iteration: iterationNum },
352
+ this.ctx.emitEvent,
353
+ )
354
+ applyLifecycleHookResults('iteration_end', hookResults)
355
+ yield* this.ctx.drainPending()
829
356
  }
357
+
358
+ await this.ctx.emitEvent({
359
+ type: 'iteration_completed',
360
+ runId: runMgr.id,
361
+ iteration: iterationNum,
362
+ hasToolCalls: true,
363
+ })
364
+ yield* this.ctx.drainPending()
365
+ iterSpan.end()
366
+ } catch (err) {
367
+ // A Stop that aborted the in-flight turn surfaces here as a
368
+ // thrown abort (the provider stream was raced against the run
369
+ // signal). Settle it as a CANCELLATION — mirroring the
370
+ // between-iteration cancel at the top of the loop — rather than
371
+ // recording it as an SDK failure (error span + failed activity)
372
+ // and re-throwing. The run then returns cleanly with a
373
+ // 'cancelled' stop reason instead of propagating an error.
374
+ if (this.ctx.abortController.signal.aborted) {
375
+ runMgr.setStopReason('cancelled')
376
+ runMgr.markCancelled()
377
+ iterSpan.end()
378
+ break
379
+ }
380
+
381
+ if (iterationActivity) {
382
+ this.ctx.activityStore.fail(iterationActivity.id, toErrorMessage(err))
383
+ }
384
+
385
+ iterSpan.setStatus({
386
+ code: SpanStatusCode.ERROR,
387
+ message: toErrorMessage(err),
388
+ })
389
+ iterSpan.recordException(err instanceof Error ? err : new Error(String(err)))
390
+ iterSpan.end()
391
+ throw err
830
392
  }
831
- } finally {
832
- unsubscribeTaskListener?.()
833
393
  }
834
394
  }
835
395
 
@@ -840,90 +400,6 @@ export class IterationOrchestrator {
840
400
  .some((t) => t.state !== 'completed' && t.state !== 'failed' && t.state !== 'canceled')
841
401
  }
842
402
 
843
- private async *waitAndInjectNotifications(): AsyncGenerator<RunEvent> {
844
- const deadline = Date.now() + (this.ctx.runConfig.timeoutMs ?? 120_000)
845
- while (
846
- this.ctx.pendingNotifications.length === 0 &&
847
- Date.now() < deadline &&
848
- !this.ctx.abortController.signal.aborted
849
- ) {
850
- await new Promise((r) => setTimeout(r, 250))
851
- }
852
-
853
- await this.injectOneTaskNotification()
854
- }
855
-
856
- /**
857
- * Canonical async completion delivery (ses_009-task-notification-envelope).
858
- *
859
- * Drains every pending task completion in one pass and emits each as
860
- * a plain USER text message wrapped in the `<task-notification>`
861
- * envelope the supervisor prompt expects.
862
- *
863
- * Why not a `tool_result` block bound to the dispatching tool_use_id:
864
- * `create_task` is documented as NON-BLOCKING and returns
865
- * "Task launched: …" immediately. That immediate return is already
866
- * recorded as the canonical tool_result for that tool_use, so a
867
- * second tool_result for the SAME tool_use_id — emitted later, after
868
- * intervening assistant turns — is rejected by Anthropic with
869
- * `messages.<n>.content.0: unexpected tool_use_id found in
870
- * tool_result blocks` because the immediately-prior assistant
871
- * message no longer carries the matching tool_use. Wrapping as a
872
- * user text envelope sidesteps the pairing rule entirely.
873
- *
874
- * Coalescing N drops into one drain replaces the previous
875
- * one-at-a-time pattern which forced a separate orchestrator
876
- * iteration per completed task on wide fan-outs.
877
- */
878
- private async injectOneTaskNotification(): Promise<void> {
879
- if (this.ctx.pendingNotifications.length === 0) return
880
- const handles = this.ctx.pendingNotifications.splice(0)
881
-
882
- for (const handle of handles) {
883
- const meta = this.ctx.launchedTasks.get(handle.taskId)
884
- const resultText =
885
- handle.result?.result ??
886
- handle.result?.lastError ??
887
- `Task finished with state: ${handle.state}`
888
-
889
- if (meta?.planTaskId && this.ctx.taskStore) {
890
- const success = handle.state === 'completed'
891
- await this.ctx.taskStore.update(meta.planTaskId as `task_${string}`, {
892
- status: 'completed',
893
- description: success ? undefined : `Failed: ${resultText.substring(0, 200)}`,
894
- })
895
- }
896
-
897
- this.ctx.launchedTasks.delete(handle.taskId)
898
-
899
- // `remaining-tasks` = inflight workers still pending after this
900
- // one drains. `launchedTasks` is the single source of truth:
901
- // it holds every dispatched worker that has NOT yet been
902
- // drained + delete()'d. The drain batch entries are still
903
- // inside launchedTasks until each iteration's delete() above
904
- // removes them, so reading the size right after that delete
905
- // gives the honest count. Adding `handles.length - 1 - i`
906
- // here used to double-count this same queue.
907
- const remainingTasks = this.ctx.launchedTasks.size
908
- const envelope =
909
- `<task-notification>\n<task-id>${xmlEscape(handle.taskId)}</task-id>\n` +
910
- `<agent-id>${xmlEscape(handle.agentId)}</agent-id>\n` +
911
- `<status>${xmlEscape(handle.state)}</status>\n` +
912
- `<result>${cdataWrap(resultText)}</result>\n` +
913
- `<remaining-tasks>${remainingTasks}</remaining-tasks>\n</task-notification>`
914
-
915
- this.ctx.runMgr.pushMessage(createUserMessage(envelope))
916
-
917
- this.ctx.log.info('Task notification injected', {
918
- taskId: handle.taskId,
919
- agentId: handle.agentId,
920
- state: handle.state,
921
- planTaskId: meta?.planTaskId,
922
- remainingNotifications: remainingTasks,
923
- })
924
- }
925
- }
926
-
927
403
  private async requestFinalResponse(model: string, reason: StopReason): Promise<void> {
928
404
  const lastAssistant = [...this.ctx.runMgr.messages]
929
405
  .reverse()
@@ -948,13 +424,22 @@ export class IterationOrchestrator {
948
424
  ),
949
425
  ]
950
426
 
427
+ // Same cache discipline as the forced-final iteration: keep the
428
+ // tools param identical to prior iterations (cache prefix intact,
429
+ // no 400 on tool blocks in history) and forbid use via tool_choice.
430
+ const finalTools = this.ctx.tools.toLLMTools(this.ctx.allowedTools)
951
431
  const response = await collect(
952
432
  this.ctx.provider.chatStream({
953
433
  model,
954
434
  messages: finalMessages,
435
+ tools: finalTools.length > 0 ? finalTools : undefined,
436
+ toolChoice: finalTools.length > 0 ? 'none' : undefined,
955
437
  temperature: this.ctx.runConfig.temperature,
956
438
  maxTokens: this.ctx.runConfig.maxResponseTokens,
957
439
  cacheControl: { type: 'auto' },
440
+ // Cancellable too: a Stop during the closing summary must not
441
+ // stream to completion.
442
+ signal: this.ctx.abortController.signal,
958
443
  }),
959
444
  )
960
445