@tomsun28/pizza 0.0.9 → 0.1.1

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 (382) hide show
  1. package/README.md +20 -143
  2. package/README.zh-CN.md +36 -0
  3. package/dist/bun/cli.d.ts +2 -0
  4. package/dist/bun/cli.js +6 -0
  5. package/dist/bun/register-bedrock.d.ts +1 -0
  6. package/dist/bun/register-bedrock.js +3 -0
  7. package/dist/cli/args.d.ts +48 -0
  8. package/dist/cli/args.js +330 -0
  9. package/dist/cli/config-selector.d.ts +13 -0
  10. package/dist/cli/config-selector.js +30 -0
  11. package/dist/cli/file-processor.d.ts +14 -0
  12. package/dist/cli/file-processor.js +82 -0
  13. package/dist/cli/initial-message.d.ts +17 -0
  14. package/dist/cli/initial-message.js +21 -0
  15. package/dist/cli/list-models.d.ts +8 -0
  16. package/dist/cli/list-models.js +96 -0
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +68 -0
  19. package/dist/config.d.ts +79 -0
  20. package/dist/config.js +223 -0
  21. package/dist/core/agent/index.d.ts +6 -0
  22. package/dist/core/agent/index.js +6 -0
  23. package/dist/core/agent/types.d.ts +202 -0
  24. package/dist/core/agent/types.js +51 -0
  25. package/dist/core/auth-storage.d.ts +135 -0
  26. package/dist/core/auth-storage.js +417 -0
  27. package/dist/core/bash-executor.d.ts +31 -0
  28. package/dist/core/bash-executor.js +107 -0
  29. package/dist/core/compaction/branch-summarization.d.ts +87 -0
  30. package/dist/core/compaction/branch-summarization.js +242 -0
  31. package/dist/core/compaction/compaction-engine.d.ts +40 -0
  32. package/dist/core/compaction/compaction-engine.js +292 -0
  33. package/dist/core/compaction/compaction.d.ts +106 -0
  34. package/dist/core/compaction/compaction.js +295 -0
  35. package/dist/core/compaction/index.d.ts +7 -0
  36. package/dist/core/compaction/index.js +7 -0
  37. package/dist/core/compaction/utils.d.ts +37 -0
  38. package/dist/core/compaction/utils.js +152 -0
  39. package/dist/core/defaults.d.ts +2 -0
  40. package/dist/core/defaults.js +1 -0
  41. package/dist/core/diagnostics.d.ts +14 -0
  42. package/dist/core/diagnostics.js +1 -0
  43. package/dist/core/event-bus.d.ts +8 -0
  44. package/dist/core/event-bus.js +24 -0
  45. package/dist/core/event-store/events.d.ts +465 -0
  46. package/dist/core/event-store/events.js +6 -0
  47. package/dist/core/event-store/index.d.ts +10 -0
  48. package/dist/core/event-store/index.js +10 -0
  49. package/dist/core/event-store/sqlite-store.d.ts +27 -0
  50. package/dist/core/event-store/sqlite-store.js +275 -0
  51. package/dist/core/event-store/store.d.ts +91 -0
  52. package/dist/core/event-store/store.js +7 -0
  53. package/dist/core/event-store/thread-scoped-store.d.ts +29 -0
  54. package/dist/core/event-store/thread-scoped-store.js +50 -0
  55. package/dist/core/event-store/types.d.ts +70 -0
  56. package/dist/core/event-store/types.js +7 -0
  57. package/dist/core/event-store/workspace.d.ts +37 -0
  58. package/dist/core/event-store/workspace.js +71 -0
  59. package/dist/core/exec.d.ts +28 -0
  60. package/dist/core/exec.js +74 -0
  61. package/dist/core/export-html/ansi-to-html.d.ts +21 -0
  62. package/dist/core/export-html/ansi-to-html.js +248 -0
  63. package/dist/core/export-html/index.d.ts +29 -0
  64. package/dist/core/export-html/index.js +186 -0
  65. package/dist/core/export-html/template.css +1017 -0
  66. package/dist/core/export-html/template.html +55 -0
  67. package/dist/core/export-html/template.js +1742 -0
  68. package/dist/core/export-html/tool-renderer.d.ts +39 -0
  69. package/dist/core/export-html/tool-renderer.js +94 -0
  70. package/dist/core/export-html/vendor/highlight.min.js +1213 -0
  71. package/dist/core/export-html/vendor/marked.min.js +6 -0
  72. package/dist/core/extensions/index.d.ts +12 -0
  73. package/dist/core/extensions/index.js +9 -0
  74. package/dist/core/extensions/loader.d.ts +24 -0
  75. package/dist/core/extensions/loader.js +492 -0
  76. package/dist/core/extensions/runner.d.ts +167 -0
  77. package/dist/core/extensions/runner.js +898 -0
  78. package/dist/core/extensions/session-context.d.ts +67 -0
  79. package/dist/core/extensions/session-context.js +272 -0
  80. package/dist/core/extensions/types.d.ts +1147 -0
  81. package/dist/core/extensions/types.js +44 -0
  82. package/dist/core/extensions/wrapper.d.ts +19 -0
  83. package/dist/core/extensions/wrapper.js +21 -0
  84. package/dist/core/footer-data-provider.d.ts +47 -0
  85. package/dist/core/footer-data-provider.js +321 -0
  86. package/dist/core/goal/index.d.ts +3 -0
  87. package/dist/core/goal/index.js +1 -0
  88. package/dist/core/goal/scheduler.d.ts +83 -0
  89. package/dist/core/goal/scheduler.js +154 -0
  90. package/dist/core/goal/types.d.ts +182 -0
  91. package/dist/core/goal/types.js +11 -0
  92. package/dist/core/index.d.ts +8 -0
  93. package/dist/core/index.js +8 -0
  94. package/dist/core/intent/classifier.d.ts +53 -0
  95. package/dist/core/intent/classifier.js +353 -0
  96. package/dist/core/intent/index.d.ts +8 -0
  97. package/dist/core/intent/index.js +8 -0
  98. package/dist/core/intent/tool-adapter.d.ts +31 -0
  99. package/dist/core/intent/tool-adapter.js +168 -0
  100. package/dist/core/intent/types.d.ts +71 -0
  101. package/dist/core/intent/types.js +6 -0
  102. package/dist/core/keybindings.d.ts +297 -0
  103. package/dist/core/keybindings.js +250 -0
  104. package/dist/core/messages.d.ts +30 -0
  105. package/dist/core/messages.js +134 -0
  106. package/dist/core/model-registry.d.ts +131 -0
  107. package/dist/core/model-registry.js +634 -0
  108. package/dist/core/model-resolver.d.ts +109 -0
  109. package/dist/core/model-resolver.js +486 -0
  110. package/dist/core/output-guard.d.ts +5 -0
  111. package/dist/core/output-guard.js +58 -0
  112. package/dist/core/package-manager.d.ts +196 -0
  113. package/dist/core/package-manager.js +1873 -0
  114. package/dist/core/projection/boundary-inferrer.d.ts +58 -0
  115. package/dist/core/projection/boundary-inferrer.js +122 -0
  116. package/dist/core/projection/event-to-message.d.ts +35 -0
  117. package/dist/core/projection/event-to-message.js +253 -0
  118. package/dist/core/projection/goal-projection.d.ts +67 -0
  119. package/dist/core/projection/goal-projection.js +339 -0
  120. package/dist/core/projection/index.d.ts +13 -0
  121. package/dist/core/projection/index.js +13 -0
  122. package/dist/core/projection/session-manager.d.ts +100 -0
  123. package/dist/core/projection/session-manager.js +331 -0
  124. package/dist/core/projection/session-projection.d.ts +58 -0
  125. package/dist/core/projection/session-projection.js +257 -0
  126. package/dist/core/projection/task-history.d.ts +13 -0
  127. package/dist/core/projection/task-history.js +61 -0
  128. package/dist/core/projection/timeline-projection.d.ts +82 -0
  129. package/dist/core/projection/timeline-projection.js +276 -0
  130. package/dist/core/projection/types.d.ts +69 -0
  131. package/dist/core/projection/types.js +6 -0
  132. package/dist/core/prompt-templates.d.ts +51 -0
  133. package/dist/core/prompt-templates.js +249 -0
  134. package/dist/core/resolve-config-value.d.ts +22 -0
  135. package/dist/core/resolve-config-value.js +125 -0
  136. package/dist/core/resource-loader.d.ts +193 -0
  137. package/dist/core/resource-loader.js +685 -0
  138. package/dist/core/runtime/ai-client.d.ts +12 -0
  139. package/dist/core/runtime/ai-client.js +153 -0
  140. package/dist/core/runtime/checkpoint.d.ts +12 -0
  141. package/dist/core/runtime/checkpoint.js +78 -0
  142. package/dist/core/runtime/index.d.ts +12 -0
  143. package/dist/core/runtime/index.js +12 -0
  144. package/dist/core/runtime/llm-types.d.ts +99 -0
  145. package/dist/core/runtime/llm-types.js +6 -0
  146. package/dist/core/runtime/local-runtime.d.ts +23 -0
  147. package/dist/core/runtime/local-runtime.js +59 -0
  148. package/dist/core/runtime/policies.d.ts +72 -0
  149. package/dist/core/runtime/policies.js +37 -0
  150. package/dist/core/runtime/reactor.d.ts +213 -0
  151. package/dist/core/runtime/reactor.js +992 -0
  152. package/dist/core/runtime/runtime.d.ts +191 -0
  153. package/dist/core/runtime/runtime.js +449 -0
  154. package/dist/core/runtime/types.d.ts +42 -0
  155. package/dist/core/runtime/types.js +1 -0
  156. package/dist/core/sdk.d.ts +12 -0
  157. package/dist/core/sdk.js +10 -0
  158. package/dist/core/session-context-builder.d.ts +9 -0
  159. package/dist/core/session-context-builder.js +87 -0
  160. package/dist/core/session-cwd.d.ts +18 -0
  161. package/dist/core/session-cwd.js +36 -0
  162. package/dist/core/session-facade-factory.d.ts +79 -0
  163. package/dist/core/session-facade-factory.js +460 -0
  164. package/dist/core/session-facade.d.ts +57 -0
  165. package/dist/core/session-facade.js +91 -0
  166. package/dist/core/session-listing.d.ts +30 -0
  167. package/dist/core/session-listing.js +168 -0
  168. package/dist/core/session-reader.d.ts +19 -0
  169. package/dist/core/session-reader.js +101 -0
  170. package/dist/core/session-ref.d.ts +7 -0
  171. package/dist/core/session-ref.js +25 -0
  172. package/dist/core/session-services.d.ts +51 -0
  173. package/dist/core/session-services.js +90 -0
  174. package/dist/core/session-stats.d.ts +43 -0
  175. package/dist/core/session-stats.js +41 -0
  176. package/dist/core/settings-manager.d.ts +236 -0
  177. package/dist/core/settings-manager.js +706 -0
  178. package/dist/core/skill-block-parser.d.ts +12 -0
  179. package/dist/core/skill-block-parser.js +15 -0
  180. package/dist/core/skills.d.ts +59 -0
  181. package/dist/core/skills.js +408 -0
  182. package/dist/core/slash-commands.d.ts +13 -0
  183. package/dist/core/slash-commands.js +17 -0
  184. package/dist/core/source-info.d.ts +17 -0
  185. package/dist/core/source-info.js +18 -0
  186. package/dist/core/system-prompt.d.ts +27 -0
  187. package/dist/core/system-prompt.js +179 -0
  188. package/dist/core/timings.d.ts +7 -0
  189. package/dist/core/timings.js +30 -0
  190. package/dist/core/tools/bash.d.ts +95 -0
  191. package/dist/core/tools/bash.js +513 -0
  192. package/dist/core/tools/builtin-commands.d.ts +75 -0
  193. package/dist/core/tools/builtin-commands.js +503 -0
  194. package/dist/core/tools/compat-commands.d.ts +12 -0
  195. package/dist/core/tools/compat-commands.js +1280 -0
  196. package/dist/core/tools/edit-diff.d.ts +59 -0
  197. package/dist/core/tools/edit-diff.js +263 -0
  198. package/dist/core/tools/edit.d.ts +49 -0
  199. package/dist/core/tools/edit.js +340 -0
  200. package/dist/core/tools/file-mutation-queue.d.ts +5 -0
  201. package/dist/core/tools/file-mutation-queue.js +36 -0
  202. package/dist/core/tools/find.d.ts +34 -0
  203. package/dist/core/tools/find.js +459 -0
  204. package/dist/core/tools/grep.d.ts +36 -0
  205. package/dist/core/tools/grep.js +422 -0
  206. package/dist/core/tools/index.d.ts +70 -0
  207. package/dist/core/tools/index.js +129 -0
  208. package/dist/core/tools/line-anchors.d.ts +33 -0
  209. package/dist/core/tools/line-anchors.js +151 -0
  210. package/dist/core/tools/ls.d.ts +36 -0
  211. package/dist/core/tools/ls.js +268 -0
  212. package/dist/core/tools/path-utils.d.ts +7 -0
  213. package/dist/core/tools/path-utils.js +80 -0
  214. package/dist/core/tools/read.d.ts +35 -0
  215. package/dist/core/tools/read.js +244 -0
  216. package/dist/core/tools/render-utils.d.ts +20 -0
  217. package/dist/core/tools/render-utils.js +48 -0
  218. package/dist/core/tools/session-split.d.ts +9 -0
  219. package/dist/core/tools/session-split.js +77 -0
  220. package/dist/core/tools/tool-definition-wrapper.d.ts +13 -0
  221. package/dist/core/tools/tool-definition-wrapper.js +32 -0
  222. package/dist/core/tools/truncate.d.ts +69 -0
  223. package/dist/core/tools/truncate.js +204 -0
  224. package/dist/core/tools/write.d.ts +25 -0
  225. package/dist/core/tools/write.js +211 -0
  226. package/dist/core/types/session-types.d.ts +123 -0
  227. package/dist/core/types/session-types.js +8 -0
  228. package/dist/index.d.ts +27 -0
  229. package/dist/index.js +38 -0
  230. package/dist/main.d.ts +11 -2
  231. package/dist/main.js +577 -149
  232. package/dist/modes/event-mapper.d.ts +115 -0
  233. package/dist/modes/event-mapper.js +200 -0
  234. package/dist/modes/gui/server.d.ts +14 -0
  235. package/dist/modes/gui/server.js +1032 -0
  236. package/dist/modes/index.d.ts +10 -0
  237. package/dist/modes/index.js +9 -0
  238. package/dist/modes/interactive/assets/clankolas.png +0 -0
  239. package/dist/modes/interactive/components/armin.d.ts +33 -0
  240. package/dist/modes/interactive/components/armin.js +328 -0
  241. package/dist/modes/interactive/components/assistant-message.d.ts +19 -0
  242. package/dist/modes/interactive/components/assistant-message.js +115 -0
  243. package/dist/modes/interactive/components/bash-execution.d.ts +33 -0
  244. package/dist/modes/interactive/components/bash-execution.js +169 -0
  245. package/dist/modes/interactive/components/bordered-loader.d.ts +15 -0
  246. package/dist/modes/interactive/components/bordered-loader.js +50 -0
  247. package/dist/modes/interactive/components/branch-summary-message.d.ts +15 -0
  248. package/dist/modes/interactive/components/branch-summary-message.js +41 -0
  249. package/dist/modes/interactive/components/brand-header.d.ts +33 -0
  250. package/dist/modes/interactive/components/brand-header.js +235 -0
  251. package/dist/modes/interactive/components/compaction-summary-message.d.ts +15 -0
  252. package/dist/modes/interactive/components/compaction-summary-message.js +42 -0
  253. package/dist/modes/interactive/components/config-selector.d.ts +70 -0
  254. package/dist/modes/interactive/components/config-selector.js +469 -0
  255. package/dist/modes/interactive/components/countdown-timer.d.ts +13 -0
  256. package/dist/modes/interactive/components/countdown-timer.js +27 -0
  257. package/dist/modes/interactive/components/custom-editor.d.ts +27 -0
  258. package/dist/modes/interactive/components/custom-editor.js +94 -0
  259. package/dist/modes/interactive/components/custom-message.d.ts +19 -0
  260. package/dist/modes/interactive/components/custom-message.js +73 -0
  261. package/dist/modes/interactive/components/daxnuts.d.ts +22 -0
  262. package/dist/modes/interactive/components/daxnuts.js +137 -0
  263. package/dist/modes/interactive/components/diff.d.ts +11 -0
  264. package/dist/modes/interactive/components/diff.js +132 -0
  265. package/dist/modes/interactive/components/dynamic-border.d.ts +14 -0
  266. package/dist/modes/interactive/components/dynamic-border.js +19 -0
  267. package/dist/modes/interactive/components/earendil-announcement.d.ts +4 -0
  268. package/dist/modes/interactive/components/earendil-announcement.js +39 -0
  269. package/dist/modes/interactive/components/extension-editor.d.ts +19 -0
  270. package/dist/modes/interactive/components/extension-editor.js +105 -0
  271. package/dist/modes/interactive/components/extension-input.d.ts +22 -0
  272. package/dist/modes/interactive/components/extension-input.js +54 -0
  273. package/dist/modes/interactive/components/extension-selector.d.ts +23 -0
  274. package/dist/modes/interactive/components/extension-selector.js +70 -0
  275. package/dist/modes/interactive/components/footer.d.ts +55 -0
  276. package/dist/modes/interactive/components/footer.js +207 -0
  277. package/dist/modes/interactive/components/index.d.ts +31 -0
  278. package/dist/modes/interactive/components/index.js +32 -0
  279. package/dist/modes/interactive/components/keybinding-hints.d.ts +7 -0
  280. package/dist/modes/interactive/components/keybinding-hints.js +21 -0
  281. package/dist/modes/interactive/components/login-dialog.d.ts +41 -0
  282. package/dist/modes/interactive/components/login-dialog.js +138 -0
  283. package/dist/modes/interactive/components/model-selector.d.ts +32 -0
  284. package/dist/modes/interactive/components/model-selector.js +209 -0
  285. package/dist/modes/interactive/components/oauth-selector.d.ts +18 -0
  286. package/dist/modes/interactive/components/oauth-selector.js +91 -0
  287. package/dist/modes/interactive/components/settings-selector.d.ts +57 -0
  288. package/dist/modes/interactive/components/settings-selector.js +299 -0
  289. package/dist/modes/interactive/components/show-images-selector.d.ts +9 -0
  290. package/dist/modes/interactive/components/show-images-selector.js +37 -0
  291. package/dist/modes/interactive/components/skill-invocation-message.d.ts +16 -0
  292. package/dist/modes/interactive/components/skill-invocation-message.js +44 -0
  293. package/dist/modes/interactive/components/theme-selector.d.ts +10 -0
  294. package/dist/modes/interactive/components/theme-selector.js +47 -0
  295. package/dist/modes/interactive/components/thinking-selector.d.ts +10 -0
  296. package/dist/modes/interactive/components/thinking-selector.js +50 -0
  297. package/dist/modes/interactive/components/timeline-view.d.ts +31 -0
  298. package/dist/modes/interactive/components/timeline-view.js +81 -0
  299. package/dist/modes/interactive/components/tool-execution.d.ts +64 -0
  300. package/dist/modes/interactive/components/tool-execution.js +305 -0
  301. package/dist/modes/interactive/components/tree-selector.d.ts +88 -0
  302. package/dist/modes/interactive/components/tree-selector.js +1072 -0
  303. package/dist/modes/interactive/components/user-message-selector.d.ts +29 -0
  304. package/dist/modes/interactive/components/user-message-selector.js +110 -0
  305. package/dist/modes/interactive/components/user-message.d.ts +9 -0
  306. package/dist/modes/interactive/components/user-message.js +27 -0
  307. package/dist/modes/interactive/components/visual-truncate.d.ts +23 -0
  308. package/dist/modes/interactive/components/visual-truncate.js +32 -0
  309. package/dist/modes/interactive/interactive-mode.d.ts +394 -0
  310. package/dist/modes/interactive/interactive-mode.js +4116 -0
  311. package/dist/modes/interactive/theme/dark.json +85 -0
  312. package/dist/modes/interactive/theme/light.json +84 -0
  313. package/dist/modes/interactive/theme/theme-schema.json +335 -0
  314. package/dist/modes/interactive/theme/theme.d.ts +80 -0
  315. package/dist/modes/interactive/theme/theme.js +973 -0
  316. package/dist/modes/print-mode.d.ts +27 -0
  317. package/dist/modes/print-mode.js +102 -0
  318. package/dist/modes/rpc/jsonl.d.ts +16 -0
  319. package/dist/modes/rpc/jsonl.js +48 -0
  320. package/dist/modes/rpc/rpc-client.d.ts +219 -0
  321. package/dist/modes/rpc/rpc-client.js +405 -0
  322. package/dist/modes/rpc/rpc-mode.d.ts +19 -0
  323. package/dist/modes/rpc/rpc-mode.js +381 -0
  324. package/dist/modes/rpc/rpc-types.d.ts +406 -0
  325. package/dist/modes/rpc/rpc-types.js +7 -0
  326. package/dist/package-manager-cli.d.ts +3 -0
  327. package/dist/package-manager-cli.js +233 -0
  328. package/dist/utils/changelog.d.ts +20 -0
  329. package/dist/utils/changelog.js +86 -0
  330. package/dist/utils/child-process.d.ts +10 -0
  331. package/dist/utils/child-process.js +77 -0
  332. package/dist/utils/clipboard-image.d.ts +10 -0
  333. package/dist/utils/clipboard-image.js +244 -0
  334. package/dist/utils/clipboard-native.d.ts +7 -0
  335. package/dist/utils/clipboard-native.js +13 -0
  336. package/dist/utils/clipboard.d.ts +1 -0
  337. package/dist/utils/clipboard.js +77 -0
  338. package/dist/utils/exif-orientation.d.ts +4 -0
  339. package/dist/utils/exif-orientation.js +157 -0
  340. package/dist/utils/frontmatter.d.ts +7 -0
  341. package/dist/utils/frontmatter.js +25 -0
  342. package/dist/utils/git.d.ts +25 -0
  343. package/dist/utils/git.js +162 -0
  344. package/dist/utils/image-convert.d.ts +8 -0
  345. package/dist/utils/image-convert.js +38 -0
  346. package/dist/utils/image-resize.d.ts +35 -0
  347. package/dist/utils/image-resize.js +136 -0
  348. package/dist/utils/mime.d.ts +1 -0
  349. package/dist/utils/mime.js +25 -0
  350. package/dist/utils/path-shims.d.ts +1 -0
  351. package/dist/utils/path-shims.js +90 -0
  352. package/dist/utils/paths.d.ts +6 -0
  353. package/dist/utils/paths.js +18 -0
  354. package/dist/utils/photon.d.ts +20 -0
  355. package/dist/utils/photon.js +120 -0
  356. package/dist/utils/shell.d.ts +29 -0
  357. package/dist/utils/shell.js +189 -0
  358. package/dist/utils/sleep.d.ts +4 -0
  359. package/dist/utils/sleep.js +16 -0
  360. package/dist/utils/tools-manager.d.ts +3 -0
  361. package/dist/utils/tools-manager.js +101 -0
  362. package/dist/vendor/bin/darwin-arm64/fd +0 -0
  363. package/dist/vendor/bin/darwin-arm64/rg +0 -0
  364. package/dist/vendor/bin/darwin-x64/fd +0 -0
  365. package/dist/vendor/bin/darwin-x64/rg +0 -0
  366. package/dist/vendor/bin/linux-arm64/fd +0 -0
  367. package/dist/vendor/bin/linux-arm64/rg +0 -0
  368. package/dist/vendor/bin/linux-x64/fd +0 -0
  369. package/dist/vendor/bin/linux-x64/rg +0 -0
  370. package/dist/vendor/bin/win32-arm64/fd.exe +0 -0
  371. package/dist/vendor/bin/win32-arm64/rg.exe +0 -0
  372. package/dist/vendor/bin/win32-x64/fd.exe +0 -0
  373. package/dist/vendor/bin/win32-x64/rg.exe +0 -0
  374. package/package.json +90 -26
  375. package/dist/extension/core.d.ts +0 -69
  376. package/dist/extension/core.js +0 -270
  377. package/dist/extension/core.js.map +0 -1
  378. package/dist/extension/index.d.ts +0 -25
  379. package/dist/extension/index.js +0 -303
  380. package/dist/extension/index.js.map +0 -1
  381. package/dist/main.js.map +0 -1
  382. package/themes/pizza.json +0 -87
