micro-models-agent 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/README.md +436 -436
  2. package/dist/agent/loop.d.ts +5 -5
  3. package/dist/agent/loop.d.ts.map +1 -1
  4. package/dist/agent/loop.js +421 -246
  5. package/dist/agent/loop.js.map +1 -1
  6. package/dist/agent/system-prompt.d.ts +3 -3
  7. package/dist/agent/system-prompt.d.ts.map +1 -1
  8. package/dist/agent/system-prompt.js +383 -278
  9. package/dist/agent/system-prompt.js.map +1 -1
  10. package/dist/cli/config.d.ts +1 -1
  11. package/dist/cli/config.d.ts.map +1 -1
  12. package/dist/cli/config.js +53 -28
  13. package/dist/cli/config.js.map +1 -1
  14. package/dist/config/config.d.ts +2 -1
  15. package/dist/config/config.d.ts.map +1 -1
  16. package/dist/config/config.js +75 -54
  17. package/dist/config/config.js.map +1 -1
  18. package/dist/config/types.d.ts +3 -1
  19. package/dist/config/types.d.ts.map +1 -1
  20. package/dist/plugins/builtin/lint-on-write/index.ts +96 -96
  21. package/dist/plugins/builtin/notify/index.js +6 -6
  22. package/dist/plugins/builtin/notify/index.ts +104 -104
  23. package/dist/setup-wizard.d.ts +1 -1
  24. package/dist/setup-wizard.d.ts.map +1 -1
  25. package/dist/setup-wizard.js +137 -105
  26. package/dist/setup-wizard.js.map +1 -1
  27. package/dist/skills/builtin/agent-test/SKILL.md +68 -68
  28. package/dist/skills/builtin/grill-me/SKILL.md +46 -46
  29. package/dist/skills/builtin/web-scraping/SKILL.md +422 -422
  30. package/dist/tools/native/project-map.d.ts +2 -2
  31. package/dist/tools/native/project-map.d.ts.map +1 -1
  32. package/dist/tools/native/project-map.js +85 -57
  33. package/dist/tools/native/project-map.js.map +1 -1
  34. package/dist/tools/native/scaffold.js +113 -113
  35. package/dist/tools/web-browse.js +7 -7
  36. package/dist/ui/ink/components/App.d.ts +0 -1
  37. package/dist/ui/ink/components/App.d.ts.map +1 -1
  38. package/dist/ui/ink/components/App.js +2 -8
  39. package/dist/ui/ink/components/App.js.map +1 -1
  40. package/dist/ui/ink/components/Conversation.d.ts +1 -2
  41. package/dist/ui/ink/components/Conversation.d.ts.map +1 -1
  42. package/dist/ui/ink/components/Conversation.js +8 -2
  43. package/dist/ui/ink/components/Conversation.js.map +1 -1
  44. package/dist/ui/ink/components/MessageBlock.d.ts +13 -1
  45. package/dist/ui/ink/components/MessageBlock.d.ts.map +1 -1
  46. package/dist/ui/ink/components/MessageBlock.js +15 -27
  47. package/dist/ui/ink/components/MessageBlock.js.map +1 -1
  48. package/dist/ui/ink/components/PromptInput.d.ts +11 -0
  49. package/dist/ui/ink/components/PromptInput.d.ts.map +1 -1
  50. package/dist/ui/ink/components/PromptInput.js +18 -141
  51. package/dist/ui/ink/components/PromptInput.js.map +1 -1
  52. package/dist/ui/ink/components/SelectInput.d.ts.map +1 -1
  53. package/dist/ui/ink/components/SelectInput.js +3 -2
  54. package/dist/ui/ink/components/SelectInput.js.map +1 -1
  55. package/dist/ui/ink/components/StatusBar.d.ts.map +1 -1
  56. package/dist/ui/ink/components/StatusBar.js +14 -6
  57. package/dist/ui/ink/components/StatusBar.js.map +1 -1
  58. package/dist/ui/ink/components/StreamBlock.d.ts.map +1 -1
  59. package/dist/ui/ink/components/StreamBlock.js +3 -3
  60. package/dist/ui/ink/components/StreamBlock.js.map +1 -1
  61. package/dist/ui/ink/components/Tags.d.ts +8 -11
  62. package/dist/ui/ink/components/Tags.d.ts.map +1 -1
  63. package/dist/ui/ink/components/Tags.js +4 -16
  64. package/dist/ui/ink/components/Tags.js.map +1 -1
  65. package/dist/ui/ink/components/ToolCallBlock.d.ts +12 -0
  66. package/dist/ui/ink/components/ToolCallBlock.d.ts.map +1 -1
  67. package/dist/ui/ink/components/ToolCallBlock.js +8 -24
  68. package/dist/ui/ink/components/ToolCallBlock.js.map +1 -1
  69. package/dist/ui/ink/renderer.d.ts +0 -1
  70. package/dist/ui/ink/renderer.d.ts.map +1 -1
  71. package/dist/ui/ink/renderer.js +1 -4
  72. package/dist/ui/ink/renderer.js.map +1 -1
  73. package/package.json +47 -47
  74. package/dist/async-map.d.ts +0 -6
  75. package/dist/async-map.d.ts.map +0 -1
  76. package/dist/async-map.js +0 -16
  77. package/dist/async-map.js.map +0 -1
  78. package/dist/blog-api/types/index.d.ts +0 -2
  79. package/dist/blog-api/types/index.d.ts.map +0 -1
  80. package/dist/blog-api/types/index.js +0 -2
  81. package/dist/blog-api/types/index.js.map +0 -1
  82. package/dist/examples.d.ts +0 -22
  83. package/dist/examples.d.ts.map +0 -1
  84. package/dist/examples.js +0 -45
  85. package/dist/examples.js.map +0 -1
  86. package/dist/fibonacci.d.ts +0 -19
  87. package/dist/fibonacci.d.ts.map +0 -1
  88. package/dist/fibonacci.js +0 -61
  89. package/dist/fibonacci.js.map +0 -1
  90. package/dist/parfymerie-landing/src/App.d.ts +0 -4
  91. package/dist/parfymerie-landing/src/App.d.ts.map +0 -1
  92. package/dist/parfymerie-landing/src/App.js +0 -12
  93. package/dist/parfymerie-landing/src/App.js.map +0 -1
  94. package/dist/parfymerie-landing/src/main.d.ts +0 -2
  95. package/dist/parfymerie-landing/src/main.d.ts.map +0 -1
  96. package/dist/parfymerie-landing/src/main.js +0 -7
  97. package/dist/parfymerie-landing/src/main.js.map +0 -1
  98. package/dist/parfymerie-landing/vite.config.d.ts +0 -3
  99. package/dist/parfymerie-landing/vite.config.d.ts.map +0 -1
  100. package/dist/parfymerie-landing/vite.config.js +0 -7
  101. package/dist/parfymerie-landing/vite.config.js.map +0 -1
  102. package/dist/repl.d.ts +0 -5
  103. package/dist/repl.d.ts.map +0 -1
  104. package/dist/repl.js +0 -472
  105. package/dist/repl.js.map +0 -1
  106. package/dist/sort.d.ts +0 -8
  107. package/dist/sort.d.ts.map +0 -1
  108. package/dist/sort.js +0 -36
  109. package/dist/sort.js.map +0 -1
  110. package/dist/stack.d.ts +0 -9
  111. package/dist/stack.d.ts.map +0 -1
  112. package/dist/stack.js +0 -22
  113. package/dist/stack.js.map +0 -1
  114. package/dist/test-run.d.ts +0 -2
  115. package/dist/test-run.d.ts.map +0 -1
  116. package/dist/test-run.js +0 -13
  117. package/dist/test-run.js.map +0 -1
  118. package/dist/ui/components/diff-view.d.ts +0 -8
  119. package/dist/ui/components/diff-view.d.ts.map +0 -1
  120. package/dist/ui/components/diff-view.js +0 -25
  121. package/dist/ui/components/diff-view.js.map +0 -1
  122. package/dist/ui/double-press.d.ts +0 -9
  123. package/dist/ui/double-press.d.ts.map +0 -1
  124. package/dist/ui/double-press.js +0 -18
  125. package/dist/ui/double-press.js.map +0 -1
  126. package/dist/ui/ink/components/ActivityLog.d.ts +0 -12
  127. package/dist/ui/ink/components/ActivityLog.d.ts.map +0 -1
  128. package/dist/ui/ink/components/ActivityLog.js +0 -11
  129. package/dist/ui/ink/components/ActivityLog.js.map +0 -1
  130. package/dist/ui/ink/components/AgentStatus.d.ts +0 -13
  131. package/dist/ui/ink/components/AgentStatus.d.ts.map +0 -1
  132. package/dist/ui/ink/components/AgentStatus.js +0 -20
  133. package/dist/ui/ink/components/AgentStatus.js.map +0 -1
  134. package/dist/ui/ink/components/BottomBar.d.ts +0 -12
  135. package/dist/ui/ink/components/BottomBar.d.ts.map +0 -1
  136. package/dist/ui/ink/components/BottomBar.js +0 -22
  137. package/dist/ui/ink/components/BottomBar.js.map +0 -1
  138. package/dist/ui/ink/components/CheckboxInput.d.ts +0 -21
  139. package/dist/ui/ink/components/CheckboxInput.d.ts.map +0 -1
  140. package/dist/ui/ink/components/CheckboxInput.js +0 -36
  141. package/dist/ui/ink/components/CheckboxInput.js.map +0 -1
  142. package/dist/ui/ink/components/InputBox.d.ts +0 -25
  143. package/dist/ui/ink/components/InputBox.d.ts.map +0 -1
  144. package/dist/ui/ink/components/InputBox.js +0 -91
  145. package/dist/ui/ink/components/InputBox.js.map +0 -1
  146. package/dist/ui/ink/components/ModeSelector.d.ts +0 -8
  147. package/dist/ui/ink/components/ModeSelector.d.ts.map +0 -1
  148. package/dist/ui/ink/components/ModeSelector.js +0 -13
  149. package/dist/ui/ink/components/ModeSelector.js.map +0 -1
  150. package/dist/ui/ink/components/QuestionBox.d.ts +0 -14
  151. package/dist/ui/ink/components/QuestionBox.d.ts.map +0 -1
  152. package/dist/ui/ink/components/QuestionBox.js +0 -35
  153. package/dist/ui/ink/components/QuestionBox.js.map +0 -1
  154. package/dist/ui/ink/components/RadioInput.d.ts +0 -17
  155. package/dist/ui/ink/components/RadioInput.d.ts.map +0 -1
  156. package/dist/ui/ink/components/RadioInput.js +0 -30
  157. package/dist/ui/ink/components/RadioInput.js.map +0 -1
  158. package/dist/ui/ink/components/SliderInput.d.ts +0 -20
  159. package/dist/ui/ink/components/SliderInput.d.ts.map +0 -1
  160. package/dist/ui/ink/components/SliderInput.js +0 -40
  161. package/dist/ui/ink/components/SliderInput.js.map +0 -1
  162. package/dist/ui/ink/components/StartupScreen.d.ts +0 -19
  163. package/dist/ui/ink/components/StartupScreen.d.ts.map +0 -1
  164. package/dist/ui/ink/components/StartupScreen.js +0 -9
  165. package/dist/ui/ink/components/StartupScreen.js.map +0 -1
  166. package/dist/ui/ink/components/WelcomeScreen.d.ts +0 -10
  167. package/dist/ui/ink/components/WelcomeScreen.d.ts.map +0 -1
  168. package/dist/ui/ink/components/WelcomeScreen.js +0 -8
  169. package/dist/ui/ink/components/WelcomeScreen.js.map +0 -1
  170. package/dist/ui/ink/components/Window.d.ts +0 -30
  171. package/dist/ui/ink/components/Window.d.ts.map +0 -1
  172. package/dist/ui/ink/components/Window.js +0 -9
  173. package/dist/ui/ink/components/Window.js.map +0 -1
  174. package/dist/ui/ink/components/ui/_core.d.ts +0 -113
  175. package/dist/ui/ink/components/ui/_core.d.ts.map +0 -1
  176. package/dist/ui/ink/components/ui/_core.js +0 -83
  177. package/dist/ui/ink/components/ui/_core.js.map +0 -1
  178. package/dist/ui/ink/components/ui/autocomplete/Autocomplete.d.ts +0 -23
  179. package/dist/ui/ink/components/ui/autocomplete/Autocomplete.d.ts.map +0 -1
  180. package/dist/ui/ink/components/ui/autocomplete/Autocomplete.js +0 -56
  181. package/dist/ui/ink/components/ui/autocomplete/Autocomplete.js.map +0 -1
  182. package/dist/ui/ink/components/ui/autocomplete/index.d.ts +0 -3
  183. package/dist/ui/ink/components/ui/autocomplete/index.d.ts.map +0 -1
  184. package/dist/ui/ink/components/ui/autocomplete/index.js +0 -2
  185. package/dist/ui/ink/components/ui/autocomplete/index.js.map +0 -1
  186. package/dist/ui/ink/components/ui/badge/Badge.d.ts +0 -13
  187. package/dist/ui/ink/components/ui/badge/Badge.d.ts.map +0 -1
  188. package/dist/ui/ink/components/ui/badge/Badge.js +0 -17
  189. package/dist/ui/ink/components/ui/badge/Badge.js.map +0 -1
  190. package/dist/ui/ink/components/ui/badge/index.d.ts +0 -3
  191. package/dist/ui/ink/components/ui/badge/index.d.ts.map +0 -1
  192. package/dist/ui/ink/components/ui/badge/index.js +0 -2
  193. package/dist/ui/ink/components/ui/badge/index.js.map +0 -1
  194. package/dist/ui/ink/components/ui/divider/Divider.d.ts +0 -12
  195. package/dist/ui/ink/components/ui/divider/Divider.d.ts.map +0 -1
  196. package/dist/ui/ink/components/ui/divider/Divider.js +0 -27
  197. package/dist/ui/ink/components/ui/divider/Divider.js.map +0 -1
  198. package/dist/ui/ink/components/ui/divider/index.d.ts +0 -3
  199. package/dist/ui/ink/components/ui/divider/index.d.ts.map +0 -1
  200. package/dist/ui/ink/components/ui/divider/index.js +0 -2
  201. package/dist/ui/ink/components/ui/divider/index.js.map +0 -1
  202. package/dist/ui/ink/components/ui/header/Header.d.ts +0 -13
  203. package/dist/ui/ink/components/ui/header/Header.d.ts.map +0 -1
  204. package/dist/ui/ink/components/ui/header/Header.js +0 -33
  205. package/dist/ui/ink/components/ui/header/Header.js.map +0 -1
  206. package/dist/ui/ink/components/ui/header/index.d.ts +0 -3
  207. package/dist/ui/ink/components/ui/header/index.d.ts.map +0 -1
  208. package/dist/ui/ink/components/ui/header/index.js +0 -2
  209. package/dist/ui/ink/components/ui/header/index.js.map +0 -1
  210. package/dist/ui/ink/components/ui/json-viewer/JSONViewer.d.ts +0 -14
  211. package/dist/ui/ink/components/ui/json-viewer/JSONViewer.d.ts.map +0 -1
  212. package/dist/ui/ink/components/ui/json-viewer/JSONViewer.js +0 -144
  213. package/dist/ui/ink/components/ui/json-viewer/JSONViewer.js.map +0 -1
  214. package/dist/ui/ink/components/ui/json-viewer/index.d.ts +0 -3
  215. package/dist/ui/ink/components/ui/json-viewer/index.d.ts.map +0 -1
  216. package/dist/ui/ink/components/ui/json-viewer/index.js +0 -2
  217. package/dist/ui/ink/components/ui/json-viewer/index.js.map +0 -1
  218. package/dist/ui/ink/components/ui/key-hint/KeyHint.d.ts +0 -14
  219. package/dist/ui/ink/components/ui/key-hint/KeyHint.d.ts.map +0 -1
  220. package/dist/ui/ink/components/ui/key-hint/KeyHint.js +0 -5
  221. package/dist/ui/ink/components/ui/key-hint/KeyHint.js.map +0 -1
  222. package/dist/ui/ink/components/ui/key-hint/index.d.ts +0 -3
  223. package/dist/ui/ink/components/ui/key-hint/index.d.ts.map +0 -1
  224. package/dist/ui/ink/components/ui/key-hint/index.js +0 -2
  225. package/dist/ui/ink/components/ui/key-hint/index.js.map +0 -1
  226. package/dist/ui/ink/components/ui/markdown/Markdown.d.ts +0 -11
  227. package/dist/ui/ink/components/ui/markdown/Markdown.d.ts.map +0 -1
  228. package/dist/ui/ink/components/ui/markdown/Markdown.js +0 -129
  229. package/dist/ui/ink/components/ui/markdown/Markdown.js.map +0 -1
  230. package/dist/ui/ink/components/ui/markdown/index.d.ts +0 -3
  231. package/dist/ui/ink/components/ui/markdown/index.d.ts.map +0 -1
  232. package/dist/ui/ink/components/ui/markdown/index.js +0 -2
  233. package/dist/ui/ink/components/ui/markdown/index.js.map +0 -1
  234. package/dist/ui/ink/components/ui/scroll-area/ScrollArea.d.ts +0 -21
  235. package/dist/ui/ink/components/ui/scroll-area/ScrollArea.d.ts.map +0 -1
  236. package/dist/ui/ink/components/ui/scroll-area/ScrollArea.js +0 -42
  237. package/dist/ui/ink/components/ui/scroll-area/ScrollArea.js.map +0 -1
  238. package/dist/ui/ink/components/ui/scroll-area/index.d.ts +0 -3
  239. package/dist/ui/ink/components/ui/scroll-area/index.d.ts.map +0 -1
  240. package/dist/ui/ink/components/ui/scroll-area/index.js +0 -2
  241. package/dist/ui/ink/components/ui/scroll-area/index.js.map +0 -1
  242. package/dist/ui/ink/components/ui/select/Select.d.ts +0 -19
  243. package/dist/ui/ink/components/ui/select/Select.d.ts.map +0 -1
  244. package/dist/ui/ink/components/ui/select/Select.js +0 -73
  245. package/dist/ui/ink/components/ui/select/Select.js.map +0 -1
  246. package/dist/ui/ink/components/ui/select/index.d.ts +0 -3
  247. package/dist/ui/ink/components/ui/select/index.d.ts.map +0 -1
  248. package/dist/ui/ink/components/ui/select/index.js +0 -2
  249. package/dist/ui/ink/components/ui/select/index.js.map +0 -1
  250. package/dist/ui/ink/components/ui/spinner/Spinner.d.ts +0 -14
  251. package/dist/ui/ink/components/ui/spinner/Spinner.d.ts.map +0 -1
  252. package/dist/ui/ink/components/ui/spinner/Spinner.js +0 -16
  253. package/dist/ui/ink/components/ui/spinner/Spinner.js.map +0 -1
  254. package/dist/ui/ink/components/ui/spinner/index.d.ts +0 -3
  255. package/dist/ui/ink/components/ui/spinner/index.d.ts.map +0 -1
  256. package/dist/ui/ink/components/ui/spinner/index.js +0 -2
  257. package/dist/ui/ink/components/ui/spinner/index.js.map +0 -1
  258. package/dist/ui/ink/components/ui/status-indicator/StatusIndicator.d.ts +0 -12
  259. package/dist/ui/ink/components/ui/status-indicator/StatusIndicator.d.ts.map +0 -1
  260. package/dist/ui/ink/components/ui/status-indicator/StatusIndicator.js +0 -39
  261. package/dist/ui/ink/components/ui/status-indicator/StatusIndicator.js.map +0 -1
  262. package/dist/ui/ink/components/ui/status-indicator/index.d.ts +0 -3
  263. package/dist/ui/ink/components/ui/status-indicator/index.d.ts.map +0 -1
  264. package/dist/ui/ink/components/ui/status-indicator/index.js +0 -2
  265. package/dist/ui/ink/components/ui/status-indicator/index.js.map +0 -1
  266. package/dist/ui/ink/components/ui/streaming-text/StreamingText.d.ts +0 -13
  267. package/dist/ui/ink/components/ui/streaming-text/StreamingText.d.ts.map +0 -1
  268. package/dist/ui/ink/components/ui/streaming-text/StreamingText.js +0 -22
  269. package/dist/ui/ink/components/ui/streaming-text/StreamingText.js.map +0 -1
  270. package/dist/ui/ink/components/ui/streaming-text/index.d.ts +0 -3
  271. package/dist/ui/ink/components/ui/streaming-text/index.d.ts.map +0 -1
  272. package/dist/ui/ink/components/ui/streaming-text/index.js +0 -2
  273. package/dist/ui/ink/components/ui/streaming-text/index.js.map +0 -1
  274. package/dist/ui/ink/components/ui/tabs/Tabs.d.ts +0 -20
  275. package/dist/ui/ink/components/ui/tabs/Tabs.d.ts.map +0 -1
  276. package/dist/ui/ink/components/ui/tabs/Tabs.js +0 -53
  277. package/dist/ui/ink/components/ui/tabs/Tabs.js.map +0 -1
  278. package/dist/ui/ink/components/ui/tabs/index.d.ts +0 -3
  279. package/dist/ui/ink/components/ui/tabs/index.d.ts.map +0 -1
  280. package/dist/ui/ink/components/ui/tabs/index.js +0 -2
  281. package/dist/ui/ink/components/ui/tabs/index.js.map +0 -1
  282. package/dist/ui/ink/components/ui/text-input/TextInput.d.ts +0 -22
  283. package/dist/ui/ink/components/ui/text-input/TextInput.d.ts.map +0 -1
  284. package/dist/ui/ink/components/ui/text-input/TextInput.js +0 -57
  285. package/dist/ui/ink/components/ui/text-input/TextInput.js.map +0 -1
  286. package/dist/ui/ink/components/ui/text-input/index.d.ts +0 -3
  287. package/dist/ui/ink/components/ui/text-input/index.d.ts.map +0 -1
  288. package/dist/ui/ink/components/ui/text-input/index.js +0 -2
  289. package/dist/ui/ink/components/ui/text-input/index.js.map +0 -1
  290. package/dist/ui/ink/components/ui/token-counter/TokenCounter.d.ts +0 -17
  291. package/dist/ui/ink/components/ui/token-counter/TokenCounter.d.ts.map +0 -1
  292. package/dist/ui/ink/components/ui/token-counter/TokenCounter.js +0 -38
  293. package/dist/ui/ink/components/ui/token-counter/TokenCounter.js.map +0 -1
  294. package/dist/ui/ink/components/ui/token-counter/index.d.ts +0 -3
  295. package/dist/ui/ink/components/ui/token-counter/index.d.ts.map +0 -1
  296. package/dist/ui/ink/components/ui/token-counter/index.js +0 -2
  297. package/dist/ui/ink/components/ui/token-counter/index.js.map +0 -1
  298. package/dist/ui/ink/components/ui/typewriter/Typewriter.d.ts +0 -14
  299. package/dist/ui/ink/components/ui/typewriter/Typewriter.d.ts.map +0 -1
  300. package/dist/ui/ink/components/ui/typewriter/Typewriter.js +0 -57
  301. package/dist/ui/ink/components/ui/typewriter/Typewriter.js.map +0 -1
  302. package/dist/ui/ink/components/ui/typewriter/index.d.ts +0 -3
  303. package/dist/ui/ink/components/ui/typewriter/index.d.ts.map +0 -1
  304. package/dist/ui/ink/components/ui/typewriter/index.js +0 -2
  305. package/dist/ui/ink/components/ui/typewriter/index.js.map +0 -1
  306. package/dist/ui/opentui-renderer.d.ts +0 -53
  307. package/dist/ui/opentui-renderer.d.ts.map +0 -1
  308. package/dist/ui/opentui-renderer.js +0 -463
  309. package/dist/ui/opentui-renderer.js.map +0 -1
  310. package/dist/utils/memoize.d.ts +0 -14
  311. package/dist/utils/memoize.d.ts.map +0 -1
  312. package/dist/utils/memoize.js +0 -51
  313. package/dist/utils/memoize.js.map +0 -1
  314. package/dist/utils/types.d.ts +0 -6
  315. package/dist/utils/types.d.ts.map +0 -1
  316. package/dist/utils/types.js +0 -2
  317. package/dist/utils/types.js.map +0 -1
  318. package/dist/utils.d.ts +0 -22
  319. package/dist/utils.d.ts.map +0 -1
  320. package/dist/utils.js +0 -81
  321. package/dist/utils.js.map +0 -1
