hyacinth-ai 0.9.6

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 (704) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/commands.json +25 -0
  4. package/dist/agents/builtins.d.ts +8 -0
  5. package/dist/agents/builtins.js +50 -0
  6. package/dist/agents/config-loader.d.ts +15 -0
  7. package/dist/agents/config-loader.js +75 -0
  8. package/dist/agents/delegate-tool.d.ts +109 -0
  9. package/dist/agents/delegate-tool.js +407 -0
  10. package/dist/agents/index.d.ts +5 -0
  11. package/dist/agents/index.js +5 -0
  12. package/dist/agents/registry.d.ts +2 -0
  13. package/dist/agents/registry.js +2 -0
  14. package/dist/bypass/agents/companion/index.d.ts +57 -0
  15. package/dist/bypass/agents/companion/index.js +487 -0
  16. package/dist/bypass/agents/orchestrator/index.d.ts +17 -0
  17. package/dist/bypass/agents/orchestrator/index.js +414 -0
  18. package/dist/bypass/agents/orchestrator.d.ts +16 -0
  19. package/dist/bypass/agents/orchestrator.js +377 -0
  20. package/dist/bypass/agents/world-engine/agent.d.ts +41 -0
  21. package/dist/bypass/agents/world-engine/agent.js +385 -0
  22. package/dist/bypass/agents/world-engine/index.d.ts +52 -0
  23. package/dist/bypass/agents/world-engine/index.js +198 -0
  24. package/dist/bypass/agents/world-engine/sim.d.ts +14 -0
  25. package/dist/bypass/agents/world-engine/sim.js +120 -0
  26. package/dist/bypass/agents/world-engine/store.d.ts +110 -0
  27. package/dist/bypass/agents/world-engine/store.js +519 -0
  28. package/dist/bypass/agents/world-engine/ticker.d.ts +37 -0
  29. package/dist/bypass/agents/world-engine/ticker.js +214 -0
  30. package/dist/bypass/agents/world-engine/tools.d.ts +6 -0
  31. package/dist/bypass/agents/world-engine/tools.js +380 -0
  32. package/dist/bypass/agents/world-engine/types.d.ts +202 -0
  33. package/dist/bypass/agents/world-engine/types.js +56 -0
  34. package/dist/bypass/agents/world-engine.d.ts +20 -0
  35. package/dist/bypass/agents/world-engine.js +63 -0
  36. package/dist/bypass/base.d.ts +48 -0
  37. package/dist/bypass/base.js +126 -0
  38. package/dist/bypass/index.d.ts +5 -0
  39. package/dist/bypass/index.js +6 -0
  40. package/dist/bypass/manager.d.ts +41 -0
  41. package/dist/bypass/manager.js +179 -0
  42. package/dist/bypass/types.d.ts +63 -0
  43. package/dist/bypass/types.js +14 -0
  44. package/dist/channels/auto-detect.d.ts +25 -0
  45. package/dist/channels/auto-detect.js +77 -0
  46. package/dist/channels/builtin/http-webhook.d.ts +25 -0
  47. package/dist/channels/builtin/http-webhook.js +287 -0
  48. package/dist/channels/builtin/tui-channel.d.ts +33 -0
  49. package/dist/channels/builtin/tui-channel.js +61 -0
  50. package/dist/channels/builtin/tui-ws-session.d.ts +16 -0
  51. package/dist/channels/builtin/tui-ws-session.js +90 -0
  52. package/dist/channels/builtin/webui-output-handler.d.ts +30 -0
  53. package/dist/channels/builtin/webui-output-handler.js +93 -0
  54. package/dist/channels/index.d.ts +5 -0
  55. package/dist/channels/index.js +3 -0
  56. package/dist/channels/interface.d.ts +148 -0
  57. package/dist/channels/interface.js +15 -0
  58. package/dist/channels/manager.d.ts +43 -0
  59. package/dist/channels/manager.js +156 -0
  60. package/dist/channels/message-queue.d.ts +36 -0
  61. package/dist/channels/message-queue.js +91 -0
  62. package/dist/channels/plugins/clawbot/clawbot-auth.d.ts +50 -0
  63. package/dist/channels/plugins/clawbot/clawbot-auth.js +235 -0
  64. package/dist/channels/plugins/clawbot/clawbot-channel.d.ts +76 -0
  65. package/dist/channels/plugins/clawbot/clawbot-channel.js +566 -0
  66. package/dist/channels/plugins/clawbot/clawbot-client.d.ts +180 -0
  67. package/dist/channels/plugins/clawbot/clawbot-client.js +312 -0
  68. package/dist/channels/plugins/clawbot/clawbot-config.d.ts +30 -0
  69. package/dist/channels/plugins/clawbot/clawbot-config.js +28 -0
  70. package/dist/channels/plugins/clawbot/clawbot-message-queue.d.ts +27 -0
  71. package/dist/channels/plugins/clawbot/clawbot-message-queue.js +114 -0
  72. package/dist/channels/plugins/clawbot/clawbot-session.d.ts +10 -0
  73. package/dist/channels/plugins/clawbot/clawbot-session.js +26 -0
  74. package/dist/channels/plugins/clawbot/index.d.ts +17 -0
  75. package/dist/channels/plugins/clawbot/index.js +58 -0
  76. package/dist/channels/plugins/feishu/feishu-channel.d.ts +57 -0
  77. package/dist/channels/plugins/feishu/feishu-channel.js +602 -0
  78. package/dist/channels/plugins/feishu/feishu-client.d.ts +23 -0
  79. package/dist/channels/plugins/feishu/feishu-client.js +86 -0
  80. package/dist/channels/plugins/feishu/feishu-config.d.ts +53 -0
  81. package/dist/channels/plugins/feishu/feishu-config.js +35 -0
  82. package/dist/channels/plugins/feishu/feishu-event.d.ts +102 -0
  83. package/dist/channels/plugins/feishu/feishu-event.js +257 -0
  84. package/dist/channels/plugins/feishu/feishu-log-suppressor.d.ts +7 -0
  85. package/dist/channels/plugins/feishu/feishu-log-suppressor.js +34 -0
  86. package/dist/channels/plugins/feishu/feishu-message-queue.d.ts +27 -0
  87. package/dist/channels/plugins/feishu/feishu-message-queue.js +112 -0
  88. package/dist/channels/plugins/feishu/feishu-send.d.ts +44 -0
  89. package/dist/channels/plugins/feishu/feishu-send.js +232 -0
  90. package/dist/channels/plugins/feishu/feishu-session.d.ts +44 -0
  91. package/dist/channels/plugins/feishu/feishu-session.js +84 -0
  92. package/dist/channels/plugins/feishu/feishu-streaming.d.ts +49 -0
  93. package/dist/channels/plugins/feishu/feishu-streaming.js +185 -0
  94. package/dist/channels/plugins/feishu/feishu-transport.d.ts +29 -0
  95. package/dist/channels/plugins/feishu/feishu-transport.js +99 -0
  96. package/dist/channels/plugins/feishu/index.d.ts +22 -0
  97. package/dist/channels/plugins/feishu/index.js +68 -0
  98. package/dist/cli/doctor.d.ts +20 -0
  99. package/dist/cli/doctor.js +317 -0
  100. package/dist/context/cache-strategy.d.ts +76 -0
  101. package/dist/context/cache-strategy.js +123 -0
  102. package/dist/context/composer.d.ts +77 -0
  103. package/dist/context/composer.js +318 -0
  104. package/dist/context/compressor.d.ts +218 -0
  105. package/dist/context/compressor.js +909 -0
  106. package/dist/context/index.d.ts +14 -0
  107. package/dist/context/index.js +8 -0
  108. package/dist/context/interface.d.ts +33 -0
  109. package/dist/context/interface.js +2 -0
  110. package/dist/context/manifest-defaults.d.ts +51 -0
  111. package/dist/context/manifest-defaults.js +133 -0
  112. package/dist/context/manifest-loader.d.ts +16 -0
  113. package/dist/context/manifest-loader.js +103 -0
  114. package/dist/context/manifest-types.d.ts +26 -0
  115. package/dist/context/manifest-types.js +2 -0
  116. package/dist/context/precision/analyzer.d.ts +31 -0
  117. package/dist/context/precision/analyzer.js +131 -0
  118. package/dist/context/precision/companion.d.ts +27 -0
  119. package/dist/context/precision/companion.js +144 -0
  120. package/dist/context/precision/default.d.ts +14 -0
  121. package/dist/context/precision/default.js +17 -0
  122. package/dist/context/precision/index.d.ts +9 -0
  123. package/dist/context/precision/index.js +7 -0
  124. package/dist/context/precision/keyword-pool.d.ts +27 -0
  125. package/dist/context/precision/keyword-pool.js +66 -0
  126. package/dist/context/precision/strategy.d.ts +18 -0
  127. package/dist/context/precision/strategy.js +51 -0
  128. package/dist/context/precision/summary-store.d.ts +18 -0
  129. package/dist/context/precision/summary-store.js +67 -0
  130. package/dist/context/precision/types.d.ts +23 -0
  131. package/dist/context/precision/types.js +2 -0
  132. package/dist/context/profiles.d.ts +45 -0
  133. package/dist/context/profiles.js +92 -0
  134. package/dist/context/prompt-builder.d.ts +76 -0
  135. package/dist/context/prompt-builder.js +133 -0
  136. package/dist/context/retriever.d.ts +44 -0
  137. package/dist/context/retriever.js +203 -0
  138. package/dist/context/router.d.ts +122 -0
  139. package/dist/context/router.js +291 -0
  140. package/dist/context/section-resolver.d.ts +38 -0
  141. package/dist/context/section-resolver.js +398 -0
  142. package/dist/context/tokenizer.d.ts +23 -0
  143. package/dist/context/tokenizer.js +78 -0
  144. package/dist/dependency/analyzer.d.ts +33 -0
  145. package/dist/dependency/analyzer.js +242 -0
  146. package/dist/dependency/data-flow-tracker.d.ts +12 -0
  147. package/dist/dependency/data-flow-tracker.js +97 -0
  148. package/dist/dependency/function-parser.d.ts +34 -0
  149. package/dist/dependency/function-parser.js +295 -0
  150. package/dist/dependency/index.d.ts +14 -0
  151. package/dist/dependency/index.js +20 -0
  152. package/dist/dependency/parser.d.ts +10 -0
  153. package/dist/dependency/parser.js +93 -0
  154. package/dist/dependency/types.d.ts +57 -0
  155. package/dist/dependency/types.js +2 -0
  156. package/dist/env/env-collector.d.ts +45 -0
  157. package/dist/env/env-collector.js +221 -0
  158. package/dist/env/index.d.ts +3 -0
  159. package/dist/env/index.js +2 -0
  160. package/dist/event-store.d.ts +27 -0
  161. package/dist/event-store.js +71 -0
  162. package/dist/evolution/git-manager.d.ts +46 -0
  163. package/dist/evolution/git-manager.js +276 -0
  164. package/dist/evolution/index.d.ts +3 -0
  165. package/dist/evolution/index.js +2 -0
  166. package/dist/flow/bootstrap-flow.d.ts +19 -0
  167. package/dist/flow/bootstrap-flow.js +72 -0
  168. package/dist/flow/flow-registry.d.ts +12 -0
  169. package/dist/flow/flow-registry.js +74 -0
  170. package/dist/flow/flow-tools.d.ts +8 -0
  171. package/dist/flow/flow-tools.js +106 -0
  172. package/dist/flow/index.d.ts +6 -0
  173. package/dist/flow/index.js +8 -0
  174. package/dist/flow/todo-flow.d.ts +25 -0
  175. package/dist/flow/todo-flow.js +107 -0
  176. package/dist/flow/types.d.ts +78 -0
  177. package/dist/flow/types.js +12 -0
  178. package/dist/flows/bootstrap.d.ts +18 -0
  179. package/dist/flows/bootstrap.js +119 -0
  180. package/dist/flows/index.d.ts +6 -0
  181. package/dist/flows/index.js +11 -0
  182. package/dist/flows/todo.d.ts +26 -0
  183. package/dist/flows/todo.js +174 -0
  184. package/dist/flows/tools.d.ts +8 -0
  185. package/dist/flows/tools.js +119 -0
  186. package/dist/flows/types.d.ts +27 -0
  187. package/dist/flows/types.js +10 -0
  188. package/dist/gateway/cli.d.ts +5 -0
  189. package/dist/gateway/cli.js +1027 -0
  190. package/dist/gateway/factory.d.ts +80 -0
  191. package/dist/gateway/factory.js +832 -0
  192. package/dist/gateway/guardian.d.ts +8 -0
  193. package/dist/gateway/guardian.js +30 -0
  194. package/dist/gateway/index.d.ts +3 -0
  195. package/dist/gateway/index.js +3 -0
  196. package/dist/gateway/server.d.ts +17 -0
  197. package/dist/gateway/server.js +96 -0
  198. package/dist/gateway/tui.d.ts +16 -0
  199. package/dist/gateway/tui.js +3273 -0
  200. package/dist/hot-reload/agent-watcher.d.ts +24 -0
  201. package/dist/hot-reload/agent-watcher.js +175 -0
  202. package/dist/hot-reload/bundle-watcher.d.ts +12 -0
  203. package/dist/hot-reload/bundle-watcher.js +35 -0
  204. package/dist/hot-reload/channel-watcher.d.ts +19 -0
  205. package/dist/hot-reload/channel-watcher.js +54 -0
  206. package/dist/hot-reload/command-watcher.d.ts +14 -0
  207. package/dist/hot-reload/command-watcher.js +54 -0
  208. package/dist/hot-reload/config-watcher.d.ts +20 -0
  209. package/dist/hot-reload/config-watcher.js +58 -0
  210. package/dist/hot-reload/index.d.ts +12 -0
  211. package/dist/hot-reload/index.js +12 -0
  212. package/dist/hot-reload/manager.d.ts +58 -0
  213. package/dist/hot-reload/manager.js +243 -0
  214. package/dist/hot-reload/manifest-watcher.d.ts +9 -0
  215. package/dist/hot-reload/manifest-watcher.js +42 -0
  216. package/dist/hot-reload/mcp-watcher.d.ts +18 -0
  217. package/dist/hot-reload/mcp-watcher.js +64 -0
  218. package/dist/hot-reload/model-catalog-watcher.d.ts +18 -0
  219. package/dist/hot-reload/model-catalog-watcher.js +62 -0
  220. package/dist/hot-reload/plugin-watcher.d.ts +20 -0
  221. package/dist/hot-reload/plugin-watcher.js +56 -0
  222. package/dist/hot-reload/prompt-watcher.d.ts +6 -0
  223. package/dist/hot-reload/prompt-watcher.js +38 -0
  224. package/dist/hot-reload/provider-watcher.d.ts +16 -0
  225. package/dist/hot-reload/provider-watcher.js +45 -0
  226. package/dist/hot-reload/skill-watcher.d.ts +19 -0
  227. package/dist/hot-reload/skill-watcher.js +82 -0
  228. package/dist/hot-reload/tool-watcher.d.ts +16 -0
  229. package/dist/hot-reload/tool-watcher.js +168 -0
  230. package/dist/hot-reload/workflow-watcher.d.ts +19 -0
  231. package/dist/hot-reload/workflow-watcher.js +84 -0
  232. package/dist/index.d.ts +3 -0
  233. package/dist/index.js +6 -0
  234. package/dist/knowledge/fts5-retriever.d.ts +30 -0
  235. package/dist/knowledge/fts5-retriever.js +223 -0
  236. package/dist/knowledge/index.d.ts +11 -0
  237. package/dist/knowledge/index.js +14 -0
  238. package/dist/knowledge/indexer.d.ts +41 -0
  239. package/dist/knowledge/indexer.js +186 -0
  240. package/dist/knowledge/query-preprocessor.d.ts +13 -0
  241. package/dist/knowledge/query-preprocessor.js +63 -0
  242. package/dist/knowledge/retriever.d.ts +45 -0
  243. package/dist/knowledge/retriever.js +9 -0
  244. package/dist/knowledge/store.d.ts +27 -0
  245. package/dist/knowledge/store.js +61 -0
  246. package/dist/knowledge/structured-store.d.ts +78 -0
  247. package/dist/knowledge/structured-store.js +280 -0
  248. package/dist/knowledge/structured-tools.d.ts +12 -0
  249. package/dist/knowledge/structured-tools.js +151 -0
  250. package/dist/knowledge/tools.d.ts +20 -0
  251. package/dist/knowledge/tools.js +238 -0
  252. package/dist/knowledge/watcher.d.ts +28 -0
  253. package/dist/knowledge/watcher.js +113 -0
  254. package/dist/lifecycle/index.d.ts +12 -0
  255. package/dist/lifecycle/index.js +14 -0
  256. package/dist/lifecycle/interface.d.ts +94 -0
  257. package/dist/lifecycle/interface.js +2 -0
  258. package/dist/lifecycle/local-model.d.ts +62 -0
  259. package/dist/lifecycle/local-model.js +331 -0
  260. package/dist/lifecycle/manager.d.ts +70 -0
  261. package/dist/lifecycle/manager.js +374 -0
  262. package/dist/lifecycle/supervisor.d.ts +89 -0
  263. package/dist/lifecycle/supervisor.js +277 -0
  264. package/dist/lifecycle-integration-test.d.ts +2 -0
  265. package/dist/lifecycle-integration-test.js +72 -0
  266. package/dist/local-model/download-manager.d.ts +59 -0
  267. package/dist/local-model/download-manager.js +517 -0
  268. package/dist/local-model/index.d.ts +109 -0
  269. package/dist/local-model/index.js +233 -0
  270. package/dist/local-model/llamacpp-provider.d.ts +95 -0
  271. package/dist/local-model/llamacpp-provider.js +379 -0
  272. package/dist/local-model/model-bridge.d.ts +48 -0
  273. package/dist/local-model/model-bridge.js +256 -0
  274. package/dist/local-model/model-registry.d.ts +47 -0
  275. package/dist/local-model/model-registry.js +188 -0
  276. package/dist/local-model/types.d.ts +49 -0
  277. package/dist/local-model/types.js +2 -0
  278. package/dist/logging/index.d.ts +3 -0
  279. package/dist/logging/index.js +3 -0
  280. package/dist/logging/logger.d.ts +56 -0
  281. package/dist/logging/logger.js +94 -0
  282. package/dist/machine/flows/bootstrap.d.ts +18 -0
  283. package/dist/machine/flows/bootstrap.js +119 -0
  284. package/dist/machine/flows/index.d.ts +6 -0
  285. package/dist/machine/flows/index.js +11 -0
  286. package/dist/machine/flows/spec.d.ts +17 -0
  287. package/dist/machine/flows/spec.js +176 -0
  288. package/dist/machine/flows/todo.d.ts +26 -0
  289. package/dist/machine/flows/todo.js +175 -0
  290. package/dist/machine/flows/tools.d.ts +8 -0
  291. package/dist/machine/flows/tools.js +119 -0
  292. package/dist/machine/flows/types.d.ts +33 -0
  293. package/dist/machine/flows/types.js +10 -0
  294. package/dist/machine/index.d.ts +8 -0
  295. package/dist/machine/index.js +9 -0
  296. package/dist/machine/registry.d.ts +34 -0
  297. package/dist/machine/registry.js +98 -0
  298. package/dist/machine/runner.d.ts +41 -0
  299. package/dist/machine/runner.js +177 -0
  300. package/dist/machine/types.d.ts +121 -0
  301. package/dist/machine/types.js +9 -0
  302. package/dist/mcp/bridge.d.ts +33 -0
  303. package/dist/mcp/bridge.js +99 -0
  304. package/dist/mcp/client.d.ts +37 -0
  305. package/dist/mcp/client.js +161 -0
  306. package/dist/mcp/config.d.ts +15 -0
  307. package/dist/mcp/config.js +85 -0
  308. package/dist/mcp/index.d.ts +7 -0
  309. package/dist/mcp/index.js +7 -0
  310. package/dist/mcp/install-manager.d.ts +31 -0
  311. package/dist/mcp/install-manager.js +158 -0
  312. package/dist/mcp/lifecycle.d.ts +41 -0
  313. package/dist/mcp/lifecycle.js +175 -0
  314. package/dist/mcp/process-transport.d.ts +21 -0
  315. package/dist/mcp/process-transport.js +59 -0
  316. package/dist/mcp/side-effect.d.ts +5 -0
  317. package/dist/mcp/side-effect.js +21 -0
  318. package/dist/mcp/system.d.ts +95 -0
  319. package/dist/mcp/system.js +279 -0
  320. package/dist/memory/companion-session.d.ts +22 -0
  321. package/dist/memory/companion-session.js +77 -0
  322. package/dist/memory/compression-boundary.d.ts +20 -0
  323. package/dist/memory/compression-boundary.js +40 -0
  324. package/dist/memory/conversation.d.ts +34 -0
  325. package/dist/memory/conversation.js +107 -0
  326. package/dist/memory/events.d.ts +42 -0
  327. package/dist/memory/events.js +56 -0
  328. package/dist/memory/index.d.ts +7 -0
  329. package/dist/memory/index.js +6 -0
  330. package/dist/memory/memory-store.d.ts +11 -0
  331. package/dist/memory/memory-store.js +51 -0
  332. package/dist/memory/session-allowlist.d.ts +11 -0
  333. package/dist/memory/session-allowlist.js +50 -0
  334. package/dist/memory/session.d.ts +48 -0
  335. package/dist/memory/session.js +275 -0
  336. package/dist/memory/stats.d.ts +23 -0
  337. package/dist/memory/stats.js +59 -0
  338. package/dist/memory/summary.d.ts +7 -0
  339. package/dist/memory/summary.js +23 -0
  340. package/dist/multimodal/index.d.ts +66 -0
  341. package/dist/multimodal/index.js +210 -0
  342. package/dist/orchestrator/index.d.ts +5 -0
  343. package/dist/orchestrator/index.js +4 -0
  344. package/dist/orchestrator/loop.d.ts +331 -0
  345. package/dist/orchestrator/loop.js +2443 -0
  346. package/dist/orchestrator/plan-store.d.ts +81 -0
  347. package/dist/orchestrator/plan-store.js +167 -0
  348. package/dist/orchestrator/planner.d.ts +20 -0
  349. package/dist/orchestrator/planner.js +34 -0
  350. package/dist/parser/index.d.ts +2 -0
  351. package/dist/parser/index.js +2 -0
  352. package/dist/parser/router.d.ts +30 -0
  353. package/dist/parser/router.js +61 -0
  354. package/dist/plugins/api.d.ts +39 -0
  355. package/dist/plugins/api.js +85 -0
  356. package/dist/plugins/define.d.ts +27 -0
  357. package/dist/plugins/define.js +32 -0
  358. package/dist/plugins/index.d.ts +9 -0
  359. package/dist/plugins/index.js +5 -0
  360. package/dist/plugins/loader.d.ts +39 -0
  361. package/dist/plugins/loader.js +128 -0
  362. package/dist/plugins/manager.d.ts +61 -0
  363. package/dist/plugins/manager.js +257 -0
  364. package/dist/plugins/types.d.ts +82 -0
  365. package/dist/plugins/types.js +2 -0
  366. package/dist/prompts/agents/code-reviewer.md +16 -0
  367. package/dist/prompts/agents/security-auditor.md +17 -0
  368. package/dist/prompts/agents/test-writer.md +15 -0
  369. package/dist/prompts/attention/attention.md +12 -0
  370. package/dist/prompts/environment/environment.md +18 -0
  371. package/dist/prompts/environment/feishu.md +107 -0
  372. package/dist/prompts/flows/bootstrap-confirm.md +14 -0
  373. package/dist/prompts/flows/bootstrap-identity.md +11 -0
  374. package/dist/prompts/flows/bootstrap-soul.md +19 -0
  375. package/dist/prompts/flows/bootstrap-user.md +11 -0
  376. package/dist/prompts/flows/spec-checklist.md +27 -0
  377. package/dist/prompts/flows/spec-define.md +64 -0
  378. package/dist/prompts/flows/spec-executing-checklist.md +15 -0
  379. package/dist/prompts/flows/spec-executing-tasks.md +15 -0
  380. package/dist/prompts/flows/spec-spec.md +51 -0
  381. package/dist/prompts/flows/spec-tasks.md +32 -0
  382. package/dist/prompts/flows/todo-execution.md +7 -0
  383. package/dist/prompts/flows/todo-planning.md +18 -0
  384. package/dist/prompts/framework/architecture.md +131 -0
  385. package/dist/prompts/identity/identity.md +2 -0
  386. package/dist/prompts/loader.d.ts +24 -0
  387. package/dist/prompts/loader.js +102 -0
  388. package/dist/prompts/loader.ts +119 -0
  389. package/dist/prompts/orchestration/summary.md +14 -0
  390. package/dist/prompts/persona/BOOTSTRAP.md +5 -0
  391. package/dist/prompts/persona/IDENTITY.md +16 -0
  392. package/dist/prompts/persona/PartnerMemory.md +5 -0
  393. package/dist/prompts/persona/PartnerSoul.md +7 -0
  394. package/dist/prompts/persona/SOUL.md +30 -0
  395. package/dist/prompts/persona/USER.md +16 -0
  396. package/dist/prompts/precise/analyze.md +23 -0
  397. package/dist/prompts/precise/persona.md +18 -0
  398. package/dist/prompts/safety/coding-standards.md +5 -0
  399. package/dist/prompts/safety/safety.md +4 -0
  400. package/dist/prompts/skills/code-review.md +4 -0
  401. package/dist/prompts/skills/debug.md +7 -0
  402. package/dist/prompts/skills/framework-reference.md +131 -0
  403. package/dist/prompts/skills/refactor.md +6 -0
  404. package/dist/prompts/summary.md +22 -0
  405. package/dist/prompts/tools/framework-capabilities.md +241 -0
  406. package/dist/prompts/tools/tool-rules.md +15 -0
  407. package/dist/provider/anthropic.d.ts +62 -0
  408. package/dist/provider/anthropic.js +361 -0
  409. package/dist/provider/catalog.d.ts +58 -0
  410. package/dist/provider/catalog.js +97 -0
  411. package/dist/provider/compatible.d.ts +89 -0
  412. package/dist/provider/compatible.js +328 -0
  413. package/dist/provider/config.d.ts +23 -0
  414. package/dist/provider/config.js +158 -0
  415. package/dist/provider/deepseek.d.ts +25 -0
  416. package/dist/provider/deepseek.js +37 -0
  417. package/dist/provider/fallback.d.ts +51 -0
  418. package/dist/provider/fallback.js +113 -0
  419. package/dist/provider/gemini.d.ts +27 -0
  420. package/dist/provider/gemini.js +170 -0
  421. package/dist/provider/index.d.ts +36 -0
  422. package/dist/provider/index.js +42 -0
  423. package/dist/provider/interface.d.ts +62 -0
  424. package/dist/provider/interface.js +2 -0
  425. package/dist/provider/local-config.d.ts +37 -0
  426. package/dist/provider/local-config.js +146 -0
  427. package/dist/provider/local.d.ts +45 -0
  428. package/dist/provider/local.js +248 -0
  429. package/dist/provider/manager.d.ts +67 -0
  430. package/dist/provider/manager.js +355 -0
  431. package/dist/provider/mimo.d.ts +19 -0
  432. package/dist/provider/mimo.js +30 -0
  433. package/dist/provider/minimax.d.ts +22 -0
  434. package/dist/provider/minimax.js +33 -0
  435. package/dist/provider/model-catalog-loader.d.ts +42 -0
  436. package/dist/provider/model-catalog-loader.js +89 -0
  437. package/dist/provider/model-channel-registry.d.ts +130 -0
  438. package/dist/provider/model-channel-registry.js +488 -0
  439. package/dist/provider/model-router.d.ts +60 -0
  440. package/dist/provider/model-router.js +87 -0
  441. package/dist/provider/openai.d.ts +54 -0
  442. package/dist/provider/openai.js +274 -0
  443. package/dist/provider/qwen.d.ts +19 -0
  444. package/dist/provider/qwen.js +30 -0
  445. package/dist/provider/resilient.d.ts +46 -0
  446. package/dist/provider/resilient.js +181 -0
  447. package/dist/provider/retry.d.ts +16 -0
  448. package/dist/provider/retry.js +78 -0
  449. package/dist/provider/router.d.ts +68 -0
  450. package/dist/provider/router.js +145 -0
  451. package/dist/provider/tool-args-recovery.d.ts +36 -0
  452. package/dist/provider/tool-args-recovery.js +91 -0
  453. package/dist/provider/user-id.d.ts +13 -0
  454. package/dist/provider/user-id.js +53 -0
  455. package/dist/provider/zhipu.d.ts +19 -0
  456. package/dist/provider/zhipu.js +32 -0
  457. package/dist/registry/agent.registry.d.ts +57 -0
  458. package/dist/registry/agent.registry.js +224 -0
  459. package/dist/registry/base.d.ts +54 -0
  460. package/dist/registry/base.js +99 -0
  461. package/dist/registry/channel.registry.d.ts +53 -0
  462. package/dist/registry/channel.registry.js +87 -0
  463. package/dist/registry/index.d.ts +9 -0
  464. package/dist/registry/index.js +10 -0
  465. package/dist/registry/mcp.registry.d.ts +2 -0
  466. package/dist/registry/mcp.registry.js +2 -0
  467. package/dist/registry/plugin.registry.d.ts +33 -0
  468. package/dist/registry/plugin.registry.js +66 -0
  469. package/dist/registry/provider.registry.d.ts +51 -0
  470. package/dist/registry/provider.registry.js +104 -0
  471. package/dist/registry/skill.registry.d.ts +33 -0
  472. package/dist/registry/skill.registry.js +95 -0
  473. package/dist/registry/tool.registry.d.ts +68 -0
  474. package/dist/registry/tool.registry.js +185 -0
  475. package/dist/repair/loop-guard.d.ts +98 -0
  476. package/dist/repair/loop-guard.js +217 -0
  477. package/dist/repair/scavenge.d.ts +19 -0
  478. package/dist/repair/scavenge.js +81 -0
  479. package/dist/repair/storm.d.ts +7 -0
  480. package/dist/repair/storm.js +7 -0
  481. package/dist/rollback/index.d.ts +5 -0
  482. package/dist/rollback/index.js +4 -0
  483. package/dist/rollback/rollback-tool.d.ts +13 -0
  484. package/dist/rollback/rollback-tool.js +167 -0
  485. package/dist/rollback/turn-recorder.d.ts +37 -0
  486. package/dist/rollback/turn-recorder.js +183 -0
  487. package/dist/rollback/turn-store.d.ts +29 -0
  488. package/dist/rollback/turn-store.js +114 -0
  489. package/dist/rollback/types.d.ts +51 -0
  490. package/dist/rollback/types.js +9 -0
  491. package/dist/runtime/config-center.d.ts +92 -0
  492. package/dist/runtime/config-center.js +467 -0
  493. package/dist/runtime/config-schema.d.ts +201 -0
  494. package/dist/runtime/config-schema.js +2 -0
  495. package/dist/runtime/defaults.d.ts +3 -0
  496. package/dist/runtime/defaults.js +146 -0
  497. package/dist/schedule/cron.d.ts +24 -0
  498. package/dist/schedule/cron.js +145 -0
  499. package/dist/schedule/index.d.ts +6 -0
  500. package/dist/schedule/index.js +4 -0
  501. package/dist/schedule/persistence.d.ts +33 -0
  502. package/dist/schedule/persistence.js +106 -0
  503. package/dist/schedule/scheduler.d.ts +85 -0
  504. package/dist/schedule/scheduler.js +630 -0
  505. package/dist/schedule/types.d.ts +185 -0
  506. package/dist/schedule/types.js +2 -0
  507. package/dist/setup/config.d.ts +189 -0
  508. package/dist/setup/config.js +253 -0
  509. package/dist/setup/index.d.ts +3 -0
  510. package/dist/setup/index.js +3 -0
  511. package/dist/setup/model-defaults.d.ts +13 -0
  512. package/dist/setup/model-defaults.js +87 -0
  513. package/dist/setup/persona-bootstrap.d.ts +42 -0
  514. package/dist/setup/persona-bootstrap.js +379 -0
  515. package/dist/setup/wizard.d.ts +32 -0
  516. package/dist/setup/wizard.js +384 -0
  517. package/dist/skills/index.d.ts +4 -0
  518. package/dist/skills/index.js +4 -0
  519. package/dist/skills/loader.d.ts +12 -0
  520. package/dist/skills/loader.js +83 -0
  521. package/dist/skills/registry.d.ts +2 -0
  522. package/dist/skills/registry.js +2 -0
  523. package/dist/skills/skill-tool.d.ts +20 -0
  524. package/dist/skills/skill-tool.js +49 -0
  525. package/dist/tools/archive.d.ts +17 -0
  526. package/dist/tools/archive.js +131 -0
  527. package/dist/tools/ask-user.d.ts +9 -0
  528. package/dist/tools/ask-user.js +62 -0
  529. package/dist/tools/background-registry.d.ts +45 -0
  530. package/dist/tools/background-registry.js +164 -0
  531. package/dist/tools/bash.d.ts +56 -0
  532. package/dist/tools/bash.js +391 -0
  533. package/dist/tools/bundle-registry.d.ts +42 -0
  534. package/dist/tools/bundle-registry.js +239 -0
  535. package/dist/tools/bundle-tools.d.ts +11 -0
  536. package/dist/tools/bundle-tools.js +168 -0
  537. package/dist/tools/channel-info.d.ts +5 -0
  538. package/dist/tools/channel-info.js +38 -0
  539. package/dist/tools/compression.d.ts +7 -0
  540. package/dist/tools/compression.js +21 -0
  541. package/dist/tools/config.d.ts +23 -0
  542. package/dist/tools/config.js +255 -0
  543. package/dist/tools/db-query.d.ts +12 -0
  544. package/dist/tools/db-query.js +107 -0
  545. package/dist/tools/diagnostics.d.ts +30 -0
  546. package/dist/tools/diagnostics.js +111 -0
  547. package/dist/tools/diff-channel.d.ts +9 -0
  548. package/dist/tools/diff-channel.js +17 -0
  549. package/dist/tools/diff-files.d.ts +17 -0
  550. package/dist/tools/diff-files.js +88 -0
  551. package/dist/tools/disk-usage.d.ts +34 -0
  552. package/dist/tools/disk-usage.js +258 -0
  553. package/dist/tools/edit.d.ts +30 -0
  554. package/dist/tools/edit.js +235 -0
  555. package/dist/tools/executor.d.ts +32 -0
  556. package/dist/tools/executor.js +66 -0
  557. package/dist/tools/file-tracker.d.ts +20 -0
  558. package/dist/tools/file-tracker.js +44 -0
  559. package/dist/tools/filtered-registry.d.ts +29 -0
  560. package/dist/tools/filtered-registry.js +50 -0
  561. package/dist/tools/flow.d.ts +10 -0
  562. package/dist/tools/flow.js +180 -0
  563. package/dist/tools/git-tool.d.ts +24 -0
  564. package/dist/tools/git-tool.js +113 -0
  565. package/dist/tools/glob.d.ts +29 -0
  566. package/dist/tools/glob.js +152 -0
  567. package/dist/tools/grep.d.ts +14 -0
  568. package/dist/tools/grep.js +325 -0
  569. package/dist/tools/http-request.d.ts +14 -0
  570. package/dist/tools/http-request.js +126 -0
  571. package/dist/tools/index.d.ts +43 -0
  572. package/dist/tools/index.js +76 -0
  573. package/dist/tools/injection-filter.d.ts +6 -0
  574. package/dist/tools/injection-filter.js +71 -0
  575. package/dist/tools/insert.d.ts +13 -0
  576. package/dist/tools/insert.js +61 -0
  577. package/dist/tools/interface.d.ts +30 -0
  578. package/dist/tools/interface.js +2 -0
  579. package/dist/tools/json-edit.d.ts +15 -0
  580. package/dist/tools/json-edit.js +122 -0
  581. package/dist/tools/multi-edit.d.ts +9 -0
  582. package/dist/tools/multi-edit.js +146 -0
  583. package/dist/tools/path-sandbox.d.ts +14 -0
  584. package/dist/tools/path-sandbox.js +60 -0
  585. package/dist/tools/process-tools.d.ts +15 -0
  586. package/dist/tools/process-tools.js +95 -0
  587. package/dist/tools/python-bridge/bridge.d.ts +24 -0
  588. package/dist/tools/python-bridge/bridge.js +39 -0
  589. package/dist/tools/python-bridge/executor.d.ts +6 -0
  590. package/dist/tools/python-bridge/executor.js +69 -0
  591. package/dist/tools/python-bridge/index.d.ts +5 -0
  592. package/dist/tools/python-bridge/index.js +4 -0
  593. package/dist/tools/python-bridge/parser.d.ts +21 -0
  594. package/dist/tools/python-bridge/parser.js +258 -0
  595. package/dist/tools/read.d.ts +16 -0
  596. package/dist/tools/read.js +276 -0
  597. package/dist/tools/registry.d.ts +2 -0
  598. package/dist/tools/registry.js +2 -0
  599. package/dist/tools/restart.d.ts +10 -0
  600. package/dist/tools/restart.js +46 -0
  601. package/dist/tools/result-buffer.d.ts +66 -0
  602. package/dist/tools/result-buffer.js +146 -0
  603. package/dist/tools/runtime-control.d.ts +163 -0
  604. package/dist/tools/runtime-control.js +1780 -0
  605. package/dist/tools/symbol-references.d.ts +5 -0
  606. package/dist/tools/symbol-references.js +331 -0
  607. package/dist/tools/system-info.d.ts +3 -0
  608. package/dist/tools/system-info.js +55 -0
  609. package/dist/tools/write.d.ts +18 -0
  610. package/dist/tools/write.js +118 -0
  611. package/dist/tools/xref/index.d.ts +25 -0
  612. package/dist/tools/xref/index.js +25 -0
  613. package/dist/tools/xref/manager.d.ts +85 -0
  614. package/dist/tools/xref/manager.js +948 -0
  615. package/dist/tools/xref/parser.d.ts +34 -0
  616. package/dist/tools/xref/parser.js +64 -0
  617. package/dist/tools/xref/regex-parser.d.ts +26 -0
  618. package/dist/tools/xref/regex-parser.js +446 -0
  619. package/dist/tools/xref/schema.d.ts +104 -0
  620. package/dist/tools/xref/schema.js +68 -0
  621. package/dist/tools/xref/ts-parser.d.ts +20 -0
  622. package/dist/tools/xref/ts-parser.js +403 -0
  623. package/dist/tools/xref/xref-build.d.ts +20 -0
  624. package/dist/tools/xref/xref-build.js +128 -0
  625. package/dist/tools/xref/xref-graph.d.ts +24 -0
  626. package/dist/tools/xref/xref-graph.js +88 -0
  627. package/dist/tools/xref/xref-query.d.ts +30 -0
  628. package/dist/tools/xref/xref-query.js +123 -0
  629. package/dist/types.d.ts +217 -0
  630. package/dist/types.js +2 -0
  631. package/dist/ui/assistant-message.d.ts +7 -0
  632. package/dist/ui/assistant-message.js +18 -0
  633. package/dist/ui/chat-log.d.ts +52 -0
  634. package/dist/ui/chat-log.js +246 -0
  635. package/dist/ui/command-registry.d.ts +73 -0
  636. package/dist/ui/command-registry.js +838 -0
  637. package/dist/ui/diff-component.d.ts +14 -0
  638. package/dist/ui/diff-component.js +42 -0
  639. package/dist/ui/hyperlink-markdown.d.ts +10 -0
  640. package/dist/ui/hyperlink-markdown.js +21 -0
  641. package/dist/ui/markdown-message.d.ts +10 -0
  642. package/dist/ui/markdown-message.js +16 -0
  643. package/dist/ui/osc8-hyperlinks.d.ts +4 -0
  644. package/dist/ui/osc8-hyperlinks.js +142 -0
  645. package/dist/ui/pi-tui-editor.d.ts +35 -0
  646. package/dist/ui/pi-tui-editor.js +79 -0
  647. package/dist/ui/slash-commands.d.ts +16 -0
  648. package/dist/ui/slash-commands.js +29 -0
  649. package/dist/ui/slash-panel.d.ts +54 -0
  650. package/dist/ui/slash-panel.js +199 -0
  651. package/dist/ui/theme.d.ts +120 -0
  652. package/dist/ui/theme.js +154 -0
  653. package/dist/ui/tool-display.d.ts +13 -0
  654. package/dist/ui/tool-display.js +85 -0
  655. package/dist/ui/tool-execution.d.ts +22 -0
  656. package/dist/ui/tool-execution.js +87 -0
  657. package/dist/ui/tui-formatters.d.ts +2 -0
  658. package/dist/ui/tui-formatters.js +145 -0
  659. package/dist/ui/user-message.d.ts +5 -0
  660. package/dist/ui/user-message.js +11 -0
  661. package/dist/update/check.d.ts +8 -0
  662. package/dist/update/check.js +28 -0
  663. package/dist/update/config.d.ts +6 -0
  664. package/dist/update/config.js +18 -0
  665. package/dist/update/download.d.ts +3 -0
  666. package/dist/update/download.js +27 -0
  667. package/dist/update/index.d.ts +6 -0
  668. package/dist/update/index.js +5 -0
  669. package/dist/update/install.d.ts +3 -0
  670. package/dist/update/install.js +33 -0
  671. package/dist/update/types.d.ts +16 -0
  672. package/dist/update/types.js +2 -0
  673. package/dist/utils/diff.d.ts +9 -0
  674. package/dist/utils/diff.js +45 -0
  675. package/dist/utils/misc.d.ts +12 -0
  676. package/dist/utils/misc.js +34 -0
  677. package/dist/world-engine/agent.d.ts +41 -0
  678. package/dist/world-engine/agent.js +385 -0
  679. package/dist/world-engine/index.d.ts +45 -0
  680. package/dist/world-engine/index.js +160 -0
  681. package/dist/world-engine/sim.d.ts +14 -0
  682. package/dist/world-engine/sim.js +120 -0
  683. package/dist/world-engine/store.d.ts +110 -0
  684. package/dist/world-engine/store.js +519 -0
  685. package/dist/world-engine/ticker.d.ts +37 -0
  686. package/dist/world-engine/ticker.js +214 -0
  687. package/dist/world-engine/tools.d.ts +6 -0
  688. package/dist/world-engine/tools.js +380 -0
  689. package/dist/world-engine/types.d.ts +202 -0
  690. package/dist/world-engine/types.js +56 -0
  691. package/docs/api.md +180 -0
  692. package/docs/architecture.md +206 -0
  693. package/docs/diagnostics/2026-06-08-config-prompt-guidance.md +67 -0
  694. package/docs/superpowers/plans/2026-06-08-global-bootstrap-mode.md +89 -0
  695. package/docs/superpowers/specs/2026-06-08-global-bootstrap-mode-design.md +174 -0
  696. package/docs/user-guide.md +226 -0
  697. package/package.json +63 -0
  698. package/plugins/example-greeter/index.js +55 -0
  699. package/plugins/example-greeter/plugin.json +8 -0
  700. package/pnpm-workspace.yaml +5 -0
  701. package/provider_arch.md +179 -0
  702. package/start-webui.sh +27 -0
  703. package/tsconfig.tsbuildinfo +1 -0
  704. package/webui-channel-design.md +789 -0
