aws-runtime-bridge 1.9.79 → 1.9.81

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 (596) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +0 -1
  2. package/dist/adapter/AcodeSdkAdapter.js +11 -1986
  3. package/dist/adapter/AdapterRegistry.d.ts +0 -1
  4. package/dist/adapter/AdapterRegistry.js +2 -100
  5. package/dist/adapter/ClaudeSdkAdapter.d.ts +0 -1
  6. package/dist/adapter/ClaudeSdkAdapter.js +4 -1300
  7. package/dist/adapter/CodexSdkAdapter.d.ts +0 -1
  8. package/dist/adapter/CodexSdkAdapter.js +4 -836
  9. package/dist/adapter/OpencodeSdkAdapter.d.ts +0 -1
  10. package/dist/adapter/OpencodeSdkAdapter.js +4 -879
  11. package/dist/adapter/SdkProviderSpi.d.ts +0 -1
  12. package/dist/adapter/SdkProviderSpi.js +2 -55
  13. package/dist/adapter/acode-tool-names.d.ts +0 -1
  14. package/dist/adapter/acode-tool-names.js +2 -37
  15. package/dist/adapter/idle-poll-loop.d.ts +0 -1
  16. package/dist/adapter/idle-poll-loop.js +2 -59
  17. package/dist/adapter/index.d.ts +0 -1
  18. package/dist/adapter/index.js +2 -11
  19. package/dist/adapter/instance-ai-config-resolver.d.ts +0 -1
  20. package/dist/adapter/instance-ai-config-resolver.js +2 -46
  21. package/dist/adapter/types.d.ts +0 -1
  22. package/dist/adapter/types.js +2 -443
  23. package/dist/adapters/cc-switch/common.d.ts +0 -1
  24. package/dist/adapters/cc-switch/common.js +2 -61
  25. package/dist/adapters/cc-switch/index.d.ts +0 -1
  26. package/dist/adapters/cc-switch/index.js +2 -57
  27. package/dist/adapters/cc-switch/mcp-acode.d.ts +0 -1
  28. package/dist/adapters/cc-switch/mcp-acode.js +2 -71
  29. package/dist/adapters/cc-switch/mcp-claude.d.ts +0 -1
  30. package/dist/adapters/cc-switch/mcp-claude.js +2 -89
  31. package/dist/adapters/cc-switch/mcp-claudecode.d.ts +0 -1
  32. package/dist/adapters/cc-switch/mcp-claudecode.js +2 -77
  33. package/dist/adapters/cc-switch/mcp-codex.d.ts +0 -1
  34. package/dist/adapters/cc-switch/mcp-codex.js +11 -249
  35. package/dist/adapters/cc-switch/mcp-opencode.d.ts +0 -1
  36. package/dist/adapters/cc-switch/mcp-opencode.js +2 -97
  37. package/dist/adapters/cc-switch/mcp-placeholder.d.ts +0 -1
  38. package/dist/adapters/cc-switch/mcp-placeholder.js +2 -19
  39. package/dist/adapters/cc-switch/skill-acode.d.ts +0 -1
  40. package/dist/adapters/cc-switch/skill-acode.js +2 -12
  41. package/dist/adapters/cc-switch/skill-claude.d.ts +0 -1
  42. package/dist/adapters/cc-switch/skill-claude.js +2 -11
  43. package/dist/adapters/cc-switch/skill-claudecode.d.ts +0 -1
  44. package/dist/adapters/cc-switch/skill-claudecode.js +2 -11
  45. package/dist/adapters/cc-switch/skill-codex.d.ts +0 -1
  46. package/dist/adapters/cc-switch/skill-codex.js +2 -6
  47. package/dist/adapters/cc-switch/skill-directory.d.ts +0 -1
  48. package/dist/adapters/cc-switch/skill-directory.js +3 -137
  49. package/dist/adapters/cc-switch/skill-opencode.d.ts +0 -1
  50. package/dist/adapters/cc-switch/skill-opencode.js +2 -11
  51. package/dist/adapters/cc-switch/skill-placeholder.d.ts +0 -1
  52. package/dist/adapters/cc-switch/skill-placeholder.js +2 -19
  53. package/dist/config.d.ts +0 -1
  54. package/dist/config.js +2 -290
  55. package/dist/index.d.ts +0 -1
  56. package/dist/index.js +2 -573
  57. package/dist/middleware/auth.d.ts +0 -1
  58. package/dist/middleware/auth.js +2 -98
  59. package/dist/routes/acode-todos.d.ts +0 -1
  60. package/dist/routes/acode-todos.js +2 -75
  61. package/dist/routes/ai-sources.d.ts +0 -1
  62. package/dist/routes/ai-sources.js +2 -132
  63. package/dist/routes/dashboard.d.ts +0 -1
  64. package/dist/routes/dashboard.js +182 -649
  65. package/dist/routes/events.d.ts +0 -1
  66. package/dist/routes/events.js +2 -22
  67. package/dist/routes/file-browser.d.ts +0 -1
  68. package/dist/routes/file-browser.js +2 -781
  69. package/dist/routes/file-diff-rollback.d.ts +0 -1
  70. package/dist/routes/file-diff-rollback.js +2 -301
  71. package/dist/routes/file-snapshot.d.ts +0 -1
  72. package/dist/routes/file-snapshot.js +2 -156
  73. package/dist/routes/git.d.ts +0 -1
  74. package/dist/routes/git.js +28 -1900
  75. package/dist/routes/instance.d.ts +0 -1
  76. package/dist/routes/instance.js +3 -748
  77. package/dist/routes/mcp.d.ts +0 -1
  78. package/dist/routes/mcp.js +2 -196
  79. package/dist/routes/processes.d.ts +0 -1
  80. package/dist/routes/processes.js +2 -252
  81. package/dist/routes/properties.d.ts +0 -1
  82. package/dist/routes/properties.js +2 -72
  83. package/dist/routes/pty.d.ts +0 -1
  84. package/dist/routes/pty.js +6 -560
  85. package/dist/routes/runtime-binding.d.ts +0 -1
  86. package/dist/routes/runtime-binding.js +2 -263
  87. package/dist/routes/sessions.d.ts +0 -1
  88. package/dist/routes/sessions.js +2 -758
  89. package/dist/routes/skills.d.ts +0 -1
  90. package/dist/routes/skills.js +2 -187
  91. package/dist/routes/sub-agents.d.ts +0 -1
  92. package/dist/routes/sub-agents.js +20 -279
  93. package/dist/routes/system-metrics.d.ts +0 -1
  94. package/dist/routes/system-metrics.js +2 -20
  95. package/dist/routes/task-context.d.ts +0 -1
  96. package/dist/routes/task-context.js +2 -78
  97. package/dist/routes/terminal.d.ts +0 -1
  98. package/dist/routes/terminal.js +54 -2882
  99. package/dist/routes/yml.d.ts +0 -1
  100. package/dist/routes/yml.js +2 -223
  101. package/dist/services/acode-package.d.ts +0 -1
  102. package/dist/services/acode-package.js +2 -111
  103. package/dist/services/agent-event-store.d.ts +0 -1
  104. package/dist/services/agent-event-store.js +2 -81
  105. package/dist/services/agent-process-manager.d.ts +0 -1
  106. package/dist/services/agent-process-manager.js +2 -796
  107. package/dist/services/ai-source-apply.d.ts +0 -1
  108. package/dist/services/ai-source-apply.js +2 -83
  109. package/dist/services/auto-register.d.ts +5 -1
  110. package/dist/services/auto-register.js +2 -1053
  111. package/dist/services/aws-client-agent-mcp.d.ts +0 -1
  112. package/dist/services/aws-client-agent-mcp.js +3 -337
  113. package/dist/services/bridge-package-publish-hooks.d.ts +0 -1
  114. package/dist/services/bridge-package-publish-hooks.js +2 -115
  115. package/dist/services/cc-switch-sdk.d.ts +0 -1
  116. package/dist/services/cc-switch-sdk.js +2 -135
  117. package/dist/services/cli-commands.d.ts +6 -1
  118. package/dist/services/cli-commands.js +6 -645
  119. package/dist/services/event-delta-pusher.d.ts +0 -1
  120. package/dist/services/event-delta-pusher.js +2 -95
  121. package/dist/services/fileSnapshotStore.d.ts +0 -1
  122. package/dist/services/fileSnapshotStore.js +2 -500
  123. package/dist/services/instance-ai-config-service.d.ts +0 -1
  124. package/dist/services/instance-ai-config-service.js +2 -174
  125. package/dist/services/instance-init-service.d.ts +0 -1
  126. package/dist/services/instance-init-service.js +2 -193
  127. package/dist/services/instance-service.d.ts +0 -1
  128. package/dist/services/instance-service.js +2 -237
  129. package/dist/services/instance-state.d.ts +0 -1
  130. package/dist/services/instance-state.js +2 -94
  131. package/dist/services/legacy-path-migration.d.ts +0 -1
  132. package/dist/services/legacy-path-migration.js +2 -66
  133. package/dist/services/lifecycle-state.d.ts +0 -1
  134. package/dist/services/lifecycle-state.js +3 -110
  135. package/dist/services/mcp-launch-binding-queue.d.ts +0 -1
  136. package/dist/services/mcp-launch-binding-queue.js +2 -175
  137. package/dist/services/orphan-monitor.d.ts +0 -1
  138. package/dist/services/orphan-monitor.js +2 -382
  139. package/dist/services/panel-auth.d.ts +0 -1
  140. package/dist/services/panel-auth.js +2 -168
  141. package/dist/services/process-detector.d.ts +0 -1
  142. package/dist/services/process-detector.js +18 -1019
  143. package/dist/services/process-registry.d.ts +0 -1
  144. package/dist/services/process-registry.js +2 -375
  145. package/dist/services/ring-buffer.d.ts +0 -1
  146. package/dist/services/ring-buffer.js +2 -103
  147. package/dist/services/runtime-binding.d.ts +0 -1
  148. package/dist/services/runtime-binding.js +6 -363
  149. package/dist/services/runtime-lifecycle-policy.d.ts +0 -1
  150. package/dist/services/runtime-lifecycle-policy.js +2 -35
  151. package/dist/services/session-output.d.ts +0 -1
  152. package/dist/services/session-output.js +5 -539
  153. package/dist/services/startup-config-wizard.d.ts +0 -1
  154. package/dist/services/startup-config-wizard.js +14 -258
  155. package/dist/services/sub-agent-manager-instance.d.ts +0 -1
  156. package/dist/services/sub-agent-manager-instance.js +2 -12
  157. package/dist/services/system-metrics.d.ts +0 -1
  158. package/dist/services/system-metrics.js +2 -252
  159. package/dist/services/terminal-persistence.d.ts +0 -1
  160. package/dist/services/terminal-persistence.js +2 -176
  161. package/dist/services/tool-installer.d.ts +0 -1
  162. package/dist/services/tool-installer.js +5 -751
  163. package/dist/services/tunnel-client.d.ts +0 -1
  164. package/dist/services/tunnel-client.js +2 -402
  165. package/dist/services/windows-service-manager.d.ts +3 -3
  166. package/dist/services/windows-service-manager.js +2 -59
  167. package/dist/services/workspace-files.d.ts +0 -1
  168. package/dist/services/workspace-files.js +2 -1275
  169. package/dist/services/workspace-watch.d.ts +0 -1
  170. package/dist/services/workspace-watch.js +2 -123
  171. package/dist/services/workspaceTaskChangeWatcher.d.ts +0 -1
  172. package/dist/services/workspaceTaskChangeWatcher.js +2 -483
  173. package/dist/types.d.ts +0 -1
  174. package/dist/types.js +1 -1
  175. package/dist/utils/file-utils.d.ts +0 -1
  176. package/dist/utils/file-utils.js +3 -208
  177. package/dist/utils/instance-ai-config-debug-logger.d.ts +0 -1
  178. package/dist/utils/instance-ai-config-debug-logger.js +4 -63
  179. package/dist/utils/logger.d.ts +0 -1
  180. package/dist/utils/logger.js +2 -154
  181. package/dist/utils/mcp-utils.d.ts +0 -1
  182. package/dist/utils/mcp-utils.js +2 -187
  183. package/dist/utils/path-utils.d.ts +0 -1
  184. package/dist/utils/path-utils.js +2 -44
  185. package/dist/utils/sdk-package-loader.d.ts +0 -1
  186. package/dist/utils/sdk-package-loader.js +2 -78
  187. package/dist/utils/validation.d.ts +0 -1
  188. package/dist/utils/validation.js +2 -53
  189. package/dist/utils/yaml-utils.d.ts +0 -1
  190. package/dist/utils/yaml-utils.js +2 -312
  191. package/node_modules/@cc-switch/sdk/dist/constants.d.ts +0 -1
  192. package/node_modules/@cc-switch/sdk/dist/constants.js +2 -63
  193. package/node_modules/@cc-switch/sdk/dist/errors.d.ts +0 -1
  194. package/node_modules/@cc-switch/sdk/dist/errors.js +2 -8
  195. package/node_modules/@cc-switch/sdk/dist/index.d.ts +0 -1
  196. package/node_modules/@cc-switch/sdk/dist/index.js +2 -9
  197. package/node_modules/@cc-switch/sdk/dist/schemas.d.ts +0 -1
  198. package/node_modules/@cc-switch/sdk/dist/schemas.js +2 -38
  199. package/node_modules/@cc-switch/sdk/dist/sdk.d.ts +0 -1
  200. package/node_modules/@cc-switch/sdk/dist/sdk.js +2 -470
  201. package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.d.ts +0 -1
  202. package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.js +4 -307
  203. package/node_modules/@cc-switch/sdk/dist/services/mcp-model.d.ts +0 -1
  204. package/node_modules/@cc-switch/sdk/dist/services/mcp-model.js +2 -37
  205. package/node_modules/@cc-switch/sdk/dist/services/mcp-service.d.ts +0 -1
  206. package/node_modules/@cc-switch/sdk/dist/services/mcp-service.js +2 -9
  207. package/node_modules/@cc-switch/sdk/dist/services/skill-model.d.ts +0 -1
  208. package/node_modules/@cc-switch/sdk/dist/services/skill-model.js +2 -41
  209. package/node_modules/@cc-switch/sdk/dist/services/skill-service.d.ts +0 -1
  210. package/node_modules/@cc-switch/sdk/dist/services/skill-service.js +2 -9
  211. package/node_modules/@cc-switch/sdk/dist/state.d.ts +0 -1
  212. package/node_modules/@cc-switch/sdk/dist/state.js +2 -19
  213. package/node_modules/@cc-switch/sdk/dist/types.d.ts +0 -1
  214. package/node_modules/@cc-switch/sdk/dist/types.js +1 -1
  215. package/node_modules/@cc-switch/sdk/dist/utils/fs.d.ts +0 -1
  216. package/node_modules/@cc-switch/sdk/dist/utils/fs.js +3 -114
  217. package/node_modules/@cc-switch/sdk/dist/utils/id.d.ts +0 -1
  218. package/node_modules/@cc-switch/sdk/dist/utils/id.js +2 -12
  219. package/node_modules/@cc-switch/sdk/package.json +1 -1
  220. package/package/acode/dist/ask-user-question-tool.d.ts +0 -1
  221. package/package/acode/dist/ask-user-question-tool.js +4 -233
  222. package/package/acode/dist/built-in-file-tools.d.ts +0 -1
  223. package/package/acode/dist/built-in-file-tools.js +4 -966
  224. package/package/acode/dist/built-in-symbols-tool.d.ts +0 -1
  225. package/package/acode/dist/built-in-symbols-tool.js +2 -117
  226. package/package/acode/dist/builtins/commandArtifacts.d.ts +0 -1
  227. package/package/acode/dist/builtins/commandArtifacts.js +9 -97
  228. package/package/acode/dist/builtins/commandOutputEvents.d.ts +0 -1
  229. package/package/acode/dist/builtins/commandOutputEvents.js +2 -56
  230. package/package/acode/dist/builtins/shellCommandPreparation.d.ts +0 -1
  231. package/package/acode/dist/builtins/shellCommandPreparation.js +5 -44
  232. package/package/acode/dist/builtins/shellCommandResult.d.ts +0 -1
  233. package/package/acode/dist/builtins/shellCommandResult.js +2 -71
  234. package/package/acode/dist/builtins/shellCommandRunner.d.ts +0 -1
  235. package/package/acode/dist/builtins/shellCommandRunner.js +2 -115
  236. package/package/acode/dist/builtins/shellOutputDecoder.d.ts +0 -1
  237. package/package/acode/dist/builtins/shellOutputDecoder.js +2 -94
  238. package/package/acode/dist/cli.d.ts +0 -1
  239. package/package/acode/dist/cli.js +4 -38
  240. package/package/acode/dist/commands/commandLoader.d.ts +0 -1
  241. package/package/acode/dist/commands/commandLoader.js +2 -33
  242. package/package/acode/dist/commands/commandParser.d.ts +0 -1
  243. package/package/acode/dist/commands/commandParser.js +3 -77
  244. package/package/acode/dist/commands/commandRegistry.d.ts +0 -1
  245. package/package/acode/dist/commands/commandRegistry.js +2 -30
  246. package/package/acode/dist/compaction.d.ts +0 -1
  247. package/package/acode/dist/compaction.js +7 -919
  248. package/package/acode/dist/config.d.ts +0 -1
  249. package/package/acode/dist/config.js +5 -105
  250. package/package/acode/dist/fs-utils.d.ts +0 -1
  251. package/package/acode/dist/fs-utils.js +2 -79
  252. package/package/acode/dist/hooks/hookBus.d.ts +0 -1
  253. package/package/acode/dist/hooks/hookBus.js +2 -40
  254. package/package/acode/dist/index.d.ts +0 -1
  255. package/package/acode/dist/index.js +2 -19
  256. package/package/acode/dist/mcpToolNames.d.ts +0 -1
  257. package/package/acode/dist/mcpToolNames.js +2 -17
  258. package/package/acode/dist/parsers/ast-cache.d.ts +0 -1
  259. package/package/acode/dist/parsers/ast-cache.js +2 -76
  260. package/package/acode/dist/parsers/language-detector.d.ts +0 -1
  261. package/package/acode/dist/parsers/language-detector.js +2 -44
  262. package/package/acode/dist/parsers/language-queries.d.ts +0 -1
  263. package/package/acode/dist/parsers/language-queries.js +12 -602
  264. package/package/acode/dist/parsers/symbol-extractor.d.ts +0 -1
  265. package/package/acode/dist/parsers/symbol-extractor.js +3 -164
  266. package/package/acode/dist/parsers/tree-sitter-loader.d.ts +0 -1
  267. package/package/acode/dist/parsers/tree-sitter-loader.js +2 -64
  268. package/package/acode/dist/parsers/types.d.ts +0 -1
  269. package/package/acode/dist/parsers/types.js +1 -7
  270. package/package/acode/dist/runtime.d.ts +0 -1
  271. package/package/acode/dist/runtime.js +86 -3771
  272. package/package/acode/dist/skill-tool.d.ts +0 -1
  273. package/package/acode/dist/skill-tool.js +5 -174
  274. package/package/acode/dist/sub-agent-manager.d.ts +0 -1
  275. package/package/acode/dist/sub-agent-manager.js +2 -1159
  276. package/package/acode/dist/sub-agent-store.d.ts +0 -1
  277. package/package/acode/dist/sub-agent-store.js +4 -236
  278. package/package/acode/dist/sub-agent-tools.d.ts +0 -1
  279. package/package/acode/dist/sub-agent-tools.js +15 -369
  280. package/package/acode/dist/sub-agent-types.d.ts +0 -1
  281. package/package/acode/dist/sub-agent-types.js +1 -8
  282. package/package/acode/dist/todo-store.d.ts +0 -1
  283. package/package/acode/dist/todo-store.js +2 -73
  284. package/package/acode/dist/todo-tool.d.ts +0 -1
  285. package/package/acode/dist/todo-tool.js +5 -121
  286. package/package/acode/dist/types.d.ts +0 -1
  287. package/package/acode/dist/types.js +1 -1
  288. package/package/acode/package.json +2 -2
  289. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +0 -1
  290. package/package/aws-client-agent-mcp/dist/agent-client.js +2 -890
  291. package/package/aws-client-agent-mcp/dist/agent-client.test.d.ts +0 -1
  292. package/package/aws-client-agent-mcp/dist/agent-client.test.js +0 -1
  293. package/package/aws-client-agent-mcp/dist/config.d.ts +0 -1
  294. package/package/aws-client-agent-mcp/dist/config.js +2 -243
  295. package/package/aws-client-agent-mcp/dist/config.test.d.ts +0 -1
  296. package/package/aws-client-agent-mcp/dist/config.test.js +0 -1
  297. package/package/aws-client-agent-mcp/dist/constants.d.ts +0 -1
  298. package/package/aws-client-agent-mcp/dist/constants.js +2 -24
  299. package/package/aws-client-agent-mcp/dist/context-manager.d.ts +0 -1
  300. package/package/aws-client-agent-mcp/dist/context-manager.js +2 -477
  301. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts +0 -1
  302. package/package/aws-client-agent-mcp/dist/context-manager.test.js +0 -1
  303. package/package/aws-client-agent-mcp/dist/http-client.d.ts +0 -1
  304. package/package/aws-client-agent-mcp/dist/http-client.js +2 -446
  305. package/package/aws-client-agent-mcp/dist/http-client.test.d.ts +0 -1
  306. package/package/aws-client-agent-mcp/dist/http-client.test.js +0 -1
  307. package/package/aws-client-agent-mcp/dist/index.d.ts +0 -1
  308. package/package/aws-client-agent-mcp/dist/index.js +2 -34
  309. package/package/aws-client-agent-mcp/dist/logger.d.ts +0 -1
  310. package/package/aws-client-agent-mcp/dist/logger.js +3 -178
  311. package/package/aws-client-agent-mcp/dist/logger.test.d.ts +0 -1
  312. package/package/aws-client-agent-mcp/dist/logger.test.js +0 -1
  313. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +0 -1
  314. package/package/aws-client-agent-mcp/dist/mcp-server.js +38 -2709
  315. package/package/aws-client-agent-mcp/dist/mcp-server.test.d.ts +0 -1
  316. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +0 -1
  317. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +0 -1
  318. package/package/aws-client-agent-mcp/dist/mcp-tools.js +22 -562
  319. package/package/aws-client-agent-mcp/dist/mcp-tools.test.d.ts +0 -1
  320. package/package/aws-client-agent-mcp/dist/mcp-tools.test.js +0 -1
  321. package/package/aws-client-agent-mcp/dist/memory-store.d.ts +0 -1
  322. package/package/aws-client-agent-mcp/dist/memory-store.js +4 -826
  323. package/package/aws-client-agent-mcp/dist/memory-store.test.d.ts +0 -1
  324. package/package/aws-client-agent-mcp/dist/memory-store.test.js +0 -1
  325. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +0 -1
  326. package/package/aws-client-agent-mcp/dist/memory-tools.js +31 -294
  327. package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +0 -1
  328. package/package/aws-client-agent-mcp/dist/message-buffer.js +2 -288
  329. package/package/aws-client-agent-mcp/dist/message-buffer.test.d.ts +0 -1
  330. package/package/aws-client-agent-mcp/dist/message-buffer.test.js +0 -1
  331. package/package/aws-client-agent-mcp/dist/messageContent.d.ts +0 -1
  332. package/package/aws-client-agent-mcp/dist/messageContent.js +2 -125
  333. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts +0 -1
  334. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js +2 -103
  335. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.d.ts +0 -1
  336. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js +0 -1
  337. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +0 -1
  338. package/package/aws-client-agent-mcp/dist/server-memory-store.js +2 -96
  339. package/package/aws-client-agent-mcp/dist/status-reporter.d.ts +0 -1
  340. package/package/aws-client-agent-mcp/dist/status-reporter.js +3 -327
  341. package/package/aws-client-agent-mcp/dist/status-reporter.test.d.ts +0 -1
  342. package/package/aws-client-agent-mcp/dist/status-reporter.test.js +0 -1
  343. package/package/aws-client-agent-mcp/dist/types.d.ts +0 -1
  344. package/package/aws-client-agent-mcp/dist/types.js +2 -9
  345. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +0 -1
  346. package/package/aws-client-agent-mcp/dist/websocket-client.js +2 -677
  347. package/package/aws-client-agent-mcp/dist/websocket-client.test.d.ts +0 -1
  348. package/package/aws-client-agent-mcp/dist/websocket-client.test.js +0 -1
  349. package/package/aws-client-agent-mcp/package.json +1 -1
  350. package/package/cc-switch-sdk/dist/constants.d.ts +0 -1
  351. package/package/cc-switch-sdk/dist/constants.js +2 -63
  352. package/package/cc-switch-sdk/dist/errors.d.ts +0 -1
  353. package/package/cc-switch-sdk/dist/errors.js +2 -8
  354. package/package/cc-switch-sdk/dist/index.d.ts +0 -1
  355. package/package/cc-switch-sdk/dist/index.js +2 -9
  356. package/package/cc-switch-sdk/dist/schemas.d.ts +0 -1
  357. package/package/cc-switch-sdk/dist/schemas.js +2 -38
  358. package/package/cc-switch-sdk/dist/sdk.d.ts +0 -1
  359. package/package/cc-switch-sdk/dist/sdk.js +2 -470
  360. package/package/cc-switch-sdk/dist/services/ai-config-service.d.ts +0 -1
  361. package/package/cc-switch-sdk/dist/services/ai-config-service.js +4 -307
  362. package/package/cc-switch-sdk/dist/services/mcp-model.d.ts +0 -1
  363. package/package/cc-switch-sdk/dist/services/mcp-model.js +2 -37
  364. package/package/cc-switch-sdk/dist/services/mcp-service.d.ts +0 -1
  365. package/package/cc-switch-sdk/dist/services/mcp-service.js +2 -9
  366. package/package/cc-switch-sdk/dist/services/skill-model.d.ts +0 -1
  367. package/package/cc-switch-sdk/dist/services/skill-model.js +2 -41
  368. package/package/cc-switch-sdk/dist/services/skill-service.d.ts +0 -1
  369. package/package/cc-switch-sdk/dist/services/skill-service.js +2 -9
  370. package/package/cc-switch-sdk/dist/state.d.ts +0 -1
  371. package/package/cc-switch-sdk/dist/state.js +2 -19
  372. package/package/cc-switch-sdk/dist/types.d.ts +0 -1
  373. package/package/cc-switch-sdk/dist/types.js +1 -1
  374. package/package/cc-switch-sdk/dist/utils/fs.d.ts +0 -1
  375. package/package/cc-switch-sdk/dist/utils/fs.js +3 -114
  376. package/package/cc-switch-sdk/dist/utils/id.d.ts +0 -1
  377. package/package/cc-switch-sdk/dist/utils/id.js +2 -12
  378. package/package/cc-switch-sdk/package.json +1 -1
  379. package/package.json +4 -2
  380. package/dist/adapter/AcodeSdkAdapter.d.ts.map +0 -1
  381. package/dist/adapter/AdapterRegistry.d.ts.map +0 -1
  382. package/dist/adapter/ClaudeSdkAdapter.d.ts.map +0 -1
  383. package/dist/adapter/CodexSdkAdapter.d.ts.map +0 -1
  384. package/dist/adapter/OpencodeSdkAdapter.d.ts.map +0 -1
  385. package/dist/adapter/SdkProviderSpi.d.ts.map +0 -1
  386. package/dist/adapter/acode-tool-names.d.ts.map +0 -1
  387. package/dist/adapter/idle-poll-loop.d.ts.map +0 -1
  388. package/dist/adapter/index.d.ts.map +0 -1
  389. package/dist/adapter/instance-ai-config-resolver.d.ts.map +0 -1
  390. package/dist/adapter/types.d.ts.map +0 -1
  391. package/dist/adapters/cc-switch/common.d.ts.map +0 -1
  392. package/dist/adapters/cc-switch/index.d.ts.map +0 -1
  393. package/dist/adapters/cc-switch/mcp-acode.d.ts.map +0 -1
  394. package/dist/adapters/cc-switch/mcp-claude.d.ts.map +0 -1
  395. package/dist/adapters/cc-switch/mcp-claudecode.d.ts.map +0 -1
  396. package/dist/adapters/cc-switch/mcp-codex.d.ts.map +0 -1
  397. package/dist/adapters/cc-switch/mcp-opencode.d.ts.map +0 -1
  398. package/dist/adapters/cc-switch/mcp-placeholder.d.ts.map +0 -1
  399. package/dist/adapters/cc-switch/skill-acode.d.ts.map +0 -1
  400. package/dist/adapters/cc-switch/skill-claude.d.ts.map +0 -1
  401. package/dist/adapters/cc-switch/skill-claudecode.d.ts.map +0 -1
  402. package/dist/adapters/cc-switch/skill-codex.d.ts.map +0 -1
  403. package/dist/adapters/cc-switch/skill-directory.d.ts.map +0 -1
  404. package/dist/adapters/cc-switch/skill-opencode.d.ts.map +0 -1
  405. package/dist/adapters/cc-switch/skill-placeholder.d.ts.map +0 -1
  406. package/dist/config.d.ts.map +0 -1
  407. package/dist/index.d.ts.map +0 -1
  408. package/dist/middleware/auth.d.ts.map +0 -1
  409. package/dist/routes/acode-todos.d.ts.map +0 -1
  410. package/dist/routes/ai-sources.d.ts.map +0 -1
  411. package/dist/routes/dashboard.d.ts.map +0 -1
  412. package/dist/routes/events.d.ts.map +0 -1
  413. package/dist/routes/file-browser.d.ts.map +0 -1
  414. package/dist/routes/file-diff-rollback.d.ts.map +0 -1
  415. package/dist/routes/file-snapshot.d.ts.map +0 -1
  416. package/dist/routes/git.d.ts.map +0 -1
  417. package/dist/routes/instance.d.ts.map +0 -1
  418. package/dist/routes/mcp.d.ts.map +0 -1
  419. package/dist/routes/processes.d.ts.map +0 -1
  420. package/dist/routes/properties.d.ts.map +0 -1
  421. package/dist/routes/pty.d.ts.map +0 -1
  422. package/dist/routes/runtime-binding.d.ts.map +0 -1
  423. package/dist/routes/sessions.d.ts.map +0 -1
  424. package/dist/routes/skills.d.ts.map +0 -1
  425. package/dist/routes/sub-agents.d.ts.map +0 -1
  426. package/dist/routes/system-metrics.d.ts.map +0 -1
  427. package/dist/routes/task-context.d.ts.map +0 -1
  428. package/dist/routes/terminal.d.ts.map +0 -1
  429. package/dist/routes/yml.d.ts.map +0 -1
  430. package/dist/services/acode-package.d.ts.map +0 -1
  431. package/dist/services/agent-event-store.d.ts.map +0 -1
  432. package/dist/services/agent-process-manager.d.ts.map +0 -1
  433. package/dist/services/ai-source-apply.d.ts.map +0 -1
  434. package/dist/services/auto-register.d.ts.map +0 -1
  435. package/dist/services/aws-client-agent-mcp.d.ts.map +0 -1
  436. package/dist/services/bridge-package-publish-hooks.d.ts.map +0 -1
  437. package/dist/services/cc-switch-sdk.d.ts.map +0 -1
  438. package/dist/services/cli-commands.d.ts.map +0 -1
  439. package/dist/services/event-delta-pusher.d.ts.map +0 -1
  440. package/dist/services/fileSnapshotStore.d.ts.map +0 -1
  441. package/dist/services/instance-ai-config-service.d.ts.map +0 -1
  442. package/dist/services/instance-init-service.d.ts.map +0 -1
  443. package/dist/services/instance-service.d.ts.map +0 -1
  444. package/dist/services/instance-state.d.ts.map +0 -1
  445. package/dist/services/legacy-path-migration.d.ts.map +0 -1
  446. package/dist/services/lifecycle-state.d.ts.map +0 -1
  447. package/dist/services/mcp-launch-binding-queue.d.ts.map +0 -1
  448. package/dist/services/orphan-monitor.d.ts.map +0 -1
  449. package/dist/services/panel-auth.d.ts.map +0 -1
  450. package/dist/services/process-detector.d.ts.map +0 -1
  451. package/dist/services/process-registry.d.ts.map +0 -1
  452. package/dist/services/ring-buffer.d.ts.map +0 -1
  453. package/dist/services/runtime-binding.d.ts.map +0 -1
  454. package/dist/services/runtime-lifecycle-policy.d.ts.map +0 -1
  455. package/dist/services/session-output.d.ts.map +0 -1
  456. package/dist/services/startup-config-wizard.d.ts.map +0 -1
  457. package/dist/services/sub-agent-manager-instance.d.ts.map +0 -1
  458. package/dist/services/system-metrics.d.ts.map +0 -1
  459. package/dist/services/terminal-persistence.d.ts.map +0 -1
  460. package/dist/services/tool-installer.d.ts.map +0 -1
  461. package/dist/services/tunnel-client.d.ts.map +0 -1
  462. package/dist/services/windows-service-manager.d.ts.map +0 -1
  463. package/dist/services/workspace-files.d.ts.map +0 -1
  464. package/dist/services/workspace-watch.d.ts.map +0 -1
  465. package/dist/services/workspaceTaskChangeWatcher.d.ts.map +0 -1
  466. package/dist/types.d.ts.map +0 -1
  467. package/dist/utils/file-utils.d.ts.map +0 -1
  468. package/dist/utils/instance-ai-config-debug-logger.d.ts.map +0 -1
  469. package/dist/utils/logger.d.ts.map +0 -1
  470. package/dist/utils/mcp-utils.d.ts.map +0 -1
  471. package/dist/utils/path-utils.d.ts.map +0 -1
  472. package/dist/utils/sdk-package-loader.d.ts.map +0 -1
  473. package/dist/utils/validation.d.ts.map +0 -1
  474. package/dist/utils/yaml-utils.d.ts.map +0 -1
  475. package/node_modules/@cc-switch/sdk/dist/constants.d.ts.map +0 -1
  476. package/node_modules/@cc-switch/sdk/dist/errors.d.ts.map +0 -1
  477. package/node_modules/@cc-switch/sdk/dist/index.d.ts.map +0 -1
  478. package/node_modules/@cc-switch/sdk/dist/schemas.d.ts.map +0 -1
  479. package/node_modules/@cc-switch/sdk/dist/sdk.d.ts.map +0 -1
  480. package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.d.ts.map +0 -1
  481. package/node_modules/@cc-switch/sdk/dist/services/mcp-model.d.ts.map +0 -1
  482. package/node_modules/@cc-switch/sdk/dist/services/mcp-service.d.ts.map +0 -1
  483. package/node_modules/@cc-switch/sdk/dist/services/skill-model.d.ts.map +0 -1
  484. package/node_modules/@cc-switch/sdk/dist/services/skill-service.d.ts.map +0 -1
  485. package/node_modules/@cc-switch/sdk/dist/state.d.ts.map +0 -1
  486. package/node_modules/@cc-switch/sdk/dist/types.d.ts.map +0 -1
  487. package/node_modules/@cc-switch/sdk/dist/utils/fs.d.ts.map +0 -1
  488. package/node_modules/@cc-switch/sdk/dist/utils/id.d.ts.map +0 -1
  489. package/package/acode/dist/ask-user-question-tool.d.ts.map +0 -1
  490. package/package/acode/dist/built-in-file-tools.d.ts.map +0 -1
  491. package/package/acode/dist/built-in-symbols-tool.d.ts.map +0 -1
  492. package/package/acode/dist/builtins/commandArtifacts.d.ts.map +0 -1
  493. package/package/acode/dist/builtins/commandOutputEvents.d.ts.map +0 -1
  494. package/package/acode/dist/builtins/shellCommandPreparation.d.ts.map +0 -1
  495. package/package/acode/dist/builtins/shellCommandResult.d.ts.map +0 -1
  496. package/package/acode/dist/builtins/shellCommandRunner.d.ts.map +0 -1
  497. package/package/acode/dist/builtins/shellOutputDecoder.d.ts.map +0 -1
  498. package/package/acode/dist/cli.d.ts.map +0 -1
  499. package/package/acode/dist/commands/commandLoader.d.ts.map +0 -1
  500. package/package/acode/dist/commands/commandParser.d.ts.map +0 -1
  501. package/package/acode/dist/commands/commandRegistry.d.ts.map +0 -1
  502. package/package/acode/dist/compaction.d.ts.map +0 -1
  503. package/package/acode/dist/config.d.ts.map +0 -1
  504. package/package/acode/dist/fs-utils.d.ts.map +0 -1
  505. package/package/acode/dist/hooks/hookBus.d.ts.map +0 -1
  506. package/package/acode/dist/index.d.ts.map +0 -1
  507. package/package/acode/dist/mcpToolNames.d.ts.map +0 -1
  508. package/package/acode/dist/parsers/ast-cache.d.ts.map +0 -1
  509. package/package/acode/dist/parsers/language-detector.d.ts.map +0 -1
  510. package/package/acode/dist/parsers/language-queries.d.ts.map +0 -1
  511. package/package/acode/dist/parsers/symbol-extractor.d.ts.map +0 -1
  512. package/package/acode/dist/parsers/tree-sitter-loader.d.ts.map +0 -1
  513. package/package/acode/dist/parsers/types.d.ts.map +0 -1
  514. package/package/acode/dist/runtime.d.ts.map +0 -1
  515. package/package/acode/dist/skill-tool.d.ts.map +0 -1
  516. package/package/acode/dist/sub-agent-manager.d.ts.map +0 -1
  517. package/package/acode/dist/sub-agent-store.d.ts.map +0 -1
  518. package/package/acode/dist/sub-agent-tools.d.ts.map +0 -1
  519. package/package/acode/dist/sub-agent-types.d.ts.map +0 -1
  520. package/package/acode/dist/todo-store.d.ts.map +0 -1
  521. package/package/acode/dist/todo-tool.d.ts.map +0 -1
  522. package/package/acode/dist/types.d.ts.map +0 -1
  523. package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +0 -1
  524. package/package/aws-client-agent-mcp/dist/agent-client.js.map +0 -1
  525. package/package/aws-client-agent-mcp/dist/agent-client.test.d.ts.map +0 -1
  526. package/package/aws-client-agent-mcp/dist/agent-client.test.js.map +0 -1
  527. package/package/aws-client-agent-mcp/dist/config.d.ts.map +0 -1
  528. package/package/aws-client-agent-mcp/dist/config.js.map +0 -1
  529. package/package/aws-client-agent-mcp/dist/config.test.d.ts.map +0 -1
  530. package/package/aws-client-agent-mcp/dist/config.test.js.map +0 -1
  531. package/package/aws-client-agent-mcp/dist/constants.d.ts.map +0 -1
  532. package/package/aws-client-agent-mcp/dist/constants.js.map +0 -1
  533. package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +0 -1
  534. package/package/aws-client-agent-mcp/dist/context-manager.js.map +0 -1
  535. package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts.map +0 -1
  536. package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +0 -1
  537. package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +0 -1
  538. package/package/aws-client-agent-mcp/dist/http-client.js.map +0 -1
  539. package/package/aws-client-agent-mcp/dist/http-client.test.d.ts.map +0 -1
  540. package/package/aws-client-agent-mcp/dist/http-client.test.js.map +0 -1
  541. package/package/aws-client-agent-mcp/dist/index.d.ts.map +0 -1
  542. package/package/aws-client-agent-mcp/dist/index.js.map +0 -1
  543. package/package/aws-client-agent-mcp/dist/logger.d.ts.map +0 -1
  544. package/package/aws-client-agent-mcp/dist/logger.js.map +0 -1
  545. package/package/aws-client-agent-mcp/dist/logger.test.d.ts.map +0 -1
  546. package/package/aws-client-agent-mcp/dist/logger.test.js.map +0 -1
  547. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +0 -1
  548. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +0 -1
  549. package/package/aws-client-agent-mcp/dist/mcp-server.test.d.ts.map +0 -1
  550. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +0 -1
  551. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +0 -1
  552. package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +0 -1
  553. package/package/aws-client-agent-mcp/dist/mcp-tools.test.d.ts.map +0 -1
  554. package/package/aws-client-agent-mcp/dist/mcp-tools.test.js.map +0 -1
  555. package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +0 -1
  556. package/package/aws-client-agent-mcp/dist/memory-store.js.map +0 -1
  557. package/package/aws-client-agent-mcp/dist/memory-store.test.d.ts.map +0 -1
  558. package/package/aws-client-agent-mcp/dist/memory-store.test.js.map +0 -1
  559. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +0 -1
  560. package/package/aws-client-agent-mcp/dist/memory-tools.js.map +0 -1
  561. package/package/aws-client-agent-mcp/dist/message-buffer.d.ts.map +0 -1
  562. package/package/aws-client-agent-mcp/dist/message-buffer.js.map +0 -1
  563. package/package/aws-client-agent-mcp/dist/message-buffer.test.d.ts.map +0 -1
  564. package/package/aws-client-agent-mcp/dist/message-buffer.test.js.map +0 -1
  565. package/package/aws-client-agent-mcp/dist/messageContent.d.ts.map +0 -1
  566. package/package/aws-client-agent-mcp/dist/messageContent.js.map +0 -1
  567. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts.map +0 -1
  568. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js.map +0 -1
  569. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.d.ts.map +0 -1
  570. package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js.map +0 -1
  571. package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +0 -1
  572. package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +0 -1
  573. package/package/aws-client-agent-mcp/dist/status-reporter.d.ts.map +0 -1
  574. package/package/aws-client-agent-mcp/dist/status-reporter.js.map +0 -1
  575. package/package/aws-client-agent-mcp/dist/status-reporter.test.d.ts.map +0 -1
  576. package/package/aws-client-agent-mcp/dist/status-reporter.test.js.map +0 -1
  577. package/package/aws-client-agent-mcp/dist/types.d.ts.map +0 -1
  578. package/package/aws-client-agent-mcp/dist/types.js.map +0 -1
  579. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +0 -1
  580. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +0 -1
  581. package/package/aws-client-agent-mcp/dist/websocket-client.test.d.ts.map +0 -1
  582. package/package/aws-client-agent-mcp/dist/websocket-client.test.js.map +0 -1
  583. package/package/cc-switch-sdk/dist/constants.d.ts.map +0 -1
  584. package/package/cc-switch-sdk/dist/errors.d.ts.map +0 -1
  585. package/package/cc-switch-sdk/dist/index.d.ts.map +0 -1
  586. package/package/cc-switch-sdk/dist/schemas.d.ts.map +0 -1
  587. package/package/cc-switch-sdk/dist/sdk.d.ts.map +0 -1
  588. package/package/cc-switch-sdk/dist/services/ai-config-service.d.ts.map +0 -1
  589. package/package/cc-switch-sdk/dist/services/mcp-model.d.ts.map +0 -1
  590. package/package/cc-switch-sdk/dist/services/mcp-service.d.ts.map +0 -1
  591. package/package/cc-switch-sdk/dist/services/skill-model.d.ts.map +0 -1
  592. package/package/cc-switch-sdk/dist/services/skill-service.d.ts.map +0 -1
  593. package/package/cc-switch-sdk/dist/state.d.ts.map +0 -1
  594. package/package/cc-switch-sdk/dist/types.d.ts.map +0 -1
  595. package/package/cc-switch-sdk/dist/utils/fs.d.ts.map +0 -1
  596. package/package/cc-switch-sdk/dist/utils/id.d.ts.map +0 -1
@@ -1,3786 +1,101 @@
1
- import { EventEmitter } from "node:events";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
7
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
8
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
9
- import { createBuiltInFileTools, } from "./built-in-file-tools.js";
10
- import { ASK_USER_QUESTION_TOOL_NAME, createAskUserQuestionTool, } from "./ask-user-question-tool.js";
11
- import { createSkillTool } from "./skill-tool.js";
12
- import { createTodoWriteTool, createTodoReadTool } from "./todo-tool.js";
13
- import { createSubAgentTools } from "./sub-agent-tools.js";
14
- import { cleanupAcodeCmdDist } from "./builtins/commandArtifacts.js";
15
- import { loadAcodeCommands } from "./commands/commandLoader.js";
16
- import { ACodeCommandRegistry } from "./commands/commandRegistry.js";
17
- import { calculateCompactionThreshold, COMPACTION_LLM_PROMPT_TEMPLATE, conversationSizeKB, estimateConversationTokens, performCompaction, pruneToolOutputs, summarizeConversation, } from "./compaction.js";
18
- import { loadAcodeConfig, loadAcodeSkillGroup } from "./config.js";
19
- import { ACodeHookBus, findDeniedHookResult } from "./hooks/hookBus.js";
20
- import { isMcpPollingToolName, getSemanticToolName } from "./mcpToolNames.js";
21
- function cloneChatContentPart(part) {
22
- if (part.type === "text") {
23
- return { type: "text", text: part.text };
24
- }
25
- return { type: "image_url", image_url: { url: part.image_url.url } };
26
- }
27
- function cloneChatToolCall(toolCall) {
28
- return {
29
- id: toolCall.id,
30
- type: "function",
31
- function: {
32
- name: toolCall.function.name,
33
- arguments: toolCall.function.arguments,
34
- },
35
- };
36
- }
37
- function cloneChatMessage(message) {
38
- const cloned = {
39
- role: message.role,
40
- content: Array.isArray(message.content)
41
- ? message.content.map(cloneChatContentPart)
42
- : message.content,
43
- };
44
- if (message.tool_call_id) {
45
- cloned.tool_call_id = message.tool_call_id;
46
- }
47
- if (message.tool_calls) {
48
- cloned.tool_calls = message.tool_calls.map(cloneChatToolCall);
49
- }
50
- return cloned;
51
- }
52
- function cloneChatConversation(conversation) {
53
- return conversation.map(cloneChatMessage);
54
- }
55
- function nowIso() {
56
- return new Date().toISOString();
57
- }
58
- function isRecord(value) {
59
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
60
- }
61
- function isJsonObject(value) {
62
- return isRecord(value);
63
- }
64
- function normalizeText(value) {
65
- const text = String(value ?? "").trim();
66
- return text || undefined;
67
- }
68
- const DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 60_000;
69
- /**
70
- * 单次 provider 请求的总时长上限(毫秒)。默认 10 分钟。
71
- * 超过该总预算即终止整个请求(含流式读取),不论是否仍在持续接收 chunk。
72
- * 与 requestTimeoutMs(流式空闲超时 / 非流式分段超时)配合:只要每个 chunk 间隔不超过 60s,
73
- * 但整体跑满 10min 仍会断开,避免长流式请求无限挂起。
74
- * 0 = 不设总时长上限(仅靠 requestTimeoutMs 保护)。
75
- */
76
- const DEFAULT_PROVIDER_REQUEST_TOTAL_TIMEOUT_MS = 10 * 60_000;
77
- const DEFAULT_PROVIDER_REQUEST_RETRY_COUNT = 2;
78
- /**
79
- * 无限重试哨兵值:配置 requestRetryCount 为 -1 时,
80
- * provider 请求在遇到可重试错误(超时/网络错误等)时无限重试,
81
- * 直到请求成功、用户中止或会话终止。
82
- * 启用后同时禁用总时长上限(requestTotalTimeoutMs 无效),
83
- * 仅依靠单次请求超时(requestTimeoutMs)保护。
84
- */
85
- const INFINITE_RETRY_SENTINEL = -1;
86
- const DEFAULT_PROVIDER_REQUEST_RETRY_DELAY_MS = 1000;
87
- const DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS = 60_000;
88
- const DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS = 0;
89
- const MCP_SDK_MAX_SAFE_TIMEOUT_MS = 2_147_483_647;
90
- /**
91
- * 子 agent 通过共享 MCP 连接允许调用的工具白名单。
92
- * 与 aws-client-agent-mcp 的 MEMORY_TOOL_NAMES 保持一致,
93
- * 防止子 agent 调用 poll_message/send_group 等与父 agent 冲突的工具。
94
- */
95
- const SUB_AGENT_ALLOWED_MCP_TOOLS = new Set([
96
- "create_memory",
97
- "read_memory",
98
- "update_memory",
99
- "delete_memory",
100
- "search_memory",
101
- "list_memories",
102
- "get_memory_stats",
103
- "list_memories_by_file",
104
- // Memory 过期回忆工具:memory_expiration 委托子 agent 需调用以整理过期记忆
105
- "memory_recurrence",
106
- "memory_recurrence_complete",
107
- ]);
108
- /**
109
- * explore 类型 sub-agent 被禁用的内建工具(只读模式黑名单)。
110
- *
111
- * 'explore' 语义上不修改主工作区文件,仅用于代码探索 / memory 整理等只读任务。
112
- * 此处过滤直接写工作区文件的内建工具:
113
- * - write_file / edit_file / delete_file:直接写工作区文件
114
- *
115
- * Bash 保留:只读探索常用(git diff / git log / ripgrep 高级查询等),
116
- * 风险由工具内部的 workingDirectory 限制 + 用户授权流程管控。
117
- *
118
- * read_file / grep / symbols / Bash / ask_user_question / skill / todo_* 等只读工具保留。
119
- * MCP 工具白名单由 SUB_AGENT_ALLOWED_MCP_TOOLS 独立施加,此处不重复。
120
- */
121
- const EXPLORE_READONLY_DENIED_TOOLS = new Set([
122
- "write_file",
123
- "edit_file",
124
- "delete_file",
125
- ]);
126
- const DEFAULT_BASH_BUILT_IN_TOOL_TIMEOUT_MS = 10 * 60_000 + 2000;
127
- const DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT_MS = 10_000;
128
- /**
129
- * 当 poll_message 返回的全部消息被 memory 自动委托处理后,重新 poll 前的等待间隔。
130
- * 防止紧密循环:MCP doPollMessage 对 updating memory 每次都返回通知,
131
- * 而 processMemoryAutoDelegates 因 URI 已在处理中而返回 null,
132
- * 导致 while(true) 无延迟重 poll,引发状态快速跳动(思考→MCP→跳过)最终连接丢失。
133
- */
134
- const DELEGATE_REPOLL_DELAY_MS = 5_000;
135
- /**
136
- * 子 agent 完成(completed/failed/cancelled)后,对应 memory URI 的委托冷却窗口。
137
- * 在此期间即使 memory 仍处 updating / 过期 / 容量超限,运行层也不会再次 spawn 子任务,
138
- * 仅保留巡检。用于切断「委托完成(但 memory 未真正处理)→ 下一轮 poll 又 spawn 相同子任务」的循环。
139
- */
140
- const MEMORY_DELEGATE_COOLDOWN_MS = 10 * 60 * 1000;
141
- const MAX_INLINE_IMAGE_BYTES = 10 * 1024 * 1024;
142
- const MAX_REQUEST_BODY_BYTES = 800 * 1024;
143
- /**
144
- * 上下文压缩调试日志开关:仅当设置环境变量 ACODE_COMPACTION_DEBUG=1 时启用。
145
- * 默认关闭,避免生产环境同步写盘阻塞事件循环。
146
- */
147
- const COMPACTION_DEBUG_ENABLED = process.env.ACODE_COMPACTION_DEBUG === "1";
148
- const COMPACTION_DEBUG_LOG = COMPACTION_DEBUG_ENABLED
149
- ? path.join(os.tmpdir(), "acode-compaction-debug.log")
150
- : "";
151
- /**
152
- * Runtime 调试日志:受 ACODE_COMPACTION_DEBUG=1 开关控制。
153
- * 默认关闭以避免生产环境同步写盘阻塞事件循环、泄漏用户消息内容。
154
- * 文件中所有原 `console.log` 调用均改为走此 helper。
155
- */
156
- function runtimeDebugLog(...args) {
157
- if (!COMPACTION_DEBUG_ENABLED)
158
- return;
159
- console.log(...args);
160
- }
161
- /** 当 provider 未配置 contextLimit 时使用的默认值 */
162
- const FALLBACK_CONTEXT_LIMIT = 64_000;
163
- /**
164
- * 解析 provider 的压缩配置,返回实际用于触发压缩的 contextLimit/maxOutput/inputLimit/threshold。
165
- * 供 session_started 事件、startCompactionDebugLog、requestChatCompletion 共用,
166
- * 确保前端显示的压缩点与后端实际触发阈值完全一致(消除前端自算公式的脱节)。
167
- */
168
- function resolveCompactionConfig(provider) {
169
- const contextLimit = (provider.contextLimit && provider.contextLimit > 0)
170
- ? provider.contextLimit
171
- : FALLBACK_CONTEXT_LIMIT;
172
- const maxOutput = (provider.maxOutput && provider.maxOutput > 0)
173
- ? provider.maxOutput
174
- : 8_192;
175
- const inputLimit = (provider.inputLimit && provider.inputLimit > 0)
176
- ? provider.inputLimit
177
- : undefined;
178
- const threshold = calculateCompactionThreshold(contextLimit, maxOutput, inputLimit);
179
- return { contextLimit, maxOutput, inputLimit, threshold };
180
- }
181
- async function withTimeout(operation, timeoutMs, label, onTimeout) {
182
- if (timeoutMs <= 0) {
183
- return await operation;
184
- }
185
- let timeout;
186
- try {
187
- return await new Promise((resolve, reject) => {
188
- timeout = setTimeout(() => {
189
- onTimeout?.();
190
- reject(new Error(`${label} timed out after ${timeoutMs}ms`));
191
- }, timeoutMs);
192
- operation.then(resolve, reject);
193
- });
194
- }
195
- finally {
196
- if (timeout) {
197
- clearTimeout(timeout);
198
- }
199
- }
200
- }
201
- async function runBestEffort(label, operation, onError, timeoutMs = DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT_MS) {
202
- try {
203
- await withTimeout(operation, timeoutMs, label);
204
- }
205
- catch (error) {
206
- onError(error instanceof Error ? error.message : String(error));
207
- }
208
- }
209
- function normalizeNonNegativeInteger(value, fallback) {
210
- if (value === 0 || value === "0") {
211
- return 0;
212
- }
213
- const numeric = typeof value === "number" ? value : Number(value);
214
- // -1 是无限重试哨兵值,直接透传
215
- if (numeric === INFINITE_RETRY_SENTINEL) {
216
- return INFINITE_RETRY_SENTINEL;
217
- }
218
- return Number.isFinite(numeric) && numeric > 0
219
- ? Math.floor(numeric)
220
- : fallback;
221
- }
222
- function normalizePositiveInteger(value, fallback) {
223
- const numeric = normalizeNonNegativeInteger(value, fallback);
224
- return numeric > 0 ? numeric : fallback;
225
- }
226
- /**
227
- * Estimate the token count of the initial context (system prompt + tool definitions).
228
- *
229
- * Uses a blended character-based heuristic: approximately 1 token per 3 characters,
230
- * which balances English (~4 chars/token) and CJK (~1.5–2 chars/token) content
231
- * commonly found in system prompts and MCP tool descriptions.
232
- */
233
- const ESTIMATED_CHARS_PER_TOKEN = 3;
234
- function estimateTokensFromText(value) {
235
- if (!value)
236
- return 0;
237
- return Math.ceil(value.length / ESTIMATED_CHARS_PER_TOKEN);
238
- }
239
- function estimateContextTokens(systemPrompt, exposedTools) {
240
- let charCount = systemPrompt.length;
241
- for (const tool of exposedTools) {
242
- const toolName = tool.kind === "mcp" ? tool.toolName : tool.toolName;
243
- charCount += toolName.length;
244
- if (tool.kind === "mcp") {
245
- if (tool.description) {
246
- charCount += tool.description.length;
247
- }
248
- charCount += JSON.stringify(tool.inputSchema).length;
249
- }
250
- else {
251
- const builtIn = tool;
252
- if (builtIn.inputSchema) {
253
- charCount += JSON.stringify(builtIn.inputSchema).length;
254
- }
255
- }
256
- }
257
- return Math.ceil(charCount / ESTIMATED_CHARS_PER_TOKEN);
258
- }
259
- /**
260
- * 估算 exposedTools 定义的 token 开销。
261
- * tools 的 JSON schema(函数名、描述、参数定义)会被序列化进请求体,
262
- * 占用可观 token 数(十几个工具可达数千 tokens),必须计入上下文估算。
263
- * 否则 estimateConversationTokens 系统性偏低,导致主动压缩阈值无法触达。
264
- */
265
- function estimateToolsTokens(exposedTools) {
266
- if (exposedTools.length === 0)
267
- return 0;
268
- const toolsJson = JSON.stringify(toOpenAiTools(exposedTools));
269
- return estimateTokensFromText(toolsJson);
270
- }
271
- async function delay(ms) {
272
- await new Promise((resolve) => setTimeout(resolve, ms));
273
- }
274
- function isBoolean(value) {
275
- return typeof value === "boolean";
276
- }
277
- function modelSupportsImageInput(model, defaultProvider, providerOptions) {
278
- if (isBoolean(providerOptions?.supportsImageInput)) {
279
- return providerOptions.supportsImageInput;
280
- }
281
- const models = isRecord(defaultProvider) ? defaultProvider.models : undefined;
282
- const modelConfig = isRecord(models) ? models[model] : undefined;
283
- if (!isRecord(modelConfig)) {
284
- return false;
285
- }
286
- const modalities = modelConfig.modalities;
287
- if (!isRecord(modalities) || !Array.isArray(modalities.input)) {
288
- return false;
289
- }
290
- return modalities.input.some((item) => String(item || "")
291
- .trim()
292
- .toLowerCase() === "image");
293
- }
294
- /**
295
- * 解析 ACode 当前会话模型:优先显式启动参数,其次兼容 cc-switch 写入的
296
- * OpenCode-style `provider.default.model`,最后回落到旧版 top-level model。
297
- */
298
- /**
299
- * 按优先级从 provider 配置中查找字段值:
300
- * 1. options.*(通用配置)
301
- * 2. models.<currentModel>.*(当前模型专属配置,面板初始化写入的位置)
302
- * 3. provider.*(provider 级别兜底)
303
- *
304
- * @param optionsValue providerOptions 上的值(已取出)
305
- * @param defaultProvider provider.default 对象
306
- * @param currentModel 当前使用的模型名
307
- * @param fieldName 要查找的字段名(如 "contextLimit")
308
- */
309
- function resolveProviderConfigValue(optionsValue, defaultProvider, currentModel, fieldName) {
310
- // 1. options 级别优先
311
- if (optionsValue !== undefined)
312
- return optionsValue;
313
- if (!defaultProvider)
314
- return undefined;
315
- // 2. models.<modelName>.<fieldName>
316
- const models = defaultProvider.models;
317
- if (isRecord(models)) {
318
- const modelConfig = models[currentModel];
319
- if (isRecord(modelConfig)) {
320
- const modelValue = modelConfig[fieldName];
321
- if (modelValue !== undefined)
322
- return modelValue;
323
- }
324
- }
325
- // 3. provider 级别兜底
326
- return defaultProvider[fieldName];
327
- }
328
- function resolveProviderConfig(config, acodeConfig) {
329
- const defaultProvider = acodeConfig.provider?.default;
330
- const providerOptions = defaultProvider?.options;
331
- const model = config.model ||
332
- defaultProvider?.model ||
333
- acodeConfig.model ||
334
- "acode-default";
335
- // effort 优先级:runtime config (per-session/per-instance) > provider options (acode.json) > provider default
336
- const effort = normalizeText(config.effort) ||
337
- normalizeText(providerOptions?.effort) ||
338
- normalizeText(defaultProvider?.effort) ||
339
- undefined;
340
- return {
341
- model,
342
- baseURL: normalizeText(providerOptions?.baseURL || providerOptions?.baseUrl),
343
- apiKey: normalizeText(providerOptions?.apiKey),
344
- effort,
345
- systemPrompt: config.systemPrompt || acodeConfig.systemPrompt,
346
- stream: providerOptions?.stream !== false,
347
- // ★ 安全修复:使用 normalizePositiveInteger 禁止设为 0,
348
- // 0 会导致 withTimeout 直接返回 operation(禁用超时),
349
- // fetch 挂起时请求永久不返回,agent 死锁。
350
- requestTimeoutMs: normalizePositiveInteger(providerOptions?.requestTimeoutMs ?? providerOptions?.timeoutMs, DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS),
351
- requestTotalTimeoutMs: normalizePositiveInteger(providerOptions?.requestTotalTimeoutMs, DEFAULT_PROVIDER_REQUEST_TOTAL_TIMEOUT_MS),
352
- requestRetryCount: normalizeNonNegativeInteger(providerOptions?.requestRetryCount, DEFAULT_PROVIDER_REQUEST_RETRY_COUNT),
353
- mcpToolTimeoutMs: normalizePositiveInteger(providerOptions?.mcpToolTimeoutMs, DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS),
354
- mcpPollingToolTimeoutMs: normalizeNonNegativeInteger(providerOptions?.mcpPollingToolTimeoutMs, DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS),
355
- supportsImageInput: modelSupportsImageInput(model, defaultProvider, providerOptions),
356
- // ★ 从 provider 配置中读取 contextLimit/inputLimit/maxOutput,
357
- // requestChatCompletion 和 startCompactionDebugLog 使用这些值计算压缩触发阈值。
358
- // 按优先级查找三个位置:
359
- // 1. provider.default.options.*(options 级别,通用配置)
360
- // 2. provider.default.models.<modelName>.*(模型级别,面板初始化写入的位置)
361
- // 3. provider.default.*(provider 级别兜底)
362
- contextLimit: normalizeNonNegativeInteger(resolveProviderConfigValue(providerOptions?.contextLimit, defaultProvider, model, "contextLimit"), 0) || undefined,
363
- inputLimit: normalizeNonNegativeInteger(resolveProviderConfigValue(providerOptions?.inputLimit, defaultProvider, model, "inputLimit"), 0) || undefined,
364
- maxOutput: normalizeNonNegativeInteger(resolveProviderConfigValue(providerOptions?.maxOutput, defaultProvider, model, "maxOutput"), 0) || undefined,
365
- };
366
- }
367
- function appendTextPart(parts, text) {
368
- const normalized = text.replace(/\s+/g, " ").trim();
369
- if (normalized) {
370
- parts.push({ type: "text", text: normalized });
371
- }
372
- }
373
- function toDataImageUrl(mediaType, rawBase64) {
374
- const trimmed = rawBase64.trim();
375
- if (/^data:image\/[a-z0-9.+-]+;base64,/i.test(trimmed)) {
376
- return trimmed;
377
- }
378
- return `data:${mediaType || "image/png"};base64,${trimmed}`;
379
- }
380
- function isDataImageUrl(url) {
381
- return /^data:image\/[a-z0-9.+-]+;base64,/i.test(url.trim());
382
- }
383
- function assertFetchableImageUrl(url) {
384
- let parsedUrl;
385
- try {
386
- parsedUrl = new URL(url);
387
- }
388
- catch {
389
- throw new Error(`ACode image URL is invalid: ${url}`);
390
- }
391
- if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
392
- throw new Error(`ACode image URL protocol is not supported: ${parsedUrl.protocol}`);
393
- }
394
- }
395
- async function responseToBase64(response) {
396
- const arrayBuffer = await response.arrayBuffer();
397
- if (arrayBuffer.byteLength > MAX_INLINE_IMAGE_BYTES) {
398
- throw new Error(`ACode image is too large to inline: ${arrayBuffer.byteLength} bytes exceeds ${MAX_INLINE_IMAGE_BYTES} bytes`);
399
- }
400
- return Buffer.from(arrayBuffer).toString("base64");
401
- }
402
- async function materializeImageUrl(url) {
403
- const trimmedUrl = url.trim();
404
- if (isDataImageUrl(trimmedUrl)) {
405
- return trimmedUrl;
406
- }
407
- assertFetchableImageUrl(trimmedUrl);
408
- const response = await fetch(trimmedUrl);
409
- if (!response.ok) {
410
- throw new Error(`ACode image fetch failed: ${response.status} ${await response.text()}`);
411
- }
412
- const contentType = response.headers.get("content-type") || "";
413
- const mediaType = contentType.split(";")[0]?.trim().toLowerCase() || "";
414
- if (!mediaType.startsWith("image/")) {
415
- throw new Error(`ACode image URL did not return an image content type: ${contentType || "unknown"}`);
416
- }
417
- return `data:${mediaType};base64,${await responseToBase64(response)}`;
418
- }
419
- function normalizeImageAttachmentUrl(attachment) {
420
- const base64 = normalizeText(attachment.base64Data ?? attachment.base64 ?? attachment.data);
421
- if (base64) {
422
- return toDataImageUrl(normalizeText(attachment.mediaType ?? attachment.mimeType) || "image/png", base64);
423
- }
424
- return normalizeText(attachment.url ?? attachment.serverUrl);
425
- }
426
- function fileAttachmentText(name, url, mediaType, size) {
427
- const details = [
428
- name ? `name=${name}` : undefined,
429
- url ? `url=${url}` : undefined,
430
- mediaType ? `mediaType=${mediaType}` : undefined,
431
- size ? `size=${size}` : undefined,
432
- ].filter((item) => Boolean(item));
433
- return `[Attached file: ${details.join(", ")}]`;
434
- }
435
- function parseMarkerAttachmentMessage(message) {
436
- const markerRegex = /\[(图片|文件)\s*:\s*([^\]\n]*?)\s*\]/gi;
437
- const parts = [];
438
- let hasImages = false;
439
- let lastIndex = 0;
440
- let match = markerRegex.exec(message);
441
- while (match !== null) {
442
- appendTextPart(parts, message.slice(lastIndex, match.index));
443
- const markerType = match[1];
444
- const markerValue = (match[2] || "").trim();
445
- if (markerType === "图片" && markerValue) {
446
- parts.push({ type: "image_url", image_url: { url: markerValue } });
447
- hasImages = true;
448
- }
449
- else if (markerType === "文件" && markerValue) {
450
- const [name = "", url = "", mediaType = "", size = ""] = markerValue
451
- .split("|")
452
- .map((item) => item.trim());
453
- appendTextPart(parts, fileAttachmentText(name, url, mediaType, size));
454
- }
455
- else {
456
- appendTextPart(parts, match[0]);
457
- }
458
- lastIndex = match.index + match[0].length;
459
- match = markerRegex.exec(message);
460
- }
461
- if (parts.length === 0) {
462
- return { content: message, hasImages: false };
463
- }
464
- appendTextPart(parts, message.slice(lastIndex));
465
- return { content: parts, hasImages };
466
- }
467
- function appendPolledMessagesContent(parts, parsed) {
468
- let hasImages = false;
469
- const messageGroups = [parsed.directMessages, parsed.groupMessages];
470
- for (const group of messageGroups) {
471
- if (!Array.isArray(group)) {
472
- continue;
473
- }
474
- for (const item of group) {
475
- if (!isRecord(item)) {
476
- continue;
477
- }
478
- const content = normalizeText(item.content);
479
- if (!content) {
480
- continue;
481
- }
482
- const senderName = normalizeText(item.senderName);
483
- appendTextPart(parts, senderName ? `Message from ${senderName}:` : "Message:");
484
- const parsedContent = parseAttachmentMessage(content);
485
- if (Array.isArray(parsedContent.content)) {
486
- parts.push(...parsedContent.content);
487
- }
488
- else {
489
- appendTextPart(parts, parsedContent.content);
490
- }
491
- hasImages = hasImages || parsedContent.hasImages;
492
- }
493
- }
494
- return hasImages;
495
- }
496
- function parseJsonAttachmentMessage(message) {
497
- let parsed;
498
- try {
499
- parsed = JSON.parse(message);
500
- }
501
- catch {
502
- return undefined;
503
- }
504
- if (!isRecord(parsed)) {
505
- return undefined;
506
- }
507
- const parts = [];
508
- let hasImages = appendPolledMessagesContent(parts, parsed);
509
- const text = normalizeText(parsed.text) || "";
510
- const rawAttachments = Array.isArray(parsed.attachments)
511
- ? parsed.attachments
512
- : [];
513
- appendTextPart(parts, text);
514
- for (const item of rawAttachments) {
515
- if (!isRecord(item)) {
516
- continue;
517
- }
518
- const attachment = item;
519
- const kind = String(attachment.type ?? attachment.kind ?? "").toLowerCase();
520
- if (kind === "image") {
521
- const imageUrl = normalizeImageAttachmentUrl(attachment);
522
- if (imageUrl) {
523
- parts.push({ type: "image_url", image_url: { url: imageUrl } });
524
- hasImages = true;
525
- }
526
- continue;
527
- }
528
- const fileUrl = normalizeText(attachment.url ?? attachment.serverUrl) || "";
529
- appendTextPart(parts, fileAttachmentText(normalizeText(attachment.name) || "", fileUrl, normalizeText(attachment.mediaType ?? attachment.mimeType) || "", normalizeText(attachment.size) || ""));
530
- }
531
- if (parts.length === 0) {
532
- return { content: message, hasImages: false };
533
- }
534
- return { content: parts, hasImages };
535
- }
536
- function parseAttachmentMessage(message) {
537
- return (parseJsonAttachmentMessage(message) ?? parseMarkerAttachmentMessage(message));
538
- }
539
- async function materializeImageContent(content) {
540
- if (!Array.isArray(content)) {
541
- return content;
542
- }
543
- const materializedParts = [];
544
- for (const part of content) {
545
- if (part.type === "text") {
546
- materializedParts.push(part);
547
- continue;
548
- }
549
- materializedParts.push({
550
- type: "image_url",
551
- image_url: { url: await materializeImageUrl(part.image_url.url) },
552
- });
553
- }
554
- return materializedParts;
555
- }
556
- function contentPreview(content) {
557
- if (typeof content === "string") {
558
- return content;
559
- }
560
- if (!Array.isArray(content)) {
561
- return "";
562
- }
563
- return content
564
- .map((part) => (part.type === "text" ? part.text : "[Image]"))
565
- .join(" ")
566
- .trim();
567
- }
568
- /**
569
- * 解析 ACode 工具权限配置。
570
- * 主流程:运行时配置优先 -> `.acode/config.json` 次之 -> 兼容旧式 allowedTools/deniedTools 字段。
571
- */
572
- function resolvePermissionConfig(config, acodeConfig) {
573
- const configured = config.permissions ?? acodeConfig.permissions ?? {};
574
- const legacyAllow = config.permissions
575
- ? []
576
- : (acodeConfig.allowedTools ?? []);
577
- const legacyDeny = config.permissions ? [] : (acodeConfig.deniedTools ?? []);
578
- return {
579
- mode: configured.mode ?? "default",
580
- allow: [...(configured.allow ?? []), ...legacyAllow]
581
- .map((rule) => rule.trim())
582
- .filter(Boolean),
583
- deny: [...(configured.deny ?? []), ...legacyDeny]
584
- .map((rule) => rule.trim())
585
- .filter(Boolean),
586
- };
587
- }
588
- function wildcardMatches(pattern, value) {
589
- if (pattern === "*") {
590
- return true;
591
- }
592
- if (pattern.endsWith("*")) {
593
- return value.startsWith(pattern.slice(0, -1));
594
- }
595
- return pattern === value;
596
- }
597
- function toolPermissionNames(tool) {
598
- if (tool.kind === "builtin") {
599
- return [tool.exposedName, tool.toolName, `builtin.${tool.toolName}`];
600
- }
601
- return [
602
- tool.exposedName,
603
- tool.toolName,
604
- `${tool.serverName}.${tool.toolName}`,
605
- `mcp.${tool.serverName}.${tool.toolName}`,
606
- ];
607
- }
608
- function matchesToolRule(rule, tool) {
609
- return toolPermissionNames(tool).some((name) => wildcardMatches(rule, name));
610
- }
611
- function assertToolAllowedByRules(tool, rules, message) {
612
- if (rules.length > 0 && !rules.some((rule) => matchesToolRule(rule, tool))) {
613
- throw new Error(message);
614
- }
615
- }
616
- function assertToolPermission(tool, permissions, commandAllowedTools) {
617
- if (permissions.mode === "bypassPermissions") {
618
- return;
619
- }
620
- if (permissions.deny.some((rule) => matchesToolRule(rule, tool))) {
621
- throw new Error(`ACode tool denied by permission rule: ${tool.exposedName}`);
622
- }
623
- assertToolAllowedByRules(tool, permissions.allow, `ACode tool is not in allowed permission rules: ${tool.exposedName}`);
624
- assertToolAllowedByRules(tool, commandAllowedTools ?? [], `ACode slash command does not allow tool: ${tool.exposedName}`);
625
- }
626
- function isToolAllowed(tool, permissions, commandAllowedTools) {
627
- try {
628
- assertToolPermission(tool, permissions, commandAllowedTools);
629
- return true;
630
- }
631
- catch {
632
- return false;
633
- }
634
- }
635
- function normalizeChatCompletionsUrl(baseURL) {
636
- const trimmed = baseURL.replace(/\/+$/g, "");
637
- if (/\/chat\/completions$/i.test(trimmed)) {
638
- return trimmed;
639
- }
640
- return `${trimmed}/chat/completions`;
641
- }
642
- function summarizeProviderUrl(baseURL) {
643
- try {
644
- const url = new URL(baseURL);
645
- url.search = "";
646
- url.hash = "";
647
- const normalizedUrl = new URL(normalizeChatCompletionsUrl(url.toString()));
648
- return `${normalizedUrl.origin}${normalizedUrl.pathname}`;
649
- }
650
- catch {
651
- return normalizeChatCompletionsUrl(baseURL).replace(/([?&](?:api[_-]?key|token|authorization)=)[^&]+/gi, "$1[redacted]");
652
- }
653
- }
654
- function summarizeLastUserMessage(messages) {
655
- const lastUserMessage = [...messages]
656
- .reverse()
657
- .find((message) => message.role === "user");
658
- const content = contentPreview(lastUserMessage?.content ?? null).trim();
659
- if (!content)
660
- return undefined;
661
- return content.length > 300 ? `${content.slice(0, 300)}...` : content;
662
- }
663
- function coerceArguments(rawArguments) {
664
- try {
665
- const parsed = JSON.parse(rawArguments);
666
- return isJsonObject(parsed) ? parsed : {};
667
- }
668
- catch {
669
- return {};
670
- }
671
- }
672
- function getBuiltInToolTimeoutMs(tool, args, fallbackTimeoutMs) {
673
- // 阻塞等待型工具不受超时限制:
674
- // - ask_user_question: 阻塞等待用户回答
675
- // - await_sub_agents: 永久阻塞等待指定子 agent 数组到达终态
676
- const BLOCKING_BUILT_IN_TOOL_NAMES = new Set([
677
- ASK_USER_QUESTION_TOOL_NAME,
678
- "await_sub_agents",
679
- ]);
680
- if (BLOCKING_BUILT_IN_TOOL_NAMES.has(tool.toolName)) {
681
- return 0;
682
- }
683
- if (tool.toolName !== "Bash") {
684
- return fallbackTimeoutMs;
685
- }
686
- const rawTimeout = args.timeout_ms ?? args.timeoutMs;
687
- const numericTimeout = typeof rawTimeout === "number"
688
- ? rawTimeout
689
- : typeof rawTimeout === "string"
690
- ? Number(rawTimeout)
691
- : NaN;
692
- if (!Number.isFinite(numericTimeout) || numericTimeout <= 0) {
693
- return Math.max(fallbackTimeoutMs, DEFAULT_BASH_BUILT_IN_TOOL_TIMEOUT_MS);
694
- }
695
- return Math.min(Math.floor(numericTimeout), 10 * 60_000) + 2000;
696
- }
697
- function createFallbackResponse(message, state) {
698
- const trimmed = message.trim();
699
- return `ACode 已接收任务。当前已加载 ${state.configMcpCount} 个 MCP 配置、${state.exposedTools.length} 个 MCP Tool 与 ${state.skills.length} 个 Skill。${trimmed ? `输入摘要:${trimmed.slice(0, 200)}` : ""}`;
700
- }
701
- function toToolResultText(result) {
702
- if (!isRecord(result)) {
703
- return String(result ?? "");
704
- }
705
- const content = result.content;
706
- if (Array.isArray(content)) {
707
- const parts = content
708
- .map((item) => {
709
- if (!isRecord(item)) {
710
- return "";
711
- }
712
- if (typeof item.text === "string") {
713
- return item.text;
714
- }
715
- return JSON.stringify(item);
716
- })
717
- .filter(Boolean);
718
- if (parts.length > 0) {
719
- return parts.join("\n");
720
- }
721
- }
722
- return JSON.stringify(result);
723
- }
724
- function getErrorMessage(error) {
725
- return error instanceof Error ? error.message : String(error);
726
- }
727
- function estimateMessageSizeBytes(message) {
728
- let size = 16;
729
- if (typeof message.content === "string") {
730
- size += message.content.length * 2;
731
- }
732
- else if (Array.isArray(message.content)) {
733
- for (const part of message.content) {
734
- if (typeof part === "object" && part !== null) {
735
- size += JSON.stringify(part).length;
736
- }
737
- }
738
- }
739
- if (message.tool_calls) {
740
- size += JSON.stringify(message.tool_calls).length;
741
- }
742
- if (message.tool_call_id) {
743
- size += message.tool_call_id.length;
744
- }
745
- return size;
746
- }
747
- /**
748
- * 按字节大小截断对话,防止 HTTP 请求体过大。
749
- * 这是与 token 压缩不同的关注点:即使 token 在上下文窗口内,
750
- * HTTP 请求体也可能因 base64 图片等内容超过网关限制。
751
- * 使用 compaction 模块的 performCompaction 进行压缩。
752
- *
753
- * ★ 支持 LLM 摘要器(与 performCompaction 一致):
754
- * 若提供 llmSummarizer,则 summarize 阶段使用 LLM 生成语义化摘要。
755
- */
756
- async function truncateConversationIfNeeded(conversation, maxBytes, contextLimit, maxOutput, llmSummarizer) {
757
- let totalBytes = 0;
758
- for (const message of conversation) {
759
- totalBytes += estimateMessageSizeBytes(message);
760
- }
761
- if (totalBytes <= maxBytes) {
762
- return 0;
763
- }
764
- const result = await performCompaction(conversation, contextLimit, maxOutput, {
765
- llmSummarizer,
766
- });
767
- if (result.strategy !== "none") {
768
- return result.prunedToolMessages + result.summarizedMessages;
769
- }
770
- // performCompaction 未生效时,强制 summarize
771
- return summarizeConversation(conversation, { llmSummarizer, contextLimit });
772
- }
773
- function isContextOverflowProviderRequestError(error) {
774
- const message = getErrorMessage(error).toLowerCase();
775
- return (message.includes("context_length_exceeded") ||
776
- message.includes("context length") ||
777
- message.includes("maximum context") ||
778
- message.includes("max context") ||
779
- message.includes("token limit") ||
780
- message.includes("tokens exceed") ||
781
- message.includes("too many tokens") ||
782
- message.includes("input is too long") ||
783
- message.includes("prompt is too long") ||
784
- message.includes("request too large") ||
785
- message.includes("413"));
786
- }
787
- function isRetryableProviderRequestError(error) {
788
- const message = getErrorMessage(error).toLowerCase();
789
- if (isContextOverflowProviderRequestError(error)) {
790
- return false;
791
- }
792
- return (message.includes("fetch failed") ||
793
- message.includes("timed out") ||
794
- message.includes("econnreset") ||
795
- message.includes("econnrefused") ||
796
- message.includes("etimedout") ||
797
- message.includes("enotfound") ||
798
- message.includes("502") ||
799
- message.includes("503") ||
800
- message.includes("504"));
801
- }
802
- function isMcpRequestTimeoutError(error) {
803
- if (typeof error !== "object" || error === null) {
804
- return false;
805
- }
806
- const code = Reflect.get(error, "code");
807
- const message = getErrorMessage(error).toLowerCase();
808
- return code === -32001 || message.includes("request timed out");
809
- }
810
- function getErrorType(error) {
811
- if (typeof error === "object" && error !== null) {
812
- const code = Reflect.get(error, "code");
813
- if (typeof code === "string" && code) {
814
- return code;
815
- }
816
- const name = Reflect.get(error, "name");
817
- if (typeof name === "string" && name) {
818
- return name;
819
- }
820
- }
821
- return "tool_error";
822
- }
823
- function toToolErrorResultText(tool, error) {
824
- return JSON.stringify({
825
- ok: false,
826
- tool: tool.exposedName,
827
- error: {
828
- type: getErrorType(error),
829
- message: getErrorMessage(error),
830
- },
831
- });
832
- }
833
- function summarizeToolArguments(toolArguments) {
834
- const serialized = JSON.stringify(toolArguments);
835
- return serialized.length > 500 ? `${serialized.slice(0, 500)}…` : serialized;
836
- }
837
- function extractReasoningText(delta) {
838
- const candidates = [
839
- delta?.reasoning_content,
840
- delta?.reasoning_text,
841
- delta?.reasoning,
842
- ];
843
- for (const candidate of candidates) {
844
- if (typeof candidate === "string" && candidate) {
845
- return candidate;
846
- }
847
- if (isRecord(candidate)) {
848
- const text = candidate.text ?? candidate.content;
849
- if (typeof text === "string" && text) {
850
- return text;
851
- }
852
- }
853
- }
854
- return "";
855
- }
856
- function buildSkillPrompt(skills) {
857
- if (skills.length === 0) {
858
- return "";
859
- }
860
- return [
861
- "Skills are available for this session.",
862
- "Use the `skill` tool to load specialized skill instructions when a user task matches one of the available skills listed in the tool description.",
863
- "Do not attempt to follow skill instructions from memory — always invoke the `skill` tool first to load the full content.",
864
- ].join(" ");
865
- }
866
- function buildCommandPrompt(commands) {
867
- if (commands.length === 0) {
868
- return "";
869
- }
870
- return [
871
- "Available slash commands:",
872
- ...commands.map((command) => `- /${command.name}${command.description ? `: ${command.description}` : ""}`),
873
- "When the user invokes a slash command, ACode expands it before model execution.",
874
- ].join("\n");
875
- }
876
- /**
877
- * 格式化 profile 信息为文本,供 rolePrompt 拼接使用。
878
- * 主流程:提取关键字段并格式化为可读文本,返回空字符串当 profile 为空。
879
- */
880
- function buildProfilePrompt(profile) {
881
- if (!profile || typeof profile !== "object") {
882
- return "";
883
- }
884
- const p = profile;
885
- const parts = [];
886
- const displayName = p.displayName;
887
- const roleName = p.roleName;
888
- const instanceName = p.instanceName;
889
- const projectName = p.projectName;
890
- const workspacePath = p.workspacePath;
891
- const runtimeMode = p.runtimeMode;
892
- const describe = p.describe;
893
- if (typeof displayName === "string" && displayName.trim()) {
894
- parts.push(`Display Name: ${displayName.trim()}`);
895
- }
896
- if (typeof roleName === "string" && roleName.trim()) {
897
- parts.push(`Role: ${roleName.trim()}`);
898
- }
899
- if (typeof instanceName === "string" && instanceName.trim()) {
900
- parts.push(`Instance: ${instanceName.trim()}`);
901
- }
902
- if (typeof projectName === "string" && projectName.trim()) {
903
- parts.push(`Project: ${projectName.trim()}`);
904
- }
905
- if (typeof workspacePath === "string" && workspacePath.trim()) {
906
- parts.push(`Workspace: ${workspacePath.trim()}`);
907
- }
908
- if (typeof runtimeMode === "string" && runtimeMode.trim()) {
909
- parts.push(`Runtime Mode: ${runtimeMode.trim()}`);
910
- }
911
- if (typeof describe === "string" && describe.trim()) {
912
- parts.push(`Description: ${describe.trim()}`);
913
- }
914
- if (parts.length === 0) {
915
- return "";
916
- }
917
- return ["[User Profile]", ...parts].join("\n");
918
- }
919
- function toOpenAiTools(tools) {
920
- return tools.map((tool) => ({
921
- type: "function",
922
- function: {
923
- name: tool.exposedName,
924
- description: tool.description || tool.toolName,
925
- parameters: tool.inputSchema,
926
- },
927
- }));
928
- }
929
- function normalizeToolName(serverName, toolName) {
930
- const normalized = `${serverName}__${toolName}`.replace(/[^a-zA-Z0-9_-]/g, "_");
931
- return normalized.slice(0, 64) || "mcp_tool";
932
- }
933
- function buildRequestHeaders(provider) {
934
- const headers = {
935
- "Content-Type": "application/json",
936
- };
937
- if (provider.apiKey) {
938
- headers.Authorization = `Bearer ${provider.apiKey}`;
939
- }
940
- return headers;
941
- }
942
- function redactRequestHeaders(headers) {
943
- return Object.fromEntries(Object.entries(headers).map(([name, value]) => [
944
- name,
945
- isSensitiveRequestHeader(name) ? redactRequestHeaderValue(value) : value,
946
- ]));
947
- }
948
- function isSensitiveRequestHeader(name) {
949
- const normalized = name.trim().toLowerCase();
950
- return (normalized === "authorization" ||
951
- normalized === "api-key" ||
952
- normalized === "x-api-key" ||
953
- normalized === "openai-api-key");
954
- }
955
- function redactRequestHeaderValue(value) {
956
- return value.trim().toLowerCase().startsWith("bearer ")
957
- ? "Bearer ***"
958
- : "***";
959
- }
960
- function parseSseDataLines(chunk) {
961
- return chunk
962
- .split(/\r?\n/)
963
- .filter((line) => line.startsWith("data:"))
964
- .map((line) => line.slice("data:".length).trim())
965
- .filter(Boolean);
966
- }
967
- function updateToolCallAccumulator(accumulators, delta) {
968
- const index = Number.isFinite(delta.index) ? Number(delta.index) : 0;
969
- const current = accumulators.get(index) ?? {
970
- id: delta.id || `call-${index}`,
971
- type: "function",
972
- function: { name: "", arguments: "" },
973
- };
974
- if (delta.id) {
975
- current.id = delta.id;
976
- }
977
- if (delta.function?.name) {
978
- current.function.name += delta.function.name;
979
- }
980
- if (delta.function?.arguments) {
981
- current.function.arguments += delta.function.arguments;
982
- }
983
- accumulators.set(index, current);
984
- }
985
- function toAccumulatedToolCalls(accumulators) {
986
- return [...accumulators.entries()]
987
- .sort(([left], [right]) => left - right)
988
- .map(([, value]) => value)
989
- .filter((toolCall) => toolCall.function.name.trim().length > 0);
990
- }
991
- export class ACodeRuntime extends EventEmitter {
992
- constructor(config) {
993
- super();
994
- this.config = config;
995
- this.initialConversation = [];
996
- this.state = {
997
- started: false,
998
- terminated: false,
999
- configMcpCount: 0,
1000
- skills: [],
1001
- provider: {
1002
- model: "acode-default",
1003
- stream: false,
1004
- requestTimeoutMs: DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS,
1005
- requestTotalTimeoutMs: DEFAULT_PROVIDER_REQUEST_TOTAL_TIMEOUT_MS,
1006
- requestRetryCount: DEFAULT_PROVIDER_REQUEST_RETRY_COUNT,
1007
- mcpToolTimeoutMs: DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS,
1008
- mcpPollingToolTimeoutMs: DEFAULT_MCP_POLLING_TOOL_CALL_TIMEOUT_MS,
1009
- supportsImageInput: false,
1010
- },
1011
- permissions: { mode: "default", allow: [], deny: [] },
1012
- mcpConnections: [],
1013
- ownsMcpConnections: true,
1014
- exposedTools: [],
1015
- commands: [],
1016
- commandRegistry: new ACodeCommandRegistry([]),
1017
- conversation: [],
1018
- accumulatedInputTokens: 0,
1019
- accumulatedOutputTokens: 0,
1020
- };
1021
- this.abortController = null;
1022
- /**
1023
- * 正在处理中的 memory 子 agent 跟踪表。
1024
- * key = memory URI,value = subId(用于防止同一 memory 被重复委托)。
1025
- * 子 agent 完成后通过 SubAgentManager 事件回调移除对应 URI。
1026
- */
1027
- this.pendingMemorySubAgents = new Map();
1028
- /**
1029
- * memory 委托冷却表(「已解决/已处理」守卫,区别于 pendingMemorySubAgents 的「进行中」守卫)。
1030
- * key = memory URI,value = 冷却到期时间戳(ms)。
1031
- * 子 agent 进入终态时写入;冷却期内即使 memory 仍待处理也不再 spawn 新子任务。
1032
- * 这是切断「委托完成→复发」循环的关键:原实现只有进行中守卫,子 agent 一结束 URI 立即释放,
1033
- * 导致下一轮 poll 对 sticky 的 updating memory 再次委托。
1034
- */
1035
- this.memoryDelegateCooldownUntil = new Map();
1036
- this.submitQueue = Promise.resolve();
1037
- this.activeTurn = false;
1038
- this.abortRequested = false;
1039
- /**
1040
- * 当前正在 await_sub_agents 工具中阻塞等待的子 agent subId 集合。
1041
- *
1042
- * 用于让 abort()/stop() 能中断 awaitSubAgents 的阻塞:
1043
- * - awaitSubAgents 调用前把 subIds 加入此 set,finally 中移除
1044
- * - abort()/stop() 时对此 set 中每个 subId 调用 manager.abort(subId)
1045
- * - manager.abort 会 settle completionPromise(resolve with error)
1046
- * - 从而让 await completionPromise 解除阻塞,execute() 正常返回,
1047
- * executeSubmit 不再永久 pending,submitQueue 链恢复,后续 submit 可执行。
1048
- *
1049
- * 没有此机制时:abort() 只能中断 fetch 流,对 await completionPromise 无效,
1050
- * 导致 executeSubmit 永久阻塞 → submitQueue 死锁 → 后续 sendMessage 全部排队 → agent 无反应。
1051
- */
1052
- this.pendingAwaitedSubIds = new Set();
1053
- this.sessionCompleteEmitted = false;
1054
- this.contextVersion = 0;
1055
- this.stopWaiters = new Set();
1056
- /** 上下文压缩调试定时器(每 5 秒输出一次) */
1057
- this.compactionDebugTimer = null;
1058
- /** 可动态刷新的 SkillTool 引用(含 refresh 方法),运行中变更时调用其 refresh */
1059
- this.skillTool = null;
1060
- /** 监听 skills 目录变更的 FSWatcher 列表 */
1061
- this.skillWatchers = [];
1062
- /** skill 目录变更后的防抖定时器,避免短时间多次触发重扫 */
1063
- this.skillRefreshTimer = null;
1064
- /**
1065
- * ask_user_question 工具的挂起等待队列。
1066
- * key: toolCallId;value: resolve/reject 回调 + 创建时的 contextVersion。
1067
- * submitQuestionAnswer() 调用时 resolve;abort/stop/supersede 时 reject。
1068
- */
1069
- this.questionWaiters = new Map();
1070
- this.hookBus = new ACodeHookBus({
1071
- sessionId: config.sessionId,
1072
- onError: (event, error) => {
1073
- this.emitAcodeEvent("error", {
1074
- message: `ACode hook failed: ${event.name}: ${error instanceof Error ? error.message : String(error)}`,
1075
- });
1076
- },
1077
- });
1078
- for (const handler of config.hooks ?? []) {
1079
- this.hookBus.register(handler);
1080
- }
1081
- }
1082
- /** 获取当前已暴露的工具名称列表(供 SubAgentManager 记录) */
1083
- getAvailableTools() {
1084
- return this.state.exposedTools.map((tool) => tool.exposedName);
1085
- }
1086
- emitAcodeEvent(type, data) {
1087
- const event = {
1088
- type,
1089
- sessionId: this.config.sessionId,
1090
- timestamp: nowIso(),
1091
- data,
1092
- };
1093
- this.emit("event", event);
1094
- }
1095
- /**
1096
- * 发射压缩子事件(phase 事件)到面板时间线。
1097
- * 子事件通过 parentId 关联到父压缩事件,在前端以嵌套子元素形式展示。
1098
- *
1099
- * @param compactionId 父压缩事件 ID
1100
- * @param phase 阶段标识(detect/prune/prune_done/summarize/summarize_done)
1101
- * @param phaseLabel 阶段显示标签(如"阶段1: 裁剪工具输出")
1102
- * @param phaseDetail 阶段详情(如"裁剪了 5 条工具输出")
1103
- */
1104
- emitCompactionPhase(compactionId, phase, phaseLabel, phaseDetail) {
1105
- this.emitAcodeEvent("context_compaction", {
1106
- action: "phase",
1107
- compactionId,
1108
- parentId: compactionId,
1109
- phase,
1110
- phaseLabel,
1111
- phaseDetail,
1112
- });
1113
- }
1114
- /**
1115
- * performCompaction 的 onPhase 回调处理:将 CompactionPhaseEvent 转换为面板子事件。
1116
- */
1117
- handleCompactionPhase(compactionId, event) {
1118
- switch (event.phase) {
1119
- case "prune_start":
1120
- this.emitCompactionPhase(compactionId, "prune", "阶段1: 裁剪工具输出", `正在裁剪 ${event.messageCount ?? 0} 条消息中的工具输出...`);
1121
- break;
1122
- case "prune_end":
1123
- this.emitCompactionPhase(compactionId, "prune_done", "阶段1: 裁剪工具输出", `裁剪了 ${event.prunedCount ?? 0} 条工具输出, 释放 ${event.freedTokens ?? 0} tokens`);
1124
- break;
1125
- case "summarize_start":
1126
- this.emitCompactionPhase(compactionId, "summarize", "阶段2: 摘要化对话", `正在摘要 ${event.messageCount ?? 0} 条早期对话...`);
1127
- break;
1128
- case "summarize_end":
1129
- this.emitCompactionPhase(compactionId, "summarize_done", "阶段2: 摘要化对话", `摘要化了 ${event.summarizedCount ?? 0} 条消息${event.usedLLM ? " (LLM 摘要)" : " (规则摘要)"}`);
1130
- break;
1131
- }
1132
- }
1133
- /**
1134
- * 启动 ACode headless 会话。
1135
- * 主流程:加载 `.acode/config.json` 与 skills 目录 -> 发出 MCP/Skill 状态 ->
1136
- * 如有初始 prompt 则执行一轮响应。
1137
- */
1138
- async start() {
1139
- // ★ 可见性:start() 早期阶段标记。子 agent(尤其 memory auto-delegate)在
1140
- // emit session_started 之前若卡住(loadAcodeConfig/loadAllSkills/loadAcodeCommands
1141
- // 读盘挂起、fetchProfilePrompt/hookBus.dispatch 调用无响应),前端 modal 会显示
1142
- // "运行中 + 0消息 + 0tokens + 当前工具 -",与"从未运行"无法区分。
1143
- // 在每个可能卡住的异步步骤前 emit startup_phase,让 SubAgentManager 据此更新
1144
- // contextSnapshot.currentTool,前端可精确定位卡点。
1145
- this.emitAcodeEvent("startup_phase", { phase: "loading_config" });
1146
- const acodeConfig = await loadAcodeConfig(this.config.configPath);
1147
- // Skills 来源(按优先级,后加载的覆盖先加载的同名 skill):
1148
- // 1. 全局目录(~/.acode/skills,由 config.skillsDir 指定)
1149
- // 2. 项目目录(<workingDirectory>/.acode/skills)
1150
- // 3. 项目目录(<workingDirectory>/.agentswork/skills)
1151
- // 各目录的 skills 合并后供 SkillTool / prompt 使用;分组信息供前端按目录展示。
1152
- // 即使目录不存在也会作为空节点返回,前端按目录展示。
1153
- this.emitAcodeEvent("startup_phase", { phase: "loading_skills" });
1154
- const { skills, skillGroups, watchedDirs } = await this.loadAllSkills();
1155
- const provider = resolveProviderConfig(this.config, acodeConfig);
1156
- // [DEBUG] ACode Runtime 最终解析的 provider 配置(写入 stdout,bridge 会捕获)
1157
- runtimeDebugLog(`[ACodeRuntime] ★★★ DEBUG ★★★ resolveProviderConfig 结果: config.model=${this.config.model ?? "(undefined)"}, ` +
1158
- `resolvedModel=${provider.model}, baseURL=${provider.baseURL ?? "(none)"}, apiKey=${provider.apiKey ? "[set]" : "(none)"}, ` +
1159
- `configPath=${this.config.configPath ?? "(none)"}`);
1160
- const permissions = resolvePermissionConfig(this.config, acodeConfig);
1161
- // 子 agent 复用父 agent 的 MCP 连接,避免启动新 MCP server 进程后 401
1162
- // 共享连接时只暴露 memory 工具,防止子 agent 调用 poll_message/send_group 等
1163
- // 与父 agent 冲突的工具(原设计通过 AWS_SUB_AGENT=true 限制,共享连接绕过了该机制)
1164
- const mcpConnections = this.config.sharedMcpConnections
1165
- ? this.filterSharedMcpConnectionsForSubAgent(this.config.sharedMcpConnections)
1166
- : await this.connectMcpServers(acodeConfig.mcpServers ?? {}, provider.mcpToolTimeoutMs);
1167
- const ownsMcpConnections = !this.config.sharedMcpConnections;
1168
- // 获取用户 profile 信息并拼接在 rolePrompt 下方
1169
- this.emitAcodeEvent("startup_phase", { phase: "fetching_profile" });
1170
- const profilePrompt = await this.fetchProfilePrompt(mcpConnections);
1171
- const agentId = this.config.agentId || "unknown";
1172
- // 重启时清理旧 spillover 输出
1173
- void cleanupAcodeCmdDist(agentId);
1174
- const builtInTools = createBuiltInFileTools(this.config.workingDirectory, agentId, this.config.onFileChange);
1175
- const askUserQuestionTool = createAskUserQuestionTool(this.buildAskUserQuestionDeps(), () => this.contextVersion);
1176
- const skillTool = createSkillTool(skills);
1177
- this.skillTool = skillTool;
1178
- const todoWriteTool = createTodoWriteTool(agentId);
1179
- const todoReadTool = createTodoReadTool(agentId);
1180
- // 注册 sub-agent 工具集(仅当注入了 subAgentManager)
1181
- const subAgentTools = this.config.subAgentManager
1182
- ? createSubAgentTools(this.buildSubAgentToolDeps(this.config.subAgentManager))
1183
- : [];
1184
- const exposedTools = [
1185
- ...builtInTools,
1186
- askUserQuestionTool,
1187
- skillTool,
1188
- todoWriteTool,
1189
- todoReadTool,
1190
- ...subAgentTools,
1191
- ...mcpConnections.flatMap((connection) => connection.tools),
1192
- ];
1193
- // ★ explore 类型 sub-agent 只读模式:过滤掉会修改主工作区文件的内建工具。
1194
- // 'explore' 语义上不修改主工作区文件(见 SubAgentManager.spawn 注释),
1195
- // 原设计仅靠 SUB_AGENT_ALLOWED_MCP_TOOLS 白名单限制 MCP 工具,
1196
- // 内建工具(write_file/edit_file/delete_file)未被过滤,
1197
- // 导致 explore 子 agent 仍能改写工作区文件,违背只读语义。
1198
- // Bash 保留:只读探索常用(git diff/log 等)。
1199
- const filteredExposedTools = this.config.subagentType === "explore"
1200
- ? exposedTools.filter((tool) => !EXPLORE_READONLY_DENIED_TOOLS.has(tool.exposedName))
1201
- : exposedTools;
1202
- const commandDirs = this.resolveCommandDirs(acodeConfig);
1203
- this.emitAcodeEvent("startup_phase", { phase: "loading_commands" });
1204
- const commands = await loadAcodeCommands(commandDirs);
1205
- const commandRegistry = new ACodeCommandRegistry(commands);
1206
- const skillPrompt = buildSkillPrompt(skills);
1207
- const commandPrompt = buildCommandPrompt(commands);
1208
- // 构造多条 system 消息:
1209
- // System 1: globalPrompt(全局提示词)
1210
- // System 2: rolePrompt(角色提示词)
1211
- // System 3: memory(短期+近期记忆)
1212
- // System 4: provider.systemPrompt + skillPrompt + commandPrompt(合并的运行时指令)
1213
- const runtimePrompt = [provider.systemPrompt, skillPrompt, commandPrompt]
1214
- .map((item) => item?.trim())
1215
- .filter((item) => Boolean(item))
1216
- .join("\n\n");
1217
- // 辅助函数:将非空文本构造为 system role 消息,消除重复的 spread+ternary 模式
1218
- const makeSystem = (content) => content?.trim() ? [{ role: 'system', content: content.trim() }] : [];
1219
- // 将 rolePrompt 与 profilePrompt 合并为第二条 system 消息
1220
- const roleWithProfile = [this.config.rolePrompt, profilePrompt]
1221
- .map((item) => item?.trim())
1222
- .filter((item) => Boolean(item))
1223
- .join("\n\n");
1224
- const initialConversation = [
1225
- ...makeSystem(this.config.globalPrompt),
1226
- ...makeSystem(roleWithProfile),
1227
- ...makeSystem(this.config.memory),
1228
- ...(runtimePrompt ? [{ role: 'system', content: runtimePrompt }] : []),
1229
- ];
1230
- const systemMessagesPlainText = initialConversation
1231
- .map((m) => m.content)
1232
- .filter(Boolean)
1233
- .join("\n\n");
1234
- this.initialConversation = cloneChatConversation(initialConversation);
1235
- this.state = {
1236
- started: true,
1237
- terminated: false,
1238
- configMcpCount: Object.keys(acodeConfig.mcpServers ?? {}).length,
1239
- skills,
1240
- provider,
1241
- permissions,
1242
- mcpConnections,
1243
- ownsMcpConnections,
1244
- exposedTools: filteredExposedTools,
1245
- commands,
1246
- commandRegistry,
1247
- conversation: cloneChatConversation(initialConversation),
1248
- accumulatedInputTokens: 0,
1249
- accumulatedOutputTokens: 0,
1250
- };
1251
- this.emitAcodeEvent("startup_phase", { phase: "session_start_hook" });
1252
- await this.hookBus.dispatch("SessionStart", {
1253
- workingDirectory: this.config.workingDirectory,
1254
- model: provider.model,
1255
- commandCount: commands.length,
1256
- });
1257
- // ★ emit 实际用于触发压缩的配置,前端 AgentContextPanel 直接显示此阈值,
1258
- // 消除前端自算公式与后端实际触发阈值(min(动态比例, hardLimit-reserved))的脱节。
1259
- // 注意:此处的 contextLimit/maxOutput/inputLimit 来自 resolveProviderConfig(读 provider.default.* 三级),
1260
- // 与 adapter 的 totalUsage.contextLimit(读顶层+查表)可能不同源——前端应以这里 emit 的值为准。
1261
- const compactionConfig = resolveCompactionConfig(provider);
1262
- this.emitAcodeEvent("session_started", {
1263
- workingDirectory: this.config.workingDirectory,
1264
- model: provider.model,
1265
- commandCount: commands.length,
1266
- estimatedContextTokens: estimateContextTokens(systemMessagesPlainText, filteredExposedTools),
1267
- contextLimit: compactionConfig.contextLimit,
1268
- maxOutput: compactionConfig.maxOutput,
1269
- ...(compactionConfig.inputLimit !== undefined
1270
- ? { inputLimit: compactionConfig.inputLimit }
1271
- : {}),
1272
- compactionThreshold: compactionConfig.threshold,
1273
- });
1274
- this.emitAcodeEvent("mcp_status", {
1275
- configPath: this.config.configPath,
1276
- installedCount: this.state.configMcpCount,
1277
- servers: Object.keys(acodeConfig.mcpServers ?? {}),
1278
- connectedCount: mcpConnections.length,
1279
- builtInToolCount: builtInTools.length,
1280
- toolCount: filteredExposedTools.length,
1281
- permissionMode: permissions.mode,
1282
- allowedToolRules: permissions.allow,
1283
- deniedToolRules: permissions.deny,
1284
- tools: filteredExposedTools.map((tool) => ({
1285
- name: tool.exposedName,
1286
- source: tool.kind === "mcp"
1287
- ? `${tool.serverName}.${tool.toolName}`
1288
- : `builtin.${tool.toolName}`,
1289
- description: tool.description || "",
1290
- })),
1291
- });
1292
- this.emitAcodeEvent("skill_status", {
1293
- skillsDir: this.config.skillsDir,
1294
- installedCount: skills.length,
1295
- skills: skills.map((skill) => ({
1296
- name: skill.name,
1297
- description: skill.description,
1298
- directory: skill.directory,
1299
- })),
1300
- skillGroups: skillGroups.map((group) => ({
1301
- dir: group.dir,
1302
- exists: group.exists,
1303
- skills: group.skills.map((skill) => ({
1304
- name: skill.name,
1305
- description: skill.description,
1306
- directory: skill.directory,
1307
- })),
1308
- })),
1309
- });
1310
- this.emitAcodeEvent("command_status", {
1311
- action: "commands_loaded",
1312
- installedCommandCount: commands.length,
1313
- commandDirs,
1314
- commands: commands.map((command) => ({
1315
- name: command.name,
1316
- description: command.description,
1317
- })),
1318
- });
1319
- if (this.config.initialPrompt) {
1320
- await this.submit(this.config.initialPrompt);
1321
- }
1322
- // 启动上下文压缩调试日志(仅当 ACODE_COMPACTION_DEBUG=1 时启用)
1323
- this.startCompactionDebugLog();
1324
- // 启动 skills 目录监听,运行中变更会触发重扫并刷新 SkillTool + 重发 skill_status
1325
- this.startSkillWatchers(watchedDirs);
1326
- }
1327
- /**
1328
- * 加载所有 skills 来源目录并合并。
1329
- *
1330
- * 三个来源(按优先级,后加载的覆盖先加载的同名 skill):
1331
- * 1. 全局目录(~/.acode/skills,由 config.skillsDir 指定)
1332
- * 2. 项目目录(<workingDirectory>/.acode/skills)
1333
- * 3. 项目目录(<workingDirectory>/.agentswork/skills)
1334
- *
1335
- * 返回合并后的 skills 列表、按目录分组的 skillGroups,以及需要监听的目录列表
1336
- * (已去重、规范化)。watchedDirs 用于启动 fs.watch 监听运行时变更。
1337
- */
1338
- async loadAllSkills() {
1339
- const globalSkillsDir = this.config.skillsDir ?? "";
1340
- const projectAcodeSkillsDir = path.join(this.config.workingDirectory, ".acode", "skills");
1341
- const projectAgentsworkSkillsDir = path.join(this.config.workingDirectory, ".agentswork", "skills");
1342
- // 按规范化路径去重,避免全局目录与项目目录路径相同时重复加载
1343
- const seenDirs = new Set();
1344
- const skillGroups = [];
1345
- const watchedDirs = [];
1346
- for (const dir of [globalSkillsDir, projectAcodeSkillsDir, projectAgentsworkSkillsDir]) {
1347
- if (!dir)
1348
- continue;
1349
- const normalized = path.resolve(dir);
1350
- if (seenDirs.has(normalized))
1351
- continue;
1352
- seenDirs.add(normalized);
1353
- skillGroups.push(await loadAcodeSkillGroup(dir));
1354
- // 即使目录不存在也加入监听列表:fs.watch 会失败但被静默忽略,
1355
- // 这样用户后续创建该目录时(如果父目录存在)能通过其他途径感知。
1356
- // 实际上 fs.watch 不存在的路径会抛错,所以这里只加入存在的目录。
1357
- try {
1358
- await fs.promises.access(normalized);
1359
- watchedDirs.push(normalized);
1360
- }
1361
- catch {
1362
- // 目录不存在,跳过监听
1363
- }
1364
- }
1365
- // 合并各目录 skills,按 name 去重(项目优先覆盖全局)
1366
- const skillsByName = new Map();
1367
- for (const group of skillGroups) {
1368
- for (const skill of group.skills) {
1369
- skillsByName.set(skill.name, skill);
1370
- }
1371
- }
1372
- const skills = [...skillsByName.values()];
1373
- return { skills, skillGroups, watchedDirs };
1374
- }
1375
- /**
1376
- * 启动 fs.watch 监听 skills 目录变更。
1377
- *
1378
- * 使用 recursive: true(Windows/macOS 原生支持,Linux 在 Node 19+ 支持)。
1379
- * Linux 旧版本不支持时会抛 ERR_FEATURE_UNAVAILABLE,fallback 到非 recursive
1380
- * 监听顶层目录(只能感知 skill 子目录的新增/删除,无法感知 SKILL.md 内容修改)。
1381
- *
1382
- * 变更事件经防抖后触发 refreshSkills(),重新扫描目录、刷新 SkillTool、重发 skill_status。
1383
- */
1384
- startSkillWatchers(dirs) {
1385
- for (const dir of dirs) {
1386
- let watcher = null;
1387
- try {
1388
- watcher = fs.watch(dir, { recursive: true }, () => {
1389
- this.scheduleSkillRefresh();
1390
- });
1391
- }
1392
- catch {
1393
- // recursive 在当前平台不支持(如 Linux 旧版本),fallback 到非 recursive
1394
- try {
1395
- watcher = fs.watch(dir, () => {
1396
- this.scheduleSkillRefresh();
1397
- });
1398
- }
1399
- catch {
1400
- // 监听失败(如权限问题),跳过该目录
1401
- continue;
1402
- }
1403
- }
1404
- if (watcher) {
1405
- watcher.on("error", () => {
1406
- // 静默处理 watcher 错误(如目录被删除),避免未捕获异常
1407
- });
1408
- this.skillWatchers.push(watcher);
1409
- }
1410
- }
1411
- }
1412
- /**
1413
- * 防抖调度 skill 重扫。短时间内多次变更事件只触发一次重扫。
1414
- */
1415
- scheduleSkillRefresh() {
1416
- if (this.skillRefreshTimer) {
1417
- clearTimeout(this.skillRefreshTimer);
1418
- }
1419
- this.skillRefreshTimer = setTimeout(() => {
1420
- this.skillRefreshTimer = null;
1421
- void this.refreshSkills();
1422
- }, ACodeRuntime.SKILL_REFRESH_DEBOUNCE_MS);
1423
- }
1424
- /**
1425
- * 重新扫描 skills 目录并刷新 SkillTool 与 state。
1426
- *
1427
- * 调用时机:fs.watch 监听到目录变更后(经防抖)。
1428
- * 副作用:
1429
- * - 更新 this.skillTool 的内部 Map 与工具描述(让下一轮 turn 的 LLM 感知新 skill)
1430
- * - 更新 this.state.skills(影响 fallback 响应等)
1431
- * - 重发 skill_status 事件(让 DevTools 面板刷新"能力"标签页)
1432
- */
1433
- async refreshSkills() {
1434
- if (!this.state.started || this.state.terminated)
1435
- return;
1436
- try {
1437
- const { skills, skillGroups } = await this.loadAllSkills();
1438
- if (this.skillTool) {
1439
- this.skillTool.refresh(skills);
1440
- }
1441
- this.state.skills = skills;
1442
- this.emitAcodeEvent("skill_status", {
1443
- skillsDir: this.config.skillsDir,
1444
- installedCount: skills.length,
1445
- skills: skills.map((skill) => ({
1446
- name: skill.name,
1447
- description: skill.description,
1448
- directory: skill.directory,
1449
- })),
1450
- skillGroups: skillGroups.map((group) => ({
1451
- dir: group.dir,
1452
- exists: group.exists,
1453
- skills: group.skills.map((skill) => ({
1454
- name: skill.name,
1455
- description: skill.description,
1456
- directory: skill.directory,
1457
- })),
1458
- })),
1459
- });
1460
- runtimeDebugLog(`[skill-watch] Skills refreshed: ${skills.length} skills ` +
1461
- `(${skills.map((s) => s.name).join(", ") || "none"})`, { sessionId: this.config.sessionId });
1462
- }
1463
- catch (error) {
1464
- console.error(`[skill-watch] Failed to refresh skills:`, error instanceof Error ? error.message : error);
1465
- }
1466
- }
1467
- /**
1468
- * 关闭所有 skills 目录监听器与防抖定时器。在 stop() 中调用。
1469
- */
1470
- stopSkillWatchers() {
1471
- if (this.skillRefreshTimer) {
1472
- clearTimeout(this.skillRefreshTimer);
1473
- this.skillRefreshTimer = null;
1474
- }
1475
- for (const watcher of this.skillWatchers) {
1476
- try {
1477
- watcher.close();
1478
- }
1479
- catch {
1480
- // 忽略关闭错误
1481
- }
1482
- }
1483
- this.skillWatchers = [];
1484
- }
1485
- /**
1486
- * 启动上下文压缩调试日志,每 5 秒写入系统临时目录下的 acode-compaction-debug.log。
1487
- * 仅当环境变量 ACODE_COMPACTION_DEBUG=1 时启用,使用异步写入避免阻塞事件循环。
1488
- */
1489
- startCompactionDebugLog() {
1490
- if (!COMPACTION_DEBUG_ENABLED)
1491
- return;
1492
- if (this.compactionDebugTimer)
1493
- return;
1494
- const provider = this.state.provider;
1495
- const { contextLimit, maxOutput, inputLimit, threshold: triggerThreshold } = resolveCompactionConfig(provider);
1496
- this.compactionDebugTimer = setInterval(() => {
1497
- try {
1498
- const actualUsed = this.state.accumulatedInputTokens + this.state.accumulatedOutputTokens;
1499
- const estimatedUsed = estimateConversationTokens(this.state.conversation)
1500
- + estimateToolsTokens(this.state.exposedTools);
1501
- const effectiveUsed = Math.max(actualUsed, estimatedUsed);
1502
- const sizeKB = conversationSizeKB(this.state.conversation);
1503
- const isRunning = this.state.started && !this.state.terminated;
1504
- const lines = [
1505
- `[${new Date().toISOString()}]`,
1506
- `started=${this.state.started}`,
1507
- `terminated=${this.state.terminated}`,
1508
- `activeTurn=${this.activeTurn}`,
1509
- `conversationSize=${this.state.conversation.length}msgs`,
1510
- `sizeKB=${sizeKB}KB`,
1511
- `estimation=${estimatedUsed} tokens`,
1512
- `actualInput=${this.state.accumulatedInputTokens}`,
1513
- `actualOutput=${this.state.accumulatedOutputTokens}`,
1514
- `actualUsed=${actualUsed}`,
1515
- `contextLimit=${contextLimit}`,
1516
- `inputLimit=${inputLimit ?? "n/a"}`,
1517
- `maxOutput=${maxOutput}`,
1518
- `triggerThreshold=${triggerThreshold}`,
1519
- `remainingToTrigger=${Math.max(0, triggerThreshold - actualUsed)}`,
1520
- `isRunning=${isRunning}`,
1521
- `shouldCompact=${isRunning ? effectiveUsed >= triggerThreshold : false}`,
1522
- `---`,
1523
- ].join(" | ");
1524
- // 异步写入,避免阻塞事件循环
1525
- fs.promises.appendFile(COMPACTION_DEBUG_LOG, lines + "\n", "utf-8").catch(() => {
1526
- // 忽略日志写入错误
1527
- });
1528
- }
1529
- catch {
1530
- // 忽略日志写入错误
1531
- }
1532
- }, 5000);
1533
- }
1534
- /**
1535
- * 停止上下文压缩调试日志。
1536
- */
1537
- stopCompactionDebugLog() {
1538
- if (this.compactionDebugTimer) {
1539
- clearInterval(this.compactionDebugTimer);
1540
- this.compactionDebugTimer = null;
1541
- }
1542
- }
1543
- /**
1544
- * 处理一轮用户输入。
1545
- * 主流程:将用户输入追加到会话 -> 调用 OpenAI-compatible 模型 -> 如模型请求
1546
- * MCP tool call 则执行工具并继续同一轮,最后输出 assistant 文本与 usage。
1547
- */
1548
- async submit(message) {
1549
- const contextVersion = this.contextVersion;
1550
- runtimeDebugLog(`[ACodeRuntime] 📬 submit() queued: contextVersion=${contextVersion}, ` +
1551
- `activeTurn=${this.activeTurn}, abortRequested=${this.abortRequested}, ` +
1552
- `terminated=${this.state.terminated}, message.length=${message.length}`);
1553
- const queued = this.submitQueue.then(() => {
1554
- runtimeDebugLog(`[ACodeRuntime] ▶️ executeSubmit dequeued: contextVersion=${contextVersion}`);
1555
- return this.executeSubmit(message, contextVersion);
1556
- });
1557
- this.submitQueue = queued.catch(() => {
1558
- runtimeDebugLog(`[ACodeRuntime] 📬 submit() queued item rejected (catch swallowed)`);
1559
- });
1560
- runtimeDebugLog(`[ACodeRuntime] 📬 submit() returning queued promise`);
1561
- return queued;
1562
- }
1563
- /**
1564
- * Reset only the conversation context while keeping the live ACode session,
1565
- * MCP connections, exposed tools, commands, and hooks available.
1566
- */
1567
- async resetContext() {
1568
- if (!this.state.started || this.state.terminated) {
1569
- throw new Error("ACode runtime is not active");
1570
- }
1571
- this.contextVersion += 1;
1572
- this.abortRequested = this.activeTurn;
1573
- this.abortController?.abort();
1574
- this.abortController = null;
1575
- this.state.conversation = cloneChatConversation(this.initialConversation);
1576
- this.state.accumulatedInputTokens = 0;
1577
- this.state.accumulatedOutputTokens = 0;
1578
- }
1579
- isTurnSuperseded(contextVersion) {
1580
- return contextVersion !== this.contextVersion;
1581
- }
1582
- async executeSubmit(message, contextVersion) {
1583
- const turnStartTime = Date.now();
1584
- runtimeDebugLog(`[ACodeRuntime] ══════════════════════════════════════════`);
1585
- runtimeDebugLog(`[ACodeRuntime] ▶️ executeSubmit STARTED`);
1586
- runtimeDebugLog(`[ACodeRuntime] Message: <redacted, length=${message.length}>`);
1587
- runtimeDebugLog(`[ACodeRuntime] 🔢 Context version: ${contextVersion}`);
1588
- runtimeDebugLog(`[ACodeRuntime] ═══════════════════════════════════════════`);
1589
- if (!this.state.started || this.state.terminated) {
1590
- console.error(`[ACodeRuntime] ❌ Runtime not active (started: ${this.state.started}, terminated: ${this.state.terminated})`);
1591
- throw new Error("ACode runtime is not active");
1592
- }
1593
- this.activeTurn = true;
1594
- this.abortRequested = false;
1595
- const originalPrompt = message.trim();
1596
- let turnCompleted = false;
1597
- try {
1598
- runtimeDebugLog(`[ACodeRuntime] 🔍 Dispatching UserPromptSubmit hook...`);
1599
- const promptHookStartTime = Date.now();
1600
- const promptHookResults = await this.hookBus.dispatch("UserPromptSubmit", { prompt: originalPrompt });
1601
- const promptHookDuration = Date.now() - promptHookStartTime;
1602
- runtimeDebugLog(`[ACodeRuntime] ✅ UserPromptSubmit hook completed in ${promptHookDuration}ms, ` +
1603
- `results: ${promptHookResults.length}`);
1604
- if (this.isTurnSuperseded(contextVersion)) {
1605
- runtimeDebugLog(`[ACodeRuntime] ⚠️ Turn superseded after hook, aborting`);
1606
- return;
1607
- }
1608
- const promptDeny = findDeniedHookResult(promptHookResults);
1609
- if (promptDeny) {
1610
- console.error(`[ACodeRuntime] ❌ Prompt denied by hook: ${promptDeny.message}`);
1611
- this.emitAcodeEvent("error", {
1612
- message: promptDeny.message || "ACode prompt was denied by hook",
1613
- });
1614
- this.emitAcodeEvent("turn_complete", { failed: true });
1615
- turnCompleted = true;
1616
- runtimeDebugLog(`[ACodeRuntime] 📤 Emitted turn_complete (failed=true) due to hook deny`);
1617
- return;
1618
- }
1619
- runtimeDebugLog(`[ACodeRuntime] 🔍 Expanding command if any...`);
1620
- const expandedCommand = this.state.commandRegistry.expand(originalPrompt);
1621
- const trimmed = expandedCommand?.prompt.trim() || originalPrompt;
1622
- const commandAllowedTools = expandedCommand?.command.allowedTools;
1623
- if (expandedCommand) {
1624
- runtimeDebugLog(`[ACodeRuntime] 📋 Slash command detected: ${expandedCommand.command.name}`);
1625
- this.emitAcodeEvent("command_status", {
1626
- action: "slash_command",
1627
- command: expandedCommand.command.name,
1628
- arguments: expandedCommand.argumentsText,
1629
- sourcePath: expandedCommand.command.sourcePath,
1630
- });
1631
- }
1632
- if (!this.state.provider.baseURL || !this.state.provider.apiKey) {
1633
- runtimeDebugLog(`[ACodeRuntime] ⚠️ No provider configured, using fallback response`);
1634
- const text = createFallbackResponse(trimmed, this.state);
1635
- this.emitAcodeEvent("assistant_delta", { text });
1636
- this.emitAcodeEvent("turn_complete", {
1637
- usage: { inputTokens: trimmed.length, outputTokens: text.length },
1638
- });
1639
- turnCompleted = true;
1640
- runtimeDebugLog(`[ACodeRuntime] 📤 Emitted turn_complete (fallback mode)`);
1641
- return;
1642
- }
1643
- runtimeDebugLog(`[ACodeRuntime] 📎 Parsing attachment message...`);
1644
- const parsedContent = parseAttachmentMessage(trimmed);
1645
- if (parsedContent.hasImages && !this.state.provider.supportsImageInput) {
1646
- console.error(`[ACodeRuntime] ❌ Image input not supported by current provider`);
1647
- throw new Error("ACode provider/model is not configured for image input. Enable provider.default.models[model].modalities.input including 'image' or options.supportsImageInput=true.");
1648
- }
1649
- runtimeDebugLog(`[ACodeRuntime] 🖼️ Materializing image content if any...`);
1650
- const userContent = await materializeImageContent(parsedContent.content);
1651
- if (this.isTurnSuperseded(contextVersion)) {
1652
- runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded after materializing content, aborting`);
1653
- return;
1654
- }
1655
- runtimeDebugLog(`[ACodeRuntime] 💬 Adding user message to conversation`);
1656
- this.state.conversation.push({ role: "user", content: userContent });
1657
- runtimeDebugLog(`[ACodeRuntime] Conversation size: ${this.state.conversation.length} messages`);
1658
- runtimeDebugLog(`[ACodeRuntime] Starting runModelTurn...`);
1659
- const modelTurnStartTime = Date.now();
1660
- const result = await this.runModelTurn(commandAllowedTools, contextVersion);
1661
- const modelTurnDuration = Date.now() - modelTurnStartTime;
1662
- runtimeDebugLog(`[ACodeRuntime] ✅ runModelTurn completed in ${modelTurnDuration}ms`);
1663
- runtimeDebugLog(`[ACodeRuntime] 📝 Result: text=${result.text ? result.text.slice(0, 80) + (result.text.length > 80 ? '...' : '') : '(empty)'}, ` +
1664
- `finishReason=${result.finishReason || '(none)'}`);
1665
- if (this.isTurnSuperseded(contextVersion)) {
1666
- runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded after runModelTurn, aborting`);
1667
- return;
1668
- }
1669
- if (result.text && !this.state.provider.stream) {
1670
- runtimeDebugLog(`[ACodeRuntime] 📤 Emitting assistant_delta (non-streaming mode)`);
1671
- this.emitAcodeEvent("assistant_delta", { text: result.text });
1672
- }
1673
- runtimeDebugLog(`[ACodeRuntime] 📤 Emitting turn_complete...`);
1674
- this.emitAcodeEvent("turn_complete", {
1675
- // ★ 使用 result.totalOutputTextLength 替代 result.text.length,
1676
- // result.text 只保留最后一次迭代的文本,导致多轮迭代时 outputTokens 被严重低估
1677
- usage: result.usage ?? {
1678
- inputTokens: trimmed.length,
1679
- outputTokens: result.totalOutputTextLength ?? result.text.length,
1680
- },
1681
- ...(result.responseBody ? { responseBody: result.responseBody } : {}),
1682
- ...(result.finishReason ? { finishReason: result.finishReason } : {}),
1683
- });
1684
- // ★ token 用量已在 runModelTurn 中按迭代更新到 state(非累加),
1685
- // 此处不再累加,避免 actualUsed 跨轮只增不减导致 shouldCompact 永远为 true。
1686
- // finalUsage 用于 turn_complete 事件的 usage 上报(当前迭代的上下文用量,覆盖式)。
1687
- runtimeDebugLog(`[ACodeRuntime] 💰 Current context usage: ` +
1688
- `input=${this.state.accumulatedInputTokens}, ` +
1689
- `output=${this.state.accumulatedOutputTokens}` +
1690
- (result.usage ? '' : ' (estimated, no provider usage)'));
1691
- turnCompleted = true;
1692
- const totalDuration = Date.now() - turnStartTime;
1693
- runtimeDebugLog(`[ACodeRuntime] ═══════════════════════════════════════════`);
1694
- runtimeDebugLog(`[ACodeRuntime] ✅ executeSubmit COMPLETED in ${totalDuration}ms`);
1695
- runtimeDebugLog(`[ACodeRuntime] ══════════════════════════════════════════`);
1696
- }
1697
- catch (error) {
1698
- const errorDuration = Date.now() - turnStartTime;
1699
- console.error(`[ACodeRuntime] ❌ executeSubmit FAILED after ${errorDuration}ms:`, error instanceof Error ? error.message : String(error));
1700
- console.error(`[ACodeRuntime] 🔍 Error stack:`, error instanceof Error ? error.stack : '(no stack)');
1701
- if (this.isTurnSuperseded(contextVersion)) {
1702
- runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded in error handler, skipping recovery`);
1703
- return;
1704
- }
1705
- if (this.abortRequested) {
1706
- runtimeDebugLog(`[ACodeRuntime] ⚠️ Abort requested, emitting aborted turn_complete`);
1707
- this.emitAcodeEvent("turn_complete", { aborted: true });
1708
- turnCompleted = true;
1709
- return;
1710
- }
1711
- runtimeDebugLog(`[ACodeRuntime] Emitting error and failed turn_complete...`);
1712
- this.emitAcodeEvent("error", {
1713
- message: error instanceof Error ? error.message : String(error),
1714
- });
1715
- this.emitAcodeEvent("turn_complete", { failed: true });
1716
- turnCompleted = true;
1717
- }
1718
- finally {
1719
- const finallyDuration = Date.now() - turnStartTime;
1720
- runtimeDebugLog(`[ACodeRuntime] 🔚 executeSubmit finally block entered after ${finallyDuration}ms`);
1721
- runtimeDebugLog(`[ACodeRuntime] 🔍 turnCompleted=${turnCompleted}, activeTurn=${this.activeTurn}, ` +
1722
- `abortRequested=${this.abortRequested}, contextVersion=${contextVersion}, ` +
1723
- `currentContextVersion=${this.contextVersion}`);
1724
- this.activeTurn = false;
1725
- this.abortRequested = false;
1726
- // 关键修复:如果异常路径没有发射 turn_complete,强制发射一个
1727
- // 这确保 adapter 层的 recoverResolvedTurnWithoutTerminalEvent 能够正确触发
1728
- if (!turnCompleted && !this.isTurnSuperseded(contextVersion)) {
1729
- console.error(`[ACodeRuntime] 🚨 CRITICAL: executeSubmit finally block reached without turn_complete event! ` +
1730
- `Duration: ${finallyDuration}ms. ` +
1731
- `This indicates a code path that should have emitted turn_complete but didn't. ` +
1732
- `Forcing failed turn_complete to prevent state stuck.`);
1733
- this.emitAcodeEvent("error", {
1734
- message: "ACode turn completed without proper completion event (internal error)",
1735
- });
1736
- this.emitAcodeEvent("turn_complete", { failed: true });
1737
- runtimeDebugLog(`[ACodeRuntime] Emergency turn_complete (failed=true) emitted`);
1738
- }
1739
- }
1740
- }
1741
- /** 中止当前轮次;由执行轮次统一发出一个终态事件,避免 aborted/failed 双终态。 */
1742
- async abort() {
1743
- const hadController = this.abortController !== null;
1744
- const wasActiveTurn = this.activeTurn;
1745
- this.abortRequested = this.activeTurn;
1746
- if (this.abortController) {
1747
- runtimeDebugLog(`[ACodeRuntime] ⛔ abort() calling abortController.abort() (activeTurn=${wasActiveTurn})`);
1748
- this.abortController.abort();
1749
- }
1750
- else {
1751
- console.warn(`[ACodeRuntime] ⛔ abort() called but abortController is null! ` +
1752
- `activeTurn=${wasActiveTurn}, abortRequested will be set to ${wasActiveTurn}. ` +
1753
- `This means abort() CANNOT cancel any running fetch/stream — ` +
1754
- `if a built-in tool is executing, it will NOT be interrupted.`);
1755
- }
1756
- this.abortController = null;
1757
- // ask_user_question 等待用户回答期间被 abort,需 reject 所有 waiter
1758
- this.rejectAllQuestionWaiters("aborted");
1759
- // ★ 取消正在 await_sub_agents 中阻塞等待的子 agent,让 completionPromise settle,
1760
- // 解除 execute() 阻塞,避免 submitQueue 永久死锁导致 agent 无反应。
1761
- this.cancelPendingAwaitedSubAgents();
1762
- // ★ 触发 stopWaiters,中断正在执行的 MCP 工具调用(callTool)。
1763
- // MCP callTool 不监听 abortController.signal,只通过 stopWaiter.promise 被中断。
1764
- // 若不触发,MCP 工具超过 120s 后看门狗触发 abort,但 callTool 仍 pending,
1765
- // executeSubmit 永久阻塞 → submitQueue 死锁 → agent 无反应。
1766
- // stopWaiters 是一次性的(触发后清空),不影响后续轮次的新 stopWaiter。
1767
- this.notifyRuntimeStop();
1768
- }
1769
- /**
1770
- * 构造 ask_user_question 工具的依赖注入。
1771
- * onQuestion: 发出 question_request 事件,让 adapter 转发到前端。
1772
- * waitForAnswer: 阻塞等待 submitQuestionAnswer() 调用 resolve,或 abort/stop/supersede 时 reject。
1773
- */
1774
- buildAskUserQuestionDeps() {
1775
- return {
1776
- onQuestion: (toolCallId, questions) => {
1777
- this.emitAcodeEvent("question_request", {
1778
- toolCallId,
1779
- questions: questions,
1780
- });
1781
- },
1782
- waitForAnswer: (toolCallId, contextVersion) => {
1783
- return new Promise((resolve, reject) => {
1784
- this.questionWaiters.set(toolCallId, {
1785
- resolve,
1786
- reject,
1787
- contextVersion,
1788
- });
1789
- });
1790
- },
1791
- };
1792
- }
1793
- /**
1794
- * 构造 sub-agent 工具集依赖(注入 SubAgentManager 句柄)。
1795
- *
1796
- * 5 个工具的依赖:
1797
- * - spawnSubAgent: 创建子 agent(不阻塞,不自动 start)
1798
- * - listSubAgents: 列出当前 agent 的所有子 agent
1799
- * - getSubAgentStatus: 查询某子 agent 实时状态
1800
- * - awaitSubAgents: 阻塞等待指定子 agent 数组完成
1801
- * - abortSubAgent: 终止某子 agent
1802
- */
1803
- buildSubAgentToolDeps(manager) {
1804
- const parentAgentId = this.config.agentId ?? "unknown";
1805
- const parentSessionId = this.config.sessionId;
1806
- const workingDirectory = this.config.workingDirectory;
1807
- const parentConfig = this.config;
1808
- return {
1809
- // 非阻塞 spawn:创建后不等待,立即返回 subId
1810
- spawnSubAgent: async (opts) => {
1811
- const subId = await manager.spawn({
1812
- parentAgentId,
1813
- parentSessionId,
1814
- subagentType: opts.subagentType,
1815
- description: opts.description,
1816
- prompt: opts.prompt,
1817
- workingDirectory,
1818
- parentConfig: {
1819
- ...parentConfig,
1820
- // 注入父 agent 的 MCP 连接,子 agent 复用而非启动新 MCP server 进程
1821
- // 避免 launch binding 已被父 agent 消费后子 agent 401
1822
- sharedMcpConnections: this.state?.mcpConnections,
1823
- },
1824
- });
1825
- // 自动 start(非阻塞,立即返回)
1826
- await manager.start(subId);
1827
- return { subId };
1828
- },
1829
- // 列出当前 agent 的所有子 agent(运行中 + 历史)
1830
- listSubAgents: async () => {
1831
- const entries = manager.listByParent(parentAgentId);
1832
- const running = [];
1833
- const history = [];
1834
- for (const entry of entries) {
1835
- const info = {
1836
- subId: entry.subId,
1837
- subagentType: entry.subagentType,
1838
- description: entry.description,
1839
- status: entry.status,
1840
- startedAt: entry.startedAt,
1841
- endedAt: entry.endedAt,
1842
- durationMs: entry.durationMs,
1843
- };
1844
- if (entry.status === "pending" || entry.status === "running") {
1845
- running.push(info);
1846
- }
1847
- else {
1848
- history.push(info);
1849
- }
1850
- }
1851
- // history 按 endedAt 倒序
1852
- history.sort((a, b) => (b.endedAt ?? 0) - (a.endedAt ?? 0));
1853
- return { running, history };
1854
- },
1855
- // 查询某子 agent 实时状态
1856
- getSubAgentStatus: async (subId) => {
1857
- const entry = manager.getSubAgent(subId);
1858
- if (!entry)
1859
- return null;
1860
- return {
1861
- subId: entry.subId,
1862
- subagentType: entry.subagentType,
1863
- description: entry.description,
1864
- status: entry.status,
1865
- startedAt: entry.startedAt,
1866
- endedAt: entry.endedAt,
1867
- durationMs: entry.durationMs,
1868
- prompt: entry.prompt,
1869
- result: entry.result,
1870
- errorMessage: entry.errorMessage,
1871
- contextSnapshot: entry.contextSnapshot,
1872
- };
1873
- },
1874
- // 阻塞等待指定子 agent 数组完成
1875
- // ★ 记录正在等待的 subIds 到 pendingAwaitedSubIds,让 abort()/stop() 能中断此阻塞:
1876
- // manager.abort(subId) 会 settle completionPromise,使 await completionPromise 解除阻塞,
1877
- // execute() 正常返回,executeSubmit 不再永久 pending,submitQueue 链恢复。
1878
- awaitSubAgents: async (subIds, timeoutMs) => {
1879
- // 记录当前等待的 subIds(用于 abort/stop 时中断)
1880
- const tracking = new Set();
1881
- for (const id of subIds) {
1882
- this.pendingAwaitedSubIds.add(id);
1883
- tracking.add(id);
1884
- }
1885
- try {
1886
- return await manager.awaitCompletion(subIds, timeoutMs);
1887
- }
1888
- finally {
1889
- // 清理跟踪记录(无论成功/失败/中断都需清理)
1890
- for (const id of tracking) {
1891
- this.pendingAwaitedSubIds.delete(id);
1892
- }
1893
- }
1894
- },
1895
- // 终止某子 agent
1896
- abortSubAgent: (subId) => manager.abort(subId),
1897
- };
1898
- }
1899
- /**
1900
- * 接收用户对 ask_user_question 的回答(由 AcodeSdkAdapter.sendQuestionAnswer 调用)。
1901
- * 若当前 turn 已被 supersede(contextVersion 变更),返回 false 让 adapter 知晓。
1902
- */
1903
- submitQuestionAnswer(toolCallId, answers) {
1904
- const waiter = this.questionWaiters.get(toolCallId);
1905
- if (!waiter) {
1906
- return false;
1907
- }
1908
- this.questionWaiters.delete(toolCallId);
1909
- // 若 turn 已被 supersede,不 resolve(避免过期答案污染新轮次)
1910
- if (this.isTurnSuperseded(waiter.contextVersion)) {
1911
- waiter.reject(new Error("turn superseded before user answered"));
1912
- return false;
1913
- }
1914
- waiter.resolve(answers);
1915
- return true;
1916
- }
1917
- /**
1918
- * 中止所有 ask_user_question 等待,让工具 execute() 抛错走 appendToolFailureResult。
1919
- * 在 abort()/stop() 中调用,确保用户点停止或会话终止时 execute 不会永久阻塞。
1920
- */
1921
- rejectAllQuestionWaiters(reason) {
1922
- if (this.questionWaiters.size === 0)
1923
- return;
1924
- for (const [, waiter] of this.questionWaiters) {
1925
- waiter.reject(new Error(`ask_user_question ${reason}`));
1926
- }
1927
- this.questionWaiters.clear();
1928
- }
1929
- /**
1930
- * 取消当前正在 await_sub_agents 工具中阻塞等待的子 agent。
1931
- *
1932
- * 核心作用:让 abort()/stop() 能解除 awaitSubAgents 的阻塞,避免 submitQueue 死锁。
1933
- *
1934
- * 工作原理:
1935
- * 1. 遍历 pendingAwaitedSubIds(awaitSubAgents 工具执行期间记录的 subIds)
1936
- * 2. 对每个 subId 调用 manager.abort(subId)
1937
- * 3. manager.abort 会 settle completionPromise(resolve with error="aborted")
1938
- * 4. awaitSubAgents 中 await completionPromise 解除阻塞,execute() 正常返回
1939
- * 5. executeSubmit 完成,submit() Promise resolve,submitQueue 链恢复
1940
- * 6. 后续 sendMessage / auto-poll 的 submit 能正常排队执行
1941
- *
1942
- * 场景:看门狗触发 abort() 或用户点击停止时,agent 正卡在 await_sub_agents
1943
- * 等待子 agent 完成。若不取消子 agent,completionPromise 永不 settle,
1944
- * executeSubmit 永久 pending,submitQueue 死锁,agent 彻底无反应。
1945
- *
1946
- * 注意:此方法 fire-and-forget,不 await manager.abort 完成。
1947
- * manager.abort 内部会同步 settle completionPromise(通过 completionResolve),
1948
- * 解除 awaitSubAgents 阻塞无需等待 abort I/O 完成。
1949
- */
1950
- cancelPendingAwaitedSubAgents() {
1951
- if (this.pendingAwaitedSubIds.size === 0)
1952
- return;
1953
- const manager = this.config.subAgentManager;
1954
- if (!manager)
1955
- return;
1956
- const subIds = Array.from(this.pendingAwaitedSubIds);
1957
- runtimeDebugLog(`[ACodeRuntime] ⛔ Cancelling ${subIds.length} pending awaited sub-agent(s): ${subIds.join(", ")}`);
1958
- // fire-and-forget:manager.abort 内部同步 settle completionPromise 即可解除阻塞
1959
- for (const subId of subIds) {
1960
- void manager.abort(subId).catch((err) => {
1961
- runtimeDebugLog(`[ACodeRuntime] ⚠️ manager.abort(${subId}) threw: ${err instanceof Error ? err.message : String(err)}`);
1962
- });
1963
- }
1964
- }
1965
- /** 终止 ACode 会话并发出 session_complete。 */
1966
- async stop() {
1967
- if (this.state.terminated) {
1968
- return;
1969
- }
1970
- this.state.terminated = true;
1971
- this.notifyRuntimeStop();
1972
- this.abortRequested = this.activeTurn;
1973
- this.abortController?.abort();
1974
- this.abortController = null;
1975
- // ask_user_question 等待用户回答期间被 stop,需 reject 所有 waiter
1976
- this.rejectAllQuestionWaiters("session terminated");
1977
- // ★ 取消正在 await_sub_agents 中阻塞等待的子 agent,让 completionPromise settle,
1978
- // 解除 execute() 阻塞,避免 submitQueue 永久死锁。
1979
- this.cancelPendingAwaitedSubAgents();
1980
- await runBestEffort("ACode active turn shutdown", this.submitQueue, (message) => {
1981
- this.emitAcodeEvent("error", { message });
1982
- });
1983
- await this.disconnectMcpServers();
1984
- await runBestEffort("ACode Stop hooks", this.hookBus.dispatch("Stop", { reason: "session_stop" }), (message) => {
1985
- this.emitAcodeEvent("error", { message });
1986
- });
1987
- this.state.terminated = true;
1988
- if (!this.sessionCompleteEmitted) {
1989
- this.sessionCompleteEmitted = true;
1990
- this.emitAcodeEvent("session_complete", { exitCode: 0 });
1991
- }
1992
- this.stopCompactionDebugLog();
1993
- this.stopSkillWatchers();
1994
- if (COMPACTION_DEBUG_ENABLED) {
1995
- try {
1996
- fs.promises.appendFile(COMPACTION_DEBUG_LOG, `[${new Date().toISOString()}] runtime stopped | ---\n`, "utf-8").catch(() => {
1997
- // 忽略
1998
- });
1999
- }
2000
- catch {
2001
- // 忽略
2002
- }
2003
- }
2004
- }
2005
- resolveCommandDirs(acodeConfig) {
2006
- const configured = [
2007
- ...(this.config.commandDirs ?? []),
2008
- ...(acodeConfig.commandDirs ?? []),
2009
- ...(acodeConfig.commands?.directories ?? []),
2010
- ];
2011
- return [
2012
- ...new Set(configured.map((directory) => directory.trim()).filter(Boolean)),
2013
- ];
2014
- }
2015
- async runModelTurn(commandAllowedTools, contextVersion = this.contextVersion) {
2016
- let finalText = "";
2017
- let finalUsage;
2018
- let finalResponseBody;
2019
- let finalFinishReason;
2020
- let accumulatedAssistantText = ""; // 累加多轮迭代的 output 文本长度
2021
- let accumulatedPromptTokens = 0; // 累加多轮迭代的 prompt_tokens
2022
- // 日志:开始 runModelTurn
2023
- runtimeDebugLog(`[ACodeRuntime] ▶️ runModelTurn started (contextVersion: ${contextVersion})`);
2024
- // 移除5次限制,添加安全上限防止真正的无限循环
2025
- const SAFETY_LIMIT = 50; // 安全上限,正常情况下不会达到
2026
- for (let step = 0; step < SAFETY_LIMIT; step += 1) {
2027
- // 详细日志:记录每次迭代
2028
- runtimeDebugLog(`[ACodeRuntime] 🔄 Iteration ${step + 1} starting...`);
2029
- if (this.isTurnSuperseded(contextVersion)) {
2030
- runtimeDebugLog(`[ACodeRuntime] ⚠️ Turn superseded at iteration ${step + 1}, aborting`);
2031
- return { text: "", totalOutputTextLength: accumulatedAssistantText.length };
2032
- }
2033
- const requestStartTime = Date.now();
2034
- const response = await this.requestChatCompletion(commandAllowedTools);
2035
- const requestDuration = Date.now() - requestStartTime;
2036
- // 日志:AI请求完成
2037
- runtimeDebugLog(`[ACodeRuntime] 🤖 AI request completed in ${requestDuration}ms, ` +
2038
- `choices: ${response.choices?.length || 0}`);
2039
- if (this.isTurnSuperseded(contextVersion)) {
2040
- runtimeDebugLog(`[ACodeRuntime] ⚠️ Turn superseded after AI response at iteration ${step + 1}`);
2041
- return { text: "", totalOutputTextLength: accumulatedAssistantText.length };
2042
- }
2043
- if (this.state.terminated) {
2044
- runtimeDebugLog(`[ACodeRuntime] Runtime terminated at iteration ${step + 1}`);
2045
- throw new Error("ACode runtime was stopped during the active turn");
2046
- }
2047
- const choice = response.choices?.[0];
2048
- finalResponseBody = JSON.stringify(response, null, 2);
2049
- finalFinishReason =
2050
- typeof choice?.finish_reason === "string"
2051
- ? choice.finish_reason
2052
- : undefined;
2053
- const message = choice?.message;
2054
- const content = typeof message?.content === "string" ? message.content : "";
2055
- const toolCalls = Array.isArray(message?.tool_calls)
2056
- ? message.tool_calls
2057
- : [];
2058
- // 日志:AI响应详情
2059
- runtimeDebugLog(`[ACodeRuntime] AI response: ` +
2060
- `content=${content ? content.slice(0, 100) + (content.length > 100 ? '...' : '') : '(empty)'}, ` +
2061
- `toolCalls=${toolCalls.length}, ` +
2062
- `finishReason=${finalFinishReason || '(none)'}`);
2063
- if (response.usage) {
2064
- const rtInputTokens = response.usage.prompt_tokens ?? response.usage.input_tokens;
2065
- const rtOutputTokens = response.usage.completion_tokens ?? response.usage.output_tokens;
2066
- if (typeof rtInputTokens === "number" &&
2067
- typeof rtOutputTokens === "number") {
2068
- // ★ finalUsage 记录当前迭代的实际 token 用量(覆盖式,非累加)。
2069
- // prompt_tokens 代表本次请求时 provider 看到的完整上下文大小,
2070
- // 若跨迭代累加会得到 N × 上下文大小 的无意义总和,导致 turn_complete
2071
- // 上报的 usage 远超真实上下文,前端"上下文"标签会瞬间跳到几 M 再跳回。
2072
- // 改为覆盖式,与 state.accumulatedInputTokens 及 AcodeSdkAdapter 的
2073
- // "当前上下文"语义保持一致。
2074
- finalUsage = {
2075
- inputTokens: rtInputTokens,
2076
- outputTokens: rtOutputTokens,
2077
- };
2078
- accumulatedPromptTokens += rtInputTokens;
2079
- // ★ 更新 state 为当前迭代的实际 token 用量(非累加)
2080
- // prompt_tokens 代表本次请求时 provider 看到的完整上下文大小,
2081
- // 是当前上下文大小的最准确度量。
2082
- // 之前用 += 累加导致 actualUsed 只增不减,shouldCompact 永远为 true。
2083
- this.state.accumulatedInputTokens = rtInputTokens;
2084
- this.state.accumulatedOutputTokens = rtOutputTokens;
2085
- runtimeDebugLog(`[ACodeRuntime] 💰 Token usage: input=${rtInputTokens}, output=${rtOutputTokens} ` +
2086
- `(turn accumulated cost: input=${accumulatedPromptTokens})`);
2087
- }
2088
- }
2089
- this.state.conversation.push({
2090
- role: "assistant",
2091
- content: content || null,
2092
- tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
2093
- });
2094
- if (content) {
2095
- finalText = content;
2096
- accumulatedAssistantText += content;
2097
- runtimeDebugLog(`[ACodeRuntime] ✨ Captured assistant text: ${content.slice(0, 80)}...`);
2098
- }
2099
- if (toolCalls.length === 0) {
2100
- runtimeDebugLog(`[ACodeRuntime] ✅ No tool calls at iteration ${step + 1}, completing turn`);
2101
- return {
2102
- text: finalText,
2103
- usage: finalUsage,
2104
- responseBody: finalResponseBody,
2105
- finishReason: finalFinishReason,
2106
- totalOutputTextLength: accumulatedAssistantText.length,
2107
- };
2108
- }
2109
- // 日志:即将执行工具调用
2110
- const toolNames = toolCalls.map(tc => tc.function.name).join(", ");
2111
- runtimeDebugLog(`[ACodeRuntime] 🔧 Executing ${toolCalls.length} tool(s) at iteration ${step + 1}: ${toolNames}`);
2112
- const toolStart = Date.now();
2113
- await this.handleToolCalls(toolCalls, commandAllowedTools, contextVersion);
2114
- const toolDuration = Date.now() - toolStart;
2115
- // 日志:工具执行完成
2116
- runtimeDebugLog(`[ACodeRuntime] ⚙️ Tool execution completed in ${toolDuration}ms at iteration ${step + 1}`);
2117
- if (this.isTurnSuperseded(contextVersion)) {
2118
- runtimeDebugLog(`[ACodeRuntime] ⚠️ Turn superseded after tool execution at iteration ${step + 1}`);
2119
- return { text: "", totalOutputTextLength: accumulatedAssistantText.length };
2120
- }
2121
- // 日志:准备下一次迭代
2122
- runtimeDebugLog(`[ACodeRuntime] 🔄 Preparing iteration ${step + 2}...`);
2123
- }
2124
- // 达到安全上限(理论上不应该发生)
2125
- console.error(`[ACodeRuntime] 🚨 CRITICAL: Reached safety limit of ${SAFETY_LIMIT} iterations! ` +
2126
- `This indicates a potential infinite loop. Forcing turn completion.`);
2127
- return {
2128
- text: finalText || `ACode 已达到安全迭代上限(${SAFETY_LIMIT}次),强制结束本轮对话。`,
2129
- usage: finalUsage,
2130
- responseBody: finalResponseBody,
2131
- finishReason: finalFinishReason || "safety_iteration_limit_reached",
2132
- totalOutputTextLength: accumulatedAssistantText.length,
2133
- };
2134
- }
2135
- /**
2136
- * LLM 摘要器:调用当前 provider 生成结构化对话摘要。
2137
- *
2138
- * 这是把 OpenCode 的 LLM 摘要能力赋能到 ACode 的核心方法:
2139
- * - OpenCode 在 compaction.process 中调用独立的 compaction agent
2140
- * - ACode 复用当前 provider 配置,发起一次非流式 chat completion 请求
2141
- * - 使用 COMPACTION_LLM_PROMPT_TEMPLATE 作为 prompt 模板
2142
- *
2143
- * 设计要点:
2144
- * 1. 非流式调用(不需要流式,简化处理)
2145
- * 2. 不带 tools(纯文本生成)
2146
- * 3. 失败时抛出异常,由 summarizeConversation 降级到规则摘要
2147
- * 4. 独立的 AbortController,避免主请求取消影响摘要
2148
- * 5. 复用 provider 的 timeout 配置
2149
- *
2150
- * @param input 摘要输入(conversationText + messageCount + contextLimit)
2151
- * @returns LLM 生成的结构化摘要文本
2152
- */
2153
- async summarizeConversationWithLLM(input) {
2154
- const provider = this.state.provider;
2155
- const baseURL = provider.baseURL;
2156
- if (!baseURL) {
2157
- throw new Error("ACode provider baseURL is not configured for LLM summarization");
2158
- }
2159
- const headers = {
2160
- "Content-Type": "application/json",
2161
- };
2162
- if (provider.apiKey) {
2163
- headers.Authorization = `Bearer ${provider.apiKey}`;
2164
- }
2165
- const requestBody = {
2166
- model: provider.model,
2167
- stream: false,
2168
- messages: [
2169
- {
2170
- role: "system",
2171
- content: "You are a helpful AI assistant tasked with summarizing conversations. " +
2172
- "When asked to summarize, provide a detailed but concise summary of the conversation. " +
2173
- "Focus on information that would be helpful for continuing the conversation. " +
2174
- "Do not respond to any questions in the conversation, only output the summary.",
2175
- },
2176
- {
2177
- role: "user",
2178
- content: `Below is an earlier conversation (${input.messageCount} messages) that needs to be summarized ` +
2179
- `to free context space (current context limit: ${input.contextLimit} tokens).\n\n` +
2180
- `--- Conversation Start ---\n${input.conversationText}\n--- Conversation End ---\n\n` +
2181
- COMPACTION_LLM_PROMPT_TEMPLATE,
2182
- },
2183
- ],
2184
- };
2185
- const abortController = new AbortController();
2186
- this.abortController = abortController;
2187
- // ★ 安全修复:用外层 withTimeout(requestTotalTimeoutMs) 兜底整个摘要请求,
2188
- // 防止 provider 持续发送 SSE keep-alive 或 chunk 间隔 < 60s 导致 idle timeout
2189
- // 永不触发、摘要请求永久挂起。与 requestChatCompletion 的安全模式一致。
2190
- // 否则子 agent 触发主动压缩时,requestChatCompletion 卡在 performCompaction →
2191
- // summarizeConversationWithLLM → fetch 永久挂起,ai_request 事件不发射,
2192
- // 前端 snapshot 全 0(当前工具"-" / 消息数 0 / Tokens 0/0)。
2193
- let timedOut = false;
2194
- try {
2195
- return await withTimeout((async () => {
2196
- const response = await withTimeout(fetch(normalizeChatCompletionsUrl(baseURL), {
2197
- method: "POST",
2198
- headers,
2199
- body: JSON.stringify(requestBody),
2200
- signal: abortController.signal,
2201
- }), provider.requestTimeoutMs, "ACode LLM summarization", () => {
2202
- timedOut = true;
2203
- abortController.abort();
2204
- });
2205
- if (!response.ok) {
2206
- // ★ 防御修复:用 withTimeout 包裹 response.text(),防止错误响应 body stream 挂起
2207
- const errorBody = await withTimeout(response.text(), provider.requestTimeoutMs, "ACode LLM summarization error response body", () => {
2208
- timedOut = true;
2209
- abortController.abort();
2210
- });
2211
- const truncatedBody = errorBody.length > 500
2212
- ? `${errorBody.slice(0, 500)}...(truncated, total ${errorBody.length} chars)`
2213
- : errorBody;
2214
- throw new Error(`ACode LLM summarization request failed: ${response.status} ${truncatedBody}`);
2215
- }
2216
- // ★ 防御修复:用 withTimeout 包裹 response.json(),防止成功响应 body stream
2217
- // 挂起(provider 返回 200 但 body 永不结束)导致摘要永久阻塞。
2218
- // 此前缺失此包裹,是子 agent 卡死的核心原因之一。
2219
- const payload = (await withTimeout(response.json(), provider.requestTimeoutMs, "ACode LLM summarization response body", () => {
2220
- timedOut = true;
2221
- abortController.abort();
2222
- }));
2223
- const content = payload.choices?.[0]?.message?.content;
2224
- if (!content || content.trim().length === 0) {
2225
- throw new Error("ACode LLM summarization returned empty content");
2226
- }
2227
- return content;
2228
- })(), provider.requestTotalTimeoutMs, "ACode LLM summarization total", () => {
2229
- // 总预算超时:abort 底层 fetch / body reader,避免泄漏
2230
- abortController.abort();
2231
- });
2232
- }
2233
- catch (error) {
2234
- // 区分 abort 原因:idle 超时 / 总预算超时 / 主动 abort,给出明确错误信息
2235
- if (error instanceof Error && error.name === "AbortError") {
2236
- throw timedOut
2237
- ? new Error(`ACode LLM summarization timed out after ${provider.requestTimeoutMs}ms`)
2238
- : new Error("ACode LLM summarization aborted");
2239
- }
2240
- throw error;
2241
- }
2242
- finally {
2243
- if (this.abortController === abortController) {
2244
- this.abortController = null;
2245
- }
2246
- }
2247
- }
2248
- async requestChatCompletion(commandAllowedTools) {
2249
- const provider = this.state.provider;
2250
- const baseURL = provider.baseURL;
2251
- if (!baseURL) {
2252
- throw new Error("ACode provider baseURL is not configured");
2253
- }
2254
- // 主动上下文压缩:在发送 AI 请求前检测 token 是否溢出。
2255
- // ★ 与 session_started 事件 emit 的阈值同源(resolveCompactionConfig),保证前端显示 = 实际触发。
2256
- const { contextLimit, maxOutput, inputLimit, threshold: compactionThreshold } = resolveCompactionConfig(provider);
2257
- // 使用 effectiveUsed = max(actualUsed, estimatedUsed)
2258
- // 因为部分 provider 不返回 usage 数据(actualUsed=0),此时降级到文本估算
2259
- const actualUsed = this.state.accumulatedInputTokens + this.state.accumulatedOutputTokens;
2260
- // ★ 估算时加入 tools 定义开销,使估算更接近实际请求 token 数。
2261
- // 之前只估算 conversation 消息,遗漏了 tools 的 JSON schema(可达数千 tokens),
2262
- // 导致估算系统性偏低、主动压缩阈值无法触达。
2263
- const estimatedUsed = estimateConversationTokens(this.state.conversation)
2264
- + estimateToolsTokens(this.state.exposedTools);
2265
- const effectiveUsed = Math.max(actualUsed, estimatedUsed);
2266
- const shouldCompact = effectiveUsed >= compactionThreshold;
2267
- runtimeDebugLog(`[ACodeRuntime] 🔍 Compaction check: ` +
2268
- `actualUsed=${actualUsed}, estimation=${estimatedUsed}, ` +
2269
- `effectiveUsed=${effectiveUsed}, contextLimit=${contextLimit}, ` +
2270
- `inputLimit=${inputLimit ?? "n/a"}, maxOutput=${maxOutput}, ` +
2271
- `triggerThreshold=${compactionThreshold}, shouldCompact=${shouldCompact}`);
2272
- if (shouldCompact) {
2273
- const sizeBeforeKB = conversationSizeKB(this.state.conversation);
2274
- const compactionId = `compaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2275
- this.emitAcodeEvent("context_compaction", {
2276
- action: "start",
2277
- compactionId,
2278
- sizeKB: sizeBeforeKB,
2279
- estimatedTokens: estimatedUsed,
2280
- actualInputTokens: this.state.accumulatedInputTokens,
2281
- actualOutputTokens: this.state.accumulatedOutputTokens,
2282
- messageCount: this.state.conversation.length,
2283
- model: provider.model,
2284
- trigger: actualUsed >= compactionThreshold ? "actual_usage" : "estimated_overflow",
2285
- });
2286
- // ★ 发射压缩子事件(检测到溢出)
2287
- this.emitCompactionPhase(compactionId, "detect", "检测到上下文溢出", `有效用量: ${effectiveUsed} tokens, 阈值: ${compactionThreshold}, 触发: ${actualUsed >= compactionThreshold ? "实际用量" : "估算溢出"}`);
2288
- const result = await performCompaction(this.state.conversation, contextLimit, maxOutput, {
2289
- llmSummarizer: (input) => this.summarizeConversationWithLLM(input),
2290
- onPhase: (phaseEvent) => this.handleCompactionPhase(compactionId, phaseEvent),
2291
- // 传入 effectiveUsed 修复文本估算低于实际 token 数时
2292
- // post-prune isOverflow 误判导致 summarize 被跳过的问题
2293
- actualUsedTokens: effectiveUsed,
2294
- });
2295
- const sizeAfterKB = conversationSizeKB(this.state.conversation);
2296
- this.emitAcodeEvent("context_compaction", {
2297
- action: "end",
2298
- compactionId,
2299
- sizeBeforeKB,
2300
- sizeAfterKB,
2301
- freedKB: Math.round((sizeBeforeKB - sizeAfterKB) * 10) / 10,
2302
- strategy: result.strategy,
2303
- freedTokens: result.freedTokens,
2304
- tokensBefore: result.tokensBefore,
2305
- tokensAfter: result.tokensAfter,
2306
- prunedToolMessages: result.prunedToolMessages,
2307
- summarizedMessages: result.summarizedMessages,
2308
- messageCountBefore: result.messageCountBefore,
2309
- messageCountAfter: result.messageCountAfter,
2310
- });
2311
- // ★ 压缩后重置累积 token 计数,使下一次请求时 actualUsed 反映压缩后的真实上下文大小。
2312
- // 若不重置,actualUsed 仍为压缩前的旧值,shouldCompact 会持续为 true 导致无效压缩循环。
2313
- this.state.accumulatedInputTokens = 0;
2314
- this.state.accumulatedOutputTokens = 0;
2315
- }
2316
- const truncatedCount = await truncateConversationIfNeeded(this.state.conversation, MAX_REQUEST_BODY_BYTES, contextLimit, maxOutput, (input) => this.summarizeConversationWithLLM(input));
2317
- if (truncatedCount > 0) {
2318
- this.emitAcodeEvent("warning", {
2319
- message: `Conversation truncated: removed ${truncatedCount} older messages to fit within ${MAX_REQUEST_BODY_BYTES} byte request size limit`,
2320
- });
2321
- // ★ HTTP body 兜底压缩后同样重置累积 token 计数,避免下一轮 actualUsed 仍为
2322
- // 压缩前旧值导致 shouldCompact 误判(与主动压缩路径行为一致,见上方 shouldCompact 块)。
2323
- this.state.accumulatedInputTokens = 0;
2324
- this.state.accumulatedOutputTokens = 0;
2325
- }
2326
- const allowedTools = this.state.exposedTools.filter((tool) => isToolAllowed(tool, this.state.permissions, commandAllowedTools));
2327
- const buildBody = () => {
2328
- const body = {
2329
- model: provider.model,
2330
- messages: this.state.conversation,
2331
- stream: provider.stream,
2332
- };
2333
- // ★ 思考强度:写入 reasoning_effort 字段,兼容 OpenAI o-series 及兼容 API。
2334
- // 未配置 effort 时不写入,由 provider 使用默认值。
2335
- if (provider.effort) {
2336
- body.reasoning_effort = provider.effort;
2337
- }
2338
- // ★ 流式请求必须设置 stream_options.include_usage,否则 provider 不返回 usage,
2339
- // 导致 accumulatedInputTokens 永远为 0,主动压缩无法基于实际用量触发。
2340
- if (provider.stream) {
2341
- body.stream_options = { include_usage: true };
2342
- }
2343
- if (allowedTools.length > 0) {
2344
- body.tools = toOpenAiTools(allowedTools);
2345
- body.tool_choice = "auto";
2346
- }
2347
- return body;
2348
- };
2349
- const requestMethod = "POST";
2350
- const requestUrl = summarizeProviderUrl(baseURL);
2351
- const requestHeaders = buildRequestHeaders(provider);
2352
- const redactedRequestHeaders = redactRequestHeaders(requestHeaders);
2353
- const emitAiRequest = (body) => {
2354
- const requestBody = JSON.stringify(body, null, 2);
2355
- // [DEBUG] ACode Runtime 实际发送的模型名称
2356
- runtimeDebugLog(`[ACodeRuntime] ★★★ DEBUG ★★★ emitAiRequest: 发送请求 model=${provider.model}, endpoint=${requestUrl}, messageCount=${this.state.conversation.length}`);
2357
- this.emitAcodeEvent("ai_request", {
2358
- model: provider.model,
2359
- endpoint: requestUrl,
2360
- requestMethod,
2361
- requestUrl,
2362
- requestHeaders: redactedRequestHeaders,
2363
- stream: provider.stream,
2364
- messageCount: this.state.conversation.length,
2365
- toolCount: allowedTools.length,
2366
- lastUserMessagePreview: summarizeLastUserMessage(this.state.conversation),
2367
- estimatedContextTokens: estimateTokensFromText(requestBody),
2368
- requestBody,
2369
- });
2370
- };
2371
- let body = buildBody();
2372
- emitAiRequest(body);
2373
- const infiniteRetries = provider.requestRetryCount === INFINITE_RETRY_SENTINEL;
2374
- const maxAttempts = infiniteRetries ? Infinity : provider.requestRetryCount + 1;
2375
- let contextOverflowCompacted = false;
2376
- for (let attempt = 1; infiniteRetries || attempt <= maxAttempts; attempt += 1) {
2377
- const requestAbortController = new AbortController();
2378
- this.abortController = requestAbortController;
2379
- let timedOut = false;
2380
- try {
2381
- return await withTimeout((async () => {
2382
- const response = await withTimeout(fetch(normalizeChatCompletionsUrl(baseURL), {
2383
- method: requestMethod,
2384
- headers: requestHeaders,
2385
- body: JSON.stringify(body),
2386
- signal: requestAbortController.signal,
2387
- }), provider.requestTimeoutMs, "ACode provider request", () => {
2388
- timedOut = true;
2389
- requestAbortController.abort();
2390
- });
2391
- if (!response.ok) {
2392
- // ★ 防御修复:用 withTimeout 包裹 response.text(),
2393
- // 防止 502/503 等错误响应的 body stream 挂起导致永久阻塞。
2394
- // 外层 withTimeout(requestTotalTimeoutMs, 10min) 兜底太晚,
2395
- // 且看门狗可能已被重试 error 事件禁用。
2396
- const errorBody = await withTimeout(response.text(), provider.requestTimeoutMs, "ACode provider error response body", () => {
2397
- timedOut = true;
2398
- requestAbortController.abort();
2399
- });
2400
- // ★ 截断过长的错误响应体(如 Cloudflare 502 HTML 页面可达数 KB),
2401
- // 避免错误消息通过事件链路传播时造成 HTTP POST body 过大或日志膨胀
2402
- const truncatedBody = errorBody.length > 500
2403
- ? `${errorBody.slice(0, 500)}...(truncated, total ${errorBody.length} chars)`
2404
- : errorBody;
2405
- throw new Error(`ACode provider request failed: ${response.status} ${truncatedBody}`);
2406
- }
2407
- const contentType = response.headers?.get?.("content-type") || "";
2408
- if (provider.stream &&
2409
- contentType.toLowerCase().includes("text/event-stream")) {
2410
- return await this.readChatCompletionStream(response, provider.requestTimeoutMs, requestAbortController);
2411
- }
2412
- const payload = (await withTimeout(response.json(), provider.requestTimeoutMs, "ACode provider response body", () => {
2413
- timedOut = true;
2414
- requestAbortController.abort();
2415
- }));
2416
- // ★ 容错处理: 如果网关返回的不是有效对象,记录警告并返回空响应
2417
- // 避免 Agent 永久卡在 thinking/tool_using 状态
2418
- if (!isRecord(payload)) {
2419
- this.emitAcodeEvent("warning", {
2420
- message: `ACode provider returned invalid response format (expected object, got ${typeof payload}); treating as empty response`,
2421
- });
2422
- return {};
2423
- }
2424
- return payload;
2425
- })(),
2426
- // ★ 安全修复:即使无限重试也保留总预算超时兜底,
2427
- // 防止 provider 持续发送 SSE keep-alive 导致 idle timeout 永不触发、
2428
- // 请求永久挂起。用户可通过配置更大的 requestTotalTimeoutMs 值来放宽限制。
2429
- provider.requestTotalTimeoutMs, "ACode provider request total", () => {
2430
- // 总预算超时:abort 底层 fetch / stream reader,避免泄漏
2431
- requestAbortController.abort();
2432
- });
2433
- }
2434
- catch (error) {
2435
- let requestError;
2436
- if (error instanceof Error && error.name === "AbortError") {
2437
- requestError = timedOut
2438
- ? new Error(`ACode provider request timed out after ${provider.requestTimeoutMs}ms`)
2439
- : new Error("ACode provider request aborted");
2440
- }
2441
- else {
2442
- requestError =
2443
- error instanceof Error ? error : new Error(String(error));
2444
- }
2445
- if (!contextOverflowCompacted &&
2446
- !this.abortRequested &&
2447
- !this.state.terminated &&
2448
- isContextOverflowProviderRequestError(requestError)) {
2449
- // 响应式上下文压缩:Provider 返回上下文溢出错误时强制执行
2450
- // 这里不通过 performCompaction(它会先检查 isOverflow),
2451
- // 而是直接调用 summarizeConversation 进行强制摘要化
2452
- const sizeBeforeKB = conversationSizeKB(this.state.conversation);
2453
- const tokensBeforeOC = estimateConversationTokens(this.state.conversation); // tokensBefore 命名冲突,加 OC 后缀
2454
- const msgCountBefore = this.state.conversation.length;
2455
- const compactionId = `compaction-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2456
- this.emitAcodeEvent("context_compaction", {
2457
- action: "start",
2458
- compactionId,
2459
- sizeKB: sizeBeforeKB,
2460
- estimatedTokens: tokensBeforeOC,
2461
- messageCount: msgCountBefore,
2462
- model: provider.model,
2463
- trigger: "provider_overflow_error",
2464
- });
2465
- // ★ 发射压缩子事件(检测到溢出)
2466
- this.emitCompactionPhase(compactionId, "detect", "Provider 返回上下文溢出", `消息: ${msgCountBefore}条, 大小: ${sizeBeforeKB}KB`);
2467
- // 先尝试 prune 旧工具输出
2468
- this.emitCompactionPhase(compactionId, "prune", "阶段1: 裁剪工具输出", "正在裁剪...");
2469
- const pruneResult = pruneToolOutputs(this.state.conversation, contextLimit);
2470
- this.emitCompactionPhase(compactionId, "prune_done", "阶段1: 裁剪工具输出", `裁剪了 ${pruneResult.prunedCount} 条工具输出, 释放 ${pruneResult.freedTokens} tokens`);
2471
- // 再强制 summarize 早期对话(使用 LLM 摘要器提升摘要质量,失败降级到规则摘要)
2472
- this.emitCompactionPhase(compactionId, "summarize", "阶段2: 摘要化对话", "正在摘要...");
2473
- const summarizedCount = await summarizeConversation(this.state.conversation, {
2474
- llmSummarizer: (input) => this.summarizeConversationWithLLM(input),
2475
- contextLimit,
2476
- });
2477
- this.emitCompactionPhase(compactionId, "summarize_done", "阶段2: 摘要化对话", `摘要化了 ${summarizedCount} 条消息`);
2478
- const sizeAfterKB = conversationSizeKB(this.state.conversation);
2479
- const tokensAfterOC = estimateConversationTokens(this.state.conversation);
2480
- const msgCountAfter = this.state.conversation.length;
2481
- contextOverflowCompacted = true;
2482
- const compactedCount = pruneResult.prunedCount + summarizedCount;
2483
- this.emitAcodeEvent("context_compaction", {
2484
- action: "end",
2485
- compactionId,
2486
- sizeBeforeKB,
2487
- sizeAfterKB,
2488
- freedKB: Math.round((sizeBeforeKB - sizeAfterKB) * 10) / 10,
2489
- tokensBefore: tokensBeforeOC,
2490
- tokensAfter: tokensAfterOC,
2491
- messageCountBefore: msgCountBefore,
2492
- messageCountAfter: msgCountAfter,
2493
- strategy: summarizedCount > 0 ? "summarize" : "prune",
2494
- freedTokens: pruneResult.freedTokens,
2495
- prunedToolMessages: pruneResult.prunedCount,
2496
- summarizedMessages: summarizedCount,
2497
- trigger: "provider_overflow_error",
2498
- });
2499
- if (compactedCount === 0) {
2500
- throw requestError;
2501
- }
2502
- // ★ 响应式压缩后同样重置累积 token 计数
2503
- this.state.accumulatedInputTokens = 0;
2504
- this.state.accumulatedOutputTokens = 0;
2505
- this.emitAcodeEvent("warning", {
2506
- message: `ACode provider context limit exceeded; summarized ${compactedCount} older messages and rebuilt the request instead of retrying the same oversized payload`,
2507
- });
2508
- body = buildBody();
2509
- emitAiRequest(body);
2510
- attempt -= 1;
2511
- continue;
2512
- }
2513
- const shouldRetry = (infiniteRetries || attempt < maxAttempts) &&
2514
- !this.abortRequested &&
2515
- !this.state.terminated &&
2516
- isRetryableProviderRequestError(requestError);
2517
- if (!shouldRetry) {
2518
- throw requestError;
2519
- }
2520
- this.emitAcodeEvent("error", {
2521
- message: `ACode provider request attempt ${attempt}${infiniteRetries ? '' : `/${maxAttempts}`} failed; retrying: ${requestError.message}`,
2522
- // ★ 关键修复:重试中的错误是瞬态的,不是致命错误。
2523
- // 如果不标记 nonFatal,适配器会把 status 设为 "error" 并取消看门狗,
2524
- // 导致后续重试尝试挂起时看门狗无法检测,agent 永久卡死。
2525
- nonFatal: true,
2526
- });
2527
- await delay(DEFAULT_PROVIDER_REQUEST_RETRY_DELAY_MS * attempt);
2528
- }
2529
- finally {
2530
- if (this.abortController === requestAbortController) {
2531
- this.abortController = null;
2532
- }
2533
- }
2534
- }
2535
- throw new Error("ACode provider request failed after all retries");
2536
- }
2537
- async readChatCompletionStream(response, idleTimeoutMs, requestAbortController) {
2538
- if (!response.body) {
2539
- throw new Error("ACode provider stream response has no body");
2540
- }
2541
- const reader = response.body.getReader();
2542
- const decoder = new TextDecoder();
2543
- const toolCallAccumulators = new Map();
2544
- let text = "";
2545
- let buffer = "";
2546
- /** 从 SSE 流最终 chunk 中提取的 usage(OpenAI 格式在最后一个 chunk 的 usage 字段中) */
2547
- let streamUsage;
2548
- const consumeFrame = (frame) => {
2549
- for (const data of parseSseDataLines(frame)) {
2550
- if (data === "[DONE]") {
2551
- continue;
2552
- }
2553
- const payload = JSON.parse(data);
2554
- if (!isRecord(payload)) {
2555
- continue;
2556
- }
2557
- // ★ 提取 SSE chunk 中的 usage(OpenAI 流式 API 在最后一个 chunk 返回完整 usage,此时 choices 为空数组)
2558
- const chunkUsage = payload.usage;
2559
- if (chunkUsage) {
2560
- streamUsage = chunkUsage;
2561
- }
2562
- const choice = payload.choices?.[0];
2563
- const delta = choice?.delta;
2564
- const reasoningText = extractReasoningText(delta);
2565
- if (reasoningText) {
2566
- this.emitAcodeEvent("thinking_delta", { text: reasoningText });
2567
- }
2568
- const content = typeof delta?.content === "string" ? delta.content : "";
2569
- if (content) {
2570
- text += content;
2571
- this.emitAcodeEvent("assistant_delta", { text: content });
2572
- }
2573
- const toolDeltas = Array.isArray(delta?.tool_calls)
2574
- ? delta.tool_calls
2575
- : [];
2576
- for (const toolDelta of toolDeltas) {
2577
- updateToolCallAccumulator(toolCallAccumulators, toolDelta);
2578
- }
2579
- }
2580
- };
2581
- while (true) {
2582
- const { done, value } = await withTimeout(reader.read(), idleTimeoutMs, "ACode provider stream idle", () => requestAbortController?.abort());
2583
- if (done) {
2584
- break;
2585
- }
2586
- buffer += decoder.decode(value, { stream: true });
2587
- const frames = buffer.split(/\r?\n\r?\n/);
2588
- buffer = frames.pop() ?? "";
2589
- for (const frame of frames) {
2590
- consumeFrame(frame);
2591
- }
2592
- }
2593
- buffer += decoder.decode();
2594
- if (buffer.trim()) {
2595
- consumeFrame(buffer);
2596
- }
2597
- return {
2598
- choices: [
2599
- {
2600
- message: {
2601
- content: text || null,
2602
- tool_calls: toAccumulatedToolCalls(toolCallAccumulators),
2603
- },
2604
- },
2605
- ],
2606
- ...(streamUsage ? { usage: streamUsage } : {}),
2607
- };
2608
- }
2609
- async handleToolCalls(toolCalls, commandAllowedTools, contextVersion = this.contextVersion) {
2610
- runtimeDebugLog(`[ACodeRuntime] handleToolCalls started with ${toolCalls.length} tool(s)`);
2611
- for (const toolCall of toolCalls) {
2612
- if (this.isTurnSuperseded(contextVersion)) {
2613
- runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded, skipping remaining tools`);
2614
- return;
2615
- }
2616
- const toolName = toolCall.function.name;
2617
- runtimeDebugLog(`[ACodeRuntime] 🔍 Looking up tool: ${toolName}`);
2618
- let tool = this.state.exposedTools.find((item) => item.exposedName === toolName);
2619
- if (!tool) {
2620
- tool = this.state.exposedTools.find((item) => item.toolName === toolName);
2621
- if (tool) {
2622
- runtimeDebugLog(`[ACodeRuntime] ↩️ 裸名回退匹配: ${toolName} -> ${tool.exposedName}`);
2623
- }
2624
- }
2625
- if (!tool) {
2626
- console.error(`[ACodeRuntime] ❌ Tool not found: ${toolName}, ` +
2627
- `available tools: ${this.state.exposedTools.map(t => t.exposedName).join(", ")}`);
2628
- this.state.conversation.push({
2629
- role: "tool",
2630
- tool_call_id: toolCall.id,
2631
- content: `ACode MCP tool not found: ${toolName}`,
2632
- });
2633
- continue;
2634
- }
2635
- runtimeDebugLog(`[ACodeRuntime] Found tool: ${tool.toolName} (kind: ${tool.kind}, source: ${tool.kind === 'mcp' ? tool.serverName : 'builtin'})`);
2636
- try {
2637
- if (tool.kind === "builtin") {
2638
- const toolArguments = coerceArguments(toolCall.function.arguments);
2639
- runtimeDebugLog(`[ACodeRuntime] Built-in tool arguments: ${JSON.stringify(toolArguments).slice(0, 200)}`);
2640
- assertToolPermission(tool, this.state.permissions, commandAllowedTools);
2641
- const preResults = await this.hookBus.dispatch("PreToolUse", {
2642
- source: "builtin",
2643
- toolName: tool.toolName,
2644
- exposedName: tool.exposedName,
2645
- arguments: toolArguments,
2646
- });
2647
- if (this.isTurnSuperseded(contextVersion)) {
2648
- return;
2649
- }
2650
- const denied = findDeniedHookResult(preResults);
2651
- if (denied) {
2652
- console.error(`[ACodeRuntime] ❌ Tool denied by hook: ${tool.exposedName}: ${denied.message}`);
2653
- throw new Error(denied.message ||
2654
- `ACode tool denied by hook: ${tool.exposedName}`);
2655
- }
2656
- runtimeDebugLog(`[ACodeRuntime] Emitting mcp_status call_tool for ${tool.exposedName}`);
2657
- this.emitAcodeEvent("mcp_status", {
2658
- action: "call_tool",
2659
- server: "builtin",
2660
- tool: tool.toolName,
2661
- exposedName: tool.exposedName,
2662
- toolUseId: toolCall.id,
2663
- arguments: toolArguments,
2664
- argumentsPreview: summarizeToolArguments(toolArguments),
2665
- });
2666
- const toolStart = Date.now();
2667
- const toolTimeoutMs = getBuiltInToolTimeoutMs(tool, toolArguments, this.state.provider.mcpToolTimeoutMs);
2668
- runtimeDebugLog(`[ACodeRuntime] ▶️ Executing built-in tool: ${tool.exposedName} ` +
2669
- `(timeout: ${toolTimeoutMs}ms, abortRequested: ${this.abortRequested})`);
2670
- if (toolTimeoutMs <= 0) {
2671
- console.warn(`[ACodeRuntime] ⚠️ Built-in tool ${tool.exposedName} has NO timeout (timeoutMs=${toolTimeoutMs})! ` +
2672
- `If this tool hangs, executeSubmit will NEVER resolve, blocking the submit queue permanently.`);
2673
- }
2674
- const result = await withTimeout(tool.execute(toolArguments, {
2675
- onOutput: (outputEvent) => {
2676
- this.emitAcodeEvent("command_output", {
2677
- server: "builtin",
2678
- tool: tool.toolName,
2679
- exposedName: tool.exposedName,
2680
- toolUseId: toolCall.id,
2681
- stream: outputEvent.stream,
2682
- text: outputEvent.text,
2683
- sequence: outputEvent.sequence,
2684
- cwd: outputEvent.cwd,
2685
- commandLineCount: outputEvent.commandLineCount,
2686
- ...(outputEvent.commandFilePath
2687
- ? { commandFilePath: outputEvent.commandFilePath }
2688
- : {}),
2689
- });
2690
- },
2691
- toolCallId: toolCall.id,
2692
- }), toolTimeoutMs, `ACode built-in tool ${tool.toolName}`);
2693
- const toolDuration = Date.now() - toolStart;
2694
- runtimeDebugLog(`[ACodeRuntime] ✅ Built-in tool ${tool.exposedName} completed in ${toolDuration}ms`);
2695
- if (this.isTurnSuperseded(contextVersion)) {
2696
- return;
2697
- }
2698
- await this.hookBus.dispatch("PostToolUse", {
2699
- source: "builtin",
2700
- toolName: tool.toolName,
2701
- exposedName: tool.exposedName,
2702
- result: toToolResultText(result),
2703
- });
2704
- if (this.isTurnSuperseded(contextVersion)) {
2705
- return;
2706
- }
2707
- const resultText = toToolResultText(result);
2708
- runtimeDebugLog(`[ACodeRuntime] 📤 Emitting mcp_status tool_result for ${tool.exposedName}, ` +
2709
- `result length: ${resultText.length} chars`);
2710
- this.emitAcodeEvent("mcp_status", {
2711
- action: "tool_result",
2712
- server: "builtin",
2713
- tool: tool.toolName,
2714
- result: resultText,
2715
- });
2716
- this.state.conversation.push({
2717
- role: "tool",
2718
- tool_call_id: toolCall.id,
2719
- content: resultText,
2720
- });
2721
- await this.appendSyntheticMultimodalUserMessage(resultText, toolCall.id, contextVersion);
2722
- continue;
2723
- }
2724
- // MCP工具处理
2725
- const connection = this.state.mcpConnections.find((item) => item.serverName === tool.serverName);
2726
- if (!connection) {
2727
- console.error(`[ACodeRuntime] ❌ MCP server not connected: ${tool.serverName}, ` +
2728
- `connected servers: ${this.state.mcpConnections.map(c => c.serverName).join(", ")}`);
2729
- this.state.conversation.push({
2730
- role: "tool",
2731
- tool_call_id: toolCall.id,
2732
- content: `ACode MCP server not connected: ${tool.serverName}`,
2733
- });
2734
- continue;
2735
- }
2736
- const toolArguments = coerceArguments(toolCall.function.arguments);
2737
- runtimeDebugLog(`[ACodeRuntime] MCP tool arguments: ${JSON.stringify(toolArguments).slice(0, 200)}`);
2738
- assertToolPermission(tool, this.state.permissions, commandAllowedTools);
2739
- const preResults = await this.hookBus.dispatch("PreToolUse", {
2740
- source: "mcp",
2741
- serverName: tool.serverName,
2742
- toolName: tool.toolName,
2743
- exposedName: tool.exposedName,
2744
- arguments: toolArguments,
2745
- });
2746
- if (this.isTurnSuperseded(contextVersion)) {
2747
- return;
2748
- }
2749
- const denied = findDeniedHookResult(preResults);
2750
- if (denied) {
2751
- console.error(`[ACodeRuntime] ❌ MCP tool denied by hook: ${tool.exposedName}: ${denied.message}`);
2752
- throw new Error(denied.message || `ACode tool denied by hook: ${tool.exposedName}`);
2753
- }
2754
- runtimeDebugLog(`[ACodeRuntime] 📤 Emitting mcp_status call_tool for MCP ${tool.exposedName}`);
2755
- this.emitAcodeEvent("mcp_status", {
2756
- action: "call_tool",
2757
- server: tool.serverName,
2758
- tool: tool.toolName,
2759
- exposedName: tool.exposedName,
2760
- toolUseId: toolCall.id,
2761
- arguments: toolArguments,
2762
- argumentsPreview: summarizeToolArguments(toolArguments),
2763
- });
2764
- const isPollingTool = isMcpPollingToolName(tool.toolName);
2765
- const toolTimeoutMs = isPollingTool
2766
- ? this.state.provider.mcpPollingToolTimeoutMs
2767
- : this.state.provider.mcpToolTimeoutMs;
2768
- const toolStart = Date.now();
2769
- runtimeDebugLog(`[ACodeRuntime] ▶️ Executing MCP tool: ${tool.exposedName} ` +
2770
- `(timeout: ${toolTimeoutMs}ms, polling: ${isPollingTool})`);
2771
- // poll_message 工具需要同时检测子 agent 完成事件(有就返回类型),
2772
- // 使用 sub-agent race 包装器替代直接 MCP 调用。
2773
- const result = isPollingTool && getSemanticToolName(tool.toolName) === "poll_message"
2774
- ? await this.callPollMessageWithSubAgentRace(connection, tool, toolArguments, toolTimeoutMs)
2775
- : await this.callMcpToolWithTimeout(connection, tool, toolArguments, toolTimeoutMs, isPollingTool);
2776
- const toolDuration = Date.now() - toolStart;
2777
- runtimeDebugLog(`[ACodeRuntime] ✅ MCP tool ${tool.exposedName} completed in ${toolDuration}ms`);
2778
- if (this.isTurnSuperseded(contextVersion)) {
2779
- return;
2780
- }
2781
- await this.hookBus.dispatch("PostToolUse", {
2782
- source: "mcp",
2783
- serverName: tool.serverName,
2784
- toolName: tool.toolName,
2785
- exposedName: tool.exposedName,
2786
- result: toToolResultText(result),
2787
- });
2788
- if (this.isTurnSuperseded(contextVersion)) {
2789
- return;
2790
- }
2791
- const resultText = toToolResultText(result);
2792
- runtimeDebugLog(`[ACodeRuntime] 📤 Emitting mcp_status tool_result for MCP ${tool.exposedName}, ` +
2793
- `result length: ${resultText.length} chars`);
2794
- this.emitAcodeEvent("mcp_status", {
2795
- action: "tool_result",
2796
- server: tool.serverName,
2797
- tool: tool.toolName,
2798
- result: resultText,
2799
- });
2800
- this.state.conversation.push({
2801
- role: "tool",
2802
- tool_call_id: toolCall.id,
2803
- content: resultText,
2804
- });
2805
- await this.appendSyntheticMultimodalUserMessage(resultText, toolCall.id, contextVersion);
2806
- }
2807
- catch (error) {
2808
- const isTimeout = error instanceof Error &&
2809
- error.message.includes("timed out after");
2810
- const isAbortError = error instanceof Error &&
2811
- error.name === "AbortError";
2812
- console.error(`[ACodeRuntime] ❌ Tool execution failed: ${toolCall.function.name}`, `isTimeout=${isTimeout}, isAbortError=${isAbortError}`, error instanceof Error ? error.message : String(error));
2813
- if (this.isTurnSuperseded(contextVersion)) {
2814
- runtimeDebugLog(`[ACodeRuntime] ️ Turn superseded after tool error, skipping`);
2815
- return;
2816
- }
2817
- if (this.abortRequested || this.state.terminated) {
2818
- // ★ 修复 BUG(时间线永久卡在"编辑文件"):
2819
- // abort/termination 时原实现直接 rethrow,跳过 appendToolFailureResult
2820
- // (3752 行的 mcp_status tool_result 发射),导致 adapter 永远收不到 tool_result,
2821
- // tool_use_end 永不发射,前端时间线永久卡在 tool_use_start 创建的工具条目。
2822
- // 修复:在 rethrow 前补发一个带 failed=true 的 tool_result,关闭时间线条目。
2823
- runtimeDebugLog(`[ACodeRuntime] ⚠️ Rethrowing error due to abort/termination ` +
2824
- `(abortRequested=${this.abortRequested}, terminated=${this.state.terminated})`);
2825
- runtimeDebugLog(`[ACodeRuntime] 📤 Emitting mcp_status tool_result (failed, aborted) ` +
2826
- `for ${tool.exposedName} before rethrow to close timeline entry`);
2827
- this.emitAcodeEvent("mcp_status", {
2828
- action: "tool_result",
2829
- server: tool.kind === "mcp" ? tool.serverName : "builtin",
2830
- tool: tool.toolName,
2831
- result: `[aborted] ${error instanceof Error ? error.message : String(error)}`,
2832
- failed: true,
2833
- aborted: true,
2834
- });
2835
- throw error instanceof Error ? error : new Error(String(error));
2836
- }
2837
- runtimeDebugLog(`[ACodeRuntime] Calling appendToolFailureResult for ${tool.exposedName} ` +
2838
- `(isTimeout=${isTimeout})`);
2839
- this.appendToolFailureResult(toolCall, tool, error);
2840
- }
2841
- }
2842
- runtimeDebugLog(`[ACodeRuntime] handleToolCalls completed for all ${toolCalls.length} tool(s)`);
2843
- }
2844
- async callMcpToolWithTimeout(connection, tool, toolArguments, timeoutMs, retrySdkTimeout) {
2845
- const sdkTimeout = timeoutMs > 0 ? timeoutMs : MCP_SDK_MAX_SAFE_TIMEOUT_MS;
2846
- // 子 agent 共享父 agent 的 MCP 连接时,注入标记让 MCP server 跳过状态上报
2847
- // (StatusReporter 绑定父 agentId,子 agent 的事件不应出现在父 agent 时间线上)
2848
- const isSubAgent = Boolean(this.config.sharedMcpConnections);
2849
- const finalArguments = isSubAgent
2850
- ? { ...toolArguments, _aws_sub_agent: true }
2851
- : toolArguments;
2852
- while (true) {
2853
- const stopWaiter = this.createRuntimeStopWaiter(`ACode MCP tool ${tool.serverName}/${tool.toolName}`);
2854
- try {
2855
- return await withTimeout(Promise.race([
2856
- connection.client.callTool({
2857
- name: tool.toolName,
2858
- arguments: finalArguments,
2859
- }, undefined, { timeout: sdkTimeout }),
2860
- stopWaiter.promise,
2861
- ]), timeoutMs, `ACode MCP tool ${tool.serverName}/${tool.toolName}`);
2862
- }
2863
- catch (error) {
2864
- if (!retrySdkTimeout ||
2865
- timeoutMs > 0 ||
2866
- !isMcpRequestTimeoutError(error)) {
2867
- throw error;
2868
- }
2869
- this.emitAcodeEvent("warning", {
2870
- message: `ACode MCP polling tool ${tool.serverName}/${tool.toolName} reached the MCP SDK timeout window and will continue polling.`,
2871
- });
2872
- }
2873
- finally {
2874
- stopWaiter.cancel();
2875
- }
2876
- }
2877
- }
2878
- /**
2879
- * 将子 agent 终态通知转为 poll_message 格式的合成 DirectMessage。
2880
- * 主流程:构建符合 MCP poll_message 返回格式的 JSON 字符串。
2881
- */
2882
- static formatSubAgentCompletionAsPollResult(notifications) {
2883
- const directMessages = notifications.map((n) => {
2884
- const summary = n.error
2885
- ? `Sub-agent ${n.subId} [${n.status}]: ${n.description}\nError: ${n.error}`
2886
- : `Sub-agent ${n.subId} [${n.status}]: ${n.description}\nOutput: ${n.output.length > 500 ? n.output.slice(0, 500) + "... (truncated)" : n.output}`;
2887
- return {
2888
- msgId: `subagent-completion-${n.subId}-${Date.now()}`,
2889
- senderId: "ACode-Runtime",
2890
- senderName: "系统",
2891
- content: summary,
2892
- requireReply: false,
2893
- prompt: `Sub-agent ${n.subId} (${n.description}) has reached terminal state: ${n.status}. Review its output and decide next steps.`,
2894
- timestamp: new Date().toISOString(),
2895
- };
2896
- });
2897
- return {
2898
- content: [
2899
- {
2900
- type: "text",
2901
- text: JSON.stringify({
2902
- directMessages,
2903
- groupMessages: [],
2904
- fromBuffer: false,
2905
- systemMessages: [],
2906
- }, null, 2),
2907
- },
2908
- ],
2909
- };
2910
- }
2911
- /**
2912
- * 将子 agent 终态通知合并到已有的 MCP poll_message 返回结果中。
2913
- * 主流程:解析 MCP 返回的 JSON -> 将通知转为 DirectMessage 插入到 directMessages 前面 -> 重新序列化。
2914
- */
2915
- static mergeSubAgentCompletionsIntoMcpResult(mcpResult, notifications) {
2916
- if (notifications.length === 0)
2917
- return mcpResult;
2918
- // 提取 MCP 结果中的文本
2919
- const resultText = toToolResultText(mcpResult);
2920
- let parsed;
2921
- try {
2922
- parsed = JSON.parse(resultText);
2923
- }
2924
- catch {
2925
- // 如果 MCP 结果不是 JSON(异常情况),直接返回原结果
2926
- return mcpResult;
2927
- }
2928
- const subAgentMessages = notifications.map((n) => {
2929
- const summary = n.error
2930
- ? `Sub-agent ${n.subId} [${n.status}]: ${n.description}\nError: ${n.error}`
2931
- : `Sub-agent ${n.subId} [${n.status}]: ${n.description}\nOutput: ${n.output.length > 500 ? n.output.slice(0, 500) + "... (truncated)" : n.output}`;
2932
- return {
2933
- msgId: `subagent-completion-${n.subId}-${Date.now()}`,
2934
- senderId: "ACode-Runtime",
2935
- senderName: "系统",
2936
- content: summary,
2937
- requireReply: false,
2938
- prompt: `Sub-agent ${n.subId} (${n.description}) has reached terminal state: ${n.status}. Review its output and decide next steps.`,
2939
- timestamp: new Date().toISOString(),
2940
- };
2941
- });
2942
- const mergedDirectMessages = [
2943
- ...subAgentMessages,
2944
- ...(parsed.directMessages ?? []),
2945
- ];
2946
- return {
2947
- content: [
2948
- {
2949
- type: "text",
2950
- text: JSON.stringify({
2951
- ...parsed,
2952
- directMessages: mergedDirectMessages,
2953
- }, null, 2),
2954
- },
2955
- ],
2956
- };
2957
- }
2958
- /**
2959
- * Memory 自动委托 sub-agent 完成一轮后的外部重新检查。
2960
- *
2961
- * 查询服务端 memory 真实状态(通过 MCP 工具直接调用,非 LLM 自报),
2962
- * 判断 sub-agent 是否真正完成了处理:
2963
- * - memory_file_change:检查每条 memory 的 memory_status 是否已回到 normal
2964
- * - memory_expiration:检查每条 memory 是否已续期(expiresAt 在未来)或已清理(read 返回 null)
2965
- * - memory_cleanup:检查 get_memory_stats 中对应类型/域是否仍 exceeded
2966
- *
2967
- * 若仍有未完成项,返回 { done: false, continueMessage } 包含 "经检查,未完成对xxx的处理,请继续"
2968
- * 及具体未完成列表,由 SubAgentManager 注入同一 sub-agent 的下一轮 submit。
2969
- *
2970
- * recheck 自身出错(MCP 连接不可用、工具调用超时等)时返回 { done: true },不阻塞 sub-agent 结束。
2971
- */
2972
- async recheckMemoryDelegate(type, uris, output, turn) {
2973
- const connection = this.state.mcpConnections.find((c) => c.serverName === 'aws-client-agent-mcp');
2974
- if (!connection) {
2975
- runtimeDebugLog(`[ACodeRuntime] recheckMemoryDelegate: aws-client-agent-mcp connection not found, treating as done`);
2976
- return { done: true };
2977
- }
2978
- try {
2979
- if (type === 'memory_file_change') {
2980
- return await this.recheckMemoryFileChange(connection, uris);
2981
- }
2982
- if (type === 'memory_expiration') {
2983
- return await this.recheckMemoryExpiration(connection, uris);
2984
- }
2985
- return await this.recheckMemoryCleanup(connection, uris);
2986
- }
2987
- catch (err) {
2988
- runtimeDebugLog(`[ACodeRuntime] recheckMemoryDelegate failed (type=${type}, turn=${turn}): ` +
2989
- `${err instanceof Error ? err.message : String(err)}, treating as NOT done (retry via recheck loop)`);
2990
- // 校验异常(读取失败/超时/服务端错误)一律判「未完成」,由 startWithRecheck 的
2991
- // 重试循环(最多 3 次)兜底;既不提前释放 URI,也不会因一次抖动就把子 agent 判为完成。
2992
- return {
2993
- done: false,
2994
- continueMessage: `经检查,无法确认以下 memory 是否已处理(校验异常:${err instanceof Error ? err.message : String(err)})。` +
2995
- `请重新执行验证步骤(read_memory / get_memory_stats)确认真实状态后再继续。`,
2996
- };
2997
- }
2998
- }
2999
- /**
3000
- * 通过 MCP read_memory 工具读取单条 memory 记录。
3001
- * 注入 _aws_sub_agent=true 避免状态上报污染父 agent 时间线。
3002
- * 返回 null 表示 memory 明确不存在(服务端返回 null/undefined 或非对象)。
3003
- * 读取/解析异常(网络抖动、超时、服务端错误)会向上抛出,交由 recheckMemoryDelegate
3004
- * 统一判定为「未完成」,避免被误当作「已处理」。
3005
- */
3006
- async readMemoryViaMcp(connection, uri) {
3007
- try {
3008
- const result = await withTimeout(connection.client.callTool({
3009
- name: 'read_memory',
3010
- arguments: { uri, _aws_sub_agent: true },
3011
- }, undefined, { timeout: 30_000 }), 30_000, `recheck read_memory ${uri}`);
3012
- const text = toToolResultText(result);
3013
- const parsed = JSON.parse(text);
3014
- if (parsed === null || parsed === undefined)
3015
- return null;
3016
- if (typeof parsed !== 'object' || Array.isArray(parsed))
3017
- return null;
3018
- return parsed;
3019
- }
3020
- catch (err) {
3021
- // 读取/解析异常(网络抖动、超时、服务端错误)应向上传播,交由 recheckMemoryDelegate
3022
- // 统一判定为「未完成」(done:false),而非被误当作「memory 不存在 = 已处理」。
3023
- // 仅当服务端正返回明确的 null/undefined 时才表示 memory 不存在。
3024
- throw err;
3025
- }
3026
- }
3027
- /** recheck memory_file_change:检查 memory_status 是否仍为 updating */
3028
- async recheckMemoryFileChange(connection, uris) {
3029
- const stillUpdating = [];
3030
- await Promise.all(uris.map(async (uri) => {
3031
- const mem = await this.readMemoryViaMcp(connection, uri);
3032
- if (mem && mem.memory_status === 'updating') {
3033
- stillUpdating.push(uri);
3034
- }
3035
- }));
3036
- if (stillUpdating.length === 0) {
3037
- runtimeDebugLog(`[ACodeRuntime] recheck memory_file_change: all ${uris.length} URIs done (memory_status=normal or deleted)`);
3038
- return { done: true };
3039
- }
3040
- const list = stillUpdating.map((u) => `- ${u}`).join('\n');
3041
- runtimeDebugLog(`[ACodeRuntime] recheck memory_file_change: ${stillUpdating.length}/${uris.length} URIs still updating`);
3042
- return {
3043
- done: false,
3044
- continueMessage: `经检查,未完成对以下 memory 的更新,请继续:\n${list}\n\n` +
3045
- `这些 memory 的 memory_status 仍为 "updating"。请重新 read_memory 查看内容,` +
3046
- `查看绑定文件最新内容,并调用 update_memory 完成更新。`,
3047
- };
3048
- }
3049
- /** recheck memory_expiration:检查 expiresAt 是否仍过期 或 memory_status 是否仍为 recalling */
3050
- async recheckMemoryExpiration(connection, uris) {
3051
- const stillExpired = [];
3052
- const now = Date.now();
3053
- await Promise.all(uris.map(async (uri) => {
3054
- const mem = await this.readMemoryViaMcp(connection, uri);
3055
- if (!mem)
3056
- return; // memory 已删除/不存在 → 视为已处理
3057
- const expiresAt = mem.expiresAt ?? mem.expires_at;
3058
- const isRecalling = mem.memory_status === 'recalling';
3059
- const isExpired = typeof expiresAt === 'string' && new Date(expiresAt).getTime() < now;
3060
- if (isRecalling || isExpired) {
3061
- stillExpired.push(uri);
3062
- }
3063
- }));
3064
- if (stillExpired.length === 0) {
3065
- runtimeDebugLog(`[ACodeRuntime] recheck memory_expiration: all ${uris.length} URIs done (renewed, cleaned, or no longer expired)`);
3066
- return { done: true };
3067
- }
3068
- const list = stillExpired.map((u) => `- ${u}`).join('\n');
3069
- runtimeDebugLog(`[ACodeRuntime] recheck memory_expiration: ${stillExpired.length}/${uris.length} URIs still expired or recalling`);
3070
- return {
3071
- done: false,
3072
- continueMessage: `经检查,未完成对以下 memory 的处理,请继续:\n${list}\n\n` +
3073
- `这些 memory 仍处于过期或回忆中状态。请调用 memory_recurrence 获取内容,` +
3074
- `对需要保留的调用 update_memory 续期,对无价值的调用 memory_recurrence_complete 清理。`,
3075
- };
3076
- }
3077
- /** recheck memory_cleanup:检查 get_memory_stats 中对应类型/域是否仍 exceeded */
3078
- async recheckMemoryCleanup(connection, uris) {
3079
- // delegate URIs 格式为 "${memoryType}/${domain}"
3080
- let stats;
3081
- try {
3082
- const result = await withTimeout(connection.client.callTool({
3083
- name: 'get_memory_stats',
3084
- arguments: { _aws_sub_agent: true },
3085
- }, undefined, { timeout: 30_000 }), 30_000, 'recheck get_memory_stats');
3086
- const text = toToolResultText(result);
3087
- stats = JSON.parse(text);
3088
- }
3089
- catch {
3090
- // stats 查询失败 → 保守判为「未完成」,避免一次抖动就把清理判为完成并释放 URI。
3091
- // 交由 startWithRecheck 的重试循环兜底;与 recheckMemoryDelegate 中央 catch 行为一致。
3092
- return {
3093
- done: false,
3094
- continueMessage: 'get_memory_stats 查询失败,无法确认容量是否已达标,请重新验证后再继续。',
3095
- };
3096
- }
3097
- const byType = (stats.by_type ?? {});
3098
- const stillExceeded = [];
3099
- for (const uri of uris) {
3100
- const sepIdx = uri.indexOf('/');
3101
- if (sepIdx < 0)
3102
- continue;
3103
- const memoryType = uri.slice(0, sepIdx);
3104
- const domain = uri.slice(sepIdx + 1);
3105
- const typeStats = byType[memoryType];
3106
- const domainStats = typeStats?.by_domain?.[domain];
3107
- if (domainStats?.exceeded) {
3108
- stillExceeded.push(`${memoryType}/${domain} (当前 ${domainStats.count ?? '?'} 条,建议上限 ${domainStats.limit ?? '?'})`);
3109
- }
3110
- }
3111
- if (stillExceeded.length === 0) {
3112
- runtimeDebugLog(`[ACodeRuntime] recheck memory_cleanup: all ${uris.length} type/domain pairs within limits`);
3113
- return { done: true };
3114
- }
3115
- const list = stillExceeded.map((u) => `- ${u}`).join('\n');
3116
- runtimeDebugLog(`[ACodeRuntime] recheck memory_cleanup: ${stillExceeded.length}/${uris.length} type/domain pairs still exceeded`);
3117
- return {
3118
- done: false,
3119
- continueMessage: `经检查,以下类型/域尚未整理完成,请继续:\n${list}\n\n` +
3120
- `请调用 list_memories 查看超限记忆,删除不再需要的 recently_memory 或对 agent_memory 调用 update_memory 续期。`,
3121
- };
3122
- }
3123
- /**
3124
- * 构造 memory 自动委托子 agent 的 prompt。
3125
- * 主流程:根据 delegate 类型(过期整理/文件变更更新)生成自包含指令。
3126
- */
3127
- buildMemoryDelegatePrompt(delegate) {
3128
- const urisText = delegate.uris.map(u => `- ${u}`).join('\n');
3129
- if (delegate.type === 'memory_expiration') {
3130
- return `你是一条专门处理 memory 过期整理的子 agent。
1
+ import{EventEmitter as De}from"node:events";import F from"node:fs";import Oe from"node:os";import K from"node:path";import{Client as Ue}from"@modelcontextprotocol/sdk/client/index.js";import{SSEClientTransport as qe}from"@modelcontextprotocol/sdk/client/sse.js";import{StdioClientTransport as Be}from"@modelcontextprotocol/sdk/client/stdio.js";import{StreamableHTTPClientTransport as ze}from"@modelcontextprotocol/sdk/client/streamableHttp.js";import{createBuiltInFileTools as Fe}from"./built-in-file-tools.js";import{ASK_USER_QUESTION_TOOL_NAME as je,createAskUserQuestionTool as We}from"./ask-user-question-tool.js";import{createSkillTool as Je}from"./skill-tool.js";import{createTodoWriteTool as Ke,createTodoReadTool as He}from"./todo-tool.js";import{createSubAgentTools as Ge}from"./sub-agent-tools.js";import{cleanupAcodeCmdDist as Qe}from"./builtins/commandArtifacts.js";import{loadAcodeCommands as Ye}from"./commands/commandLoader.js";import{ACodeCommandRegistry as ce}from"./commands/commandRegistry.js";import{calculateCompactionThreshold as Ve,COMPACTION_LLM_PROMPT_TEMPLATE as Xe,conversationSizeKB as j,estimateConversationTokens as H,performCompaction as le,pruneToolOutputs as Ze,summarizeConversation as me}from"./compaction.js";import{loadAcodeConfig as et,loadAcodeSkillGroup as tt}from"./config.js";import{ACodeHookBus as ot,findDeniedHookResult as X}from"./hooks/hookBus.js";import{isMcpPollingToolName as nt,getSemanticToolName as st}from"./mcpToolNames.js";function it(o){return o.type==="text"?{type:"text",text:o.text}:{type:"image_url",image_url:{url:o.image_url.url}}}function rt(o){return{id:o.id,type:"function",function:{name:o.function.name,arguments:o.function.arguments}}}function at(o){const e={role:o.role,content:Array.isArray(o.content)?o.content.map(it):o.content};return o.tool_call_id&&(e.tool_call_id=o.tool_call_id),o.tool_calls&&(e.tool_calls=o.tool_calls.map(rt)),e}function Z(o){return o.map(at)}function ut(){return new Date().toISOString()}function k(o){return!!(o&&typeof o=="object"&&!Array.isArray(o))}function de(o){return k(o)}function N(o){return String(o??"").trim()||void 0}const pe=6e4,he=10*6e4,fe=2,ee=-1,ct=1e3,G=6e4,ge=0,lt=2147483647,mt=new Set(["create_memory","read_memory","update_memory","delete_memory","search_memory","list_memories","get_memory_stats","list_memories_by_file","memory_recurrence","memory_recurrence_complete"]),dt=new Set(["write_file","edit_file","delete_file"]),pt=10*6e4+2e3,ht=1e4,ye=5e3,Ce=10*60*1e3,Ae=10*1024*1024,Te=800*1024,Q=process.env.ACODE_COMPACTION_DEBUG==="1",_e=Q?K.join(Oe.tmpdir(),"acode-compaction-debug.log"):"";function c(...o){Q&&console.log(...o)}const ft=64e3;function te(o){const e=o.contextLimit&&o.contextLimit>0?o.contextLimit:ft,t=o.maxOutput&&o.maxOutput>0?o.maxOutput:8192,n=o.inputLimit&&o.inputLimit>0?o.inputLimit:void 0,i=Ve(e,t,n);return{contextLimit:e,maxOutput:t,inputLimit:n,threshold:i}}async function v(o,e,t,n){if(e<=0)return await o;let i;try{return await new Promise((a,s)=>{i=setTimeout(()=>{n?.(),s(new Error(`${t} timed out after ${e}ms`))},e),o.then(a,s)})}finally{i&&clearTimeout(i)}}async function oe(o,e,t,n=ht){try{await v(e,n,o)}catch(i){t(i instanceof Error?i.message:String(i))}}function q(o,e){if(o===0||o==="0")return 0;const t=typeof o=="number"?o:Number(o);return t===ee?ee:Number.isFinite(t)&&t>0?Math.floor(t):e}function ne(o,e){const t=q(o,e);return t>0?t:e}const we=3;function be(o){return o?Math.ceil(o.length/we):0}function gt(o,e){let t=o.length;for(const n of e){const i=(n.kind==="mcp",n.toolName);if(t+=i.length,n.kind==="mcp")n.description&&(t+=n.description.length),t+=JSON.stringify(n.inputSchema).length;else{const a=n;a.inputSchema&&(t+=JSON.stringify(a.inputSchema).length)}}return Math.ceil(t/we)}function $e(o){if(o.length===0)return 0;const e=JSON.stringify(Ne(o));return be(e)}async function yt(o){await new Promise(e=>setTimeout(e,o))}function Ct(o){return typeof o=="boolean"}function At(o,e,t){if(Ct(t?.supportsImageInput))return t.supportsImageInput;const n=k(e)?e.models:void 0,i=k(n)?n[o]:void 0;if(!k(i))return!1;const a=i.modalities;return!k(a)||!Array.isArray(a.input)?!1:a.input.some(s=>String(s||"").trim().toLowerCase()==="image")}function se(o,e,t,n){if(o!==void 0)return o;if(!e)return;const i=e.models;if(k(i)){const a=i[t];if(k(a)){const s=a[n];if(s!==void 0)return s}}return e[n]}function Tt(o,e){const t=e.provider?.default,n=t?.options,i=o.model||t?.model||e.model||"acode-default",a=N(o.effort)||N(n?.effort)||N(t?.effort)||void 0;return{model:i,baseURL:N(n?.baseURL||n?.baseUrl),apiKey:N(n?.apiKey),effort:a,systemPrompt:o.systemPrompt||e.systemPrompt,stream:n?.stream!==!1,requestTimeoutMs:ne(n?.requestTimeoutMs??n?.timeoutMs,pe),requestTotalTimeoutMs:ne(n?.requestTotalTimeoutMs,he),requestRetryCount:q(n?.requestRetryCount,fe),mcpToolTimeoutMs:ne(n?.mcpToolTimeoutMs,G),mcpPollingToolTimeoutMs:q(n?.mcpPollingToolTimeoutMs,ge),supportsImageInput:At(i,t,n),contextLimit:q(se(n?.contextLimit,t,i,"contextLimit"),0)||void 0,inputLimit:q(se(n?.inputLimit,t,i,"inputLimit"),0)||void 0,maxOutput:q(se(n?.maxOutput,t,i,"maxOutput"),0)||void 0}}function O(o,e){const t=e.replace(/\s+/g," ").trim();t&&o.push({type:"text",text:t})}function _t(o,e){const t=e.trim();return/^data:image\/[a-z0-9.+-]+;base64,/i.test(t)?t:`data:${o||"image/png"};base64,${t}`}function wt(o){return/^data:image\/[a-z0-9.+-]+;base64,/i.test(o.trim())}function bt(o){let e;try{e=new URL(o)}catch{throw new Error(`ACode image URL is invalid: ${o}`)}if(e.protocol!=="http:"&&e.protocol!=="https:")throw new Error(`ACode image URL protocol is not supported: ${e.protocol}`)}async function $t(o){const e=await o.arrayBuffer();if(e.byteLength>Ae)throw new Error(`ACode image is too large to inline: ${e.byteLength} bytes exceeds ${Ae} bytes`);return Buffer.from(e).toString("base64")}async function Rt(o){const e=o.trim();if(wt(e))return e;bt(e);const t=await fetch(e);if(!t.ok)throw new Error(`ACode image fetch failed: ${t.status} ${await t.text()}`);const n=t.headers.get("content-type")||"",i=n.split(";")[0]?.trim().toLowerCase()||"";if(!i.startsWith("image/"))throw new Error(`ACode image URL did not return an image content type: ${n||"unknown"}`);return`data:${i};base64,${await $t(t)}`}function kt(o){const e=N(o.base64Data??o.base64??o.data);return e?_t(N(o.mediaType??o.mimeType)||"image/png",e):N(o.url??o.serverUrl)}function Re(o,e,t,n){return`[Attached file: ${[o?`name=${o}`:void 0,e?`url=${e}`:void 0,t?`mediaType=${t}`:void 0,n?`size=${n}`:void 0].filter(a=>!!a).join(", ")}]`}function Mt(o){const e=/\[(图片|文件)\s*:\s*([^\]\n]*?)\s*\]/gi,t=[];let n=!1,i=0,a=e.exec(o);for(;a!==null;){O(t,o.slice(i,a.index));const s=a[1],r=(a[2]||"").trim();if(s==="\u56FE\u7247"&&r)t.push({type:"image_url",image_url:{url:r}}),n=!0;else if(s==="\u6587\u4EF6"&&r){const[u="",m="",l="",f=""]=r.split("|").map(y=>y.trim());O(t,Re(u,m,l,f))}else O(t,a[0]);i=a.index+a[0].length,a=e.exec(o)}return t.length===0?{content:o,hasImages:!1}:(O(t,o.slice(i)),{content:t,hasImages:n})}function St(o,e){let t=!1;const n=[e.directMessages,e.groupMessages];for(const i of n)if(Array.isArray(i))for(const a of i){if(!k(a))continue;const s=N(a.content);if(!s)continue;const r=N(a.senderName);O(o,r?`Message from ${r}:`:"Message:");const u=ie(s);Array.isArray(u.content)?o.push(...u.content):O(o,u.content),t=t||u.hasImages}return t}function vt(o){let e;try{e=JSON.parse(o)}catch{return}if(!k(e))return;const t=[];let n=St(t,e);const i=N(e.text)||"",a=Array.isArray(e.attachments)?e.attachments:[];O(t,i);for(const s of a){if(!k(s))continue;const r=s;if(String(r.type??r.kind??"").toLowerCase()==="image"){const l=kt(r);l&&(t.push({type:"image_url",image_url:{url:l}}),n=!0);continue}const m=N(r.url??r.serverUrl)||"";O(t,Re(N(r.name)||"",m,N(r.mediaType??r.mimeType)||"",N(r.size)||""))}return t.length===0?{content:o,hasImages:!1}:{content:t,hasImages:n}}function ie(o){return vt(o)??Mt(o)}async function ke(o){if(!Array.isArray(o))return o;const e=[];for(const t of o){if(t.type==="text"){e.push(t);continue}e.push({type:"image_url",image_url:{url:await Rt(t.image_url.url)}})}return e}function xt(o){return typeof o=="string"?o:Array.isArray(o)?o.map(e=>e.type==="text"?e.text:"[Image]").join(" ").trim():""}function Et(o,e){const t=o.permissions??e.permissions??{},n=o.permissions?[]:e.allowedTools??[],i=o.permissions?[]:e.deniedTools??[];return{mode:t.mode??"default",allow:[...t.allow??[],...n].map(a=>a.trim()).filter(Boolean),deny:[...t.deny??[],...i].map(a=>a.trim()).filter(Boolean)}}function Nt(o,e){return o==="*"?!0:o.endsWith("*")?e.startsWith(o.slice(0,-1)):o===e}function It(o){return o.kind==="builtin"?[o.exposedName,o.toolName,`builtin.${o.toolName}`]:[o.exposedName,o.toolName,`${o.serverName}.${o.toolName}`,`mcp.${o.serverName}.${o.toolName}`]}function Me(o,e){return It(e).some(t=>Nt(o,t))}function Se(o,e,t){if(e.length>0&&!e.some(n=>Me(n,o)))throw new Error(t)}function re(o,e,t){if(e.mode!=="bypassPermissions"){if(e.deny.some(n=>Me(n,o)))throw new Error(`ACode tool denied by permission rule: ${o.exposedName}`);Se(o,e.allow,`ACode tool is not in allowed permission rules: ${o.exposedName}`),Se(o,t??[],`ACode slash command does not allow tool: ${o.exposedName}`)}}function Lt(o,e,t){try{return re(o,e,t),!0}catch{return!1}}function Y(o){const e=o.replace(/\/+$/g,"");return/\/chat\/completions$/i.test(e)?e:`${e}/chat/completions`}function Pt(o){try{const e=new URL(o);e.search="",e.hash="";const t=new URL(Y(e.toString()));return`${t.origin}${t.pathname}`}catch{return Y(o).replace(/([?&](?:api[_-]?key|token|authorization)=)[^&]+/gi,"$1[redacted]")}}function Dt(o){const e=[...o].reverse().find(n=>n.role==="user"),t=xt(e?.content??null).trim();if(t)return t.length>300?`${t.slice(0,300)}...`:t}function ve(o){try{const e=JSON.parse(o);return de(e)?e:{}}catch{return{}}}function Ot(o,e,t){if(new Set([je,"await_sub_agents"]).has(o.toolName))return 0;if(o.toolName!=="Bash")return t;const i=e.timeout_ms??e.timeoutMs,a=typeof i=="number"?i:typeof i=="string"?Number(i):NaN;return!Number.isFinite(a)||a<=0?Math.max(t,pt):Math.min(Math.floor(a),10*6e4)+2e3}function Ut(o,e){const t=o.trim();return`ACode \u5DF2\u63A5\u6536\u4EFB\u52A1\u3002\u5F53\u524D\u5DF2\u52A0\u8F7D ${e.configMcpCount} \u4E2A MCP \u914D\u7F6E\u3001${e.exposedTools.length} \u4E2A MCP Tool \u4E0E ${e.skills.length} \u4E2A Skill\u3002${t?`\u8F93\u5165\u6458\u8981\uFF1A${t.slice(0,200)}`:""}`}function P(o){if(!k(o))return String(o??"");const e=o.content;if(Array.isArray(e)){const t=e.map(n=>k(n)?typeof n.text=="string"?n.text:JSON.stringify(n):"").filter(Boolean);if(t.length>0)return t.join(`
2
+ `)}return JSON.stringify(o)}function W(o){return o instanceof Error?o.message:String(o)}function qt(o){let e=16;if(typeof o.content=="string")e+=o.content.length*2;else if(Array.isArray(o.content))for(const t of o.content)typeof t=="object"&&t!==null&&(e+=JSON.stringify(t).length);return o.tool_calls&&(e+=JSON.stringify(o.tool_calls).length),o.tool_call_id&&(e+=o.tool_call_id.length),e}async function Bt(o,e,t,n,i){let a=0;for(const r of o)a+=qt(r);if(a<=e)return 0;const s=await le(o,t,n,{llmSummarizer:i});return s.strategy!=="none"?s.prunedToolMessages+s.summarizedMessages:me(o,{llmSummarizer:i,contextLimit:t})}function xe(o){const e=W(o).toLowerCase();return e.includes("context_length_exceeded")||e.includes("context length")||e.includes("maximum context")||e.includes("max context")||e.includes("token limit")||e.includes("tokens exceed")||e.includes("too many tokens")||e.includes("input is too long")||e.includes("prompt is too long")||e.includes("request too large")||e.includes("413")}function zt(o){const e=W(o).toLowerCase();return xe(o)?!1:e.includes("fetch failed")||e.includes("timed out")||e.includes("econnreset")||e.includes("econnrefused")||e.includes("etimedout")||e.includes("enotfound")||e.includes("502")||e.includes("503")||e.includes("504")}function Ft(o){if(typeof o!="object"||o===null)return!1;const e=Reflect.get(o,"code"),t=W(o).toLowerCase();return e===-32001||t.includes("request timed out")}function jt(o){if(typeof o=="object"&&o!==null){const e=Reflect.get(o,"code");if(typeof e=="string"&&e)return e;const t=Reflect.get(o,"name");if(typeof t=="string"&&t)return t}return"tool_error"}function Wt(o,e){return JSON.stringify({ok:!1,tool:o.exposedName,error:{type:jt(e),message:W(e)}})}function Ee(o){const e=JSON.stringify(o);return e.length>500?`${e.slice(0,500)}\u2026`:e}function Jt(o){const e=[o?.reasoning_content,o?.reasoning_text,o?.reasoning];for(const t of e){if(typeof t=="string"&&t)return t;if(k(t)){const n=t.text??t.content;if(typeof n=="string"&&n)return n}}return""}function Kt(o){return o.length===0?"":["Skills are available for this session.","Use the `skill` tool to load specialized skill instructions when a user task matches one of the available skills listed in the tool description.","Do not attempt to follow skill instructions from memory \u2014 always invoke the `skill` tool first to load the full content."].join(" ")}function Ht(o){return o.length===0?"":["Available slash commands:",...o.map(e=>`- /${e.name}${e.description?`: ${e.description}`:""}`),"When the user invokes a slash command, ACode expands it before model execution."].join(`
3
+ `)}function Gt(o){if(!o||typeof o!="object")return"";const e=o,t=[],n=e.displayName,i=e.roleName,a=e.instanceName,s=e.projectName,r=e.workspacePath,u=e.runtimeMode,m=e.describe;return typeof n=="string"&&n.trim()&&t.push(`Display Name: ${n.trim()}`),typeof i=="string"&&i.trim()&&t.push(`Role: ${i.trim()}`),typeof a=="string"&&a.trim()&&t.push(`Instance: ${a.trim()}`),typeof s=="string"&&s.trim()&&t.push(`Project: ${s.trim()}`),typeof r=="string"&&r.trim()&&t.push(`Workspace: ${r.trim()}`),typeof u=="string"&&u.trim()&&t.push(`Runtime Mode: ${u.trim()}`),typeof m=="string"&&m.trim()&&t.push(`Description: ${m.trim()}`),t.length===0?"":["[User Profile]",...t].join(`
4
+ `)}function Ne(o){return o.map(e=>({type:"function",function:{name:e.exposedName,description:e.description||e.toolName,parameters:e.inputSchema}}))}function Qt(o,e){return`${o}__${e}`.replace(/[^a-zA-Z0-9_-]/g,"_").slice(0,64)||"mcp_tool"}function Yt(o){const e={"Content-Type":"application/json"};return o.apiKey&&(e.Authorization=`Bearer ${o.apiKey}`),e}function Vt(o){return Object.fromEntries(Object.entries(o).map(([e,t])=>[e,Xt(e)?Zt(t):t]))}function Xt(o){const e=o.trim().toLowerCase();return e==="authorization"||e==="api-key"||e==="x-api-key"||e==="openai-api-key"}function Zt(o){return o.trim().toLowerCase().startsWith("bearer ")?"Bearer ***":"***"}function eo(o){return o.split(/\r?\n/).filter(e=>e.startsWith("data:")).map(e=>e.slice(5).trim()).filter(Boolean)}function to(o,e){const t=Number.isFinite(e.index)?Number(e.index):0,n=o.get(t)??{id:e.id||`call-${t}`,type:"function",function:{name:"",arguments:""}};e.id&&(n.id=e.id),e.function?.name&&(n.function.name+=e.function.name),e.function?.arguments&&(n.function.arguments+=e.function.arguments),o.set(t,n)}function oo(o){return[...o.entries()].sort(([e],[t])=>e-t).map(([,e])=>e).filter(e=>e.function.name.trim().length>0)}class B extends De{constructor(e){super(),this.config=e,this.initialConversation=[],this.state={started:!1,terminated:!1,configMcpCount:0,skills:[],provider:{model:"acode-default",stream:!1,requestTimeoutMs:pe,requestTotalTimeoutMs:he,requestRetryCount:fe,mcpToolTimeoutMs:G,mcpPollingToolTimeoutMs:ge,supportsImageInput:!1},permissions:{mode:"default",allow:[],deny:[]},mcpConnections:[],ownsMcpConnections:!0,exposedTools:[],commands:[],commandRegistry:new ce([]),conversation:[],accumulatedInputTokens:0,accumulatedOutputTokens:0},this.abortController=null,this.pendingMemorySubAgents=new Map,this.memoryDelegateCooldownUntil=new Map,this.submitQueue=Promise.resolve(),this.activeTurn=!1,this.abortRequested=!1,this.pendingAwaitedSubIds=new Set,this.sessionCompleteEmitted=!1,this.contextVersion=0,this.stopWaiters=new Set,this.compactionDebugTimer=null,this.skillTool=null,this.skillWatchers=[],this.skillRefreshTimer=null,this.questionWaiters=new Map,this.hookBus=new ot({sessionId:e.sessionId,onError:(t,n)=>{this.emitAcodeEvent("error",{message:`ACode hook failed: ${t.name}: ${n instanceof Error?n.message:String(n)}`})}});for(const t of e.hooks??[])this.hookBus.register(t)}getAvailableTools(){return this.state.exposedTools.map(e=>e.exposedName)}emitAcodeEvent(e,t){const n={type:e,sessionId:this.config.sessionId,timestamp:ut(),data:t};this.emit("event",n)}emitCompactionPhase(e,t,n,i){this.emitAcodeEvent("context_compaction",{action:"phase",compactionId:e,parentId:e,phase:t,phaseLabel:n,phaseDetail:i})}handleCompactionPhase(e,t){switch(t.phase){case"prune_start":this.emitCompactionPhase(e,"prune","\u9636\u6BB51: \u88C1\u526A\u5DE5\u5177\u8F93\u51FA",`\u6B63\u5728\u88C1\u526A ${t.messageCount??0} \u6761\u6D88\u606F\u4E2D\u7684\u5DE5\u5177\u8F93\u51FA...`);break;case"prune_end":this.emitCompactionPhase(e,"prune_done","\u9636\u6BB51: \u88C1\u526A\u5DE5\u5177\u8F93\u51FA",`\u88C1\u526A\u4E86 ${t.prunedCount??0} \u6761\u5DE5\u5177\u8F93\u51FA, \u91CA\u653E ${t.freedTokens??0} tokens`);break;case"summarize_start":this.emitCompactionPhase(e,"summarize","\u9636\u6BB52: \u6458\u8981\u5316\u5BF9\u8BDD",`\u6B63\u5728\u6458\u8981 ${t.messageCount??0} \u6761\u65E9\u671F\u5BF9\u8BDD...`);break;case"summarize_end":this.emitCompactionPhase(e,"summarize_done","\u9636\u6BB52: \u6458\u8981\u5316\u5BF9\u8BDD",`\u6458\u8981\u5316\u4E86 ${t.summarizedCount??0} \u6761\u6D88\u606F${t.usedLLM?" (LLM \u6458\u8981)":" (\u89C4\u5219\u6458\u8981)"}`);break}}async start(){this.emitAcodeEvent("startup_phase",{phase:"loading_config"});const e=await et(this.config.configPath);this.emitAcodeEvent("startup_phase",{phase:"loading_skills"});const{skills:t,skillGroups:n,watchedDirs:i}=await this.loadAllSkills(),a=Tt(this.config,e);c(`[ACodeRuntime] \u2605\u2605\u2605 DEBUG \u2605\u2605\u2605 resolveProviderConfig \u7ED3\u679C: config.model=${this.config.model??"(undefined)"}, resolvedModel=${a.model}, baseURL=${a.baseURL??"(none)"}, apiKey=${a.apiKey?"[set]":"(none)"}, configPath=${this.config.configPath??"(none)"}`);const s=Et(this.config,e),r=this.config.sharedMcpConnections?this.filterSharedMcpConnectionsForSubAgent(this.config.sharedMcpConnections):await this.connectMcpServers(e.mcpServers??{},a.mcpToolTimeoutMs),u=!this.config.sharedMcpConnections;this.emitAcodeEvent("startup_phase",{phase:"fetching_profile"});const m=await this.fetchProfilePrompt(r),l=this.config.agentId||"unknown";Qe(l);const f=Fe(this.config.workingDirectory,l,this.config.onFileChange),y=We(this.buildAskUserQuestionDeps(),()=>this.contextVersion),w=Je(t);this.skillTool=w;const g=Ke(l),b=He(l),h=this.config.subAgentManager?Ge(this.buildSubAgentToolDeps(this.config.subAgentManager)):[],d=[...f,y,w,g,b,...h,...r.flatMap(p=>p.tools)],_=this.config.subagentType==="explore"?d.filter(p=>!dt.has(p.exposedName)):d,M=this.resolveCommandDirs(e);this.emitAcodeEvent("startup_phase",{phase:"loading_commands"});const $=await Ye(M),A=new ce($),S=Kt(t),I=Ht($),C=[a.systemPrompt,S,I].map(p=>p?.trim()).filter(p=>!!p).join(`
3131
5
 
3132
- 当前有以下 ${delegate.uris.length} 条即将遗忘的记忆需要整理:
3133
- ${urisText}
6
+ `),T=p=>p?.trim()?[{role:"system",content:p.trim()}]:[],x=[this.config.rolePrompt,m].map(p=>p?.trim()).filter(p=>!!p).join(`
3134
7
 
3135
- 请执行以下步骤(核心逻辑:处理 → 验证 → 未完成则继续):
8
+ `),R=[...T(this.config.globalPrompt),...T(x),...T(this.config.memory),...C?[{role:"system",content:C}]:[]],E=R.map(p=>p.content).filter(Boolean).join(`
3136
9
 
3137
- === 处理阶段 ===
3138
- 1. 调用 memory_recurrence(limit=10) 分批获取这些过期记忆的完整内容(最早过期优先)
3139
- 2. 逐条分析:这些内容是否仍然有价值?是否需要保留、更新、归档或删除?
3140
- 3. 对于需要保留的,调用 update_memory 续期;对于无价值的,调用 memory_recurrence_complete 将其清理到永久记忆或删除
10
+ `);this.initialConversation=Z(R),this.state={started:!0,terminated:!1,configMcpCount:Object.keys(e.mcpServers??{}).length,skills:t,provider:a,permissions:s,mcpConnections:r,ownsMcpConnections:u,exposedTools:_,commands:$,commandRegistry:A,conversation:Z(R),accumulatedInputTokens:0,accumulatedOutputTokens:0},this.emitAcodeEvent("startup_phase",{phase:"session_start_hook"}),await this.hookBus.dispatch("SessionStart",{workingDirectory:this.config.workingDirectory,model:a.model,commandCount:$.length});const L=te(a);this.emitAcodeEvent("session_started",{workingDirectory:this.config.workingDirectory,model:a.model,commandCount:$.length,estimatedContextTokens:gt(E,_),contextLimit:L.contextLimit,maxOutput:L.maxOutput,...L.inputLimit!==void 0?{inputLimit:L.inputLimit}:{},compactionThreshold:L.threshold}),this.emitAcodeEvent("mcp_status",{configPath:this.config.configPath,installedCount:this.state.configMcpCount,servers:Object.keys(e.mcpServers??{}),connectedCount:r.length,builtInToolCount:f.length,toolCount:_.length,permissionMode:s.mode,allowedToolRules:s.allow,deniedToolRules:s.deny,tools:_.map(p=>({name:p.exposedName,source:p.kind==="mcp"?`${p.serverName}.${p.toolName}`:`builtin.${p.toolName}`,description:p.description||""}))}),this.emitAcodeEvent("skill_status",{skillsDir:this.config.skillsDir,installedCount:t.length,skills:t.map(p=>({name:p.name,description:p.description,directory:p.directory})),skillGroups:n.map(p=>({dir:p.dir,exists:p.exists,skills:p.skills.map(D=>({name:D.name,description:D.description,directory:D.directory}))}))}),this.emitAcodeEvent("command_status",{action:"commands_loaded",installedCommandCount:$.length,commandDirs:M,commands:$.map(p=>({name:p.name,description:p.description}))}),this.config.initialPrompt&&await this.submit(this.config.initialPrompt),this.startCompactionDebugLog(),this.startSkillWatchers(i)}async loadAllSkills(){const e=this.config.skillsDir??"",t=K.join(this.config.workingDirectory,".acode","skills"),n=K.join(this.config.workingDirectory,".agentswork","skills"),i=new Set,a=[],s=[];for(const m of[e,t,n]){if(!m)continue;const l=K.resolve(m);if(!i.has(l)){i.add(l),a.push(await tt(m));try{await F.promises.access(l),s.push(l)}catch{}}}const r=new Map;for(const m of a)for(const l of m.skills)r.set(l.name,l);return{skills:[...r.values()],skillGroups:a,watchedDirs:s}}startSkillWatchers(e){for(const t of e){let n=null;try{n=F.watch(t,{recursive:!0},()=>{this.scheduleSkillRefresh()})}catch{try{n=F.watch(t,()=>{this.scheduleSkillRefresh()})}catch{continue}}n&&(n.on("error",()=>{}),this.skillWatchers.push(n))}}scheduleSkillRefresh(){this.skillRefreshTimer&&clearTimeout(this.skillRefreshTimer),this.skillRefreshTimer=setTimeout(()=>{this.skillRefreshTimer=null,this.refreshSkills()},B.SKILL_REFRESH_DEBOUNCE_MS)}async refreshSkills(){if(!(!this.state.started||this.state.terminated))try{const{skills:e,skillGroups:t}=await this.loadAllSkills();this.skillTool&&this.skillTool.refresh(e),this.state.skills=e,this.emitAcodeEvent("skill_status",{skillsDir:this.config.skillsDir,installedCount:e.length,skills:e.map(n=>({name:n.name,description:n.description,directory:n.directory})),skillGroups:t.map(n=>({dir:n.dir,exists:n.exists,skills:n.skills.map(i=>({name:i.name,description:i.description,directory:i.directory}))}))}),c(`[skill-watch] Skills refreshed: ${e.length} skills (${e.map(n=>n.name).join(", ")||"none"})`,{sessionId:this.config.sessionId})}catch(e){console.error("[skill-watch] Failed to refresh skills:",e instanceof Error?e.message:e)}}stopSkillWatchers(){this.skillRefreshTimer&&(clearTimeout(this.skillRefreshTimer),this.skillRefreshTimer=null);for(const e of this.skillWatchers)try{e.close()}catch{}this.skillWatchers=[]}startCompactionDebugLog(){if(!Q||this.compactionDebugTimer)return;const e=this.state.provider,{contextLimit:t,maxOutput:n,inputLimit:i,threshold:a}=te(e);this.compactionDebugTimer=setInterval(()=>{try{const s=this.state.accumulatedInputTokens+this.state.accumulatedOutputTokens,r=H(this.state.conversation)+$e(this.state.exposedTools),u=Math.max(s,r),m=j(this.state.conversation),l=this.state.started&&!this.state.terminated,f=[`[${new Date().toISOString()}]`,`started=${this.state.started}`,`terminated=${this.state.terminated}`,`activeTurn=${this.activeTurn}`,`conversationSize=${this.state.conversation.length}msgs`,`sizeKB=${m}KB`,`estimation=${r} tokens`,`actualInput=${this.state.accumulatedInputTokens}`,`actualOutput=${this.state.accumulatedOutputTokens}`,`actualUsed=${s}`,`contextLimit=${t}`,`inputLimit=${i??"n/a"}`,`maxOutput=${n}`,`triggerThreshold=${a}`,`remainingToTrigger=${Math.max(0,a-s)}`,`isRunning=${l}`,`shouldCompact=${l?u>=a:!1}`,"---"].join(" | ");F.promises.appendFile(_e,f+`
11
+ `,"utf-8").catch(()=>{})}catch{}},5e3)}stopCompactionDebugLog(){this.compactionDebugTimer&&(clearInterval(this.compactionDebugTimer),this.compactionDebugTimer=null)}async submit(e){const t=this.contextVersion;c(`[ACodeRuntime] \u{1F4EC} submit() queued: contextVersion=${t}, activeTurn=${this.activeTurn}, abortRequested=${this.abortRequested}, terminated=${this.state.terminated}, message.length=${e.length}`);const n=this.submitQueue.then(()=>(c(`[ACodeRuntime] \u25B6\uFE0F executeSubmit dequeued: contextVersion=${t}`),this.executeSubmit(e,t)));return this.submitQueue=n.catch(()=>{c("[ACodeRuntime] \u{1F4EC} submit() queued item rejected (catch swallowed)")}),c("[ACodeRuntime] \u{1F4EC} submit() returning queued promise"),n}async resetContext(){if(!this.state.started||this.state.terminated)throw new Error("ACode runtime is not active");this.contextVersion+=1,this.abortRequested=this.activeTurn,this.abortController?.abort(),this.abortController=null,this.state.conversation=Z(this.initialConversation),this.state.accumulatedInputTokens=0,this.state.accumulatedOutputTokens=0}isTurnSuperseded(e){return e!==this.contextVersion}async executeSubmit(e,t){const n=Date.now();if(c("[ACodeRuntime] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"),c("[ACodeRuntime] \u25B6\uFE0F executeSubmit STARTED"),c(`[ACodeRuntime] Message: <redacted, length=${e.length}>`),c(`[ACodeRuntime] \u{1F522} Context version: ${t}`),c("[ACodeRuntime] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"),!this.state.started||this.state.terminated)throw console.error(`[ACodeRuntime] \u274C Runtime not active (started: ${this.state.started}, terminated: ${this.state.terminated})`),new Error("ACode runtime is not active");this.activeTurn=!0,this.abortRequested=!1;const i=e.trim();let a=!1;try{c("[ACodeRuntime] \u{1F50D} Dispatching UserPromptSubmit hook...");const s=Date.now(),r=await this.hookBus.dispatch("UserPromptSubmit",{prompt:i}),u=Date.now()-s;if(c(`[ACodeRuntime] \u2705 UserPromptSubmit hook completed in ${u}ms, results: ${r.length}`),this.isTurnSuperseded(t)){c("[ACodeRuntime] \u26A0\uFE0F Turn superseded after hook, aborting");return}const m=X(r);if(m){console.error(`[ACodeRuntime] \u274C Prompt denied by hook: ${m.message}`),this.emitAcodeEvent("error",{message:m.message||"ACode prompt was denied by hook"}),this.emitAcodeEvent("turn_complete",{failed:!0}),a=!0,c("[ACodeRuntime] \u{1F4E4} Emitted turn_complete (failed=true) due to hook deny");return}c("[ACodeRuntime] \u{1F50D} Expanding command if any...");const l=this.state.commandRegistry.expand(i),f=l?.prompt.trim()||i,y=l?.command.allowedTools;if(l&&(c(`[ACodeRuntime] \u{1F4CB} Slash command detected: ${l.command.name}`),this.emitAcodeEvent("command_status",{action:"slash_command",command:l.command.name,arguments:l.argumentsText,sourcePath:l.command.sourcePath})),!this.state.provider.baseURL||!this.state.provider.apiKey){c("[ACodeRuntime] \u26A0\uFE0F No provider configured, using fallback response");const M=Ut(f,this.state);this.emitAcodeEvent("assistant_delta",{text:M}),this.emitAcodeEvent("turn_complete",{usage:{inputTokens:f.length,outputTokens:M.length}}),a=!0,c("[ACodeRuntime] \u{1F4E4} Emitted turn_complete (fallback mode)");return}c("[ACodeRuntime] \u{1F4CE} Parsing attachment message...");const w=ie(f);if(w.hasImages&&!this.state.provider.supportsImageInput)throw console.error("[ACodeRuntime] \u274C Image input not supported by current provider"),new Error("ACode provider/model is not configured for image input. Enable provider.default.models[model].modalities.input including 'image' or options.supportsImageInput=true.");c("[ACodeRuntime] \u{1F5BC}\uFE0F Materializing image content if any...");const g=await ke(w.content);if(this.isTurnSuperseded(t)){c("[ACodeRuntime] \uFE0F Turn superseded after materializing content, aborting");return}c("[ACodeRuntime] \u{1F4AC} Adding user message to conversation"),this.state.conversation.push({role:"user",content:g}),c(`[ACodeRuntime] Conversation size: ${this.state.conversation.length} messages`),c("[ACodeRuntime] Starting runModelTurn...");const b=Date.now(),h=await this.runModelTurn(y,t),d=Date.now()-b;if(c(`[ACodeRuntime] \u2705 runModelTurn completed in ${d}ms`),c(`[ACodeRuntime] \u{1F4DD} Result: text=${h.text?h.text.slice(0,80)+(h.text.length>80?"...":""):"(empty)"}, finishReason=${h.finishReason||"(none)"}`),this.isTurnSuperseded(t)){c("[ACodeRuntime] \uFE0F Turn superseded after runModelTurn, aborting");return}h.text&&!this.state.provider.stream&&(c("[ACodeRuntime] \u{1F4E4} Emitting assistant_delta (non-streaming mode)"),this.emitAcodeEvent("assistant_delta",{text:h.text})),c("[ACodeRuntime] \u{1F4E4} Emitting turn_complete..."),this.emitAcodeEvent("turn_complete",{usage:h.usage??{inputTokens:f.length,outputTokens:h.totalOutputTextLength??h.text.length},...h.responseBody?{responseBody:h.responseBody}:{},...h.finishReason?{finishReason:h.finishReason}:{}}),c(`[ACodeRuntime] \u{1F4B0} Current context usage: input=${this.state.accumulatedInputTokens}, output=${this.state.accumulatedOutputTokens}`+(h.usage?"":" (estimated, no provider usage)")),a=!0;const _=Date.now()-n;c("[ACodeRuntime] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"),c(`[ACodeRuntime] \u2705 executeSubmit COMPLETED in ${_}ms`),c("[ACodeRuntime] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550")}catch(s){const r=Date.now()-n;if(console.error(`[ACodeRuntime] \u274C executeSubmit FAILED after ${r}ms:`,s instanceof Error?s.message:String(s)),console.error("[ACodeRuntime] \u{1F50D} Error stack:",s instanceof Error?s.stack:"(no stack)"),this.isTurnSuperseded(t)){c("[ACodeRuntime] \uFE0F Turn superseded in error handler, skipping recovery");return}if(this.abortRequested){c("[ACodeRuntime] \u26A0\uFE0F Abort requested, emitting aborted turn_complete"),this.emitAcodeEvent("turn_complete",{aborted:!0}),a=!0;return}c("[ACodeRuntime] Emitting error and failed turn_complete..."),this.emitAcodeEvent("error",{message:s instanceof Error?s.message:String(s)}),this.emitAcodeEvent("turn_complete",{failed:!0}),a=!0}finally{const s=Date.now()-n;c(`[ACodeRuntime] \u{1F51A} executeSubmit finally block entered after ${s}ms`),c(`[ACodeRuntime] \u{1F50D} turnCompleted=${a}, activeTurn=${this.activeTurn}, abortRequested=${this.abortRequested}, contextVersion=${t}, currentContextVersion=${this.contextVersion}`),this.activeTurn=!1,this.abortRequested=!1,!a&&!this.isTurnSuperseded(t)&&(console.error(`[ACodeRuntime] \u{1F6A8} CRITICAL: executeSubmit finally block reached without turn_complete event! Duration: ${s}ms. This indicates a code path that should have emitted turn_complete but didn't. Forcing failed turn_complete to prevent state stuck.`),this.emitAcodeEvent("error",{message:"ACode turn completed without proper completion event (internal error)"}),this.emitAcodeEvent("turn_complete",{failed:!0}),c("[ACodeRuntime] Emergency turn_complete (failed=true) emitted"))}}async abort(){const e=this.abortController!==null,t=this.activeTurn;this.abortRequested=this.activeTurn,this.abortController?(c(`[ACodeRuntime] \u26D4 abort() calling abortController.abort() (activeTurn=${t})`),this.abortController.abort()):console.warn(`[ACodeRuntime] \u26D4 abort() called but abortController is null! activeTurn=${t}, abortRequested will be set to ${t}. This means abort() CANNOT cancel any running fetch/stream \u2014 if a built-in tool is executing, it will NOT be interrupted.`),this.abortController=null,this.rejectAllQuestionWaiters("aborted"),this.cancelPendingAwaitedSubAgents(),this.notifyRuntimeStop()}buildAskUserQuestionDeps(){return{onQuestion:(e,t)=>{this.emitAcodeEvent("question_request",{toolCallId:e,questions:t})},waitForAnswer:(e,t)=>new Promise((n,i)=>{this.questionWaiters.set(e,{resolve:n,reject:i,contextVersion:t})})}}buildSubAgentToolDeps(e){const t=this.config.agentId??"unknown",n=this.config.sessionId,i=this.config.workingDirectory,a=this.config;return{spawnSubAgent:async s=>{const r=await e.spawn({parentAgentId:t,parentSessionId:n,subagentType:s.subagentType,description:s.description,prompt:s.prompt,workingDirectory:i,parentConfig:{...a,sharedMcpConnections:this.state?.mcpConnections}});return await e.start(r),{subId:r}},listSubAgents:async()=>{const s=e.listByParent(t),r=[],u=[];for(const m of s){const l={subId:m.subId,subagentType:m.subagentType,description:m.description,status:m.status,startedAt:m.startedAt,endedAt:m.endedAt,durationMs:m.durationMs};m.status==="pending"||m.status==="running"?r.push(l):u.push(l)}return u.sort((m,l)=>(l.endedAt??0)-(m.endedAt??0)),{running:r,history:u}},getSubAgentStatus:async s=>{const r=e.getSubAgent(s);return r?{subId:r.subId,subagentType:r.subagentType,description:r.description,status:r.status,startedAt:r.startedAt,endedAt:r.endedAt,durationMs:r.durationMs,prompt:r.prompt,result:r.result,errorMessage:r.errorMessage,contextSnapshot:r.contextSnapshot}:null},awaitSubAgents:async(s,r)=>{const u=new Set;for(const m of s)this.pendingAwaitedSubIds.add(m),u.add(m);try{return await e.awaitCompletion(s,r)}finally{for(const m of u)this.pendingAwaitedSubIds.delete(m)}},abortSubAgent:s=>e.abort(s)}}submitQuestionAnswer(e,t){const n=this.questionWaiters.get(e);return n?(this.questionWaiters.delete(e),this.isTurnSuperseded(n.contextVersion)?(n.reject(new Error("turn superseded before user answered")),!1):(n.resolve(t),!0)):!1}rejectAllQuestionWaiters(e){if(this.questionWaiters.size!==0){for(const[,t]of this.questionWaiters)t.reject(new Error(`ask_user_question ${e}`));this.questionWaiters.clear()}}cancelPendingAwaitedSubAgents(){if(this.pendingAwaitedSubIds.size===0)return;const e=this.config.subAgentManager;if(!e)return;const t=Array.from(this.pendingAwaitedSubIds);c(`[ACodeRuntime] \u26D4 Cancelling ${t.length} pending awaited sub-agent(s): ${t.join(", ")}`);for(const n of t)e.abort(n).catch(i=>{c(`[ACodeRuntime] \u26A0\uFE0F manager.abort(${n}) threw: ${i instanceof Error?i.message:String(i)}`)})}async stop(){if(!this.state.terminated&&(this.state.terminated=!0,this.notifyRuntimeStop(),this.abortRequested=this.activeTurn,this.abortController?.abort(),this.abortController=null,this.rejectAllQuestionWaiters("session terminated"),this.cancelPendingAwaitedSubAgents(),await oe("ACode active turn shutdown",this.submitQueue,e=>{this.emitAcodeEvent("error",{message:e})}),await this.disconnectMcpServers(),await oe("ACode Stop hooks",this.hookBus.dispatch("Stop",{reason:"session_stop"}),e=>{this.emitAcodeEvent("error",{message:e})}),this.state.terminated=!0,this.sessionCompleteEmitted||(this.sessionCompleteEmitted=!0,this.emitAcodeEvent("session_complete",{exitCode:0})),this.stopCompactionDebugLog(),this.stopSkillWatchers(),Q))try{F.promises.appendFile(_e,`[${new Date().toISOString()}] runtime stopped | ---
12
+ `,"utf-8").catch(()=>{})}catch{}}resolveCommandDirs(e){const t=[...this.config.commandDirs??[],...e.commandDirs??[],...e.commands?.directories??[]];return[...new Set(t.map(n=>n.trim()).filter(Boolean))]}async runModelTurn(e,t=this.contextVersion){let n="",i,a,s,r="",u=0;c(`[ACodeRuntime] \u25B6\uFE0F runModelTurn started (contextVersion: ${t})`);const m=50;for(let l=0;l<m;l+=1){if(c(`[ACodeRuntime] \u{1F504} Iteration ${l+1} starting...`),this.isTurnSuperseded(t))return c(`[ACodeRuntime] \u26A0\uFE0F Turn superseded at iteration ${l+1}, aborting`),{text:"",totalOutputTextLength:r.length};const f=Date.now(),y=await this.requestChatCompletion(e),w=Date.now()-f;if(c(`[ACodeRuntime] \u{1F916} AI request completed in ${w}ms, choices: ${y.choices?.length||0}`),this.isTurnSuperseded(t))return c(`[ACodeRuntime] \u26A0\uFE0F Turn superseded after AI response at iteration ${l+1}`),{text:"",totalOutputTextLength:r.length};if(this.state.terminated)throw c(`[ACodeRuntime] Runtime terminated at iteration ${l+1}`),new Error("ACode runtime was stopped during the active turn");const g=y.choices?.[0];a=JSON.stringify(y,null,2),s=typeof g?.finish_reason=="string"?g.finish_reason:void 0;const b=g?.message,h=typeof b?.content=="string"?b.content:"",d=Array.isArray(b?.tool_calls)?b.tool_calls:[];if(c(`[ACodeRuntime] AI response: content=${h?h.slice(0,100)+(h.length>100?"...":""):"(empty)"}, toolCalls=${d.length}, finishReason=${s||"(none)"}`),y.usage){const A=y.usage.prompt_tokens??y.usage.input_tokens,S=y.usage.completion_tokens??y.usage.output_tokens;typeof A=="number"&&typeof S=="number"&&(i={inputTokens:A,outputTokens:S},u+=A,this.state.accumulatedInputTokens=A,this.state.accumulatedOutputTokens=S,c(`[ACodeRuntime] \u{1F4B0} Token usage: input=${A}, output=${S} (turn accumulated cost: input=${u})`))}if(this.state.conversation.push({role:"assistant",content:h||null,tool_calls:d.length>0?d:void 0}),h&&(n=h,r+=h,c(`[ACodeRuntime] \u2728 Captured assistant text: ${h.slice(0,80)}...`)),d.length===0)return c(`[ACodeRuntime] \u2705 No tool calls at iteration ${l+1}, completing turn`),{text:n,usage:i,responseBody:a,finishReason:s,totalOutputTextLength:r.length};const _=d.map(A=>A.function.name).join(", ");c(`[ACodeRuntime] \u{1F527} Executing ${d.length} tool(s) at iteration ${l+1}: ${_}`);const M=Date.now();await this.handleToolCalls(d,e,t);const $=Date.now()-M;if(c(`[ACodeRuntime] \u2699\uFE0F Tool execution completed in ${$}ms at iteration ${l+1}`),this.isTurnSuperseded(t))return c(`[ACodeRuntime] \u26A0\uFE0F Turn superseded after tool execution at iteration ${l+1}`),{text:"",totalOutputTextLength:r.length};c(`[ACodeRuntime] \u{1F504} Preparing iteration ${l+2}...`)}return console.error(`[ACodeRuntime] \u{1F6A8} CRITICAL: Reached safety limit of ${m} iterations! This indicates a potential infinite loop. Forcing turn completion.`),{text:n||`ACode \u5DF2\u8FBE\u5230\u5B89\u5168\u8FED\u4EE3\u4E0A\u9650\uFF08${m}\u6B21\uFF09\uFF0C\u5F3A\u5236\u7ED3\u675F\u672C\u8F6E\u5BF9\u8BDD\u3002`,usage:i,responseBody:a,finishReason:s||"safety_iteration_limit_reached",totalOutputTextLength:r.length}}async summarizeConversationWithLLM(e){const t=this.state.provider,n=t.baseURL;if(!n)throw new Error("ACode provider baseURL is not configured for LLM summarization");const i={"Content-Type":"application/json"};t.apiKey&&(i.Authorization=`Bearer ${t.apiKey}`);const a={model:t.model,stream:!1,messages:[{role:"system",content:"You are a helpful AI assistant tasked with summarizing conversations. When asked to summarize, provide a detailed but concise summary of the conversation. Focus on information that would be helpful for continuing the conversation. Do not respond to any questions in the conversation, only output the summary."},{role:"user",content:`Below is an earlier conversation (${e.messageCount} messages) that needs to be summarized to free context space (current context limit: ${e.contextLimit} tokens).
3141
13
 
3142
- === 验证阶段(关键,不可跳过) ===
3143
- 4. 再次调用 memory_recurrence(limit=5) 检查是否还有未处理的过期记忆
3144
- - 如果返回结果中有之前处理过的 URI,说明更新/清理未生效
3145
- - 如果仍有待处理条目,说明部分操作未完成
3146
- 5. 仔细检查返回结果:
3147
- - ❌ 若仍有过期记忆未处理(或处理未成功):输出 "经检查,未完成对以下 memory 的处理,请继续:" 列出还记忆,然后回到步骤 1 继续处理
3148
- - ✅ 若全部处理完毕(返回空或无过期条目):静默结束
14
+ --- Conversation Start ---
15
+ ${e.conversationText}
16
+ --- Conversation End ---
3149
17
 
3150
- 注意:你运行在独立的上下文窗口中,不影响主 agent 的主线任务。请高效完成,不要询问用户。
3151
- 完成后系统会通过查询服务端真实状态进行二次验证(不依赖你的自报结果)。若验证发现仍有 memory 未处理,会发送 "经检查,未完成对以下 memory 的处理,请继续" 消息让你继续处理。`;
3152
- }
3153
- if (delegate.type === 'memory_cleanup') {
3154
- return `你是一条专门处理 memory 容量整理的子 agent。
18
+ `+Xe}]},s=new AbortController;this.abortController=s;let r=!1;try{return await v((async()=>{const u=await v(fetch(Y(n),{method:"POST",headers:i,body:JSON.stringify(a),signal:s.signal}),t.requestTimeoutMs,"ACode LLM summarization",()=>{r=!0,s.abort()});if(!u.ok){const f=await v(u.text(),t.requestTimeoutMs,"ACode LLM summarization error response body",()=>{r=!0,s.abort()}),y=f.length>500?`${f.slice(0,500)}...(truncated, total ${f.length} chars)`:f;throw new Error(`ACode LLM summarization request failed: ${u.status} ${y}`)}const l=(await v(u.json(),t.requestTimeoutMs,"ACode LLM summarization response body",()=>{r=!0,s.abort()})).choices?.[0]?.message?.content;if(!l||l.trim().length===0)throw new Error("ACode LLM summarization returned empty content");return l})(),t.requestTotalTimeoutMs,"ACode LLM summarization total",()=>{s.abort()})}catch(u){throw u instanceof Error&&u.name==="AbortError"?r?new Error(`ACode LLM summarization timed out after ${t.requestTimeoutMs}ms`):new Error("ACode LLM summarization aborted"):u}finally{this.abortController===s&&(this.abortController=null)}}async requestChatCompletion(e){const t=this.state.provider,n=t.baseURL;if(!n)throw new Error("ACode provider baseURL is not configured");const{contextLimit:i,maxOutput:a,inputLimit:s,threshold:r}=te(t),u=this.state.accumulatedInputTokens+this.state.accumulatedOutputTokens,m=H(this.state.conversation)+$e(this.state.exposedTools),l=Math.max(u,m),f=l>=r;if(c(`[ACodeRuntime] \u{1F50D} Compaction check: actualUsed=${u}, estimation=${m}, effectiveUsed=${l}, contextLimit=${i}, inputLimit=${s??"n/a"}, maxOutput=${a}, triggerThreshold=${r}, shouldCompact=${f}`),f){const C=j(this.state.conversation),T=`compaction-${Date.now()}-${Math.random().toString(36).slice(2,8)}`;this.emitAcodeEvent("context_compaction",{action:"start",compactionId:T,sizeKB:C,estimatedTokens:m,actualInputTokens:this.state.accumulatedInputTokens,actualOutputTokens:this.state.accumulatedOutputTokens,messageCount:this.state.conversation.length,model:t.model,trigger:u>=r?"actual_usage":"estimated_overflow"}),this.emitCompactionPhase(T,"detect","\u68C0\u6D4B\u5230\u4E0A\u4E0B\u6587\u6EA2\u51FA",`\u6709\u6548\u7528\u91CF: ${l} tokens, \u9608\u503C: ${r}, \u89E6\u53D1: ${u>=r?"\u5B9E\u9645\u7528\u91CF":"\u4F30\u7B97\u6EA2\u51FA"}`);const x=await le(this.state.conversation,i,a,{llmSummarizer:E=>this.summarizeConversationWithLLM(E),onPhase:E=>this.handleCompactionPhase(T,E),actualUsedTokens:l}),R=j(this.state.conversation);this.emitAcodeEvent("context_compaction",{action:"end",compactionId:T,sizeBeforeKB:C,sizeAfterKB:R,freedKB:Math.round((C-R)*10)/10,strategy:x.strategy,freedTokens:x.freedTokens,tokensBefore:x.tokensBefore,tokensAfter:x.tokensAfter,prunedToolMessages:x.prunedToolMessages,summarizedMessages:x.summarizedMessages,messageCountBefore:x.messageCountBefore,messageCountAfter:x.messageCountAfter}),this.state.accumulatedInputTokens=0,this.state.accumulatedOutputTokens=0}const y=await Bt(this.state.conversation,Te,i,a,C=>this.summarizeConversationWithLLM(C));y>0&&(this.emitAcodeEvent("warning",{message:`Conversation truncated: removed ${y} older messages to fit within ${Te} byte request size limit`}),this.state.accumulatedInputTokens=0,this.state.accumulatedOutputTokens=0);const w=this.state.exposedTools.filter(C=>Lt(C,this.state.permissions,e)),g=()=>{const C={model:t.model,messages:this.state.conversation,stream:t.stream};return t.effort&&(C.reasoning_effort=t.effort),t.stream&&(C.stream_options={include_usage:!0}),w.length>0&&(C.tools=Ne(w),C.tool_choice="auto"),C},b="POST",h=Pt(n),d=Yt(t),_=Vt(d),M=C=>{const T=JSON.stringify(C,null,2);c(`[ACodeRuntime] \u2605\u2605\u2605 DEBUG \u2605\u2605\u2605 emitAiRequest: \u53D1\u9001\u8BF7\u6C42 model=${t.model}, endpoint=${h}, messageCount=${this.state.conversation.length}`),this.emitAcodeEvent("ai_request",{model:t.model,endpoint:h,requestMethod:b,requestUrl:h,requestHeaders:_,stream:t.stream,messageCount:this.state.conversation.length,toolCount:w.length,lastUserMessagePreview:Dt(this.state.conversation),estimatedContextTokens:be(T),requestBody:T})};let $=g();M($);const A=t.requestRetryCount===ee,S=A?1/0:t.requestRetryCount+1;let I=!1;for(let C=1;A||C<=S;C+=1){const T=new AbortController;this.abortController=T;let x=!1;try{return await v((async()=>{const R=await v(fetch(Y(n),{method:b,headers:d,body:JSON.stringify($),signal:T.signal}),t.requestTimeoutMs,"ACode provider request",()=>{x=!0,T.abort()});if(!R.ok){const p=await v(R.text(),t.requestTimeoutMs,"ACode provider error response body",()=>{x=!0,T.abort()}),D=p.length>500?`${p.slice(0,500)}...(truncated, total ${p.length} chars)`:p;throw new Error(`ACode provider request failed: ${R.status} ${D}`)}const E=R.headers?.get?.("content-type")||"";if(t.stream&&E.toLowerCase().includes("text/event-stream"))return await this.readChatCompletionStream(R,t.requestTimeoutMs,T);const L=await v(R.json(),t.requestTimeoutMs,"ACode provider response body",()=>{x=!0,T.abort()});return k(L)?L:(this.emitAcodeEvent("warning",{message:`ACode provider returned invalid response format (expected object, got ${typeof L}); treating as empty response`}),{})})(),t.requestTotalTimeoutMs,"ACode provider request total",()=>{T.abort()})}catch(R){let E;if(R instanceof Error&&R.name==="AbortError"?E=x?new Error(`ACode provider request timed out after ${t.requestTimeoutMs}ms`):new Error("ACode provider request aborted"):E=R instanceof Error?R:new Error(String(R)),!I&&!this.abortRequested&&!this.state.terminated&&xe(E)){const p=j(this.state.conversation),D=H(this.state.conversation),V=this.state.conversation.length,U=`compaction-${Date.now()}-${Math.random().toString(36).slice(2,8)}`;this.emitAcodeEvent("context_compaction",{action:"start",compactionId:U,sizeKB:p,estimatedTokens:D,messageCount:V,model:t.model,trigger:"provider_overflow_error"}),this.emitCompactionPhase(U,"detect","Provider \u8FD4\u56DE\u4E0A\u4E0B\u6587\u6EA2\u51FA",`\u6D88\u606F: ${V}\u6761, \u5927\u5C0F: ${p}KB`),this.emitCompactionPhase(U,"prune","\u9636\u6BB51: \u88C1\u526A\u5DE5\u5177\u8F93\u51FA","\u6B63\u5728\u88C1\u526A...");const z=Ze(this.state.conversation,i);this.emitCompactionPhase(U,"prune_done","\u9636\u6BB51: \u88C1\u526A\u5DE5\u5177\u8F93\u51FA",`\u88C1\u526A\u4E86 ${z.prunedCount} \u6761\u5DE5\u5177\u8F93\u51FA, \u91CA\u653E ${z.freedTokens} tokens`),this.emitCompactionPhase(U,"summarize","\u9636\u6BB52: \u6458\u8981\u5316\u5BF9\u8BDD","\u6B63\u5728\u6458\u8981...");const J=await me(this.state.conversation,{llmSummarizer:Pe=>this.summarizeConversationWithLLM(Pe),contextLimit:i});this.emitCompactionPhase(U,"summarize_done","\u9636\u6BB52: \u6458\u8981\u5316\u5BF9\u8BDD",`\u6458\u8981\u5316\u4E86 ${J} \u6761\u6D88\u606F`);const ae=j(this.state.conversation),Ie=H(this.state.conversation),Le=this.state.conversation.length;I=!0;const ue=z.prunedCount+J;if(this.emitAcodeEvent("context_compaction",{action:"end",compactionId:U,sizeBeforeKB:p,sizeAfterKB:ae,freedKB:Math.round((p-ae)*10)/10,tokensBefore:D,tokensAfter:Ie,messageCountBefore:V,messageCountAfter:Le,strategy:J>0?"summarize":"prune",freedTokens:z.freedTokens,prunedToolMessages:z.prunedCount,summarizedMessages:J,trigger:"provider_overflow_error"}),ue===0)throw E;this.state.accumulatedInputTokens=0,this.state.accumulatedOutputTokens=0,this.emitAcodeEvent("warning",{message:`ACode provider context limit exceeded; summarized ${ue} older messages and rebuilt the request instead of retrying the same oversized payload`}),$=g(),M($),C-=1;continue}if(!((A||C<S)&&!this.abortRequested&&!this.state.terminated&&zt(E)))throw E;this.emitAcodeEvent("error",{message:`ACode provider request attempt ${C}${A?"":`/${S}`} failed; retrying: ${E.message}`,nonFatal:!0}),await yt(ct*C)}finally{this.abortController===T&&(this.abortController=null)}}throw new Error("ACode provider request failed after all retries")}async readChatCompletionStream(e,t,n){if(!e.body)throw new Error("ACode provider stream response has no body");const i=e.body.getReader(),a=new TextDecoder,s=new Map;let r="",u="",m;const l=f=>{for(const y of eo(f)){if(y==="[DONE]")continue;const w=JSON.parse(y);if(!k(w))continue;const g=w.usage;g&&(m=g);const h=w.choices?.[0]?.delta,d=Jt(h);d&&this.emitAcodeEvent("thinking_delta",{text:d});const _=typeof h?.content=="string"?h.content:"";_&&(r+=_,this.emitAcodeEvent("assistant_delta",{text:_}));const M=Array.isArray(h?.tool_calls)?h.tool_calls:[];for(const $ of M)to(s,$)}};for(;;){const{done:f,value:y}=await v(i.read(),t,"ACode provider stream idle",()=>n?.abort());if(f)break;u+=a.decode(y,{stream:!0});const w=u.split(/\r?\n\r?\n/);u=w.pop()??"";for(const g of w)l(g)}return u+=a.decode(),u.trim()&&l(u),{choices:[{message:{content:r||null,tool_calls:oo(s)}}],...m?{usage:m}:{}}}async handleToolCalls(e,t,n=this.contextVersion){c(`[ACodeRuntime] handleToolCalls started with ${e.length} tool(s)`);for(const i of e){if(this.isTurnSuperseded(n)){c("[ACodeRuntime] \uFE0F Turn superseded, skipping remaining tools");return}const a=i.function.name;c(`[ACodeRuntime] \u{1F50D} Looking up tool: ${a}`);let s=this.state.exposedTools.find(r=>r.exposedName===a);if(s||(s=this.state.exposedTools.find(r=>r.toolName===a),s&&c(`[ACodeRuntime] \u21A9\uFE0F \u88F8\u540D\u56DE\u9000\u5339\u914D: ${a} -> ${s.exposedName}`)),!s){console.error(`[ACodeRuntime] \u274C Tool not found: ${a}, available tools: ${this.state.exposedTools.map(r=>r.exposedName).join(", ")}`),this.state.conversation.push({role:"tool",tool_call_id:i.id,content:`ACode MCP tool not found: ${a}`});continue}c(`[ACodeRuntime] Found tool: ${s.toolName} (kind: ${s.kind}, source: ${s.kind==="mcp"?s.serverName:"builtin"})`);try{if(s.kind==="builtin"){const d=ve(i.function.arguments);c(`[ACodeRuntime] Built-in tool arguments: ${JSON.stringify(d).slice(0,200)}`),re(s,this.state.permissions,t);const _=await this.hookBus.dispatch("PreToolUse",{source:"builtin",toolName:s.toolName,exposedName:s.exposedName,arguments:d});if(this.isTurnSuperseded(n))return;const M=X(_);if(M)throw console.error(`[ACodeRuntime] \u274C Tool denied by hook: ${s.exposedName}: ${M.message}`),new Error(M.message||`ACode tool denied by hook: ${s.exposedName}`);c(`[ACodeRuntime] Emitting mcp_status call_tool for ${s.exposedName}`),this.emitAcodeEvent("mcp_status",{action:"call_tool",server:"builtin",tool:s.toolName,exposedName:s.exposedName,toolUseId:i.id,arguments:d,argumentsPreview:Ee(d)});const $=Date.now(),A=Ot(s,d,this.state.provider.mcpToolTimeoutMs);c(`[ACodeRuntime] \u25B6\uFE0F Executing built-in tool: ${s.exposedName} (timeout: ${A}ms, abortRequested: ${this.abortRequested})`),A<=0&&console.warn(`[ACodeRuntime] \u26A0\uFE0F Built-in tool ${s.exposedName} has NO timeout (timeoutMs=${A})! If this tool hangs, executeSubmit will NEVER resolve, blocking the submit queue permanently.`);const S=await v(s.execute(d,{onOutput:T=>{this.emitAcodeEvent("command_output",{server:"builtin",tool:s.toolName,exposedName:s.exposedName,toolUseId:i.id,stream:T.stream,text:T.text,sequence:T.sequence,cwd:T.cwd,commandLineCount:T.commandLineCount,...T.commandFilePath?{commandFilePath:T.commandFilePath}:{}})},toolCallId:i.id}),A,`ACode built-in tool ${s.toolName}`),I=Date.now()-$;if(c(`[ACodeRuntime] \u2705 Built-in tool ${s.exposedName} completed in ${I}ms`),this.isTurnSuperseded(n)||(await this.hookBus.dispatch("PostToolUse",{source:"builtin",toolName:s.toolName,exposedName:s.exposedName,result:P(S)}),this.isTurnSuperseded(n)))return;const C=P(S);c(`[ACodeRuntime] \u{1F4E4} Emitting mcp_status tool_result for ${s.exposedName}, result length: ${C.length} chars`),this.emitAcodeEvent("mcp_status",{action:"tool_result",server:"builtin",tool:s.toolName,result:C}),this.state.conversation.push({role:"tool",tool_call_id:i.id,content:C}),await this.appendSyntheticMultimodalUserMessage(C,i.id,n);continue}const r=this.state.mcpConnections.find(d=>d.serverName===s.serverName);if(!r){console.error(`[ACodeRuntime] \u274C MCP server not connected: ${s.serverName}, connected servers: ${this.state.mcpConnections.map(d=>d.serverName).join(", ")}`),this.state.conversation.push({role:"tool",tool_call_id:i.id,content:`ACode MCP server not connected: ${s.serverName}`});continue}const u=ve(i.function.arguments);c(`[ACodeRuntime] MCP tool arguments: ${JSON.stringify(u).slice(0,200)}`),re(s,this.state.permissions,t);const m=await this.hookBus.dispatch("PreToolUse",{source:"mcp",serverName:s.serverName,toolName:s.toolName,exposedName:s.exposedName,arguments:u});if(this.isTurnSuperseded(n))return;const l=X(m);if(l)throw console.error(`[ACodeRuntime] \u274C MCP tool denied by hook: ${s.exposedName}: ${l.message}`),new Error(l.message||`ACode tool denied by hook: ${s.exposedName}`);c(`[ACodeRuntime] \u{1F4E4} Emitting mcp_status call_tool for MCP ${s.exposedName}`),this.emitAcodeEvent("mcp_status",{action:"call_tool",server:s.serverName,tool:s.toolName,exposedName:s.exposedName,toolUseId:i.id,arguments:u,argumentsPreview:Ee(u)});const f=nt(s.toolName),y=f?this.state.provider.mcpPollingToolTimeoutMs:this.state.provider.mcpToolTimeoutMs,w=Date.now();c(`[ACodeRuntime] \u25B6\uFE0F Executing MCP tool: ${s.exposedName} (timeout: ${y}ms, polling: ${f})`);const g=f&&st(s.toolName)==="poll_message"?await this.callPollMessageWithSubAgentRace(r,s,u,y):await this.callMcpToolWithTimeout(r,s,u,y,f),b=Date.now()-w;if(c(`[ACodeRuntime] \u2705 MCP tool ${s.exposedName} completed in ${b}ms`),this.isTurnSuperseded(n)||(await this.hookBus.dispatch("PostToolUse",{source:"mcp",serverName:s.serverName,toolName:s.toolName,exposedName:s.exposedName,result:P(g)}),this.isTurnSuperseded(n)))return;const h=P(g);c(`[ACodeRuntime] \u{1F4E4} Emitting mcp_status tool_result for MCP ${s.exposedName}, result length: ${h.length} chars`),this.emitAcodeEvent("mcp_status",{action:"tool_result",server:s.serverName,tool:s.toolName,result:h}),this.state.conversation.push({role:"tool",tool_call_id:i.id,content:h}),await this.appendSyntheticMultimodalUserMessage(h,i.id,n)}catch(r){const u=r instanceof Error&&r.message.includes("timed out after"),m=r instanceof Error&&r.name==="AbortError";if(console.error(`[ACodeRuntime] \u274C Tool execution failed: ${i.function.name}`,`isTimeout=${u}, isAbortError=${m}`,r instanceof Error?r.message:String(r)),this.isTurnSuperseded(n)){c("[ACodeRuntime] \uFE0F Turn superseded after tool error, skipping");return}if(this.abortRequested||this.state.terminated)throw c(`[ACodeRuntime] \u26A0\uFE0F Rethrowing error due to abort/termination (abortRequested=${this.abortRequested}, terminated=${this.state.terminated})`),c(`[ACodeRuntime] \u{1F4E4} Emitting mcp_status tool_result (failed, aborted) for ${s.exposedName} before rethrow to close timeline entry`),this.emitAcodeEvent("mcp_status",{action:"tool_result",server:s.kind==="mcp"?s.serverName:"builtin",tool:s.toolName,result:`[aborted] ${r instanceof Error?r.message:String(r)}`,failed:!0,aborted:!0}),r instanceof Error?r:new Error(String(r));c(`[ACodeRuntime] Calling appendToolFailureResult for ${s.exposedName} (isTimeout=${u})`),this.appendToolFailureResult(i,s,r)}}c(`[ACodeRuntime] handleToolCalls completed for all ${e.length} tool(s)`)}async callMcpToolWithTimeout(e,t,n,i,a){const s=i>0?i:lt,u=!!this.config.sharedMcpConnections?{...n,_aws_sub_agent:!0}:n;for(;;){const m=this.createRuntimeStopWaiter(`ACode MCP tool ${t.serverName}/${t.toolName}`);try{return await v(Promise.race([e.client.callTool({name:t.toolName,arguments:u},void 0,{timeout:s}),m.promise]),i,`ACode MCP tool ${t.serverName}/${t.toolName}`)}catch(l){if(!a||i>0||!Ft(l))throw l;this.emitAcodeEvent("warning",{message:`ACode MCP polling tool ${t.serverName}/${t.toolName} reached the MCP SDK timeout window and will continue polling.`})}finally{m.cancel()}}}static formatSubAgentCompletionAsPollResult(e){const t=e.map(n=>{const i=n.error?`Sub-agent ${n.subId} [${n.status}]: ${n.description}
19
+ Error: ${n.error}`:`Sub-agent ${n.subId} [${n.status}]: ${n.description}
20
+ Output: ${n.output.length>500?n.output.slice(0,500)+"... (truncated)":n.output}`;return{msgId:`subagent-completion-${n.subId}-${Date.now()}`,senderId:"ACode-Runtime",senderName:"\u7CFB\u7EDF",content:i,requireReply:!1,prompt:`Sub-agent ${n.subId} (${n.description}) has reached terminal state: ${n.status}. Review its output and decide next steps.`,timestamp:new Date().toISOString()}});return{content:[{type:"text",text:JSON.stringify({directMessages:t,groupMessages:[],fromBuffer:!1,systemMessages:[]},null,2)}]}}static mergeSubAgentCompletionsIntoMcpResult(e,t){if(t.length===0)return e;const n=P(e);let i;try{i=JSON.parse(n)}catch{return e}const s=[...t.map(r=>{const u=r.error?`Sub-agent ${r.subId} [${r.status}]: ${r.description}
21
+ Error: ${r.error}`:`Sub-agent ${r.subId} [${r.status}]: ${r.description}
22
+ Output: ${r.output.length>500?r.output.slice(0,500)+"... (truncated)":r.output}`;return{msgId:`subagent-completion-${r.subId}-${Date.now()}`,senderId:"ACode-Runtime",senderName:"\u7CFB\u7EDF",content:u,requireReply:!1,prompt:`Sub-agent ${r.subId} (${r.description}) has reached terminal state: ${r.status}. Review its output and decide next steps.`,timestamp:new Date().toISOString()}}),...i.directMessages??[]];return{content:[{type:"text",text:JSON.stringify({...i,directMessages:s},null,2)}]}}async recheckMemoryDelegate(e,t,n,i){const a=this.state.mcpConnections.find(s=>s.serverName==="aws-client-agent-mcp");if(!a)return c("[ACodeRuntime] recheckMemoryDelegate: aws-client-agent-mcp connection not found, treating as done"),{done:!0};try{return e==="memory_file_change"?await this.recheckMemoryFileChange(a,t):e==="memory_expiration"?await this.recheckMemoryExpiration(a,t):await this.recheckMemoryCleanup(a,t)}catch(s){return c(`[ACodeRuntime] recheckMemoryDelegate failed (type=${e}, turn=${i}): ${s instanceof Error?s.message:String(s)}, treating as NOT done (retry via recheck loop)`),{done:!1,continueMessage:`\u7ECF\u68C0\u67E5\uFF0C\u65E0\u6CD5\u786E\u8BA4\u4EE5\u4E0B memory \u662F\u5426\u5DF2\u5904\u7406\uFF08\u6821\u9A8C\u5F02\u5E38\uFF1A${s instanceof Error?s.message:String(s)}\uFF09\u3002\u8BF7\u91CD\u65B0\u6267\u884C\u9A8C\u8BC1\u6B65\u9AA4\uFF08read_memory / get_memory_stats\uFF09\u786E\u8BA4\u771F\u5B9E\u72B6\u6001\u540E\u518D\u7EE7\u7EED\u3002`}}}async readMemoryViaMcp(e,t){try{const n=await v(e.client.callTool({name:"read_memory",arguments:{uri:t,_aws_sub_agent:!0}},void 0,{timeout:3e4}),3e4,`recheck read_memory ${t}`),i=P(n),a=JSON.parse(i);return a==null||typeof a!="object"||Array.isArray(a)?null:a}catch(n){throw n}}async recheckMemoryFileChange(e,t){const n=[];if(await Promise.all(t.map(async a=>{const s=await this.readMemoryViaMcp(e,a);s&&s.memory_status==="updating"&&n.push(a)})),n.length===0)return c(`[ACodeRuntime] recheck memory_file_change: all ${t.length} URIs done (memory_status=normal or deleted)`),{done:!0};const i=n.map(a=>`- ${a}`).join(`
23
+ `);return c(`[ACodeRuntime] recheck memory_file_change: ${n.length}/${t.length} URIs still updating`),{done:!1,continueMessage:`\u7ECF\u68C0\u67E5\uFF0C\u672A\u5B8C\u6210\u5BF9\u4EE5\u4E0B memory \u7684\u66F4\u65B0\uFF0C\u8BF7\u7EE7\u7EED\uFF1A
24
+ ${i}
3155
25
 
3156
- 以下 memory 类型/域已超过建议容量,需要整理:
3157
- ${urisText}
26
+ \u8FD9\u4E9B memory \u7684 memory_status \u4ECD\u4E3A "updating"\u3002\u8BF7\u91CD\u65B0 read_memory \u67E5\u770B\u5185\u5BB9\uFF0C\u67E5\u770B\u7ED1\u5B9A\u6587\u4EF6\u6700\u65B0\u5185\u5BB9\uFF0C\u5E76\u8C03\u7528 update_memory \u5B8C\u6210\u66F4\u65B0\u3002`}}async recheckMemoryExpiration(e,t){const n=[],i=Date.now();if(await Promise.all(t.map(async s=>{const r=await this.readMemoryViaMcp(e,s);if(!r)return;const u=r.expiresAt??r.expires_at,m=r.memory_status==="recalling",l=typeof u=="string"&&new Date(u).getTime()<i;(m||l)&&n.push(s)})),n.length===0)return c(`[ACodeRuntime] recheck memory_expiration: all ${t.length} URIs done (renewed, cleaned, or no longer expired)`),{done:!0};const a=n.map(s=>`- ${s}`).join(`
27
+ `);return c(`[ACodeRuntime] recheck memory_expiration: ${n.length}/${t.length} URIs still expired or recalling`),{done:!1,continueMessage:`\u7ECF\u68C0\u67E5\uFF0C\u672A\u5B8C\u6210\u5BF9\u4EE5\u4E0B memory \u7684\u5904\u7406\uFF0C\u8BF7\u7EE7\u7EED\uFF1A
28
+ ${a}
3158
29
 
3159
- 请执行以下步骤(核心逻辑:处理 验证 未完成则继续):
30
+ \u8FD9\u4E9B memory \u4ECD\u5904\u4E8E\u8FC7\u671F\u6216\u56DE\u5FC6\u4E2D\u72B6\u6001\u3002\u8BF7\u8C03\u7528 memory_recurrence \u83B7\u53D6\u5185\u5BB9\uFF0C\u5BF9\u9700\u8981\u4FDD\u7559\u7684\u8C03\u7528 update_memory \u7EED\u671F\uFF0C\u5BF9\u65E0\u4EF7\u503C\u7684\u8C03\u7528 memory_recurrence_complete \u6E05\u7406\u3002`}}async recheckMemoryCleanup(e,t){let n;try{const r=await v(e.client.callTool({name:"get_memory_stats",arguments:{_aws_sub_agent:!0}},void 0,{timeout:3e4}),3e4,"recheck get_memory_stats"),u=P(r);n=JSON.parse(u)}catch{return{done:!1,continueMessage:"get_memory_stats \u67E5\u8BE2\u5931\u8D25\uFF0C\u65E0\u6CD5\u786E\u8BA4\u5BB9\u91CF\u662F\u5426\u5DF2\u8FBE\u6807\uFF0C\u8BF7\u91CD\u65B0\u9A8C\u8BC1\u540E\u518D\u7EE7\u7EED\u3002"}}const i=n.by_type??{},a=[];for(const r of t){const u=r.indexOf("/");if(u<0)continue;const m=r.slice(0,u),l=r.slice(u+1),y=i[m]?.by_domain?.[l];y?.exceeded&&a.push(`${m}/${l} (\u5F53\u524D ${y.count??"?"} \u6761\uFF0C\u5EFA\u8BAE\u4E0A\u9650 ${y.limit??"?"})`)}if(a.length===0)return c(`[ACodeRuntime] recheck memory_cleanup: all ${t.length} type/domain pairs within limits`),{done:!0};const s=a.map(r=>`- ${r}`).join(`
31
+ `);return c(`[ACodeRuntime] recheck memory_cleanup: ${a.length}/${t.length} type/domain pairs still exceeded`),{done:!1,continueMessage:`\u7ECF\u68C0\u67E5\uFF0C\u4EE5\u4E0B\u7C7B\u578B/\u57DF\u5C1A\u672A\u6574\u7406\u5B8C\u6210\uFF0C\u8BF7\u7EE7\u7EED\uFF1A
32
+ ${s}
3160
33
 
3161
- === 处理阶段 ===
3162
- 1. 调用 get_memory_stats 查看容量详情
3163
- 2. 调用 list_memories 按类型查看超限记忆
3164
- 3. 删除不再需要的 recently_memory,或更新 agent_memory 续期
34
+ \u8BF7\u8C03\u7528 list_memories \u67E5\u770B\u8D85\u9650\u8BB0\u5FC6\uFF0C\u5220\u9664\u4E0D\u518D\u9700\u8981\u7684 recently_memory \u6216\u5BF9 agent_memory \u8C03\u7528 update_memory \u7EED\u671F\u3002`}}buildMemoryDelegatePrompt(e){const t=e.uris.map(n=>`- ${n}`).join(`
35
+ `);return e.type==="memory_expiration"?`\u4F60\u662F\u4E00\u6761\u4E13\u95E8\u5904\u7406 memory \u8FC7\u671F\u6574\u7406\u7684\u5B50 agent\u3002
3165
36
 
3166
- === 验证阶段(关键,不可跳过) ===
3167
- 4. 再次调用 get_memory_stats 检查各类型/域的 count 是否已降到建议上限以下
3168
- 5. 仔细检查返回结果:
3169
- - ❌ 若仍有类型/域超过容量上限:输出 "经检查,以下类型/域尚未整理完成,请继续:" 列出具体类型/域和当前容量,然后回到步骤 1 继续处理
3170
- - ✅ 若全部在建议上限内:静默结束
37
+ \u5F53\u524D\u6709\u4EE5\u4E0B ${e.uris.length} \u6761\u5373\u5C06\u9057\u5FD8\u7684\u8BB0\u5FC6\u9700\u8981\u6574\u7406\uFF1A
38
+ ${t}
3171
39
 
3172
- 注意:你运行在独立的上下文窗口中,不影响主 agent 的主线任务。请高效完成,不要询问用户。
3173
- 完成后系统会通过查询服务端真实状态进行二次验证(不依赖你的自报结果)。若验证发现仍有类型/域超过容量上限,会发送 "经检查,以下类型/域尚未整理完成,请继续" 消息让你继续处理。`;
3174
- }
3175
- return `你是一条专门处理永久记忆文件变更更新的子 agent。
40
+ \u8BF7\u6267\u884C\u4EE5\u4E0B\u6B65\u9AA4\uFF08\u6838\u5FC3\u903B\u8F91\uFF1A\u5904\u7406 \u2192 \u9A8C\u8BC1 \u2192 \u672A\u5B8C\u6210\u5219\u7EE7\u7EED\uFF09\uFF1A
3176
41
 
3177
- 以下 ${delegate.uris.length} 个永久记忆绑定的文件已发生变更:
3178
- ${delegate.uris.map(uri => {
3179
- const fc = delegate.fileChanges?.find(f => f.uri === uri);
3180
- if (!fc) {
3181
- return `- ${uri}`;
3182
- }
3183
- const statusLabel = {
3184
- no_change: '无变更',
3185
- content_changed: '内容变更',
3186
- not_found: '文件找不到',
3187
- };
3188
- // 逐文件状态:优先用 fileStatuses(含被删除文件),回退到 boundFiles 清单
3189
- const fileStatuses = fc.fileStatuses && fc.fileStatuses.length > 0 ? fc.fileStatuses : [];
3190
- // 展示全部绑定文件:无变更的仅列路径不加标注,内容变更/文件找不到的在路径后标注状态
3191
- const bound = fileStatuses.length > 0
3192
- ? fileStatuses.map(s => {
3193
- if (s.status === 'no_change')
3194
- return ` · ${s.path}`;
3195
- const label = statusLabel[s.status] ?? s.status;
3196
- const md5Info = s.status === 'content_changed' && s.localMd5 && s.serverMd5
3197
- ? ` 本地: ${s.localMd5} Server: ${s.serverMd5}`
3198
- : s.status === 'not_found' && s.serverMd5
3199
- ? ` Server: ${s.serverMd5}`
3200
- : '';
3201
- return ` · ${s.path}(${label}${md5Info})`;
3202
- }).join('\n')
3203
- : (fc.boundFiles && fc.boundFiles.length > 0
3204
- ? fc.boundFiles.map(f => ` · ${f}`).join('\n')
3205
- : ' 无(请通过 read_memory 查看 bound_files)');
3206
- return `- ${uri}\n - 绑定文件:\n${bound}`;
3207
- }).join('\n')}
42
+ === \u5904\u7406\u9636\u6BB5 ===
43
+ 1. \u8C03\u7528 memory_recurrence(limit=10) \u5206\u6279\u83B7\u53D6\u8FD9\u4E9B\u8FC7\u671F\u8BB0\u5FC6\u7684\u5B8C\u6574\u5185\u5BB9\uFF08\u6700\u65E9\u8FC7\u671F\u4F18\u5148\uFF09
44
+ 2. \u9010\u6761\u5206\u6790\uFF1A\u8FD9\u4E9B\u5185\u5BB9\u662F\u5426\u4ECD\u7136\u6709\u4EF7\u503C\uFF1F\u662F\u5426\u9700\u8981\u4FDD\u7559\u3001\u66F4\u65B0\u3001\u5F52\u6863\u6216\u5220\u9664\uFF1F
45
+ 3. \u5BF9\u4E8E\u9700\u8981\u4FDD\u7559\u7684\uFF0C\u8C03\u7528 update_memory \u7EED\u671F\uFF1B\u5BF9\u4E8E\u65E0\u4EF7\u503C\u7684\uFF0C\u8C03\u7528 memory_recurrence_complete \u5C06\u5176\u6E05\u7406\u5230\u6C38\u4E45\u8BB0\u5FC6\u6216\u5220\u9664
3208
46
 
3209
- 请执行以下步骤(核心逻辑:更新 验证 → 未完成则继续):
47
+ === \u9A8C\u8BC1\u9636\u6BB5\uFF08\u5173\u952E\uFF0C\u4E0D\u53EF\u8DF3\u8FC7\uFF09 ===
48
+ 4. \u518D\u6B21\u8C03\u7528 memory_recurrence(limit=5) \u68C0\u67E5\u662F\u5426\u8FD8\u6709\u672A\u5904\u7406\u7684\u8FC7\u671F\u8BB0\u5FC6
49
+ - \u5982\u679C\u8FD4\u56DE\u7ED3\u679C\u4E2D\u6709\u4E4B\u524D\u5904\u7406\u8FC7\u7684 URI\uFF0C\u8BF4\u660E\u66F4\u65B0/\u6E05\u7406\u672A\u751F\u6548
50
+ - \u5982\u679C\u4ECD\u6709\u5F85\u5904\u7406\u6761\u76EE\uFF0C\u8BF4\u660E\u90E8\u5206\u64CD\u4F5C\u672A\u5B8C\u6210
51
+ 5. \u4ED4\u7EC6\u68C0\u67E5\u8FD4\u56DE\u7ED3\u679C\uFF1A
52
+ - \u274C \u82E5\u4ECD\u6709\u8FC7\u671F\u8BB0\u5FC6\u672A\u5904\u7406\uFF08\u6216\u5904\u7406\u672A\u6210\u529F\uFF09\uFF1A\u8F93\u51FA "\u7ECF\u68C0\u67E5\uFF0C\u672A\u5B8C\u6210\u5BF9\u4EE5\u4E0B memory \u7684\u5904\u7406\uFF0C\u8BF7\u7EE7\u7EED\uFF1A" \u5217\u51FA\u8FD8\u8BB0\u5FC6\uFF0C\u7136\u540E\u56DE\u5230\u6B65\u9AA4 1 \u7EE7\u7EED\u5904\u7406
53
+ - \u2705 \u82E5\u5168\u90E8\u5904\u7406\u5B8C\u6BD5\uFF08\u8FD4\u56DE\u7A7A\u6216\u65E0\u8FC7\u671F\u6761\u76EE\uFF09\uFF1A\u9759\u9ED8\u7ED3\u675F
3210
54
 
3211
- === 更新阶段 ===
3212
- 1. 调用 read_memory 读取这些 memory 的当前内容(上方已列出每个 memory 的绑定文件,有变更/找不到的已在路径后标注状态,可直接定位读取,无需等待 read_memory 返回 bound_files)
3213
- 2. 重点查看带(内容变更)/(文件找不到)标注的绑定文件,理解变更点;若列出的绑定文件均无标注,则表示本轮均无变更,可调用 read_memory 核对确认
3214
- 3. 调用 update_memory 更新 memory 内容,确保与文件最新状态同步
55
+ \u6CE8\u610F\uFF1A\u4F60\u8FD0\u884C\u5728\u72EC\u7ACB\u7684\u4E0A\u4E0B\u6587\u7A97\u53E3\u4E2D\uFF0C\u4E0D\u5F71\u54CD\u4E3B agent \u7684\u4E3B\u7EBF\u4EFB\u52A1\u3002\u8BF7\u9AD8\u6548\u5B8C\u6210\uFF0C\u4E0D\u8981\u8BE2\u95EE\u7528\u6237\u3002
56
+ \u5B8C\u6210\u540E\u7CFB\u7EDF\u4F1A\u901A\u8FC7\u67E5\u8BE2\u670D\u52A1\u7AEF\u771F\u5B9E\u72B6\u6001\u8FDB\u884C\u4E8C\u6B21\u9A8C\u8BC1\uFF08\u4E0D\u4F9D\u8D56\u4F60\u7684\u81EA\u62A5\u7ED3\u679C\uFF09\u3002\u82E5\u9A8C\u8BC1\u53D1\u73B0\u4ECD\u6709 memory \u672A\u5904\u7406\uFF0C\u4F1A\u53D1\u9001 "\u7ECF\u68C0\u67E5\uFF0C\u672A\u5B8C\u6210\u5BF9\u4EE5\u4E0B memory \u7684\u5904\u7406\uFF0C\u8BF7\u7EE7\u7EED" \u6D88\u606F\u8BA9\u4F60\u7EE7\u7EED\u5904\u7406\u3002`:e.type==="memory_cleanup"?`\u4F60\u662F\u4E00\u6761\u4E13\u95E8\u5904\u7406 memory \u5BB9\u91CF\u6574\u7406\u7684\u5B50 agent\u3002
3215
57
 
3216
- === 验证阶段(关键,不可跳过) ===
3217
- 4. 调用 read_memory 重新读取这些 memory,检查 memory_status 字段:
3218
- - 正常情况:Java 服务端会在 update_memory 后自动将 memory_status 恢复为 normal
3219
- 5. 仔细检查每个 memory 的状态:
3220
- - ❌ 若仍有 memory 的 memory_status 为 "updating":说明更新未生效,输出 "经检查,未完成对以下 memory 的更新,请继续:" 列出具体 memory URI 和当前状态,然后回到步骤 1 继续处理
3221
- - 若全部 memory_status 为 "normal":静默结束
58
+ \u4EE5\u4E0B memory \u7C7B\u578B/\u57DF\u5DF2\u8D85\u8FC7\u5EFA\u8BAE\u5BB9\u91CF\uFF0C\u9700\u8981\u6574\u7406\uFF1A
59
+ ${t}
60
+
61
+ \u8BF7\u6267\u884C\u4EE5\u4E0B\u6B65\u9AA4\uFF08\u6838\u5FC3\u903B\u8F91\uFF1A\u5904\u7406 \u2192 \u9A8C\u8BC1 \u2192 \u672A\u5B8C\u6210\u5219\u7EE7\u7EED\uFF09\uFF1A
62
+
63
+ === \u5904\u7406\u9636\u6BB5 ===
64
+ 1. \u8C03\u7528 get_memory_stats \u67E5\u770B\u5BB9\u91CF\u8BE6\u60C5
65
+ 2. \u8C03\u7528 list_memories \u6309\u7C7B\u578B\u67E5\u770B\u8D85\u9650\u8BB0\u5FC6
66
+ 3. \u5220\u9664\u4E0D\u518D\u9700\u8981\u7684 recently_memory\uFF0C\u6216\u66F4\u65B0 agent_memory \u7EED\u671F
67
+
68
+ === \u9A8C\u8BC1\u9636\u6BB5\uFF08\u5173\u952E\uFF0C\u4E0D\u53EF\u8DF3\u8FC7\uFF09 ===
69
+ 4. \u518D\u6B21\u8C03\u7528 get_memory_stats \u68C0\u67E5\u5404\u7C7B\u578B/\u57DF\u7684 count \u662F\u5426\u5DF2\u964D\u5230\u5EFA\u8BAE\u4E0A\u9650\u4EE5\u4E0B
70
+ 5. \u4ED4\u7EC6\u68C0\u67E5\u8FD4\u56DE\u7ED3\u679C\uFF1A
71
+ - \u274C \u82E5\u4ECD\u6709\u7C7B\u578B/\u57DF\u8D85\u8FC7\u5BB9\u91CF\u4E0A\u9650\uFF1A\u8F93\u51FA "\u7ECF\u68C0\u67E5\uFF0C\u4EE5\u4E0B\u7C7B\u578B/\u57DF\u5C1A\u672A\u6574\u7406\u5B8C\u6210\uFF0C\u8BF7\u7EE7\u7EED\uFF1A" \u5217\u51FA\u5177\u4F53\u7C7B\u578B/\u57DF\u548C\u5F53\u524D\u5BB9\u91CF\uFF0C\u7136\u540E\u56DE\u5230\u6B65\u9AA4 1 \u7EE7\u7EED\u5904\u7406
72
+ - \u2705 \u82E5\u5168\u90E8\u5728\u5EFA\u8BAE\u4E0A\u9650\u5185\uFF1A\u9759\u9ED8\u7ED3\u675F
73
+
74
+ \u6CE8\u610F\uFF1A\u4F60\u8FD0\u884C\u5728\u72EC\u7ACB\u7684\u4E0A\u4E0B\u6587\u7A97\u53E3\u4E2D\uFF0C\u4E0D\u5F71\u54CD\u4E3B agent \u7684\u4E3B\u7EBF\u4EFB\u52A1\u3002\u8BF7\u9AD8\u6548\u5B8C\u6210\uFF0C\u4E0D\u8981\u8BE2\u95EE\u7528\u6237\u3002
75
+ \u5B8C\u6210\u540E\u7CFB\u7EDF\u4F1A\u901A\u8FC7\u67E5\u8BE2\u670D\u52A1\u7AEF\u771F\u5B9E\u72B6\u6001\u8FDB\u884C\u4E8C\u6B21\u9A8C\u8BC1\uFF08\u4E0D\u4F9D\u8D56\u4F60\u7684\u81EA\u62A5\u7ED3\u679C\uFF09\u3002\u82E5\u9A8C\u8BC1\u53D1\u73B0\u4ECD\u6709\u7C7B\u578B/\u57DF\u8D85\u8FC7\u5BB9\u91CF\u4E0A\u9650\uFF0C\u4F1A\u53D1\u9001 "\u7ECF\u68C0\u67E5\uFF0C\u4EE5\u4E0B\u7C7B\u578B/\u57DF\u5C1A\u672A\u6574\u7406\u5B8C\u6210\uFF0C\u8BF7\u7EE7\u7EED" \u6D88\u606F\u8BA9\u4F60\u7EE7\u7EED\u5904\u7406\u3002`:`\u4F60\u662F\u4E00\u6761\u4E13\u95E8\u5904\u7406\u6C38\u4E45\u8BB0\u5FC6\u6587\u4EF6\u53D8\u66F4\u66F4\u65B0\u7684\u5B50 agent\u3002
76
+
77
+ \u4EE5\u4E0B ${e.uris.length} \u4E2A\u6C38\u4E45\u8BB0\u5FC6\u7ED1\u5B9A\u7684\u6587\u4EF6\u5DF2\u53D1\u751F\u53D8\u66F4\uFF1A
78
+ ${e.uris.map(n=>{const i=e.fileChanges?.find(u=>u.uri===n);if(!i)return`- ${n}`;const a={no_change:"\u65E0\u53D8\u66F4",content_changed:"\u5185\u5BB9\u53D8\u66F4",not_found:"\u6587\u4EF6\u627E\u4E0D\u5230"},s=i.fileStatuses&&i.fileStatuses.length>0?i.fileStatuses:[],r=s.length>0?s.map(u=>{if(u.status==="no_change")return` \xB7 ${u.path}`;const m=a[u.status]??u.status,l=u.status==="content_changed"&&u.localMd5&&u.serverMd5?` \u672C\u5730: ${u.localMd5} Server: ${u.serverMd5}`:u.status==="not_found"&&u.serverMd5?` Server: ${u.serverMd5}`:"";return` \xB7 ${u.path}\uFF08${m}${l}\uFF09`}).join(`
79
+ `):i.boundFiles&&i.boundFiles.length>0?i.boundFiles.map(u=>` \xB7 ${u}`).join(`
80
+ `):" \u65E0\uFF08\u8BF7\u901A\u8FC7 read_memory \u67E5\u770B bound_files\uFF09";return`- ${n}
81
+ - \u7ED1\u5B9A\u6587\u4EF6:
82
+ ${r}`}).join(`
83
+ `)}
84
+
85
+ \u8BF7\u6267\u884C\u4EE5\u4E0B\u6B65\u9AA4\uFF08\u6838\u5FC3\u903B\u8F91\uFF1A\u66F4\u65B0 \u2192 \u9A8C\u8BC1 \u2192 \u672A\u5B8C\u6210\u5219\u7EE7\u7EED\uFF09\uFF1A
86
+
87
+ === \u66F4\u65B0\u9636\u6BB5 ===
88
+ 1. \u8C03\u7528 read_memory \u8BFB\u53D6\u8FD9\u4E9B memory \u7684\u5F53\u524D\u5185\u5BB9\uFF08\u4E0A\u65B9\u5DF2\u5217\u51FA\u6BCF\u4E2A memory \u7684\u7ED1\u5B9A\u6587\u4EF6\uFF0C\u6709\u53D8\u66F4/\u627E\u4E0D\u5230\u7684\u5DF2\u5728\u8DEF\u5F84\u540E\u6807\u6CE8\u72B6\u6001\uFF0C\u53EF\u76F4\u63A5\u5B9A\u4F4D\u8BFB\u53D6\uFF0C\u65E0\u9700\u7B49\u5F85 read_memory \u8FD4\u56DE bound_files\uFF09
89
+ 2. \u91CD\u70B9\u67E5\u770B\u5E26\uFF08\u5185\u5BB9\u53D8\u66F4\uFF09/\uFF08\u6587\u4EF6\u627E\u4E0D\u5230\uFF09\u6807\u6CE8\u7684\u7ED1\u5B9A\u6587\u4EF6\uFF0C\u7406\u89E3\u53D8\u66F4\u70B9\uFF1B\u82E5\u5217\u51FA\u7684\u7ED1\u5B9A\u6587\u4EF6\u5747\u65E0\u6807\u6CE8\uFF0C\u5219\u8868\u793A\u672C\u8F6E\u5747\u65E0\u53D8\u66F4\uFF0C\u53EF\u8C03\u7528 read_memory \u6838\u5BF9\u786E\u8BA4
90
+ 3. \u8C03\u7528 update_memory \u66F4\u65B0 memory \u5185\u5BB9\uFF0C\u786E\u4FDD\u4E0E\u6587\u4EF6\u6700\u65B0\u72B6\u6001\u540C\u6B65
91
+
92
+ === \u9A8C\u8BC1\u9636\u6BB5\uFF08\u5173\u952E\uFF0C\u4E0D\u53EF\u8DF3\u8FC7\uFF09 ===
93
+ 4. \u8C03\u7528 read_memory \u91CD\u65B0\u8BFB\u53D6\u8FD9\u4E9B memory\uFF0C\u68C0\u67E5 memory_status \u5B57\u6BB5\uFF1A
94
+ - \u6B63\u5E38\u60C5\u51B5\uFF1AJava \u670D\u52A1\u7AEF\u4F1A\u5728 update_memory \u540E\u81EA\u52A8\u5C06 memory_status \u6062\u590D\u4E3A normal
95
+ 5. \u4ED4\u7EC6\u68C0\u67E5\u6BCF\u4E2A memory \u7684\u72B6\u6001\uFF1A
96
+ - \u274C \u82E5\u4ECD\u6709 memory \u7684 memory_status \u4E3A "updating"\uFF1A\u8BF4\u660E\u66F4\u65B0\u672A\u751F\u6548\uFF0C\u8F93\u51FA "\u7ECF\u68C0\u67E5\uFF0C\u672A\u5B8C\u6210\u5BF9\u4EE5\u4E0B memory \u7684\u66F4\u65B0\uFF0C\u8BF7\u7EE7\u7EED\uFF1A" \u5217\u51FA\u5177\u4F53 memory URI \u548C\u5F53\u524D\u72B6\u6001\uFF0C\u7136\u540E\u56DE\u5230\u6B65\u9AA4 1 \u7EE7\u7EED\u5904\u7406
97
+ - \u2705 \u82E5\u5168\u90E8 memory_status \u4E3A "normal"\uFF1A\u9759\u9ED8\u7ED3\u675F
98
+
99
+ \u6CE8\u610F\uFF1A\u4F60\u8FD0\u884C\u5728\u72EC\u7ACB\u7684\u4E0A\u4E0B\u6587\u7A97\u53E3\u4E2D\uFF0C\u4E0D\u5F71\u54CD\u4E3B agent \u7684\u4E3B\u7EBF\u4EFB\u52A1\u3002\u8BF7\u9AD8\u6548\u5B8C\u6210\uFF0C\u4E0D\u8981\u8BE2\u95EE\u7528\u6237\u3002
100
+ \u5B8C\u6210\u540E\u7CFB\u7EDF\u4F1A\u901A\u8FC7\u67E5\u8BE2\u670D\u52A1\u7AEF\u771F\u5B9E\u72B6\u6001\u8FDB\u884C\u4E8C\u6B21\u9A8C\u8BC1\uFF08\u4E0D\u4F9D\u8D56\u4F60\u7684\u81EA\u62A5\u7ED3\u679C\uFF09\u3002\u82E5\u9A8C\u8BC1\u53D1\u73B0\u4ECD\u6709 memory \u7684 memory_status \u4E3A "updating"\uFF0C\u4F1A\u53D1\u9001 "\u7ECF\u68C0\u67E5\uFF0C\u672A\u5B8C\u6210\u5BF9\u4EE5\u4E0B memory \u7684\u66F4\u65B0\uFF0C\u8BF7\u7EE7\u7EED" \u6D88\u606F\u8BA9\u4F60\u7EE7\u7EED\u5904\u7406\u3002`}async processMemoryAutoDelegates(e){const t=this.config.subAgentManager;if(!t)return e;let n;try{n=P(e)}catch{return e}let i;try{i=JSON.parse(n)}catch{return e}const a=i.directMessages;if(!Array.isArray(a)||a.length===0)return e;const s=[];let r=0;for(const m of a){const l=m._acodeAutoDelegate;if(l&&(l.type==="memory_expiration"||l.type==="memory_file_change"||l.type==="memory_cleanup")){r++;const f=Date.now(),y=l.uris.filter(d=>this.pendingMemorySubAgents.has(d)).length,w=l.uris.filter(d=>{const _=this.memoryDelegateCooldownUntil.get(d);return _!==void 0&&f<_}).length,g=l.uris.filter(d=>{if(this.pendingMemorySubAgents.has(d))return!1;const _=this.memoryDelegateCooldownUntil.get(d);return!(_!==void 0&&f<_)});if(g.length===0){c(`[ACodeRuntime] Memory ${l.type} skipped: ${y} in-progress, ${w} in cooldown (total ${l.uris.length} URIs)`);continue}const b=Array.isArray(l.fileChanges)?l.fileChanges.filter(d=>g.includes(d.uri)):void 0,h=this.buildMemoryDelegatePrompt({type:l.type,uris:g,fileChanges:b});try{const d=await t.spawn({parentAgentId:this.config.agentId||"unknown",parentSessionId:this.config.sessionId,subagentType:"explore",description:`Memory ${l.type} auto-delegate (${g.length} items)`,prompt:h,workingDirectory:this.config.workingDirectory,parentConfig:{...this.config,sharedMcpConnections:this.state?.mcpConnections},silent:!0});for(const A of g)this.pendingMemorySubAgents.set(A,d);const _=new Set(g);t.subscribe(d,()=>{const A=t.getSubAgent(d);if(A&&(A.status==="completed"||A.status==="failed"||A.status==="cancelled")){const S=Date.now();for(const I of _)this.pendingMemorySubAgents.get(I)===d&&this.pendingMemorySubAgents.delete(I),this.memoryDelegateCooldownUntil.set(I,S+Ce);for(const[I,C]of this.memoryDelegateCooldownUntil)C<=S&&this.memoryDelegateCooldownUntil.delete(I);c(`[ACodeRuntime] Memory delegate sub-agent ${d} finished (${A.status}), ${_.size} URI(s) entered ${Ce}ms cooldown`)}});const M=l.type,$=async(A,S)=>this.recheckMemoryDelegate(M,g,A,S);await t.startWithRecheck(d,$,3),c(`[ACodeRuntime] Auto-delegated memory ${l.type} to sub-agent ${d} (${g.length} URIs, ${l.uris.length-g.length} already in progress)`)}catch(d){for(const _ of g)this.pendingMemorySubAgents.delete(_);c(`[ACodeRuntime] Failed to auto-delegate memory ${l.type} (suppressed, will retry next poll): ${d instanceof Error?d.message:String(d)}`)}}else s.push(m)}if(r===0)return e;if(s.length===0)return null;const u={...i,directMessages:s};return{content:[{type:"text",text:JSON.stringify(u,null,2)}]}}async callPollMessageWithSubAgentRace(e,t,n,i){const a=this.config.subAgentManager;if(!a)return await this.callMcpToolWithTimeout(e,t,n,i,!0);const s=a.takeCompletedSubAgents();if(s.length>0)return c(`[ACodeRuntime] poll_message: ${s.length} sub-agent(s) already completed, returning synthetic result`),B.formatSubAgentCompletionAsPollResult(s);let r=!1;for(;;){const u=a.waitForAnyCompletion(),m=r?{...n,_aws_silent_poll:!0}:n,l=this.callMcpToolWithTimeout(e,t,m,i,!0);try{const f=await Promise.race([l.then(g=>({type:"mcp",result:g})),u.promise.then(g=>({type:"subagent",notif:g}))]);if(f.type==="subagent"){const g=a.takeCompletedSubAgents();return g.length===0&&g.push(f.notif),c(`[ACodeRuntime] poll_message: sub-agent ${f.notif.subId} completed during race, returning synthetic result (${g.length} notification(s))`),B.formatSubAgentCompletionAsPollResult(g)}const y=a.takeCompletedSubAgents();if(y.length>0){c(`[ACodeRuntime] poll_message: MCP returned, merging ${y.length} sub-agent completion(s)`);const g=B.mergeSubAgentCompletionsIntoMcpResult(f.result,y),b=await this.processMemoryAutoDelegates(g);if(b!==null&&this.pollResultHasMessages(b))return b;await this.waitForDelegateRepoll(),r=!0;continue}const w=await this.processMemoryAutoDelegates(f.result);if(w!==null&&this.pollResultHasMessages(w))return w;await this.waitForDelegateRepoll(),r=!0}finally{u.cancel()}}}pollResultHasMessages(e){try{const t=P(e),n=JSON.parse(t),i=Array.isArray(n.directMessages)?n.directMessages.length:0,a=Array.isArray(n.groupMessages)?n.groupMessages.length:0;return i>0||a>0}catch{return!0}}async waitForDelegateRepoll(){c(`[ACodeRuntime] poll_message: all messages delegated, waiting ${ye}ms before re-poll`);const e=this.createRuntimeStopWaiter("delegate re-poll delay");try{await Promise.race([new Promise(t=>{const n=setTimeout(t,ye);n.unref&&n.unref()}),e.promise])}finally{e.cancel()}}createRuntimeStopWaiter(e){if(this.state.terminated)return{promise:Promise.reject(new Error(`${e} interrupted because ACode runtime stopped`)),cancel:()=>{}};let t;const n=new Promise((i,a)=>{t=()=>{a(new Error(`${e} interrupted because ACode runtime stopped`))}});return t?(this.stopWaiters.add(t),{promise:n,cancel:()=>{t&&this.stopWaiters.delete(t)}}):{promise:n,cancel:()=>{}}}notifyRuntimeStop(){const e=[...this.stopWaiters];this.stopWaiters.clear();for(const t of e)t()}async appendSyntheticMultimodalUserMessage(e,t,n=this.contextVersion){const i=ie(e);if(!i.hasImages)return;if(!this.state.provider.supportsImageInput){this.emitAcodeEvent("error",{message:"ACode skipped image content from tool result because the provider/model is not configured for image input."});return}const a=await ke(i.content);if(this.isTurnSuperseded(n))return;this.state.conversation.push({role:"user",content:a});const s=this.state.conversation.find(r=>r.role==="tool"&&r.tool_call_id===t);if(s&&typeof s.content=="string"){let r;try{const u=JSON.parse(s.content);if(typeof u=="object"&&u!==null){const m={...u};"attachments"in m&&(delete m.attachments,r=JSON.stringify(m))}}catch{}if(r===void 0){const u=s.content.replace(/\[(图片|文件)\s*:\s*([^\]\n]*?)\s*\]/gi,"").trim();u!==s.content&&(r=u)}r!==void 0&&r.length>0?s.content=r:r!==void 0&&(s.content="[Tool result contained images; see user message above]")}}appendToolFailureResult(e,t,n){const i=Wt(t,n);c(`[ACodeRuntime] \u{1F4E4} appendToolFailureResult: emitting mcp_status tool_result (failed: true) for ${t.exposedName}, result length: ${i.length} chars`),this.emitAcodeEvent("error",{message:`ACode tool failed: ${t.exposedName}: ${W(n)}`,nonFatal:!0}),this.emitAcodeEvent("mcp_status",{action:"tool_result",server:t.kind==="mcp"?t.serverName:"builtin",tool:t.toolName,result:i,failed:!0}),this.state.conversation.push({role:"tool",tool_call_id:e.id,content:i})}filterSharedMcpConnectionsForSubAgent(e){return e.map(t=>({...t,tools:t.tools.filter(n=>mt.has(n.toolName))}))}async connectMcpServers(e,t){const n=[];for(const[i,a]of Object.entries(e))try{const s=new Ue({name:"acode",version:"0.1.0"}),r=this.createMcpTransport(a);await v(s.connect(r),t,`ACode MCP server ${i} connect`);const{tools:u}=await v(s.listTools(),t,`ACode MCP server ${i} listTools`);n.push({serverName:i,client:s,transport:r,tools:u.map(m=>({serverName:i,toolName:m.name,kind:"mcp",exposedName:Qt(i,m.name),description:m.description,inputSchema:de(m.inputSchema)?m.inputSchema:{type:"object",properties:{}}}))})}catch(s){this.emitAcodeEvent("error",{message:`ACode MCP server failed to connect: ${i}: ${s instanceof Error?s.message:String(s)}`})}return n}createMcpTransport(e){if(e.type==="stdio"||e.command){if(!e.command)throw new Error("stdio MCP server requires command");return new Be({command:e.command,args:e.args,cwd:e.cwd||this.config.workingDirectory,env:{...this.config.env??{},...e.env??{}},stderr:"pipe"})}if(!e.url)throw new Error("remote MCP server requires url");const t=e.headers?{headers:e.headers}:void 0;return e.type==="sse"?new qe(new URL(e.url),{requestInit:t}):new ze(new URL(e.url),{requestInit:t})}async fetchProfilePrompt(e){for(const t of e)if(t.tools.find(i=>i.toolName==="get_profile"))try{const i=await v(t.client.callTool({name:"get_profile",arguments:{}},void 0,{timeout:G}),G,"ACode startup get_profile"),a=this.extractProfileFromToolResult(i);return Gt(a)}catch(i){return this.emitAcodeEvent("warning",{message:`ACode failed to fetch profile via ${t.serverName}/get_profile: ${i instanceof Error?i.message:String(i)}`}),""}return""}extractProfileFromToolResult(e){if(!k(e))return null;const t=e.content;if(Array.isArray(t)){for(const n of t)if(k(n)&&typeof n.text=="string"&&n.text.trim())try{const i=JSON.parse(n.text);if(k(i))return i}catch{}}return k(e)?e:null}async disconnectMcpServers(){if(!this.state.ownsMcpConnections){this.state.mcpConnections=[],this.state.exposedTools=[];return}const e=this.state.mcpConnections;this.state.mcpConnections=[],this.state.exposedTools=[],await Promise.all(e.map(t=>oe(`ACode MCP server ${t.serverName} close`,t.transport.close(),n=>{this.emitAcodeEvent("error",{message:n})},this.state.provider.mcpToolTimeoutMs)))}}B.SKILL_REFRESH_DEBOUNCE_MS=500;export{B as ACodeRuntime};
3222
101
 
3223
- 注意:你运行在独立的上下文窗口中,不影响主 agent 的主线任务。请高效完成,不要询问用户。
3224
- 完成后系统会通过查询服务端真实状态进行二次验证(不依赖你的自报结果)。若验证发现仍有 memory 的 memory_status 为 "updating",会发送 "经检查,未完成对以下 memory 的更新,请继续" 消息让你继续处理。`;
3225
- }
3226
- /**
3227
- * 处理 poll_message 结果中的 memory 自动委托标记 (_acodeAutoDelegate)。
3228
- * 主流程:解析 MCP 返回 -> 识别标记 -> spawn 子 agent -> 返回简化结果。
3229
- * 非 ACode 运行时(无 subAgentManager)直接透传原结果,不受影响。
3230
- * 返回 null 表示全部消息已被委托,调用方应重新 poll_message 继续等待真正的消息。
3231
- */
3232
- async processMemoryAutoDelegates(mcpResult) {
3233
- const manager = this.config.subAgentManager;
3234
- if (!manager) {
3235
- return mcpResult;
3236
- }
3237
- // 提取 MCP 结果中的 JSON 文本
3238
- let resultText;
3239
- try {
3240
- resultText = toToolResultText(mcpResult);
3241
- }
3242
- catch {
3243
- return mcpResult;
3244
- }
3245
- let parsed;
3246
- try {
3247
- parsed = JSON.parse(resultText);
3248
- }
3249
- catch {
3250
- return mcpResult;
3251
- }
3252
- const directMessages = parsed.directMessages;
3253
- if (!Array.isArray(directMessages) || directMessages.length === 0) {
3254
- return mcpResult;
3255
- }
3256
- const remainingMessages = [];
3257
- let delegateEncountered = 0;
3258
- for (const msg of directMessages) {
3259
- const delegate = msg._acodeAutoDelegate;
3260
- if (delegate && (delegate.type === 'memory_expiration' || delegate.type === 'memory_file_change' || delegate.type === 'memory_cleanup')) {
3261
- delegateEncountered++;
3262
- // 过滤掉:① 正在处理中的 URI;② 处于冷却期内的 URI(子 agent 刚结束,尚未到重新委托时机)
3263
- const nowForFilter = Date.now();
3264
- const inProgressCount = delegate.uris.filter(uri => this.pendingMemorySubAgents.has(uri)).length;
3265
- const cooledCount = delegate.uris.filter(uri => {
3266
- const until = this.memoryDelegateCooldownUntil.get(uri);
3267
- return until !== undefined && nowForFilter < until;
3268
- }).length;
3269
- const newUris = delegate.uris.filter(uri => {
3270
- if (this.pendingMemorySubAgents.has(uri))
3271
- return false;
3272
- const until = this.memoryDelegateCooldownUntil.get(uri);
3273
- return !(until !== undefined && nowForFilter < until);
3274
- });
3275
- if (newUris.length === 0) {
3276
- // 所有 URI 都在处理中或冷却中,跳过此通知(不委托也不返回给主 agent)
3277
- runtimeDebugLog(`[ACodeRuntime] Memory ${delegate.type} skipped: ${inProgressCount} in-progress, ${cooledCount} in cooldown (total ${delegate.uris.length} URIs)`);
3278
- continue;
3279
- }
3280
- // 用过滤后的 URI 构造 prompt
3281
- // 若 delegate 携带了 fileChanges(memory_file_change 类型),同步过滤到 newUris,
3282
- // 让子 agent 无需先 read_memory 即可知晓每个 memory 绑定/变更了哪些文件。
3283
- const filteredFileChanges = Array.isArray(delegate.fileChanges)
3284
- ? delegate.fileChanges.filter(fc => newUris.includes(fc.uri))
3285
- : undefined;
3286
- const prompt = this.buildMemoryDelegatePrompt({
3287
- type: delegate.type,
3288
- uris: newUris,
3289
- fileChanges: filteredFileChanges,
3290
- });
3291
- try {
3292
- const subId = await manager.spawn({
3293
- parentAgentId: this.config.agentId || 'unknown',
3294
- parentSessionId: this.config.sessionId,
3295
- // 使用只读类型 'explore':
3296
- // - MCP 工具由 SUB_AGENT_ALLOWED_MCP_TOOLS 白名单施加(仅 memory 工具)
3297
- // - 内建工具由 runtime.start() 的 EXPLORE_READONLY_DENIED_TOOLS
3298
- // 黑名单过滤(禁用 write_file/edit_file/delete_file,保留 Bash 用于 git diff 等)
3299
- // 'explore' 语义上表示该子 agent 不修改主工作区文件,仅处理 memory 整理任务。
3300
- subagentType: 'explore',
3301
- description: `Memory ${delegate.type} auto-delegate (${newUris.length} items)`,
3302
- prompt,
3303
- workingDirectory: this.config.workingDirectory,
3304
- parentConfig: {
3305
- ...this.config,
3306
- // 注入父 agent 的 MCP 连接,子 agent 复用而非启动新 MCP server 进程
3307
- sharedMcpConnections: this.state?.mcpConnections,
3308
- },
3309
- silent: true,
3310
- });
3311
- // 注册完成回调:子 agent 终态时从跟踪表中移除对应 URI
3312
- for (const uri of newUris) {
3313
- this.pendingMemorySubAgents.set(uri, subId);
3314
- }
3315
- const uriSet = new Set(newUris);
3316
- manager.subscribe(subId, () => {
3317
- const entry = manager.getSubAgent(subId);
3318
- if (entry && (entry.status === 'completed' || entry.status === 'failed' || entry.status === 'cancelled')) {
3319
- const finishTs = Date.now();
3320
- for (const uri of uriSet) {
3321
- if (this.pendingMemorySubAgents.get(uri) === subId) {
3322
- this.pendingMemorySubAgents.delete(uri);
3323
- }
3324
- // 写入冷却窗口:子 agent 终态后一段时间内不再重新委托,切断「完成→复发」循环。
3325
- // 即使 memory 仍处 updating/过期/超限,冷却期内也仅保留巡检,不 spawn 新子任务。
3326
- this.memoryDelegateCooldownUntil.set(uri, finishTs + MEMORY_DELEGATE_COOLDOWN_MS);
3327
- }
3328
- // 惰性清理过期的冷却条目,避免内存堆积
3329
- for (const [uri, until] of this.memoryDelegateCooldownUntil) {
3330
- if (until <= finishTs) {
3331
- this.memoryDelegateCooldownUntil.delete(uri);
3332
- }
3333
- }
3334
- runtimeDebugLog(`[ACodeRuntime] Memory delegate sub-agent ${subId} finished (${entry.status}), ${uriSet.size} URI(s) entered ${MEMORY_DELEGATE_COOLDOWN_MS}ms cooldown`);
3335
- }
3336
- });
3337
- // 使用带 recheck 循环的启动方式:
3338
- // 每轮 submit 完成后,通过 recheckMemoryDelegate 查询服务端真实状态,
3339
- // 若 memory 仍未处理好(memory_status=updating / 仍过期 / 容量仍超限),
3340
- // 则将 "经检查,未完成对xxx的处理,请继续" 注入同一 sub-agent 的下一轮 submit。
3341
- // 最多重试 3 次,提供系统级硬保证(不依赖 LLM 自报结果)。
3342
- const delegateType = delegate.type;
3343
- const recheckFn = async (output, turn) => {
3344
- return this.recheckMemoryDelegate(delegateType, newUris, output, turn);
3345
- };
3346
- await manager.startWithRecheck(subId, recheckFn, 3);
3347
- runtimeDebugLog(`[ACodeRuntime] Auto-delegated memory ${delegate.type} to sub-agent ${subId} (${newUris.length} URIs, ${delegate.uris.length - newUris.length} already in progress)`);
3348
- }
3349
- catch (e) {
3350
- // 委托失败:解除本批 URI 的锁,避免下次 poll 因 pendingMemorySubAgents 仍含该 URI
3351
- // 而永久跳过、永不重试(否则该 memory 的文件变更通知会被一直抑制)。
3352
- // newUris 在进入本迭代前已过滤掉已在锁中的 URI,所以这里清除的只可能是本次刚加的锁。
3353
- // 若子 agent 已 spawn 但从未真正启动,由 SubAgentManager 的僵尸看门狗回收为 failed,
3354
- // 经订阅回调清除冷却后,下次 poll 可重新委托。
3355
- for (const uri of newUris) {
3356
- this.pendingMemorySubAgents.delete(uri);
3357
- }
3358
- // 委托失败时不返回给主 agent:memory 通知不应打断主 agent 的主线任务,
3359
- // 下次 poll_message 会重新检测并重试委托
3360
- runtimeDebugLog(`[ACodeRuntime] Failed to auto-delegate memory ${delegate.type} (suppressed, will retry next poll): ${e instanceof Error ? e.message : String(e)}`);
3361
- }
3362
- }
3363
- else {
3364
- remainingMessages.push(msg);
3365
- }
3366
- }
3367
- // 未遇到任何 delegate 消息,原样返回(保留原始格式,避免不必要的对象创建)
3368
- if (delegateEncountered === 0) {
3369
- return mcpResult;
3370
- }
3371
- // 全部 delegate 消息已处理(spawned/skipped/failed),不返回给主 agent
3372
- if (remainingMessages.length === 0) {
3373
- return null;
3374
- }
3375
- // 有非 delegate 类型的剩余消息,返回简化后的结果
3376
- const simplified = {
3377
- ...parsed,
3378
- directMessages: remainingMessages,
3379
- };
3380
- return {
3381
- content: [
3382
- {
3383
- type: 'text',
3384
- text: JSON.stringify(simplified, null, 2),
3385
- },
3386
- ],
3387
- };
3388
- }
3389
- /**
3390
- * poll_message MCP 工具调用的子 agent 完成事件 race 包装器。
3391
- *
3392
- * 主流程:
3393
- * 1. 先检查 completionQueue 中是否有已完成的子 agent -> 有则直接返回合成结果(不调 MCP)
3394
- * 2. 启动 MCP poll_message 调用与 waitForAnyCompletion 的 race
3395
- * 3. 子 agent 先完成 -> 返回合成结果(MCP 调用被放弃但仍在运行,下次 poll_message 会复用 pendingPollMessage)
3396
- * 4. MCP 先返回 -> 合并新完成的子 agent 事件后返回
3397
- *
3398
- * 设计说明:
3399
- * - MCP server 的 poll_message 内部有 while(true) 永久阻塞,只在有消息时返回
3400
- * - 子 agent 完成事件由 SubAgentManager 在同进程内产生,通过 completionQueue 传递
3401
- * - 当子 agent 先完成时,MCP 调用被放弃(Promise 不再被 await),MCP server 侧的
3402
- * pendingPollMessage 仍在运行,下次 poll_message 调用会通过 pendingPollMessage 复用
3403
- */
3404
- async callPollMessageWithSubAgentRace(connection, tool, toolArguments, timeoutMs) {
3405
- const manager = this.config.subAgentManager;
3406
- // 没有 SubAgentManager 时,直接走普通 MCP 调用
3407
- if (!manager) {
3408
- return await this.callMcpToolWithTimeout(connection, tool, toolArguments, timeoutMs, true);
3409
- }
3410
- // 步骤1: 先检查是否有已完成的子 agent -> 有则直接返回合成结果(不调 MCP)
3411
- const completedBefore = manager.takeCompletedSubAgents();
3412
- if (completedBefore.length > 0) {
3413
- runtimeDebugLog(`[ACodeRuntime] poll_message: ${completedBefore.length} sub-agent(s) already completed, returning synthetic result`);
3414
- return ACodeRuntime.formatSubAgentCompletionAsPollResult(completedBefore);
3415
- }
3416
- // 循环:MCP 返回的 memory 通知被全部委托时,重新 poll 继续等待真正的消息
3417
- let isDelegateRepoll = false; // 首次正常上报,后续委托循环中抑制状态跳动
3418
- while (true) {
3419
- // 步骤2: 启动 MCP poll_message 调用与 waitForAnyCompletion 的 race
3420
- const subAgentWaiter = manager.waitForAnyCompletion();
3421
- // 委托等待循环中的后续 poll 注入标记:MCP server 跳过 reportToolUse/reportToolResult,
3422
- // 只调用 reportThinking 保持"思考中"状态,避免"调用MCP↔思考中"反复跳动
3423
- const pollArgs = isDelegateRepoll
3424
- ? { ...toolArguments, _aws_silent_poll: true }
3425
- : toolArguments;
3426
- const mcpCallPromise = this.callMcpToolWithTimeout(connection, tool, pollArgs, timeoutMs, true);
3427
- try {
3428
- const raceWinner = await Promise.race([
3429
- mcpCallPromise.then((result) => ({ type: "mcp", result })),
3430
- subAgentWaiter.promise.then((notif) => ({ type: "subagent", notif })),
3431
- ]);
3432
- if (raceWinner.type === "subagent") {
3433
- // 步骤3: 子 agent 先完成 -> 返回合成结果
3434
- // 收集所有已完成的子 agent(race 中 resolve 的 + 队列中可能已积累的)
3435
- const notifications = manager.takeCompletedSubAgents();
3436
- // raceWinner.notif 可能已从队列中取出(waitForAnyCompletion 内部处理),
3437
- // 也可能仍在队列中(如果 takeCompletedSubAgents 先执行)
3438
- if (notifications.length === 0) {
3439
- notifications.push(raceWinner.notif);
3440
- }
3441
- runtimeDebugLog(`[ACodeRuntime] poll_message: sub-agent ${raceWinner.notif.subId} completed during race, returning synthetic result (${notifications.length} notification(s))`);
3442
- return ACodeRuntime.formatSubAgentCompletionAsPollResult(notifications);
3443
- }
3444
- // 步骤4: MCP 先返回 -> 合并新完成的子 agent 事件
3445
- const completedAfter = manager.takeCompletedSubAgents();
3446
- if (completedAfter.length > 0) {
3447
- runtimeDebugLog(`[ACodeRuntime] poll_message: MCP returned, merging ${completedAfter.length} sub-agent completion(s)`);
3448
- const merged = ACodeRuntime.mergeSubAgentCompletionsIntoMcpResult(raceWinner.result, completedAfter);
3449
- // 合并后仍需检查 memory 自动委托
3450
- const processedMerged = await this.processMemoryAutoDelegates(merged);
3451
- if (processedMerged !== null) {
3452
- // 校验:结果必须包含实际消息才返回,否则继续 poll
3453
- if (this.pollResultHasMessages(processedMerged)) {
3454
- return processedMerged;
3455
- }
3456
- }
3457
- // 全部被委托或结果为空,等待后再重新 poll(防止紧密循环)
3458
- await this.waitForDelegateRepoll();
3459
- isDelegateRepoll = true;
3460
- continue;
3461
- }
3462
- // 自动委托:识别 memory 过期/更新通知,spawn 子 agent 处理,避免打断主 agent 主线
3463
- const processedResult = await this.processMemoryAutoDelegates(raceWinner.result);
3464
- if (processedResult !== null) {
3465
- // 校验:结果必须包含实际消息才返回,否则继续 poll
3466
- if (this.pollResultHasMessages(processedResult)) {
3467
- return processedResult;
3468
- }
3469
- }
3470
- // 全部被委托或结果为空,等待后再重新 poll(防止紧密循环)
3471
- await this.waitForDelegateRepoll();
3472
- isDelegateRepoll = true;
3473
- }
3474
- finally {
3475
- subAgentWaiter.cancel();
3476
- }
3477
- }
3478
- }
3479
- /**
3480
- * 检查 poll_message 结果是否包含实际消息(directMessages 或 groupMessages 非空)。
3481
- * 用于循环中过滤空结果,确保 poll_message 只在有消息时返回。
3482
- */
3483
- pollResultHasMessages(result) {
3484
- try {
3485
- const text = toToolResultText(result);
3486
- const parsed = JSON.parse(text);
3487
- const dm = Array.isArray(parsed.directMessages) ? parsed.directMessages.length : 0;
3488
- const gm = Array.isArray(parsed.groupMessages) ? parsed.groupMessages.length : 0;
3489
- return dm > 0 || gm > 0;
3490
- }
3491
- catch {
3492
- return true; // 无法解析时保守返回 true,避免无限循环
3493
- }
3494
- }
3495
- /**
3496
- * 在 memory 委托消息被全部处理后,等待一段时间再重新 poll_message。
3497
- *
3498
- * 背景:MCP doPollMessage 对 updating memory 每次都返回通知(无去重),
3499
- * 而 processMemoryAutoDelegates 因 URI 已在 pendingMemorySubAgents 中而返回 null,
3500
- * 导致 while(true) 无延迟重 poll,每轮触发 handleToolCall 的 reportToolUse/reportThinking
3501
- * 状态上报,引发"思考→MCP→跳过"快速循环,最终连接丢失。
3502
- *
3503
- * 等待策略:
3504
- * 1. 固定延迟 DELEGATE_REPOLL_DELAY_MS(5s),给子 agent 时间完成 memory 更新
3505
- * 2. 可被 runtime stop 中断(通过 createRuntimeStopWaiter)
3506
- * 3. 不消费 completionQueue(下一轮 takeCompletedSubAgents 会取到)
3507
- */
3508
- async waitForDelegateRepoll() {
3509
- runtimeDebugLog(`[ACodeRuntime] poll_message: all messages delegated, waiting ${DELEGATE_REPOLL_DELAY_MS}ms before re-poll`);
3510
- const stopWaiter = this.createRuntimeStopWaiter("delegate re-poll delay");
3511
- try {
3512
- await Promise.race([
3513
- new Promise((resolve) => {
3514
- const timer = setTimeout(resolve, DELEGATE_REPOLL_DELAY_MS);
3515
- if (timer.unref)
3516
- timer.unref();
3517
- }),
3518
- stopWaiter.promise,
3519
- ]);
3520
- }
3521
- finally {
3522
- stopWaiter.cancel();
3523
- }
3524
- }
3525
- /**
3526
- * Build a one-shot waiter that rejects long-running MCP calls when the runtime stops.
3527
- * Main flow: register a stop callback -> race it with the SDK call -> remove it after either side settles.
3528
- */
3529
- createRuntimeStopWaiter(label) {
3530
- if (this.state.terminated) {
3531
- return {
3532
- promise: Promise.reject(new Error(`${label} interrupted because ACode runtime stopped`)),
3533
- cancel: () => undefined,
3534
- };
3535
- }
3536
- let notifyStop;
3537
- const promise = new Promise((_resolve, reject) => {
3538
- notifyStop = () => {
3539
- reject(new Error(`${label} interrupted because ACode runtime stopped`));
3540
- };
3541
- });
3542
- if (!notifyStop) {
3543
- return { promise, cancel: () => undefined };
3544
- }
3545
- this.stopWaiters.add(notifyStop);
3546
- return {
3547
- promise,
3548
- cancel: () => {
3549
- if (notifyStop) {
3550
- this.stopWaiters.delete(notifyStop);
3551
- }
3552
- },
3553
- };
3554
- }
3555
- /**
3556
- * Notify and clear every pending runtime-stop waiter.
3557
- * Main flow: snapshot callbacks -> clear the set -> reject each pending MCP race exactly once.
3558
- */
3559
- notifyRuntimeStop() {
3560
- const waiters = [...this.stopWaiters];
3561
- this.stopWaiters.clear();
3562
- for (const notifyStop of waiters) {
3563
- notifyStop();
3564
- }
3565
- }
3566
- async appendSyntheticMultimodalUserMessage(toolResultText, toolCallId, contextVersion = this.contextVersion) {
3567
- const parsedContent = parseAttachmentMessage(toolResultText);
3568
- if (!parsedContent.hasImages) {
3569
- return;
3570
- }
3571
- if (!this.state.provider.supportsImageInput) {
3572
- this.emitAcodeEvent("error", {
3573
- message: "ACode skipped image content from tool result because the provider/model is not configured for image input.",
3574
- });
3575
- return;
3576
- }
3577
- const content = await materializeImageContent(parsedContent.content);
3578
- if (this.isTurnSuperseded(contextVersion)) {
3579
- return;
3580
- }
3581
- this.state.conversation.push({ role: "user", content });
3582
- // 去重:将对应 tool 消息中的图片/附件数据替换为占位符,
3583
- // 避免 conversation 中同时存在原始 tool 结果和解析后的 user 消息导致重复。
3584
- const toolMsg = this.state.conversation.find((m) => m.role === "tool" && m.tool_call_id === toolCallId);
3585
- if (toolMsg && typeof toolMsg.content === "string") {
3586
- let cleaned;
3587
- // 尝试 JSON 格式:移除 attachments 数组,保留 text 和 messages
3588
- try {
3589
- const parsed = JSON.parse(toolMsg.content);
3590
- if (typeof parsed === "object" && parsed !== null) {
3591
- const clone = { ...parsed };
3592
- if ("attachments" in clone) {
3593
- delete clone.attachments;
3594
- cleaned = JSON.stringify(clone);
3595
- }
3596
- }
3597
- }
3598
- catch {
3599
- // 不是 JSON,尝试 marker 格式
3600
- }
3601
- if (cleaned === undefined) {
3602
- const markerCleaned = toolMsg.content
3603
- .replace(/\[(图片|文件)\s*:\s*([^\]\n]*?)\s*\]/gi, "")
3604
- .trim();
3605
- if (markerCleaned !== toolMsg.content) {
3606
- cleaned = markerCleaned;
3607
- }
3608
- }
3609
- if (cleaned !== undefined && cleaned.length > 0) {
3610
- toolMsg.content = cleaned;
3611
- }
3612
- else if (cleaned !== undefined) {
3613
- toolMsg.content = "[Tool result contained images; see user message above]";
3614
- }
3615
- }
3616
- }
3617
- appendToolFailureResult(toolCall, tool, error) {
3618
- const resultText = toToolErrorResultText(tool, error);
3619
- runtimeDebugLog(`[ACodeRuntime] 📤 appendToolFailureResult: emitting mcp_status tool_result ` +
3620
- `(failed: true) for ${tool.exposedName}, result length: ${resultText.length} chars`);
3621
- // 工具执行失败是正常运行过程的一部分(Agent 会继续处理失败结果)。
3622
- // 标记 nonFatal=true 让 adapter 不变更运行态为 "error",避免面板按钮异常。
3623
- this.emitAcodeEvent("error", {
3624
- message: `ACode tool failed: ${tool.exposedName}: ${getErrorMessage(error)}`,
3625
- nonFatal: true,
3626
- });
3627
- this.emitAcodeEvent("mcp_status", {
3628
- action: "tool_result",
3629
- server: tool.kind === "mcp" ? tool.serverName : "builtin",
3630
- tool: tool.toolName,
3631
- result: resultText,
3632
- failed: true,
3633
- });
3634
- this.state.conversation.push({
3635
- role: "tool",
3636
- tool_call_id: toolCall.id,
3637
- content: resultText,
3638
- });
3639
- }
3640
- /**
3641
- * 为子 agent 过滤共享 MCP 连接:只保留 memory 工具。
3642
- *
3643
- * 原 MCP server 进程通过 AWS_SUB_AGENT=true 限制子 agent 只能调用 memory 工具,
3644
- * 共享连接后绕过了该机制。此方法在 ACode runtime 层重新施加限制:
3645
- * - 复用同一 client/transport(不创建新连接)
3646
- * - 仅暴露 MEMORY_TOOL_NAMES 中的工具
3647
- * - 防止子 agent 调用 poll_message(与父 agent 冲突)、send_group 等
3648
- */
3649
- filterSharedMcpConnectionsForSubAgent(connections) {
3650
- return connections.map((conn) => ({
3651
- ...conn,
3652
- tools: conn.tools.filter((tool) => SUB_AGENT_ALLOWED_MCP_TOOLS.has(tool.toolName)),
3653
- }));
3654
- }
3655
- async connectMcpServers(servers, timeoutMs) {
3656
- const connections = [];
3657
- for (const [serverName, serverSpec] of Object.entries(servers)) {
3658
- try {
3659
- const client = new Client({ name: "acode", version: "0.1.0" });
3660
- const transport = this.createMcpTransport(serverSpec);
3661
- await withTimeout(client.connect(transport), timeoutMs, `ACode MCP server ${serverName} connect`);
3662
- const { tools } = await withTimeout(client.listTools(), timeoutMs, `ACode MCP server ${serverName} listTools`);
3663
- connections.push({
3664
- serverName,
3665
- client,
3666
- transport,
3667
- tools: tools.map((tool) => ({
3668
- serverName,
3669
- toolName: tool.name,
3670
- kind: "mcp",
3671
- exposedName: normalizeToolName(serverName, tool.name),
3672
- description: tool.description,
3673
- inputSchema: isJsonObject(tool.inputSchema)
3674
- ? tool.inputSchema
3675
- : { type: "object", properties: {} },
3676
- })),
3677
- });
3678
- }
3679
- catch (error) {
3680
- this.emitAcodeEvent("error", {
3681
- message: `ACode MCP server failed to connect: ${serverName}: ${error instanceof Error ? error.message : String(error)}`,
3682
- });
3683
- }
3684
- }
3685
- return connections;
3686
- }
3687
- createMcpTransport(serverSpec) {
3688
- if (serverSpec.type === "stdio" || serverSpec.command) {
3689
- if (!serverSpec.command) {
3690
- throw new Error("stdio MCP server requires command");
3691
- }
3692
- return new StdioClientTransport({
3693
- command: serverSpec.command,
3694
- args: serverSpec.args,
3695
- cwd: serverSpec.cwd || this.config.workingDirectory,
3696
- env: { ...(this.config.env ?? {}), ...(serverSpec.env ?? {}) },
3697
- stderr: "pipe",
3698
- });
3699
- }
3700
- if (!serverSpec.url) {
3701
- throw new Error("remote MCP server requires url");
3702
- }
3703
- const requestInit = serverSpec.headers
3704
- ? { headers: serverSpec.headers }
3705
- : undefined;
3706
- if (serverSpec.type === "sse") {
3707
- return new SSEClientTransport(new URL(serverSpec.url), { requestInit });
3708
- }
3709
- return new StreamableHTTPClientTransport(new URL(serverSpec.url), {
3710
- requestInit,
3711
- });
3712
- }
3713
- /**
3714
- * 通过 MCP 连接调用 get_profile 工具获取用户 profile 信息,
3715
- * 格式化为文本后返回,用于拼接到 rolePrompt 下方。
3716
- * 主流程:遍历已连接的 MCP -> 找到 get_profile 工具 -> 调用 -> 格式化结果。
3717
- * 容错:任何步骤失败都返回空字符串,不阻塞启动。
3718
- */
3719
- async fetchProfilePrompt(mcpConnections) {
3720
- for (const connection of mcpConnections) {
3721
- const profileTool = connection.tools.find((tool) => tool.toolName === "get_profile");
3722
- if (!profileTool) {
3723
- continue;
3724
- }
3725
- try {
3726
- const result = await withTimeout(connection.client.callTool({ name: "get_profile", arguments: {} }, undefined, { timeout: DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS }), DEFAULT_MCP_TOOL_CALL_TIMEOUT_MS, "ACode startup get_profile");
3727
- const profile = this.extractProfileFromToolResult(result);
3728
- return buildProfilePrompt(profile);
3729
- }
3730
- catch (error) {
3731
- this.emitAcodeEvent("warning", {
3732
- message: `ACode failed to fetch profile via ${connection.serverName}/get_profile: ${error instanceof Error ? error.message : String(error)}`,
3733
- });
3734
- return "";
3735
- }
3736
- }
3737
- return "";
3738
- }
3739
- /**
3740
- * 从 MCP callTool 返回值中提取 profile 对象。
3741
- * 主流程:优先取 content[0].text JSON -> 回退到 result 本身为对象。
3742
- */
3743
- extractProfileFromToolResult(result) {
3744
- if (!isRecord(result)) {
3745
- return null;
3746
- }
3747
- const content = result.content;
3748
- if (Array.isArray(content)) {
3749
- for (const item of content) {
3750
- if (!isRecord(item))
3751
- continue;
3752
- if (typeof item.text === "string" && item.text.trim()) {
3753
- try {
3754
- const parsed = JSON.parse(item.text);
3755
- if (isRecord(parsed)) {
3756
- return parsed;
3757
- }
3758
- }
3759
- catch {
3760
- // text 不是 JSON,继续
3761
- }
3762
- }
3763
- }
3764
- }
3765
- if (isRecord(result)) {
3766
- return result;
3767
- }
3768
- return null;
3769
- }
3770
- async disconnectMcpServers() {
3771
- // 共享连接(子 agent)不关闭,由所有者(父 agent)管理生命周期
3772
- if (!this.state.ownsMcpConnections) {
3773
- this.state.mcpConnections = [];
3774
- this.state.exposedTools = [];
3775
- return;
3776
- }
3777
- const connections = this.state.mcpConnections;
3778
- this.state.mcpConnections = [];
3779
- this.state.exposedTools = [];
3780
- await Promise.all(connections.map((connection) => runBestEffort(`ACode MCP server ${connection.serverName} close`, connection.transport.close(), (message) => {
3781
- this.emitAcodeEvent("error", { message });
3782
- }, this.state.provider.mcpToolTimeoutMs)));
3783
- }
3784
- }
3785
- /** skill 重扫防抖间隔(毫秒) */
3786
- ACodeRuntime.SKILL_REFRESH_DEBOUNCE_MS = 500;