@@ -1,55 +1,55 @@
1
- import { readFileSync, existsSync } from 'node:fs';
2
- import { join, isAbsolute, relative } from 'node:path';
3
- import { parseToolCalls } from '../tools/parse-tool-call.js';
4
- import { parseFileRefs } from '../file-loader.js';
5
- import { ContextManager } from './context.js';
6
- import { autoLearnFromError } from './memory.js';
7
- import { collectCodingStyle } from './user-profile.js';
8
- import { ModeRegistry } from './modes/registry.js';
9
- import { applyMode } from './modes/applier.js';
10
- import { loadSkill, loadSkillsFromRefs, enforceSkillBudget, preloadSkillsByKeywords, isSkillRequest, listInstalledSkills } from '../skills/loader.js';
11
- import { runPreToolUseHooks, runPostToolUseHooks } from '../tools/hooks.js';
12
- import { isMCPCommand, parseMCPCommand, callMCPTool } from '../tools/mcp.js';
13
- import { showPlan, getPlanPendingCount, clearPlan, getNextPendingStep, updatePlanStep, extractDirFromStepTitle } from '../tools/plan.js';
14
- import { generateToolCallId } from '../tools/id-gen.js';
15
- import { checkFileSyntax } from '../tools/syntax-check.js';
16
- import { loadConfig, getLLMConfig, getGroundingConfig } from '../config/config.js';
17
- import { buildSystemPrompt } from './system-prompt.js';
18
- import { logInfo, logWarn, logError, logDebug, logToolCall, setStatusWriter, writeStatus } from '../logger.js';
19
- import { buildSubagentContext, runIsolatedLoop, doChat } from '../tools/subagent.js';
20
- import { SubagentRenderer } from '../ui/subagent-renderer.js';
21
- import { ToolExecutor } from './tool-executor.js';
22
- import { ErrorEscalator } from './error-escalation.js';
23
- import { WriteEnforcer } from './write-enforcer.js';
24
- import { PluginManager } from '../plugins/manager.js';
25
- import { loadAllPluginsWithInfo } from '../plugins/loader.js';
26
- import { handleToolAfter as lintHandleToolAfter } from '../plugins/builtin/lint-on-write/index.js';
27
- import { handleAgentDone, handleToolBlocked, handleToolAfter as notifyHandleToolAfter } from '../plugins/builtin/notify/index.js';
28
- import { planHandler } from '../tools/native/plan.js';
29
- import { todoHandler } from '../tools/native/todo.js';
30
- import { questionHandler } from '../tools/native/question.js';
31
- import { approveHandler } from '../tools/native/approve.js';
32
- import { userProfileHandler } from '../tools/native/user-profile.js';
33
- import { themeHandler } from '../tools/native/theme.js';
34
- import { scaffoldHandler } from '../tools/native/scaffold.js';
35
- import { projectMapHandler } from '../tools/native/project-map.js';
36
- import { bashHandler } from '../tools/bash.js';
37
- import { readFileHandler } from '../tools/fs/read-file.js';
38
- import { writeFileHandler } from '../tools/fs/write-file.js';
39
- import { editFileHandler } from '../tools/fs/edit-file.js';
40
- import { listDirHandler } from '../tools/fs/list-dir.js';
41
- import { createDirHandler } from '../tools/fs/create-dir.js';
42
- import { deleteFileHandler } from '../tools/fs/delete-file.js';
43
- import { copyFileHandler } from '../tools/fs/copy-file.js';
44
- import { moveFileHandler } from '../tools/fs/move-file.js';
45
- import { fileInfoHandler } from '../tools/fs/file-info.js';
46
- import { globSearchHandler } from '../tools/fs/glob-search.js';
47
- import { grepSearchHandler } from '../tools/fs/grep-search.js';
48
- import { treeHandler } from '../tools/fs/tree.js';
49
- import { webSearchHandler } from '../tools/web-search.js';
50
- import { webFetchHandler } from '../tools/web-fetch.js';
51
- import { webBrowseHandler } from '../tools/web-browse.js';
52
- import { truncateOutput } from './tool-executor.js';
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join, isAbsolute, relative } from "node:path";
3
+ import { parseToolCalls } from "../tools/parse-tool-call.js";
4
+ import { parseFileRefs } from "../file-loader.js";
5
+ import { ContextManager } from "./context.js";
6
+ import { autoLearnFromError } from "./memory.js";
7
+ import { collectCodingStyle } from "./user-profile.js";
8
+ import { ModeRegistry } from "./modes/registry.js";
9
+ import { applyMode } from "./modes/applier.js";
10
+ import { loadSkill, loadSkillsFromRefs, enforceSkillBudget, preloadSkillsByKeywords, isSkillRequest, listInstalledSkills, } from "../skills/loader.js";
11
+ import { runPreToolUseHooks, runPostToolUseHooks } from "../tools/hooks.js";
12
+ import { isMCPCommand, parseMCPCommand, callMCPTool } from "../tools/mcp.js";
13
+ import { showPlan, getPlanPendingCount, clearPlan, getNextPendingStep, updatePlanStep, extractDirFromStepTitle, } from "../tools/plan.js";
14
+ import { generateToolCallId } from "../tools/id-gen.js";
15
+ import { checkFileSyntax } from "../tools/syntax-check.js";
16
+ import { loadConfig, getLLMConfig, getGroundingConfig, } from "../config/config.js";
17
+ import { buildSystemPrompt } from "./system-prompt.js";
18
+ import { logInfo, logWarn, logError, logDebug, logToolCall, setStatusWriter, writeStatus, } from "../logger.js";
19
+ import { buildSubagentContext, runIsolatedLoop, doChat, } from "../tools/subagent.js";
20
+ import { SubagentRenderer } from "../ui/subagent-renderer.js";
21
+ import { ToolExecutor } from "./tool-executor.js";
22
+ import { ErrorEscalator } from "./error-escalation.js";
23
+ import { WriteEnforcer } from "./write-enforcer.js";
24
+ import { PluginManager } from "../plugins/manager.js";
25
+ import { loadAllPluginsWithInfo } from "../plugins/loader.js";
26
+ import { handleToolAfter as lintHandleToolAfter } from "../plugins/builtin/lint-on-write/index.js";
27
+ import { handleAgentDone, handleToolBlocked, handleToolAfter as notifyHandleToolAfter, } from "../plugins/builtin/notify/index.js";
28
+ import { planHandler } from "../tools/native/plan.js";
29
+ import { todoHandler } from "../tools/native/todo.js";
30
+ import { questionHandler } from "../tools/native/question.js";
31
+ import { approveHandler } from "../tools/native/approve.js";
32
+ import { userProfileHandler } from "../tools/native/user-profile.js";
33
+ import { themeHandler } from "../tools/native/theme.js";
34
+ import { scaffoldHandler } from "../tools/native/scaffold.js";
35
+ import { projectMapHandler } from "../tools/native/project-map.js";
36
+ import { bashHandler } from "../tools/bash.js";
37
+ import { readFileHandler } from "../tools/fs/read-file.js";
38
+ import { writeFileHandler } from "../tools/fs/write-file.js";
39
+ import { editFileHandler } from "../tools/fs/edit-file.js";
40
+ import { listDirHandler } from "../tools/fs/list-dir.js";
41
+ import { createDirHandler } from "../tools/fs/create-dir.js";
42
+ import { deleteFileHandler } from "../tools/fs/delete-file.js";
43
+ import { copyFileHandler } from "../tools/fs/copy-file.js";
44
+ import { moveFileHandler } from "../tools/fs/move-file.js";
45
+ import { fileInfoHandler } from "../tools/fs/file-info.js";
46
+ import { globSearchHandler } from "../tools/fs/glob-search.js";
47
+ import { grepSearchHandler } from "../tools/fs/grep-search.js";
48
+ import { treeHandler } from "../tools/fs/tree.js";
49
+ import { webSearchHandler } from "../tools/web-search.js";
50
+ import { webFetchHandler } from "../tools/web-fetch.js";
51
+ import { webBrowseHandler } from "../tools/web-browse.js";
52
+ import { truncateOutput } from "./tool-executor.js";
53
53
  /**
54
54
  * Основной цикл ReAct-агента.
55
55
  *
@@ -85,8 +85,12 @@ function extractToolCallFallback(response) {
85
85
  const m = response.match(re);
86
86
  if (m) {
87
87
  let cmd = m[1];
88
- cmd = cmd.replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
89
- return { name: 'bash', cmd };
88
+ cmd = cmd
89
+ .replace(/\\n/g, "\n")
90
+ .replace(/\\t/g, "\t")
91
+ .replace(/\\"/g, '"')
92
+ .replace(/\\\\/g, "\\");
93
+ return { name: "bash", cmd };
90
94
  }
91
95
  }
92
96
  return null;
@@ -114,30 +118,39 @@ function extractToolCallFallback(response) {
114
118
  * @param options - дополнительные опции (quiet, showThinking, mode)
115
119
  */