@@ -0,0 +1,2443 @@
1
+ import { ProviderManager } from '../provider/manager.js';
2
+ import { getModelInfo } from '../provider/catalog.js';
3
+ import { LocalProvider } from '../provider/local.js';
4
+ import { appendEvent } from '../event-store.js';
5
+ import { OutputRouter } from '../parser/router.js';
6
+ import { formatPlanAsText } from './plan-store.js';
7
+ import path from 'node:path';
8
+ import crypto from 'node:crypto';
9
+ import { ImageStore, buildUserContentWithImages, buildUserContentWithInlineImages, createViewImageTool } from '../multimodal/index.js';
10
+ import { createLogger } from '../logging/logger.js';
11
+ import { isFlowTool } from '../tools/flow.js';
12
+ import { DEFAULT_MAX_CONTEXT_TOKENS } from '../setup/config.js';
13
+ import { getDefaultConfig } from '../runtime/defaults.js';
14
+ import { mainUserId } from '../provider/user-id.js';
15
+ import { getModelContextWindow } from '../setup/model-defaults.js';
16
+ import { LoopGuard, isMutating, ToolGuard } from '../repair/loop-guard.js';
17
+ import { scavengeToolCalls } from '../repair/scavenge.js';
18
+ import { ToolResultBuffer } from '../tools/result-buffer.js';
19
+ import { sanitizeToolResult } from '../tools/injection-filter.js';
20
+ import { getActiveProfile, getActiveRouter } from '../context/profiles.js';
21
+ import { NormalRouter } from '../context/router.js';
22
+ import { GitManager } from '../evolution/git-manager.js';
23
+ import { extractTextContent } from '../utils/misc.js';
24
+ import * as sessionAllowlist from '../memory/session-allowlist.js';
25
+ /** Format a Date as YYYY-MM-DD HH:mm (cache-friendly, minute precision) */
26
+ function formatTimestamp(date = new Date()) {
27
+ const pad = (n) => String(n).padStart(2, '0');
28
+ const y = date.getFullYear();
29
+ const m = pad(date.getMonth() + 1);
30
+ const d = pad(date.getDate());
31
+ const h = pad(date.getHours());
32
+ const min = pad(date.getMinutes());
33
+ return `${y}-${m}-${d} ${h}:${min}`;
34
+ }
35
+ /** 从末尾累加消息,直到累计 token 数超过 budget,返回保护条数 */
36
+ function computeProtectCount(messages, tokenBudget) {
37
+ // 使用简化的 char/4 估算
38
+ let tokens = 0;
39
+ let count = 0;
40
+ for (let i = messages.length - 1; i >= 0; i--) {
41
+ const rawContent = messages[i].content;
42
+ const text = typeof rawContent === 'string'
43
+ ? rawContent
44
+ : JSON.stringify(rawContent);
45
+ tokens += Math.ceil(text.length / 4) + 4; // +4 for role/overhead
46
+ count++;
47
+ if (tokens >= tokenBudget)
48
+ break;
49
+ }
50
+ return Math.max(2, count); // 至少保护 2 条
51
+ }
52
+ /** 判断两条消息是否具有相同的 role 和 text content */
53
+ function isSameTextMessage(a, b) {
54
+ if (a.role !== b.role)
55
+ return false;
56
+ const aContents = Array.isArray(a.content) ? a.content : [a.content];
57
+ const bContents = Array.isArray(b.content) ? b.content : [b.content];
58
+ if (aContents.length !== bContents.length)
59
+ return false;
60
+ for (let i = 0; i < aContents.length; i++) {
61
+ const ac = aContents[i];
62
+ const bc = bContents[i];
63
+ if (ac.type !== bc.type)
64
+ return false;
65
+ if (ac.type === 'text') {
66
+ if (ac.text !== bc.text)
67
+ return false;
68
+ }
69
+ else if (ac.type === 'image') {
70
+ const aSrc = ac.source;
71
+ const bSrc = bc.source;
72
+ if (aSrc?.type !== bSrc?.type)
73
+ return false;
74
+ if (aSrc?.type === 'base64' && aSrc?.data !== bSrc?.data)
75
+ return false;
76
+ if (aSrc?.type === 'url' && aSrc?.url !== bSrc?.url)
77
+ return false;
78
+ }
79
+ else {
80
+ // For other types (tool_use, tool_result, thinking), compare serialized
81
+ if (JSON.stringify(ac) !== JSON.stringify(bc))
82
+ return false;
83
+ }
84
+ }
85
+ return true;
86
+ }
87
+ // ── 半块 Unicode 字符画 ─────────────────────────────────────────────
88
+ // 终端字符宽高比约 1:2,用 ▀▄█ 每个字符承载 2 个垂直像素,ANSI 真彩色。
89
+ // 返回 { text, width } — width 是视觉列数,供外部对齐。
90
+ const RST = '\x1b[0m';
91
+ function ansi(r, g, b) { return `\x1b[38;2;${r};${g};${b}m`; }
92
+ function ansiBg(r, g, b) { return `\x1b[48;2;${r};${g};${b}m`; }
93
+ export async function imageToAscii(imagePath, maxWidth = 60) {
94
+ let sharp;
95
+ try {
96
+ // @ts-expect-error — sharp 0.35 的 types 与 pnpm exports 解析不兼容
97
+ sharp = (await import('sharp')).default;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ try {
103
+ const image = sharp(imagePath);
104
+ const metadata = await image.metadata();
105
+ const imgWidth = metadata.width || 100;
106
+ const imgHeight = metadata.height || 100;
107
+ const targetWidth = Math.min(maxWidth, imgWidth);
108
+ const targetHeight = Math.max(2, Math.floor(targetWidth * (imgHeight / imgWidth) / 2)) * 2;
109
+ const { data, info } = await image
110
+ .resize(targetWidth, targetHeight, { fit: 'inside' })
111
+ .ensureAlpha()
112
+ .raw()
113
+ .toBuffer({ resolveWithObject: true });
114
+ const w = info.width, h = info.height;
115
+ const lines = [];
116
+ for (let y = 0; y < h; y += 2) {
117
+ let line = '';
118
+ for (let x = 0; x < w; x++) {
119
+ const ti = (y * w + x) * 4;
120
+ const bi = ((y + 1) * w + x) * 4;
121
+ const tr = data[ti], tg = data[ti + 1], tb = data[ti + 2], ta = data[ti + 3];
122
+ const br = data[bi], bg = data[bi + 1], bb = data[bi + 2], ba = data[bi + 3];
123
+ const topOn = (tr + tg + tb) / 3 >= 128 && ta >= 64;
124
+ const botOn = (br + bg + bb) / 3 >= 128 && ba >= 64;
125
+ const idx = (topOn ? 2 : 0) | (botOn ? 1 : 0);
126
+ if (idx === 0)
127
+ line += ' ';
128
+ else if (idx === 1)
129
+ line += ansi(br, bg, bb) + '▄' + RST;
130
+ else if (idx === 2)
131
+ line += ansi(tr, tg, tb) + '▀' + RST;
132
+ else
133
+ line += ansi(tr, tg, tb) + ansiBg(br, bg, bb) + '▀' + RST;
134
+ }
135
+ lines.push(line);
136
+ }
137
+ return { text: lines.join('\n'), width: w };
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ // ─── AgentLoop ───────────────────────────────────────────────────────
144
+ /**
145
+ * AgentLoop — 串联所有模块的完整 Agent 主循环
146
+ *
147
+ * 核心流程:compose -> LLM -> parse -> tool -> compose 的循环
148
+ *
149
+ * 通过 OutputHandler 接口解耦输出:
150
+ * - 传 null → 无输出(静默模式)
151
+ * - 传实现 → 输出到 TUI / CLI / 日志 等任意目标
152
+ */
153
+ export class AgentLoop {
154
+ provider;
155
+ contextComposer;
156
+ compressor;
157
+ toolExecutor;
158
+ toolRegistry;
159
+ conversationStore;
160
+ eventStore;
161
+ statsManager;
162
+ sessionDir;
163
+ summaryStore;
164
+ maxTurns;
165
+ maxContextTokens;
166
+ skillRegistry;
167
+ mcpBridge;
168
+ dependencyAnalyzer;
169
+ personaDir;
170
+ providerRouter;
171
+ modelRouter;
172
+ turnRecorder;
173
+ abortController = null;
174
+ interrupted = false;
175
+ /** 串行化锁:确保 run() 不会并发执行 */
176
+ _runMutex = Promise.resolve();
177
+ recentToolNames = [];
178
+ currentSummary;
179
+ orchestrator;
180
+ activePlan;
181
+ agentRegistry;
182
+ outputHandler;
183
+ compressCount = 0;
184
+ needsAggressiveCompress = false;
185
+ cacheHitTokens = 0;
186
+ cacheMissTokens = 0;
187
+ cacheTurns = [];
188
+ logCacheHits;
189
+ currentTurn = 0;
190
+ /** 公开只读访问器 — 供 TurnRecorder / RollbackTool 等查询当前回合号 */
191
+ get turnNumber() { return this.currentTurn; }
192
+ lastSavedSummary;
193
+ pendingImpactInfo = null;
194
+ requestId;
195
+ logger;
196
+ flowRegistry;
197
+ /** 旁路Agent 管理器(factory 注入) */
198
+ bypassManager;
199
+ /** 本轮用户消息的旁路注入缓存(preTurn 首轮产出,后续迭代复用) */
200
+ _bypassInjections;
201
+ activeProvider;
202
+ scheduler = null;
203
+ schedulerInitialized = false;
204
+ /** 定时任务触发后待注入对话的通知 */
205
+ pendingTaskNotifications = [];
206
+ /** 当前正在执行的定时任务名(供 TUI 显示上下文),run 前设置,run 后清除 */
207
+ pendingTaskName = null;
208
+ /** 知识库状态引用(factory 注入) */
209
+ kbState = null;
210
+ /** 图片索引存储(会话级) */
211
+ imageStore = new ImageStore();
212
+ /** 待注入图片队列(view_image 工具填充,下次 compose 前消费) */
213
+ pendingImageInjections = [];
214
+ /** 渠道预取图片(渠道层在 run() 前写入,_runInternal 一次性消费) */
215
+ channelImages = null;
216
+ /** Fallback 通知(onFallback 回调写入,runTurn 一次性消费后清空) */
217
+ pendingFallbackInfo = null;
218
+ /** 上下文组装策略(精确模式切换用,deprecated:新代码使用 activeRouter) */
219
+ composeStrategy = null;
220
+ /** 当前激活的上下文路由器,初始化为 NormalRouter,首次 syncRouter() 时同步到全局状态 */
221
+ activeRouter = new NormalRouter();
222
+ /** Whether any tools were executed inline during the current stream */
223
+ inlineToolExecuted = false;
224
+ /** Stores results from inline tool execution, keyed by tool_use_id */
225
+ inlineToolResults = new Map();
226
+ /** Tools that require user confirmation before execution */
227
+ dangerousTools;
228
+ /** Tools whitelisted by user (skip confirmation — session-level, from 'always' response) */
229
+ allowlistTools;
230
+ /** AOR (Absence of Restriction): skip all future permission checks for this session */
231
+ unrestrictedTools = false;
232
+ /** Allowed bash commands (glob pattern matched, from config) */
233
+ allowedCommands = new Set();
234
+ configCenter;
235
+ contextDirty = false;
236
+ loopGuard;
237
+ resultBuffer;
238
+ bundleRegistry;
239
+ gitManager;
240
+ switchingProvider = false;
241
+ bypassProvider;
242
+ lastContextTokens = 0;
243
+ needsCompression = false;
244
+ pendingCompression = null;
245
+ lifecycleSupervisor = null;
246
+ previousProviderWasLocal = false;
247
+ /** 当前 AgentLoop 的 thinking 状态(per-session 隔离) */
248
+ thinkingEnabled = false;
249
+ thinkingEffort = undefined;
250
+ constructor(provider, contextComposer, compressor, orchestrator, toolExecutor, toolRegistry, conversationStore, eventStore, statsManager, sessionDir, summaryStore, maxTurns = getDefaultConfig().session.maxTurns, maxContextTokens, outputHandler, skillRegistry, mcpBridge, dependencyAnalyzer, agentRegistry, personaDir, flowRegistry, providerRouter, dangerousTools, allowlistTools, configCenter, modelRouter, turnRecorder) {
251
+ this.provider = provider;
252
+ this.contextComposer = contextComposer;
253
+ this.compressor = compressor;
254
+ this.toolExecutor = toolExecutor;
255
+ this.toolRegistry = toolRegistry;
256
+ this.conversationStore = conversationStore;
257
+ this.eventStore = eventStore;
258
+ this.statsManager = statsManager;
259
+ this.sessionDir = sessionDir;
260
+ this.summaryStore = summaryStore;
261
+ this.maxTurns = maxTurns;
262
+ this.maxContextTokens = maxContextTokens;
263
+ this.skillRegistry = skillRegistry;
264
+ this.mcpBridge = mcpBridge;
265
+ this.dependencyAnalyzer = dependencyAnalyzer;
266
+ this.personaDir = personaDir;
267
+ this.providerRouter = providerRouter;
268
+ this.modelRouter = modelRouter;
269
+ this.turnRecorder = turnRecorder;
270
+ this.orchestrator = orchestrator;
271
+ this.outputHandler = outputHandler ?? null;
272
+ this.agentRegistry = agentRegistry;
273
+ // MachineRegistry 由 factory.ts 注入,不创建默认实例(空注册表无实际作用)
274
+ this.flowRegistry = flowRegistry;
275
+ this.dangerousTools = dangerousTools ?? new Set(['write', 'bash']);
276
+ this.allowlistTools = allowlistTools ?? new Set();
277
+ this.configCenter = configCenter;
278
+ this.maxContextTokens = configCenter
279
+ ? configCenter.get('session.maxContext')
280
+ : DEFAULT_MAX_CONTEXT_TOKENS;
281
+ this.requestId = crypto.randomUUID();
282
+ this.logger = createLogger('AgentLoop').child('request', { requestId: this.requestId });
283
+ this.gitManager = new GitManager(process.cwd());
284
+ const stormWindowSize = this.configCenter ? this.configCenter.get('repair.storm.windowSize') : undefined;
285
+ const stormThreshold = this.configCenter ? this.configCenter.get('repair.storm.threshold') : undefined;
286
+ const stormExempt = this.configCenter ? this.configCenter.get('repair.storm.stormExemptTools') : undefined;
287
+ const textLoopEnabled = this.configCenter ? this.configCenter.get('repair.textLoop.enabled') : undefined;
288
+ const textLoopSim = this.configCenter ? this.configCenter.get('repair.textLoop.similarity') : undefined;
289
+ const textLoopThresh = this.configCenter ? this.configCenter.get('repair.textLoop.threshold') : undefined;
290
+ this.loopGuard = new LoopGuard({
291
+ tool: {
292
+ enabled: true,
293
+ windowSize: typeof stormWindowSize === 'number' && stormWindowSize > 0 ? stormWindowSize : 6,
294
+ threshold: typeof stormThreshold === 'number' && stormThreshold > 0 ? stormThreshold : 3,
295
+ exemptTools: Array.isArray(stormExempt) ? stormExempt : [],
296
+ },
297
+ text: {
298
+ enabled: textLoopEnabled !== false,
299
+ windowSize: 6,
300
+ threshold: typeof textLoopThresh === 'number' && textLoopThresh > 0 ? textLoopThresh : 3,
301
+ minLength: 30,
302
+ similarity: typeof textLoopSim === 'number' && textLoopSim > 0 ? textLoopSim : 0.75,
303
+ },
304
+ });
305
+ const bufferEnabled = this.configCenter ? this.configCenter.get('tools.resultBuffer.enabled') : undefined;
306
+ const bufferThreshold = this.configCenter ? this.configCenter.get('tools.resultBuffer.threshold') : undefined;
307
+ const bufferIncludePreview = this.configCenter ? this.configCenter.get('tools.resultBuffer.includePreview') : undefined;
308
+ const bufferPreviewChars = this.configCenter ? this.configCenter.get('tools.resultBuffer.previewChars') : undefined;
309
+ this.resultBuffer = new ToolResultBuffer({
310
+ sessionDir: this.sessionDir,
311
+ enabled: typeof bufferEnabled === 'boolean' ? bufferEnabled : true,
312
+ threshold: typeof bufferThreshold === 'number' && bufferThreshold > 0 ? bufferThreshold : 16384,
313
+ includePreview: typeof bufferIncludePreview === 'boolean' ? bufferIncludePreview : true,
314
+ previewChars: typeof bufferPreviewChars === 'number' && bufferPreviewChars > 0 ? bufferPreviewChars : 500,
315
+ });
316
+ this.logCacheHits = this.configCenter
317
+ ? this.configCenter.get('logging.logCacheHits') === true
318
+ : false;
319
+ // ── Restore persisted values from configCenter ──
320
+ if (this.configCenter) {
321
+ // Restore allowedTools (session-level allowlist)
322
+ const persistedAllowedTools = this.configCenter.get('safety.allowedTools');
323
+ if (Array.isArray(persistedAllowedTools) && persistedAllowedTools.length > 0) {
324
+ this.allowlistTools = new Set(persistedAllowedTools);
325
+ }
326
+ // Restore allowedCommands (bash glob patterns)
327
+ const persistedAllowedCommands = this.configCenter.get('safety.allowedCommands');
328
+ if (Array.isArray(persistedAllowedCommands) && persistedAllowedCommands.length > 0) {
329
+ this.allowedCommands = new Set(persistedAllowedCommands);
330
+ }
331
+ // Restore dangerousTools
332
+ const persistedDangerousTools = this.configCenter.get('safety.dangerousTools');
333
+ if (Array.isArray(persistedDangerousTools)) {
334
+ this.dangerousTools = new Set(persistedDangerousTools);
335
+ }
336
+ // 用户未显式关闭时才从模型目录自动开启思考
337
+ const thinkingCfg = this.configCenter?.get('provider.enableThinking');
338
+ if (thinkingCfg !== false) {
339
+ const info = getModelInfo(this.provider.getProviderType(), this.provider.getModel());
340
+ if (info?.reasoningEffort) {
341
+ this.thinkingEnabled = true;
342
+ this.provider.setThinking?.(true, info.reasoningEffort);
343
+ }
344
+ }
345
+ }
346
+ // ── Load persisted allowlist from session directory ──
347
+ sessionAllowlist.load(this.sessionDir).then(data => {
348
+ for (const tool of data.allowedTools) {
349
+ this.allowlistTools.add(tool);
350
+ }
351
+ for (const cmd of data.allowedCommands) {
352
+ this.allowedCommands.add(cmd);
353
+ }
354
+ }).catch(() => { });
355
+ // ── RuntimeConfigCenter watch subscriptions ──
356
+ if (this.configCenter) {
357
+ // Subscribe to dangerous tools changes
358
+ this.configCenter.watch('safety.dangerousTools', (event) => {
359
+ this.dangerousTools = new Set(event.newValue);
360
+ });
361
+ // Subscribe to allowed commands (bash glob patterns)
362
+ this.configCenter.watch('safety.allowedCommands', (event) => {
363
+ this.allowedCommands = new Set(event.newValue);
364
+ });
365
+ // Subscribe to context changes (mark dirty for next compose)
366
+ this.configCenter.watch('context.*', (_event) => {
367
+ this.contextDirty = true;
368
+ });
369
+ // 思考模式由 models-catalog.json 的 reasoningEffort 控制,启动时自动应用
370
+ // 运行时通过 /model thinking <on|off|high|max> 临时覆盖
371
+ }
372
+ // ── 注册 view_image 工具(依赖 ImageStore) ──
373
+ this.toolRegistry.register(createViewImageTool(this.imageStore, this.pendingImageInjections));
374
+ }
375
+ /**
376
+ * 中断当前运行:设置 interrupted 标志,中止 Provider 请求,
377
+ * 并通过 OutputHandler 通知上层
378
+ */
379
+ interrupt() {
380
+ this.interrupted = true;
381
+ this.abortController?.abort();
382
+ this.outputHandler?.onInterrupt?.();
383
+ this.outputHandler?.onStatus?.('Operation interrupted.', 'warn');
384
+ }
385
+ /** Get current turn/mode info for status display */
386
+ getTurnInfo(turnCount, tokensUsed) {
387
+ let planStepsTotal;
388
+ let planStepsDone;
389
+ if (this.activePlan) {
390
+ planStepsTotal = this.activePlan.steps.length;
391
+ planStepsDone = this.activePlan.steps.filter(s => s.status === 'completed').length;
392
+ }
393
+ const hasCache = this.cacheHitTokens > 0 || this.cacheMissTokens > 0;
394
+ // Latest turn cache hit rate
395
+ const latestTurn = this.cacheTurns.at(-1);
396
+ return {
397
+ turnCount,
398
+ maxTurns: this.maxTurns,
399
+ tokensUsed,
400
+ maxContextTokens: this.maxContextTokens,
401
+ planStepsTotal,
402
+ planStepsDone,
403
+ sessionId: path.basename(this.sessionDir),
404
+ compressCount: this.compressCount,
405
+ cacheHitTokens: hasCache ? this.cacheHitTokens : undefined,
406
+ cacheMissTokens: hasCache ? this.cacheMissTokens : undefined,
407
+ cacheHitRate: latestTurn ? Math.round(latestTurn.hitRate * 10) / 10 : undefined,
408
+ cacheHistory: this.cacheTurns.length > 0 ? [...this.cacheTurns] : undefined,
409
+ };
410
+ }
411
+ getRecentToolCallNames() {
412
+ return this.recentToolNames;
413
+ }
414
+ /** Bootstrap Flow 是否活跃(供 CLI/TUI 判断是否需要发送空消息启动引导) */
415
+ get bootstrapActive() {
416
+ return this.flowRegistry.getActive()?.id === 'bootstrap';
417
+ }
418
+ /** 就地切换到指定 session,无需重启进程 */
419
+ async switchSession(newSessionDir) {
420
+ this.sessionDir = newSessionDir;
421
+ this.currentSummary = undefined;
422
+ this.compressCount = 0;
423
+ this.needsAggressiveCompress = false;
424
+ this.inlineToolResults.clear();
425
+ this.pendingImpactInfo = null;
426
+ this.pendingTaskNotifications = [];
427
+ this.pendingCompression = null;
428
+ this.lastContextTokens = 0;
429
+ // 重载摘要
430
+ try {
431
+ const persisted = await this.summaryStore.load(this.sessionDir);
432
+ if (persisted)
433
+ this.currentSummary = persisted;
434
+ }
435
+ catch { }
436
+ // 重载权限白名单
437
+ sessionAllowlist.load(this.sessionDir).then(data => {
438
+ this.allowlistTools = new Set(data.allowedTools ?? []);
439
+ this.allowedCommands = new Set(data.allowedCommands ?? []);
440
+ }).catch(() => { });
441
+ }
442
+ /** 设置调度器 */
443
+ setScheduler(scheduler) {
444
+ this.scheduler = scheduler;
445
+ }
446
+ /**
447
+ * 陪伴模式:从 JSONL 中移除本轮工具调用完整回合。
448
+ * 找到最后一条 user 文本消息,从它开始截断文件——
449
+ * 整个工具调用回合(user → tool_use → tool_result → 跟进文本)都不留痕迹。
450
+ */
451
+ async removeLastRoundFromJsonl() {
452
+ try {
453
+ const jsonlPath = path.join(this.sessionDir, 'conversation.jsonl');
454
+ const fsSync = await import('node:fs');
455
+ if (!fsSync.existsSync(jsonlPath))
456
+ return;
457
+ const content = fsSync.readFileSync(jsonlPath, 'utf-8');
458
+ const lines = content.split('\n').filter(l => l.trim());
459
+ if (lines.length === 0)
460
+ return;
461
+ // 从末尾往前找本轮第一个 tool_use assistant 消息
462
+ let firstToolUse = -1;
463
+ for (let i = lines.length - 1; i >= 0; i--) {
464
+ try {
465
+ const msg = JSON.parse(lines[i]);
466
+ if (msg.role === 'assistant' && Array.isArray(msg.content) &&
467
+ msg.content.some((b) => b.type === 'tool_use')) {
468
+ firstToolUse = i;
469
+ }
470
+ else if (firstToolUse !== -1) {
471
+ break; // 遇到非 tool_use 消息,本轮的 tool 区域结束
472
+ }
473
+ }
474
+ catch { /* skip */ }
475
+ }
476
+ if (firstToolUse === -1)
477
+ return;
478
+ // 从 tool_use 往前找到触发它的 user 文本消息(排除 tool_result)
479
+ let cutIndex = firstToolUse;
480
+ for (let i = firstToolUse - 1; i >= 0; i--) {
481
+ try {
482
+ const msg = JSON.parse(lines[i]);
483
+ if (msg.role === 'user') {
484
+ const c = msg.content;
485
+ if (!Array.isArray(c) || !c.some((b) => b.type === 'tool_result')) {
486
+ cutIndex = i;
487
+ break;
488
+ }
489
+ }
490
+ }
491
+ catch { /* skip */ }
492
+ }
493
+ // 截断:保留 cutIndex 之前的所有行
494
+ const kept = lines.slice(0, cutIndex);
495
+ const newContent = kept.length > 0 ? kept.join('\n') + '\n' : '';
496
+ fsSync.writeFileSync(jsonlPath, newContent, 'utf-8');
497
+ }
498
+ catch { /* 文件操作失败不阻塞 */ }
499
+ }
500
+ /**
501
+ * 陪伴模式工具调用清理:
502
+ * - companion_mode 切换 → 剥离工具痕迹,保留 LLM 文本
503
+ * - 其他工具 → 整轮砍掉(原有行为)
504
+ * - 找不到触发消息(跨 session) → 处理整个文件
505
+ */
506
+ async cleanCompanionJsonl() {
507
+ try {
508
+ const jsonlPath = path.join(this.sessionDir, 'conversation.jsonl');
509
+ const fsSync = await import('node:fs');
510
+ if (!fsSync.existsSync(jsonlPath))
511
+ return;
512
+ const content = fsSync.readFileSync(jsonlPath, 'utf-8');
513
+ const lines = content.split('\n').filter(l => l.trim());
514
+ if (lines.length === 0)
515
+ return;
516
+ // 从末尾往前找最后一条纯文本 user 消息
517
+ let triggerIdx = -1;
518
+ for (let i = lines.length - 1; i >= 0; i--) {
519
+ try {
520
+ const msg = JSON.parse(lines[i]);
521
+ if (msg.role !== 'user')
522
+ continue;
523
+ const blocks = Array.isArray(msg.content) ? msg.content : [msg.content];
524
+ if (blocks.every((b) => b.type === 'tool_result'))
525
+ continue;
526
+ triggerIdx = i;
527
+ break;
528
+ }
529
+ catch { /* skip */ }
530
+ }
531
+ // 检查是否有 companion_mode 工具(跨 session 时从 0 开始扫描)
532
+ let hasCompanionModeTool = false;
533
+ const scanFrom = triggerIdx === -1 ? 0 : triggerIdx;
534
+ for (let i = scanFrom; i < lines.length; i++) {
535
+ try {
536
+ const msg = JSON.parse(lines[i]);
537
+ if (msg.role !== 'assistant')
538
+ continue;
539
+ const blocks = Array.isArray(msg.content) ? msg.content : [msg.content];
540
+ if (blocks.some((b) => b.type === 'tool_use' && b.name === 'companion_mode')) {
541
+ hasCompanionModeTool = true;
542
+ break;
543
+ }
544
+ }
545
+ catch { /* skip */ }
546
+ }
547
+ // companion_mode 切换:剥离 tool 痕迹,保留 LLM 文本
548
+ if (hasCompanionModeTool) {
549
+ const processFrom = triggerIdx === -1 ? 0 : triggerIdx + 1;
550
+ const kept = [];
551
+ for (let i = 0; i < processFrom; i++)
552
+ kept.push(lines[i]);
553
+ for (let i = processFrom; i < lines.length; i++) {
554
+ try {
555
+ const msg = JSON.parse(lines[i]);
556
+ if (msg.role === 'user') {
557
+ const blocks = Array.isArray(msg.content) ? msg.content : [msg.content];
558
+ if (blocks.some((b) => b.type === 'tool_result'))
559
+ continue;
560
+ kept.push(lines[i]);
561
+ continue;
562
+ }
563
+ if (msg.role === 'assistant') {
564
+ const blocks = Array.isArray(msg.content) ? msg.content : [msg.content];
565
+ // 跳过包含 companion_mode tool_use 的消息(确认语如 "好的,进入陪伴模式。")
566
+ if (blocks.some((b) => b.type === 'tool_use' && b.name === 'companion_mode'))
567
+ continue;
568
+ const textBlocks = blocks.filter((b) => b.type === 'text');
569
+ if (textBlocks.length === 0)
570
+ continue;
571
+ kept.push(JSON.stringify({
572
+ role: 'assistant',
573
+ content: textBlocks.length === 1 ? textBlocks[0] : textBlocks,
574
+ }));
575
+ continue;
576
+ }
577
+ kept.push(lines[i]);
578
+ }
579
+ catch { /* skip */ }
580
+ }
581
+ fsSync.writeFileSync(jsonlPath, kept.join('\n') + (kept.length ? '\n' : ''), 'utf-8');
582
+ return;
583
+ }
584
+ // 普通工具:整轮砍掉
585
+ if (triggerIdx !== -1) {
586
+ const kept = lines.slice(0, triggerIdx);
587
+ fsSync.writeFileSync(jsonlPath, kept.length ? kept.join('\n') + '\n' : '', 'utf-8');
588
+ }
589
+ }
590
+ catch { /* 文件操作失败不阻塞 */ }
591
+ }
592
+ /**
593
+ * 陪伴模式定时任务专用:只移除触发提示词和工具链,保留模型自然回复。
594
+ * 效果:模型看起来像是"主动"搭话,而非响应系统指令。
595
+ */
596
+ async removeTriggerFromJsonl() {
597
+ try {
598
+ const jsonlPath = path.join(this.sessionDir, 'conversation.jsonl');
599
+ const fsSync = await import('node:fs');
600
+ if (!fsSync.existsSync(jsonlPath))
601
+ return;
602
+ const content = fsSync.readFileSync(jsonlPath, 'utf-8');
603
+ const lines = content.split('\n').filter(l => l.trim());
604
+ if (lines.length === 0)
605
+ return;
606
+ // 从末尾找最后一条 user 文本消息(触发提示词)
607
+ let triggerIdx = -1;
608
+ for (let i = lines.length - 1; i >= 0; i--) {
609
+ try {
610
+ const msg = JSON.parse(lines[i]);
611
+ if (msg.role !== 'user')
612
+ continue;
613
+ const blocks = Array.isArray(msg.content) ? msg.content : [msg.content];
614
+ if (blocks.every((b) => b.type === 'tool_result'))
615
+ continue;
616
+ triggerIdx = i;
617
+ break;
618
+ }
619
+ catch { /* skip */ }
620
+ }
621
+ if (triggerIdx === -1)
622
+ return;
623
+ // 收集要移除的索引:触发词 + 之后所有的 tool_use / tool_result
624
+ const removeIndices = new Set();
625
+ removeIndices.add(triggerIdx);
626
+ for (let i = triggerIdx + 1; i < lines.length; i++) {
627
+ try {
628
+ const msg = JSON.parse(lines[i]);
629
+ const blocks = Array.isArray(msg.content) ? msg.content : [msg.content];
630
+ if (msg.role === 'assistant' && blocks.some((b) => b.type === 'tool_use')) {
631
+ removeIndices.add(i);
632
+ }
633
+ if (msg.role === 'user' && blocks.every((b) => b.type === 'tool_result')) {
634
+ removeIndices.add(i);
635
+ }
636
+ }
637
+ catch { /* skip */ }
638
+ }
639
+ const kept = lines.filter((_, i) => !removeIndices.has(i));
640
+ const newContent = kept.length > 0 ? kept.join('\n') + '\n' : '';
641
+ fsSync.writeFileSync(jsonlPath, newContent, 'utf-8');
642
+ }
643
+ catch { /* 文件操作失败不阻塞 */ }
644
+ }
645
+ /**
646
+ * 同步全局 Router 到当前 loop。
647
+ * 每轮 runTurn 开头调用,所有渠道的 loop 自动切换会话和上下文行为。
648
+ */
649
+ async syncRouter() {
650
+ const globalRouter = getActiveRouter();
651
+ if (this.activeRouter?.name === globalRouter.name)
652
+ return;
653
+ // 切出旧 Router
654
+ if (this.activeRouter) {
655
+ await this.activeRouter.onDeactivate?.(this);
656
+ }
657
+ // 切入新 Router
658
+ this.activeRouter = globalRouter;
659
+ await this.activeRouter.onActivate?.(this);
660
+ }
661
+ /** 获取调度器 */
662
+ getScheduler() {
663
+ return this.scheduler;
664
+ }
665
+ /** 添加定时任务 */
666
+ async addScheduledTask(name, scheduleType, time) {
667
+ if (!this.scheduler)
668
+ return;
669
+ await this.scheduler.addTask(name, scheduleType, { time }, { type: 'scheduled', target: name, payload: {} }, []);
670
+ }
671
+ /** 注入 LifecycleSupervisor,用于运行时切换 provider 时自动管理本地模型进程 */
672
+ setLifecycleSupervisor(supervisor) {
673
+ this.lifecycleSupervisor = supervisor;
674
+ }
675
+ /** 注入工具包注册表(在 factory.ts 中紧接 AgentLoop 创建后调用) */
676
+ setBundleRegistry(registry) {
677
+ this.bundleRegistry = registry;
678
+ }
679
+ /** 运行时替换 outputHandler(用于 server 模式按请求切换流式输出) */
680
+ setOutputHandler(handler) {
681
+ this.outputHandler = handler;
682
+ }
683
+ /** 获取当前 active Provider(考虑 Router 路由) */
684
+ getActiveProvider() {
685
+ return this.activeProvider ?? this.provider;
686
+ }
687
+ /** 运行时切换 KVCache 隔离 ID(模式切换用:普通↔陪伴) */
688
+ setActiveUserId(userId) {
689
+ const provider = this.activeProvider ?? this.provider;
690
+ provider.setUserId?.(userId);
691
+ }
692
+ /** 由 factory 注入:旁路Agent 共享的 Provider 实例 */
693
+ setBypassProvider(provider) {
694
+ this.bypassProvider = provider;
695
+ }
696
+ /** 运行时切换旁路Agent 的 KVCache 隔离 ID(与主Agent 同步切换) */
697
+ setBypassUserId(userId) {
698
+ this.bypassProvider?.setUserId?.(userId);
699
+ }
700
+ /** 切换 Provider 路由模式 */
701
+ toggleProvider() {
702
+ if (!this.providerRouter)
703
+ return;
704
+ const info = this.providerRouter.getRoutingInfo();
705
+ if (info.mode === 'manual') {
706
+ this.providerRouter.clearDefault();
707
+ }
708
+ else {
709
+ const localProviders = this.providerRouter.list().filter(name => {
710
+ const p = this.providerRouter?.get(name);
711
+ const t = p?.getProviderType();
712
+ return t === 'llamacpp' || t === 'local' || t === 'ollama';
713
+ });
714
+ const onlineProviders = this.providerRouter.list().filter(name => {
715
+ const p = this.providerRouter?.get(name);
716
+ const t = p?.getProviderType();
717
+ return t !== 'llamacpp' && t !== 'local' && t !== 'ollama';
718
+ });
719
+ if (info.isLocal && onlineProviders.length > 0) {
720
+ this.providerRouter.setDefault(onlineProviders[0]);
721
+ // 从本地切走 → 停止本地模型
722
+ if (this.lifecycleSupervisor) {
723
+ const modelKey = this.configCenter?.get('provider.local.modelKey') || '';
724
+ this.lifecycleSupervisor.stopModel(modelKey).catch(() => { });
725
+ }
726
+ this.previousProviderWasLocal = false;
727
+ }
728
+ else if (!info.isLocal && localProviders.length > 0) {
729
+ this.providerRouter.setDefault(localProviders[0]);
730
+ if (this.lifecycleSupervisor) {
731
+ const existingBaseUrl = this.configCenter?.get('provider.local.baseUrl');
732
+ if (!existingBaseUrl) {
733
+ const modelKey = this.configCenter?.get('provider.local.modelKey') || '';
734
+ this.lifecycleSupervisor.startModelOnDemand(process.cwd(), modelKey)
735
+ .then((modelInfo) => {
736
+ if (modelInfo) {
737
+ this.lifecycleSupervisor?.getModelManager().getModels();
738
+ }
739
+ })
740
+ .catch(() => { });
741
+ }
742
+ }
743
+ this.previousProviderWasLocal = true;
744
+ }
745
+ }
746
+ }
747
+ /** 切换到指定名称的 Provider */
748
+ /** 注册新 Provider(用于 switch_provider 工具带 api_key 动态注册) */
749
+ registerProvider(name, provider) {
750
+ this.providerRouter?.register(name, provider);
751
+ }
752
+ async switchProvider(providerName) {
753
+ if (this.switchingProvider)
754
+ return;
755
+ if (!this.providerRouter) {
756
+ throw new Error('ProviderRouter not available');
757
+ }
758
+ this.switchingProvider = true;
759
+ try {
760
+ let newProvider = this.providerRouter.get(providerName);
761
+ if (!newProvider && this.configCenter) {
762
+ // 本地模型:跳过 configCenter,始终自动检测
763
+ if (providerName === 'local' || providerName === 'llamacpp' || providerName === 'ollama') {
764
+ const { getLocalProviderConfigLoader, detectLocalBackend } = await import('../provider/local-config.js');
765
+ const localCfg = getLocalProviderConfigLoader();
766
+ let detected = await detectLocalBackend();
767
+ // 未检测到运行中的服务 → 尝试自动拉起(通过 lifecycle supervisor 管理进程)
768
+ if (!detected && this.lifecycleSupervisor) {
769
+ // 先试 Ollama
770
+ const ollamaInfo = await this.lifecycleSupervisor.startOllamaOnDemand(process.cwd());
771
+ if (ollamaInfo) {
772
+ detected = { backend: 'ollama', baseUrl: ollamaInfo.baseUrl, port: ollamaInfo.port ?? 11434 };
773
+ }
774
+ else {
775
+ // 再试 llama.cpp
776
+ const { getLocalProviderConfigLoader: getCfg } = await import('../provider/local-config.js');
777
+ const cfg = getCfg();
778
+ const modelKey = this.configCenter?.get('provider.local.modelKey')
779
+ || cfg?.defaultModel || 'local';
780
+ try {
781
+ const info = await this.lifecycleSupervisor.startModelOnDemand(process.cwd(), modelKey);
782
+ if (info) {
783
+ detected = { backend: 'llamacpp', baseUrl: info.baseUrl, port: info.port ?? 8080 };
784
+ }
785
+ }
786
+ catch { /* 启动失败 */ }
787
+ }
788
+ }
789
+ if (!detected && !localCfg?.defaultModel) {
790
+ throw new Error('本地模型服务未配置。请安装 Ollama 或 llama.cpp,并确保服务正在运行。');
791
+ }
792
+ // 检测到的优先,否则 fallback 到默认配置
793
+ const baseUrl = detected?.baseUrl || localCfg?.baseUrl || 'http://127.0.0.1:11434/v1';
794
+ const backend = detected?.backend || localCfg?.backend;
795
+ // 模型名解析优先级:
796
+ // 1. Ollama 后端 → 查询 /api/tags 获取真实模型列表,匹配配置或取第一个
797
+ // 2. RuntimeConfigCenter 中已有的 provider.local.model(TUI /model 命令写入)
798
+ // 3. 硬编码兜底 llama3.2
799
+ let model;
800
+ if (backend === 'ollama') {
801
+ const { fetchOllamaModels, pickBestOllamaModel } = await import('../provider/local-config.js');
802
+ const ollamaModels = await fetchOllamaModels();
803
+ // 优先匹配运行时配置中的 model(TUI 切换时写入)或 localCfg 的 defaultModel
804
+ const preferred = this.configCenter?.get('provider.local.model')
805
+ || localCfg?.defaultModel
806
+ || null;
807
+ const best = pickBestOllamaModel(ollamaModels, preferred);
808
+ if (best) {
809
+ model = best;
810
+ }
811
+ else {
812
+ // Ollama 在运行但没有任何模型 → 给出明确错误
813
+ throw new Error('Ollama is running but no models found. ' +
814
+ 'Run "ollama pull <model>" to download a model first.');
815
+ }
816
+ }
817
+ else {
818
+ model = this.configCenter?.get('provider.local.model')
819
+ || localCfg?.defaultModel
820
+ || 'llama3.2';
821
+ }
822
+ try {
823
+ newProvider = new LocalProvider({ baseUrl, model, backend });
824
+ if (newProvider) {
825
+ this.providerRouter.register(providerName, newProvider);
826
+ }
827
+ }
828
+ catch {
829
+ // fallback failed
830
+ }
831
+ }
832
+ else {
833
+ const created = this.tryCreateProviderFromConfig(providerName);
834
+ if (created) {
835
+ newProvider = created;
836
+ this.providerRouter.register(providerName, created);
837
+ }
838
+ }
839
+ }
840
+ if (!newProvider) {
841
+ const available = this.providerRouter.list().join(', ');
842
+ throw new Error(`Provider "${providerName}" not found. Available in router: ${available}. ` +
843
+ `Use list_providers to see all options.`);
844
+ }
845
+ const newType = newProvider.getProviderType();
846
+ const isLocal = newType === 'local' || newType === 'llamacpp' || newType === 'ollama';
847
+ // 如果切换到本地模型,启动服务;如果从本地模型切走,停止服务
848
+ if (this.lifecycleSupervisor) {
849
+ const prevProvider = this.provider;
850
+ const prevType = prevProvider.getProviderType();
851
+ const prevWasLocal = prevType === 'local' || prevType === 'llamacpp' || prevType === 'ollama';
852
+ if (isLocal && !prevWasLocal) {
853
+ const existingBaseUrl = this.configCenter?.get('provider.local.baseUrl');
854
+ if (existingBaseUrl) {
855
+ // 模型已通过外部(如 tui 面板 /model/local_*)启动并写入配置
856
+ }
857
+ else {
858
+ const modelKey = this.configCenter?.get('provider.local.modelKey') || '';
859
+ try {
860
+ const modelInfo = await this.lifecycleSupervisor?.startModelOnDemand(process.cwd(), modelKey);
861
+ if (modelInfo && newProvider && 'setBaseUrl' in newProvider && 'setModel' in newProvider) {
862
+ newProvider.setBaseUrl(modelInfo.baseUrl);
863
+ newProvider.setModel(modelInfo.modelName);
864
+ }
865
+ }
866
+ catch {
867
+ // 模型启动失败,仍然尝试切换(可能服务已在外部运行)
868
+ }
869
+ }
870
+ }
871
+ else if (!isLocal && prevWasLocal) {
872
+ const modelKey = this.configCenter?.get('provider.local.modelKey') || '';
873
+ this.lifecycleSupervisor.stopModel(modelKey).catch(() => { });
874
+ }
875
+ this.previousProviderWasLocal = isLocal;
876
+ }
877
+ this.provider = newProvider;
878
+ this.activeProvider = undefined;
879
+ this.orchestrator?.setProvider(newProvider);
880
+ this.providerRouter.setDefault(providerName);
881
+ // 同步主通道到 ModelRouter,确保 registry 中 main 通道持有最新 Provider
882
+ this.modelRouter?.setMainProvider(newProvider, providerName);
883
+ // 自动裁剪上下文到新模型上限
884
+ const modelLimit = getModelContextWindow(newProvider.getProviderType(), newProvider.getModel());
885
+ // 1. 如果当前上下文已经超过新模型上限 → 触发压缩
886
+ if (this.lastContextTokens > modelLimit) {
887
+ this.maxContextTokens = modelLimit;
888
+ this.needsCompression = true;
889
+ if (this.configCenter) {
890
+ // 仅内存更新,不持久化——避免覆盖用户自定义值
891
+ this.configCenter.set('session.maxContext', modelLimit);
892
+ }
893
+ this.outputHandler?.onStatus?.(`Context (${this.lastContextTokens.toLocaleString()}) exceeds new model limit (${modelLimit.toLocaleString()}), will force compression on next turn`, 'warn');
894
+ }
895
+ // 2. 当前上下文没超,但 maxContextTokens 设置得比新模型上限高 → 只裁剪上限
896
+ else if (this.maxContextTokens > modelLimit) {
897
+ this.maxContextTokens = modelLimit;
898
+ if (this.configCenter) {
899
+ // 仅内存更新,不持久化——避免覆盖用户自定义值
900
+ this.configCenter.set('session.maxContext', modelLimit);
901
+ }
902
+ this.outputHandler?.onStatus?.(`maxContextTokens updated: ${modelLimit.toLocaleString()} (model: ${newProvider.getModel()})`, 'info');
903
+ }
904
+ // 3. 当前上下文和新模型上限都够用 → 无需操作
905
+ // 注意:不再在此处写 configCenter.set('provider.active') + save(),
906
+ // 避免共享同一 RuntimeConfigCenter 单例的其他 AgentLoop 被迫切换 provider。
907
+ // 持久化由调用方(如 TUI /model 命令)显式负责。
908
+ this.outputHandler?.onStatus?.(`Provider switched to ${providerName} (${newProvider.getProviderType()}/${newProvider.getModel()})`, 'info');
909
+ }
910
+ finally {
911
+ this.switchingProvider = false;
912
+ }
913
+ }
914
+ /** 从 RuntimeConfigCenter 中的配置 + 环境变量动态创建 Provider */
915
+ tryCreateProviderFromConfig(providerName) {
916
+ if (!this.configCenter)
917
+ return undefined;
918
+ const section = this.configCenter.get(`provider.${providerName}`);
919
+ if (!section || typeof section !== 'object')
920
+ return undefined;
921
+ const { model, apiKeyEnv, baseUrl } = section;
922
+ const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : undefined;
923
+ const isLocal = providerName === 'local' || providerName === 'llamacpp' || providerName === 'ollama';
924
+ if (!apiKey && !isLocal)
925
+ return undefined;
926
+ try {
927
+ return ProviderManager.createProviderFromConfig({
928
+ type: providerName,
929
+ apiKey: apiKey ?? '',
930
+ model: model ?? '',
931
+ baseUrl,
932
+ userId: this.sessionDir ? mainUserId(path.basename(this.sessionDir)) : undefined,
933
+ });
934
+ }
935
+ catch {
936
+ return undefined;
937
+ }
938
+ }
939
+ /** 订阅 RuntimeConfigCenter 变更,让 update_config 即时生效 */
940
+ subscribeConfig() {
941
+ if (!this.configCenter)
942
+ return;
943
+ // provider.active 变更 → 自动切换 provider
944
+ this.configCenter.watch('provider.active', (event) => {
945
+ const name = event.newValue;
946
+ if (!name || typeof name !== 'string')
947
+ return;
948
+ // 守卫:如果与当前 provider 相同,跳过,避免重复切换
949
+ const currentType = this.getActiveProvider().getProviderType();
950
+ if (name === currentType)
951
+ return;
952
+ if (this.providerRouter?.get(name)) {
953
+ this.switchProvider(name).catch(() => {
954
+ this.outputHandler?.onStatus?.(`Config changed provider to "${name}" but switch failed`, 'error');
955
+ });
956
+ }
957
+ });
958
+ // provider.<name>.model 变更 → 如果当前 active provider 匹配,重新创建 provider 并切换
959
+ this.configCenter.watch('provider.*.model', (event) => {
960
+ const newModel = event.newValue;
961
+ if (!newModel || typeof newModel !== 'string')
962
+ return;
963
+ if (!this.providerRouter || !this.configCenter)
964
+ return;
965
+ const activeName = this.configCenter.get('provider.active');
966
+ if (!activeName)
967
+ return;
968
+ // 解析路径 provider.X.model → 提取 X
969
+ const watchPath = event.path;
970
+ const parts = watchPath.split('.');
971
+ if (parts.length < 3 || parts[0] !== 'provider' || parts[2] !== 'model')
972
+ return;
973
+ const changedProvider = parts[1];
974
+ // 只响应当前 active provider 的 model 变更
975
+ if (changedProvider !== activeName)
976
+ return;
977
+ // 注销旧的 provider,用新 model 重建
978
+ this.providerRouter.unregister(activeName);
979
+ const created = this.tryCreateProviderFromConfig(activeName);
980
+ if (created) {
981
+ this.providerRouter.register(activeName, created);
982
+ }
983
+ this.switchProvider(activeName).catch(() => {
984
+ this.outputHandler?.onStatus?.(`Config changed model for "${activeName}" but switch failed`, 'error');
985
+ });
986
+ });
987
+ // session.maxTurns 变更 → 即时更新
988
+ this.configCenter.watch('session.maxTurns', (event) => {
989
+ if (typeof event.newValue === 'number' && event.newValue > 0) {
990
+ this.maxTurns = event.newValue;
991
+ this.outputHandler?.onStatus?.(`maxTurns updated to ${event.newValue}`, 'info');
992
+ }
993
+ });
994
+ // session.maxContext 变更 → 即时更新(裁剪到当前模型上限)
995
+ this.configCenter.watch('session.maxContext', (event) => {
996
+ if (typeof event.newValue === 'number' && event.newValue > 0) {
997
+ const activeP = this.getActiveProvider();
998
+ const modelLimit = getModelContextWindow(activeP.getProviderType(), activeP.getModel());
999
+ const clamped = Math.min(event.newValue, modelLimit);
1000
+ this.maxContextTokens = clamped;
1001
+ if (clamped < event.newValue) {
1002
+ this.outputHandler?.onStatus?.(`maxContextTokens capped to ${clamped} (model limit: ${modelLimit})`, 'warn');
1003
+ }
1004
+ else {
1005
+ this.outputHandler?.onStatus?.(`maxContextTokens updated to ${clamped}`, 'info');
1006
+ }
1007
+ }
1008
+ });
1009
+ }
1010
+ /** 切换回自动路由模式 */
1011
+ switchToAutoRoute() {
1012
+ this.providerRouter?.clearDefault();
1013
+ this.outputHandler?.onStatus?.('Switched to auto route mode', 'info');
1014
+ }
1015
+ /** 获取 Provider 路由信息 */
1016
+ getProviderRoutingInfo() {
1017
+ if (!this.providerRouter)
1018
+ return null;
1019
+ const info = this.providerRouter.getRoutingInfo();
1020
+ return {
1021
+ providerLabel: info.providerName,
1022
+ isLocal: info.isLocal,
1023
+ mode: info.mode,
1024
+ };
1025
+ }
1026
+ setModelSource(role, source) {
1027
+ if (!this.modelRouter || !this.configCenter)
1028
+ return;
1029
+ const currentModels = this.configCenter.get('models');
1030
+ if (currentModels) {
1031
+ currentModels[role] = { ...currentModels[role], source };
1032
+ this.configCenter.set('models', currentModels);
1033
+ this.configCenter.save().catch(() => { });
1034
+ }
1035
+ }
1036
+ getModelSources() {
1037
+ if (!this.modelRouter || !this.configCenter)
1038
+ return null;
1039
+ const models = this.configCenter.get('models');
1040
+ if (!models)
1041
+ return null;
1042
+ return {
1043
+ assessment: models.assessment?.source ?? 'main',
1044
+ planning: models.planning?.source ?? 'main',
1045
+ compression: models.compression?.source ?? 'main',
1046
+ };
1047
+ }
1048
+ /** 定时任务触发时唤醒 Agent,自动发起一轮对话 */
1049
+ async notifyTaskFired(taskName) {
1050
+ this.pendingTaskNotifications.push({ name: taskName, firedAt: new Date().toISOString() });
1051
+ this.pendingTaskName = taskName;
1052
+ // Router 提供模式对应的提示词风格(陪伴→自然,正常→系统提示)
1053
+ await this.syncRouter();
1054
+ const prompt = this.activeRouter.getTaskPrompt(taskName);
1055
+ await this.run(prompt);
1056
+ this.pendingTaskName = null;
1057
+ }
1058
+ /**
1059
+ * 运行一轮对话(从用户输入到 Agent 停止或调用工具后继续)
1060
+ */
1061
+ async run(userInput) {
1062
+ return new Promise((resolve, reject) => {
1063
+ this._runMutex = this._runMutex.then(async () => {
1064
+ try {
1065
+ await this._runInternal(userInput);
1066
+ resolve();
1067
+ }
1068
+ catch (err) {
1069
+ reject(err);
1070
+ }
1071
+ });
1072
+ });
1073
+ }
1074
+ /**
1075
+ * run() 的内部实现,由串行化锁保护
1076
+ */
1077
+ async _runInternal(userInput) {
1078
+ this.interrupted = false;
1079
+ this.abortController = new AbortController();
1080
+ // 每次用户输入重置防重复检测窗口
1081
+ this.loopGuard.reset();
1082
+ // 启动调度器
1083
+ if (this.scheduler && !this.schedulerInitialized) {
1084
+ await this.scheduler.start();
1085
+ this.schedulerInitialized = true;
1086
+ }
1087
+ // 注入待处理的定时任务通知
1088
+ if (this.pendingTaskNotifications.length > 0) {
1089
+ const notes = this.pendingTaskNotifications.splice(0);
1090
+ for (const note of notes) {
1091
+ const msg = `[Scheduled Task] "${note.name}" triggered at ${note.firedAt}.`;
1092
+ this.outputHandler?.onStatus?.(msg, 'info');
1093
+ }
1094
+ }
1095
+ // 注册 Ctrl+C 中断处理
1096
+ const onSigInt = () => {
1097
+ this.interrupted = true;
1098
+ this.abortController?.abort();
1099
+ this.outputHandler?.onStatus?.('Interrupted. Press Ctrl+C again to exit.', 'warn');
1100
+ };
1101
+ process.once('SIGINT', onSigInt);
1102
+ try {
1103
+ // Load persisted summary from disk (always reload to pick up
1104
+ // updates from other loop instances sharing the same session dir)
1105
+ try {
1106
+ const persisted = await this.summaryStore.load(this.sessionDir);
1107
+ if (persisted) {
1108
+ this.currentSummary = persisted;
1109
+ }
1110
+ }
1111
+ catch { }
1112
+ // 0. 确保 Router 已同步——写用户消息前切换 session,避免跨 session 碎片
1113
+ await this.syncRouter();
1114
+ // 0.5 输入预处理:陪伴模式下剥离 [[旁白]] 并交给旁路(正常模式无此钩子,原样返回)
1115
+ if (this.activeRouter.transformUserInput) {
1116
+ try {
1117
+ userInput = await this.activeRouter.transformUserInput(userInput, this);
1118
+ }
1119
+ catch { /* 旁白处理失败则按原输入继续 */ }
1120
+ }
1121
+ // 1. 将用户输入追加到 conversation(视觉模型自动检测图片路径或渠道预取图片)
1122
+ const activeP = this.getActiveProvider();
1123
+ const hasVision = activeP.getCapabilities?.()?.vision ?? false;
1124
+ let userContent;
1125
+ if (hasVision && this.channelImages && this.channelImages.length > 0) {
1126
+ // 渠道预取图片(飞书/HTTP 等已下载为 base64)
1127
+ userContent = buildUserContentWithInlineImages(userInput, this.channelImages, this.imageStore);
1128
+ this.channelImages = null; // 一次性消费
1129
+ }
1130
+ else if (hasVision) {
1131
+ userContent = await buildUserContentWithImages(userInput, this.imageStore);
1132
+ }
1133
+ else {
1134
+ userContent = { type: 'text', text: userInput };
1135
+ }
1136
+ const userMessage = {
1137
+ role: 'user',
1138
+ content: userContent,
1139
+ };
1140
+ // 纯旁白轮:旁路产出仅作瞬态触发,不落盘(不污染档案、不被误当用户消息)
1141
+ if (!this.activeRouter.ephemeralInput) {
1142
+ await this.conversationStore.append(this.sessionDir, userMessage);
1143
+ }
1144
+ // 记录用户输入事件(瞬态旁白触发不记录,避免与真实用户输入混淆)
1145
+ if (!this.activeRouter.ephemeralInput) {
1146
+ await this.eventStore.append(this.sessionDir, {
1147
+ type: 'user_input',
1148
+ content: userInput,
1149
+ timestamp: formatTimestamp(),
1150
+ });
1151
+ }
1152
+ // 2. 主循环:compose -> LLM -> parse -> tool -> compose
1153
+ // 不设总轮次上限 — 超长开发任务可能需要数百轮。
1154
+ // LoopGuard 跟踪连续触发次数,超过上限后强制停止以防止死循环。
1155
+ let turnCount = 0;
1156
+ let toolWasCalled = false;
1157
+ while (true) {
1158
+ if (this.interrupted) {
1159
+ this.outputHandler?.onStatus?.('Agent stopped by user.', 'info');
1160
+ break;
1161
+ }
1162
+ const result = await this.runTurn();
1163
+ turnCount++;
1164
+ if (result.toolCalled)
1165
+ toolWasCalled = true;
1166
+ // 更新 stats
1167
+ await this.statsManager.increment(this.sessionDir, 'turn_count', 1);
1168
+ // ── 旁路Agent 迭代审查(每次 LLM 回复后) ──────────
1169
+ if (this.bypassManager) {
1170
+ try {
1171
+ const fullHistory = await this.conversationStore.readAll(this.sessionDir);
1172
+ let iterAssistant = '';
1173
+ let iterUser = userInput;
1174
+ for (let i = fullHistory.length - 1; i >= 0; i--) {
1175
+ const m = fullHistory[i];
1176
+ const text = extractTextContent(m.content);
1177
+ if (m.role === 'assistant' && !iterAssistant && text)
1178
+ iterAssistant = text;
1179
+ if (m.role === 'user' && text) {
1180
+ iterUser = text;
1181
+ break;
1182
+ }
1183
+ }
1184
+ const iterCtx = {
1185
+ userInput: iterUser,
1186
+ assistantOutput: iterAssistant,
1187
+ history: [],
1188
+ toolCallsThisTurn: this.recentToolNames ?? [],
1189
+ isLastIteration: false, // 仍在 loop 中
1190
+ };
1191
+ this.bypassManager.postTurn(iterCtx);
1192
+ }
1193
+ catch { /* ignore */ }
1194
+ }
1195
+ if (result.stop) {
1196
+ break;
1197
+ }
1198
+ // LoopGuard 升级检查:连续触发超过上限 → 强制停止
1199
+ if (this.loopGuard.escalated) {
1200
+ this.outputHandler?.onStatus?.('LoopGuard: repeated suppression detected. Stopping to prevent infinite loop.', 'warn');
1201
+ break;
1202
+ }
1203
+ }
1204
+ // ── Post-turn cleanup(由 Router 控制)────────────────────
1205
+ if (this.activeRouter.onPostTurn) {
1206
+ await this.activeRouter.onPostTurn(this, this.pendingTaskName, toolWasCalled);
1207
+ }
1208
+ // ── 旁路Agent postTurn:后台观察,不阻塞 ────────────────
1209
+ if (this.bypassManager) {
1210
+ // 从 jsonl 读取本轮对话(与原来 CompanionRouter.onPostTurn 一致)
1211
+ let postUserInput = userInput;
1212
+ let postAssistantOutput = '';
1213
+ try {
1214
+ const fullHistory = await this.conversationStore.readAll(this.sessionDir);
1215
+ // 取最后一条用户消息和助手消息
1216
+ for (let i = fullHistory.length - 1; i >= 0; i--) {
1217
+ const m = fullHistory[i];
1218
+ const text = extractTextContent(m.content);
1219
+ if (m.role === 'assistant' && !postAssistantOutput && text) {
1220
+ postAssistantOutput = text;
1221
+ }
1222
+ if (m.role === 'user' && text) {
1223
+ postUserInput = text;
1224
+ break; // 只取最后一轮的用户消息
1225
+ }
1226
+ }
1227
+ }
1228
+ catch { /* ignore */ }
1229
+ const postCtx = {
1230
+ userInput: postUserInput,
1231
+ assistantOutput: postAssistantOutput,
1232
+ history: [],
1233
+ toolCallsThisTurn: this.recentToolNames ?? [],
1234
+ isLastIteration: true, // while 循环已退出,这是本轮用户消息的最后一次
1235
+ };
1236
+ this.bypassManager.postTurn(postCtx);
1237
+ // 清除本轮的旁路注入缓存,下一轮用户消息重新 preTurn
1238
+ this._bypassInjections = undefined;
1239
+ }
1240
+ // 每轮结束后回收已处理图片:旧 base64 → 占位符 + 模型描述
1241
+ if (!this.interrupted) {
1242
+ this.recycleProcessedImages().catch(() => { });
1243
+ }
1244
+ }
1245
+ catch (error) {
1246
+ const message = error instanceof Error ? error.message : String(error);
1247
+ this.outputHandler?.onStatus?.(`Provider error: ${message}`, 'error');
1248
+ throw error;
1249
+ }
1250
+ finally {
1251
+ // 清理中断监听
1252
+ process.removeListener('SIGINT', onSigInt);
1253
+ this.abortController = null;
1254
+ }
1255
+ }
1256
+ /**
1257
+ * 内部方法:执行一轮 LLM 调用
1258
+ */
1259
+ async runTurn() {
1260
+ this.currentTurn++;
1261
+ // ── 回合回滚:记录回合开始前状态 ──
1262
+ if (this.turnRecorder) {
1263
+ this.turnRecorder.startTurn(this.currentTurn).catch(err => {
1264
+ this.logger.warn('TurnRecorder startTurn failed', { error: err.message });
1265
+ });
1266
+ }
1267
+ // ── 全局陪伴模式同步 ──────────────────────────────────────────
1268
+ // 所有渠道共享同一份上下文路由。检测全局 Router 变化,
1269
+ // 自动切换当前 loop 的 sessionDir 和上下文行为。
1270
+ await this.syncRouter();
1271
+ // 从 conversation 读取历史
1272
+ const history = await this.conversationStore.readAll(this.sessionDir);
1273
+ // 组装上下文 — 按当前 Router 过滤工具定义
1274
+ const ctxRouter = this.activeRouter;
1275
+ const profile = getActiveProfile(); // 保留向后兼容
1276
+ let toolDefinitions = this.toolRegistry.getToolDefinitions(ctxRouter.name === 'companion');
1277
+ // Router 指定工具白名单时,使用 Router 过滤
1278
+ if (ctxRouter.toolAllowlist.length > 0) {
1279
+ const allowed = new Set(ctxRouter.toolAllowlist);
1280
+ toolDefinitions = toolDefinitions.filter(t => allowed.has(t.name));
1281
+ }
1282
+ else if (this.bundleRegistry) {
1283
+ // 工具包展开:激活时触发激进压缩 + pendingBundleSummary,下轮注入 summary 段(Zone 3)
1284
+ const allowed = this.bundleRegistry.getActiveToolNames();
1285
+ if (allowed.length > 0) {
1286
+ const allowedSet = new Set(allowed);
1287
+ toolDefinitions = toolDefinitions.filter(t => allowedSet.has(t.name));
1288
+ }
1289
+ }
1290
+ // 黑名单过滤:始终生效,优先级高于白名单
1291
+ if (ctxRouter.toolBlacklist.length > 0) {
1292
+ const blocked = new Set(ctxRouter.toolBlacklist);
1293
+ toolDefinitions = toolDefinitions.filter(t => !blocked.has(t.name));
1294
+ }
1295
+ // 获取最后一条 user 消息作为 userInput
1296
+ // 排除纯 tool_result 的 user 消息(避免误删工具结果)
1297
+ const lastUserTextMsg = [...history].reverse().find((m) => m.role === 'user' && hasTextContent(m.content));
1298
+ let userInputText = lastUserTextMsg
1299
+ ? extractTextContent(lastUserTextMsg.content)
1300
+ : '';
1301
+ // 陪伴模式纯旁白轮:本轮 input 来自旁路 LLM 的瞬态产出(未落盘、不在 history 中),仅本轮注入
1302
+ const ephemeralUserInput = this.activeRouter.ephemeralInput ?? null;
1303
+ // 判断是否是工具执行后的续轮(history 中有 tool_use)
1304
+ const hasPendingToolCalls = history.some((m) => m.role === 'assistant' && hasToolUseContent(m.content));
1305
+ // Apply runtime config changes if context is dirty
1306
+ if (this.configCenter && this.contextDirty) {
1307
+ const newThreshold = this.configCenter.get('context.compressThreshold');
1308
+ if (newThreshold != null) {
1309
+ this.compressor.setCompressThreshold(newThreshold);
1310
+ }
1311
+ const newDepth = this.configCenter.get('context.compressDepth');
1312
+ if (newDepth != null) {
1313
+ this.compressor.setCompressDepth(newDepth);
1314
+ }
1315
+ this.contextDirty = false;
1316
+ }
1317
+ // 切换到大窗口→小窗口模型后,强制全量重压缩
1318
+ if (this.needsCompression) {
1319
+ this.outputHandler?.onStatus?.(`Forcing full context recompression to fit new model limit (${this.maxContextTokens.toLocaleString()})`, 'warn');
1320
+ }
1321
+ let uncompressedMsgs = history;
1322
+ let historySummary = this.currentSummary;
1323
+ // 从 history 中排除最后一条 user 文本消息(compose 会重新添加)
1324
+ // 工具执行续轮时保留在历史中供上下文参考,但不清除 userInput 以避免重复注入
1325
+ // 瞬态旁白轮:当前 input 不在 history 中,不剥离任何历史 user 消息
1326
+ const historyWithoutLastUser = hasPendingToolCalls || ephemeralUserInput
1327
+ ? history
1328
+ : lastUserTextMsg
1329
+ ? history.filter((m) => !isSameTextMessage(m, lastUserTextMsg))
1330
+ : history;
1331
+ // 续轮时清空 userInput,防止同一条用户消息被重新注入为"新输入"
1332
+ // 判断依据:历史最末尾不是用户新文本(而是 tool_result),说明是续轮
1333
+ // 如果末尾是用户文本消息(如新的"好了停吧"),则保留 userInput
1334
+ const lastMsg = history[history.length - 1];
1335
+ const hasFreshUserInput = lastMsg?.role === 'user' && hasTextContent(lastMsg.content);
1336
+ if (!hasFreshUserInput) {
1337
+ userInputText = '';
1338
+ }
1339
+ // 纯旁白轮:用旁路瞬态产出覆盖本轮 userInput,并消费一次(续轮/下一轮不再注入)
1340
+ if (ephemeralUserInput) {
1341
+ userInputText = ephemeralUserInput;
1342
+ this.activeRouter.ephemeralInput = null;
1343
+ }
1344
+ // 确定本轮实际使用的 Provider(路由决策前置,确保 compose 看到正确的 providerType)
1345
+ let activeProvider = this.provider;
1346
+ if (this.providerRouter) {
1347
+ activeProvider = this.providerRouter.route({ complexity: 'medium' });
1348
+ }
1349
+ this.activeProvider = activeProvider;
1350
+ // 一次性 fallback 通知(由 onFallback 回调写入,此处消费)
1351
+ if (this.pendingFallbackInfo) {
1352
+ this.outputHandler?.onStatus?.(this.pendingFallbackInfo, 'warn');
1353
+ this.pendingFallbackInfo = null;
1354
+ }
1355
+ // 精确模式/陪伴模式:应用策略
1356
+ let effectiveHistory = historyWithoutLastUser;
1357
+ let effectivePersonaDir = this.personaDir;
1358
+ // Router 处理历史过滤(陪伴模式的 tool 轮次剥离等)
1359
+ effectiveHistory = hasPendingToolCalls
1360
+ ? historyWithoutLastUser
1361
+ : this.activeRouter.filterHistory(historyWithoutLastUser);
1362
+ // 精确模式(ComposeStrategy):叠加关键词过滤 + analyzeTurn
1363
+ if (this.composeStrategy) {
1364
+ const opts = this.composeStrategy.prepareCompose(this.personaDir);
1365
+ effectivePersonaDir = opts.personaDir;
1366
+ if (this.composeStrategy.name === 'precise') {
1367
+ effectiveHistory = this.composeStrategy.filterHistory(effectiveHistory);
1368
+ }
1369
+ if (opts.preciseMode) {
1370
+ this.contextComposer.activeConditions.add('precise_mode');
1371
+ }
1372
+ else {
1373
+ this.contextComposer.activeConditions.delete('precise_mode');
1374
+ }
1375
+ }
1376
+ // 处理待注入图片队列(仅视觉模型)
1377
+ if (this.pendingImageInjections.length > 0 && (activeProvider.getCapabilities?.()?.vision ?? false)) {
1378
+ for (const pi of this.pendingImageInjections) {
1379
+ const imgMsg = {
1380
+ role: 'user',
1381
+ content: [
1382
+ { type: 'image', source: { type: 'base64', media_type: pi.media_type, data: pi.data } },
1383
+ { type: 'text', text: `[Re-examining Image #${pi.imgId}]` },
1384
+ ],
1385
+ };
1386
+ await this.conversationStore.append(this.sessionDir, imgMsg);
1387
+ effectiveHistory = [...effectiveHistory, imgMsg];
1388
+ }
1389
+ this.pendingImageInjections.length = 0;
1390
+ }
1391
+ // 更新知识库检索查询(必须在 compose 之前,确保 Zone 4 读到当前轮提问)
1392
+ if (this.kbState) {
1393
+ this.kbState.lastQuery = userInputText;
1394
+ }
1395
+ // ── 旁路Agent preTurn:仅在首轮迭代运行,后续复用缓存 ───
1396
+ if (this._bypassInjections === undefined && this.bypassManager) {
1397
+ const preTurnCtx = {
1398
+ userInput: userInputText,
1399
+ recentHistory: (history ?? []).slice(-20),
1400
+ contextBudget: { used: this.lastContextTokens, total: this.maxContextTokens },
1401
+ recentToolCalls: this.recentToolNames ?? [],
1402
+ };
1403
+ const preTurnResult = await this.bypassManager.preTurn(preTurnCtx);
1404
+ if (preTurnResult.transformedInput !== undefined) {
1405
+ userInputText = preTurnResult.transformedInput;
1406
+ }
1407
+ this._bypassInjections = preTurnResult.injections;
1408
+ }
1409
+ // 合并 preTurn 产出 + postTurn 运行时注入(如纠正)
1410
+ const runtimeInjections = this.bypassManager?.consumeInjections() ?? [];
1411
+ const bypassInjections = [
1412
+ ...(this._bypassInjections ?? []),
1413
+ ...runtimeInjections,
1414
+ ];
1415
+ // 运行时注入消费后即清空,不带到下一轮
1416
+ if (runtimeInjections.length > 0) {
1417
+ // 不修改 _bypassInjections,只在本轮使用合并结果
1418
+ }
1419
+ // Compose with layered options
1420
+ const layeredResult = await this.contextComposer.compose({
1421
+ sessionDir: this.sessionDir,
1422
+ providerType: activeProvider.getProviderType(),
1423
+ maxContextTokens: this.maxContextTokens,
1424
+ cwd: process.cwd(),
1425
+ timestamp: formatTimestamp(),
1426
+ tools: toolDefinitions,
1427
+ history: effectiveHistory,
1428
+ userInput: userInputText,
1429
+ historySummary,
1430
+ currentPlan: this.activePlan ? formatPlanAsText(this.activePlan) : undefined,
1431
+ zone3Hashes: undefined,
1432
+ impactInfo: this.pendingImpactInfo ?? undefined,
1433
+ fullHistory: history,
1434
+ personaDir: effectivePersonaDir,
1435
+ gitManager: this.gitManager,
1436
+ profile,
1437
+ bypassInjections,
1438
+ });
1439
+ this.pendingImpactInfo = null; // 清除已使用的影响面信息
1440
+ const messages = layeredResult.messages;
1441
+ // Flow 注入在 Zone 5(flow_injection),由 manifest 统一管理
1442
+ // 更新 current_context_tokens 到 Stats
1443
+ this.lastContextTokens = layeredResult.zoneBreakdown.total;
1444
+ // ── 压缩触发:compose 后检测 Zone 总 token 是否超过阈值 ──
1445
+ const compressThreshold = this.configCenter
1446
+ ? this.configCenter.get('context.compressThreshold') ?? 0.75
1447
+ : 0.75;
1448
+ const emergencyThreshold = this.configCenter
1449
+ ? this.configCenter.get('context.emergencyThreshold') ?? 0.92
1450
+ : 0.92;
1451
+ // Step 1: 消费上一轮的后台压缩结果
1452
+ if (this.pendingCompression) {
1453
+ const compressionResult = await this.pendingCompression;
1454
+ this.pendingCompression = null;
1455
+ if (compressionResult) {
1456
+ const compressedHistory = compressionResult.messages;
1457
+ historySummary = compressionResult.summary || this.currentSummary;
1458
+ await this.conversationStore.replace(this.sessionDir, compressedHistory);
1459
+ // 更新 uncompressedMsgs 为压缩后的消息,避免 Step 2 用旧数据再次压缩
1460
+ uncompressedMsgs = compressedHistory;
1461
+ if (compressionResult.summary) {
1462
+ this.currentSummary = compressionResult.summary;
1463
+ if (compressionResult.summary !== this.lastSavedSummary) {
1464
+ await this.summaryStore.save(this.sessionDir, compressionResult.summary);
1465
+ this.lastSavedSummary = compressionResult.summary;
1466
+ }
1467
+ }
1468
+ if (compressionResult.phasesUsed.length > 0) {
1469
+ this.compressCount++;
1470
+ await this.statsManager.increment(this.sessionDir, 'compact_count', 1);
1471
+ }
1472
+ // 重新 compose(用压缩后的 history)
1473
+ const compressedHistoryWithoutLastUser = hasPendingToolCalls
1474
+ ? compressedHistory
1475
+ : lastUserTextMsg
1476
+ ? compressedHistory.filter((m) => !isSameTextMessage(m, lastUserTextMsg))
1477
+ : compressedHistory;
1478
+ const reLayeredResult = await this.contextComposer.compose({
1479
+ sessionDir: this.sessionDir,
1480
+ providerType: activeProvider.getProviderType(),
1481
+ maxContextTokens: this.maxContextTokens,
1482
+ cwd: process.cwd(),
1483
+ timestamp: formatTimestamp(),
1484
+ tools: toolDefinitions,
1485
+ history: compressedHistoryWithoutLastUser,
1486
+ userInput: userInputText,
1487
+ historySummary,
1488
+ currentPlan: this.activePlan ? formatPlanAsText(this.activePlan) : undefined,
1489
+ zone3Hashes: undefined,
1490
+ impactInfo: this.pendingImpactInfo ?? undefined,
1491
+ fullHistory: history,
1492
+ personaDir: this.personaDir,
1493
+ gitManager: this.gitManager,
1494
+ profile,
1495
+ bypassInjections,
1496
+ });
1497
+ layeredResult.messages.length = 0;
1498
+ layeredResult.messages.push(...reLayeredResult.messages);
1499
+ layeredResult.zoneBreakdown = reLayeredResult.zoneBreakdown;
1500
+ // 更新 lastContextTokens 为压缩后的实际值
1501
+ const preCompressTokens = this.lastContextTokens;
1502
+ this.lastContextTokens = reLayeredResult.zoneBreakdown.total;
1503
+ // 输出压缩结果(使用实际 token 数)
1504
+ if (compressionResult.phasesUsed.length > 0) {
1505
+ this.outputHandler?.onStatus?.(`compress-result:${preCompressTokens}:${this.lastContextTokens}`, 'info');
1506
+ }
1507
+ // 激进压缩兜底检测:常规压缩后仍超标
1508
+ if (reLayeredResult.zoneBreakdown.total > this.maxContextTokens * compressThreshold) {
1509
+ if (!this.needsAggressiveCompress) {
1510
+ this.needsAggressiveCompress = true;
1511
+ this.outputHandler?.onStatus?.(`Compression insufficient (${reLayeredResult.zoneBreakdown.total.toLocaleString()} > ${Math.floor(this.maxContextTokens * compressThreshold).toLocaleString()}), will unprotect recent messages next turn`, 'warn');
1512
+ }
1513
+ else {
1514
+ this.logger.error(`Compressor failed to reduce context below safety threshold: ${reLayeredResult.zoneBreakdown.total}/${this.maxContextTokens}`);
1515
+ this.needsAggressiveCompress = false;
1516
+ this.outputHandler?.onStatus?.(`Compressor failed after aggressive compression, continuing with ${reLayeredResult.zoneBreakdown.total.toLocaleString()} tokens`, 'error');
1517
+ }
1518
+ }
1519
+ else if (this.needsAggressiveCompress) {
1520
+ this.needsAggressiveCompress = false;
1521
+ }
1522
+ }
1523
+ }
1524
+ // Step 2: 当前轮次超标 → 异步或同步压缩
1525
+ const currentTokens = layeredResult.zoneBreakdown.total;
1526
+ // 压缩条件:token 超阈值,或 trigger_compression 主动要求
1527
+ if (currentTokens > this.maxContextTokens * compressThreshold || this.needsCompression) {
1528
+ this.needsCompression = false;
1529
+ const zone5TailBudget = Math.floor(this.maxContextTokens * 0.15);
1530
+ const protectCount = this.needsAggressiveCompress
1531
+ ? 0
1532
+ : computeProtectCount(uncompressedMsgs, zone5TailBudget);
1533
+ // 紧急阈值:上下文接近爆满 → 同步压缩,停主对话等结果
1534
+ if (currentTokens > this.maxContextTokens * emergencyThreshold) {
1535
+ this.outputHandler?.onStatus?.(`⚠ Emergency: ${currentTokens.toLocaleString()} tokens (${Math.round(currentTokens / this.maxContextTokens * 100)}%) — compressing synchronously to prevent overflow`, 'warn');
1536
+ if (uncompressedMsgs && uncompressedMsgs.length > 0) {
1537
+ this.outputHandler?.onStatus?.('compress-start', 'info');
1538
+ try {
1539
+ const emergencyResult = await this.compressor.compress(uncompressedMsgs, this.currentSummary, 0, // 不保护最近消息
1540
+ this.maxContextTokens, undefined);
1541
+ if (emergencyResult) {
1542
+ const compressedHistory = emergencyResult.messages;
1543
+ historySummary = emergencyResult.summary || this.currentSummary;
1544
+ await this.conversationStore.replace(this.sessionDir, compressedHistory);
1545
+ uncompressedMsgs = compressedHistory;
1546
+ if (emergencyResult.summary) {
1547
+ this.currentSummary = emergencyResult.summary;
1548
+ if (emergencyResult.summary !== this.lastSavedSummary) {
1549
+ await this.summaryStore.save(this.sessionDir, emergencyResult.summary);
1550
+ this.lastSavedSummary = emergencyResult.summary;
1551
+ }
1552
+ }
1553
+ if (emergencyResult.phasesUsed.length > 0) {
1554
+ this.compressCount++;
1555
+ await this.statsManager.increment(this.sessionDir, 'compact_count', 1);
1556
+ }
1557
+ // 重新 compose
1558
+ const emergencyHistory = hasPendingToolCalls
1559
+ ? compressedHistory
1560
+ : lastUserTextMsg
1561
+ ? compressedHistory.filter((m) => !isSameTextMessage(m, lastUserTextMsg))
1562
+ : compressedHistory;
1563
+ const reLayeredResult = await this.contextComposer.compose({
1564
+ sessionDir: this.sessionDir,
1565
+ providerType: activeProvider.getProviderType(),
1566
+ maxContextTokens: this.maxContextTokens,
1567
+ cwd: process.cwd(),
1568
+ timestamp: formatTimestamp(),
1569
+ tools: toolDefinitions,
1570
+ history: emergencyHistory,
1571
+ userInput: userInputText,
1572
+ historySummary,
1573
+ currentPlan: this.activePlan ? formatPlanAsText(this.activePlan) : undefined,
1574
+ zone3Hashes: undefined,
1575
+ impactInfo: this.pendingImpactInfo ?? undefined,
1576
+ fullHistory: history,
1577
+ personaDir: this.personaDir,
1578
+ gitManager: this.gitManager,
1579
+ profile,
1580
+ bypassInjections,
1581
+ });
1582
+ layeredResult.messages.length = 0;
1583
+ layeredResult.messages.push(...reLayeredResult.messages);
1584
+ layeredResult.zoneBreakdown = reLayeredResult.zoneBreakdown;
1585
+ const preTokens = this.lastContextTokens;
1586
+ this.lastContextTokens = reLayeredResult.zoneBreakdown.total;
1587
+ this.outputHandler?.onStatus?.(`compress-result:${preTokens}:${this.lastContextTokens}`, 'info');
1588
+ }
1589
+ }
1590
+ catch (err) {
1591
+ this.logger.warn('Emergency compression failed', { error: err?.message ?? String(err) });
1592
+ }
1593
+ finally {
1594
+ this.outputHandler?.onStatus?.('compress-end', 'info');
1595
+ }
1596
+ }
1597
+ }
1598
+ else {
1599
+ // 正常阈值:异步后台压缩(不阻塞 LLM 调用)
1600
+ this.outputHandler?.onStatus?.(`Context ${currentTokens.toLocaleString()} > ${Math.floor(this.maxContextTokens * compressThreshold).toLocaleString()} → compressing in background${this.needsAggressiveCompress ? ' (recent messages unprotected)' : ''} (protect: ${protectCount} msgs)`, 'warn');
1601
+ if (uncompressedMsgs && uncompressedMsgs.length > 0) {
1602
+ this.outputHandler?.onStatus?.('compress-start', 'info');
1603
+ this.pendingCompression = this.compressor.compress(uncompressedMsgs, this.currentSummary, protectCount, this.maxContextTokens, undefined).catch((err) => {
1604
+ this.logger.warn('Background compression failed', err);
1605
+ return null;
1606
+ }).finally(() => {
1607
+ this.outputHandler?.onStatus?.('compress-end', 'info');
1608
+ });
1609
+ }
1610
+ }
1611
+ }
1612
+ await this.statsManager.update(this.sessionDir, {
1613
+ current_context_tokens: layeredResult.zoneBreakdown.total,
1614
+ });
1615
+ // 调用 provider 流式请求 LLM
1616
+ // 动态读取 thinking 配置(TUI /think 命令可运行时切换)
1617
+ const thinkingEnabled = this.configCenter?.get('provider.enableThinking') ?? false;
1618
+ const thinkingEffort = thinkingEnabled
1619
+ ? getModelInfo(this.provider.getProviderType(), this.provider.getModel())?.reasoningEffort
1620
+ : undefined;
1621
+ this.getActiveProvider().setThinking?.(thinkingEnabled, thinkingEffort);
1622
+ const stream = activeProvider.createStream(messages, toolDefinitions, this.abortController?.signal);
1623
+ // 使用 OutputRouter 解析流式输出
1624
+ const router = new OutputRouter();
1625
+ // 累积 assistant 回复内容
1626
+ const textParts = [];
1627
+ const thinkingParts = [];
1628
+ const toolCalls = [];
1629
+ let usageInput = 0;
1630
+ let usageOutput = 0;
1631
+ let stopReason;
1632
+ const oh = this.outputHandler;
1633
+ oh?.onTurnStart?.();
1634
+ router.onText = (content) => {
1635
+ oh?.onText?.(content);
1636
+ textParts.push(content);
1637
+ // Write event (fire-and-forget)
1638
+ appendEvent(this.sessionDir, {
1639
+ type: 'text',
1640
+ content,
1641
+ timestamp: new Date().toISOString(),
1642
+ }).catch(() => { });
1643
+ };
1644
+ router.onThinking = (content) => {
1645
+ oh?.onThinking?.(content);
1646
+ thinkingParts.push(content);
1647
+ appendEvent(this.sessionDir, {
1648
+ type: 'thinking',
1649
+ content,
1650
+ timestamp: new Date().toISOString(),
1651
+ }).catch(() => { });
1652
+ };
1653
+ router.onToolUse = (id, name, input) => {
1654
+ const inputSummary = summarizeToolInput(input);
1655
+ oh?.onToolUse?.(name, inputSummary, id);
1656
+ toolCalls.push({ id, name, input });
1657
+ appendEvent(this.sessionDir, {
1658
+ type: 'tool_call',
1659
+ id,
1660
+ name,
1661
+ input,
1662
+ timestamp: new Date().toISOString(),
1663
+ }).catch(() => { });
1664
+ };
1665
+ router.onUsage = (inputTokens, outputTokens, hit, miss, anthroRead, anthroCreation) => {
1666
+ usageInput = inputTokens;
1667
+ usageOutput = outputTokens;
1668
+ // 归一化:DeepSeek/OpenAI 用 cache_hit_tokens/cache_miss_tokens,
1669
+ // Anthropic 用 cache_read_input_tokens(命中)/ cache_creation_input_tokens(新建)。
1670
+ // 优先使用 DeepSeek 格式,否则从 Anthropic 字段推导。
1671
+ let effectiveHit;
1672
+ let effectiveMiss;
1673
+ if (hit !== undefined && miss !== undefined) {
1674
+ // DeepSeek / OpenAI 格式:直接使用
1675
+ effectiveHit = hit;
1676
+ effectiveMiss = miss;
1677
+ }
1678
+ else if (anthroRead !== undefined && inputTokens > 0) {
1679
+ // Anthropic 格式:cache_read 是命中量,未命中 = 总量 - 命中
1680
+ effectiveHit = anthroRead;
1681
+ effectiveMiss = Math.max(0, inputTokens - anthroRead);
1682
+ }
1683
+ if (effectiveHit !== undefined && effectiveMiss !== undefined) {
1684
+ this.cacheHitTokens = effectiveHit;
1685
+ this.cacheMissTokens = effectiveMiss;
1686
+ if (this.logCacheHits) {
1687
+ const total = effectiveHit + effectiveMiss;
1688
+ const hitRate = total > 0 ? (effectiveHit / total) * 100 : 0;
1689
+ const record = {
1690
+ turn: this.currentTurn,
1691
+ timestamp: new Date().toISOString(),
1692
+ inputTokens,
1693
+ outputTokens,
1694
+ hitTokens: effectiveHit,
1695
+ missTokens: effectiveMiss,
1696
+ hitRate: Math.round(hitRate * 100) / 100,
1697
+ };
1698
+ this.cacheTurns.push(record);
1699
+ }
1700
+ }
1701
+ };
1702
+ router.onStop = (reason) => {
1703
+ stopReason = reason;
1704
+ };
1705
+ // 消费流 (inline tool execution: dispatch tools immediately when TOOL_USE arrives)
1706
+ const inlineToolPromises = [];
1707
+ try {
1708
+ for await (const event of stream) {
1709
+ if (this.interrupted)
1710
+ break;
1711
+ router.route(event);
1712
+ // Inline tool execution: execute immediately when TOOL_USE arrives in the stream
1713
+ if (event.type === 'TOOL_USE') {
1714
+ const { id, name, input } = event;
1715
+ if (id && name) {
1716
+ this.inlineToolExecuted = true;
1717
+ inlineToolPromises.push(this.executeSingleToolInline(id, name, input));
1718
+ }
1719
+ }
1720
+ }
1721
+ }
1722
+ catch (err) {
1723
+ const name = (err instanceof Error) ? err.name : '';
1724
+ if (name === 'AbortError' || name === 'APIUserAbortError') {
1725
+ // 用户中断,正常退出
1726
+ }
1727
+ else {
1728
+ throw err;
1729
+ }
1730
+ }
1731
+ // Wait for all inline tool executions to complete before proceeding
1732
+ if (inlineToolPromises.length > 0) {
1733
+ await Promise.allSettled(inlineToolPromises);
1734
+ }
1735
+ // 去重: ResilientProvider 重试流时可能累积重复的 toolCalls,
1736
+ // 保留 last-wins(成功重试的那份),清理孤儿 inlineToolResults
1737
+ if (toolCalls.length > 0) {
1738
+ const seen = new Map();
1739
+ for (let i = 0; i < toolCalls.length; i++) {
1740
+ const key = `${toolCalls[i].name}|${JSON.stringify(toolCalls[i].input)}`;
1741
+ seen.set(key, i);
1742
+ }
1743
+ const deduped = new Set(seen.values());
1744
+ const orphanIds = new Set();
1745
+ for (let i = 0; i < toolCalls.length; i++) {
1746
+ if (!deduped.has(i)) {
1747
+ orphanIds.add(toolCalls[i].id);
1748
+ }
1749
+ }
1750
+ if (orphanIds.size > 0) {
1751
+ this.logger.debug(`Deduped ${orphanIds.size} orphan tool call(s) from stream retry`);
1752
+ for (const id of orphanIds) {
1753
+ this.inlineToolResults.delete(id);
1754
+ }
1755
+ const kept = toolCalls.filter((_, i) => deduped.has(i));
1756
+ toolCalls.length = 0;
1757
+ toolCalls.push(...kept);
1758
+ }
1759
+ }
1760
+ // Scavenge: recover tool calls from thinking/text content that the model forgot to declare
1761
+ const scavengeEnabled = this.configCenter
1762
+ ? this.configCenter.get('repair.scavenge.enabled')
1763
+ : true;
1764
+ if (scavengeEnabled !== false && toolCalls.length === 0 && thinkingParts.length > 0) {
1765
+ const scavenged = scavengeToolCalls(thinkingParts, textParts, toolCalls);
1766
+ if (scavenged.length > toolCalls.length) {
1767
+ const newCalls = scavenged.filter(c => c.id.startsWith('scvg_'));
1768
+ this.logger.debug(`Scavenged ${newCalls.length} tool(s) from thinking: ${newCalls.map(c => c.name).join(', ')}`);
1769
+ newCalls.forEach(c => {
1770
+ this.outputHandler?.onToolUse?.(c.name, JSON.stringify(c.input).slice(0, 80), c.id);
1771
+ });
1772
+ toolCalls.length = 0;
1773
+ toolCalls.push(...scavenged);
1774
+ }
1775
+ }
1776
+ // thinking-only 模型兜底:thinking 有内容但 text 为空时,提升 thinking 为 text
1777
+ // (如 Qwen3.5 thinking 模式只输出 reasoning_content,不输出 content)
1778
+ if (textParts.length === 0 && thinkingParts.length > 0) {
1779
+ const thinkingText = thinkingParts.join('');
1780
+ textParts.push(thinkingText);
1781
+ // 通过 outputHandler 让 TUI 显示这段内容
1782
+ oh?.onText?.(thinkingText);
1783
+ }
1784
+ // 输出刷新
1785
+ if (textParts.length > 0 || thinkingParts.length > 0) {
1786
+ oh?.onFlush?.();
1787
+ }
1788
+ // 更新 stats
1789
+ const currentStats = await this.statsManager.get(this.sessionDir);
1790
+ const statsUpdate = {
1791
+ input_tokens: currentStats.input_tokens + usageInput,
1792
+ output_tokens: currentStats.output_tokens + usageOutput,
1793
+ };
1794
+ if (this.logCacheHits) {
1795
+ statsUpdate.cache_turns = this.cacheTurns.length > 0 ? this.cacheTurns : currentStats.cache_turns;
1796
+ }
1797
+ await this.statsManager.update(this.sessionDir, statsUpdate);
1798
+ // 记录 usage 事件
1799
+ if (usageInput > 0 || usageOutput > 0) {
1800
+ await this.eventStore.append(this.sessionDir, {
1801
+ type: 'usage',
1802
+ input_tokens: usageInput,
1803
+ output_tokens: usageOutput,
1804
+ timestamp: formatTimestamp(),
1805
+ });
1806
+ }
1807
+ // 工作流完成检测在工具执行后进行(工具可能完成最后一步)
1808
+ // 异步分析本轮对话(精确模式关键词提取)
1809
+ if (this.composeStrategy && this.composeStrategy.name === 'precise') {
1810
+ const allMsgs = await this.conversationStore.readAll(this.sessionDir);
1811
+ this.composeStrategy.analyzeTurn(allMsgs, activeProvider).catch(() => { });
1812
+ }
1813
+ // 构建 assistant 消息内容
1814
+ const assistantContent = [];
1815
+ // thinking 作为独立内容块
1816
+ if (thinkingParts.length > 0) {
1817
+ assistantContent.push({ type: 'thinking', thinking: thinkingParts.join('') });
1818
+ }
1819
+ // text 作为独立内容块
1820
+ if (textParts.length > 0) {
1821
+ assistantContent.push({ type: 'text', text: textParts.join('') });
1822
+ }
1823
+ // 添加工具调用(Flow 工具不记入历史,只记事件)
1824
+ for (const tc of toolCalls) {
1825
+ if (!isFlowTool(tc.name)) {
1826
+ assistantContent.push({
1827
+ type: 'tool_use',
1828
+ id: tc.id,
1829
+ name: tc.name,
1830
+ input: tc.input,
1831
+ });
1832
+ }
1833
+ // 事件记录保留全部(含 Flow 工具),用于诊断
1834
+ await this.eventStore.append(this.sessionDir, {
1835
+ type: 'tool_call',
1836
+ tool_name: tc.name,
1837
+ tool_use_id: tc.id,
1838
+ timestamp: formatTimestamp(),
1839
+ });
1840
+ }
1841
+ // 追加 assistant 消息到 conversation
1842
+ if (assistantContent.length > 0) {
1843
+ const assistantMessage = {
1844
+ role: 'assistant',
1845
+ content: assistantContent,
1846
+ };
1847
+ await this.conversationStore.append(this.sessionDir, assistantMessage);
1848
+ }
1849
+ // 如果有工具调用,执行工具并将结果追加到 conversation
1850
+ if (toolCalls.length > 0) {
1851
+ // 更新 recentToolNames 用于模式检测
1852
+ this.recentToolNames = toolCalls.map(tc => tc.name);
1853
+ // Update plan progress based on tool calls
1854
+ if (this.activePlan && toolCalls.length > 0) {
1855
+ const updatedPlan = this.orchestrator.updatePlanProgress(this.activePlan, toolCalls[0].name);
1856
+ this.activePlan = updatedPlan;
1857
+ }
1858
+ if (this.inlineToolExecuted) {
1859
+ // Tools were executed inline during the stream — flush results to conversation
1860
+ await this.flushInlineToolResults(toolCalls);
1861
+ this.inlineToolExecuted = false;
1862
+ this.inlineToolResults.clear();
1863
+ }
1864
+ else {
1865
+ // Fallback: execute tools after stream (for providers that don't emit TOOL_USE events mid-stream)
1866
+ await this.executeTools(toolCalls);
1867
+ }
1868
+ // ── Flow 步骤完成检测 ──
1869
+ // flow_complete 工具内部已执行 advance() + guard 检查,
1870
+ // 此处仅做 post-advance 清理(terminal → deactivate 已在工具内处理)
1871
+ if (toolCalls.some(tc => tc.name === 'flow_complete')) {
1872
+ const active = this.flowRegistry.getActive();
1873
+ if (!active) {
1874
+ // Flow 已在工具内完成并清理,无需额外处理
1875
+ }
1876
+ }
1877
+ // 工具执行完毕后,不停止,继续下一轮
1878
+ await this.checkTextLoop(textParts);
1879
+ // ── 回合回滚:回合结束记录 ──
1880
+ if (this.turnRecorder) {
1881
+ this.turnRecorder.endTurn().catch(err => {
1882
+ this.logger.warn('TurnRecorder endTurn failed', { error: err.message });
1883
+ });
1884
+ }
1885
+ return { stop: false, toolCalled: true };
1886
+ }
1887
+ // 没有 tool_calls,说明 Agent 正常结束
1888
+ await this.checkTextLoop(textParts);
1889
+ appendEvent(this.sessionDir, {
1890
+ type: 'stop',
1891
+ reason: stopReason || 'end_turn',
1892
+ timestamp: new Date().toISOString(),
1893
+ }).catch(() => { });
1894
+ // ── 回合回滚:回合结束记录 ──
1895
+ if (this.turnRecorder) {
1896
+ this.turnRecorder.endTurn().catch(err => {
1897
+ this.logger.warn('TurnRecorder endTurn failed', { error: err.message });
1898
+ });
1899
+ }
1900
+ return { stop: true, stopReason: stopReason ?? 'end_turn' };
1901
+ }
1902
+ /**
1903
+ * 检测本轮文本输出是否陷入循环。
1904
+ * 在 assistant 消息和工具结果全部写入 conversation 后调用,
1905
+ * 确保反射消息位于本轮对话末尾,下一轮 compose 时 LLM 能正确感知上下文。
1906
+ */
1907
+ async checkTextLoop(textParts) {
1908
+ const textLoopEnabled = this.configCenter
1909
+ ? this.configCenter.get('repair.textLoop.enabled')
1910
+ : true;
1911
+ if (textLoopEnabled === false || textParts.length === 0)
1912
+ return;
1913
+ const fullText = textParts.join('');
1914
+ const { loop, reflection } = this.loopGuard.checkTextOutput(fullText);
1915
+ if (loop && reflection) {
1916
+ const loopMsg = {
1917
+ role: 'user',
1918
+ content: { type: 'text', text: reflection },
1919
+ };
1920
+ await this.conversationStore.append(this.sessionDir, loopMsg);
1921
+ this.outputHandler?.onStatus?.(reflection, 'warn');
1922
+ // 文本循环连续触发:直接标记强制 stop(与工具抑制共享 escalated 判断)
1923
+ // LoopGuard.escalated 由 guardCount 驱动,checkTextOutput 已累加
1924
+ }
1925
+ }
1926
+ /** Check if a bash command matches any allowedCommands glob pattern */
1927
+ isCommandAllowed(command) {
1928
+ if (!command)
1929
+ return false;
1930
+ const trimmed = command.trim();
1931
+ for (const pattern of this.allowedCommands) {
1932
+ if (trimmed === pattern)
1933
+ return true;
1934
+ if (pattern.includes('*')) {
1935
+ const re = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
1936
+ if (re.test(trimmed))
1937
+ return true;
1938
+ }
1939
+ }
1940
+ return false;
1941
+ }
1942
+ /**
1943
+ * 内部方法:执行工具调用并写回结果
1944
+ */
1945
+ async executeTools(toolCalls) {
1946
+ const permittedCalls = [];
1947
+ for (const tc of toolCalls) {
1948
+ if (this.dangerousTools.has(tc.name) && this.outputHandler?.onPermissionRequest) {
1949
+ // Step 0: AOR — unrestricted mode, skip all permissions
1950
+ if (this.unrestrictedTools) {
1951
+ permittedCalls.push(tc);
1952
+ continue;
1953
+ }
1954
+ // Step 1: check session allowlist
1955
+ if (this.allowlistTools.has(tc.name)) {
1956
+ permittedCalls.push(tc);
1957
+ continue;
1958
+ }
1959
+ // Step 2: check allowedCommands glob (bash only)
1960
+ if (tc.name === 'bash' && this.isCommandAllowed(tc.input?.command)) {
1961
+ permittedCalls.push(tc);
1962
+ continue;
1963
+ }
1964
+ // Step 3: pop permission
1965
+ const result = await this.outputHandler.onPermissionRequest(tc.name, tc.input);
1966
+ if (result === 'no') {
1967
+ // Permission denied — add error result directly to conversation
1968
+ const deniedMessage = {
1969
+ role: 'user',
1970
+ content: {
1971
+ type: 'tool_result',
1972
+ tool_use_id: tc.id,
1973
+ content: 'Permission denied by user',
1974
+ is_error: true,
1975
+ },
1976
+ };
1977
+ await this.conversationStore.append(this.sessionDir, deniedMessage);
1978
+ this.outputHandler?.onToolResult?.('Permission denied by user', true, tc.id);
1979
+ continue;
1980
+ }
1981
+ if (result === 'aor') {
1982
+ this.unrestrictedTools = true;
1983
+ this.outputHandler?.onStatus?.('AOR mode enabled — all future tool calls unrestricted', 'warn');
1984
+ }
1985
+ // 'yes', 'always', or 'aor' — allow this call
1986
+ if (result === 'always') {
1987
+ this.allowlistTools.add(tc.name);
1988
+ sessionAllowlist.addTool(this.sessionDir, tc.name).catch(() => { });
1989
+ if (tc.name === 'bash' && tc.input?.command) {
1990
+ sessionAllowlist.addCommand(this.sessionDir, tc.input.command).catch(() => { });
1991
+ }
1992
+ }
1993
+ }
1994
+ permittedCalls.push(tc);
1995
+ }
1996
+ if (permittedCalls.length === 0)
1997
+ return;
1998
+ // LoopGuard tool check: detect and suppress repeated identical tool calls
1999
+ const stormEnabled = this.configCenter
2000
+ ? this.configCenter.get('repair.storm.enabled')
2001
+ : true;
2002
+ let executableCalls = permittedCalls;
2003
+ if (stormEnabled !== false) {
2004
+ const { suppressed, reflections } = this.loopGuard.checkToolCalls(permittedCalls);
2005
+ const suppressedCalls = [];
2006
+ executableCalls = [];
2007
+ for (const tc of permittedCalls) {
2008
+ if (suppressed.has(tc.id)) {
2009
+ suppressedCalls.push(tc);
2010
+ }
2011
+ else {
2012
+ executableCalls.push(tc);
2013
+ }
2014
+ }
2015
+ // Inject reflection for suppressed calls
2016
+ for (const tc of suppressedCalls) {
2017
+ const reflectionText = reflections.get(tc.id) ?? ToolGuard.reflectionPrompt(tc);
2018
+ const reflectionMsg = {
2019
+ role: 'user',
2020
+ content: {
2021
+ type: 'tool_result',
2022
+ tool_use_id: tc.id,
2023
+ content: `[Storm suppressed] ${reflectionText}`,
2024
+ is_error: true,
2025
+ },
2026
+ };
2027
+ await this.conversationStore.append(this.sessionDir, reflectionMsg);
2028
+ this.outputHandler?.onToolResult?.(`Storm suppressed: ${tc.name}`, true, tc.id);
2029
+ }
2030
+ }
2031
+ if (executableCalls.length === 0)
2032
+ return;
2033
+ // ── 回合回滚:记录写操作的前置状态 ──
2034
+ if (this.turnRecorder) {
2035
+ const projectDir = this.gitManager.getRepoPath();
2036
+ for (const tc of executableCalls) {
2037
+ if (['write', 'edit', 'multi_edit'].includes(tc.name)) {
2038
+ const filePath = tc.input.file_path;
2039
+ if (filePath) {
2040
+ this.turnRecorder.recordPreState(path.resolve(projectDir, filePath));
2041
+ }
2042
+ }
2043
+ }
2044
+ }
2045
+ // Execute permitted tools
2046
+ const results = await this.toolExecutor.executeParallel(executableCalls);
2047
+ // ── 回合回滚:记录 bash 命令 ──
2048
+ if (this.turnRecorder) {
2049
+ for (const tc of executableCalls) {
2050
+ if (tc.name === 'bash') {
2051
+ const cmd = tc.input.command;
2052
+ if (cmd)
2053
+ this.turnRecorder.recordCommand(cmd);
2054
+ }
2055
+ }
2056
+ }
2057
+ // 影响面分析:检查是否有 edit 或 write 工具被调用
2058
+ if (this.dependencyAnalyzer && permittedCalls.some(tc => tc.name === 'edit' || tc.name === 'write')) {
2059
+ const editedFiles = permittedCalls
2060
+ .filter(tc => tc.name === 'edit' || tc.name === 'write')
2061
+ .map(tc => tc.input.file_path)
2062
+ .filter(Boolean);
2063
+ if (editedFiles.length > 0) {
2064
+ // 先增量更新依赖图
2065
+ await this.dependencyAnalyzer.incrementalUpdate(editedFiles, this.gitManager);
2066
+ // 再查询影响面
2067
+ const impacts = editedFiles.map(f => this.dependencyAnalyzer.getImpact(f));
2068
+ const impactLines = impacts
2069
+ .filter(i => i.allImpacts.length > 0)
2070
+ .map(i => `[Dependency Impact] ${i.sourceFile} → affects: ${i.allImpacts.join(', ')}`);
2071
+ if (impactLines.length > 0) {
2072
+ this.pendingImpactInfo = impactLines.join('\n');
2073
+ }
2074
+ }
2075
+ }
2076
+ // 将工具结果追加到 conversation(过大的结果先缓冲到磁盘)
2077
+ // 但读缓冲文件本身的结果不再二次缓冲(避免递归缓冲)
2078
+ const bufferDir = this.resultBuffer.getBufferDir();
2079
+ for (const result of results) {
2080
+ const call = executableCalls.find(c => c.id === result.tool_use_id);
2081
+ // Flow 工具结果不记入历史 — 状态由 Zone 5 注入体现
2082
+ if (call?.name && isFlowTool(call.name))
2083
+ continue;
2084
+ const sanitized = sanitizeToolResult(result.content);
2085
+ const skipBuffer = call?.name === 'read' && typeof call.input.file_path === 'string' &&
2086
+ call.input.file_path.startsWith(bufferDir);
2087
+ const content = skipBuffer ? sanitized : this.resultBuffer.maybeBuffer(sanitized, result.tool_use_id);
2088
+ const toolResultMessage = {
2089
+ role: 'user',
2090
+ content: {
2091
+ type: 'tool_result',
2092
+ tool_use_id: result.tool_use_id,
2093
+ content,
2094
+ is_error: result.is_error,
2095
+ },
2096
+ };
2097
+ await this.conversationStore.append(this.sessionDir, toolResultMessage);
2098
+ // 显示工具结果摘要
2099
+ this.outputHandler?.onToolResult?.(content, result.is_error ?? false, result.tool_use_id);
2100
+ // 消费 diff 通道
2101
+ const { popDiff: popD2 } = await import('../tools/diff-channel.js');
2102
+ const diffData = popD2(result.tool_use_id);
2103
+ if (diffData)
2104
+ this.outputHandler?.onDiff?.(result.tool_use_id, diffData.filePath, diffData.lines);
2105
+ }
2106
+ }
2107
+ /**
2108
+ * Execute a single tool inline during the SSE stream.
2109
+ * Stores the result for later conversation append; fires onToolResult for real-time feedback.
2110
+ */
2111
+ async executeSingleToolInline(id, name, input) {
2112
+ const tool = this.toolRegistry.get(name);
2113
+ if (!tool) {
2114
+ const errContent = `Unknown tool: ${name}`;
2115
+ this.outputHandler?.onToolResult?.(errContent, true, id);
2116
+ this.inlineToolResults.set(id, { content: errContent, isError: true });
2117
+ appendEvent(this.sessionDir, {
2118
+ type: 'tool_result',
2119
+ tool_use_id: id,
2120
+ name,
2121
+ content: errContent,
2122
+ timestamp: new Date().toISOString(),
2123
+ }).catch(() => { });
2124
+ return;
2125
+ }
2126
+ // Permission check for dangerous tools
2127
+ if (this.dangerousTools.has(name) && this.outputHandler?.onPermissionRequest) {
2128
+ // Step 0: AOR — unrestricted mode, skip all permissions
2129
+ if (!this.unrestrictedTools) {
2130
+ // Step 1: check session allowlist
2131
+ if (this.allowlistTools.has(name)) {
2132
+ // allowed, proceed
2133
+ }
2134
+ else if (name === 'bash' && this.isCommandAllowed(input?.command)) {
2135
+ // Step 2: check allowedCommands glob
2136
+ // allowed, proceed
2137
+ }
2138
+ else {
2139
+ // Step 3: pop permission
2140
+ const result = await this.outputHandler.onPermissionRequest(name, input);
2141
+ if (result === 'no') {
2142
+ const deniedMsg = 'Permission denied by user';
2143
+ this.outputHandler?.onToolResult?.(deniedMsg, true, id);
2144
+ this.inlineToolResults.set(id, { content: deniedMsg, isError: true });
2145
+ appendEvent(this.sessionDir, {
2146
+ type: 'tool_result',
2147
+ tool_use_id: id,
2148
+ name,
2149
+ content: deniedMsg,
2150
+ timestamp: new Date().toISOString(),
2151
+ }).catch(() => { });
2152
+ return;
2153
+ }
2154
+ if (result === 'aor') {
2155
+ this.unrestrictedTools = true;
2156
+ this.outputHandler?.onStatus?.('AOR mode enabled — all future tool calls unrestricted', 'warn');
2157
+ }
2158
+ if (result === 'always') {
2159
+ this.allowlistTools.add(name);
2160
+ sessionAllowlist.addTool(this.sessionDir, name).catch(() => { });
2161
+ if (name === 'bash' && input?.command) {
2162
+ sessionAllowlist.addCommand(this.sessionDir, input.command).catch(() => { });
2163
+ }
2164
+ }
2165
+ }
2166
+ }
2167
+ }
2168
+ // LoopGuard tool check for inline execution
2169
+ const stormEnabled = this.configCenter
2170
+ ? this.configCenter.get('repair.storm.enabled')
2171
+ : true;
2172
+ const STORM_EXEMPT = ['read', 'glob', 'grep'];
2173
+ if (stormEnabled !== false && !isMutating(name) && !STORM_EXEMPT.includes(name) && !isFlowTool(name)) {
2174
+ const { suppressed } = this.loopGuard.checkToolCalls([{ id, name, input }]);
2175
+ if (suppressed.has(id)) {
2176
+ const stormMsg = `[Storm suppressed] ${ToolGuard.reflectionPrompt({ id, name, input })}`;
2177
+ this.outputHandler?.onToolResult?.(stormMsg, true, id);
2178
+ this.inlineToolResults.set(id, { content: stormMsg, isError: true });
2179
+ appendEvent(this.sessionDir, {
2180
+ type: 'tool_result',
2181
+ tool_use_id: id,
2182
+ name,
2183
+ content: stormMsg,
2184
+ timestamp: new Date().toISOString(),
2185
+ }).catch(() => { });
2186
+ return;
2187
+ }
2188
+ }
2189
+ // ── 回合回滚:记录写操作前置状态 ──
2190
+ if (this.turnRecorder) {
2191
+ if (['write', 'edit', 'multi_edit'].includes(name)) {
2192
+ const filePath = input.file_path;
2193
+ if (filePath) {
2194
+ this.turnRecorder.recordPreState(path.resolve(this.gitManager.getRepoPath(), filePath));
2195
+ }
2196
+ }
2197
+ else if (name === 'bash') {
2198
+ const cmd = input.command;
2199
+ if (cmd)
2200
+ this.turnRecorder.recordCommand(cmd);
2201
+ }
2202
+ }
2203
+ // Execute tool directly
2204
+ try {
2205
+ const rawResult = await tool.execute(input, this.abortController?.signal ?? undefined);
2206
+ // 读缓冲文件本身的结果不再二次缓冲(避免递归缓冲)
2207
+ const isBufferedRead = name === 'read' && typeof input.file_path === 'string' &&
2208
+ input.file_path.startsWith(this.resultBuffer.getBufferDir());
2209
+ const result = isBufferedRead
2210
+ ? sanitizeToolResult(rawResult)
2211
+ : this.resultBuffer.maybeBuffer(sanitizeToolResult(rawResult), name);
2212
+ this.outputHandler?.onToolResult?.(result, false, id);
2213
+ // 消费 diff 通道(edit/write 按 filePath 写入)
2214
+ if (name === 'edit' || name === 'write' || name === 'multi_edit') {
2215
+ const { popDiff: popD } = await import('../tools/diff-channel.js');
2216
+ const fp = input?.file_path;
2217
+ if (fp) {
2218
+ const diffData = popD(fp);
2219
+ if (diffData)
2220
+ this.outputHandler?.onDiff?.(id, diffData.filePath, diffData.lines);
2221
+ }
2222
+ }
2223
+ this.inlineToolResults.set(id, { content: result, isError: false });
2224
+ appendEvent(this.sessionDir, {
2225
+ type: 'tool_result',
2226
+ tool_use_id: id,
2227
+ name,
2228
+ content: result.slice(0, 1000), // truncate long results in events log
2229
+ timestamp: new Date().toISOString(),
2230
+ }).catch(() => { });
2231
+ }
2232
+ catch (err) {
2233
+ const errMsg = err instanceof Error ? err.message : String(err);
2234
+ this.outputHandler?.onToolResult?.(`Error: ${errMsg}`, true, id);
2235
+ this.inlineToolResults.set(id, { content: `Error: ${errMsg}`, isError: true });
2236
+ appendEvent(this.sessionDir, {
2237
+ type: 'tool_result',
2238
+ tool_use_id: id,
2239
+ name,
2240
+ content: `Error: ${errMsg}`,
2241
+ timestamp: new Date().toISOString(),
2242
+ }).catch(() => { });
2243
+ }
2244
+ }
2245
+ /**
2246
+ * Flush inline tool results to the conversation store and run dependency analysis.
2247
+ * Called after the assistant message has been appended to maintain correct message ordering.
2248
+ */
2249
+ async flushInlineToolResults(toolCalls) {
2250
+ // Dependency impact analysis (same logic as executeTools)
2251
+ if (this.dependencyAnalyzer && toolCalls.some(tc => tc.name === 'edit' || tc.name === 'write')) {
2252
+ const editedFiles = toolCalls
2253
+ .filter(tc => tc.name === 'edit' || tc.name === 'write')
2254
+ .map(tc => tc.input.file_path)
2255
+ .filter(Boolean);
2256
+ if (editedFiles.length > 0) {
2257
+ await this.dependencyAnalyzer.incrementalUpdate(editedFiles, this.gitManager);
2258
+ const impacts = editedFiles.map(f => this.dependencyAnalyzer.getImpact(f));
2259
+ const impactLines = impacts
2260
+ .filter(i => i.allImpacts.length > 0)
2261
+ .map(i => `[Dependency Impact] ${i.sourceFile} -> affects: ${i.allImpacts.join(', ')}`);
2262
+ if (impactLines.length > 0) {
2263
+ this.pendingImpactInfo = impactLines.join('\n');
2264
+ }
2265
+ }
2266
+ }
2267
+ // Append stored inline results to conversation
2268
+ for (const tc of toolCalls) {
2269
+ // Flow 工具结果不记入历史 — 状态由 Zone 5 注入体现
2270
+ if (isFlowTool(tc.name))
2271
+ continue;
2272
+ const stored = this.inlineToolResults.get(tc.id);
2273
+ if (!stored) {
2274
+ // Tool wasn't executed inline (e.g., filtered out) — skip
2275
+ continue;
2276
+ }
2277
+ const toolResultMessage = {
2278
+ role: 'user',
2279
+ content: {
2280
+ type: 'tool_result',
2281
+ tool_use_id: tc.id,
2282
+ content: stored.content,
2283
+ is_error: stored.isError,
2284
+ },
2285
+ };
2286
+ await this.conversationStore.append(this.sessionDir, toolResultMessage);
2287
+ }
2288
+ }
2289
+ /**
2290
+ * 回收已处理图片:将非最后一轮的 image base64 替换为占位符。
2291
+ *
2292
+ * - 保留最后一条 user 消息中的图片(模型当前轮还在看)
2293
+ * - 更早的图片 → 用模型后续的回复作为描述,替换为纯文本占位符
2294
+ * - 无模型描述时 → 用元信息占位符
2295
+ * - 替换后的占位符包含 img_id,模型可通过 view_image 重新查看
2296
+ */
2297
+ async recycleProcessedImages() {
2298
+ try {
2299
+ const history = await this.conversationStore.readAll(this.sessionDir);
2300
+ if (history.length === 0)
2301
+ return;
2302
+ // 找到最后一条真正的 user 消息(跳过 tool_result,它们 role 也是 user)
2303
+ let lastUserIdx = -1;
2304
+ for (let i = history.length - 1; i >= 0; i--) {
2305
+ const m = history[i];
2306
+ if (m.role === 'user') {
2307
+ const items = Array.isArray(m.content) ? m.content : [m.content];
2308
+ // 跳过纯 tool_result 消息
2309
+ if (items.every(c => c.type === 'tool_result'))
2310
+ continue;
2311
+ lastUserIdx = i;
2312
+ break;
2313
+ }
2314
+ }
2315
+ if (lastUserIdx < 0)
2316
+ return;
2317
+ let modified = false;
2318
+ const cleaned = history.map((msg, idx) => {
2319
+ // 保留最后一条 user 消息中的图片
2320
+ if (idx === lastUserIdx)
2321
+ return msg;
2322
+ const items = Array.isArray(msg.content) ? msg.content : [msg.content];
2323
+ let changed = false;
2324
+ const newItems = items.map((item, itemIdx) => {
2325
+ if (item.type !== 'image')
2326
+ return item;
2327
+ changed = true;
2328
+ modified = true;
2329
+ // 1) 从相邻 text 块提取 img_id
2330
+ let imgId = '';
2331
+ if (itemIdx + 1 < items.length && items[itemIdx + 1].type === 'text') {
2332
+ const m = items[itemIdx + 1].text.match(/\[Image indexed as #(img_\d{3})/);
2333
+ if (m)
2334
+ imgId = m[1];
2335
+ }
2336
+ if (!imgId) {
2337
+ for (const ti of items) {
2338
+ if (ti.type !== 'text')
2339
+ continue;
2340
+ const m = ti.text.match(/\[Image indexed as #(img_\d{3})/);
2341
+ if (m) {
2342
+ imgId = m[1];
2343
+ break;
2344
+ }
2345
+ }
2346
+ }
2347
+ // 无已有索引 → 存入 ImageStore
2348
+ const src = item.source;
2349
+ if (!imgId && src.type === 'base64' && src.data) {
2350
+ imgId = this.imageStore.store(src.data, src.media_type || 'image/png', '');
2351
+ }
2352
+ // 2) 从后续 assistant 回复中提取模型对图片的描述
2353
+ let description = '';
2354
+ for (let j = idx + 1; j < Math.min(history.length, idx + 4); j++) {
2355
+ const nextMsg = history[j];
2356
+ if (nextMsg?.role !== 'assistant')
2357
+ continue;
2358
+ const nextItems = Array.isArray(nextMsg.content) ? nextMsg.content : [nextMsg.content];
2359
+ for (const ni of nextItems) {
2360
+ if (ni.type === 'text' && ni.text.trim().length > 10) {
2361
+ description = ni.text.replace(/^#{1,4}\s+/gm, '').trim().slice(0, 250);
2362
+ break;
2363
+ }
2364
+ }
2365
+ if (description)
2366
+ break;
2367
+ }
2368
+ // 3) 回存描述到 ImageStore
2369
+ if (description && imgId) {
2370
+ this.imageStore.setDescription(imgId, description);
2371
+ }
2372
+ // 4) 返回占位符
2373
+ if (description) {
2374
+ return { type: 'text', text: `[Image #${imgId}: ${description} — view_image("${imgId}") to re-examine]` };
2375
+ }
2376
+ // fallback: 元信息
2377
+ const mime = src.media_type || 'image/unknown';
2378
+ const ext = mime.split('/')[1] || 'unknown';
2379
+ const decodedBytes = src.data ? Math.ceil(src.data.length * 0.75) : 0;
2380
+ const sizeStr = decodedBytes < 1024 ? `${decodedBytes}B` : `${(decodedBytes / 1024).toFixed(1)}KB`;
2381
+ return { type: 'text', text: `[Image #${imgId || '?'}: ${ext.toUpperCase()}, ${sizeStr} — view_image("${imgId || '?'}") to re-examine]` };
2382
+ });
2383
+ if (!changed)
2384
+ return msg;
2385
+ // 过滤冗余的 [Image indexed as #...] 文本块
2386
+ const filtered = newItems.filter(it => {
2387
+ if (it.type === 'text' && /^\[Image indexed as #img_\d{3}:/.test(it.text))
2388
+ return false;
2389
+ return true;
2390
+ });
2391
+ return { ...msg, content: filtered.length === 1 ? filtered[0] : filtered };
2392
+ });
2393
+ if (modified) {
2394
+ await this.conversationStore.replace(this.sessionDir, cleaned);
2395
+ }
2396
+ }
2397
+ catch {
2398
+ // 回收失败不影响主流程
2399
+ }
2400
+ }
2401
+ /** 释放资源:停路由器(含 WorldEngine)+ 停调度器 */
2402
+ async shutdown() {
2403
+ await this.activeRouter.onDeactivate?.(this).catch(() => { });
2404
+ await this.scheduler?.stop();
2405
+ }
2406
+ }
2407
+ /**
2408
+ * 判断消息是否包含文本内容(而非纯 tool_result)
2409
+ */
2410
+ function hasTextContent(content) {
2411
+ if (typeof content === 'string')
2412
+ return true;
2413
+ if (Array.isArray(content)) {
2414
+ return content.some((c) => c.type === 'text');
2415
+ }
2416
+ return content.type === 'text';
2417
+ }
2418
+ /**
2419
+ * 判断消息是否包含 tool_use 内容
2420
+ */
2421
+ function hasToolUseContent(content) {
2422
+ if (typeof content === 'string')
2423
+ return false;
2424
+ if (Array.isArray(content)) {
2425
+ return content.some((c) => c.type === 'tool_use');
2426
+ }
2427
+ return content.type === 'tool_use';
2428
+ }
2429
+ /**
2430
+ * 生成工具输入的摘要字符串
2431
+ */
2432
+ function summarizeToolInput(input) {
2433
+ const entries = Object.entries(input);
2434
+ if (entries.length === 0)
2435
+ return '{}';
2436
+ const parts = entries.map(([key, value]) => {
2437
+ const str = typeof value === 'string' ? value : JSON.stringify(value);
2438
+ const truncated = str.length > 80 ? str.slice(0, 77) + '...' : str;
2439
+ return `${key}=${truncated}`;
2440
+ });
2441
+ return parts.join(', ');
2442
+ }
2443
+ //# sourceMappingURL=loop.js.map