@calliopelabs/cli 0.8.20 → 2.0.2

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 (402) hide show
  1. package/dist/agents/agent-config-loader.d.ts +60 -0
  2. package/dist/agents/agent-config-loader.d.ts.map +1 -0
  3. package/dist/agents/agent-config-loader.js +402 -0
  4. package/dist/agents/agent-config-loader.js.map +1 -0
  5. package/dist/agents/agent-config-presets.d.ts +10 -0
  6. package/dist/agents/agent-config-presets.d.ts.map +1 -0
  7. package/dist/agents/agent-config-presets.js +940 -0
  8. package/dist/agents/agent-config-presets.js.map +1 -0
  9. package/dist/agents/agent-config-types.d.ts +145 -0
  10. package/dist/agents/agent-config-types.d.ts.map +1 -0
  11. package/dist/agents/agent-config-types.js +12 -0
  12. package/dist/agents/agent-config-types.js.map +1 -0
  13. package/dist/{agterm → agents}/agent-detection.d.ts +1 -1
  14. package/dist/{agterm → agents}/agent-detection.d.ts.map +1 -1
  15. package/dist/{agterm → agents}/agent-detection.js +21 -5
  16. package/dist/agents/agent-detection.js.map +1 -0
  17. package/dist/agents/aggregator.d.ts +19 -0
  18. package/dist/agents/aggregator.d.ts.map +1 -0
  19. package/dist/agents/aggregator.js +141 -0
  20. package/dist/agents/aggregator.js.map +1 -0
  21. package/dist/{agterm → agents}/cli-backend.d.ts +1 -1
  22. package/dist/{agterm → agents}/cli-backend.d.ts.map +1 -1
  23. package/dist/{agterm → agents}/cli-backend.js +90 -12
  24. package/dist/agents/cli-backend.js.map +1 -0
  25. package/dist/agents/council-types.d.ts +113 -0
  26. package/dist/agents/council-types.d.ts.map +1 -0
  27. package/dist/agents/council-types.js +81 -0
  28. package/dist/agents/council-types.js.map +1 -0
  29. package/dist/agents/council.d.ts +107 -0
  30. package/dist/agents/council.d.ts.map +1 -0
  31. package/dist/agents/council.js +586 -0
  32. package/dist/agents/council.js.map +1 -0
  33. package/dist/agents/decomposer.d.ts +33 -0
  34. package/dist/agents/decomposer.d.ts.map +1 -0
  35. package/dist/agents/decomposer.js +138 -0
  36. package/dist/agents/decomposer.js.map +1 -0
  37. package/dist/agents/dynamic-tools.d.ts +52 -0
  38. package/dist/agents/dynamic-tools.d.ts.map +1 -0
  39. package/dist/agents/dynamic-tools.js +395 -0
  40. package/dist/agents/dynamic-tools.js.map +1 -0
  41. package/dist/agents/index.d.ts +29 -0
  42. package/dist/agents/index.d.ts.map +1 -0
  43. package/dist/agents/index.js +29 -0
  44. package/dist/agents/index.js.map +1 -0
  45. package/dist/agents/installer.d.ts +39 -0
  46. package/dist/agents/installer.d.ts.map +1 -0
  47. package/dist/agents/installer.js +205 -0
  48. package/dist/agents/installer.js.map +1 -0
  49. package/dist/{agterm → agents}/orchestrator.d.ts +7 -2
  50. package/dist/agents/orchestrator.d.ts.map +1 -0
  51. package/dist/{agterm → agents}/orchestrator.js +22 -2
  52. package/dist/agents/orchestrator.js.map +1 -0
  53. package/dist/agents/sdk-backend.d.ts +63 -0
  54. package/dist/agents/sdk-backend.d.ts.map +1 -0
  55. package/dist/agents/sdk-backend.js +489 -0
  56. package/dist/agents/sdk-backend.js.map +1 -0
  57. package/dist/agents/swarm-types.d.ts +83 -0
  58. package/dist/agents/swarm-types.d.ts.map +1 -0
  59. package/dist/agents/swarm-types.js +20 -0
  60. package/dist/agents/swarm-types.js.map +1 -0
  61. package/dist/agents/swarm.d.ts +74 -0
  62. package/dist/agents/swarm.d.ts.map +1 -0
  63. package/dist/agents/swarm.js +307 -0
  64. package/dist/agents/swarm.js.map +1 -0
  65. package/dist/{agterm → agents}/tools.d.ts +7 -5
  66. package/dist/agents/tools.d.ts.map +1 -0
  67. package/dist/agents/tools.js +776 -0
  68. package/dist/agents/tools.js.map +1 -0
  69. package/dist/{agterm → agents}/types.d.ts +14 -2
  70. package/dist/agents/types.d.ts.map +1 -0
  71. package/dist/{agterm → agents}/types.js +2 -2
  72. package/dist/agents/types.js.map +1 -0
  73. package/dist/api-server.d.ts +26 -0
  74. package/dist/api-server.d.ts.map +1 -0
  75. package/dist/api-server.js +230 -0
  76. package/dist/api-server.js.map +1 -0
  77. package/dist/auto-checkpoint.d.ts +35 -0
  78. package/dist/auto-checkpoint.d.ts.map +1 -0
  79. package/dist/auto-checkpoint.js +143 -0
  80. package/dist/auto-checkpoint.js.map +1 -0
  81. package/dist/auto-compressor.d.ts +44 -0
  82. package/dist/auto-compressor.d.ts.map +1 -0
  83. package/dist/auto-compressor.js +145 -0
  84. package/dist/auto-compressor.js.map +1 -0
  85. package/dist/background-jobs.d.ts +45 -0
  86. package/dist/background-jobs.d.ts.map +1 -0
  87. package/dist/background-jobs.js +122 -0
  88. package/dist/background-jobs.js.map +1 -0
  89. package/dist/bin.d.ts +6 -2
  90. package/dist/bin.d.ts.map +1 -1
  91. package/dist/bin.js +127 -24
  92. package/dist/bin.js.map +1 -1
  93. package/dist/checkpoint.d.ts +49 -0
  94. package/dist/checkpoint.d.ts.map +1 -0
  95. package/dist/checkpoint.js +219 -0
  96. package/dist/checkpoint.js.map +1 -0
  97. package/dist/circuit-breaker/breaker.d.ts +80 -0
  98. package/dist/circuit-breaker/breaker.d.ts.map +1 -0
  99. package/dist/circuit-breaker/breaker.js +408 -0
  100. package/dist/circuit-breaker/breaker.js.map +1 -0
  101. package/dist/circuit-breaker/defaults.d.ts +8 -0
  102. package/dist/circuit-breaker/defaults.d.ts.map +1 -0
  103. package/dist/circuit-breaker/defaults.js +35 -0
  104. package/dist/circuit-breaker/defaults.js.map +1 -0
  105. package/dist/circuit-breaker/index.d.ts +9 -0
  106. package/dist/circuit-breaker/index.d.ts.map +1 -0
  107. package/dist/circuit-breaker/index.js +8 -0
  108. package/dist/circuit-breaker/index.js.map +1 -0
  109. package/dist/circuit-breaker/types.d.ts +77 -0
  110. package/dist/circuit-breaker/types.d.ts.map +1 -0
  111. package/dist/circuit-breaker/types.js +8 -0
  112. package/dist/circuit-breaker/types.js.map +1 -0
  113. package/dist/circuit-breaker.d.ts +8 -0
  114. package/dist/circuit-breaker.d.ts.map +1 -0
  115. package/dist/circuit-breaker.js +7 -0
  116. package/dist/circuit-breaker.js.map +1 -0
  117. package/dist/cli/agent.d.ts +9 -0
  118. package/dist/cli/agent.d.ts.map +1 -0
  119. package/dist/cli/agent.js +262 -0
  120. package/dist/cli/agent.js.map +1 -0
  121. package/dist/cli/commands.d.ts +12 -0
  122. package/dist/cli/commands.d.ts.map +1 -0
  123. package/dist/{cli.js → cli/commands.js} +285 -422
  124. package/dist/cli/commands.js.map +1 -0
  125. package/dist/cli/index.d.ts +8 -0
  126. package/dist/cli/index.d.ts.map +1 -0
  127. package/dist/cli/index.js +222 -0
  128. package/dist/cli/index.js.map +1 -0
  129. package/dist/cli/types.d.ts +30 -0
  130. package/dist/cli/types.d.ts.map +1 -0
  131. package/dist/cli/types.js +20 -0
  132. package/dist/cli/types.js.map +1 -0
  133. package/dist/companions.d.ts +54 -0
  134. package/dist/companions.d.ts.map +1 -0
  135. package/dist/companions.js +440 -0
  136. package/dist/companions.js.map +1 -0
  137. package/dist/config.d.ts +23 -1
  138. package/dist/config.d.ts.map +1 -1
  139. package/dist/config.js +95 -22
  140. package/dist/config.js.map +1 -1
  141. package/dist/diff.d.ts +27 -0
  142. package/dist/diff.d.ts.map +1 -1
  143. package/dist/diff.js +415 -10
  144. package/dist/diff.js.map +1 -1
  145. package/dist/errors.d.ts.map +1 -1
  146. package/dist/errors.js +20 -11
  147. package/dist/errors.js.map +1 -1
  148. package/dist/git-status.d.ts +23 -0
  149. package/dist/git-status.d.ts.map +1 -0
  150. package/dist/git-status.js +92 -0
  151. package/dist/git-status.js.map +1 -0
  152. package/dist/headless.d.ts +25 -0
  153. package/dist/headless.d.ts.map +1 -0
  154. package/dist/headless.js +182 -0
  155. package/dist/headless.js.map +1 -0
  156. package/dist/hud/api.d.ts +35 -0
  157. package/dist/hud/api.d.ts.map +1 -0
  158. package/dist/hud/api.js +448 -0
  159. package/dist/hud/api.js.map +1 -0
  160. package/dist/hud/palettes.d.ts +9 -0
  161. package/dist/hud/palettes.d.ts.map +1 -0
  162. package/dist/hud/palettes.js +280 -0
  163. package/dist/hud/palettes.js.map +1 -0
  164. package/dist/hud/skins.d.ts +12 -0
  165. package/dist/hud/skins.d.ts.map +1 -0
  166. package/dist/hud/skins.js +365 -0
  167. package/dist/hud/skins.js.map +1 -0
  168. package/dist/hud/theme-packs/api.d.ts +51 -0
  169. package/dist/hud/theme-packs/api.d.ts.map +1 -0
  170. package/dist/hud/theme-packs/api.js +145 -0
  171. package/dist/hud/theme-packs/api.js.map +1 -0
  172. package/dist/hud/theme-packs/index.d.ts +18 -0
  173. package/dist/hud/theme-packs/index.d.ts.map +1 -0
  174. package/dist/hud/theme-packs/index.js +38 -0
  175. package/dist/hud/theme-packs/index.js.map +1 -0
  176. package/dist/hud/theme-packs/types.d.ts +29 -0
  177. package/dist/hud/theme-packs/types.d.ts.map +1 -0
  178. package/dist/hud/theme-packs/types.js +9 -0
  179. package/dist/hud/theme-packs/types.js.map +1 -0
  180. package/dist/hud/types.d.ts +182 -0
  181. package/dist/hud/types.d.ts.map +1 -0
  182. package/dist/hud/types.js +7 -0
  183. package/dist/hud/types.js.map +1 -0
  184. package/dist/idle-eviction.d.ts +34 -0
  185. package/dist/idle-eviction.d.ts.map +1 -0
  186. package/dist/idle-eviction.js +78 -0
  187. package/dist/idle-eviction.js.map +1 -0
  188. package/dist/index.d.ts +9 -3
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +9 -3
  191. package/dist/index.js.map +1 -1
  192. package/dist/iteration-ledger.d.ts +105 -0
  193. package/dist/iteration-ledger.d.ts.map +1 -0
  194. package/dist/iteration-ledger.js +237 -0
  195. package/dist/iteration-ledger.js.map +1 -0
  196. package/dist/markdown.d.ts.map +1 -1
  197. package/dist/markdown.js +1 -27
  198. package/dist/markdown.js.map +1 -1
  199. package/dist/mcp.d.ts +35 -0
  200. package/dist/mcp.d.ts.map +1 -1
  201. package/dist/mcp.js +291 -7
  202. package/dist/mcp.js.map +1 -1
  203. package/dist/memory.d.ts.map +1 -1
  204. package/dist/memory.js +12 -2
  205. package/dist/memory.js.map +1 -1
  206. package/dist/model-detection.d.ts +5 -0
  207. package/dist/model-detection.d.ts.map +1 -1
  208. package/dist/model-detection.js +278 -10
  209. package/dist/model-detection.js.map +1 -1
  210. package/dist/model-router.d.ts.map +1 -1
  211. package/dist/model-router.js +33 -11
  212. package/dist/model-router.js.map +1 -1
  213. package/dist/plugins.d.ts +8 -0
  214. package/dist/plugins.d.ts.map +1 -1
  215. package/dist/plugins.js +97 -6
  216. package/dist/plugins.js.map +1 -1
  217. package/dist/providers/anthropic.d.ts +10 -0
  218. package/dist/providers/anthropic.d.ts.map +1 -0
  219. package/dist/providers/anthropic.js +221 -0
  220. package/dist/providers/anthropic.js.map +1 -0
  221. package/dist/providers/bedrock.d.ts +17 -0
  222. package/dist/providers/bedrock.d.ts.map +1 -0
  223. package/dist/providers/bedrock.js +574 -0
  224. package/dist/providers/bedrock.js.map +1 -0
  225. package/dist/providers/compat.d.ts +13 -0
  226. package/dist/providers/compat.d.ts.map +1 -0
  227. package/dist/providers/compat.js +202 -0
  228. package/dist/providers/compat.js.map +1 -0
  229. package/dist/providers/google.d.ts +10 -0
  230. package/dist/providers/google.d.ts.map +1 -0
  231. package/dist/providers/google.js +203 -0
  232. package/dist/providers/google.js.map +1 -0
  233. package/dist/providers/index.d.ts +23 -0
  234. package/dist/providers/index.d.ts.map +1 -0
  235. package/dist/providers/index.js +145 -0
  236. package/dist/providers/index.js.map +1 -0
  237. package/dist/providers/ollama.d.ts +17 -0
  238. package/dist/providers/ollama.d.ts.map +1 -0
  239. package/dist/providers/ollama.js +289 -0
  240. package/dist/providers/ollama.js.map +1 -0
  241. package/dist/providers/openai.d.ts +121 -0
  242. package/dist/providers/openai.d.ts.map +1 -0
  243. package/dist/providers/openai.js +485 -0
  244. package/dist/providers/openai.js.map +1 -0
  245. package/dist/providers/types.d.ts +63 -0
  246. package/dist/providers/types.d.ts.map +1 -0
  247. package/dist/providers/types.js +164 -0
  248. package/dist/providers/types.js.map +1 -0
  249. package/dist/sandbox-native.d.ts +59 -0
  250. package/dist/sandbox-native.d.ts.map +1 -0
  251. package/dist/sandbox-native.js +292 -0
  252. package/dist/sandbox-native.js.map +1 -0
  253. package/dist/sandbox.d.ts +2 -2
  254. package/dist/sandbox.d.ts.map +1 -1
  255. package/dist/sandbox.js +59 -13
  256. package/dist/sandbox.js.map +1 -1
  257. package/dist/scope.d.ts +3 -1
  258. package/dist/scope.d.ts.map +1 -1
  259. package/dist/scope.js +13 -1
  260. package/dist/scope.js.map +1 -1
  261. package/dist/session-timeout.d.ts +31 -0
  262. package/dist/session-timeout.d.ts.map +1 -0
  263. package/dist/session-timeout.js +100 -0
  264. package/dist/session-timeout.js.map +1 -0
  265. package/dist/setup.d.ts.map +1 -1
  266. package/dist/setup.js +29 -17
  267. package/dist/setup.js.map +1 -1
  268. package/dist/smart-router.d.ts +73 -0
  269. package/dist/smart-router.d.ts.map +1 -0
  270. package/dist/smart-router.js +332 -0
  271. package/dist/smart-router.js.map +1 -0
  272. package/dist/storage.d.ts +19 -0
  273. package/dist/storage.d.ts.map +1 -1
  274. package/dist/storage.js +164 -1
  275. package/dist/storage.js.map +1 -1
  276. package/dist/streaming.d.ts +4 -0
  277. package/dist/streaming.d.ts.map +1 -1
  278. package/dist/streaming.js +12 -0
  279. package/dist/streaming.js.map +1 -1
  280. package/dist/styles.d.ts +32 -0
  281. package/dist/styles.d.ts.map +1 -1
  282. package/dist/styles.js +91 -0
  283. package/dist/styles.js.map +1 -1
  284. package/dist/summarization.d.ts +1 -1
  285. package/dist/summarization.js +4 -4
  286. package/dist/summarization.js.map +1 -1
  287. package/dist/terminal-image.d.ts +115 -0
  288. package/dist/terminal-image.d.ts.map +1 -0
  289. package/dist/terminal-image.js +766 -0
  290. package/dist/terminal-image.js.map +1 -0
  291. package/dist/terminal-recording.d.ts +55 -0
  292. package/dist/terminal-recording.d.ts.map +1 -0
  293. package/dist/terminal-recording.js +182 -0
  294. package/dist/terminal-recording.js.map +1 -0
  295. package/dist/themes.d.ts +19 -35
  296. package/dist/themes.d.ts.map +1 -1
  297. package/dist/themes.js +101 -210
  298. package/dist/themes.js.map +1 -1
  299. package/dist/tmux.d.ts +35 -0
  300. package/dist/tmux.d.ts.map +1 -0
  301. package/dist/tmux.js +106 -0
  302. package/dist/tmux.js.map +1 -0
  303. package/dist/tools.d.ts +3 -3
  304. package/dist/tools.d.ts.map +1 -1
  305. package/dist/tools.js +587 -45
  306. package/dist/tools.js.map +1 -1
  307. package/dist/trust.d.ts +53 -0
  308. package/dist/trust.d.ts.map +1 -0
  309. package/dist/trust.js +154 -0
  310. package/dist/trust.js.map +1 -0
  311. package/dist/types.d.ts +7 -3
  312. package/dist/types.d.ts.map +1 -1
  313. package/dist/types.js +70 -32
  314. package/dist/types.js.map +1 -1
  315. package/dist/ui/agent.d.ts +61 -0
  316. package/dist/ui/agent.d.ts.map +1 -0
  317. package/dist/ui/agent.js +768 -0
  318. package/dist/ui/agent.js.map +1 -0
  319. package/dist/ui/chat-input.d.ts +32 -0
  320. package/dist/ui/chat-input.d.ts.map +1 -0
  321. package/dist/ui/chat-input.js +355 -0
  322. package/dist/ui/chat-input.js.map +1 -0
  323. package/dist/ui/commands.d.ts +92 -0
  324. package/dist/ui/commands.d.ts.map +1 -0
  325. package/dist/ui/commands.js +3006 -0
  326. package/dist/ui/commands.js.map +1 -0
  327. package/dist/ui/completions.d.ts +22 -0
  328. package/dist/ui/completions.d.ts.map +1 -0
  329. package/dist/ui/completions.js +215 -0
  330. package/dist/ui/completions.js.map +1 -0
  331. package/dist/ui/components.d.ts +38 -0
  332. package/dist/ui/components.d.ts.map +1 -0
  333. package/dist/ui/components.js +422 -0
  334. package/dist/ui/components.js.map +1 -0
  335. package/dist/ui/context.d.ts +12 -0
  336. package/dist/ui/context.d.ts.map +1 -0
  337. package/dist/ui/context.js +102 -0
  338. package/dist/ui/context.js.map +1 -0
  339. package/dist/ui/error-boundary.d.ts +33 -0
  340. package/dist/ui/error-boundary.d.ts.map +1 -0
  341. package/dist/ui/error-boundary.js +94 -0
  342. package/dist/ui/error-boundary.js.map +1 -0
  343. package/dist/ui/frame.d.ts +13 -0
  344. package/dist/ui/frame.d.ts.map +1 -0
  345. package/dist/ui/frame.js +89 -0
  346. package/dist/ui/frame.js.map +1 -0
  347. package/dist/ui/index.d.ts +12 -0
  348. package/dist/ui/index.d.ts.map +1 -0
  349. package/dist/ui/index.js +928 -0
  350. package/dist/ui/index.js.map +1 -0
  351. package/dist/ui/messages.d.ts +19 -0
  352. package/dist/ui/messages.d.ts.map +1 -0
  353. package/dist/ui/messages.js +181 -0
  354. package/dist/ui/messages.js.map +1 -0
  355. package/dist/ui/modals.d.ts +52 -0
  356. package/dist/ui/modals.d.ts.map +1 -0
  357. package/dist/ui/modals.js +204 -0
  358. package/dist/ui/modals.js.map +1 -0
  359. package/dist/ui/pack-picker.d.ts +12 -0
  360. package/dist/ui/pack-picker.d.ts.map +1 -0
  361. package/dist/ui/pack-picker.js +101 -0
  362. package/dist/ui/pack-picker.js.map +1 -0
  363. package/dist/ui/status-bar.d.ts +20 -0
  364. package/dist/ui/status-bar.d.ts.map +1 -0
  365. package/dist/ui/status-bar.js +41 -0
  366. package/dist/ui/status-bar.js.map +1 -0
  367. package/dist/ui/theme-picker.d.ts +24 -0
  368. package/dist/ui/theme-picker.d.ts.map +1 -0
  369. package/dist/ui/theme-picker.js +190 -0
  370. package/dist/ui/theme-picker.js.map +1 -0
  371. package/dist/ui/types.d.ts +62 -0
  372. package/dist/ui/types.d.ts.map +1 -0
  373. package/dist/ui/types.js +7 -0
  374. package/dist/ui/types.js.map +1 -0
  375. package/dist/version-check.d.ts.map +1 -1
  376. package/dist/version-check.js +1 -9
  377. package/dist/version-check.js.map +1 -1
  378. package/package.json +8 -3
  379. package/dist/agterm/agent-detection.js.map +0 -1
  380. package/dist/agterm/cli-backend.js.map +0 -1
  381. package/dist/agterm/index.d.ts +0 -12
  382. package/dist/agterm/index.d.ts.map +0 -1
  383. package/dist/agterm/index.js +0 -15
  384. package/dist/agterm/index.js.map +0 -1
  385. package/dist/agterm/orchestrator.d.ts.map +0 -1
  386. package/dist/agterm/orchestrator.js.map +0 -1
  387. package/dist/agterm/tools.d.ts.map +0 -1
  388. package/dist/agterm/tools.js +0 -278
  389. package/dist/agterm/tools.js.map +0 -1
  390. package/dist/agterm/types.d.ts.map +0 -1
  391. package/dist/agterm/types.js.map +0 -1
  392. package/dist/cli.d.ts +0 -14
  393. package/dist/cli.d.ts.map +0 -1
  394. package/dist/cli.js.map +0 -1
  395. package/dist/providers.d.ts +0 -51
  396. package/dist/providers.d.ts.map +0 -1
  397. package/dist/providers.js +0 -1146
  398. package/dist/providers.js.map +0 -1
  399. package/dist/ui-cli.d.ts +0 -17
  400. package/dist/ui-cli.d.ts.map +0 -1
  401. package/dist/ui-cli.js +0 -3730
  402. package/dist/ui-cli.js.map +0 -1