116
120
  export async function runAgentLoop(provider, registry, prompt, contextManager, previousMessages, renderer, providerName, options) {
117
- logInfo('LOOP', `runAgentLoop started: "${prompt.slice(0, 50)}"`);
121
+ logInfo("LOOP", `runAgentLoop started: "${prompt.slice(0, 50)}"`);
118
122
  renderer?.resetAbort?.();
119
123
  // Парсинг ссылок на файлы в промпте (формат @path/to/file)
120
124
  const { prompt: enrichedPrompt, files } = await parseFileRefs(prompt);
121
- logDebug('LOOP', `parseFileRefs: ${files.length} files, prompt len: ${enrichedPrompt.length}`);
125
+ logDebug("LOOP", `parseFileRefs: ${files.length} files, prompt len: ${enrichedPrompt.length}`);
122
126
  const quiet = options?.quiet ?? false;
123
127
  // ─── ModeRegistry: конфигурация режима ──
124
128
  const modeRegistry = new ModeRegistry();
125
- const modeConfig = modeRegistry.getOrFallback(options?.mode || 'base');
126
- const applied = applyMode(modeConfig, { quiet, showThinking: options?.showThinking });
129
+ const modeConfig = modeRegistry.getOrFallback(options?.mode || "base");
130
+ const applied = applyMode(modeConfig, {
131
+ quiet,
132
+ showThinking: options?.showThinking,
133
+ });
127
134
  // ─── Behaviors из ModeConfig ──────────────────────────
128
135
  const behaviors = modeConfig.behaviors;
129
136
  const planEnforcement = behaviors.planEnforcement !== false;
130
- const directWriteStreakMax = typeof behaviors.directWriteStreakLimit === 'number'
131
- ? behaviors.directWriteStreakLimit : (behaviors.directWriteStreak !== false ? 3 : Infinity);
132
- const continuationNudgeMax = typeof behaviors.continuationNudgeLimit === 'number'
133
- ? behaviors.continuationNudgeLimit : (behaviors.continuationNudge !== false ? 2 : 0);
137
+ const directWriteStreakMax = typeof behaviors.directWriteStreakLimit === "number"
138
+ ? behaviors.directWriteStreakLimit
139
+ : behaviors.directWriteStreak !== false
140
+ ? 3
141
+ : Infinity;
142
+ const continuationNudgeMax = typeof behaviors.continuationNudgeLimit === "number"
143
+ ? behaviors.continuationNudgeLimit
144
+ : behaviors.continuationNudge !== false
145
+ ? 2
146
+ : 0;
134
147
  const readConsolidation = behaviors.readConsolidation !== false;
135
148
  // Автозагрузка скилов из промпта (если пользователь указал @skill-name)
136
149
  const loadedSkills = loadSkillsFromRefs(prompt, process.cwd());
137
- const loadedSkillNames = new Set(loadedSkills.map(s => s.name));
150
+ const loadedSkillNames = new Set(loadedSkills.map((s) => s.name));
138
151
  if (loadedSkills.length > 0) {
139
- logInfo('LOOP', `loaded ${loadedSkills.length} skills: ${loadedSkills.map(s => s.name).join(', ')}`);
140
- renderer?.writeInfo(` Loaded ${loadedSkills.length} skill(s): ${loadedSkills.map(s => s.name).join(', ')}`);
152
+ logInfo("LOOP", `loaded ${loadedSkills.length} skills: ${loadedSkills.map((s) => s.name).join(", ")}`);
153
+ renderer?.writeInfo(` Loaded ${loadedSkills.length} skill(s): ${loadedSkills.map((s) => s.name).join(", ")}`);
141
154
  }
142
155
  // Принудительная загрузка скилов из ModeConfig
143
156
  for (const skillName of applied.forcedSkills) {
@@ -146,11 +159,11 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
146
159
  if (skill) {
147
160
  loadedSkills.push(skill);
148
161
  loadedSkillNames.add(skillName);
149
- logInfo('LOOP', `auto-loaded ${skillName} skill for ${modeConfig.id} mode`);
162
+ logInfo("LOOP", `auto-loaded ${skillName} skill for ${modeConfig.id} mode`);
150
163
  renderer?.writeInfo(` Auto-loaded @${skillName} skill for ${modeConfig.id} mode`);
151
164
  }
152
165
  else {
153
- logWarn('LOOP', `${skillName} skill not found for ${modeConfig.id} mode`);
166
+ logWarn("LOOP", `${skillName} skill not found for ${modeConfig.id} mode`);
154
167
  renderer?.writeWarning(` Warning: @${skillName} skill not found (required for ${modeConfig.id} mode)`);
155
168
  }
156
169
  }
@@ -162,8 +175,8 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
162
175
  const budget = applied.skillTokenBudget;
163
176
  const { allowed, dropped } = enforceSkillBudget(loadedSkills, budget);
164
177
  if (dropped.length > 0) {
165
- renderer?.writeWarning(` Skill limit (${maxSkills} max in ${modeConfig.id} mode): dropped ${dropped.join(', ')}`);
166
- logInfo('LOOP', `skill limit: dropped ${dropped.join(', ')} (budget: ${budget} tokens)`);
178
+ renderer?.writeWarning(` Skill limit (${maxSkills} max in ${modeConfig.id} mode): dropped ${dropped.join(", ")}`);
179
+ logInfo("LOOP", `skill limit: dropped ${dropped.join(", ")} (budget: ${budget} tokens)`);
167
180
  loadedSkills.length = 0;
168
181
  loadedSkills.push(...allowed);
169
182
  for (const s of dropped)
@@ -171,33 +184,41 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
171
184
  }
172
185
  }
