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