@@ -0,0 +1,768 @@
1
+ /**
2
+ * UI Module - Agent Runner
3
+ *
4
+ * Core agent execution loop, tool handling, and message validation.
5
+ * Extracted from TerminalChat using an AgentContext state bag.
6
+ */
7
+ import { spawn } from 'child_process';
8
+ import * as config from '../config.js';
9
+ import { chat } from '../providers/index.js';
10
+ import { estimateContextUsage, needsSummarization } from '../providers/types.js';
11
+ import { executeTool, getTools } from '../tools.js';
12
+ import { DEFAULT_MODELS, RISK_CONFIG, calculateCost } from '../types.js';
13
+ import { getModelContextLimit } from '../model-detection.js';
14
+ import { assessToolRisk, requiresConfirmation } from '../risk.js';
15
+ import { formatError, classifyError } from '../errors.js';
16
+ import { getAvailableProviders } from '../providers/index.js';
17
+ import * as storage from '../storage.js';
18
+ import * as hooks from '../hooks.js';
19
+ import * as modelRouter from '../model-router.js';
20
+ import * as summarization from '../summarization.js';
21
+ import { executeParallel, getParallelizationStats } from '../parallel-tools.js';
22
+ import { setMood } from '../companions.js';
23
+ import { checkAndWarnContextLimit } from './context.js';
24
+ import { smartRoute } from '../smart-router.js';
25
+ import { shouldCheckpoint, createCheckpoint } from '../auto-checkpoint.js';
26
+ import { recordEvent } from '../terminal-recording.js';
27
+ // ============================================================================
28
+ // Tool Result Truncation
29
+ // ============================================================================
30
+ /**
31
+ * Truncate tool result content to fit within available context.
32
+ * For small models (< 16K), aggressively cap tool output to prevent context overflow.
33
+ */
34
+ function truncateToolResult(content, modelLimit) {
35
+ // Scale max tool result size based on model context
36
+ // Small models: 25% of context, large models: up to 50K chars
37
+ const maxChars = modelLimit < 8000 ? Math.floor(modelLimit * 0.6) // ~2.4K chars for 4K model
38
+ : modelLimit < 16000 ? Math.floor(modelLimit * 0.8) // ~12K chars for 16K model
39
+ : modelLimit < 32000 ? 20000
40
+ : 50000;
41
+ if (content.length <= maxChars)
42
+ return content;
43
+ const half = Math.floor(maxChars / 2);
44
+ const trimmed = content.slice(0, half) + `\n\n... [truncated ${content.length - maxChars} chars] ...\n\n` + content.slice(-half);
45
+ return trimmed;
46
+ }
47
+ // ============================================================================
48
+ // Validate and Repair Messages
49
+ // ============================================================================
50
+ /**
51
+ * Validate and repair message history to ensure tool_use always has tool_result.
52
+ */
53
+ export function validateAndRepairMessagesImpl(ctx) {
54
+ const messages = ctx.llmMessages.current;
55
+ let repaired = false;
56
+ for (let i = 0; i < messages.length; i++) {
57
+ const msg = messages[i];
58
+ if (msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length > 0) {
59
+ // Check that each tool_use has a corresponding tool_result
60
+ for (const toolCall of msg.toolCalls) {
61
+ const hasResult = messages.slice(i + 1).some(m => m.role === 'tool' && m.toolCallId === toolCall.id);
62
+ if (!hasResult) {
63
+ // Add a placeholder tool_result for the missing tool call
64
+ ctx.debugLog('repair', 'Adding missing tool_result for', toolCall.id);
65
+ // Find the right position to insert (right after this assistant message or after existing tool results)
66
+ let insertPos = i + 1;
67
+ while (insertPos < messages.length && messages[insertPos].role === 'tool') {
68
+ insertPos++;
69
+ }
70
+ messages.splice(insertPos, 0, {
71
+ role: 'tool',
72
+ content: '[Error: Tool execution was interrupted. Please retry.]',
73
+ toolCallId: toolCall.id,
74
+ });
75
+ repaired = true;
76
+ }
77
+ }
78
+ }
79
+ }
80
+ if (repaired) {
81
+ ctx.addMessage('system', '🔧 Repaired corrupted message history (missing tool results).');
82
+ }
83
+ return repaired;
84
+ }
85
+ // ============================================================================
86
+ // Run Agent
87
+ // ============================================================================
88
+ /**
89
+ * Run agent with user prompt. Core execution loop that handles LLM calls,
90
+ * tool execution (parallel + sequential), auto-compaction, and queued messages.
91
+ */
92
+ export async function runAgentImpl(ctx, content) {
93
+ ctx.debugLog('runAgent', 'ENTER', typeof content === 'string' ? content.substring(0, 50) : '[complex]');
94
+ setMood('thinking');
95
+ // Validate message history before adding new content
96
+ ctx.validateAndRepairMessages();
97
+ ctx.llmMessages.current.push({ role: 'user', content });
98
+ ctx.setStats(s => ({ ...s, messageCount: s.messageCount + 1 }));
99
+ ctx.setStreamingResponse('');
100
+ // Smart routing (cross-provider) takes precedence over autoRoute (single-provider)
101
+ let effectiveModel = ctx.model;
102
+ let effectiveProvider = ctx.provider;
103
+ if (ctx.smartRouteActive && ctx.smartRoutingConfig?.enabled && typeof content === 'string') {
104
+ const decision = smartRoute(content, ctx.smartRoutingConfig, {
105
+ messageCount: ctx.stats.messageCount,
106
+ hasCode: content.includes('```') || /\.(ts|js|py|go|rs|java)/.test(content),
107
+ });
108
+ effectiveModel = decision.selected.model;
109
+ effectiveProvider = decision.selected.provider;
110
+ if (effectiveModel !== ctx.model || effectiveProvider !== ctx.provider) {
111
+ ctx.addMessage('system', `[Smart route: ${decision.selected.provider}/${decision.selected.tier} - ${decision.taskType}/${decision.complexity}]`);
112
+ }
113
+ }
114
+ else if (ctx.autoRoute && typeof content === 'string') {
115
+ const routeDecision = modelRouter.routeRequest(content, ctx.provider, {
116
+ messageCount: ctx.stats.messageCount,
117
+ hasCode: content.includes('```') || /\.(ts|js|py|go|rs|java)/.test(content),
118
+ });
119
+ effectiveModel = routeDecision.model.model;
120
+ if (effectiveModel !== ctx.model) {
121
+ ctx.addMessage('system', `[Auto-route: ${routeDecision.tier} tier - ${routeDecision.reason}]`);
122
+ }
123
+ }
124
+ const maxIterations = config.get('maxIterations') || Infinity; // 0 = unlimited
125
+ let completedNaturally = false;
126
+ // Check context limit and warn if approaching capacity
127
+ // Uses model's actual context length from API when available
128
+ let currentContextTokens = ctx.estimateContextTokens();
129
+ const modelLimit = getModelContextLimit(ctx.actualProvider, effectiveModel || ctx.actualModel);
130
+ let contextPercentage = (currentContextTokens / modelLimit) * 100;
131
+ // Adaptive preserveRecent: small models keep fewer messages to leave room for output
132
+ const preserveRecent = modelLimit < 8000 ? 2 : modelLimit < 16000 ? 4 : modelLimit < 32000 ? 6 : modelLimit < 64000 ? 10 : 15;
133
+ // Auto-compact if we're over 75% capacity to prevent API errors
134
+ if (contextPercentage > 75) {
135
+ ctx.addMessage('system', `🔄 Context at ${Math.round(contextPercentage)}% - auto-compacting to prevent errors...`);
136
+ const result = summarization.summarizeConversation(ctx.llmMessages.current, {
137
+ maxTokens: Math.floor(modelLimit * 0.7), // Target 70% of limit after compaction
138
+ preserveRecent,
139
+ });
140
+ if (result.summarizedCount > 0) {
141
+ ctx.llmMessages.current = result.messages;
142
+ currentContextTokens = ctx.estimateContextTokens();
143
+ contextPercentage = (currentContextTokens / modelLimit) * 100;
144
+ ctx.setContextTokens(currentContextTokens);
145
+ ctx.addMessage('system', `✓ Compacted ${result.summarizedCount} messages. Now at ${Math.round(contextPercentage)}% (${Math.round(currentContextTokens / 1000)}K/${Math.round(modelLimit / 1000)}K)`);
146
+ }
147
+ else {
148
+ // If compaction didn't help enough, force-trim old messages
149
+ if (contextPercentage > 98) {
150
+ const systemMsgs = ctx.llmMessages.current.filter(m => m.role === 'system');
151
+ const recentMsgs = ctx.llmMessages.current.filter(m => m.role !== 'system').slice(-5);
152
+ ctx.llmMessages.current = [...systemMsgs, ...recentMsgs];
153
+ currentContextTokens = ctx.estimateContextTokens();
154
+ contextPercentage = (currentContextTokens / modelLimit) * 100;
155
+ ctx.setContextTokens(currentContextTokens);
156
+ ctx.addMessage('system', `⚠️ Force-trimmed to last 5 messages (${Math.round(contextPercentage)}%). Use /clear for a full reset.`);
157
+ }
158
+ }
159
+ }
160
+ else if (contextPercentage > 65) {
161
+ ctx.addMessage('system', `⚠️ Context at ${Math.round(contextPercentage)}% capacity (${Math.round(currentContextTokens / 1000)}K/${Math.round(modelLimit / 1000)}K tokens)
162
+ Consider: /summarize compact | /clear | shorter messages`);
163
+ }
164
+ // Inject failed approaches into context so the agent avoids repeating mistakes
165
+ if (ctx.ledger) {
166
+ const failedMsg = ctx.ledger.getFailedApproachesMessage();
167
+ if (failedMsg) {
168
+ ctx.llmMessages.current.push({ role: 'user', content: failedMsg });
169
+ }
170
+ }
171
+ for (let i = 0; i < maxIterations; i++) {
172
+ // Start ledger tracking for this iteration
173
+ ctx.ledger?.startIteration(i + 1);
174
+ // Safety check at start of each iteration - context may have grown from tool results
175
+ if (i > 0) {
176
+ const iterContextTokens = ctx.estimateContextTokens();
177
+ const iterContextPercentage = (iterContextTokens / modelLimit) * 100;
178
+ if (iterContextPercentage > 75) {
179
+ ctx.addMessage('system', `🔄 Context grew to ${Math.round(iterContextPercentage)}% - auto-compacting...`);
180
+ const result = summarization.summarizeConversation(ctx.llmMessages.current, {
181
+ maxTokens: Math.floor(modelLimit * 0.7),
182
+ preserveRecent,
183
+ });
184
+ if (result.summarizedCount > 0) {
185
+ ctx.llmMessages.current = result.messages;
186
+ ctx.setContextTokens(ctx.estimateContextTokens());
187
+ ctx.addMessage('system', `✓ Compacted ${result.summarizedCount} messages during iteration ${i + 1}`);
188
+ }
189
+ }
190
+ }
191
+ try {
192
+ // Update thinking state for LLM call
193
+ ctx.setThinkingState({
194
+ status: i === 0 ? 'Analyzing request...' : 'Processing response...',
195
+ detail: `Iteration ${i + 1}/${maxIterations}`,
196
+ iteration: i + 1,
197
+ maxIterations,
198
+ });
199
+ ctx.setActivityState({
200
+ action: i === 0 ? 'Analyzing request' : 'Processing',
201
+ target: `iteration ${i + 1}`,
202
+ startTime: Date.now(),
203
+ });
204
+ // Streaming callback for final response
205
+ const onToken = (token) => {
206
+ ctx.setThinkingState(null); // Clear thinking when streaming starts
207
+ ctx.setStreamingResponse(prev => prev + token);
208
+ };
209
+ // Retry callback for error recovery
210
+ const onRetry = (attempt, error, delayMs) => {
211
+ ctx.setThinkingState({
212
+ status: `Retrying... (attempt ${attempt + 1})`,
213
+ detail: `${error.message.substring(0, 40)}... Waiting ${Math.round(delayMs / 1000)}s`,
214
+ iteration: i + 1,
215
+ maxIterations,
216
+ });
217
+ };
218
+ ctx.debugLog('chat', 'WAITING for LLM response', `iteration=${i + 1}`);
219
+ // Validate message history to prevent orphaned tool_result errors
220
+ let validatedMessages = summarization.validateMessageHistory(ctx.llmMessages.current);
221
+ if (validatedMessages.length !== ctx.llmMessages.current.length) {
222
+ ctx.debugLog('chat', 'CLEANED orphaned tool results', `removed=${ctx.llmMessages.current.length - validatedMessages.length}`);
223
+ ctx.llmMessages.current = validatedMessages;
224
+ }
225
+ // Pre-request summarization check - summarize BEFORE sending if context is too large
226
+ const tools = getTools(ctx.agtermEnabled);
227
+ const contextCheck = estimateContextUsage(ctx.provider, effectiveModel || DEFAULT_MODELS[ctx.provider], validatedMessages, tools);
228
+ ctx.debugLog('chat', 'CONTEXT CHECK', `estimated=${contextCheck.estimated}, limit=${contextCheck.limit}, percent=${contextCheck.percent}%`);
229
+ if (contextCheck.needsSummarization) {
230
+ ctx.debugLog('chat', 'PRE-REQUEST SUMMARIZING', `estimated=${contextCheck.estimated} >= 80% of ${contextCheck.limit}`);
231
+ const result = summarization.summarizeConversation(validatedMessages, { maxTokens: Math.floor(contextCheck.limit * 0.6) });
232
+ if (result.summarizedCount > 0) {
233
+ ctx.llmMessages.current = result.messages;
234
+ validatedMessages = result.messages;
235
+ ctx.debugLog('chat', 'PRE-SUMMARIZED', `removed=${result.summarizedCount} messages, reduced from ${result.originalTokens} to ${result.reducedTokens}`);
236
+ }
237
+ }
238
+ const response = await chat(ctx.provider, validatedMessages, tools, effectiveModel, onToken, onRetry);
239
+ ctx.debugLog('chat', 'GOT response', `toolCalls=${response.toolCalls?.length ?? 0}`);
240
+ // Update token stats and cost
241
+ if (response.usage) {
242
+ const usageCost = calculateCost(ctx.model || DEFAULT_MODELS[ctx.provider], response.usage.inputTokens, response.usage.outputTokens);
243
+ ctx.setStats(s => ({
244
+ ...s,
245
+ inputTokens: s.inputTokens + response.usage.inputTokens,
246
+ outputTokens: s.outputTokens + response.usage.outputTokens,
247
+ cost: s.cost + usageCost,
248
+ }));
249
+ // Record in iteration ledger
250
+ ctx.ledger?.recordTokens(response.usage.inputTokens, response.usage.outputTokens, usageCost);
251
+ // Persist cost to storage
252
+ storage.recordCost(usageCost, ctx.actualProvider, ctx.sessionRef.current?.id);
253
+ // Auto-summarize if context is getting too full (85% threshold)
254
+ if (needsSummarization(ctx.provider, ctx.model || DEFAULT_MODELS[ctx.provider], response.usage.inputTokens)) {
255
+ ctx.debugLog('chat', 'AUTO-SUMMARIZING', `inputTokens=${response.usage.inputTokens}`);
256
+ const postCompactLimit = Math.floor(getModelContextLimit(ctx.provider, ctx.model || DEFAULT_MODELS[ctx.provider]) * 0.6);
257
+ const result = summarization.summarizeConversation(ctx.llmMessages.current, { maxTokens: postCompactLimit });
258
+ if (result.summarizedCount > 0) {
259
+ ctx.llmMessages.current = result.messages;
260
+ ctx.debugLog('chat', 'SUMMARIZED', `removed=${result.summarizedCount} messages`);
261
+ }
262
+ }
263
+ }
264
+ // Circuit breaker check after each iteration
265
+ if (ctx.circuitBreaker) {
266
+ const iterData = {
267
+ iteration: i + 1,
268
+ inputTokens: response.usage?.inputTokens,
269
+ outputTokens: response.usage?.outputTokens,
270
+ cost: response.usage ? calculateCost(ctx.model || DEFAULT_MODELS[ctx.provider], response.usage.inputTokens, response.usage.outputTokens) : undefined,
271
+ toolCalls: response.toolCalls?.map(tc => ({ name: tc.name, arguments: tc.arguments })),
272
+ content: response.content,
273
+ timestamp: new Date(),
274
+ };
275
+ const breakerResult = ctx.circuitBreaker.check(iterData);
276
+ ctx.setBreakerHealth?.(ctx.circuitBreaker.getHealth());
277
+ if (breakerResult.tripped) {
278
+ ctx.addMessage('system', `\u26a0\ufe0f Circuit breaker tripped: ${breakerResult.breaker}\n${breakerResult.message}\n\nUse /breaker resume to continue, /breaker status for details.`);
279
+ completedNaturally = true;
280
+ break;
281
+ }
282
+ }
283
+ // Handle tool calls with parallel execution support
284
+ if (response.toolCalls?.length) {
285
+ ctx.llmMessages.current.push({
286
+ role: 'assistant',
287
+ content: response.content,
288
+ toolCalls: response.toolCalls,
289
+ });
290
+ const preChecks = [];
291
+ const executableTools = [];
292
+ for (const toolCall of response.toolCalls) {
293
+ const args = toolCall.arguments;
294
+ const toolPreview = String(args.command || args.path || '...');
295
+ const risk = assessToolRisk(toolCall);
296
+ const riskConfig = RISK_CONFIG[risk.level];
297
+ const riskDisplay = risk.level !== 'none' ? ` [${riskConfig.bar}]` : '';
298
+ const preCheck = {
299
+ toolCall,
300
+ args,
301
+ preview: toolPreview,
302
+ risk,
303
+ riskDisplay,
304
+ blocked: false,
305
+ };
306
+ // Check blocking conditions
307
+ const PLAN_MODE_ALLOWED = new Set(['think', 'ask_question', 'create_plan', 'read_file', 'list_files']);
308
+ if (ctx.mode === 'plan' && !PLAN_MODE_ALLOWED.has(toolCall.name)) {
309
+ preCheck.blocked = true;
310
+ preCheck.blockReason = 'plan mode';
311
+ preCheck.blockContent = '[Plan mode: Tool not executed. Describe what this would do.]';
312
+ ctx.addMessage('tool', `📋 ${toolCall.name}: ${toolPreview}${riskDisplay} (plan mode - not executed)`);
313
+ }
314
+ else if (ctx.confirmMode && requiresConfirmation(risk, false) && toolCall.name !== 'think') {
315
+ preCheck.blocked = true;
316
+ preCheck.blockReason = 'confirmation required';
317
+ preCheck.blockContent = `[Operation blocked - ${risk.level} risk: ${risk.reason}. User confirmation required.]`;
318
+ const riskIcon = risk.level === 'critical' ? '🛑' : '⚠️';
319
+ ctx.addMessage('tool', `${riskIcon} ${toolCall.name}: ${toolPreview}${riskDisplay}\n → Requires confirmation (use /confirm off to disable)`);
320
+ }
321
+ else {
322
+ // Check pre-tool hooks
323
+ const preHookResult = await hooks.checkHooksAllow('pre-tool', {
324
+ tool: toolCall.name,
325
+ toolArgs: args,
326
+ });
327
+ if (!preHookResult.allowed) {
328
+ preCheck.blocked = true;
329
+ preCheck.blockReason = 'blocked by hook';
330
+ preCheck.blockContent = `[Blocked by hook: ${preHookResult.reason}]`;
331
+ ctx.addMessage('tool', `⚡ ${toolCall.name}: ${toolPreview}${riskDisplay}`);
332
+ ctx.addMessage('tool', `🛑 Blocked by hook: ${preHookResult.reason}`);
333
+ }
334
+ else {
335
+ // Tool can be executed
336
+ executableTools.push(toolCall);
337
+ ctx.addMessage('tool', `⚡ ${toolCall.name}: ${toolPreview}${riskDisplay}`);
338
+ }
339
+ }
340
+ preChecks.push(preCheck);
341
+ // Record blocked tools in ledger and add to LLM messages
342
+ if (preCheck.blocked) {
343
+ ctx.ledger?.recordAction(toolCall.name, args, 'blocked', preCheck.blockReason);
344
+ ctx.llmMessages.current.push({
345
+ role: 'tool',
346
+ content: preCheck.blockContent,
347
+ toolCallId: toolCall.id,
348
+ });
349
+ }
350
+ }
351
+ // ============================================================
352
+ // Phase 2: Execute tools (parallel when beneficial)
353
+ // ============================================================
354
+ if (executableTools.length > 0) {
355
+ const parallelStats = getParallelizationStats(executableTools);
356
+ const useParallel = parallelStats.maxParallel > 1 && executableTools.length > 1;
357
+ if (useParallel) {
358
+ // Show parallelization info
359
+ ctx.setThinkingState({
360
+ status: `Executing ${executableTools.length} tools in parallel...`,
361
+ detail: `${parallelStats.stages} stages, up to ${parallelStats.maxParallel}x speedup`,
362
+ iteration: i + 1,
363
+ maxIterations,
364
+ });
365
+ ctx.setActivityState({
366
+ action: `Executing ${executableTools.length} tools`,
367
+ target: 'in parallel',
368
+ startTime: Date.now(),
369
+ });
370
+ // Execute in parallel using dependency-aware staging
371
+ ctx.debugLog('tools', 'PARALLEL exec start', `count=${executableTools.length}`);
372
+ const results = await executeParallel(executableTools, async (call) => {
373
+ const result = await executeTool(call, process.cwd());
374
+ return result.result;
375
+ }, (completed, total, current) => {
376
+ const args = current.arguments;
377
+ const target = args.path || args.command?.substring(0, 30) || current.name;
378
+ ctx.setActivityState({
379
+ action: `Running ${current.name}`,
380
+ target: target,
381
+ startTime: Date.now(),
382
+ });
383
+ ctx.setThinkingState({
384
+ status: `Executing tools... (${completed + 1}/${total})`,
385
+ detail: current.name,
386
+ iteration: i + 1,
387
+ maxIterations,
388
+ });
389
+ });
390
+ ctx.debugLog('tools', 'PARALLEL exec done', `results=${results.length}`);
391
+ // Process results sequentially for UI and LLM messages
392
+ for (const result of results) {
393
+ const toolCall = result.toolCall;
394
+ const args = toolCall.arguments;
395
+ recordEvent('tool_call', toolCall.name, { name: toolCall.name, arguments: args });
396
+ recordEvent('tool_result', (result.result || result.error || '').slice(0, 1000), { name: toolCall.name, isError: !!result.error });
397
+ // Record in iteration ledger
398
+ ctx.ledger?.recordAction(toolCall.name, args, result.error ? 'error' : 'ok', result.error || undefined);
399
+ // Execute post-tool hooks
400
+ hooks.executeHooks('post-tool', {
401
+ tool: toolCall.name,
402
+ toolArgs: args,
403
+ toolResult: result.result,
404
+ }).catch((err) => {
405
+ ctx.debugLog('hooks', `post-tool hook failed for ${toolCall.name}:`, err instanceof Error ? err.message : err);
406
+ });
407
+ // Display result
408
+ if (toolCall.name === 'think') {
409
+ const thought = String(args.thought || '');
410
+ ctx.addMessage('tool', thought);
411
+ }
412
+ else if (result.error) {
413
+ ctx.addMessage('tool', `Error: ${result.error}`);
414
+ }
415
+ else {
416
+ const preview = result.result.split('\n').slice(0, 3).join('\n');
417
+ ctx.addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''));
418
+ }
419
+ ctx.llmMessages.current.push({
420
+ role: 'tool',
421
+ content: truncateToolResult(result.error ? `Error: ${result.error}` : result.result, modelLimit),
422
+ toolCallId: toolCall.id,
423
+ });
424
+ }
425
+ }
426
+ else {
427
+ // Sequential execution (single tool or dependencies prevent parallelization)
428
+ ctx.debugLog('tools', 'SEQUENTIAL exec start', `count=${executableTools.length}`);
429
+ for (const toolCall of executableTools) {
430
+ const args = toolCall.arguments;
431
+ const toolPreview = String(args.command || args.path || args.content?.toString().substring(0, 30) || '...');
432
+ // Set activity state for streaming indicator
433
+ const actionMap = {
434
+ read_file: 'Reading',
435
+ write_file: 'Writing',
436
+ edit_file: 'Editing',
437
+ bash: 'Running',
438
+ search: 'Searching',
439
+ glob: 'Finding',
440
+ think: 'Thinking',
441
+ };
442
+ const action = actionMap[toolCall.name] || `Executing ${toolCall.name}`;
443
+ const target = toolCall.name === 'bash'
444
+ ? args.command?.substring(0, 40) + (args.command?.length > 40 ? '...' : '')
445
+ : toolCall.name === 'think'
446
+ ? undefined
447
+ : args.path || args.pattern;
448
+ ctx.setActivityState({ action, target, startTime: Date.now() });
449
+ // Special handling for think tool UI
450
+ if (toolCall.name === 'think') {
451
+ const thought = String(args.thought || '');
452
+ ctx.setThinkingState({
453
+ status: 'Reasoning...',
454
+ detail: thought.substring(0, 60) + (thought.length > 60 ? '...' : ''),
455
+ thinking: thought,
456
+ iteration: i + 1,
457
+ maxIterations,
458
+ });
459
+ }
460
+ else {
461
+ ctx.setThinkingState({
462
+ status: `Executing ${toolCall.name}...`,
463
+ detail: toolPreview.substring(0, 60),
464
+ thinking: undefined,
465
+ iteration: i + 1,
466
+ maxIterations,
467
+ });
468
+ }
469
+ ctx.debugLog('tools', 'EXEC', toolCall.name, toolPreview.substring(0, 30));
470
+ recordEvent('tool_call', toolCall.name, { name: toolCall.name, arguments: args });
471
+ // Auto-checkpoint before destructive operations
472
+ if (shouldCheckpoint(toolCall.name, args)) {
473
+ const hash = createCheckpoint(toolCall.name, args);
474
+ if (hash) {
475
+ ctx.debugLog('checkpoint', `auto-checkpoint ${hash} before ${toolCall.name}`);
476
+ }
477
+ }
478
+ // Stream shell output in real-time (#15)
479
+ const shellStreamCallback = toolCall.name === 'shell' ? (chunk) => {
480
+ ctx.setActivityState({
481
+ action: 'Running shell',
482
+ target: args.command?.substring(0, 40),
483
+ startTime: Date.now(),
484
+ detail: chunk.trimEnd().split('\n').pop()?.substring(0, 60),
485
+ });
486
+ } : undefined;
487
+ const result = await executeTool(toolCall, process.cwd(), 60000, shellStreamCallback);
488
+ ctx.debugLog('tools', 'DONE', toolCall.name);
489
+ recordEvent('tool_result', result.result.slice(0, 1000), { name: toolCall.name, isError: result.isError });
490
+ // Record in iteration ledger
491
+ ctx.ledger?.recordAction(toolCall.name, args, result.isError ? 'error' : 'ok', result.isError ? result.result : undefined);
492
+ // Execute post-tool hooks
493
+ hooks.executeHooks('post-tool', {
494
+ tool: toolCall.name,
495
+ toolArgs: args,
496
+ toolResult: result.result,
497
+ }).catch((err) => {
498
+ ctx.debugLog('hooks', `post-tool hook failed for ${toolCall.name}:`, err instanceof Error ? err.message : err);
499
+ });
500
+ // Display result - use displayResult for UI, full result for LLM (#25)
501
+ if (toolCall.name === 'think') {
502
+ const thought = String(args.thought || '');
503
+ ctx.addMessage('tool', thought);
504
+ }
505
+ else if (toolCall.name === 'ask_question') {
506
+ // Display question prominently (#42)
507
+ const question = String(args.question || '');
508
+ const options = Array.isArray(args.options) ? args.options : undefined;
509
+ const contextNote = typeof args.context === 'string' ? args.context : undefined;
510
+ let questionMsg = `❓ ${question}`;
511
+ if (contextNote)
512
+ questionMsg += `\n ${contextNote}`;
513
+ if (options)
514
+ questionMsg += '\n' + options.map((o, i) => ` ${i + 1}. ${o}`).join('\n');
515
+ ctx.addMessage('assistant', questionMsg);
516
+ // Tell the LLM that we're waiting for user input
517
+ ctx.llmMessages.current.push({
518
+ role: 'tool',
519
+ content: '[Waiting for user response. The user will reply with their answer.]',
520
+ toolCallId: toolCall.id,
521
+ });
522
+ // Break out of tool loop - let user respond naturally
523
+ completedNaturally = true;
524
+ }
525
+ else if (toolCall.name === 'create_plan') {
526
+ // Display plan as a checklist for user approval (#19)
527
+ const planTitle = String(args.title || 'Plan');
528
+ const planSteps = Array.isArray(args.steps) ? args.steps : [];
529
+ const planReasoning = typeof args.reasoning === 'string' ? args.reasoning : undefined;
530
+ let planMsg = `📋 Plan: ${planTitle}\n`;
531
+ if (planReasoning)
532
+ planMsg += `\n ${planReasoning}\n`;
533
+ planMsg += '\n' + planSteps.map((s, idx) => ` ${idx + 1}. [ ] ${s}`).join('\n');
534
+ planMsg += '\n\n Type /approve to execute, or provide feedback to revise.';
535
+ ctx.addMessage('assistant', planMsg);
536
+ // Tell the LLM to wait for user approval
537
+ ctx.llmMessages.current.push({
538
+ role: 'tool',
539
+ content: '[Plan displayed to user. Waiting for approval. The user will either type /approve to execute the plan, or provide feedback to revise it. Do NOT proceed with execution until the user approves.]',
540
+ toolCallId: toolCall.id,
541
+ });
542
+ // Break out of tool loop - wait for user approval
543
+ completedNaturally = true;
544
+ }
545
+ else {
546
+ const display = result.displayResult || result.result;
547
+ const preview = display.split('\n').slice(0, 5).join('\n');
548
+ ctx.addMessage('tool', preview + (display.split('\n').length > 5 ? '\n...' : ''));
549
+ }
550
+ if (toolCall.name !== 'ask_question') {
551
+ ctx.llmMessages.current.push({
552
+ role: 'tool',
553
+ content: truncateToolResult(result.result, modelLimit),
554
+ toolCallId: toolCall.id,
555
+ });
556
+ }
557
+ }
558
+ }
559
+ }
560
+ ctx.ledger?.endIteration();
561
+ if (completedNaturally)
562
+ break; // ask_question pauses for user input (#42)
563
+ continue;
564
+ }
565
+ // Final response - move streaming content to message history
566
+ ctx.setThinkingState(null);
567
+ ctx.llmMessages.current.push({ role: 'assistant', content: response.content });
568
+ ctx.addMessage('assistant', response.content);
569
+ recordEvent('output', response.content.slice(0, 5000));
570
+ ctx.setStreamingResponse('');
571
+ ctx.setContextTokens(ctx.estimateContextTokens());
572
+ checkAndWarnContextLimit(ctx.actualProvider, ctx.actualModel, ctx.estimateContextTokens(), ctx.addMessage);
573
+ setMood('success');
574
+ // Auto-continue if response was truncated due to length
575
+ if (response.finishReason === 'length') {
576
+ ctx.addMessage('system', '(auto-continuing...)');
577
+ ctx.llmMessages.current.push({ role: 'user', content: 'Please continue where you left off.' });
578
+ continue; // Loop again to get continuation
579
+ }
580
+ completedNaturally = true;
581
+ // End iteration ledger entry
582
+ ctx.ledger?.endIteration('success');
583
+ // Auto-save full message history for session persistence
584
+ storage.saveMessageHistory(ctx.llmMessages.current);
585
+ break;
586
+ }
587
+ catch (error) {
588
+ ctx.setThinkingState(null);
589
+ ctx.setActivityState(null);
590
+ ctx.setStreamingResponse('');
591
+ // End iteration ledger entry with error
592
+ ctx.ledger?.endIteration('error');
593
+ // Format error with provider context for better suggestions
594
+ setMood('error');
595
+ const errorMsg = formatError(error, { provider: ctx.actualProvider });
596
+ ctx.addMessage('error', errorMsg);
597
+ // Classify error to provide additional recovery suggestions
598
+ const classified = classifyError(error);
599
+ const availableProviders = getAvailableProviders();
600
+ const otherProviders = availableProviders.filter(p => p !== ctx.actualProvider);
601
+ // Feed error to circuit breaker
602
+ if (ctx.circuitBreaker) {
603
+ const errorIterData = {
604
+ iteration: i + 1,
605
+ error: errorMsg,
606
+ timestamp: new Date(),
607
+ };
608
+ const breakerResult = ctx.circuitBreaker.check(errorIterData);
609
+ ctx.setBreakerHealth?.(ctx.circuitBreaker.getHealth());
610
+ if (breakerResult.tripped) {
611
+ ctx.addMessage('system', `\u26a0\ufe0f Circuit breaker tripped: ${breakerResult.breaker}\n${breakerResult.message}\n\nUse /breaker resume to continue.`);
612
+ completedNaturally = true;
613
+ break;
614
+ }
615
+ }
616
+ // Retryable errors continue the loop (circuit breaker handles safety)
617
+ const isRetryable = classified.category === 'rate_limit' || classified.category === 'server' || classified.category === 'timeout' || classified.category === 'network';
618
+ // Suggest alternatives based on error type
619
+ if (classified.category === 'rate_limit' || classified.category === 'server') {
620
+ if (otherProviders.length > 0) {
621
+ ctx.addMessage('system', `\u{1f4a1} Try switching providers: /provider ${otherProviders[0]} or /models to see alternatives`);
622
+ }
623
+ }
624
+ else if (classified.category === 'timeout' || classified.category === 'network') {
625
+ ctx.addMessage('system', `\u{1f4a1} Network issue detected. Check connection and try again, or use /provider to switch.`);
626
+ }
627
+ else if (classified.category === 'auth') {
628
+ ctx.addMessage('system', `\u{1f4a1} Run 'calliope --setup' to reconfigure API keys.`);
629
+ }
630
+ if (isRetryable && ctx.circuitBreaker) {
631
+ // Retryable errors: continue the loop, circuit breaker will catch repeated failures
632
+ ctx.addMessage('system', `Retrying... (circuit breaker will pause after ${ctx.circuitBreaker.getConfig().breakers['repeated-failure'].maxConsecutiveErrors} consecutive failures)`);
633
+ await new Promise(r => setTimeout(r, 2000)); // Brief delay before retry
634
+ continue;
635
+ }
636
+ // Non-retryable errors (auth, etc.) still kill the session
637
+ completedNaturally = true;
638
+ // On error, clear queued messages to prevent infinite retry loop
639
+ const currentQueuedOnError = ctx.queuedMessagesRef.current;
640
+ if (currentQueuedOnError.length > 0) {
641
+ ctx.addMessage('system', `\u26a0\ufe0f Cleared ${currentQueuedOnError.length} queued message(s) due to error. Use /clear to reset conversation.`);
642
+ ctx.setQueuedMessages([]);
643
+ }
644
+ return; // Exit early on error - don't process queued messages
645
+ }
646
+ }
647
+ // Only show warning if we actually hit the iteration limit (not errors or natural completion)
648
+ if (!completedNaturally) {
649
+ ctx.addMessage('system', `⚠️ Reached ${maxIterations} iterations limit. Task may be incomplete. Adjust with /set maxIterations <number>.`);
650
+ }
651
+ // Update context tokens after agent run
652
+ ctx.setContextTokens(ctx.estimateContextTokens());
653
+ // Process any queued messages (human-in-the-loop feedback)
654
+ // CRITICAL: Use ref to get current value, not stale closure
655
+ const currentQueued = ctx.queuedMessagesRef.current;
656
+ ctx.debugLog('runAgent', 'EXIT loop', `queued=${currentQueued.length}`);
657
+ if (currentQueued.length > 0) {
658
+ const queued = [...currentQueued];
659
+ ctx.setQueuedMessages([]); // Clear the queue
660
+ ctx.queuedMessagesRef.current = []; // Also clear ref immediately
661
+ // Combine queued messages into a single follow-up
662
+ const followUp = queued.length === 1
663
+ ? queued[0]
664
+ : `[Multiple follow-up messages from user:]\n${queued.map((m, i) => `${i + 1}. ${m}`).join('\n')}`;
665
+ ctx.addMessage('system', `📨 Processing ${queued.length} queued message${queued.length > 1 ? 's' : ''}...`);
666
+ // Recursively run agent with follow-up
667
+ // Use setTimeout to avoid stack overflow and allow UI to update
668
+ // Note: handleSubmit's finally will set isProcessing=false, so we need to re-enable it
669
+ ctx.debugLog('runAgent', 'SCHEDULING recursive call for queued messages');
670
+ setTimeout(() => {
671
+ ctx.debugLog('runAgent', 'RECURSIVE call starting');
672
+ ctx.setIsProcessing(true);
673
+ runAgentImpl(ctx, followUp).finally(() => {
674
+ ctx.setIsProcessing(false);
675
+ ctx.setThinkingState(null);
676
+ ctx.setActivityState(null);
677
+ ctx.setStreamingResponse('');
678
+ ctx.setEditingQueueIndex(null);
679
+ });
680
+ }, 100);
681
+ }
682
+ ctx.debugLog('runAgent', 'RETURN');
683
+ }
684
+ // ============================================================================
685
+ // Run Loop
686
+ // ============================================================================
687
+ /**
688
+ * Start caffeinate to prevent system sleep during long operations (macOS).
689
+ */
690
+ function startCaffeinate() {
691
+ if (process.platform !== 'darwin')
692
+ return null;
693
+ try {
694
+ const proc = spawn('caffeinate', ['-di'], { stdio: 'ignore', detached: true });
695
+ proc.unref();
696
+ return proc;
697
+ }
698
+ catch {
699
+ return null;
700
+ }
701
+ }
702
+ function stopCaffeinate(proc) {
703
+ if (proc) {
704
+ try {
705
+ proc.kill('SIGTERM');
706
+ }
707
+ catch { /* already dead */ }
708
+ }
709
+ }
710
+ /**
711
+ * Agent loop - runs prompt repeatedly until completion promise or max iterations.
712
+ */
713
+ export async function runLoopImpl(ctx, prompt, maxIter, completionPromise) {
714
+ ctx.setIsProcessing(true);
715
+ setMood('focused');
716
+ // Prevent system sleep during long agent loops (macOS)
717
+ const caffeinateProc = startCaffeinate();
718
+ for (let i = 0; i < maxIter; i++) {
719
+ // Check if cancelled
720
+ if (ctx.loopCancelledRef.current) {
721
+ ctx.addMessage('system', '🛑 Loop cancelled by user');
722
+ break;
723
+ }
724
+ ctx.setLoopIteration(i + 1);
725
+ ctx.addMessage('system', `🔄 Loop iteration ${i + 1}/${maxIter}`);
726
+ // First iteration: send original prompt. Subsequent: send continuation.
727
+ const iterationPrompt = i === 0
728
+ ? prompt
729
+ : `Continue working on the task: "${prompt}"\n\nThis is iteration ${i + 1}. Review what you've done so far and continue making progress.`;
730
+ ctx.llmMessages.current.push({ role: 'user', content: iterationPrompt });
731
+ try {
732
+ // Run the agent
733
+ await runAgentImpl(ctx, iterationPrompt);
734
+ // Check for completion promise in the last assistant message
735
+ if (completionPromise) {
736
+ const lastMessage = ctx.llmMessages.current[ctx.llmMessages.current.length - 1];
737
+ if (lastMessage?.role === 'assistant') {
738
+ const content = typeof lastMessage.content === 'string'
739
+ ? lastMessage.content
740
+ : JSON.stringify(lastMessage.content);
741
+ if (content.includes(completionPromise)) {
742
+ ctx.addMessage('system', `🎉 Completion promise "${completionPromise}" detected! Loop finished.`);
743
+ break;
744
+ }
745
+ }
746
+ }
747
+ // Check cancelled again after agent run
748
+ if (ctx.loopCancelledRef.current) {
749
+ ctx.addMessage('system', '🛑 Loop cancelled by user');
750
+ break;
751
+ }
752
+ // Small delay between iterations
753
+ await new Promise(r => setTimeout(r, 500));
754
+ }
755
+ catch (error) {
756
+ ctx.addMessage('error', `Loop error: ${error instanceof Error ? error.message : String(error)}`);
757
+ break;
758
+ }
759
+ }
760
+ // If we completed all iterations without hitting completion promise
761
+ if (!ctx.loopCancelledRef.current && !completionPromise) {
762
+ ctx.addMessage('system', `✅ Loop completed ${maxIter} iterations`);
763
+ }
764
+ ctx.setLoopActive(false);
765
+ ctx.setIsProcessing(false);
766
+ stopCaffeinate(caffeinateProc);
767
+ }
768
+ //# sourceMappingURL=agent.js.map