173
186
  // Pre-load скиллов по ключевым словам (без language-specific хардкода)
174
- if (loadedSkills.length === 0 && options?.mode !== 'fast') {
187
+ if (loadedSkills.length === 0 && options?.mode !== "fast") {
175
188
  const keywordSkills = preloadSkillsByKeywords(prompt, process.cwd());
176
189
  for (const skill of keywordSkills) {
177
190
  if (!loadedSkillNames.has(skill.name)) {
178
191
  loadedSkills.push(skill);
179
192
  loadedSkillNames.add(skill.name);
180
- logInfo('LOOP', `keyword preloaded skill: ${skill.name}`);
193
+ logInfo("LOOP", `keyword preloaded skill: ${skill.name}`);
181
194
  renderer?.writeInfo(` Auto-loaded @${skill.name} (keyword match)`);
182
195
  }
183
196
  }
184
197
  // Если пользователь явно просит "используй скилл", но ничего не нашлось — подсказываем
185
198
  if (keywordSkills.length === 0 && isSkillRequest(prompt)) {
186
199
  const allSkills = listInstalledSkills(process.cwd());
187
- renderer?.writeWarning(` You asked for a skill but none matched. Use @skill-name in your prompt to load one. Installed: ${allSkills.slice(0, 10).map(s => '@' + s).join(', ')}... (${allSkills.length} total)`);
200
+ renderer?.writeWarning(` You asked for a skill but none matched. Use @skill-name in your prompt to load one. Installed: ${allSkills
201
+ .slice(0, 10)
202
+ .map((s) => "@" + s)
203
+ .join(", ")}... (${allSkills.length} total)`);
188
204
  }
189
205
  }
190
206
  if (files.length > 0) {
191
207
  const totalTokens = files.reduce((sum, f) => sum + f.tokens, 0);
192
- const totalLines = files.reduce((sum, f) => sum + f.content.split('\n').length, 0);
193
- logInfo('LOOP', `loaded ${files.length} files (${totalLines} lines, ~${totalTokens} tokens)`);
208
+ const totalLines = files.reduce((sum, f) => sum + f.content.split("\n").length, 0);
209
+ logInfo("LOOP", `loaded ${files.length} files (${totalLines} lines, ~${totalTokens} tokens)`);
194
210
  renderer?.writeInfo(` Loaded ${files.length} files (${totalLines} lines, ~${totalTokens} tokens)`);
195
211
  }
196
212
  setStatusWriter(renderer ? (msg) => renderer.writeDim(` ${msg}`) : null);
197
- writeStatus('Initializing...');
213
+ writeStatus("Initializing...");
198
214
  // ─── PluginManager: загрузка и активация плагинов ──
199
215
  const pluginManager = new PluginManager();
200
- const pluginLogger = { info: logInfo.bind(null, 'PLUGIN'), warn: logWarn.bind(null, 'PLUGIN'), error: logError.bind(null, 'PLUGIN'), debug: logDebug.bind(null, 'PLUGIN') };
216
+ const pluginLogger = {
217
+ info: logInfo.bind(null, "PLUGIN"),
218
+ warn: logWarn.bind(null, "PLUGIN"),
219
+ error: logError.bind(null, "PLUGIN"),
220
+ debug: logDebug.bind(null, "PLUGIN"),
221
+ };
201
222
  const pluginCtx = { cwd: process.cwd(), config: {}, logger: pluginLogger };
202
223
  // Загружаем все плагины (builtin + project + user)
203
224
  const pluginInfos = await loadAllPluginsWithInfo(pluginManager);
@@ -206,7 +227,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
206
227
  await pluginManager.activateAll(modeConfig.plugins, pluginCtx);
207
228
  }
208
229
  // Авто-активация builtin плагинов (все моды)
209
- const builtinPlugins = pluginInfos.filter(p => p.source === 'builtin');
230
+ const builtinPlugins = pluginInfos.filter((p) => p.source === "builtin");
210
231
  for (const bp of builtinPlugins) {
211
232
  if (!pluginManager.isActive(bp.name)) {
212
233
  await pluginManager.activate(bp.name, pluginCtx);
@@ -220,15 +241,15 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
220
241
  }
221
242
  }
222
243
  // Регистрация обработчиков событий для всех активных плагинов