@@ -0,0 +1,992 @@
1
+ /**
2
+ * Reactor — Event-Driven Agent Core
3
+ *
4
+ * Replaces the AgentLoop while-loop with a set of typed handlers that react
5
+ * to events appended to the EventStore. Each handler is a pure async function
6
+ * that may emit new events. The reactor itself is stateless — all mutable
7
+ * state lives in the EventStore.
8
+ *
9
+ * Event flow:
10
+ *
11
+ * USER_MESSAGE / USER_INTERRUPT / TOOL_RESULTS_AGGREGATED / RETRY_SCHEDULED
12
+ * ↓
13
+ * [onUserMessage] ──→ emit AGENT_TURN_REQUESTED
14
+ * ↓
15
+ * [onAgentTurnRequested] ──→ emit LLM_CALL_REQUESTED
16
+ * ↓
17
+ * [onLlmCallRequested] ──→ call LLM → emit AGENT_MESSAGE_START/CHUNK/END
18
+ * ↓
19
+ * [onAgentMessageEnd]
20
+ * ├── if no tool_calls → emit AGENT_TURN_COMPLETED
21
+ * └── if tool_calls → emit INTENT_TOOL_CALL (one per call)
22
+ * ↓
23
+ * [onIntentToolCall] ──→ (approval gate) → emit TOOL_EXECUTION_START/END
24
+ * ↓
25
+ * [onToolExecutionEnd] ──→ track count → if all done → emit TOOL_RESULTS_AGGREGATED
26
+ * ↓
27
+ * [onToolResultsAggregated] ──→ emit AGENT_TURN_REQUESTED ← loop back
28
+ *
29
+ * Other handlers (run in parallel, do not gate the main flow):
30
+ * onAgentTurnCompleted — UI notification
31
+ * onLlmCallFailed — retry scheduling
32
+ * onUserInterrupt — abort signal + turn completion
33
+ * onCompactionRequested — compaction orchestration
34
+ */
35
+ import { extractToolCalls } from "../projection/event-to-message.js";
36
+ import { DefaultRetryPolicy, NoopCompactionPolicy } from "./policies.js";
37
+ /**
38
+ * Safety limit: if the model calls the exact same set of tools (by name) in this
39
+ * many consecutive turns, the reactor assumes it is stuck in an unproductive loop
40
+ * and completes the turn instead of requesting another. Prevents runaway loops
41
+ * (e.g. a model repeatedly calling a control tool like session_split).
42
+ */
43
+ const MAX_CONSECUTIVE_IDENTICAL_TOOL_ROUNDS = 6;
44
+ export class Reactor {
45
+ constructor(config) {
46
+ this.turnTrackers = new Map();
47
+ this.handlers = {};
48
+ this.unsubscribers = [];
49
+ this._isRunning = false;
50
+ /** Pending follow-up messages (delivered after current turn completes without interrupt). */
51
+ this.followUpQueue = [];
52
+ /** Retry timers waiting to re-enter the turn loop. */
53
+ this.retryTimers = new Map();
54
+ /** Set when abort() is called with no new content — next turn completion should discard queued follow-ups. */
55
+ this._abortedByUser = false;
56
+ /** Tool names called in each consecutive tool-use round within the current prompt cycle. */
57
+ this._toolRoundSignatures = [];
58
+ // ─── INTENT_TOOL_CALL ──────────────────────────────────────────────────
59
+ this._pendingApprovals = new Map();
60
+ this.config = config;
61
+ this.retryPolicy = config.retryPolicy ?? new DefaultRetryPolicy();
62
+ this.compactionPolicy = config.compactionPolicy ?? new NoopCompactionPolicy();
63
+ this._registerAllHandlers();
64
+ }
65
+ // =========================================================================
66
+ // Lifecycle
67
+ // =========================================================================
68
+ /** Subscribe to all events and start the reactor. */
69
+ async start() {
70
+ if (this._isRunning)
71
+ return;
72
+ this._isRunning = true;
73
+ this.abortController = new AbortController();
74
+ // Subscribe to the event store — all events flow through the store
75
+ const unsub = this.config.store.subscribe((event) => this._dispatch(event));
76
+ this.unsubscribers.push(unsub);
77
+ // Replay any pre-existing follow-up events that were queued while the reactor was idle
78
+ this._replayPendingFollowUps();
79
+ // Emit RUNTIME_STARTED if the store is fresh
80
+ if (this.config.store.size === 0) {
81
+ this._emit({
82
+ actor_id: "runtime",
83
+ type: "RUNTIME_STARTED",
84
+ payload: {},
85
+ });
86
+ }
87
+ }
88
+ /**
89
+ * Replay USER_FOLLOWUP_QUEUED events that were appended while the reactor was idle.
90
+ * Skips events that have already been consumed (i.e. an AGENT_TURN_COMPLETED with a
91
+ * caused_by chain that includes them).
92
+ *
93
+ * For now we use a simple heuristic: count followups vs followup-driven user messages.
94
+ * If followups outnumber the consumed ones, replay the difference into the in-memory queue.
95
+ */
96
+ _replayPendingFollowUps() {
97
+ const followups = this.config.store.query({ types: ["USER_FOLLOWUP_QUEUED"] });
98
+ if (followups.length === 0)
99
+ return;
100
+ // A USER_FOLLOWUP_QUEUED is "consumed" when a USER_MESSAGE event has it as caused_by.
101
+ const userMessages = this.config.store.query({ types: ["USER_MESSAGE"] });
102
+ const consumedCausedBy = new Set(userMessages.map((m) => m.caused_by).filter((id) => !!id));
103
+ for (const f of followups) {
104
+ if (consumedCausedBy.has(f.event_id))
105
+ continue;
106
+ const p = f.payload;
107
+ this.followUpQueue.push({ content: p.content, images: p.images });
108
+ }
109
+ }
110
+ /** Stop the reactor. Clears all subscriptions. */
111
+ stop() {
112
+ this._isRunning = false;
113
+ this.abortController?.abort();
114
+ for (const { timeout } of this.retryTimers.values()) {
115
+ clearTimeout(timeout);
116
+ }
117
+ this.retryTimers.clear();
118
+ for (const unsub of this.unsubscribers)
119
+ unsub();
120
+ this.unsubscribers = [];
121
+ }
122
+ /** Check if the reactor is running. */
123
+ get isRunning() {
124
+ return this._isRunning;
125
+ }
126
+ /** Abort signal for the currently active reactor run. */
127
+ get signal() {
128
+ return this.abortController?.signal;
129
+ }
130
+ // =========================================================================
131
+ // Event dispatch
132
+ // =========================================================================
133
+ async _dispatch(event) {
134
+ const handler = this.handlers[event.type];
135
+ if (!handler)
136
+ return;
137
+ try {
138
+ await handler(event);
139
+ }
140
+ catch (err) {
141
+ // Emit as RUNTIME_ERROR instead of throwing — the reactor must keep running
142
+ const msg = err instanceof Error ? err.message : String(err);
143
+ this._emit({
144
+ actor_id: "runtime",
145
+ type: "RUNTIME_ERROR",
146
+ payload: { error: msg, stack: err instanceof Error ? err.stack : undefined, causing_event_id: event.event_id },
147
+ });
148
+ }
149
+ }
150
+ _emit(input) {
151
+ return this.config.store.append(input);
152
+ }
153
+ /** Check if the reactor has been asked to stop. */
154
+ _shouldInterrupt() {
155
+ return this.abortController?.signal.aborted ?? false;
156
+ }
157
+ /** Interrupt the reactor. */
158
+ interrupt() {
159
+ this.abortController?.abort();
160
+ }
161
+ // =========================================================================
162
+ // Handler registration
163
+ // =========================================================================
164
+ _registerAllHandlers() {
165
+ this.handlers = {
166
+ USER_MESSAGE: this._onUserMessage.bind(this),
167
+ USER_INTERRUPT: this._onUserInterrupt.bind(this),
168
+ USER_FOLLOWUP_QUEUED: this._onUserFollowupQueued.bind(this),
169
+ AGENT_TURN_REQUESTED: this._onAgentTurnRequested.bind(this),
170
+ LLM_CALL_REQUESTED: this._onLlmCallRequested.bind(this),
171
+ AGENT_MESSAGE_END: this._onAgentMessageEnd.bind(this),
172
+ INTENT_TOOL_CALL: this._onIntentToolCall.bind(this),
173
+ TOOL_EXECUTION_END: this._onToolExecutionEnd.bind(this),
174
+ TOOL_RESULTS_AGGREGATED: this._onToolResultsAggregated.bind(this),
175
+ LLM_CALL_FAILED: this._onLlmCallFailed.bind(this),
176
+ AGENT_TURN_COMPLETED: this._onAgentTurnCompleted.bind(this),
177
+ USER_APPROVAL: this._onUserApproval.bind(this),
178
+ USER_REJECTION: this._onUserRejection.bind(this),
179
+ COMPACTION_REQUESTED: this._onCompactionRequested.bind(this),
180
+ SESSION_BOUNDARY_INFERRED: this._onSessionBoundaryInferred.bind(this),
181
+ };
182
+ }
183
+ // =========================================================================
184
+ // User-facing entry points (used by modes)
185
+ // =========================================================================
186
+ /**
187
+ * Send a user message to the agent (idle path).
188
+ * For interrupt-while-streaming use steer(). For after-turn-completion use followUp().
189
+ */
190
+ prompt(content, images) {
191
+ this._emit({
192
+ actor_id: "user",
193
+ type: "USER_MESSAGE",
194
+ payload: { content, images },
195
+ });
196
+ }
197
+ /**
198
+ * Steer: interrupt current turn with a new user message.
199
+ * The reactor aborts the current LLM call (if any) and treats the steer message
200
+ * as a fresh USER_MESSAGE once the turn settles.
201
+ */
202
+ steer(content, images) {
203
+ this._emit({
204
+ actor_id: "user",
205
+ type: "USER_INTERRUPT",
206
+ payload: { content, images, reason: "steer" },
207
+ });
208
+ }
209
+ /**
210
+ * Follow-up: queue a message to be delivered after the current turn naturally completes.
211
+ * Does not interrupt. If the reactor is idle, behaves like prompt().
212
+ */
213
+ followUp(content, images) {
214
+ this._emit({
215
+ actor_id: "user",
216
+ type: "USER_FOLLOWUP_QUEUED",
217
+ payload: { content, images },
218
+ });
219
+ }
220
+ /** Number of pending follow-up messages. */
221
+ get pendingFollowUpCount() {
222
+ return this.followUpQueue.length;
223
+ }
224
+ /** Number of scheduled retries waiting to re-enter the turn loop. */
225
+ get pendingRetryCount() {
226
+ return this.retryTimers.size;
227
+ }
228
+ /** Clear pending follow-ups (e.g. on user-explicit cancel). */
229
+ clearFollowUpQueue() {
230
+ this.followUpQueue = [];
231
+ }
232
+ // =========================================================================
233
+ // HANDLERS
234
+ // =========================================================================
235
+ // ─── USER_MESSAGE ───────────────────────────────────────────────────────
236
+ async _onUserMessage(event) {
237
+ const payload = event.payload;
238
+ if (this._shouldInterrupt())
239
+ return;
240
+ // Reset loop detection for a new prompt cycle
241
+ this._toolRoundSignatures = [];
242
+ // Emit thinking start
243
+ this._emit({
244
+ actor_id: "coder_agent",
245
+ type: "AGENT_THINKING_START",
246
+ payload: { model: this.config.model.model_id },
247
+ caused_by: event.event_id,
248
+ });
249
+ // Emit turn requested — this kicks off the reactor loop
250
+ this._emit({
251
+ actor_id: "coder_agent",
252
+ type: "AGENT_TURN_REQUESTED",
253
+ payload: { reason: "user_message" },
254
+ caused_by: event.event_id,
255
+ });
256
+ }
257
+ // ─── USER_INTERRUPT ─────────────────────────────────────────────────────
258
+ async _onUserInterrupt(event) {
259
+ const payload = event.payload;
260
+ this.interrupt();
261
+ // Track hard abort (no new content) so _onAgentTurnCompleted doesn't drain follow-ups.
262
+ if (payload.content === undefined) {
263
+ this._abortedByUser = true;
264
+ }
265
+ // If steer carried new content, queue it as a follow-up so the turn-completion
266
+ // handler picks it up after the current turn settles (aborted state).
267
+ if (payload.content !== undefined) {
268
+ this.followUpQueue.push({
269
+ content: payload.content,
270
+ images: payload.images,
271
+ });
272
+ }
273
+ // Cancel any pending compaction so we don't block turn completion
274
+ this.compactionAbort?.abort();
275
+ if (this.retryTimers.size > 0) {
276
+ for (const [scheduledEventId, retry] of this.retryTimers) {
277
+ clearTimeout(retry.timeout);
278
+ this._emit({
279
+ actor_id: "runtime",
280
+ type: "RETRY_ABORTED",
281
+ payload: {
282
+ attempt: retry.attempt,
283
+ reason: "user_interrupt",
284
+ error_message: retry.errorMessage,
285
+ scheduled_event_id: scheduledEventId,
286
+ },
287
+ caused_by: event.event_id,
288
+ });
289
+ }
290
+ this.retryTimers.clear();
291
+ this._emit({
292
+ actor_id: "coder_agent",
293
+ type: "AGENT_TURN_END",
294
+ payload: { tool_calls_count: 0 },
295
+ caused_by: event.event_id,
296
+ });
297
+ this._emit({
298
+ actor_id: "coder_agent",
299
+ type: "AGENT_TURN_COMPLETED",
300
+ payload: { reason: "aborted" },
301
+ caused_by: event.event_id,
302
+ });
303
+ }
304
+ }
305
+ // ─── USER_FOLLOWUP_QUEUED ───────────────────────────────────────────────
306
+ async _onUserFollowupQueued(event) {
307
+ const payload = event.payload;
308
+ this.followUpQueue.push({ content: payload.content, images: payload.images });
309
+ }
310
+ // ─── AGENT_TURN_REQUESTED ───────────────────────────────────────────────
311
+ async _onAgentTurnRequested(event) {
312
+ if (this._shouldInterrupt())
313
+ return;
314
+ const payload = event.payload;
315
+ // Build context from projection
316
+ const context = this.config.projection.buildContext({
317
+ max_tokens: this.config.contextBudget,
318
+ });
319
+ // Stash context for the LLM_CALL_REQUESTED handler. MUST be set before _emit
320
+ // because store subscribers fire synchronously and _onLlmCallRequested reads
321
+ // _pendingContext during its sync portion before awaiting the LLM call.
322
+ this._pendingContext = context;
323
+ // Emit turn start
324
+ this._emit({
325
+ actor_id: "coder_agent",
326
+ type: "AGENT_TURN_START",
327
+ payload: { message_count: context.messages.length },
328
+ caused_by: event.event_id,
329
+ });
330
+ // Request an LLM call
331
+ this._emit({
332
+ actor_id: "coder_agent",
333
+ type: "LLM_CALL_REQUESTED",
334
+ payload: { message_count: context.messages.length },
335
+ caused_by: event.event_id,
336
+ });
337
+ }
338
+ // ─── LLM_CALL_REQUESTED ─────────────────────────────────────────────────
339
+ async _onLlmCallRequested(event) {
340
+ if (this._shouldInterrupt())
341
+ return;
342
+ const context = this._pendingContext ?? this.config.projection.buildContext({ max_tokens: this.config.contextBudget });
343
+ this._pendingContext = undefined;
344
+ // Emit AGENT_MESSAGE_START up front so chunks have a parent in the causal chain.
345
+ const msgStart = this._emit({
346
+ actor_id: "coder_agent",
347
+ type: "AGENT_MESSAGE_START",
348
+ payload: { model: { provider: this.config.model.provider, model_id: this.config.model.model_id } },
349
+ caused_by: event.event_id,
350
+ });
351
+ try {
352
+ const response = await this.config.llmClient.complete({
353
+ messages: context.messages,
354
+ systemPrompt: this.config.systemPrompt,
355
+ model: this.config.model,
356
+ tools: this.config.tools,
357
+ signal: this.abortController?.signal,
358
+ onChunk: (chunk) => {
359
+ // Translate LLMChunk into an AGENT_MESSAGE_CHUNK event so projections / UI
360
+ // can render the streaming response in real time.
361
+ this._emit({
362
+ actor_id: "coder_agent",
363
+ type: "AGENT_MESSAGE_CHUNK",
364
+ payload: { chunk: chunk },
365
+ caused_by: msgStart.event_id,
366
+ });
367
+ },
368
+ });
369
+ this._handleLlmResponse(response, event.event_id, msgStart.event_id);
370
+ }
371
+ catch (err) {
372
+ const msg = err instanceof Error ? err.message : String(err);
373
+ const isRetryable = this.retryPolicy.isRetryable({ message: msg });
374
+ this._emit({
375
+ actor_id: "runtime",
376
+ type: "LLM_CALL_FAILED",
377
+ payload: { error: msg, retryable: isRetryable },
378
+ caused_by: event.event_id,
379
+ });
380
+ }
381
+ }
382
+ _handleLlmResponse(response, causedBy, msgStartEventId) {
383
+ // AGENT_MESSAGE_START is now emitted by _onLlmCallRequested before the call so chunks
384
+ // can reference it. For backwards compat with callers that don't stream, emit it here
385
+ // if not already emitted.
386
+ if (!msgStartEventId) {
387
+ const started = this._emit({
388
+ actor_id: "coder_agent",
389
+ type: "AGENT_MESSAGE_START",
390
+ payload: { model: { provider: response.provider, model_id: response.model } },
391
+ caused_by: causedBy,
392
+ });
393
+ msgStartEventId = started.event_id;
394
+ }
395
+ // Emit message end (the chunking is optional; for now we skip to end for simplicity)
396
+ const toolCalls = extractToolCalls(response.content);
397
+ // Append the AGENT_MESSAGE_END event.
398
+ const msgEnd = this._emit({
399
+ actor_id: "coder_agent",
400
+ type: "AGENT_MESSAGE_END",
401
+ payload: {
402
+ content: response.content,
403
+ model: { provider: response.provider, model_id: response.model },
404
+ usage: response.usage,
405
+ stop_reason: response.stopReason,
406
+ error_message: response.errorMessage,
407
+ },
408
+ caused_by: causedBy,
409
+ });
410
+ // Emit thinking end (turn_end is now emitted in _onAgentMessageEnd)
411
+ this._emit({
412
+ actor_id: "coder_agent",
413
+ type: "AGENT_THINKING_END",
414
+ payload: {},
415
+ caused_by: msgEnd.event_id,
416
+ });
417
+ // The ON_AGENT_MESSAGE_END handler will dispatch tool calls or complete the turn
418
+ }
419
+ // ─── AGENT_MESSAGE_END ──────────────────────────────────────────────────
420
+ async _onAgentMessageEnd(event) {
421
+ const payload = event.payload;
422
+ const toolCalls = extractToolCalls(payload.content);
423
+ if (this._shouldInterrupt()) {
424
+ // Emit turn end before completion when interrupted
425
+ this._emit({
426
+ actor_id: "coder_agent",
427
+ type: "AGENT_TURN_END",
428
+ payload: { tool_calls_count: toolCalls.length },
429
+ caused_by: event.event_id,
430
+ });
431
+ this._emit({
432
+ actor_id: "coder_agent",
433
+ type: "AGENT_TURN_COMPLETED",
434
+ payload: { reason: "aborted" },
435
+ caused_by: event.event_id,
436
+ });
437
+ return;
438
+ }
439
+ if (toolCalls.length === 0 || payload.stop_reason !== "tool_use") {
440
+ // No tool calls — emit turn end and complete the turn
441
+ this._emit({
442
+ actor_id: "coder_agent",
443
+ type: "AGENT_TURN_END",
444
+ payload: { tool_calls_count: 0 },
445
+ caused_by: event.event_id,
446
+ });
447
+ this._emit({
448
+ actor_id: "coder_agent",
449
+ type: "AGENT_TURN_COMPLETED",
450
+ payload: { reason: payload.stop_reason, error_message: payload.error_message },
451
+ caused_by: event.event_id,
452
+ });
453
+ return;
454
+ }
455
+ // Track this turn's tool executions
456
+ const tracker = {
457
+ assistantMessageEventId: event.event_id,
458
+ expectedCount: toolCalls.length,
459
+ expectedToolCallIds: new Set(toolCalls.map((toolCall) => toolCall.id)),
460
+ received: [],
461
+ abortSignal: this.abortController?.signal,
462
+ };
463
+ this.turnTrackers.set(event.event_id, tracker);
464
+ // Record the sorted tool-name signature for loop detection
465
+ const signature = toolCalls.map((tc) => tc.name).sort();
466
+ this._toolRoundSignatures.push(signature);
467
+ // Emit one INTENT_TOOL_CALL per tool call
468
+ for (const toolCall of toolCalls) {
469
+ const classification = this.config.classifier.classify(toolCall.name, toolCall.arguments);
470
+ this._emit({
471
+ actor_id: "coder_agent",
472
+ type: "INTENT_TOOL_CALL",
473
+ payload: {
474
+ tool_call_id: toolCall.id,
475
+ tool_name: toolCall.name,
476
+ arguments: toolCall.arguments,
477
+ requires_approval: classification.requires_approval,
478
+ classification,
479
+ },
480
+ caused_by: event.event_id,
481
+ });
482
+ }
483
+ }
484
+ async _onIntentToolCall(event) {
485
+ if (this._shouldInterrupt())
486
+ return;
487
+ const payload = event.payload;
488
+ if (payload.requires_approval) {
489
+ if (!this.config.approvalHandler) {
490
+ this._emitToolExecutionEnd(payload.tool_call_id, payload.tool_name, {
491
+ content: [{ type: "text", text: "Tool execution requires approval, but no approval handler is available." }],
492
+ is_error: true,
493
+ error_message: "Approval required but no approval handler is available",
494
+ }, event.event_id);
495
+ return;
496
+ }
497
+ // Wait for user approval/rejection
498
+ const approved = await new Promise((resolve) => {
499
+ this._pendingApprovals.set(event.event_id, {
500
+ resolve,
501
+ tool_call_id: payload.tool_call_id,
502
+ tool_name: payload.tool_name,
503
+ arguments: payload.arguments,
504
+ });
505
+ this.config.approvalHandler?.requestApproval(event.event_id, payload.classification);
506
+ });
507
+ if (!approved) {
508
+ this._emitToolExecutionEnd(payload.tool_call_id, payload.tool_name, {
509
+ content: [{ type: "text", text: "Tool execution rejected by user." }],
510
+ is_error: true,
511
+ error_message: "User rejected the tool call",
512
+ }, event.event_id);
513
+ return;
514
+ }
515
+ }
516
+ // Execute the tool
517
+ await this._executeTool(event.event_id, payload.tool_call_id, payload.tool_name, payload.arguments);
518
+ }
519
+ async _executeTool(causedBy, tool_call_id, tool_name, args) {
520
+ const startTime = Date.now();
521
+ this._emit({
522
+ actor_id: "runtime",
523
+ type: "TOOL_EXECUTION_START",
524
+ payload: { tool_call_id, tool_name, arguments: args },
525
+ caused_by: causedBy,
526
+ });
527
+ try {
528
+ const result = await this.config.runtimeAdapter.executeTool({
529
+ tool_call_id,
530
+ tool_name,
531
+ arguments: args,
532
+ caused_by: causedBy,
533
+ signal: this.abortController?.signal,
534
+ onUpdate: (partial) => {
535
+ this._emitToolExecutionUpdate(tool_call_id, partial, causedBy);
536
+ },
537
+ });
538
+ this._emitToolExecutionEnd(tool_call_id, tool_name, result, causedBy, startTime);
539
+ }
540
+ catch (err) {
541
+ const msg = err instanceof Error ? err.message : String(err);
542
+ this._emitToolExecutionEnd(tool_call_id, tool_name, {
543
+ content: [{ type: "text", text: msg }],
544
+ is_error: true,
545
+ error_message: msg,
546
+ }, causedBy, startTime);
547
+ }
548
+ }
549
+ _emitToolExecutionUpdate(tool_call_id, partial, causedBy) {
550
+ this._emit({
551
+ actor_id: "runtime",
552
+ type: "TOOL_EXECUTION_UPDATE",
553
+ payload: {
554
+ tool_call_id,
555
+ update: stringifyToolUpdate(partial),
556
+ progress: partial.progress,
557
+ },
558
+ caused_by: causedBy,
559
+ });
560
+ }
561
+ _emitToolExecutionEnd(tool_call_id, tool_name, result, causedBy, startTime) {
562
+ this._emit({
563
+ actor_id: "runtime",
564
+ type: "TOOL_EXECUTION_END",
565
+ payload: {
566
+ tool_call_id,
567
+ tool_name,
568
+ result: result.content,
569
+ details: result.details,
570
+ is_error: result.is_error,
571
+ duration_ms: startTime !== undefined ? Date.now() - startTime : 0,
572
+ file_mutations: result.file_mutations,
573
+ },
574
+ caused_by: causedBy,
575
+ });
576
+ }
577
+ // ─── TOOL_EXECUTION_END ─────────────────────────────────────────────────
578
+ async _onToolExecutionEnd(event) {
579
+ const payload = event.payload;
580
+ // Emit FILE_MUTATION_APPLIED for each mutation reported by the tool
581
+ if (payload.file_mutations?.length) {
582
+ for (const mutation of payload.file_mutations) {
583
+ this._emit({
584
+ actor_id: "runtime",
585
+ type: "FILE_MUTATION_APPLIED",
586
+ payload: {
587
+ path: mutation.path,
588
+ operation: mutation.operation,
589
+ diff: mutation.diff,
590
+ tool_name: payload.tool_name,
591
+ tool_call_id: payload.tool_call_id,
592
+ },
593
+ caused_by: event.event_id,
594
+ });
595
+ }
596
+ }
597
+ // Find the tracker for this tool call's assistant message
598
+ // Walk up the causal chain from the TOOL_EXECUTION_END event
599
+ const causedBy = event.caused_by;
600
+ // The caused_by points to the INTENT_TOOL_CALL event.
601
+ // We need to find the INTENT_TOOL_CALL → AGENT_MESSAGE_END chain.
602
+ // Actually, the causal chain in EventStore should give us this.
603
+ // For now, find the tracker by looking up the chain:
604
+ const chain = causedBy ? this.config.store.getCausalChain(event.event_id) : [];
605
+ // Find the closest AGENT_MESSAGE_END in the causal chain. Consecutive
606
+ // tool-use turns include older assistant messages earlier in the chain.
607
+ let assistantMessageEventId;
608
+ for (let i = chain.length - 1; i >= 0; i--) {
609
+ const e = chain[i];
610
+ if (e.type === "AGENT_MESSAGE_END") {
611
+ assistantMessageEventId = e.event_id;
612
+ break;
613
+ }
614
+ }
615
+ let tracker = assistantMessageEventId ? this.turnTrackers.get(assistantMessageEventId) : undefined;
616
+ if (!tracker || !tracker.expectedToolCallIds.has(payload.tool_call_id)) {
617
+ assistantMessageEventId = undefined;
618
+ for (const [messageEventId, tracker] of this.turnTrackers) {
619
+ if (tracker.expectedToolCallIds.has(payload.tool_call_id)) {
620
+ assistantMessageEventId = messageEventId;
621
+ break;
622
+ }
623
+ }
624
+ tracker = assistantMessageEventId ? this.turnTrackers.get(assistantMessageEventId) : undefined;
625
+ }
626
+ if (!tracker || !assistantMessageEventId)
627
+ return;
628
+ if (tracker.received.some((result) => result.tool_call_id === payload.tool_call_id))
629
+ return;
630
+ tracker.received.push({
631
+ tool_call_id: payload.tool_call_id,
632
+ tool_name: payload.tool_name,
633
+ result: { content: payload.result, is_error: payload.is_error },
634
+ is_error: payload.is_error,
635
+ });
636
+ // If all tools have completed, emit TOOL_RESULTS_AGGREGATED
637
+ if (tracker.received.length === tracker.expectedCount) {
638
+ this.turnTrackers.delete(assistantMessageEventId);
639
+ this._emit({
640
+ actor_id: "coder_agent",
641
+ type: "TOOL_RESULTS_AGGREGATED",
642
+ payload: {
643
+ assistant_message_event_id: assistantMessageEventId,
644
+ tool_call_count: tracker.expectedCount,
645
+ any_error: tracker.received.some((r) => r.is_error),
646
+ },
647
+ caused_by: event.event_id,
648
+ });
649
+ }
650
+ }
651
+ // ─── TOOL_RESULTS_AGGREGATED ────────────────────────────────────────────
652
+ async _onToolResultsAggregated(event) {
653
+ const payload = event.payload;
654
+ if (this._shouldInterrupt()) {
655
+ // Emit turn end before completion when interrupted
656
+ this._emit({
657
+ actor_id: "coder_agent",
658
+ type: "AGENT_TURN_END",
659
+ payload: { tool_calls_count: payload.tool_call_count },
660
+ caused_by: event.event_id,
661
+ });
662
+ this._emit({
663
+ actor_id: "coder_agent",
664
+ type: "AGENT_TURN_COMPLETED",
665
+ payload: { reason: "aborted" },
666
+ caused_by: event.event_id,
667
+ });
668
+ return;
669
+ }
670
+ // Emit turn end to signal completion of this tool-call turn
671
+ this._emit({
672
+ actor_id: "coder_agent",
673
+ type: "AGENT_TURN_END",
674
+ payload: { tool_calls_count: payload.tool_call_count },
675
+ caused_by: event.event_id,
676
+ });
677
+ // Loop detection: if the model has called the exact same set of tools
678
+ // (by name) in too many consecutive rounds, break the loop by completing
679
+ // the turn instead of requesting another LLM round.
680
+ if (this._isInToolLoop()) {
681
+ this._emit({
682
+ actor_id: "coder_agent",
683
+ type: "AGENT_TURN_COMPLETED",
684
+ payload: { reason: "loop_detected" },
685
+ caused_by: event.event_id,
686
+ });
687
+ return;
688
+ }
689
+ // Kick off the next turn with the tool results
690
+ this._emit({
691
+ actor_id: "coder_agent",
692
+ type: "AGENT_TURN_REQUESTED",
693
+ payload: { reason: "tool_results" },
694
+ caused_by: event.event_id,
695
+ });
696
+ }
697
+ // ─── SESSION_BOUNDARY_INFERRED ──────────────────────────────────────────
698
+ /**
699
+ * When a session split occurs, refresh the projection to point to the new
700
+ * session. The new session's start_event_id is set to the current USER_MESSAGE,
701
+ * so buildContext() will include the user's request + tool results but exclude
702
+ * old conversation history. This lets the model continue working in the same
703
+ * turn with a clean context.
704
+ */
705
+ _onSessionBoundaryInferred(_event) {
706
+ if (!this.config.sessionManager)
707
+ return;
708
+ const newProjection = this.config.sessionManager.getActiveSession();
709
+ if (newProjection) {
710
+ this.config.projection = newProjection;
711
+ }
712
+ }
713
+ /**
714
+ * Check whether the model is stuck in a loop — calling the exact same set
715
+ * of tool names in too many consecutive rounds.
716
+ */
717
+ _isInToolLoop() {
718
+ const sigs = this._toolRoundSignatures;
719
+ if (sigs.length < MAX_CONSECUTIVE_IDENTICAL_TOOL_ROUNDS)
720
+ return false;
721
+ const recent = sigs.slice(-MAX_CONSECUTIVE_IDENTICAL_TOOL_ROUNDS);
722
+ const first = recent[0].join(",");
723
+ return recent.every((sig) => sig.join(",") === first);
724
+ }
725
+ // ─── AGENT_TURN_COMPLETED ───────────────────────────────────────────────
726
+ //
727
+ // Drain the followUp queue (if any) or let the reactor go idle.
728
+ // Also triggers compaction check after a non-error turn.
729
+ _onAgentTurnCompleted(event) {
730
+ const payload = event.payload;
731
+ // If abort() was called, discard any pending follow-ups so the reactor stops.
732
+ // _abortedByUser is set by _onUserInterrupt when the interrupt has no new content.
733
+ if (this._abortedByUser) {
734
+ this.followUpQueue = [];
735
+ this._abortedByUser = false;
736
+ return;
737
+ }
738
+ if (this._scheduleRetryForCompletedError(event)) {
739
+ return;
740
+ }
741
+ // If a steer or follow-up message is queued, deliver it as a new USER_MESSAGE
742
+ // so the turn cycle restarts.
743
+ if (this.followUpQueue.length > 0) {
744
+ // Reset abort controller for the new turn — the previous turn's interrupt
745
+ // has already been handled.
746
+ this.abortController = new AbortController();
747
+ const next = this.followUpQueue.shift();
748
+ this._emit({
749
+ actor_id: "user",
750
+ type: "USER_MESSAGE",
751
+ payload: { content: next.content, images: next.images },
752
+ caused_by: event.event_id,
753
+ });
754
+ return;
755
+ }
756
+ // Check compaction policy after a non-aborted turn
757
+ if (payload.reason !== "aborted") {
758
+ this._checkCompaction(event);
759
+ }
760
+ }
761
+ // ─── LLM_CALL_FAILED ────────────────────────────────────────────────────
762
+ async _onLlmCallFailed(event) {
763
+ const payload = event.payload;
764
+ const retryResult = this._scheduleRetry(event, payload.error, payload.retryable);
765
+ if (retryResult === "scheduled")
766
+ return;
767
+ const attempt = this._attemptCount(event.event_id);
768
+ // Emit turn end before completion
769
+ this._emit({
770
+ actor_id: "coder_agent",
771
+ type: "AGENT_TURN_END",
772
+ payload: { tool_calls_count: 0 },
773
+ caused_by: event.event_id,
774
+ });
775
+ this._emit({
776
+ actor_id: "coder_agent",
777
+ type: "AGENT_TURN_COMPLETED",
778
+ payload: {
779
+ reason: "error",
780
+ error_message: retryResult === "max_attempts"
781
+ ? `Max retries (${this.retryPolicy.maxAttempts}) exceeded: ${payload.error}`
782
+ : retryResult === "backoff_exhausted"
783
+ ? `Retry backoff exhausted: ${payload.error}`
784
+ : payload.error,
785
+ },
786
+ caused_by: event.event_id,
787
+ });
788
+ }
789
+ _scheduleRetryForCompletedError(event) {
790
+ if (this.config.retryAssistantErrorCompletions === false)
791
+ return false;
792
+ const payload = event.payload;
793
+ if (payload.reason !== "error")
794
+ return false;
795
+ if (this._errorAlreadyHandledByLlmFailure(event))
796
+ return false;
797
+ const errorMessage = payload.error_message ?? "Agent turn completed with error";
798
+ return this._scheduleRetry(event, errorMessage, this.retryPolicy.isRetryable({ message: errorMessage })) === "scheduled";
799
+ }
800
+ _errorAlreadyHandledByLlmFailure(event) {
801
+ const causedBy = event.caused_by ? this.config.store.get(event.caused_by) : undefined;
802
+ return causedBy?.type === "LLM_CALL_FAILED";
803
+ }
804
+ _scheduleRetry(event, error, retryable) {
805
+ const attempt = this._attemptCount(event.event_id);
806
+ if (!retryable || attempt >= this.retryPolicy.maxAttempts) {
807
+ return retryable ? "max_attempts" : "not_retryable";
808
+ }
809
+ const nextAttempt = attempt + 1;
810
+ const delayMs = this.retryPolicy.nextDelayMs(nextAttempt);
811
+ if (delayMs === null) {
812
+ return "backoff_exhausted";
813
+ }
814
+ const retryScheduled = this._emit({
815
+ actor_id: "runtime",
816
+ type: "RETRY_SCHEDULED",
817
+ payload: {
818
+ attempt: nextAttempt,
819
+ max_attempts: this.retryPolicy.maxAttempts,
820
+ delay_ms: delayMs,
821
+ error_message: error,
822
+ },
823
+ caused_by: event.event_id,
824
+ });
825
+ const timeout = setTimeout(() => {
826
+ this.retryTimers.delete(retryScheduled.event_id);
827
+ if (!this._shouldInterrupt()) {
828
+ this._emit({
829
+ actor_id: "coder_agent",
830
+ type: "AGENT_TURN_REQUESTED",
831
+ payload: { reason: "retry", retry_attempt: nextAttempt },
832
+ caused_by: retryScheduled.event_id,
833
+ });
834
+ }
835
+ }, delayMs);
836
+ this.retryTimers.set(retryScheduled.event_id, {
837
+ timeout,
838
+ attempt: nextAttempt,
839
+ errorMessage: error,
840
+ });
841
+ return "scheduled";
842
+ }
843
+ /** Walk causal chain back to find the highest retry_attempt seen. */
844
+ _attemptCount(eventId) {
845
+ const chain = this.config.store.getCausalChain(eventId);
846
+ let attempt = 0;
847
+ for (const e of chain) {
848
+ if (e.type === "AGENT_TURN_REQUESTED") {
849
+ const p = e.payload;
850
+ if (p.retry_attempt !== undefined && p.retry_attempt > attempt) {
851
+ attempt = p.retry_attempt;
852
+ }
853
+ }
854
+ }
855
+ return attempt;
856
+ }
857
+ // ─── USER_APPROVAL ──────────────────────────────────────────────────────
858
+ async _onUserApproval(event) {
859
+ const payload = event.payload;
860
+ const pending = this._pendingApprovals.get(payload.intent_event_id);
861
+ if (pending) {
862
+ this._pendingApprovals.delete(payload.intent_event_id);
863
+ pending.resolve(true);
864
+ }
865
+ }
866
+ // ─── USER_REJECTION ─────────────────────────────────────────────────────
867
+ async _onUserRejection(event) {
868
+ const payload = event.payload;
869
+ const pending = this._pendingApprovals.get(payload.intent_event_id);
870
+ if (pending) {
871
+ this._pendingApprovals.delete(payload.intent_event_id);
872
+ pending.resolve(false);
873
+ }
874
+ }
875
+ // ─── COMPACTION ─────────────────────────────────────────────────────────
876
+ async _onCompactionRequested(event) {
877
+ if (this.compactionAbort)
878
+ return; // Already running
879
+ const payload = event.payload;
880
+ const abortController = new AbortController();
881
+ this.compactionAbort = abortController;
882
+ this._emit({
883
+ actor_id: "compactor",
884
+ type: "COMPACTION_START",
885
+ payload: { token_count: payload.token_count },
886
+ caused_by: event.event_id,
887
+ });
888
+ try {
889
+ const outcome = await this.compactionPolicy.compact(payload.reason, abortController.signal);
890
+ this._emit({
891
+ actor_id: "compactor",
892
+ type: "COMPACTION_END",
893
+ payload: {
894
+ summary: outcome.summary,
895
+ first_kept_event_id: outcome.first_kept_event_id,
896
+ tokens_before: outcome.tokens_before,
897
+ tokens_after: outcome.tokens_after,
898
+ },
899
+ caused_by: event.event_id,
900
+ });
901
+ }
902
+ catch (err) {
903
+ const msg = err instanceof Error ? err.message : String(err);
904
+ if (abortController.signal.aborted) {
905
+ this._emit({
906
+ actor_id: "compactor",
907
+ type: "COMPACTION_ABORTED",
908
+ payload: {
909
+ reason: "user_cancelled",
910
+ message: msg,
911
+ token_count: payload.token_count,
912
+ },
913
+ caused_by: event.event_id,
914
+ });
915
+ return;
916
+ }
917
+ this._emit({
918
+ actor_id: "compactor",
919
+ type: "COMPACTION_END",
920
+ payload: {
921
+ summary: `Compaction failed: ${msg}`,
922
+ first_kept_event_id: "",
923
+ tokens_before: 0,
924
+ },
925
+ caused_by: event.event_id,
926
+ });
927
+ }
928
+ finally {
929
+ this.compactionAbort = undefined;
930
+ }
931
+ }
932
+ /**
933
+ * Check compaction policy after a non-error turn. Emits COMPACTION_REQUESTED if
934
+ * threshold or overflow conditions are met.
935
+ */
936
+ _checkCompaction(turnCompletedEvent) {
937
+ const policy = this.compactionPolicy;
938
+ // Walk the causal chain to find the AGENT_MESSAGE_END that produced this turn completion.
939
+ // This avoids mutable state (_lastAssistantMessageEvent) that races with synchronous event dispatch.
940
+ const lastAssistant = this._findLastAssistantMessage(turnCompletedEvent.event_id);
941
+ // Overflow takes priority — when it fires we skip the threshold check entirely.
942
+ // This prevents both "overflow" and "threshold" COMPACTION_REQUESTED events from
943
+ // being emitted in the same turn (overflow implies context is over threshold too).
944
+ if (policy.isOverflow(lastAssistant)) {
945
+ const tokens = policy.estimateContextTokens();
946
+ this._emit({
947
+ actor_id: "runtime",
948
+ type: "COMPACTION_REQUESTED",
949
+ payload: { reason: "overflow", token_count: tokens },
950
+ caused_by: turnCompletedEvent.event_id,
951
+ });
952
+ return;
953
+ }
954
+ const tokens = policy.estimateContextTokens();
955
+ const window = policy.contextWindow();
956
+ if (window > 0 && window !== Number.MAX_SAFE_INTEGER && tokens > window * policy.threshold()) {
957
+ this._emit({
958
+ actor_id: "runtime",
959
+ type: "COMPACTION_REQUESTED",
960
+ payload: { reason: "threshold", token_count: tokens },
961
+ caused_by: turnCompletedEvent.event_id,
962
+ });
963
+ }
964
+ }
965
+ /**
966
+ * Walk causal chain back from the given event to find the most recent AGENT_MESSAGE_END.
967
+ * Replaces the mutable _lastAssistantMessageEvent field which races with synchronous dispatch.
968
+ */
969
+ _findLastAssistantMessage(eventId) {
970
+ const chain = this.config.store.getCausalChain(eventId);
971
+ for (let i = chain.length - 1; i >= 0; i--) {
972
+ if (chain[i].type === "AGENT_MESSAGE_END")
973
+ return chain[i];
974
+ }
975
+ return undefined;
976
+ }
977
+ }
978
+ function stringifyToolUpdate(partial) {
979
+ const text = partial.content
980
+ .filter((block) => {
981
+ return block.type === "text" && "text" in block && typeof block.text === "string";
982
+ })
983
+ .map((block) => block.text)
984
+ .join("\n");
985
+ if (text)
986
+ return text;
987
+ if (partial.content.length > 0)
988
+ return JSON.stringify(partial.content);
989
+ if (partial.details !== undefined)
990
+ return JSON.stringify(partial.details);
991
+ return "";
992
+ }