223
- if (pluginManager.isActive('lint-on-write')) {
224
- pluginManager.on('tool:after', 'lint-on-write', (toolName, args, result) => {
225
- return lintHandleToolAfter(toolName, args, result, process.cwd(), '.lint-detected');
244
+ if (pluginManager.isActive("lint-on-write")) {
245
+ pluginManager.on("tool:after", "lint-on-write", (toolName, args, result) => {
246
+ return lintHandleToolAfter(toolName, args, result, process.cwd(), ".lint-detected");
226
247
  });
227
248
  }
228
- if (pluginManager.isActive('notify')) {
229
- pluginManager.on('agent:done', 'notify', () => handleAgentDone());
230
- pluginManager.on('tool:blocked', 'notify', (toolName, args) => handleToolBlocked(toolName, args));
231
- pluginManager.on('tool:after', 'notify', (toolName, args, result) => notifyHandleToolAfter(toolName, args, result));
249
+ if (pluginManager.isActive("notify")) {
250
+ pluginManager.on("agent:done", "notify", () => handleAgentDone());
251
+ pluginManager.on("tool:blocked", "notify", (toolName, args) => handleToolBlocked(toolName, args));
252
+ pluginManager.on("tool:after", "notify", (toolName, args, result) => notifyHandleToolAfter(toolName, args, result));
232
253
  }
233
254
  const pluginSections = await pluginManager.getPromptSections();
234
255
  // Сборка system prompt (включает инструкции, информацию о проекте, скилы, память, плагины)
@@ -239,17 +260,19 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
239
260
  setStatusWriter(null);
240
261
  // Начальное состояние истории: system prompt + предыдущие сообщения + текущий промпт
241
262
  const messages = [
242
- { role: 'system', content: systemPrompt },
263
+ { role: "system", content: systemPrompt },
243
264
  ...(previousMessages || []),
244
- { role: 'user', content: enrichedPrompt },
265
+ { role: "user", content: enrichedPrompt },
245
266
  ];
246
267
  const ctx = contextManager ?? new ContextManager(provider.contextWindow);
247
268
  const maxSteps = config.agent.maxSteps ?? 100;
248
269
  let consecutiveEmpty = 0; // счётчик пустых ответов подряд
249
- let pendingSyntaxError = '';
270
+ let pendingSyntaxError = "";
250
271
  let aborted = false;
251
272
  let continuationNudges = 0; // счётчик подсказок "продолжай работать"
252
273
  let consecutiveMissingGrounding = 0; // счётчик пропущенных grounding-маркеров подряд
274
+ let groundingRetryCount = 0; // сколько раз был CRITICAL retry подряд
275
+ let consecutiveInvalidCalls = 0; // счётчик невалидных tool call подряд
253
276
  /** Счётчик бесполезных действий (list_dir, glob_search, todo list без результата) */
254
277
  let uselessActionCount = 0;
255
278
  /** История последних чтений файлов для.detect повторных чтений */
@@ -258,16 +281,36 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
258
281
  const recentGrepPatterns = [];
259
282
  /** История последних вопросов для detect повторных вопросов */
260
283
  const recentQuestions = [];
284
+ /** История последних bash-команд для detect циклических вызовов */
285
+ const recentBashCommands = [];
261
286
  // ─── ToolExecutor: единый диспетчер инструментов ──
262
287
  const toolExecutor = new ToolExecutor();
263
288
  toolExecutor.setPluginManager(pluginManager);
264
289
  toolExecutor.registerAll([
265
- planHandler, todoHandler, questionHandler, approveHandler,
266
- userProfileHandler, themeHandler, scaffoldHandler, projectMapHandler, bashHandler,
267
- readFileHandler, writeFileHandler, editFileHandler, listDirHandler,
268
- createDirHandler, deleteFileHandler, copyFileHandler, moveFileHandler,
269
- fileInfoHandler, globSearchHandler, grepSearchHandler, treeHandler,
270
- webSearchHandler, webFetchHandler, webBrowseHandler,
290
+ planHandler,
291
+ todoHandler,
292
+ questionHandler,
293
+ approveHandler,
294
+ userProfileHandler,
295
+ themeHandler,
296
+ scaffoldHandler,
297
+ projectMapHandler,
298
+ bashHandler,
299
+ readFileHandler,
300
+ writeFileHandler,
301
+ editFileHandler,
302
+ listDirHandler,
303
+ createDirHandler,
304
+ deleteFileHandler,
305
+ copyFileHandler,
306
+ moveFileHandler,
307
+ fileInfoHandler,
308
+ globSearchHandler,
309
+ grepSearchHandler,
310
+ treeHandler,
311
+ webSearchHandler,
312
+ webFetchHandler,
313
+ webBrowseHandler,
271
314
  ]);
272
315
  const errorEscalator = new ErrorEscalator();
273
316
  const writeEnforcer = new WriteEnforcer();
@@ -290,7 +333,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
290
333
  }
291
334
  let skillsAutoLoaded = false;
292
335
  const createdFiles = []; // отслеживание созданных/изменённых файлов
293
- logDebug('LOOP', `step ${step + 1}/${maxSteps} starting, ${messages.length} messages`);
336
+ logDebug("LOOP", `step ${step + 1}/${maxSteps} starting, ${messages.length} messages`);
294
337
  // Инкрементальный подсчёт — считаем только новые сообщения (быстрее в ~100x на больших историях)
295
338
  if (step === 0) {
296
339
  await ctx.updateTokens(messages);
@@ -298,8 +341,8 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
298
341
  else {
299
342
  await ctx.updateTokensIncremental(messages);
300
343
  }
301
- const skillNames = loadedSkills.map(s => s.name).join(', ');
302
- const usageText = ctx.formatUsage() + (skillNames ? ` │ skills: ${skillNames}` : '');
344
+ const skillNames = loadedSkills.map((s) => s.name).join(", ");
345
+ const usageText = ctx.formatUsage() + (skillNames ? ` │ skills: ${skillNames}` : "");
303
346
  renderer?.updateContextUsage(usageText);
304
347
  renderer?.updateStatus({
305
348
  provider: providerName,
@@ -315,7 +358,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
315
358
  const before = messages.length;
316
359
  messages.splice(0, messages.length, ...ctx.compact(messages));
317
360
  if (!quiet) {
318
- renderer?.writeBracketTag('compact', `${before} → ${messages.length} messages`);
361
+ renderer?.writeBracketTag("compact", `${before} → ${messages.length} messages`);
319
362
  }
320
363
  await ctx.updateTokens(messages);
321
364
  }
@@ -324,13 +367,13 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
324
367
  const before = messages.length;
325
368
  messages.splice(0, messages.length, ...ctx.emergencyCompact(messages));
326
369
  if (!quiet) {
327
- renderer?.writeBracketTag('compact', `EMERGENCY: ${before} → ${messages.length}`);
370
+ renderer?.writeBracketTag("compact", `EMERGENCY: ${before} → ${messages.length}`);
328
371
  }
329
372
  await ctx.updateTokens(messages);
330
373
  }
331
374
  // ─── Запрос к LLM ────────────────────────────────────
332
375
  const llmCfg = getLLMConfig(config);
333
- logInfo('LOOP', `calling doChat, messages: ${messages.length}, options.showThinking: ${options?.showThinking}, retries: ${llmCfg.retries}`);
376
+ logInfo("LOOP", `calling doChat, messages: ${messages.length}, options.showThinking: ${options?.showThinking}, retries: ${llmCfg.retries}`);
334
377
  const { response } = await doChat(provider, messages, renderer, renderer?.abortSignal, llmCfg.retries, options?.showThinking);
335
378
  // Проверка прерывания во время генерации
336
379
  if (renderer?.abortSignal?.aborted) {
@@ -339,12 +382,21 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
339
382
  }
340
383
  // ─── Автозагрузка скилов по @упоминаниям ─────────────
341
384
  // Если ответ содержит @skill-name, загружаем скил и пересобираем system prompt
342
- if (options?.mode !== 'fast' && !skillsAutoLoaded) {
385
+ if (options?.mode !== "fast" && !skillsAutoLoaded) {
343
386
  const suggestedSkillRefs = response.matchAll(/@(\w[\w-]*)/g);
344
387
  const newSkillNames = [];
345
388
  for (const match of suggestedSkillRefs) {
346
389
  const name = match[1];
347
- if (['email', 'api', 'id', 'json', 'url', 'http', 'https', 'localhost'].includes(name))
390
+ if ([
391
+ "email",
392
+ "api",
393
+ "id",
394
+ "json",
395
+ "url",
396
+ "http",
397
+ "https",
398
+ "localhost",
399
+ ].includes(name))
348
400
  continue;
349
401
  if (!loadedSkillNames.has(name)) {
350
402
  const skill = loadSkill(name, process.cwd());
@@ -362,16 +414,19 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
362
414
  if (autoBudget > 0 && loadedSkills.length > 0) {
363
415
  const { allowed, dropped } = enforceSkillBudget(loadedSkills, autoBudget);
364
416
  if (dropped.length > 0) {
365
- renderer?.writeWarning(` Auto-load budget: dropped ${dropped.join(', ')} (budget: ${autoBudget} tokens)`);
366
- logInfo('LOOP', `auto-load budget: dropped ${dropped.join(', ')} (budget: ${autoBudget} tokens)`);
417
+ renderer?.writeWarning(` Auto-load budget: dropped ${dropped.join(", ")} (budget: ${autoBudget} tokens)`);
418
+ logInfo("LOOP", `auto-load budget: dropped ${dropped.join(", ")} (budget: ${autoBudget} tokens)`);
367
419
  loadedSkills.length = 0;
368
420
  loadedSkills.push(...allowed);
369
421
  for (const s of dropped)
370
422
  loadedSkillNames.delete(s);
371
423
  }
372
424
  }
373
- renderer?.writeInfo(` Auto-loaded ${loadedSkills.filter(s => newSkillNames.includes(s.name)).length} skill(s): ${newSkillNames.join(', ')}`);
374
- messages[0] = { role: 'system', content: buildSystemPrompt(loadedSkills, options?.mode, pluginSections, options?.flags) };
425
+ renderer?.writeInfo(` Auto-loaded ${loadedSkills.filter((s) => newSkillNames.includes(s.name)).length} skill(s): ${newSkillNames.join(", ")}`);
426
+ messages[0] = {
427
+ role: "system",
428
+ content: buildSystemPrompt(loadedSkills, options?.mode, pluginSections, options?.flags),
429
+ };
375
430
  // Не удаляем ответ ассистента — skills загружаются для следующих шагов цикла.
376
431
  // Модель увидит обновлённый system prompt на следующей итерации и сможет
377
432
  // использовать загруженные скилы без retry (экономит ~один LLM-запрос).
@@ -380,105 +435,124 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
380
435
  // ─── Обработка пустых ответов ────────────────────────
381
436
  // Маленькие модели иногда возвращают пустой ответ — повторяем с подсказкой
382
437
  if (!response.trim()) {
383
- logWarn('LOOP', `empty response #${consecutiveEmpty + 1}`);
438
+ logWarn("LOOP", `empty response #${consecutiveEmpty + 1}`);
384
439
  consecutiveEmpty++;
385
440
  // После компактинга — более информативное сообщение о пустом ответе
386
441
  if (ctx.compactCount > 0 && consecutiveEmpty <= 3) {
387
442
  let lastUserMsg;
388
443
  for (let i = messages.length - 1; i >= 0; i--) {
389
- if (messages[i].role === 'user') {
444
+ if (messages[i].role === "user") {
390
445
  lastUserMsg = messages[i];
391
446
  break;
392
447
  }
393
448
  }
394
449
  const contextHint = lastUserMsg
395
- ? `[System: You returned an empty response. The conversation was compressed. Continue working on: "${(lastUserMsg.content || '').slice(0, 150)}..."]`
396
- : '[System: You returned an empty response. Continue with the task.]';
397
- messages.push({ role: 'user', content: contextHint });
450
+ ? `[System: You returned an empty response. The conversation was compressed. Continue working on: "${(lastUserMsg.content || "").slice(0, 150)}..."]`
451
+ : "[System: You returned an empty response. Continue with the task.]";
452
+ messages.push({ role: "user", content: contextHint });
398
453
  if (!quiet) {
399
454
  renderer?.writeDim(` [empty response, retrying with context hint]`);
400
455
  }
401
456
  continue;
402
457
  }
403
458
  const lastMsg = messages[messages.length - 1];
404
- if ((lastMsg?.role === 'user' || lastMsg?.role === 'system') && consecutiveEmpty <= 2) {
459
+ if ((lastMsg?.role === "user" || lastMsg?.role === "system") &&
460
+ consecutiveEmpty <= 2) {
405
461
  // После system-сообщений (например "Skills loaded") — более конкретная подсказка
406
- const isAfterSystem = lastMsg?.role === 'system';
462
+ const isAfterSystem = lastMsg?.role === "system";
407
463
  const hint = isAfterSystem
408
- ? '[System: You returned an empty response. The skills are loaded. Now create a plan and start working on the task. Do NOT wait for further instructions.]'
409
- : 'Continue.';
410
- messages.push({ role: 'user', content: hint });
464
+ ? "[System: You returned an empty response. The skills are loaded. Now create a plan and start working on the task. Do NOT wait for further instructions.]"
465
+ : "Continue.";
466
+ messages.push({ role: "user", content: hint });
411
467
  if (!quiet) {
412
- renderer?.writeDim(` [empty response, retrying${isAfterSystem ? ' with skills hint' : ''}]`);
468
+ renderer?.writeDim(` [empty response, retrying${isAfterSystem ? " with skills hint" : ""}]`);
413
469
  }
414
470
  continue;
415
471
  }
416
472
  renderer?.writeError(consecutiveEmpty > 3
417
473
  ? `[model returned empty response ${consecutiveEmpty} times in a row — stopping]`
418
- : '[model returned empty response]');
474
+ : "[model returned empty response]");
419
475
  return { messages };
420
476
  }
421
477
  consecutiveEmpty = 0;
422
- messages.push({ role: 'assistant', content: response });
478
+ messages.push({ role: "assistant", content: response });
423
479
  const { validCalls, invalidCalls } = parseToolCalls(response);
424
480
  if (validCalls.length === 0) {
425
481
  if (invalidCalls.length > 0) {
426
482
  // If the response has substantial text outside tool_call tags,
427
483
  // it's a description with examples — not a failed tool call attempt.
428
- const textOutside = response.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim();
484
+ const textOutside = response
485
+ .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "")
486
+ .trim();
429
487
  if (textOutside.length < 100) {
430
- const feedback = `[Tool call error(s)]\n${invalidCalls.join('\n')}\n\nFix the tool call format and try again. Use ONLY valid tool names: bash, read_file, write_file, edit_file, list_dir, create_dir, delete_file, copy_file, move_file, file_info, glob_search, grep_search, tree, web_search, web_fetch, web_browse, plan, todo, question, approve, subagent, scaffold.`;
488
+ consecutiveInvalidCalls++;
489
+ const feedback = `[Tool call error(s)]\n${invalidCalls.join("\n")}\n\nFix the tool call format and try again. Use ONLY valid tool names: bash, read_file, write_file, edit_file, list_dir, create_dir, delete_file, copy_file, move_file, file_info, glob_search, grep_search, tree, web_search, web_fetch, web_browse, plan, todo, question, approve, subagent, scaffold.`;
431
490
  if (!quiet) {
432
491
  renderer?.writeError(` [invalid tool call: ${invalidCalls[0]}]`);
433
492
  }
434
- messages.push({ role: 'tool', content: feedback, tool_call_id: `invalid-${Date.now()}` });
493
+ messages.push({
494
+ role: "tool",
495
+ content: feedback,
496
+ tool_call_id: `invalid-${Date.now()}`,
497
+ });
435
498
  continue;
436
499
  }
437
500
  // Text response with example tool calls — just log and proceed
438
- logWarn('LOOP', `ignored ${invalidCalls.length} invalid tool call(s) in descriptive text`);
501
+ logWarn("LOOP", `ignored ${invalidCalls.length} invalid tool call(s) in descriptive text`);
439
502
  }
440
503
  // Nudge model to continue working after a tool result — but only if
441
504
  // the agent didn't already write a meaningful text response.
442
- if (continuationNudgeMax > 0 && continuationNudges < continuationNudgeMax) {
505
+ if (continuationNudgeMax > 0 &&
506
+ continuationNudges < continuationNudgeMax) {
443
507
  const lastThree = messages.slice(-3);
444
- const hasRecentToolResult = lastThree.some(m => m.role === 'tool');
445
- const alreadyResponded = response.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim().length > 20;
508
+ const hasRecentToolResult = lastThree.some((m) => m.role === "tool");
509
+ const alreadyResponded = response.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "").trim()
510
+ .length > 20;
446
511
  if (hasRecentToolResult && !alreadyResponded) {
447
512
  continuationNudges++;
448
513
  if (!quiet) {
449
514
  renderer?.writeDim(` [nudge ${continuationNudges}/${continuationNudgeMax}: continue working]`);
450
515
  }
451
- messages.push({ role: 'user', content: '[Tool result received. Write a response to the user. Do NOT call additional tools unless there is actual work to do.]' });
516
+ messages.push({
517
+ role: "user",
518
+ content: "[Tool result received. Write a response to the user. Do NOT call additional tools unless there is actual work to do.]",
519
+ });
452
520
  continue;
453
521
  }
454
522
  }
455
523
  if (!skillsAutoLoaded) {
456
- const textOutside = response.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '').trim();
524
+ const textOutside = response
525
+ .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "")
526
+ .trim();
457
527
  const looksLikeDone = /done|готово|создан|выполн|completed|finished/i.test(textOutside);
458
528
  // Проверка: план с незавершёнными шагами — не даём агенту остановиться
459
529
  const pendingSteps = getPlanPendingCount();
460
530
  if (pendingSteps > 0 && looksLikeDone) {
461
531
  const planText = showPlan();
462
- const pendingLines = planText.split('\n').filter(l => l.includes('○') || l.includes('►'));
463
- const nextStep = pendingLines[0] || '';
532
+ const pendingLines = planText
533
+ .split("\n")
534
+ .filter((l) => l.includes("○") || l.includes("►"));
535
+ const nextStep = pendingLines[0] || "";
464
536
  const nextStepMatch = nextStep.match(/\d+\.\s*(.+)/);
465
- const nextStepTitle = nextStepMatch ? nextStepMatch[1].trim() : '';
537
+ const nextStepTitle = nextStepMatch ? nextStepMatch[1].trim() : "";
466
538
  if (!quiet) {
467
539
  renderer?.writeWarning(` [plan has ${pendingSteps} incomplete steps — pushing agent to continue]`);
468
540
  }
469
541
  const nudge = nextStepTitle
470
542
  ? `Plan is NOT complete! ${pendingSteps} step(s) remaining.\nNext step: ${nextStepTitle}\nUse subagent to execute this step, then mark done: plan({action:"done", step:N})`
471
- : `Plan is NOT complete! ${pendingSteps} step(s) remaining:\n${pendingLines.join('\n')}\n\nContinue with the next step using subagent.`;
472
- messages.push({ role: 'user', content: nudge });
543
+ : `Plan is NOT complete! ${pendingSteps} step(s) remaining:\n${pendingLines.join("\n")}\n\nContinue with the next step using subagent.`;
544
+ messages.push({ role: "user", content: nudge });
473
545
  continuationNudges = 0;
474
546
  continue;
475
547
  }
476
548
  // Phase 4.1: Аудит файлов при завершении плана
477
549
  if (pendingSteps === 0 && looksLikeDone) {
478
550
  const plan = showPlan();
479
- if (!plan.startsWith('No active plan')) {
551
+ if (!plan.startsWith("No active plan")) {
480
552
  // Извлекаем заголовки шагов и проверяем наличие файлов
481
- const stepLines = plan.split('\n').filter(l => l.match(/\d+\.\s/));
553
+ const stepLines = plan
554
+ .split("\n")
555
+ .filter((l) => l.match(/\d+\.\s/));
482
556
  const missingFiles = [];
483
557
  for (const line of stepLines) {
484
558
  const titleMatch = line.match(/\d+\.\s*(.+)/);
@@ -486,22 +560,23 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
486
560
  continue;
487
561
  const title = titleMatch[1].trim();
488
562
  // Извлекаем имя файла из заголовка (паттерн: "Create file.ext" или "file.ext")
489
- const fileMatch = title.match(/(?:Create|Создать|Add|Добавить)\s+(\S+\.(?:ts|tsx|js|jsx|json|html|css|md|yaml|yml))/i)
490
- || title.match(/(\w+\.(?:ts|tsx|js|jsx|json|html|css|md|yaml|yml))/i);
563
+ const fileMatch = title.match(/(?:Create|Создать|Add|Добавить)\s+(\S+\.(?:ts|tsx|js|jsx|json|html|css|md|yaml|yml))/i) ||
564
+ title.match(/(\w+\.(?:ts|tsx|js|jsx|json|html|css|md|yaml|yml))/i);
491
565
  if (fileMatch) {
492
566
  const fileName = fileMatch[1];
493
- if (!existsSync(fileName) && !createdFiles.some(f => f.endsWith(fileName))) {
567
+ if (!existsSync(fileName) &&
568
+ !createdFiles.some((f) => f.endsWith(fileName))) {
494
569
  missingFiles.push(fileName);
495
570
  }
496
571
  }
497
572
  }
498
573
  if (missingFiles.length > 0) {
499
574
  if (!quiet) {
500
- renderer?.writeWarning(` [plan complete but missing files: ${missingFiles.join(', ')}]`);
575
+ renderer?.writeWarning(` [plan complete but missing files: ${missingFiles.join(", ")}]`);
501
576
  }
502
577
  messages.push({
503
- role: 'user',
504
- content: `Plan claims all steps done, but these files are missing: ${missingFiles.join(', ')}. Create them before finishing.`,
578
+ role: "user",
579
+ content: `Plan claims all steps done, but these files are missing: ${missingFiles.join(", ")}. Create them before finishing.`,
505
580
  });
506
581
  continuationNudges = 0;
507
582
  continue;
@@ -509,16 +584,19 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
509
584
  }
510
585
  }
511
586
  // Stuck detection: если модель делает бесполезные действия
512
- const isUselessAction = validCalls.some(c => c.parsed.name === 'list_dir' || c.parsed.name === 'tree' ||
513
- (c.parsed.name === 'todo' && c.parsed.arguments?.action === 'list') ||
514
- (c.parsed.name === 'question' && !c.parsed.arguments?.question));
515
- // Phase 5.1: Расширенное detect — повторные чтения, поиски и вопросы
587
+ const isUselessAction = validCalls.some((c) => c.parsed.name === "list_dir" ||
588
+ c.parsed.name === "tree" ||
589
+ (c.parsed.name === "todo" &&
590
+ c.parsed.arguments?.action === "list") ||
591
+ (c.parsed.name === "question" && !c.parsed.arguments?.question));
592
+ // Phase 5.1: Расширенное detect — повторные чтения, поиски, вопросы и bash
516
593
  let isRepeatedRead = false;
517
594
  let isRepeatedSearch = false;
518
595
  let isRepeatedQuestion = false;
596
+ let isRepeatedBash = false;
519
597
  for (const c of validCalls) {
520
- if (c.parsed.name === 'read_file') {
521
- const filePath = String(c.parsed.arguments?.path || '');
598
+ if (c.parsed.name === "read_file") {
599
+ const filePath = String(c.parsed.arguments?.path || "");
522
600
  if (recentReadFiles.includes(filePath)) {
523
601
  isRepeatedRead = true;
524
602
  }
@@ -526,8 +604,8 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
526
604
  if (recentReadFiles.length > 5)
527
605
  recentReadFiles.shift();
528
606
  }
529
- if (c.parsed.name === 'grep_search') {
530
- const pattern = String(c.parsed.arguments?.pattern || '');
607
+ if (c.parsed.name === "grep_search") {
608
+ const pattern = String(c.parsed.arguments?.pattern || "");
531
609
  if (recentGrepPatterns.includes(pattern)) {
532
610
  isRepeatedSearch = true;
533
611
  }
@@ -535,8 +613,8 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
535
613
  if (recentGrepPatterns.length > 5)
536
614
  recentGrepPatterns.shift();
537
615
  }
538
- if (c.parsed.name === 'question') {
539
- const questionText = String(c.parsed.arguments?.question || '');
616
+ if (c.parsed.name === "question") {
617
+ const questionText = String(c.parsed.arguments?.question || "");
540
618
  if (recentQuestions.includes(questionText)) {
541
619
  isRepeatedQuestion = true;
542
620
  }
@@ -544,8 +622,22 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
544
622
  if (recentQuestions.length > 3)
545
623
  recentQuestions.shift();
546
624
  }
625
+ if (c.parsed.name === "bash") {
626
+ const cmdText = String(c.parsed.arguments?.cmd || "");
627
+ const normalized = cmdText.trim().replace(/\s+/g, " ");
628
+ if (recentBashCommands.includes(normalized)) {
629
+ isRepeatedBash = true;
630
+ }
631
+ recentBashCommands.push(normalized);
632
+ if (recentBashCommands.length > 5)
633
+ recentBashCommands.shift();
634
+ }
547
635
  }
548
- const isStuckAction = isUselessAction || isRepeatedRead || isRepeatedSearch || isRepeatedQuestion;
636
+ const isStuckAction = isUselessAction ||
637
+ isRepeatedRead ||
638
+ isRepeatedSearch ||
639
+ isRepeatedQuestion ||
640
+ isRepeatedBash;
549
641
  if (isStuckAction) {
550
642
  uselessActionCount++;
551
643
  if (uselessActionCount >= 3) {
@@ -554,7 +646,7 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
554
646
  }
555
647
  clearPlan();
556
648
  messages.push({
557
- role: 'user',
649
+ role: "user",
558
650
  content: `[System: You are stuck in a loop. STOP calling the same tool. The user already answered your question — use their answer and proceed with the task. Do NOT ask the same question again.]`,
559
651
  });
560
652
  uselessActionCount = 0;
@@ -562,17 +654,20 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
562
654
  continue;
563
655
  }
564
656
  }
565
- else if (validCalls.some(c => c.parsed.name === 'subagent' || c.parsed.name === 'write_file' || c.parsed.name === 'bash')) {
657
+ else if (validCalls.some((c) => c.parsed.name === "subagent" ||
658
+ c.parsed.name === "write_file" ||
659
+ c.parsed.name === "bash")) {
566
660
  uselessActionCount = 0;
567
661
  recentReadFiles.length = 0;
568
662
  recentGrepPatterns.length = 0;
569
663
  recentQuestions.length = 0;
664
+ recentBashCommands.length = 0;
570
665
  }
571
666
  // Если модель claim-ит done но есть созданные файлы — напоминаем о полноте
572
667
  if (createdFiles.length > 0 && looksLikeDone) {
573
668
  messages.push({
574
- role: 'user',
575
- content: `Files created: ${createdFiles.join(', ')}. Before finishing — verify all files are complete and connected to each other. If any file is missing or not imported, continue working.`,
669
+ role: "user",
670
+ content: `Files created: ${createdFiles.join(", ")}. Before finishing — verify all files are complete and connected to each other. If any file is missing or not imported, continue working.`,
576
671
  });
577
672
  continuationNudges = 0;
578
673
  continue;
@@ -587,48 +682,75 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
587
682
  const groundingCfg = getGroundingConfig(config);
588
683
  if (groundingCfg.enabled) {
589
684
  const groundMatches = Array.from(response.matchAll(/@grounding\[файл:\s*([^\]]+)\]/g));
590
- const groundPaths = groundMatches.map(m => m[1].trim());
591
- const pathCalls = validCalls.filter(c => c.parsed.arguments?.path);
685
+ const groundPaths = groundMatches.map((m) => m[1].trim());
686
+ const pathCalls = validCalls.filter((c) => c.parsed.arguments?.path);
592
687
  if (pathCalls.length > groundPaths.length) {
593
688
  consecutiveMissingGrounding++;
594
689
  const maxMissing = groundingCfg.maxMissingGrounding ?? 3;
690
+ const maxRetries = groundingCfg.maxConsecutiveGroundingErrors ?? 5;
595
691
  if (consecutiveMissingGrounding >= maxMissing) {
692
+ // CRITICAL — сброс счётчика + учёт retry
693
+ consecutiveMissingGrounding = 0;
694
+ groundingRetryCount++;
695
+ if (groundingRetryCount > maxRetries) {
696
+ // Полный bypass — выполняем tool_call без маркера
697
+ if (!quiet)
698
+ renderer?.writeWarning(` [grounding: exceeded limit, bypassing]`);
699
+ groundingRetryCount = 0;
700
+ messages.push({
701
+ role: "user",
702
+ content: `[Note: Grounding check disabled after repeated failures. Proceeding without marker. To re-enable, reset the agent.]`,
703
+ });
704
+ // fall through — tool call будет выполнен
705
+ }
706
+ else {
707
+ if (!quiet)
708
+ renderer?.writeError(` [grounding: ${groundingRetryCount}/${maxRetries} retries]`);
709
+ messages.push({
710
+ role: "user",
711
+ content: `[CRITICAL: Output @grounding[файл: <path>] BEFORE each <tool_call>.\nExample:\n@grounding[файл: package.json]\n<tool_call>{"name":"read_file","arguments":{"path":"package.json"}}</tool_call>]`,
712
+ });
713
+ continue;
714
+ }
715
+ }
716
+ else if (groundingCfg.strictMode) {
596
717
  if (!quiet)
597
- renderer?.writeError(` [grounding: ${consecutiveMissingGrounding}/${maxMissing} missing markers]`);
718
+ renderer?.writeError(` [grounding: strict mode, retrying]`);
598
719
  messages.push({
599
- role: 'user',
600
- content: `[Grounding Error: Missing @grounding[файл: path] marker before tool call. CRITICAL: Output the marker on its own line BEFORE every <tool_call>.]`,
720
+ role: "user",
721
+ content: `[Grounding Error: Missing @grounding[файл: path] marker. Output the marker BEFORE each <tool_call>. Example:\n@grounding[файл: package.json]\n<tool_call>{"name":"read_file","arguments":{"path":"package.json"}}</tool_call>]`,
601
722
  });
602
723
  continue;
603
724
  }
604
- if (groundingCfg.strictMode) {
725
+ else {
605
726
  if (!quiet)
606
- renderer?.writeError(` [grounding: strict mode, retrying]`);
727
+ renderer?.writeDim(` [grounding: missing marker, reminder ${consecutiveMissingGrounding}/${maxMissing}]`);
607
728
  messages.push({
608
- role: 'user',
609
- content: `[Grounding Error: Missing @grounding[файл: path] marker. Try again with proper grounding marker before each tool call.]`,
729
+ role: "user",
730
+ content: `[Reminder: Include @grounding[файл: path] marker before each <tool_call> to prevent path hallucinations. Example:\n@grounding[файл: package.json]\n<tool_call>{"name":"read_file","arguments":{"path":"package.json"}}</tool_call>]`,
610
731
  });
611
732
  continue;
612
733
  }
613
- if (!quiet)
614
- renderer?.writeDim(` [grounding: missing marker, reminder ${consecutiveMissingGrounding}/${maxMissing}]`);
615
- messages.push({
616
- role: 'user',
617
- content: `[Reminder: Include @grounding[файл: path] marker before each <tool_call> to prevent path hallucinations.]`,
618
- });
619
- continue;
620
734
  }
621
735
  else {
622
736
  consecutiveMissingGrounding = 0;
737
+ groundingRetryCount = 0;
623
738
  // Проверка соответствия пути в маркере и в аргументах tool_call
624
739
  for (let i = 0; i < pathCalls.length && i < groundPaths.length; i++) {
625
- const callPath = String(pathCalls[i].parsed.arguments.path || '').replace(/\\/g, '/').replace(/^\.\//, '');
626
- const groundPath = groundPaths[i].replace(/\\/g, '/').replace(/^\.\//, '');
627
- if (callPath && groundPath && !callPath.endsWith(groundPath) && !groundPath.endsWith(callPath)) {
740
+ const callPath = String(pathCalls[i].parsed.arguments.path || "")
741
+ .replace(/\\/g, "/")
742
+ .replace(/^\.\//, "");
743
+ const groundPath = groundPaths[i]
744
+ .replace(/\\/g, "/")
745
+ .replace(/^\.\//, "");
746
+ if (callPath &&
747
+ groundPath &&
748
+ !callPath.endsWith(groundPath) &&
749
+ !groundPath.endsWith(callPath)) {
628
750
  if (!quiet)
629
751
  renderer?.writeWarning(` [grounding: path mismatch — marker says "${groundPath}", tool says "${callPath}"]`);
630
752
  messages.push({
631
- role: 'user',
753
+ role: "user",
632
754
  content: `[Warning: Grounding marker path "${groundPath}" doesn't match tool call path "${callPath}". Fix the marker to match the actual path.]`,
633
755
  });
634
756
  break;
@@ -646,9 +768,11 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
646
768
  // ─── Исполнение tool calls ──────────────────────────
647
769
  for (const call of validCalls) {
648
770
  const { parsed, cmd } = call;
649
- const isBash = parsed.name === 'bash';
771
+ const isBash = parsed.name === "bash";
650
772
  const preview = isBash
651
- ? (cmd.length > 200 ? cmd.slice(0, 200) + '...' : cmd)
773
+ ? cmd.length > 200
774
+ ? cmd.slice(0, 200) + "..."
775
+ : cmd
652
776
  : JSON.stringify(parsed.arguments);
653
777
  const toolId = generateToolCallId();
654
778
  logToolCall(parsed.name, preview);
@@ -656,67 +780,79 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
656
780
  renderer?.writeToolHeader(parsed.name);
657
781
  renderer?.writeToolPreview(parsed.name, preview);
658
782
  }
659
- const hookCmd = isBash ? cmd : `${parsed.name} ${JSON.stringify(parsed.arguments)}`;
783
+ const hookCmd = isBash
784
+ ? cmd
785
+ : `${parsed.name} ${JSON.stringify(parsed.arguments)}`;
660
786
  const preHook = runPreToolUseHooks(process.cwd(), parsed.name, hookCmd);
661
787
  if (!preHook.continue) {
662
788
  if (!quiet) {
663
- renderer?.writeBracketTag('hook blocked', preHook.message || '');
789
+ renderer?.writeBracketTag("hook blocked", preHook.message || "");
664
790
  }
665
- messages.push({ role: 'tool', content: `Hook blocked: ${preHook.message || ''}`, tool_call_id: toolId });
791
+ messages.push({
792
+ role: "tool",
793
+ content: `Hook blocked: ${preHook.message || ""}`,
794
+ tool_call_id: toolId,
795
+ });
666
796
  continue;
667
797
  }
668
798
  if (pendingSyntaxError && !isBash) {
669
- const isWriteOp = parsed.name === 'write_file' || parsed.name === 'edit_file';
670
- const newPath = String(parsed.arguments.path ?? '');
799
+ const isWriteOp = parsed.name === "write_file" || parsed.name === "edit_file";
800
+ const newPath = String(parsed.arguments.path ?? "");
671
801
  // Allow editing config files (tsconfig.json, package.json) since they
672
802
  // can fix syntax errors (e.g. missing jsx flag in tsconfig)
673
- const isConfig = newPath === 'tsconfig.json' || newPath === 'package.json';
803
+ const isConfig = newPath === "tsconfig.json" || newPath === "package.json";
674
804
  if (isWriteOp && newPath !== pendingSyntaxError && !isConfig) {
675
805
  const blocked = `BLOCKED: You have a syntax error in ${pendingSyntaxError}. Fix it first before writing new files. If the error is about jsx, module resolution, or missing types — edit tsconfig.json (it is exempt from this block).`;
676
806
  if (!quiet) {
677
807
  renderer?.writeError(` [blocked: fix syntax error in ${pendingSyntaxError} first]`);
678
808
  }
679
- messages.push({ role: 'tool', content: blocked, tool_call_id: toolId });
809
+ messages.push({
810
+ role: "tool",
811
+ content: blocked,
812
+ tool_call_id: toolId,
813
+ });
680
814
  continue;
681
815
  }
682
816
  }
683
- let toolOutput = '';
817
+ let toolOutput = "";
684
818
  let toolExitCode = 0;
685
819
  const toolStart = performance.now();
686
820
  // ─── Субагент: изолированный контекст для задачи ──
687
- if (parsed.name === 'subagent') {
688
- const task = String(parsed.arguments.task || '');
821
+ if (parsed.name === "subagent") {
822
+ const task = String(parsed.arguments.task || "");
689
823
  if (!task) {
690
- toolOutput = 'Error: task is required';
824
+ toolOutput = "Error: task is required";
691
825
  toolExitCode = 1;
692
826
  }
693
827
  else {
694
828
  // Авто-пометка следующего pending шага как in_progress
695
829
  const nextPending = getNextPendingStep();
696
830
  if (nextPending) {
697
- updatePlanStep(nextPending.id, 'in_progress');
831
+ updatePlanStep(nextPending.id, "in_progress");
698
832
  }
699
833
  // Phase 2.3: Авто-подстановка полного пути в задачу subagent
700
834
  let finalTask = task;
701
835
  if (nextPending) {
702
836
  const expectedDir = extractDirFromStepTitle(nextPending.title);
703
- if (expectedDir && !task.includes(expectedDir + '/')) {
837
+ if (expectedDir && !task.includes(expectedDir + "/")) {
704
838
  finalTask = `${task} (directory: ${expectedDir}/)`;
705
- logInfo('AGENT', `injected dir path into subagent task: "${expectedDir}"`);
839
+ logInfo("AGENT", `injected dir path into subagent task: "${expectedDir}"`);
706
840
  }
707
841
  }
708
842
  const autoContext = buildSubagentContext(process.cwd());
709
- logInfo('AGENT', `spawning subagent | task: "${finalTask.slice(0, 150)}"`);
843
+ logInfo("AGENT", `spawning subagent | task: "${finalTask.slice(0, 150)}"`);
710
844
  // Показываем спиннер на основном рендерере пока субагент работает
711
845
  if (!quiet) {
712
- const taskPreview = finalTask.length > 60 ? finalTask.slice(0, 57) + '...' : finalTask;
846
+ const taskPreview = finalTask.length > 60
847
+ ? finalTask.slice(0, 57) + "..."
848
+ : finalTask;
713
849
  renderer?.writeSpinner(` [subagent] ${taskPreview}`);
714
850
  }
715
851
  const subRenderer = new SubagentRenderer();
716
852
  try {
717
853
  const childResult = await runIsolatedLoop(provider, registry, finalTask, autoContext, process.cwd(), quiet, subRenderer, toolExecutor, modeConfig.subagentMode, llmCfg.toolModel);
718
854
  toolOutput = childResult.summary;
719
- logInfo('AGENT', `subagent returned | files: ${childResult.createdFiles.length} | turns: ${childResult.turns} | errors: ${childResult.errors.length}`);
855
+ logInfo("AGENT", `subagent returned | files: ${childResult.createdFiles.length} | turns: ${childResult.turns} | errors: ${childResult.errors.length}`);
720
856
  if (childResult.createdFiles.length > 0) {
721
857
  for (const f of childResult.createdFiles) {
722
858
  if (!createdFiles.includes(f))
@@ -727,26 +863,30 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
727
863
  if (nextPending && childResult.createdFiles.length > 0) {
728
864
  const expectedDir = extractDirFromStepTitle(nextPending.title);
729
865
  if (expectedDir) {
730
- const wrongDirFiles = childResult.createdFiles.filter(f => !f.startsWith(expectedDir + '/') && !f.startsWith(expectedDir + '\\'));
866
+ const wrongDirFiles = childResult.createdFiles.filter((f) => !f.startsWith(expectedDir + "/") &&
867
+ !f.startsWith(expectedDir + "\\"));
731
868
  if (wrongDirFiles.length > 0) {
732
- const correction = `⚠ Files created in wrong directory: ${wrongDirFiles.join(', ')}. ` +
869
+ const correction = `⚠ Files created in wrong directory: ${wrongDirFiles.join(", ")}. ` +
733
870
  `They should be in ${expectedDir}/. Use write_file with full path: ${expectedDir}/${wrongDirFiles[0]}`;
734
- toolOutput += '\n\n' + correction;
871
+ toolOutput += "\n\n" + correction;
735
872
  if (!quiet) {
736
- renderer?.writeWarning(` [subagent created files in wrong directory: ${wrongDirFiles.join(', ')}]`);
873
+ renderer?.writeWarning(` [subagent created files in wrong directory: ${wrongDirFiles.join(", ")}]`);
737
874
  }
738
875
  }
739
876
  }
740
877
  }
741
878
  const buf = subRenderer.getBuffer();
742
879
  if (!quiet && renderer?.writeToolBuffer) {
743
- renderer.writeToolBuffer({ lines: buf.lines, toolCalls: buf.toolCalls });
880
+ renderer.writeToolBuffer({
881
+ lines: buf.lines,
882
+ toolCalls: buf.toolCalls,
883
+ });
744
884
  }
745
885
  }
746
886
  catch (e) {
747
887
  toolOutput = `Subagent error: ${e.message}`;
748
888
  toolExitCode = 1;
749
- logError('AGENT', `subagent error: ${e.message}`);
889
+ logError("AGENT", `subagent error: ${e.message}`);
750
890
  }
751
891
  }
752
892
  const subTime = ((performance.now() - toolStart) / 1000).toFixed(1);
@@ -760,12 +900,13 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
760
900
  const mcpCmd = parseMCPCommand(cmd);
761
901
  if (mcpCmd) {
762
902
  if (!quiet) {
763
- renderer?.writeBracketTag('mcp', `${mcpCmd.server}/${mcpCmd.tool}`);
903
+ renderer?.writeBracketTag("mcp", `${mcpCmd.server}/${mcpCmd.tool}`);
764
904
  }
765
905
  toolOutput = callMCPTool(process.cwd(), mcpCmd.server, mcpCmd.tool, mcpCmd.args);
766
906
  }
767
907
  else {
768
- toolOutput = 'Invalid MCP command. Format: mcp <server> <tool> [args_json]';
908
+ toolOutput =
909
+ "Invalid MCP command. Format: mcp <server> <tool> [args_json]";
769
910
  toolExitCode = 1;
770
911
  }
771
912
  // ─── Все инструменты через ToolExecutor ──────────────
@@ -776,19 +917,25 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
776
917
  let executorArgs = parsed.arguments;
777
918
  // Bash-команды
778
919
  if (isBash) {
779
- executorName = 'bash';
920
+ executorName = "bash";
780
921
  executorArgs = { cmd, timeout: parsed.arguments?.timeout ?? 120 };
781
922
  }
782
- const hookCmd = isBash ? cmd : `${parsed.name} ${JSON.stringify(parsed.arguments)}`;
783
- const executorCall = { id: toolId, name: executorName, arguments: executorArgs };
923
+ const hookCmd = isBash
924
+ ? cmd
925
+ : `${parsed.name} ${JSON.stringify(parsed.arguments)}`;
926
+ const executorCall = {
927
+ id: toolId,
928
+ name: executorName,
929
+ arguments: executorArgs,
930
+ };
784
931
  const toolCtx = { cwd: process.cwd(), renderer, quiet };
785
932
  const { result, elapsed } = await toolExecutor.execute(executorCall, toolCtx, hookCmd);
786
- toolOutput = result.stdout || result.stderr || '(no output)';
933
+ toolOutput = result.stdout || result.stderr || "(no output)";
787
934
  toolExitCode = result.exitCode;
788
935
  // Обработка question/approve: приостанавливаем цикл
789
936
  if (result.questionPending) {
790
937
  messages.push({
791
- role: 'tool',
938
+ role: "tool",
792
939
  content: toolOutput,
793
940
  tool_call_id: toolId,
794
941
  });
@@ -799,75 +946,99 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
799
946
  }
800
947
  }
801
948
  const toolTime = ((performance.now() - toolStart) / 1000).toFixed(1);
802
- logToolCall(parsed.name, preview, toolExitCode !== 0 ? `exit:${toolExitCode} ${toolOutput.slice(0, 100)}` : `ok (${toolTime}s)`);
949
+ logToolCall(parsed.name, preview, toolExitCode !== 0
950
+ ? `exit:${toolExitCode} ${toolOutput.slice(0, 100)}`
951
+ : `ok (${toolTime}s)`);
803
952
  if (toolExitCode !== 0) {
804
953
  const errText = toolOutput;
805
954
  if (!quiet) {
806
- for (const line of errText.split('\n').slice(0, 5)) {
955
+ for (const line of errText.split("\n").slice(0, 5)) {
807
956
  renderer?.writeError(` ${line}`);
808
957
  }
809
958
  }
810
- autoLearnFromError(hookCmd, toolExitCode !== 0 ? toolOutput : '', '');
959
+ autoLearnFromError(hookCmd, toolExitCode !== 0 ? toolOutput : "", "");
811
960
  }
812
961
  runPostToolUseHooks(process.cwd(), parsed.name, hookCmd, toolOutput, toolExitCode);
813
962
  // Track sequential read_file calls for consolidation
814
- if (readConsolidation && !isBash && parsed.name === 'read_file' && toolExitCode === 0) {
815
- const filePath = String(parsed.arguments.path ?? '');
963
+ if (readConsolidation &&
964
+ !isBash &&
965
+ parsed.name === "read_file" &&
966
+ toolExitCode === 0) {
967
+ const filePath = String(parsed.arguments.path ?? "");
816
968
  const offset = Math.max(1, Number(parsed.arguments.offset ?? 1));
817
969
  const limit = Math.max(1, Number(parsed.arguments.limit ?? 500));
818
- if (lastReadFile && lastReadFile.path === filePath && offset === lastReadFile.offset + lastReadFile.limit) {
970
+ if (lastReadFile &&
971
+ lastReadFile.path === filePath &&
972
+ offset === lastReadFile.offset + lastReadFile.limit) {
819
973
  // Sequential read — merge with previous result
820
- lastReadFile.content += '\n' + toolOutput;
974
+ lastReadFile.content += "\n" + toolOutput;
821
975
  lastReadFile.offset = offset;
822
976
  lastReadFile.limit = limit;
823
977
  // Replace the last tool message content with merged content
824
978
  for (let i = messages.length - 1; i >= 0; i--) {
825
- if (messages[i].role === 'tool' && messages[i].tool_call_id === lastReadFile.toolId) {
979
+ if (messages[i].role === "tool" &&
980
+ messages[i].tool_call_id === lastReadFile.toolId) {
826
981
  messages[i].content = lastReadFile.content;
827
982
  break;
828
983
  }
829
984
  }
830
985
  // Remove the trailing "Continue." nudge + the new tool result that would be pushed
831
- while (messages.length > 0 && messages[messages.length - 1].role === 'user' && messages[messages.length - 1].content === 'Continue.') {
986
+ while (messages.length > 0 &&
987
+ messages[messages.length - 1].role === "user" &&
988
+ messages[messages.length - 1].content === "Continue.") {
832
989
  messages.pop();
833
990
  }
834
991
  if (!quiet) {
835
- renderer?.writeBracketTag('consolidate', `merged read_file chunks for ${filePath}`);
992
+ renderer?.writeBracketTag("consolidate", `merged read_file chunks for ${filePath}`);
836
993
  }
837
994
  continue; // skip pushing this result separately
838
995
  }
839
996
  else {
840
997
  // New file or non-sequential — start fresh
841
- lastReadFile = { path: filePath, offset, limit, content: toolOutput, toolId };
998
+ lastReadFile = {
999
+ path: filePath,
1000
+ offset,
1001
+ limit,
1002
+ content: toolOutput,
1003
+ toolId,
1004
+ };
842
1005
  }
843
1006
  }
844
- else if (!isBash && parsed.name !== 'read_file') {
1007
+ else if (!isBash && parsed.name !== "read_file") {
845
1008
  lastReadFile = null;
846
1009
  }
847
1010
  // ─── Проверка синтаксиса после записи файлов ────────
848
1011
  let hasSyntaxError = false;
849
1012
  if (toolExitCode === 0) {
850
1013
  const isWriteOp = isBash
851
- ? (cmd.match(/Set-Content|Add-Content/))
852
- : (parsed.name === 'write_file' || parsed.name === 'edit_file');
1014
+ ? cmd.match(/Set-Content|Add-Content/)
1015
+ : parsed.name === "write_file" || parsed.name === "edit_file";
853
1016
  if (isWriteOp) {
854
1017
  const rawPath = isBash
855
- ? cmd.match(/-Path\s+['"]?([^'"]+\.(?:ts|tsx|js|jsx|json))['"]?/i)?.[1]?.replace(/\\/g, '/')
856
- : String(parsed.arguments.path ?? '');
1018
+ ? cmd
1019
+ .match(/-Path\s+['"]?([^'"]+\.(?:ts|tsx|js|jsx|json))['"]?/i)?.[1]
1020
+ ?.replace(/\\/g, "/")
1021
+ : String(parsed.arguments.path ?? "");
857
1022
  if (rawPath && /\.(ts|tsx|js|jsx|json)$/.test(rawPath)) {
858
- const filePath = isAbsolute(rawPath) ? relative(process.cwd(), rawPath).replace(/\\/g, '/') : rawPath;
1023
+ const filePath = isAbsolute(rawPath)
1024
+ ? relative(process.cwd(), rawPath).replace(/\\/g, "/")
1025
+ : rawPath;
859
1026
  const syntaxResult = checkFileSyntax(filePath, process.cwd());
860
- if (syntaxResult !== 'OK') {
861
- const errorLines = syntaxResult.split('\n').filter(l => l.trim());
1027
+ if (syntaxResult !== "OK") {
1028
+ const errorLines = syntaxResult
1029
+ .split("\n")
1030
+ .filter((l) => l.trim());
862
1031
  const shown = errorLines.slice(0, 3);
863
- const extra = errorLines.length > 3 ? ` (+${errorLines.length - 3} more)` : '';
1032
+ const extra = errorLines.length > 3
1033
+ ? ` (+${errorLines.length - 3} more)`
1034
+ : "";
864
1035
  if (!quiet) {
865
- renderer?.writeBracketTag('syntax', `${shown.join('; ')}${extra}`);
1036
+ renderer?.writeBracketTag("syntax", `${shown.join("; ")}${extra}`);
866
1037
  }
867
1038
  const fullPath = join(process.cwd(), filePath);
868
- let contextLines = '';
1039
+ let contextLines = "";
869
1040
  try {
870
- const fileContent = readFileSync(fullPath, 'utf-8').split('\n');
1041
+ const fileContent = readFileSync(fullPath, "utf-8").split("\n");
871
1042
  for (const errLine of shown) {
872
1043
  const lineMatch = errLine.match(/\((\d+),\d+\)/);
873
1044
  if (lineMatch) {
@@ -877,21 +1048,23 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
877
1048
  const end = Math.min(fileContent.length, lineNum + 1);
878
1049
  contextLines += `\n--- Line ${lineNum} ---\n`;
879
1050
  for (let i = start; i < end; i++) {
880
- const marker = i === lineNum - 1 ? ' >> ' : ' ';
1051
+ const marker = i === lineNum - 1 ? " >> " : " ";
881
1052
  contextLines += `${marker}${i + 1}: ${fileContent[i]}\n`;
882
1053
  }
883
1054
  }
884
1055
  }
885
1056
  }
886
1057
  }
887
- catch { /* ignore read errors */ }
1058
+ catch {
1059
+ /* ignore read errors */
1060
+ }
888
1061
  toolOutput = `SYNTAX ERROR in ${filePath}:\n${syntaxResult}${contextLines}`;
889
1062
  // Detect config-related TS errors (missing jsx, module issues, etc.)
890
- const isConfigError = syntaxResult.includes('TS17004')
891
- || syntaxResult.includes('TS2307')
892
- || syntaxResult.includes('Cannot find module')
893
- || syntaxResult.includes('jsx')
894
- || syntaxResult.includes('moduleResolution');
1063
+ const isConfigError = syntaxResult.includes("TS17004") ||
1064
+ syntaxResult.includes("TS2307") ||
1065
+ syntaxResult.includes("Cannot find module") ||
1066
+ syntaxResult.includes("jsx") ||
1067
+ syntaxResult.includes("moduleResolution");
895
1068
  if (isConfigError) {
896
1069
  toolOutput += `\n\nThis looks like a tsconfig.json configuration issue, NOT a code syntax error.\nCheck tsconfig.json — you may need to add "jsx": "react-jsx" or fix module settings.\nDo NOT modify the source code to work around config errors. Edit tsconfig.json instead.`;
897
1070
  }
@@ -903,16 +1076,18 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
903
1076
  }
904
1077
  else {
905
1078
  if (pendingSyntaxError === filePath) {
906
- pendingSyntaxError = '';
1079
+ pendingSyntaxError = "";
907
1080
  }
908
1081
  }
909
1082
  // Автодетекция стиля кодирования из содержимого файла
910
1083
  if (toolExitCode === 0 && filePath) {
911
1084
  try {
912
- const fullContent = readFileSync(join(process.cwd(), filePath), 'utf-8');
1085
+ const fullContent = readFileSync(join(process.cwd(), filePath), "utf-8");
913
1086
  collectCodingStyle(filePath, fullContent);
914
1087
  }
915
- catch { /* ignore */ }
1088
+ catch {
1089
+ /* ignore */
1090
+ }
916
1091
  // Track created/modified files for completion reporting
917
1092
  if (!hasSyntaxError && !createdFiles.includes(filePath)) {
918
1093
  createdFiles.push(filePath);
@@ -922,14 +1097,14 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
922
1097
  }
923
1098
  }
924
1099
  messages.push({
925
- role: 'tool',
1100
+ role: "tool",
926
1101
  content: truncateOutput(parsed.name, toolOutput, toolExitCode),
927
1102
  tool_call_id: toolId,
928
1103
  });
929
1104
  // ─── Error escalation + specialist auto-delegation ──────────
930
1105
  if (toolExitCode !== 0 && toolOutput) {
931
1106
  const action = await errorEscalator.handleToolError(parsed.name, toolOutput, parsed.arguments, toolId, messages, createdFiles, enrichedPrompt, renderer, quiet, provider, registry, toolExecutor, modeConfig, llmCfg);
932
- if (action === 'continue')
1107
+ if (action === "continue")
933
1108
  continue;
934
1109
  }
935
1110
  else {
@@ -944,12 +1119,12 @@ export async function runAgentLoop(provider, registry, prompt, contextManager, p
944
1119
  }
945
1120
  }
946
1121
  if (aborted) {
947
- renderer?.writeWarning('Agent stopped (double ESC)');
1122
+ renderer?.writeWarning("Agent stopped (double ESC)");
948
1123
  }
949
- logInfo('LOOP', `runAgentLoop done: ${messages.length} total messages, aborted=${aborted}`);
1124
+ logInfo("LOOP", `runAgentLoop done: ${messages.length} total messages, aborted=${aborted}`);
950
1125
  // Agent:done event for notify plugin
951
- if (pluginManager.isActive('notify')) {
952
- pluginManager.emit('agent:done').catch(() => { });
1126
+ if (pluginManager.isActive("notify")) {
1127
+ pluginManager.emit("agent:done").catch(() => { });
953
1128
  }
954
1129
  return { messages };
955
1130
  }