evolcore 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/CHANGELOG.md +813 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION-0.5.0.md +378 -0
  4. package/README.md +318 -14
  5. package/ROLE_ACCESS_CONTROL.md +174 -0
  6. package/assets/.env.template +4 -0
  7. package/assets/brand/evolcore/README.md +19 -0
  8. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  9. package/assets/brand/evolcore/evolcore-app-icon.svg +13 -0
  10. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  11. package/assets/brand/evolcore/evolcore-brand-board.svg +126 -0
  12. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  13. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  14. package/assets/brand/evolcore/evolcore-logo-reverse.svg +14 -0
  15. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  16. package/assets/brand/evolcore/evolcore-logo.svg +14 -0
  17. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  18. package/assets/brand/evolcore/evolcore-mark.svg +10 -0
  19. package/bin/ec-safe-output.js +161 -0
  20. package/bin/ec.js +29 -0
  21. package/dist/agents/baseagent.js +163 -0
  22. package/dist/agents/claude-runner.js +2385 -0
  23. package/dist/agents/codex-app-server-client.js +448 -0
  24. package/dist/agents/codex-runner.js +2639 -0
  25. package/dist/agents/gemini-runner.js +666 -0
  26. package/dist/agents/runner-types.js +75 -0
  27. package/dist/aun/aid/agentmd.js +216 -0
  28. package/dist/aun/aid/client.js +132 -0
  29. package/dist/aun/aid/control-aid.js +91 -0
  30. package/dist/aun/aid/identity.js +518 -0
  31. package/dist/aun/aid/index.js +4 -0
  32. package/dist/aun/aid/store.js +74 -0
  33. package/dist/aun/aid/types.js +1 -0
  34. package/dist/aun/aid/validation.js +21 -0
  35. package/dist/aun/group-identity.js +10 -0
  36. package/dist/aun/msg/group-index.js +6 -0
  37. package/dist/aun/msg/group.js +1231 -0
  38. package/dist/aun/msg/history.js +123 -0
  39. package/dist/aun/msg/index.js +5 -0
  40. package/dist/aun/msg/p2p.js +393 -0
  41. package/dist/aun/msg/payload-type.js +27 -0
  42. package/dist/aun/msg/upload.js +137 -0
  43. package/dist/aun/outbox.js +160 -0
  44. package/dist/aun/rpc/caller.js +42 -0
  45. package/dist/aun/rpc/connection.js +25 -0
  46. package/dist/aun/rpc/index.js +2 -0
  47. package/dist/aun/service-proxy.js +225 -0
  48. package/dist/aun/storage/download.js +29 -0
  49. package/dist/aun/storage/index.js +3 -0
  50. package/dist/aun/storage/manage.js +10 -0
  51. package/dist/aun/storage/upload.js +68 -0
  52. package/dist/channels/aun.js +4147 -0
  53. package/dist/channels/daemon.js +422 -0
  54. package/dist/channels/dingtalk.js +649 -0
  55. package/dist/channels/feishu.js +1789 -0
  56. package/dist/channels/qqbot.js +409 -0
  57. package/dist/channels/wechat.js +817 -0
  58. package/dist/channels/wecom.js +565 -0
  59. package/dist/cli/agent-command.js +641 -0
  60. package/dist/cli/agent.js +1059 -0
  61. package/dist/cli/aun-commands.js +1948 -0
  62. package/dist/cli/bench.js +1228 -0
  63. package/dist/cli/cli-argv.js +66 -0
  64. package/dist/cli/code-stats.js +329 -0
  65. package/dist/cli/command-log.js +82 -0
  66. package/dist/cli/config-selector.js +69 -0
  67. package/dist/cli/config.js +261 -0
  68. package/dist/cli/ctl-command.js +62 -0
  69. package/dist/cli/daemon-commands.js +2887 -0
  70. package/dist/cli/fs-command.js +1447 -0
  71. package/dist/cli/handoff-command.js +302 -0
  72. package/dist/cli/help.js +31 -0
  73. package/dist/cli/index.js +358 -0
  74. package/dist/cli/init-channel.js +1377 -0
  75. package/dist/cli/init.js +553 -0
  76. package/dist/cli/link-rules.js +240 -0
  77. package/dist/cli/model.js +590 -0
  78. package/dist/cli/net-check.js +723 -0
  79. package/dist/cli/queue-command.js +126 -0
  80. package/dist/cli/response.js +345 -0
  81. package/dist/cli/restart-monitor.js +456 -0
  82. package/dist/cli/stats.js +607 -0
  83. package/dist/cli/task-context.js +80 -0
  84. package/dist/cli/trigger-command.js +505 -0
  85. package/dist/cli/version.js +88 -0
  86. package/dist/cli/watch-logs.js +33 -0
  87. package/dist/cli/watch-msg.js +673 -0
  88. package/dist/config/boot-log.js +266 -0
  89. package/dist/config/builtin-role-templates.js +30 -0
  90. package/dist/config/builtin-roles.js +85 -0
  91. package/dist/config/config-batch-get.js +11 -0
  92. package/dist/config/config-field-policy.js +261 -0
  93. package/dist/config/config-manager.js +916 -0
  94. package/dist/config/config-operation-service.js +384 -0
  95. package/dist/config/contact-book.js +371 -0
  96. package/dist/config/gateway-config.js +858 -0
  97. package/dist/config/lifecycle.js +17 -0
  98. package/dist/config/mention-mode.js +27 -0
  99. package/dist/config/merge.js +161 -0
  100. package/dist/config/owner-policy.js +4 -0
  101. package/dist/config/peer-role-resolver.js +139 -0
  102. package/dist/config/resolved-config-op.js +483 -0
  103. package/dist/config/role-config-v4-startup.js +32 -0
  104. package/dist/config/role-config-v5-startup.js +27 -0
  105. package/dist/config/role-schema.js +105 -0
  106. package/dist/config/role-service.js +159 -0
  107. package/dist/config/role-store.js +204 -0
  108. package/dist/config/roles.js +55 -0
  109. package/dist/config/schema-registry.js +154 -0
  110. package/dist/config/snapshot.js +598 -0
  111. package/dist/config-store.js +503 -0
  112. package/dist/core/auth/agent-delegation.js +111 -0
  113. package/dist/core/auth/auth-gateway.js +166 -0
  114. package/dist/core/auth/authenticated-actor.js +6 -0
  115. package/dist/core/auth/authorization-audit.js +110 -0
  116. package/dist/core/auth/operation-authorizer.js +718 -0
  117. package/dist/core/auth/operation-catalog.js +675 -0
  118. package/dist/core/baseagent-loader.js +54 -0
  119. package/dist/core/bootstrap-service.js +175 -0
  120. package/dist/core/capability/capability-manager.js +316 -0
  121. package/dist/core/capability/providers/claude-capability-provider.js +176 -0
  122. package/dist/core/capability/providers/codex-capability-provider.js +148 -0
  123. package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
  124. package/dist/core/capability/types.js +27 -0
  125. package/dist/core/causation/audit.js +103 -0
  126. package/dist/core/causation/aun-association.js +111 -0
  127. package/dist/core/causation/context.js +93 -0
  128. package/dist/core/causation/index.js +4 -0
  129. package/dist/core/causation/types.js +2 -0
  130. package/dist/core/channel-loader.js +277 -0
  131. package/dist/core/command/agent-control.js +616 -0
  132. package/dist/core/command/cli-intent-parser.js +225 -0
  133. package/dist/core/command/command-handler.js +1638 -0
  134. package/dist/core/command/menu-handler.js +3354 -0
  135. package/dist/core/command/menu-protocol.js +247 -0
  136. package/dist/core/command/role-menu.js +1524 -0
  137. package/dist/core/command/slash-gate.js +148 -0
  138. package/dist/core/command/slash-handler.js +3056 -0
  139. package/dist/core/daemon-file-cache.js +216 -0
  140. package/dist/core/event-bus.js +32 -0
  141. package/dist/core/event-catalog.js +740 -0
  142. package/dist/core/evolagent-registry.js +546 -0
  143. package/dist/core/evolagent.js +331 -0
  144. package/dist/core/handoff/dispatcher.js +229 -0
  145. package/dist/core/handoff/mutex.js +46 -0
  146. package/dist/core/handoff/runtime.js +324 -0
  147. package/dist/core/handoff/store.js +537 -0
  148. package/dist/core/handoff/types.js +12 -0
  149. package/dist/core/inference/text-inference.js +173 -0
  150. package/dist/core/interaction-registration.js +10 -0
  151. package/dist/core/interaction-router.js +278 -0
  152. package/dist/core/message/create-status.js +67 -0
  153. package/dist/core/message/im-renderer.js +657 -0
  154. package/dist/core/message/items-formatter.js +76 -0
  155. package/dist/core/message/logical-queue-bridge.js +123 -0
  156. package/dist/core/message/message-bridge.js +803 -0
  157. package/dist/core/message/message-cache.js +56 -0
  158. package/dist/core/message/message-log.js +339 -0
  159. package/dist/core/message/message-processor.js +4 -0
  160. package/dist/core/message/message-queue.js +1436 -0
  161. package/dist/core/message/message-utils.js +70 -0
  162. package/dist/core/message/peer-mode.js +105 -0
  163. package/dist/core/message/pending-hints.js +232 -0
  164. package/dist/core/message/response-depth.js +33 -0
  165. package/dist/core/message/response-engine.js +3776 -0
  166. package/dist/core/message/response-snapshot.js +83 -0
  167. package/dist/core/message/stream-debouncer.js +130 -0
  168. package/dist/core/message/stream-idle-monitor.js +124 -0
  169. package/dist/core/model/config-scope.js +162 -0
  170. package/dist/core/model/field-scope.js +78 -0
  171. package/dist/core/model/model-catalog.js +227 -0
  172. package/dist/core/model/model-diagnostics.js +182 -0
  173. package/dist/core/model/model-permission.js +90 -0
  174. package/dist/core/permission/approval-gateway.js +1017 -0
  175. package/dist/core/permission/ec-command-parser.js +347 -0
  176. package/dist/core/permission/execution-sandbox.js +16 -0
  177. package/dist/core/permission/index.js +6 -0
  178. package/dist/core/permission/mode.js +24 -0
  179. package/dist/core/permission/sandbox-runtime.js +265 -0
  180. package/dist/core/permission/tool-policy.js +987 -0
  181. package/dist/core/permission/unix-socket-policy.js +99 -0
  182. package/dist/core/protected-paths.js +330 -0
  183. package/dist/core/relation/peer-identity.js +222 -0
  184. package/dist/core/relation/peer-key.js +1 -0
  185. package/dist/core/role/runtime-policy.js +141 -0
  186. package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
  187. package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
  188. package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
  189. package/dist/core/session/session-file-adapter.js +7 -0
  190. package/dist/core/session/session-file-health.js +45 -0
  191. package/dist/core/session/session-fs-store.js +232 -0
  192. package/dist/core/session/session-key.js +24 -0
  193. package/dist/core/session/session-manager.js +1587 -0
  194. package/dist/core/session/session-mapper.js +100 -0
  195. package/dist/core/session/session-renew.js +314 -0
  196. package/dist/core/session/session-title.js +128 -0
  197. package/dist/core/session/session-turn-coordinator.js +205 -0
  198. package/dist/core/session/session-turns.js +67 -0
  199. package/dist/core/system-channels.js +29 -0
  200. package/dist/eck/baseagent-caps.js +18 -0
  201. package/dist/eck/detect.js +47 -0
  202. package/dist/eck/group-rules-sync.js +345 -0
  203. package/dist/eck/init.js +77 -0
  204. package/dist/eck/kit-renderer.js +359 -0
  205. package/dist/eck/manifest-engine.js +446 -0
  206. package/dist/eck/message-renderer.js +199 -0
  207. package/dist/eck/rules-loader.js +28 -0
  208. package/dist/index.js +2870 -4
  209. package/dist/ipc.js +748 -0
  210. package/dist/paths.js +262 -0
  211. package/dist/product.js +18 -0
  212. package/dist/response-system/context-builder.js +71 -0
  213. package/dist/response-system/coordinator.js +117 -0
  214. package/dist/response-system/decision-executor.js +86 -0
  215. package/dist/response-system/engines/v1/index.js +21 -0
  216. package/dist/response-system/engines/v1/interactive-flow.js +27 -0
  217. package/dist/response-system/engines/v1/proactive-flow.js +137 -0
  218. package/dist/response-system/engines/v1/types.js +1 -0
  219. package/dist/response-system/extensions.js +41 -0
  220. package/dist/response-system/index.js +6 -0
  221. package/dist/response-system/modes/index.js +7 -0
  222. package/dist/response-system/modes/single-session/index.js +72 -0
  223. package/dist/response-system/queues/fifo-queue.js +44 -0
  224. package/dist/response-system/queues/index.js +6 -0
  225. package/dist/response-system/queues/lifo-queue.js +42 -0
  226. package/dist/response-system/queues/priority-queue.js +63 -0
  227. package/dist/response-system/registry.js +97 -0
  228. package/dist/response-system/resolver.js +37 -0
  229. package/dist/response-system/selector.js +23 -0
  230. package/dist/response-system/types.js +7 -0
  231. package/dist/stats/billing.js +151 -0
  232. package/dist/stats/budget.js +93 -0
  233. package/dist/stats/db.js +403 -0
  234. package/dist/stats/eck-vars.js +89 -0
  235. package/dist/stats/index.js +11 -0
  236. package/dist/stats/normalizer.js +80 -0
  237. package/dist/stats/price-resolver.js +138 -0
  238. package/dist/stats/query.js +763 -0
  239. package/dist/stats/role-budget.js +168 -0
  240. package/dist/stats/writer.js +151 -0
  241. package/dist/trigger/anomaly-store.js +258 -0
  242. package/dist/trigger/audit.js +152 -0
  243. package/dist/trigger/event-source.js +119 -0
  244. package/dist/trigger/feedback.js +685 -0
  245. package/dist/trigger/history.js +290 -0
  246. package/dist/trigger/manager.js +291 -0
  247. package/dist/trigger/parser.js +520 -0
  248. package/dist/trigger/patch.js +148 -0
  249. package/dist/trigger/scheduler.js +1600 -0
  250. package/dist/trigger/script-executor.js +155 -0
  251. package/dist/trigger/state.js +145 -0
  252. package/dist/trigger/types.js +1 -0
  253. package/dist/trigger/validation.js +621 -0
  254. package/dist/types.js +12 -0
  255. package/dist/utils/aid-bind.js +299 -0
  256. package/dist/utils/atomic-write.js +95 -0
  257. package/dist/utils/avatar-upload.js +123 -0
  258. package/dist/utils/cross-platform.js +297 -0
  259. package/dist/utils/ecweb-utils.js +73 -0
  260. package/dist/utils/error-dict.json +153 -0
  261. package/dist/utils/error-utils.js +349 -0
  262. package/dist/utils/instance-registry.js +437 -0
  263. package/dist/utils/locale.js +21 -0
  264. package/dist/utils/log-writer.js +224 -0
  265. package/dist/utils/logger.js +89 -0
  266. package/dist/utils/markdown-to-plain-text.js +20 -0
  267. package/dist/utils/media-cache.js +271 -0
  268. package/dist/utils/model-prices.jsonl +17 -0
  269. package/dist/utils/npm-ops.js +210 -0
  270. package/dist/utils/process-introspect.js +133 -0
  271. package/dist/utils/process-tree-stats.js +271 -0
  272. package/dist/utils/project-path.js +74 -0
  273. package/dist/utils/stats.js +410 -0
  274. package/dist/utils/tool-summary.js +284 -0
  275. package/dist/utils/welcome.js +268 -0
  276. package/kits/docs/GUIDE.md +20 -0
  277. package/kits/docs/INDEX.md +65 -0
  278. package/kits/docs/aun/CHEATSHEET.md +19 -0
  279. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  280. package/kits/docs/channels/aun.md +65 -0
  281. package/kits/docs/channels/feishu.md +56 -0
  282. package/kits/docs/context-assembly.md +366 -0
  283. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  284. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  285. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  286. package/kits/docs/eck_templates/runtime.template.md +19 -0
  287. package/kits/docs/evolcore/INDEX.md +66 -0
  288. package/kits/docs/evolcore/agent.md +69 -0
  289. package/kits/docs/evolcore/aid.md +49 -0
  290. package/kits/docs/evolcore/config.md +149 -0
  291. package/kits/docs/evolcore/ctl.md +46 -0
  292. package/kits/docs/evolcore/event.md +216 -0
  293. package/kits/docs/evolcore/fs-architecture.md +1215 -0
  294. package/kits/docs/evolcore/fs.md +101 -0
  295. package/kits/docs/evolcore/group-fs.md +17 -0
  296. package/kits/docs/evolcore/group-rules.md +226 -0
  297. package/kits/docs/evolcore/group.md +141 -0
  298. package/kits/docs/evolcore/model.md +47 -0
  299. package/kits/docs/evolcore/msg.md +130 -0
  300. package/kits/docs/evolcore/response.md +80 -0
  301. package/kits/docs/evolcore/rpc.md +35 -0
  302. package/kits/docs/evolcore/self-summary.md +29 -0
  303. package/kits/docs/evolcore/stats.md +70 -0
  304. package/kits/docs/evolcore/storage.md +49 -0
  305. package/kits/docs/evolcore/trigger.md +524 -0
  306. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
  307. package/kits/docs/identity/PATH_OPS.md +16 -0
  308. package/kits/docs/identity/ROLE_DETAIL.md +23 -0
  309. package/kits/docs/identity/identity-tools.md +26 -0
  310. package/kits/docs/path-registry.md +43 -0
  311. package/kits/docs/prompt-loading-architecture.md +266 -0
  312. package/kits/docs/venues/aun-group.md +45 -0
  313. package/kits/docs/venues/aun-private.md +10 -0
  314. package/kits/docs/venues/client-desktop.md +10 -0
  315. package/kits/docs/venues/client-mobile.md +10 -0
  316. package/kits/docs/venues/feishu-group.md +13 -0
  317. package/kits/docs/venues/feishu-private.md +9 -0
  318. package/kits/docs/venues/group.md +25 -0
  319. package/kits/docs/venues/private.md +10 -0
  320. package/kits/eck_manifest.auxiliary.json +43 -0
  321. package/kits/eck_manifest.json +191 -0
  322. package/kits/eck_message_manifest.json +63 -0
  323. package/kits/migrations/README-role-config-v4.md +32 -0
  324. package/kits/migrations/migrate-role-config-v4.mjs +623 -0
  325. package/kits/migrations/migrate-role-config-v5.mjs +346 -0
  326. package/kits/migrations/rename-config-file.mjs +99 -0
  327. package/kits/rules/01-overview.md +142 -0
  328. package/kits/rules/02-navigation.md +76 -0
  329. package/kits/rules/03-identity.md +34 -0
  330. package/kits/rules/04-relation.md +59 -0
  331. package/kits/rules/05-venue.md +44 -0
  332. package/kits/rules/06-channel.md +59 -0
  333. package/kits/schemas/_meta.json +29 -0
  334. package/kits/schemas/agent-config.schema.1.json +177 -0
  335. package/kits/schemas/agent-config.schema.2.json +239 -0
  336. package/kits/schemas/agent-config.schema.3.json +119 -0
  337. package/kits/schemas/agent-config.schema.4.json +208 -0
  338. package/kits/schemas/agent-config.schema.5.json +326 -0
  339. package/kits/schemas/contact-book.schema.1.json +36 -0
  340. package/kits/schemas/daemon.schema.1.json +90 -0
  341. package/kits/schemas/defaults.schema.1.json +81 -0
  342. package/kits/schemas/menu-exec-schema-commands.md +208 -0
  343. package/kits/schemas/migrations/README.md +28 -0
  344. package/kits/schemas/relation-config.schema.1.json +158 -0
  345. package/kits/schemas/relation-config.schema.2.json +73 -0
  346. package/kits/schemas/relation-config.schema.3.json +50 -0
  347. package/kits/schemas/relation-config.schema.4.json +47 -0
  348. package/kits/schemas/role-config.schema.1.json +200 -0
  349. package/kits/schemas/role-registry.schema.1.json +35 -0
  350. package/kits/schemas/single-session.schema.1.json +31 -0
  351. package/kits/templates/bootstrap-welcome.md +15 -0
  352. package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
  353. package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
  354. package/kits/templates/message-fragments/inject-default.md +2 -0
  355. package/kits/templates/message-fragments/item.md +2 -0
  356. package/kits/templates/roles/admin.json +8 -0
  357. package/kits/templates/roles/member.json +40 -0
  358. package/kits/templates/roles/owner.json +8 -0
  359. package/kits/templates/roles/visitor.json +39 -0
  360. package/kits/templates/system-fragments/baseagent.md +14 -0
  361. package/kits/templates/system-fragments/bootstrap.md +16 -0
  362. package/kits/templates/system-fragments/channel.md +48 -0
  363. package/kits/templates/system-fragments/commands.md +26 -0
  364. package/kits/templates/system-fragments/identity.md +11 -0
  365. package/kits/templates/system-fragments/relation.md +19 -0
  366. package/kits/templates/system-fragments/session.md +53 -0
  367. package/kits/templates/system-fragments/venue.md +31 -0
  368. package/package.json +50 -15
  369. package/dist/index.d.ts +0 -7
  370. package/dist/index.d.ts.map +0 -1
  371. package/dist/index.js.map +0 -1
@@ -0,0 +1,2385 @@
1
+ import { query, forkSession as sdkForkSession, getSessionMessages as sdkGetSessionMessages, resolveSettings as sdkResolveSettings } from '@anthropic-ai/claude-agent-sdk';
2
+ import { atomicReadJson, atomicWriteJson, ensureDir } from '../utils/atomic-write.js';
3
+ import { resolveAnthropicConfig } from './baseagent.js';
4
+ import { DEFAULT_PERMISSION_MODE } from '../types.js';
5
+ import { renderActionAsText } from '../core/interaction-router.js';
6
+ import { buildEnvelope, sendInteractionPayload } from '../core/message/message-utils.js';
7
+ import path from 'path';
8
+ import fs from 'fs';
9
+ import os from 'os';
10
+ import crypto from 'crypto';
11
+ import { logger } from '../utils/logger.js';
12
+ import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
13
+ import { checkDangerousCommand, checkReadonly, evaluateToolPreflight } from '../core/permission/tool-policy.js';
14
+ import { summarizeToolInput } from '../utils/tool-summary.js';
15
+ import { encodePath } from '../utils/cross-platform.js';
16
+ import { getPackageRoot, resolvePaths } from '../paths.js';
17
+ import { resolveEffective } from '../config/config-manager.js';
18
+ import { sanitizeSessionTitle } from '../core/session/session-title.js';
19
+ import { resolveClaudeCapabilityRunOptionsForProject } from '../core/capability/capability-manager.js';
20
+ import { normalizePermissionMode } from '../core/permission/mode.js';
21
+ import { resolvePhaseOneExecutionSandbox } from '../core/permission/execution-sandbox.js';
22
+ import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, resolveProtectedCandidate, } from '../core/protected-paths.js';
23
+ import { ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable } from '../core/permission/sandbox-runtime.js';
24
+ import { buildClaudeUnixSocketAllowlist } from '../core/permission/unix-socket-policy.js';
25
+ import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
26
+ export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
27
+ // Built-in tools execute inside the Claude runtime and are covered by the
28
+ // runner's filesystem/shell policy. MCP and future unknown tools can perform
29
+ // side effects in another process or service, outside that sandbox, so they
30
+ // must never inherit auto-allow behavior merely because they are not Bash.
31
+ const CLAUDE_BUILTIN_TOOLS = new Set([
32
+ 'Agent',
33
+ 'AskUserQuestion',
34
+ 'Bash',
35
+ 'CronCreate',
36
+ 'CronDelete',
37
+ 'CronList',
38
+ 'Edit',
39
+ 'EnterPlanMode',
40
+ 'ExitPlanMode',
41
+ 'Glob',
42
+ 'Grep',
43
+ 'NotebookEdit',
44
+ 'Read',
45
+ 'SendMessage',
46
+ 'Skill',
47
+ 'Task',
48
+ 'TaskCreate',
49
+ 'TaskGet',
50
+ 'TaskList',
51
+ 'TaskOutput',
52
+ 'TaskStop',
53
+ 'TaskUpdate',
54
+ 'TeamCreate',
55
+ 'TeamDelete',
56
+ 'TodoWrite',
57
+ 'ToolSearch',
58
+ 'WebFetch',
59
+ 'WebSearch',
60
+ 'Write',
61
+ ]);
62
+ function isClaudeMcpTool(toolName) {
63
+ return toolName.startsWith('mcp__');
64
+ }
65
+ function isUnknownClaudeTool(toolName) {
66
+ return !isClaudeMcpTool(toolName) && !CLAUDE_BUILTIN_TOOLS.has(toolName);
67
+ }
68
+ function stableClaudePermissionValue(value, depth = 0) {
69
+ if (depth > 8)
70
+ return '[truncated]';
71
+ if (value === undefined || value === null || typeof value !== 'object')
72
+ return value;
73
+ if (Array.isArray(value))
74
+ return value.map(entry => stableClaudePermissionValue(entry, depth + 1));
75
+ const record = value;
76
+ return Object.fromEntries(Object.keys(record)
77
+ .sort()
78
+ .map(key => [key, stableClaudePermissionValue(record[key], depth + 1)]));
79
+ }
80
+ function collectClaudePermissionStrings(value, output = []) {
81
+ if (typeof value === 'string') {
82
+ output.push(value);
83
+ return output;
84
+ }
85
+ if (Array.isArray(value)) {
86
+ for (const entry of value)
87
+ collectClaudePermissionStrings(entry, output);
88
+ return output;
89
+ }
90
+ if (!value || typeof value !== 'object')
91
+ return output;
92
+ for (const entry of Object.values(value)) {
93
+ collectClaudePermissionStrings(entry, output);
94
+ }
95
+ return output;
96
+ }
97
+ function inspectClaudePermissionExpansion(input, options, projectPath) {
98
+ const blockedPath = typeof options.blockedPath === 'string' && options.blockedPath.length > 0
99
+ ? options.blockedPath
100
+ : undefined;
101
+ const rawSuggestions = Array.isArray(options.suggestions) ? options.suggestions : [];
102
+ const suggestions = rawSuggestions.slice(0, 64).map(entry => stableClaudePermissionValue(entry));
103
+ const directoryPaths = rawSuggestions.flatMap(entry => {
104
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
105
+ return [];
106
+ const record = entry;
107
+ if (record.type !== 'addDirectories' && record.type !== 'removeDirectories')
108
+ return [];
109
+ return Array.isArray(record.directories)
110
+ ? record.directories.filter((value) => typeof value === 'string' && value.length > 0)
111
+ : [];
112
+ });
113
+ const candidatePaths = [
114
+ ...(blockedPath ? [blockedPath] : []),
115
+ ...directoryPaths,
116
+ ];
117
+ const canonicalDirectoryPaths = directoryPaths.map(candidate => resolveProtectedCandidate(candidate, projectPath));
118
+ let protectedReason;
119
+ for (const candidate of candidatePaths) {
120
+ const canonical = resolveProtectedCandidate(candidate, projectPath);
121
+ if (containsHClassReference(candidate) || isHClassPath(canonical, { root: resolvePaths().root })) {
122
+ protectedReason = '🔒 Claude SDK 权限请求涉及 EvolCore H 类受保护路径,已拒绝';
123
+ break;
124
+ }
125
+ if (containsLClassReference(candidate) || isLClassPath(canonical, { root: resolvePaths().root })) {
126
+ protectedReason = '🔒 Claude SDK 权限请求涉及 EvolCore L 类受保护路径,已拒绝';
127
+ break;
128
+ }
129
+ }
130
+ if (!protectedReason && collectClaudePermissionStrings(rawSuggestions).some(containsHClassReference)) {
131
+ protectedReason = '🔒 Claude SDK 权限规则涉及 EvolCore H 类受保护路径,已拒绝';
132
+ }
133
+ const hasDirectoryExpansion = rawSuggestions.some(entry => {
134
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
135
+ return false;
136
+ const type = entry.type;
137
+ return type === 'addDirectories' || type === 'removeDirectories';
138
+ });
139
+ const hasUnknownSuggestion = rawSuggestions.some(entry => {
140
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
141
+ return true;
142
+ return !new Set(['addRules', 'replaceRules', 'removeRules', 'setMode', 'addDirectories', 'removeDirectories'])
143
+ .has(String(entry.type));
144
+ });
145
+ const requiresApproval = !!blockedPath || hasDirectoryExpansion || hasUnknownSuggestion;
146
+ const permissionContext = {
147
+ ...(blockedPath ? { blockedPath } : {}),
148
+ ...(blockedPath ? { canonicalBlockedPath: resolveProtectedCandidate(blockedPath, projectPath) } : {}),
149
+ ...(canonicalDirectoryPaths.length > 0 ? { canonicalDirectoryPaths } : {}),
150
+ suggestions,
151
+ };
152
+ return {
153
+ blockedPath,
154
+ ...(blockedPath ? { canonicalBlockedPath: permissionContext.canonicalBlockedPath } : {}),
155
+ suggestions,
156
+ requiresApproval,
157
+ protectedReason,
158
+ approvalInput: requiresApproval
159
+ ? { ...input, __evolcoreClaudePermissionContext: permissionContext }
160
+ : input,
161
+ };
162
+ }
163
+ function buildClaudeManagedLockdownSettings(capabilityOptions) {
164
+ const settings = capabilityOptions?.settings;
165
+ const rawAllowedMcpServers = settings && typeof settings === 'object' && !Array.isArray(settings)
166
+ ? settings.allowedMcpServers
167
+ : undefined;
168
+ const allowedMcpServers = Array.isArray(rawAllowedMcpServers)
169
+ ? rawAllowedMcpServers.flatMap(entry => {
170
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
171
+ return [];
172
+ const serverName = entry.serverName;
173
+ return typeof serverName === 'string' && serverName ? [{ serverName }] : [];
174
+ })
175
+ : undefined;
176
+ return {
177
+ allowManagedHooksOnly: true,
178
+ disableSkillShellExecution: true,
179
+ allowedHttpHookUrls: [],
180
+ allowManagedPermissionRulesOnly: true,
181
+ ...(allowedMcpServers ? {
182
+ allowManagedMcpServersOnly: true,
183
+ allowedMcpServers,
184
+ } : {}),
185
+ };
186
+ }
187
+ const CLAUDE_SANDBOX_GLOB_MAGIC = /[*?[\]]/;
188
+ async function assertClaudeSettingSourcesHaveLiteralSandboxPaths(cwd, settingSources, managedSettings) {
189
+ if (process.platform !== 'linux' || settingSources.length === 0)
190
+ return;
191
+ const resolved = await sdkResolveSettings({ cwd, settingSources, managedSettings });
192
+ for (const source of resolved.sources) {
193
+ const filesystem = source.settings.sandbox?.filesystem;
194
+ for (const key of ['denyRead', 'denyWrite', 'allowRead', 'allowWrite']) {
195
+ for (const target of filesystem?.[key] ?? []) {
196
+ if (!CLAUDE_SANDBOX_GLOB_MAGIC.test(target))
197
+ continue;
198
+ const origin = source.path ?? source.source;
199
+ throw new Error(`[ClaudeSandbox] glob path in ${origin} is unsafe on Linux sandbox projections: ${key}=${target}`);
200
+ }
201
+ }
202
+ }
203
+ }
204
+ // ── 模型别名解析 ──
205
+ // SDK 内置的别名表可能落后于代理实际可用的最新模型,
206
+ // 因此优先从 {baseUrl}/models 动态获取各系列最新版本,失败则使用持久化的最近成功值。
207
+ // 已验证可用但尚未出现在 /models 列表中的模型 ID 会被注入候选列表,
208
+ // 等列表更新后注入自动变为 no-op。
209
+ const MODEL_FAMILIES = ['opus', 'sonnet', 'haiku'];
210
+ /** 已验证可用但可能尚未出现在 /models 列表中的模型 ID(注入候选) */
211
+ const INJECTED_MODELS = [];
212
+ /** 启动默认值:某网关尚无任何成功刷新记录时使用。 */
213
+ const BOOTSTRAP_MODEL_ALIASES = {
214
+ 'opus': 'claude-opus-4-8',
215
+ 'sonnet': 'claude-sonnet-4-6',
216
+ 'haiku': 'claude-haiku-4-5-20251001',
217
+ };
218
+ const MODEL_ALIAS_TTL_MS = 5 * 60 * 1000; // 5min
219
+ const MODEL_ALIAS_STORE_VERSION = 1;
220
+ const modelAliasCache = new Map(); // key: baseUrl
221
+ const modelAliasFallbacks = new Map(); // key: baseUrl,最近一次成功解析值
222
+ const modelAliasInFlight = new Set(); // 去重并发刷新
223
+ const loadedModelAliasFallbacks = new Set(); // key: store path + gateway hash
224
+ function normalizeModelGatewayUrl(baseUrl) {
225
+ return baseUrl.trim().replace(/\/+$/, '');
226
+ }
227
+ function modelGatewayKey(baseUrl) {
228
+ return crypto.createHash('sha256').update(normalizeModelGatewayUrl(baseUrl)).digest('hex');
229
+ }
230
+ function modelAliasStorePath() {
231
+ return resolvePaths().claudeModelCache;
232
+ }
233
+ function sanitizeModelAliases(value) {
234
+ if (!value || typeof value !== 'object' || Array.isArray(value))
235
+ return {};
236
+ const aliases = {};
237
+ for (const family of MODEL_FAMILIES) {
238
+ const model = value[family];
239
+ if (typeof model !== 'string')
240
+ continue;
241
+ const pattern = new RegExp(`^claude-${family}-[A-Za-z0-9._-]+$`);
242
+ if (pattern.test(model))
243
+ aliases[family] = model;
244
+ }
245
+ return aliases;
246
+ }
247
+ function readPersistedAliasStore() {
248
+ try {
249
+ const value = atomicReadJson(modelAliasStorePath());
250
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
251
+ return { $schema_version: MODEL_ALIAS_STORE_VERSION, gateways: {} };
252
+ }
253
+ const raw = value;
254
+ if (raw.$schema_version !== MODEL_ALIAS_STORE_VERSION || !raw.gateways || typeof raw.gateways !== 'object' || Array.isArray(raw.gateways)) {
255
+ return { $schema_version: MODEL_ALIAS_STORE_VERSION, gateways: {} };
256
+ }
257
+ const gateways = {};
258
+ for (const [key, entry] of Object.entries(raw.gateways)) {
259
+ if (!/^[a-f0-9]{64}$/.test(key) || !entry || typeof entry !== 'object' || Array.isArray(entry))
260
+ continue;
261
+ const record = entry;
262
+ const aliases = sanitizeModelAliases(record.aliases);
263
+ if (Object.keys(aliases).length === 0)
264
+ continue;
265
+ gateways[key] = {
266
+ aliases,
267
+ updatedAt: typeof record.updatedAt === 'number' && Number.isFinite(record.updatedAt) ? record.updatedAt : 0,
268
+ };
269
+ }
270
+ return { $schema_version: MODEL_ALIAS_STORE_VERSION, gateways };
271
+ }
272
+ catch (error) {
273
+ logger.warn(`[AgentRunner] Failed to load persisted model aliases: ${error instanceof Error ? error.message : String(error)}`);
274
+ return { $schema_version: MODEL_ALIAS_STORE_VERSION, gateways: {} };
275
+ }
276
+ }
277
+ function loadPersistedModelAliases(baseUrl) {
278
+ const cacheKey = normalizeModelGatewayUrl(baseUrl);
279
+ const storePath = modelAliasStorePath();
280
+ const gatewayKey = modelGatewayKey(cacheKey);
281
+ const loadKey = `${storePath}\0${gatewayKey}`;
282
+ if (loadedModelAliasFallbacks.has(loadKey))
283
+ return;
284
+ loadedModelAliasFallbacks.add(loadKey);
285
+ const persisted = readPersistedAliasStore().gateways[gatewayKey];
286
+ if (!persisted)
287
+ return;
288
+ modelAliasFallbacks.set(cacheKey, persisted.aliases);
289
+ logger.info(`[AgentRunner] Loaded persisted model aliases: ${JSON.stringify(persisted.aliases)}`);
290
+ }
291
+ function persistModelAliases(baseUrl, aliases, updatedAt) {
292
+ try {
293
+ const store = readPersistedAliasStore();
294
+ store.gateways[modelGatewayKey(baseUrl)] = { aliases, updatedAt };
295
+ atomicWriteJson(modelAliasStorePath(), store);
296
+ }
297
+ catch (error) {
298
+ logger.warn(`[AgentRunner] Failed to persist model aliases: ${error instanceof Error ? error.message : String(error)}`);
299
+ }
300
+ }
301
+ // ── 网关价格缓存(从 /v1/models 的 pricing/effective_pricing 提取)─────────────
302
+ // 与别名刷新同范式:按 baseUrl 缓存,1h TTL,stale-while-revalidate(缺失/过期时
303
+ // fire-and-forget 触发刷新,本轮先用旧值或回退,不阻塞查询)。
304
+ const GATEWAY_PRICING_TTL_MS = 60 * 60 * 1000; // 1h
305
+ const gatewayPricingCache = new Map(); // key: baseUrl
306
+ const gatewayPricingInFlight = new Set(); // 去重并发刷新
307
+ /** 把 /v1/models 单个 model 的价格对象转为 PriceQuad(与 gateway-control 的 apiPricingToQuad 等价)。
308
+ * 单位假设:接口价与 model-prices.jsonl 同口径(USD per 1M token),不做换算。 */
309
+ function apiPricingToQuad(p) {
310
+ if (!p || typeof p !== 'object')
311
+ return undefined;
312
+ const n = (v) => (typeof v === 'number' && isFinite(v) ? v : undefined);
313
+ const quad = {
314
+ input: n(p.input),
315
+ output: n(p.output),
316
+ cache_read: n(p.cache_read),
317
+ cache_write: n(p.cache_write),
318
+ };
319
+ // 全空则视为无价
320
+ if (quad.input === undefined && quad.output === undefined
321
+ && quad.cache_read === undefined && quad.cache_write === undefined)
322
+ return undefined;
323
+ return quad;
324
+ }
325
+ /** 拉取网关 /v1/models 的官方价(pricing) + 网关价(effective_pricing),写入 gatewayPricingCache。
326
+ * 失败静默——保持回退到本地价表 / official。 */
327
+ async function refreshGatewayPricing(baseUrl, apiKey) {
328
+ if (gatewayPricingInFlight.has(baseUrl))
329
+ return;
330
+ gatewayPricingInFlight.add(baseUrl);
331
+ try {
332
+ const url = `${baseUrl.replace(/\/+$/, '')}/v1/models`;
333
+ const controller = new AbortController();
334
+ const timer = setTimeout(() => controller.abort(), 5000);
335
+ const resp = await fetch(url, {
336
+ signal: controller.signal,
337
+ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
338
+ });
339
+ clearTimeout(timer);
340
+ if (!resp.ok)
341
+ return;
342
+ const json = await resp.json();
343
+ const arr = Array.isArray(json?.data) ? json.data
344
+ : Array.isArray(json?.models) ? json.models : [];
345
+ const official = new Map();
346
+ const gateway = new Map();
347
+ for (const m of arr) {
348
+ const id = typeof m === 'string' ? m : (m?.id || m?.name || m?.model);
349
+ if (!id || typeof m !== 'object')
350
+ continue;
351
+ const off = apiPricingToQuad(m.pricing);
352
+ if (off)
353
+ official.set(id, off);
354
+ const gw = apiPricingToQuad(m.effective_pricing);
355
+ if (gw)
356
+ gateway.set(id, gw);
357
+ }
358
+ // 汇率:接口 usd_to_cny,缺失则留空(计费层默认用 7)
359
+ const usdToCny = (typeof json?.usd_to_cny === 'number' && json.usd_to_cny > 0) ? json.usd_to_cny : undefined;
360
+ if (official.size > 0 || gateway.size > 0) {
361
+ gatewayPricingCache.set(baseUrl, { cache: { official, gateway, usdToCny }, fetchedAt: Date.now() });
362
+ logger.info(`[AgentRunner] Refreshed gateway pricing from ${url}: official=${official.size} gateway=${gateway.size} usd_to_cny=${usdToCny ?? '(default 7)'}`);
363
+ }
364
+ }
365
+ catch {
366
+ // 网络/解析失败:保持回退,不打断查询
367
+ }
368
+ finally {
369
+ gatewayPricingInFlight.delete(baseUrl);
370
+ }
371
+ }
372
+ /** 从模型 ID 列表中提取各 claude 系列的最新版本(按 major.minor 取最高,minor 可省略) */
373
+ function deriveAliasesFromModelIds(ids) {
374
+ // 注入已验证可用的模型(如果列表中已有则去重无影响)
375
+ const allIds = [...new Set([...ids, ...INJECTED_MODELS])];
376
+ const best = {};
377
+ for (const id of allIds) {
378
+ const m = id.match(/^claude-(opus|sonnet|haiku)-(\d+)(?:-(\d+))?/);
379
+ if (!m)
380
+ continue;
381
+ const [, family, majorStr, minorStr] = m;
382
+ const major = parseInt(majorStr, 10);
383
+ const minor = minorStr ? parseInt(minorStr, 10) : 0;
384
+ const cur = best[family];
385
+ if (!cur || major > cur.major || (major === cur.major && minor > cur.minor)) {
386
+ best[family] = { id, major, minor };
387
+ }
388
+ }
389
+ const aliases = {};
390
+ for (const family of MODEL_FAMILIES) {
391
+ const info = best[family];
392
+ if (info)
393
+ aliases[family] = info.id;
394
+ }
395
+ return aliases;
396
+ }
397
+ /** 异步刷新某 baseUrl 的别名缓存(失败静默,不抛出) */
398
+ async function refreshModelAliases(baseUrl, apiKey) {
399
+ const cacheKey = normalizeModelGatewayUrl(baseUrl);
400
+ if (modelAliasInFlight.has(cacheKey))
401
+ return;
402
+ modelAliasInFlight.add(cacheKey);
403
+ loadPersistedModelAliases(cacheKey);
404
+ try {
405
+ const url = `${cacheKey}/models`;
406
+ const controller = new AbortController();
407
+ const timer = setTimeout(() => controller.abort(), 5000);
408
+ const resp = await fetch(url, {
409
+ signal: controller.signal,
410
+ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
411
+ });
412
+ clearTimeout(timer);
413
+ if (!resp.ok)
414
+ return;
415
+ const json = await resp.json();
416
+ const ids = Array.isArray(json?.data)
417
+ ? json.data.map((m) => m?.id).filter((x) => typeof x === 'string')
418
+ : [];
419
+ const aliases = deriveAliasesFromModelIds(ids);
420
+ if (ids.length > 0 || Object.keys(aliases).length > 0) {
421
+ const updatedAt = Date.now();
422
+ modelAliasCache.set(cacheKey, { aliases, ids, fetchedAt: updatedAt });
423
+ if (Object.keys(aliases).length > 0) {
424
+ const previousAliases = modelAliasFallbacks.get(cacheKey) || {};
425
+ const latestAliases = { ...previousAliases, ...aliases };
426
+ modelAliasFallbacks.set(cacheKey, latestAliases);
427
+ persistModelAliases(cacheKey, latestAliases, updatedAt);
428
+ }
429
+ logger.info(`[AgentRunner] Refreshed models from ${url}: ${ids.length} ids, aliases ${JSON.stringify(aliases)}`);
430
+ }
431
+ }
432
+ catch {
433
+ // 网络/解析失败:保留该网关最近一次成功结果,不打断查询
434
+ }
435
+ finally {
436
+ modelAliasInFlight.delete(cacheKey);
437
+ }
438
+ }
439
+ /** 将短别名展开为完整 model ID,已是完整 ID 则原样返回 */
440
+ function resolveModelAlias(model, baseUrl) {
441
+ // 非短别名(已经是完整 ID)直接返回
442
+ if (!MODEL_FAMILIES.includes(model))
443
+ return model;
444
+ const family = model;
445
+ // 优先使用动态缓存
446
+ if (baseUrl) {
447
+ const cacheKey = normalizeModelGatewayUrl(baseUrl);
448
+ loadPersistedModelAliases(cacheKey);
449
+ const cached = modelAliasCache.get(cacheKey);
450
+ if (cached && (Date.now() - cached.fetchedAt < MODEL_ALIAS_TTL_MS)) {
451
+ return cached.aliases[family] || modelAliasFallbacks.get(cacheKey)?.[family] || BOOTSTRAP_MODEL_ALIASES[family];
452
+ }
453
+ return modelAliasFallbacks.get(cacheKey)?.[family] || BOOTSTRAP_MODEL_ALIASES[family];
454
+ }
455
+ return BOOTSTRAP_MODEL_ALIASES[family];
456
+ }
457
+ /**
458
+ * 为支持 1M 上下文的模型追加 `[1m]` 后缀——仅在交给 SDK query() 时调用。
459
+ * 目录与校验层始终使用不带后缀的基础 ID,避免与网关 /models 返回值(无 `[1m]`)冲突。
460
+ */
461
+ function applyContextWindow(modelId) {
462
+ if (/\[1m\]$/.test(modelId))
463
+ return modelId; // 已带后缀
464
+ if (isOneMillionContextModel(modelId))
465
+ return `${modelId}[1m]`;
466
+ return modelId;
467
+ }
468
+ /** 解析别名 + 追加 1M 后缀,得到最终交给 SDK 的 model 串。 */
469
+ function resolveSdkModel(model, baseUrl) {
470
+ return applyContextWindow(resolveModelAlias(model, baseUrl));
471
+ }
472
+ class MessageStream {
473
+ queue = [];
474
+ waiting = null;
475
+ done = false;
476
+ push(text, images, inputId = crypto.randomUUID()) {
477
+ let content;
478
+ if (images && images.length > 0) {
479
+ logger.debug('[MessageStream] Creating multimodal message with', images.length, 'images');
480
+ content = [
481
+ { type: 'text', text },
482
+ ...images.map((img) => ({
483
+ type: 'image',
484
+ source: {
485
+ type: 'base64',
486
+ media_type: img.mimeType || 'image/png',
487
+ data: img.data,
488
+ },
489
+ })),
490
+ ];
491
+ }
492
+ else {
493
+ content = text;
494
+ }
495
+ const message = {
496
+ type: 'user',
497
+ message: { role: 'user', content },
498
+ parent_tool_use_id: null,
499
+ uuid: inputId,
500
+ session_id: '',
501
+ };
502
+ this.queue.push(message);
503
+ this.waiting?.();
504
+ return inputId;
505
+ }
506
+ end() {
507
+ this.done = true;
508
+ this.waiting?.();
509
+ }
510
+ async *[Symbol.asyncIterator]() {
511
+ while (true) {
512
+ while (this.queue.length > 0) {
513
+ yield this.queue.shift();
514
+ }
515
+ if (this.done)
516
+ return;
517
+ await new Promise((r) => {
518
+ this.waiting = r;
519
+ });
520
+ this.waiting = null;
521
+ }
522
+ }
523
+ }
524
+ export class AgentRunner {
525
+ name = 'claude';
526
+ capabilities = { clear: true, compact: true, fork: true, forkAtTurn: true, askUserQuestion: true, planApproval: true, fileRewind: 'checkpoint' };
527
+ apiKey;
528
+ model;
529
+ effort;
530
+ permissionMode = DEFAULT_PERMISSION_MODE;
531
+ baseUrl;
532
+ config;
533
+ activeSessions = new Map();
534
+ activeStreams = new Map();
535
+ activeMessageStreams = new Map();
536
+ interruptFns = new Map();
537
+ activeQueries = new Map();
538
+ streamDone = new Map();
539
+ streamDoneResolvers = new Map();
540
+ onSessionIdUpdate;
541
+ onCompactStart;
542
+ permissionGateway;
543
+ sendPromptFn;
544
+ permissionContexts = new Map();
545
+ claudeExecutablePath;
546
+ /** 每个 session 最近的子进程 stderr 行(环形缓冲),用于子进程崩溃时还原真正原因 */
547
+ recentStderr = new Map();
548
+ static STDERR_BUFFER_MAX = 80;
549
+ constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
550
+ this.apiKey = apiKey;
551
+ this.model = model || 'sonnet';
552
+ this.effort = undefined;
553
+ this.baseUrl = baseUrl;
554
+ this.config = config;
555
+ this.onSessionIdUpdate = onSessionIdUpdate;
556
+ if (config) {
557
+ const anthropic = resolveAnthropicConfig(config);
558
+ this.claudeExecutablePath = anthropic.pathToClaudeCodeExecutable;
559
+ }
560
+ }
561
+ getAgentEnv(runtimeEnv, evolcoreSessionId) {
562
+ const env = {
563
+ ...process.env,
564
+ ANTHROPIC_AUTH_TOKEN: this.apiKey,
565
+ PATH: process.env.PATH,
566
+ DISABLE_AUTOUPDATER: '1',
567
+ ...(this.baseUrl ? { ANTHROPIC_BASE_URL: this.baseUrl } : {}),
568
+ ...(evolcoreSessionId ? { EVOLCORE_SESSION_ID: evolcoreSessionId } : {}),
569
+ ...(runtimeEnv ?? {}),
570
+ };
571
+ const bubblewrapPath = resolveBubblewrapPath();
572
+ return bubblewrapPath
573
+ ? prependExecutableDirectory(env, bubblewrapPath)
574
+ : env;
575
+ }
576
+ setModel(model) {
577
+ this.model = model;
578
+ }
579
+ getModel() {
580
+ return this.model;
581
+ }
582
+ /** 返回当前网关 /v1/models 的价格缓存(1h TTL,stale-while-revalidate)。
583
+ * 缺失/过期时 fire-and-forget 触发刷新,本轮先返回旧值或 undefined(回退本地价表)。 */
584
+ getGatewayPricing() {
585
+ if (!this.baseUrl)
586
+ return undefined;
587
+ const entry = gatewayPricingCache.get(this.baseUrl);
588
+ if (!entry || Date.now() - entry.fetchedAt > GATEWAY_PRICING_TTL_MS) {
589
+ refreshGatewayPricing(this.baseUrl, this.apiKey); // 不 await,stale-while-revalidate
590
+ }
591
+ return entry?.cache;
592
+ }
593
+ async resolveCapabilityRunOptions(projectPath) {
594
+ const claudeConfig = this.config?.agents?.claude;
595
+ let agentConfig = claudeConfig?.evolcoreAgentConfig;
596
+ if (claudeConfig?.evolcoreAgentAid) {
597
+ try {
598
+ agentConfig = resolveEffective({ self: claudeConfig.evolcoreAgentAid }, { cache: true });
599
+ }
600
+ catch { }
601
+ }
602
+ return await resolveClaudeCapabilityRunOptionsForProject(agentConfig, projectPath, 'claude');
603
+ }
604
+ async listModels() {
605
+ if (this.baseUrl) {
606
+ const cacheKey = normalizeModelGatewayUrl(this.baseUrl);
607
+ loadPersistedModelAliases(cacheKey);
608
+ let cached = modelAliasCache.get(cacheKey);
609
+ const stale = !cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS);
610
+ // 缓存为空(首次打开)→ 等待刷新;缓存仅过期 → 后台刷新不阻塞
611
+ if (!cached) {
612
+ await refreshModelAliases(this.baseUrl, this.apiKey);
613
+ cached = modelAliasCache.get(cacheKey);
614
+ }
615
+ else if (stale) {
616
+ refreshModelAliases(this.baseUrl, this.apiKey);
617
+ }
618
+ // 有缓存时返回网关 /models 的全量原始 ID
619
+ if (cached && cached.ids.length > 0)
620
+ return cached.ids;
621
+ }
622
+ // 无 baseUrl / 刷新超时或失败 → 回退短别名
623
+ const fallback = this.baseUrl ? modelAliasFallbacks.get(normalizeModelGatewayUrl(this.baseUrl)) : undefined;
624
+ return Object.values({ ...BOOTSTRAP_MODEL_ALIASES, ...fallback });
625
+ }
626
+ /** 将短别名解析为当前代理实际使用的完整 model ID(仅用于展示,不改变持久化值) */
627
+ resolveModelId(model) {
628
+ return resolveModelAlias(model, this.baseUrl);
629
+ }
630
+ setEffort(effort) {
631
+ this.effort = effort;
632
+ }
633
+ getEffort() {
634
+ return this.effort;
635
+ }
636
+ // ── PermissionController 接口 ──
637
+ setMode(mode) {
638
+ this.permissionMode = mode;
639
+ }
640
+ getMode() {
641
+ return normalizePermissionMode(this.permissionMode).mode;
642
+ }
643
+ listModes() {
644
+ return [
645
+ { key: 'readonly', nameZh: '只读', description: '只读操作自动执行,写入直接拒绝', available: true },
646
+ { key: 'auto', nameZh: '自动', description: '常规操作自动执行,危险操作自动拒绝', available: true },
647
+ { key: 'request', nameZh: '审批', description: '默认能力自动执行,缺失能力进入人工审批', available: true },
648
+ { key: 'bypass', nameZh: '放行', description: 'owner 关闭任务沙盒,危险操作和硬边界仍受控', available: true },
649
+ ];
650
+ }
651
+ setPermissionGateway(gateway) {
652
+ this.permissionGateway = gateway;
653
+ }
654
+ setSendPrompt(fn) {
655
+ this.sendPromptFn = fn;
656
+ }
657
+ setPermissionContext(sessionId, context) {
658
+ this.permissionContexts.set(sessionId, context);
659
+ }
660
+ toSdkPermissionMode(mode) {
661
+ const normalized = normalizePermissionMode(mode ?? this.permissionMode);
662
+ // Public permission policy is enforced by PreToolUse. Keeping the SDK in
663
+ // default mode preserves canUseTool reachability for request mode.
664
+ return normalized.workflow === 'plan' ? 'plan' : 'default';
665
+ }
666
+ // ── Compactable 接口 ──
667
+ async compact(sessionId, agentSessionId, projectPath) {
668
+ return this.compactSession(sessionId, agentSessionId, projectPath);
669
+ }
670
+ syncFromUserSettings() {
671
+ try {
672
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
673
+ if (!fs.existsSync(settingsPath))
674
+ return;
675
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
676
+ // agent config 显式配置优先,不被 settings.json 覆盖
677
+ const configModel = this.config?.agents?.claude?.model;
678
+ if (!configModel && settings.model && settings.model !== this.model) {
679
+ logger.info(`[AgentRunner] Synced model from ~/.claude/settings.json: ${settings.model}`);
680
+ this.model = settings.model;
681
+ }
682
+ const configEffort = this.config?.agents?.claude?.effort;
683
+ if (!configEffort) {
684
+ const newEffort = settings.effortLevel || undefined;
685
+ if (newEffort !== this.effort) {
686
+ logger.info(`[AgentRunner] Synced effort from ~/.claude/settings.json: ${newEffort ?? 'auto'}`);
687
+ this.effort = newEffort;
688
+ }
689
+ }
690
+ }
691
+ catch (error) {
692
+ logger.debug(`[AgentRunner] Failed to sync from ~/.claude/settings.json:`, error);
693
+ }
694
+ }
695
+ setCompactStartCallback(callback) {
696
+ this.onCompactStart = callback;
697
+ }
698
+ /**
699
+ * 处理 AskUserQuestion 工具调用:将 SDK 问题转换为飞书 action 卡片,逐个收集用户答案
700
+ * SDK 期望返回 updatedInput 中包含 answers 字段:{ [questionText]: selectedLabel | selectedLabel[] }
701
+ */
702
+ async handleAskUserQuestion(sessionId, input, options) {
703
+ const questions = input.questions;
704
+ // 没有交互上下文(无渠道适配器),回退到纯文本
705
+ const permCtx = this.permissionContexts.get(sessionId);
706
+ if (usesUnattendedInteractions(permCtx)) {
707
+ logger.info(`[AgentRunner] interaction.auto_resolved kind=ask_user session=${sessionId}`);
708
+ const unattendedAnswer = '当前为无人值守 Trigger,请基于已有上下文选择最安全合理的方案并继续执行。';
709
+ const answers = {};
710
+ for (const question of questions ?? []) {
711
+ answers[question.question] = question.multiSelect ? [unattendedAnswer] : unattendedAnswer;
712
+ }
713
+ return {
714
+ behavior: 'allow',
715
+ updatedInput: { ...input, answers },
716
+ decisionClassification: 'user_temporary',
717
+ };
718
+ }
719
+ if (!permCtx?.adapter || !permCtx?.channelId) {
720
+ return this.handleAskUserQuestionFallback(sessionId, input, questions, options);
721
+ }
722
+ const adapterHasInteractionPath = !!permCtx.adapter.send;
723
+ if (!adapterHasInteractionPath || !permCtx.interactionRouter) {
724
+ return this.handleAskUserQuestionFallback(sessionId, input, questions, options);
725
+ }
726
+ // 立即暂停 idle 监控,不等卡片发完再 register
727
+ permCtx.interactionRouter?.markWaiting(sessionId);
728
+ let waitMarked = true;
729
+ const answers = {};
730
+ const sendPrompt = permCtx.adapter && permCtx.channelId
731
+ ? async (text) => permCtx.adapter.send(buildEnvelope({ channel: permCtx.adapter.channelName, channelId: permCtx.channelId, replyContext: permCtx.replyContext }), { kind: 'result.text', text, isFinal: true })
732
+ : this.sendPromptFn;
733
+ for (let i = 0; i < questions.length; i++) {
734
+ const q = questions[i];
735
+ const requestId = `ask-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
736
+ const cardTitle = q.header ? `💬 ${q.header}` : `💬 问题 ${i + 1}/${questions.length}`;
737
+ let interaction;
738
+ if (q.multiSelect) {
739
+ // 多选:使用 checkers + form 提交(JSON 2.0 CardKit 路径)
740
+ interaction = {
741
+ type: 'interaction',
742
+ id: requestId,
743
+ kind: {
744
+ kind: 'action',
745
+ title: cardTitle,
746
+ body: q.question,
747
+ checkers: q.options.map(opt => ({
748
+ key: opt.label,
749
+ label: opt.label,
750
+ description: opt.description,
751
+ })),
752
+ buttons: [
753
+ { key: 'submit', label: '✅ 确认选择', style: 'primary' },
754
+ ],
755
+ allowCustomInput: true,
756
+ },
757
+ channelId: permCtx.channelId,
758
+ sessionId,
759
+ };
760
+ }
761
+ else {
762
+ // 单选:保持按钮模式
763
+ const bodyLines = [q.question];
764
+ if (q.options.some(opt => opt.description)) {
765
+ bodyLines.push('');
766
+ q.options.forEach((opt, idx) => {
767
+ bodyLines.push(`${idx + 1}. **${opt.label}**${opt.description ? ` — ${opt.description}` : ''}`);
768
+ });
769
+ }
770
+ interaction = {
771
+ type: 'interaction',
772
+ id: requestId,
773
+ kind: {
774
+ kind: 'action',
775
+ title: cardTitle,
776
+ body: bodyLines.join('\n'),
777
+ buttons: q.options.map(opt => ({
778
+ key: opt.label,
779
+ label: opt.label,
780
+ style: 'default',
781
+ })),
782
+ allowCustomInput: true,
783
+ },
784
+ channelId: permCtx.channelId,
785
+ sessionId,
786
+ };
787
+ }
788
+ let cardSent = false;
789
+ try {
790
+ await permCtx.flushPending?.();
791
+ const envelope = buildEnvelope({
792
+ taskId: permCtx.taskId,
793
+ channel: permCtx.channel ?? permCtx.adapter.channelName,
794
+ channelId: permCtx.channelId,
795
+ agentName: permCtx.agentName,
796
+ chatmode: permCtx.chatmode,
797
+ replyContext: permCtx.replyContext,
798
+ });
799
+ const optionLines = q.options.map((o, idx) => ` ${idx + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
800
+ const fallbackText = `💬 ${q.header || q.question}\n${q.header ? q.question + '\n' : ''}${optionLines}`;
801
+ const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
802
+ cardSent = !!result;
803
+ }
804
+ catch (err) {
805
+ logger.warn(`[AgentRunner] AskUserQuestion card send failed for q${i}:`, err);
806
+ }
807
+ if (!cardSent) {
808
+ await permCtx.flushPending?.();
809
+ const firstLabel = q.options[0]?.label || '';
810
+ answers[q.question] = q.multiSelect ? [firstLabel] : firstLabel;
811
+ if (sendPrompt) {
812
+ const optText = q.options.map((o, idx) => ` ${idx + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
813
+ await sendPrompt(`💬 ${q.header || q.question}\n${q.header ? q.question + '\n' : ''}${optText}\n → 自动选择:${firstLabel}`);
814
+ }
815
+ continue;
816
+ }
817
+ try {
818
+ await permCtx.turn?.onInteractionOpen?.({
819
+ interactionId: requestId,
820
+ kind: 'ask_user',
821
+ toolUseId: options.toolUseID,
822
+ requestId: options.requestId,
823
+ expiresAt: Date.now() + 20 * 60 * 1000,
824
+ });
825
+ }
826
+ catch (error) {
827
+ logger.warn(`[AgentRunner] Failed to persist AskUserQuestion open interaction: ${error instanceof Error ? error.message : String(error)}`);
828
+ }
829
+ // 等待用户交互:回答、取消、超时和 AbortSignal 只能结算一次。
830
+ let closeCancellation;
831
+ const cancellationClosed = new Promise(resolve => { closeCancellation = resolve; });
832
+ const settlement = await new Promise((resolve) => {
833
+ let settled = false;
834
+ const finish = (value) => {
835
+ if (settled)
836
+ return;
837
+ settled = true;
838
+ options.signal.removeEventListener('abort', onAbort);
839
+ resolve(value);
840
+ };
841
+ const onAbort = () => {
842
+ void permCtx?.interactionRouter?.cancel(requestId);
843
+ finish({ kind: 'cancelled', reason: 'aborted' });
844
+ };
845
+ permCtx?.interactionRouter?.register(requestId, sessionId, (action, values) => {
846
+ if (action === 'cancel') {
847
+ finish({ kind: 'cancelled', reason: 'explicit_cancel' });
848
+ }
849
+ else if (action === '_custom_input') {
850
+ const customText = values?.custom_text;
851
+ finish({ kind: 'answered', value: typeof customText === 'string' && customText.trim() ? customText.trim() : null });
852
+ }
853
+ else if (action === '_show_input') {
854
+ if (permCtx?.interceptNextMessage) {
855
+ permCtx.interactionRouter?.markWaiting(sessionId);
856
+ sendPrompt?.('✏️ 请直接发送你的自定义回复').catch(() => { });
857
+ permCtx.interceptNextMessage(sessionId, (msg) => {
858
+ permCtx.interactionRouter?.unmarkWaiting(sessionId);
859
+ const text = (msg.content || '').trim();
860
+ finish({ kind: 'answered', value: text || null });
861
+ });
862
+ }
863
+ else {
864
+ finish({ kind: 'cancelled', reason: 'explicit_cancel' });
865
+ }
866
+ }
867
+ else if (action === 'submit' && q.multiSelect && values) {
868
+ const selected = [];
869
+ q.options.forEach((opt, idx) => {
870
+ if (values[`opt_${idx}`] === true)
871
+ selected.push(opt.label);
872
+ });
873
+ finish({ kind: 'answered', value: selected.length > 0 ? selected : null });
874
+ }
875
+ else {
876
+ finish({ kind: 'answered', value: action });
877
+ }
878
+ }, {
879
+ initiatorId: permCtx.userId,
880
+ fallbackCommand: 'ask',
881
+ timeoutMs: 20 * 60 * 1000,
882
+ onCancel: async (reason) => {
883
+ finish({ kind: 'cancelled', reason });
884
+ await cancellationClosed;
885
+ },
886
+ });
887
+ if (options.signal.aborted)
888
+ onAbort();
889
+ else
890
+ options.signal.addEventListener('abort', onAbort, { once: true });
891
+ if (waitMarked) {
892
+ permCtx?.interactionRouter?.unmarkWaiting(sessionId);
893
+ waitMarked = false;
894
+ }
895
+ });
896
+ if (settlement.kind === 'cancelled') {
897
+ try {
898
+ permCtx.cancelIntercept?.(sessionId);
899
+ try {
900
+ await permCtx.turn?.onInteractionSettled?.(requestId, settlement.reason === 'timeout' ? 'timed_out' : 'cancelled');
901
+ }
902
+ catch (error) {
903
+ logger.warn(`[AgentRunner] Failed to persist AskUserQuestion cancellation: ${error instanceof Error ? error.message : String(error)}`);
904
+ }
905
+ return {
906
+ behavior: 'deny',
907
+ message: `用户交互已取消(${settlement.reason})`,
908
+ interrupt: true,
909
+ decisionClassification: 'user_reject',
910
+ };
911
+ }
912
+ finally {
913
+ closeCancellation();
914
+ }
915
+ }
916
+ closeCancellation();
917
+ answers[q.question] = settlement.value ?? '';
918
+ try {
919
+ await permCtx.turn?.onInteractionSettled?.(requestId, 'answered');
920
+ }
921
+ catch (error) {
922
+ logger.warn(`[AgentRunner] Failed to persist AskUserQuestion answer: ${error instanceof Error ? error.message : String(error)}`);
923
+ }
924
+ }
925
+ if (waitMarked) {
926
+ permCtx?.interactionRouter?.unmarkWaiting(sessionId);
927
+ }
928
+ const updatedInput = { ...input, answers };
929
+ return { behavior: 'allow', updatedInput, decisionClassification: 'user_temporary' };
930
+ }
931
+ /**
932
+ * AskUserQuestion 纯文本 fallback:发送选项列表,等待用户通过 /ask 命令选择
933
+ * 注册到 interactionRouter,用户回复 /ask 1 或 /ask 自定义内容
934
+ */
935
+ async handleAskUserQuestionFallback(sessionId, input, questions, options) {
936
+ const permCtx = this.permissionContexts.get(sessionId);
937
+ const sendPrompt = permCtx?.adapter && permCtx?.channelId
938
+ ? async (text) => permCtx.adapter.send(buildEnvelope({ channel: permCtx.adapter.channelName, channelId: permCtx.channelId, replyContext: permCtx.replyContext }), { kind: 'result.text', text, isFinal: true })
939
+ : this.sendPromptFn;
940
+ const answers = {};
941
+ if (questions?.length) {
942
+ for (const q of questions) {
943
+ if (sendPrompt && permCtx?.interactionRouter) {
944
+ const requestId = `ask-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
945
+ const interaction = {
946
+ type: 'interaction',
947
+ id: requestId,
948
+ channelId: permCtx.channelId || '',
949
+ sessionId,
950
+ initiatorId: permCtx.userId,
951
+ kind: {
952
+ kind: 'action',
953
+ title: `💬 ${q.question}`,
954
+ body: q.options.map((o, i) => `${i + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n'),
955
+ buttons: q.options.map((o, i) => ({ key: `opt-${i}`, label: o.label })),
956
+ },
957
+ fallback: {
958
+ command: 'ask',
959
+ buttonArgMap: Object.fromEntries(q.options.map((_, i) => [`opt-${i}`, String(i + 1)])),
960
+ acceptFreeText: true,
961
+ freeTextHint: '或回复 /ask <自定义内容>',
962
+ },
963
+ };
964
+ await sendPrompt(renderActionAsText(interaction));
965
+ try {
966
+ await permCtx.turn?.onInteractionOpen?.({
967
+ interactionId: requestId,
968
+ kind: 'ask_user',
969
+ toolUseId: options.toolUseID,
970
+ requestId: options.requestId,
971
+ expiresAt: Date.now() + 20 * 60 * 1000,
972
+ });
973
+ }
974
+ catch (error) {
975
+ logger.warn(`[AgentRunner] Failed to persist AskUserQuestion fallback open interaction: ${error instanceof Error ? error.message : String(error)}`);
976
+ }
977
+ let closeCancellation;
978
+ const cancellationClosed = new Promise(resolve => { closeCancellation = resolve; });
979
+ const settlement = await new Promise((resolve) => {
980
+ let settled = false;
981
+ const finish = (value) => {
982
+ if (settled)
983
+ return;
984
+ settled = true;
985
+ options.signal.removeEventListener('abort', onAbort);
986
+ resolve(value);
987
+ };
988
+ const onAbort = () => {
989
+ void permCtx.interactionRouter?.cancel(requestId);
990
+ finish({ kind: 'cancelled', reason: 'aborted' });
991
+ };
992
+ permCtx.interactionRouter.register(requestId, sessionId, (action) => {
993
+ const num = parseInt(action.trim(), 10);
994
+ if (num >= 1 && num <= q.options.length) {
995
+ finish({ kind: 'answered', value: q.options[num - 1].label });
996
+ }
997
+ else {
998
+ finish({ kind: 'answered', value: action.trim() });
999
+ }
1000
+ }, {
1001
+ initiatorId: permCtx.userId,
1002
+ fallbackCommand: 'ask',
1003
+ timeoutMs: 20 * 60 * 1000,
1004
+ onCancel: async (reason) => {
1005
+ finish({ kind: 'cancelled', reason });
1006
+ await cancellationClosed;
1007
+ },
1008
+ });
1009
+ if (options.signal.aborted)
1010
+ onAbort();
1011
+ else
1012
+ options.signal.addEventListener('abort', onAbort, { once: true });
1013
+ });
1014
+ if (settlement.kind === 'cancelled') {
1015
+ try {
1016
+ try {
1017
+ await permCtx.turn?.onInteractionSettled?.(requestId, settlement.reason === 'timeout' ? 'timed_out' : 'cancelled');
1018
+ }
1019
+ catch (error) {
1020
+ logger.warn(`[AgentRunner] Failed to persist AskUserQuestion fallback cancellation: ${error instanceof Error ? error.message : String(error)}`);
1021
+ }
1022
+ return {
1023
+ behavior: 'deny',
1024
+ message: `用户交互已取消(${settlement.reason})`,
1025
+ interrupt: true,
1026
+ decisionClassification: 'user_reject',
1027
+ };
1028
+ }
1029
+ finally {
1030
+ closeCancellation();
1031
+ }
1032
+ }
1033
+ closeCancellation();
1034
+ answers[q.question] = settlement.value;
1035
+ try {
1036
+ await permCtx.turn?.onInteractionSettled?.(requestId, 'answered');
1037
+ }
1038
+ catch (error) {
1039
+ logger.warn(`[AgentRunner] Failed to persist AskUserQuestion fallback answer: ${error instanceof Error ? error.message : String(error)}`);
1040
+ }
1041
+ }
1042
+ else {
1043
+ const firstLabel = q.options[0]?.label || '';
1044
+ answers[q.question] = firstLabel;
1045
+ if (sendPrompt) {
1046
+ const optText = q.options.map((o, i) => ` ${i + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
1047
+ await sendPrompt(`💬 ${q.question}\n${optText}\n\n → 自动选择:${firstLabel}`);
1048
+ }
1049
+ }
1050
+ }
1051
+ }
1052
+ const updatedInput = { ...input, answers };
1053
+ return { behavior: 'allow', updatedInput, decisionClassification: 'user_temporary' };
1054
+ }
1055
+ /**
1056
+ * 处理 ExitPlanMode 工具调用:plan mode 审批,等待用户批准后才继续执行
1057
+ */
1058
+ async handleExitPlanMode(sessionId, input, options) {
1059
+ const permCtx = this.permissionContexts.get(sessionId);
1060
+ if (usesUnattendedInteractions(permCtx)) {
1061
+ logger.info(`[AgentRunner] interaction.auto_resolved kind=plan_approval session=${sessionId}`);
1062
+ return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
1063
+ }
1064
+ const sendPrompt = permCtx?.adapter && permCtx?.channelId
1065
+ ? async (text) => permCtx.adapter.send(buildEnvelope({ channel: permCtx.adapter.channelName, channelId: permCtx.channelId, replyContext: permCtx.replyContext }), { kind: 'result.text', text, isFinal: true })
1066
+ : this.sendPromptFn;
1067
+ // 无任何交互能力,直接 allow
1068
+ if (!permCtx?.channelId || !sendPrompt) {
1069
+ return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
1070
+ }
1071
+ // 立即暂停 idle 监控,不等卡片发完再 register
1072
+ permCtx.interactionRouter?.markWaiting(sessionId);
1073
+ const waitForPlanDecision = async (requestId) => {
1074
+ const router = permCtx.interactionRouter;
1075
+ try {
1076
+ await permCtx.turn?.onInteractionOpen?.({
1077
+ interactionId: requestId,
1078
+ kind: 'plan_approval',
1079
+ toolUseId: options.toolUseID,
1080
+ requestId: options.requestId,
1081
+ expiresAt: Date.now() + 20 * 60 * 1000,
1082
+ });
1083
+ }
1084
+ catch (error) {
1085
+ logger.warn(`[AgentRunner] Failed to persist ExitPlanMode open interaction: ${error instanceof Error ? error.message : String(error)}`);
1086
+ }
1087
+ let closeCancellation;
1088
+ const cancellationClosed = new Promise(resolve => { closeCancellation = resolve; });
1089
+ const settlement = await new Promise(resolve => {
1090
+ let settled = false;
1091
+ const finish = (value) => {
1092
+ if (settled)
1093
+ return;
1094
+ settled = true;
1095
+ options.signal.removeEventListener('abort', onAbort);
1096
+ resolve(value);
1097
+ };
1098
+ const deny = (message) => finish({
1099
+ kind: 'decision',
1100
+ state: 'denied',
1101
+ response: { behavior: 'deny', message, decisionClassification: 'user_reject' },
1102
+ });
1103
+ const onAbort = () => {
1104
+ void router.cancel(requestId);
1105
+ finish({ kind: 'cancelled', reason: 'aborted' });
1106
+ };
1107
+ router.register(requestId, sessionId, (action, values) => {
1108
+ const trimmed = action.trim();
1109
+ if (trimmed === 'cancel') {
1110
+ finish({ kind: 'cancelled', reason: 'explicit_cancel' });
1111
+ }
1112
+ else if (trimmed === '_custom_input') {
1113
+ const feedback = typeof values?.custom_text === 'string' ? values.custom_text.trim() : '';
1114
+ deny(feedback || '用户提交了反馈');
1115
+ }
1116
+ else if (trimmed === '_show_input') {
1117
+ if (permCtx.interceptNextMessage) {
1118
+ router.markWaiting(sessionId);
1119
+ sendPrompt?.('✏️ 请直接发送你的反馈意见').catch(() => { });
1120
+ permCtx.interceptNextMessage(sessionId, msg => {
1121
+ router.unmarkWaiting(sessionId);
1122
+ const feedback = (msg.content || '').trim();
1123
+ deny(feedback || '用户提交了反馈');
1124
+ });
1125
+ }
1126
+ else {
1127
+ deny('用户提交了反馈');
1128
+ }
1129
+ }
1130
+ else if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝') {
1131
+ deny('用户拒绝了计划');
1132
+ }
1133
+ else {
1134
+ finish({
1135
+ kind: 'decision',
1136
+ state: 'answered',
1137
+ response: { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' },
1138
+ });
1139
+ }
1140
+ }, {
1141
+ initiatorId: permCtx.userId,
1142
+ fallbackCommand: 'ask',
1143
+ timeoutMs: 20 * 60 * 1000,
1144
+ onCancel: async (reason) => {
1145
+ finish({ kind: 'cancelled', reason });
1146
+ await cancellationClosed;
1147
+ },
1148
+ });
1149
+ if (options.signal.aborted)
1150
+ onAbort();
1151
+ else
1152
+ options.signal.addEventListener('abort', onAbort, { once: true });
1153
+ });
1154
+ try {
1155
+ if (settlement.kind === 'cancelled') {
1156
+ permCtx.cancelIntercept?.(sessionId);
1157
+ try {
1158
+ await permCtx.turn?.onInteractionSettled?.(requestId, settlement.reason === 'timeout' ? 'timed_out' : 'cancelled');
1159
+ }
1160
+ catch (error) {
1161
+ logger.warn(`[AgentRunner] Failed to persist ExitPlanMode cancellation: ${error instanceof Error ? error.message : String(error)}`);
1162
+ }
1163
+ return {
1164
+ behavior: 'deny',
1165
+ message: `计划审批已取消(${settlement.reason})`,
1166
+ interrupt: true,
1167
+ decisionClassification: 'user_reject',
1168
+ };
1169
+ }
1170
+ try {
1171
+ await permCtx.turn?.onInteractionSettled?.(requestId, settlement.state);
1172
+ }
1173
+ catch (error) {
1174
+ logger.warn(`[AgentRunner] Failed to persist ExitPlanMode decision: ${error instanceof Error ? error.message : String(error)}`);
1175
+ }
1176
+ return settlement.response;
1177
+ }
1178
+ finally {
1179
+ closeCancellation();
1180
+ }
1181
+ };
1182
+ // 尝试发送交互卡片
1183
+ let cardSent = false;
1184
+ if (permCtx.adapter?.send) {
1185
+ // 发送计划内容:找 plans 目录中最新修改的 .md 文件
1186
+ if (sendPrompt) {
1187
+ try {
1188
+ const plansDir = path.join(process.env.HOME || '/root', '.claude', 'plans');
1189
+ const files = fs.readdirSync(plansDir)
1190
+ .filter((f) => f.endsWith('.md'))
1191
+ .map((f) => ({ name: f, mtime: fs.statSync(path.join(plansDir, f)).mtimeMs }))
1192
+ .sort((a, b) => b.mtime - a.mtime);
1193
+ if (files.length > 0) {
1194
+ const planContent = fs.readFileSync(path.join(plansDir, files[0].name), 'utf-8');
1195
+ if (planContent.trim()) {
1196
+ await sendPrompt(`📋 **计划内容**\n\n${planContent}`);
1197
+ }
1198
+ }
1199
+ }
1200
+ catch {
1201
+ // 读取失败不影响后续审批流程
1202
+ }
1203
+ }
1204
+ const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1205
+ const interaction = {
1206
+ type: 'interaction',
1207
+ id: requestId,
1208
+ kind: {
1209
+ kind: 'action',
1210
+ title: '📋 计划审批',
1211
+ body: 'AI 已完成规划,等待审批。\n请查看以上计划内容后决定。',
1212
+ buttons: [
1213
+ { key: 'approve', label: '✅ 批准执行', style: 'primary' },
1214
+ { key: 'reject', label: '❌ 拒绝', style: 'danger' },
1215
+ ],
1216
+ allowCustomInput: true,
1217
+ },
1218
+ channelId: permCtx.channelId,
1219
+ sessionId,
1220
+ initiatorId: permCtx.userId,
1221
+ fallback: {
1222
+ command: 'ask',
1223
+ buttonArgMap: { approve: '1', reject: '2' },
1224
+ },
1225
+ };
1226
+ try {
1227
+ await permCtx.flushPending?.();
1228
+ const envelope = buildEnvelope({
1229
+ taskId: permCtx.taskId,
1230
+ channel: permCtx.channel ?? permCtx.adapter.channelName,
1231
+ channelId: permCtx.channelId,
1232
+ agentName: permCtx.agentName,
1233
+ chatmode: permCtx.chatmode,
1234
+ replyContext: permCtx.replyContext,
1235
+ });
1236
+ const fallbackText = '📋 计划审批:AI 已完成规划,等待审批。\n回复 /ask 1 批准 / /ask 2 拒绝';
1237
+ const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
1238
+ cardSent = !!result;
1239
+ }
1240
+ catch (err) {
1241
+ logger.warn('[AgentRunner] ExitPlanMode card send failed:', err);
1242
+ }
1243
+ if (cardSent && permCtx.interactionRouter) {
1244
+ permCtx.interactionRouter?.unmarkWaiting(sessionId);
1245
+ return await waitForPlanDecision(requestId);
1246
+ }
1247
+ }
1248
+ // 文本 fallback:注册到 interactionRouter,等待用户 /ask 回复
1249
+ if (permCtx.interactionRouter) {
1250
+ const fallbackRequestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1251
+ const fallbackInteraction = {
1252
+ type: 'interaction',
1253
+ id: fallbackRequestId,
1254
+ channelId: permCtx.channelId || '',
1255
+ sessionId,
1256
+ initiatorId: permCtx.userId,
1257
+ kind: {
1258
+ kind: 'action',
1259
+ title: '📋 计划审批',
1260
+ body: 'AI 已完成规划,等待审批。',
1261
+ buttons: [
1262
+ { key: 'approve', label: '✅ 批准执行', style: 'primary' },
1263
+ { key: 'reject', label: '❌ 拒绝', style: 'danger' },
1264
+ ],
1265
+ },
1266
+ fallback: {
1267
+ command: 'ask',
1268
+ buttonArgMap: { approve: '1', reject: '2' },
1269
+ },
1270
+ };
1271
+ await permCtx.flushPending?.();
1272
+ await sendPrompt(renderActionAsText(fallbackInteraction));
1273
+ permCtx.interactionRouter.unmarkWaiting(sessionId);
1274
+ return await waitForPlanDecision(fallbackRequestId);
1275
+ }
1276
+ // 无交互能力,发提示后直接 allow
1277
+ permCtx?.interactionRouter?.unmarkWaiting(sessionId);
1278
+ await permCtx.flushPending?.();
1279
+ await sendPrompt('📋 计划审批\nAI 已完成规划,自动批准执行。');
1280
+ return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
1281
+ }
1282
+ /**
1283
+ * SDK 原始事件 → 标准 AgentEvent 转换
1284
+ * 所有 SDK 特有的事件类型引用封装在此方法内
1285
+ */
1286
+ async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel) {
1287
+ let lastSessionId;
1288
+ // tool_use_id → tool_name 映射,用于从 SDKUserMessage 的 tool_result 块中还原工具名
1289
+ const toolUseNames = new Map();
1290
+ let turnCount = 0;
1291
+ const seenMessageIds = new Set();
1292
+ let lastModelCall;
1293
+ let lastAssistantUuid;
1294
+ // 流式收集各次大模型调用(fallback:SDK iterations 为空时使用)
1295
+ const collectedCalls = [];
1296
+ try {
1297
+ for await (const event of sdkStream) {
1298
+ // 提取 session_id(任意 SDK 事件都可能携带)
1299
+ if (event.session_id && event.session_id !== lastSessionId) {
1300
+ lastSessionId = event.session_id;
1301
+ this.updateSessionId(sessionId, event.session_id);
1302
+ yield { type: 'session_id', sessionId: event.session_id };
1303
+ }
1304
+ if (event.type === 'user' && event.uuid === inputId) {
1305
+ yield { type: 'input_accepted', inputId };
1306
+ }
1307
+ if (event.type === 'stream_event') {
1308
+ const streamEvent = event.event;
1309
+ if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
1310
+ lastModelCall = {
1311
+ uuid: event.uuid,
1312
+ model: streamEvent.message.model,
1313
+ tokenUsage: streamEvent.message.usage,
1314
+ };
1315
+ // 流式收集:每个 message_start = 一次新的大模型调用
1316
+ collectedCalls.push({
1317
+ call_index: collectedCalls.length,
1318
+ model: streamEvent.message.model ?? callModel ?? this.model,
1319
+ request_id: event.request_id,
1320
+ tokenUsage: { ...streamEvent.message.usage },
1321
+ });
1322
+ }
1323
+ else if (streamEvent?.type === 'message_delta' && streamEvent.usage) {
1324
+ lastModelCall = {
1325
+ ...lastModelCall,
1326
+ uuid: lastModelCall?.uuid ?? event.uuid,
1327
+ tokenUsage: {
1328
+ ...(lastModelCall?.tokenUsage ?? {}),
1329
+ ...streamEvent.usage,
1330
+ },
1331
+ };
1332
+ // 将 message_delta 的 usage 合并进当前(最后一次)收集的调用
1333
+ const last = collectedCalls[collectedCalls.length - 1];
1334
+ if (last)
1335
+ last.tokenUsage = { ...last.tokenUsage, ...streamEvent.usage };
1336
+ }
1337
+ continue;
1338
+ }
1339
+ // system: compact_boundary → compact
1340
+ if (event.type === 'system' && event.subtype === 'compact_boundary') {
1341
+ yield {
1342
+ type: 'compact',
1343
+ preTokens: event.compact_metadata?.pre_tokens || 0,
1344
+ postTokens: event.compact_metadata?.post_tokens,
1345
+ durationMs: event.compact_metadata?.duration_ms,
1346
+ };
1347
+ }
1348
+ // system: task_progress → task_progress
1349
+ if (event.type === 'system' && event.subtype === 'task_progress') {
1350
+ yield {
1351
+ type: 'task_progress',
1352
+ summary: event.summary,
1353
+ toolUses: event.tool_uses,
1354
+ durationMs: event.duration_ms,
1355
+ };
1356
+ }
1357
+ // system: session_state_changed → state_changed
1358
+ if (event.type === 'system' && event.subtype === 'session_state_changed') {
1359
+ yield { type: 'state_changed', state: event.state };
1360
+ }
1361
+ // assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
1362
+ if (event.type === 'assistant' && event.message?.content) {
1363
+ lastAssistantUuid = event.uuid ?? lastAssistantUuid;
1364
+ const msgId = event.message.id;
1365
+ if (!msgId || !seenMessageIds.has(msgId)) {
1366
+ if (msgId)
1367
+ seenMessageIds.add(msgId);
1368
+ turnCount++;
1369
+ }
1370
+ if (event.message.usage) {
1371
+ lastModelCall = {
1372
+ ...lastModelCall,
1373
+ messageId: event.message.id,
1374
+ requestId: event.request_id,
1375
+ model: event.message.model,
1376
+ tokenUsage: {
1377
+ ...event.message.usage,
1378
+ ...(lastModelCall?.tokenUsage ?? {}),
1379
+ },
1380
+ };
1381
+ }
1382
+ // 统计本轮 base agent 全部输出字符数(text + tool_use input)
1383
+ let turnOutputChars = 0;
1384
+ for (const content of event.message.content) {
1385
+ if (content.type === 'tool_use') {
1386
+ const inputStr = typeof content.input === 'string' ? content.input : JSON.stringify(content.input || '');
1387
+ turnOutputChars += inputStr.length;
1388
+ }
1389
+ else if (content.type === 'text' && content.text) {
1390
+ turnOutputChars += content.text.length;
1391
+ }
1392
+ }
1393
+ for (const content of event.message.content) {
1394
+ if (content.type === 'tool_use') {
1395
+ if (content.id)
1396
+ toolUseNames.set(content.id, content.name);
1397
+ yield { type: 'tool_use', name: content.name, input: content.input, callId: content.id, turn: turnCount, outputTokens: turnOutputChars, assistantUuid: event.uuid };
1398
+ }
1399
+ else if (content.type === 'text' && content.text) {
1400
+ yield { type: 'text', text: content.text, outputTokens: turnOutputChars, turn: turnCount, assistantUuid: event.uuid };
1401
+ }
1402
+ }
1403
+ }
1404
+ // user: 提取 tool_result 块(SDK 将工具结果嵌套在 SDKUserMessage 中)
1405
+ if (event.type === 'user' && event.message?.content) {
1406
+ const contentArray = Array.isArray(event.message.content) ? event.message.content : [];
1407
+ for (const block of contentArray) {
1408
+ if (typeof block === 'object' && block !== null && block.type === 'tool_result') {
1409
+ const toolName = toolUseNames.get(block.tool_use_id) || '';
1410
+ const resultContent = typeof block.content === 'string'
1411
+ ? block.content
1412
+ : block.content != null ? JSON.stringify(block.content) : '';
1413
+ yield {
1414
+ type: 'tool_result',
1415
+ name: toolName,
1416
+ result: resultContent,
1417
+ isError: block.is_error === true,
1418
+ error: block.is_error === true ? resultContent : undefined,
1419
+ callId: block.tool_use_id,
1420
+ };
1421
+ }
1422
+ }
1423
+ }
1424
+ // result → complete(含 permission_denials 提取)
1425
+ if (event.type === 'result') {
1426
+ // 先发出被拒绝的权限事件
1427
+ if (Array.isArray(event.permission_denials)) {
1428
+ for (const denial of event.permission_denials) {
1429
+ yield {
1430
+ type: 'tool_result',
1431
+ name: denial.tool_name || '',
1432
+ result: '',
1433
+ isError: true,
1434
+ error: `权限被拒绝: ${denial.tool_name}`,
1435
+ };
1436
+ }
1437
+ }
1438
+ // 剥离 SDK result 中混入的 <thinking>...</thinking> 块
1439
+ const cleanResult = typeof event.result === 'string'
1440
+ ? event.result.replace(/<thinking>[\s\S]*?<\/thinking>\s*/g, '').trim()
1441
+ : event.result;
1442
+ // 从 usage 求当前上下文占用。
1443
+ // Claude:input_tokens 是净输入(不含 cache),三项求和 = 实际上下文长度。
1444
+ // 非 Claude(DeepSeek/OpenAI 兼容):cache_read 是服务端 KV cache 不占上下文窗口,
1445
+ // input_tokens 本身就是完整的上下文输入量。
1446
+ const u = event.usage;
1447
+ const effectiveModel = callModel ?? this.model;
1448
+ const isClaudeModel = isClaudeContextUsageModel(effectiveModel);
1449
+ const totalTokens = contextTokensForUsage(u, !!isClaudeModel);
1450
+ const contextWindowTokens = realContextWindowForModel(sdkModel);
1451
+ const autoCompactTokens = autoCompactWindowForModel(sdkModel);
1452
+ const contextUsage = totalTokens > 0 ? {
1453
+ totalTokens,
1454
+ maxTokens: contextWindowTokens,
1455
+ percentage: Math.round((totalTokens / contextWindowTokens) * 100),
1456
+ autoCompactTokens,
1457
+ model: callModel ?? this.model,
1458
+ effort: callEffort ?? this.effort,
1459
+ } : undefined;
1460
+ if (lastModelCall?.tokenUsage) {
1461
+ const lastUsageForContext = usageForContext(lastModelCall.tokenUsage);
1462
+ const lastTotalTokens = contextTokensForUsage(lastUsageForContext, !!isClaudeModel);
1463
+ lastModelCall = {
1464
+ ...lastModelCall,
1465
+ contextUsage: lastTotalTokens > 0 ? {
1466
+ totalTokens: lastTotalTokens,
1467
+ maxTokens: contextWindowTokens,
1468
+ percentage: Math.round((lastTotalTokens / contextWindowTokens) * 100),
1469
+ autoCompactTokens,
1470
+ model: callModel ?? this.model,
1471
+ effort: callEffort ?? this.effort,
1472
+ } : undefined,
1473
+ };
1474
+ }
1475
+ const contextUsageForCall = (usage) => {
1476
+ const callTotalTokens = contextTokensForUsage(usageForContext(usage), !!isClaudeModel);
1477
+ return callTotalTokens > 0 ? {
1478
+ totalTokens: callTotalTokens,
1479
+ maxTokens: contextWindowTokens,
1480
+ percentage: Math.round((callTotalTokens / contextWindowTokens) * 100),
1481
+ autoCompactTokens,
1482
+ model: callModel ?? this.model,
1483
+ effort: callEffort ?? this.effort,
1484
+ } : undefined;
1485
+ };
1486
+ // 组装 modelCalls:优先 SDK iterations,fallback 流式收集,兜底降级单行。
1487
+ const callModel_ = callModel ?? this.model;
1488
+ let modelCalls;
1489
+ const iterArr = Array.isArray(u?.iterations) && u.iterations.length > 0 ? u.iterations : null;
1490
+ if (iterArr) {
1491
+ modelCalls = iterArr.map((it, i) => ({
1492
+ call_index: i, model: callModel_, tokenUsage: it, contextUsage: contextUsageForCall(it),
1493
+ }));
1494
+ }
1495
+ else if (collectedCalls.length > 0) {
1496
+ modelCalls = collectedCalls.map(call => ({
1497
+ ...call,
1498
+ contextUsage: contextUsageForCall(call.tokenUsage),
1499
+ }));
1500
+ }
1501
+ else if (u) {
1502
+ // 降级:无逐次数据,写一条累计行
1503
+ modelCalls = [{ call_index: 0, model: callModel_, tokenUsage: u, contextUsage: contextUsageForCall(u), degraded: true }];
1504
+ }
1505
+ yield {
1506
+ type: 'complete',
1507
+ result: cleanResult,
1508
+ subtype: event.subtype,
1509
+ isError: event.is_error,
1510
+ errors: event.errors,
1511
+ durationMs: event.duration_ms,
1512
+ ttftMs: event.ttft_ms,
1513
+ costUsd: event.total_cost_usd,
1514
+ terminalReason: event.terminal_reason,
1515
+ sessionTitle: event.session_title,
1516
+ numTurns: event.num_turns,
1517
+ tokenUsage: event.usage,
1518
+ contextUsage,
1519
+ lastModelCall,
1520
+ modelCalls,
1521
+ assistantUuid: lastAssistantUuid,
1522
+ };
1523
+ // result 是 SDK 流的终结事件,不再等待后续(防止 interrupt 后流不关闭导致挂起)
1524
+ return;
1525
+ }
1526
+ }
1527
+ }
1528
+ catch (err) {
1529
+ // 子进程崩溃(如 exited with code 1)时,把缓冲的 stderr 打出来还原真实原因。
1530
+ // SDK 包装后的错误信息不含子进程实际报错,缓冲区才是根因所在。
1531
+ const buf = this.recentStderr.get(sessionId);
1532
+ if (buf && buf.length > 0) {
1533
+ logger.error(`[AgentRunner] Subprocess stream failed (session=${sessionId}). Last ${buf.length} stderr line(s):\n${buf.join('\n')}`);
1534
+ }
1535
+ else {
1536
+ logger.error(`[AgentRunner] Subprocess stream failed (session=${sessionId}) with no captured stderr.`);
1537
+ }
1538
+ throw err;
1539
+ }
1540
+ finally {
1541
+ this.recentStderr.delete(sessionId);
1542
+ }
1543
+ }
1544
+ async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
1545
+ // 记录当前 evolcore session ID,用于 Agent ctl 环境变量注入
1546
+ // 同步用户级配置到内存
1547
+ this.syncFromUserSettings();
1548
+ // 异步刷新模型别名缓存(fire-and-forget,不阻塞查询)
1549
+ if (this.baseUrl) {
1550
+ const cacheKey = normalizeModelGatewayUrl(this.baseUrl);
1551
+ const cached = modelAliasCache.get(cacheKey);
1552
+ if (!cached || (Date.now() - cached.fetchedAt > MODEL_ALIAS_TTL_MS)) {
1553
+ refreshModelAliases(this.baseUrl, this.apiKey);
1554
+ }
1555
+ // 顺带预热网关价格缓存(1h TTL),让本轮结束写库时已就绪
1556
+ const pricing = gatewayPricingCache.get(this.baseUrl);
1557
+ if (!pricing || (Date.now() - pricing.fetchedAt > GATEWAY_PRICING_TTL_MS)) {
1558
+ refreshGatewayPricing(this.baseUrl, this.apiKey);
1559
+ }
1560
+ }
1561
+ ensureDir(projectPath);
1562
+ ensureDir(path.join(projectPath, '.claude'));
1563
+ // 优先使用传入的 agentSessionId(从数据库恢复),否则使用内存中的
1564
+ let agentSessionId = initialClaudeSessionId || this.activeSessions.get(sessionId);
1565
+ // 验证会话文件是否存在且有效(仅在有 agentSessionId 时)
1566
+ if (agentSessionId) {
1567
+ const homeDir = os.homedir();
1568
+ const encodedProjectPath = encodePath(projectPath);
1569
+ const sessionFile = path.join(homeDir, '.claude', 'projects', encodedProjectPath, `${agentSessionId}.jsonl`);
1570
+ let isValid = false;
1571
+ if (fs.existsSync(sessionFile)) {
1572
+ try {
1573
+ const content = fs.readFileSync(sessionFile, 'utf-8');
1574
+ const lines = content.split('\n').filter(l => l.trim());
1575
+ // 查找第一个包含 sessionId 和 version 的行(跳过 queue-operation)
1576
+ for (const line of lines) {
1577
+ try {
1578
+ const data = JSON.parse(line);
1579
+ if (data.sessionId && data.version) {
1580
+ isValid = true;
1581
+ break;
1582
+ }
1583
+ }
1584
+ catch { }
1585
+ }
1586
+ if (!isValid) {
1587
+ logger.warn(`[AgentRunner] Session file missing session data: ${sessionFile}`);
1588
+ }
1589
+ }
1590
+ catch (error) {
1591
+ logger.warn(`[AgentRunner] Session file corrupted: ${sessionFile}`);
1592
+ }
1593
+ }
1594
+ if (!isValid) {
1595
+ logger.warn(`[AgentRunner] Invalid session file, starting new session`);
1596
+ agentSessionId = undefined;
1597
+ this.activeSessions.delete(sessionId);
1598
+ if (this.onSessionIdUpdate) {
1599
+ this.onSessionIdUpdate(sessionId, '');
1600
+ }
1601
+ }
1602
+ }
1603
+ // PreCompact Hook - 在压缩开始时触发
1604
+ const preCompactHook = async () => {
1605
+ if (this.onCompactStart) {
1606
+ this.onCompactStart(sessionId);
1607
+ }
1608
+ return {};
1609
+ };
1610
+ // 本次调用使用的权限模式:优先 permissionModeOverride(message-processor 按 关系>角色>出厂默认 解析后传入),
1611
+ // 缺省回落 agent 级 this.permissionMode。作为 per-call 入参(hook/canUseTool 闭包捕获),
1612
+ // 不写实例字段,多对端并发互不污染(与 model/effort 同构)。
1613
+ const requestedPermissionMode = modelOverride?.permissionMode || this.permissionMode;
1614
+ const normalizedPermission = normalizePermissionMode(requestedPermissionMode);
1615
+ const callPermissionMode = normalizedPermission.mode;
1616
+ const runPermissionContext = this.permissionContexts.get(sessionId);
1617
+ const executionSandbox = resolvePhaseOneExecutionSandbox(callPermissionMode, runPermissionContext?.role);
1618
+ const permissionPrompt = runPermissionContext?.sendPrompt ?? this.sendPromptFn;
1619
+ const permissionContext = (signal) => {
1620
+ const context = this.permissionContexts.get(sessionId);
1621
+ return context && signal ? { ...context, abortSignal: signal } : context;
1622
+ };
1623
+ const hookDecision = (decision, reason, updatedInput) => ({
1624
+ hookSpecificOutput: {
1625
+ hookEventName: 'PreToolUse',
1626
+ permissionDecision: decision,
1627
+ ...(reason ? { permissionDecisionReason: reason } : {}),
1628
+ ...(updatedInput ? { updatedInput } : {}),
1629
+ },
1630
+ });
1631
+ // A rewritten helper command is trusted only after this hook produced its
1632
+ // random one-shot payload from a validated output wrapper.
1633
+ const trustedSafeOutputCommands = new Set();
1634
+ // PreToolUse Hook - 黑名单检查 + input 修正(不可绕过,所有模式都走)
1635
+ const preToolUseHook = async (input) => {
1636
+ const toolName = input.tool_name;
1637
+ const originalInput = (input.tool_input || {});
1638
+ let toolInput = originalInput;
1639
+ if (toolName === 'Bash' && 'dangerouslyDisableSandbox' in toolInput) {
1640
+ toolInput = { ...toolInput };
1641
+ delete toolInput.dangerouslyDisableSandbox;
1642
+ }
1643
+ // Remove fields known to be rejected by the SDK schema without granting
1644
+ // permission as a side effect. The cleaned input still goes through the
1645
+ // complete policy below.
1646
+ const sanitizeRules = {
1647
+ 'EnterPlanMode': ['reason'],
1648
+ 'ExitPlanMode': ['reason'],
1649
+ 'ExitWorktree': ['reason'],
1650
+ };
1651
+ const fieldsToRemove = sanitizeRules[toolName];
1652
+ if (fieldsToRemove?.some(field => field in toolInput)) {
1653
+ toolInput = { ...toolInput };
1654
+ for (const field of fieldsToRemove)
1655
+ delete toolInput[field];
1656
+ }
1657
+ if (toolName === 'Read' && toolInput.pages === '') {
1658
+ if (toolInput === originalInput)
1659
+ toolInput = { ...toolInput };
1660
+ delete toolInput.pages;
1661
+ }
1662
+ let updatedInput = toolInput === originalInput ? undefined : toolInput;
1663
+ const preparedSafeOutputCommand = toolName === 'Bash' && typeof toolInput.command === 'string'
1664
+ ? toolInput.command
1665
+ : undefined;
1666
+ if (preparedSafeOutputCommand && trustedSafeOutputCommands.has(preparedSafeOutputCommand)) {
1667
+ return hookDecision('allow', undefined, {
1668
+ ...toolInput,
1669
+ dangerouslyDisableSandbox: true,
1670
+ });
1671
+ }
1672
+ // proactive 模式行为策略(首次工具调用必须是 ec msg send)
1673
+ const policyResult = this.permissionContexts.get(sessionId)?.policyHook?.(toolName, toolInput);
1674
+ if (policyResult?.block) {
1675
+ return hookDecision('deny', policyResult.reason || '当前会话策略拒绝此工具调用', updatedInput);
1676
+ }
1677
+ const permCtx = this.permissionContexts.get(sessionId);
1678
+ const commandBeforePreflight = toolName === 'Bash' && typeof toolInput.command === 'string'
1679
+ ? toolInput.command
1680
+ : undefined;
1681
+ const preflight = evaluateToolPreflight(toolName, toolInput, {
1682
+ sessionId,
1683
+ channel: permCtx?.channel,
1684
+ userId: permCtx?.userId,
1685
+ role: permCtx?.role,
1686
+ permissionMode: callPermissionMode,
1687
+ allowHostProcessCommands: true,
1688
+ safeOutputHelperPath: path.join(getPackageRoot(), 'bin', 'ec-safe-output.js'),
1689
+ projectPath,
1690
+ });
1691
+ toolInput = preflight.input;
1692
+ if (toolInput !== originalInput)
1693
+ updatedInput = toolInput;
1694
+ if (preflight.behavior === 'deny')
1695
+ return hookDecision('deny', preflight.message, updatedInput);
1696
+ if (preflight.behavior === 'allow') {
1697
+ const commandAfterPreflight = typeof toolInput.command === 'string' ? toolInput.command : undefined;
1698
+ if (commandAfterPreflight && commandAfterPreflight !== commandBeforePreflight) {
1699
+ trustedSafeOutputCommands.add(commandAfterPreflight);
1700
+ }
1701
+ return hookDecision('allow', undefined, {
1702
+ ...toolInput,
1703
+ dangerouslyDisableSandbox: true,
1704
+ });
1705
+ }
1706
+ // These tools need the dedicated canUseTool interaction handlers below.
1707
+ if (toolName === 'AskUserQuestion' || toolName === 'ExitPlanMode') {
1708
+ return hookDecision('defer', undefined, updatedInput);
1709
+ }
1710
+ if (callPermissionMode === 'readonly') {
1711
+ const permCtx = this.permissionContexts.get(sessionId);
1712
+ const readonlyContext = {
1713
+ sessionId,
1714
+ channel: permCtx?.channel,
1715
+ peerId: permCtx?.userId,
1716
+ role: permCtx?.role,
1717
+ allowLiteralReadShell: true,
1718
+ };
1719
+ const roResult = checkReadonly(toolName, toolInput, projectPath, readonlyContext);
1720
+ if (roResult.behavior === 'deny') {
1721
+ return hookDecision('deny', roResult.message, updatedInput);
1722
+ }
1723
+ return hookDecision('allow', undefined, updatedInput);
1724
+ }
1725
+ if (isClaudeMcpTool(toolName)) {
1726
+ return hookDecision('allow', undefined, updatedInput);
1727
+ }
1728
+ if (isUnknownClaudeTool(toolName)) {
1729
+ if (callPermissionMode === 'auto') {
1730
+ return hookDecision('deny', `auto 模式拒绝未知工具 ${toolName}:该工具不受 Claude 本地 sandbox 的完整约束`, updatedInput);
1731
+ }
1732
+ // request/bypass only receive external tools admitted by the session's
1733
+ // capability configuration. Admission is server/app-wide, not per tool.
1734
+ return hookDecision('allow', undefined, updatedInput);
1735
+ }
1736
+ const danger = checkDangerousCommand(toolName, toolInput);
1737
+ if (danger.isDangerous) {
1738
+ if (callPermissionMode === 'auto') {
1739
+ return hookDecision('deny', `危险操作已由 auto 模式拒绝:${danger.reason}`, updatedInput);
1740
+ }
1741
+ if (callPermissionMode === 'request' || callPermissionMode === 'bypass') {
1742
+ const approval = await requestDangerousCommandPermission(this.permissionGateway, sessionId, toolName, toolInput, permissionPrompt, permissionContext(), `claude:${callPermissionMode}`, callPermissionMode);
1743
+ if (approval.matched && approval.decision === 'deny') {
1744
+ return hookDecision('deny', '危险操作未获人工批准', updatedInput);
1745
+ }
1746
+ }
1747
+ return hookDecision('allow', undefined, updatedInput);
1748
+ }
1749
+ if (callPermissionMode === 'auto' || callPermissionMode === 'request' || callPermissionMode === 'bypass') {
1750
+ return hookDecision('allow', undefined, updatedInput);
1751
+ }
1752
+ return hookDecision('deny', '未知权限模式已拒绝工具调用', updatedInput);
1753
+ };
1754
+ // PermissionDenied Hook - auto 模式下 SDK 拒绝操作时通知用户
1755
+ const permissionDeniedHook = async (input) => {
1756
+ if (callPermissionMode === 'auto' && permissionPrompt) {
1757
+ const toolName = input.tool_name || '未知工具';
1758
+ const reason = input.reason || 'AI 判断此操作有风险';
1759
+ const message = `⚠️ 操作已自动拦截\n工具: ${toolName}\n原因: ${reason}`;
1760
+ try {
1761
+ await permissionPrompt(message);
1762
+ }
1763
+ catch (err) {
1764
+ logger.error('[PermissionDenied] Failed to send notification:', err);
1765
+ }
1766
+ }
1767
+ return {};
1768
+ };
1769
+ // SDK-level canUseTool 回调:接入 PermissionGateway 的用户审批入口
1770
+ // 黑名单已在 PreToolUse hook 拦截,危险命令在此处进入审批流程
1771
+ const canUseToolCallback = async (toolName, input, options) => {
1772
+ // 特殊处理:AskUserQuestion 工具(SDK 内置的用户交互工具)
1773
+ // 这不是权限审批,而是收集用户答案,需要构造表单卡片
1774
+ if (toolName === 'AskUserQuestion') {
1775
+ return await this.handleAskUserQuestion(sessionId, input, options);
1776
+ }
1777
+ // 特殊处理:ExitPlanMode 工具(plan mode 审批)
1778
+ if (toolName === 'ExitPlanMode') {
1779
+ return await this.handleExitPlanMode(sessionId, input, options);
1780
+ }
1781
+ if (toolName === 'Bash' && 'dangerouslyDisableSandbox' in input) {
1782
+ input = { ...input };
1783
+ delete input.dangerouslyDisableSandbox;
1784
+ }
1785
+ const preparedSafeOutputCommand = toolName === 'Bash' && typeof input.command === 'string'
1786
+ ? input.command
1787
+ : undefined;
1788
+ if (preparedSafeOutputCommand && trustedSafeOutputCommands.delete(preparedSafeOutputCommand)) {
1789
+ return {
1790
+ behavior: 'allow',
1791
+ updatedInput: { ...input, dangerouslyDisableSandbox: true },
1792
+ decisionClassification: 'user_permanent',
1793
+ };
1794
+ }
1795
+ const permCtx = this.permissionContexts.get(sessionId);
1796
+ const preflight = evaluateToolPreflight(toolName, input, {
1797
+ sessionId,
1798
+ channel: permCtx?.channel,
1799
+ userId: permCtx?.userId,
1800
+ role: permCtx?.role,
1801
+ permissionMode: callPermissionMode,
1802
+ allowHostProcessCommands: true,
1803
+ safeOutputHelperPath: path.join(getPackageRoot(), 'bin', 'ec-safe-output.js'),
1804
+ projectPath,
1805
+ });
1806
+ input = preflight.input;
1807
+ if (preflight.behavior === 'deny') {
1808
+ return { behavior: 'deny', message: preflight.message, decisionClassification: 'user_reject' };
1809
+ }
1810
+ if (preflight.behavior === 'allow') {
1811
+ return {
1812
+ behavior: 'allow',
1813
+ updatedInput: { ...input, dangerouslyDisableSandbox: true },
1814
+ decisionClassification: 'user_permanent',
1815
+ };
1816
+ }
1817
+ const permissionExpansion = inspectClaudePermissionExpansion(input, options, projectPath);
1818
+ if (permissionExpansion.protectedReason) {
1819
+ return {
1820
+ behavior: 'deny',
1821
+ message: permissionExpansion.protectedReason,
1822
+ decisionClassification: 'user_reject',
1823
+ };
1824
+ }
1825
+ if (permissionExpansion.requiresApproval) {
1826
+ if (callPermissionMode === 'readonly' || callPermissionMode === 'auto') {
1827
+ return {
1828
+ behavior: 'deny',
1829
+ message: `${callPermissionMode} 模式拒绝 Claude SDK 文件系统或未知权限扩权请求`,
1830
+ decisionClassification: 'user_reject',
1831
+ };
1832
+ }
1833
+ if (callPermissionMode === 'request' || callPermissionMode === 'bypass') {
1834
+ if (!this.permissionGateway || !permissionPrompt) {
1835
+ return {
1836
+ behavior: 'deny',
1837
+ message: '扩权审批通道不可用,操作已拒绝',
1838
+ decisionClassification: 'user_reject',
1839
+ };
1840
+ }
1841
+ const expansionSummary = options.title
1842
+ || options.description
1843
+ || summarizeToolInput(toolName, input);
1844
+ const expansionDecision = await this.permissionGateway.requestPermission(sessionId, toolName, permissionExpansion.approvalInput, permissionPrompt, permissionContext(options.signal), expansionSummary, options.decisionReason || 'Claude SDK 请求扩大当前会话权限', `claude:${callPermissionMode}`);
1845
+ if (expansionDecision === 'deny') {
1846
+ return {
1847
+ behavior: 'deny',
1848
+ message: 'Claude SDK 扩权未获人工批准',
1849
+ decisionClassification: 'user_reject',
1850
+ };
1851
+ }
1852
+ return {
1853
+ behavior: 'allow',
1854
+ updatedInput: input,
1855
+ decisionClassification: 'user_temporary',
1856
+ };
1857
+ }
1858
+ }
1859
+ // Defensive duplicate of PreToolUse enforcement for SDK edge cases.
1860
+ if (callPermissionMode === 'readonly') {
1861
+ const permCtx = this.permissionContexts.get(sessionId);
1862
+ const readonlyContext = {
1863
+ sessionId,
1864
+ channel: permCtx?.channel,
1865
+ peerId: permCtx?.userId,
1866
+ role: permCtx?.role,
1867
+ allowLiteralReadShell: true,
1868
+ };
1869
+ const roResult = checkReadonly(toolName, input, projectPath, readonlyContext);
1870
+ if (roResult.behavior === 'deny') {
1871
+ return { behavior: 'deny', message: roResult.message, decisionClassification: 'user_reject' };
1872
+ }
1873
+ return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
1874
+ }
1875
+ if (isClaudeMcpTool(toolName)) {
1876
+ return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
1877
+ }
1878
+ if (isUnknownClaudeTool(toolName)) {
1879
+ if (callPermissionMode === 'auto') {
1880
+ return {
1881
+ behavior: 'deny',
1882
+ message: `auto 模式拒绝未知工具 ${toolName}:该工具不受 Claude 本地 sandbox 的完整约束`,
1883
+ decisionClassification: 'user_reject',
1884
+ };
1885
+ }
1886
+ return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
1887
+ }
1888
+ const danger = checkDangerousCommand(toolName, input);
1889
+ if (danger.isDangerous) {
1890
+ if (callPermissionMode === 'auto') {
1891
+ return { behavior: 'deny', message: `auto 模式拒绝危险操作:${danger.reason}`, decisionClassification: 'user_reject' };
1892
+ }
1893
+ if (callPermissionMode === 'request' || callPermissionMode === 'bypass') {
1894
+ const dangerDecision = await requestDangerousCommandPermission(this.permissionGateway, sessionId, toolName, input, permissionPrompt, permissionContext(options.signal), `claude:${callPermissionMode}`, callPermissionMode);
1895
+ if (dangerDecision.matched && dangerDecision.decision === 'deny') {
1896
+ return { behavior: 'deny', message: '危险操作未获人工批准', decisionClassification: 'user_reject' };
1897
+ }
1898
+ return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
1899
+ }
1900
+ return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
1901
+ }
1902
+ if (callPermissionMode === 'auto' || callPermissionMode === 'bypass') {
1903
+ return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_permanent' };
1904
+ }
1905
+ // request mode must fail closed if the EvolCore approval channel is not
1906
+ // installed; otherwise the SDK escalation callback becomes a bypass.
1907
+ if (!this.permissionGateway || !permissionPrompt) {
1908
+ return { behavior: 'deny', message: '审批通道不可用,操作已拒绝', decisionClassification: 'user_reject' };
1909
+ }
1910
+ const summary = options.title
1911
+ || options.description
1912
+ || summarizeToolInput(toolName, input);
1913
+ const decision = await this.permissionGateway.requestPermission(sessionId, toolName, input, permissionPrompt, permissionContext(options.signal), summary, options.decisionReason, `claude:${callPermissionMode}`);
1914
+ if (decision === 'deny') {
1915
+ return { behavior: 'deny', message: '用户拒绝或审批超时', decisionClassification: 'user_reject' };
1916
+ }
1917
+ return {
1918
+ behavior: 'allow',
1919
+ updatedInput: input,
1920
+ decisionClassification: 'user_temporary'
1921
+ };
1922
+ };
1923
+ const useSettingSources = this.config?.agents?.claude?.useSettingSources !== false;
1924
+ const settingSources = useSettingSources ? ['project', 'user'] : [];
1925
+ const enableSummaries = this.config?.agents?.claude?.agentProgressSummaries !== false;
1926
+ const excludeDynamic = this.config?.agents?.claude?.excludeDynamicSections === true;
1927
+ // 公共 options(新旧模式共用)
1928
+ const sdkPermissionMode = this.toSdkPermissionMode(requestedPermissionMode);
1929
+ // 本次调用使用的模型/强度:优先 modelOverride(message-processor 按 关系>agent>全局 解析后传入),
1930
+ // 缺省回落 agent 级 this.model。作为 per-call 入参传入,无共享状态,多对端并发互不污染。
1931
+ const callModel = modelOverride?.model || this.model;
1932
+ const callEffort = (modelOverride?.effort ?? this.effort);
1933
+ const callSessionTitle = sanitizeSessionTitle(modelOverride?.sessionTitle);
1934
+ logger.info(`[AgentRunner] runQuery model=${callModel} effort=${callEffort ?? 'auto'} ` +
1935
+ `permMode=${requestedPermissionMode}->${callPermissionMode} role=${runPermissionContext?.role ?? 'none'} ` +
1936
+ `sandbox=${executionSandbox.state} sdkMode=${sdkPermissionMode}`);
1937
+ if (systemPromptAppend) {
1938
+ logger.info(`[AgentRunner] systemPromptAppend: ${systemPromptAppend.length} chars`);
1939
+ }
1940
+ else {
1941
+ logger.info(`[AgentRunner] systemPromptAppend: none`);
1942
+ }
1943
+ const sdkModel = resolveSdkModel(callModel, this.baseUrl);
1944
+ const capabilityOptions = await this.resolveCapabilityRunOptions(projectPath);
1945
+ const managedSettings = buildClaudeManagedLockdownSettings(capabilityOptions);
1946
+ const sandboxOptions = executionSandbox.state === 'off'
1947
+ ? { enabled: false }
1948
+ : await (async () => {
1949
+ const sandboxProjectionStartedAt = Date.now();
1950
+ ensureClaudeGitWorktreeConfig(projectPath);
1951
+ const sandboxFilesystem = buildClaudeProtectedFilesystem(resolvePaths().root);
1952
+ const sandboxUnixSockets = buildClaudeUnixSocketAllowlist(projectPath, {
1953
+ role: runPermissionContext?.role,
1954
+ });
1955
+ await assertClaudeSettingSourcesHaveLiteralSandboxPaths(projectPath, [...settingSources], managedSettings);
1956
+ const sandboxProjectionDurationMs = Date.now() - sandboxProjectionStartedAt;
1957
+ if (sandboxProjectionDurationMs > 2_000) {
1958
+ throw new Error(`[ClaudeSandbox] protected filesystem projection exceeded 2000ms: ${sandboxProjectionDurationMs}ms`);
1959
+ }
1960
+ logger.info(`[ClaudeSandbox] projection ready denyRead=${sandboxFilesystem.denyRead.length} ` +
1961
+ `denyWrite=${sandboxFilesystem.denyWrite.length} durationMs=${sandboxProjectionDurationMs}`);
1962
+ return {
1963
+ enabled: true,
1964
+ failIfUnavailable: shouldFailIfClaudeSandboxUnavailable(),
1965
+ allowUnsandboxedCommands: true,
1966
+ network: {
1967
+ allowUnixSockets: sandboxUnixSockets,
1968
+ allowAllUnixSockets: false,
1969
+ allowLocalBinding: false,
1970
+ },
1971
+ filesystem: sandboxFilesystem,
1972
+ };
1973
+ })();
1974
+ if (executionSandbox.state === 'off') {
1975
+ logger.info('[ClaudeSandbox] disabled for authenticated owner bypass task');
1976
+ }
1977
+ const commonOptions = {
1978
+ cwd: projectPath,
1979
+ model: sdkModel,
1980
+ ...capabilityOptions,
1981
+ strictMcpConfig: false,
1982
+ managedSettings,
1983
+ ...(callEffort ? { effort: callEffort } : {}),
1984
+ ...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
1985
+ autoCompactWindow: autoCompactWindowForModel(sdkModel),
1986
+ advisorModel: 'haiku',
1987
+ canUseTool: canUseToolCallback,
1988
+ permissionMode: sdkPermissionMode,
1989
+ sandbox: sandboxOptions,
1990
+ persistSession: true,
1991
+ includePartialMessages: true,
1992
+ enableFileCheckpointing: true,
1993
+ hooks: {
1994
+ PreCompact: [{ matcher: '.*', hooks: [preCompactHook] }],
1995
+ PreToolUse: [{ matcher: '.*', hooks: [preToolUseHook] }],
1996
+ PermissionDenied: [{ matcher: '.*', hooks: [permissionDeniedHook] }]
1997
+ },
1998
+ ...(enableSummaries ? { agentProgressSummaries: true } : {}),
1999
+ stderr: (msg) => {
2000
+ const trimmed = msg.trim();
2001
+ if (trimmed) {
2002
+ // 环形缓冲:保留最近 N 行,供子进程崩溃时还原真实原因
2003
+ let buf = this.recentStderr.get(sessionId);
2004
+ if (!buf) {
2005
+ buf = [];
2006
+ this.recentStderr.set(sessionId, buf);
2007
+ }
2008
+ buf.push(trimmed);
2009
+ if (buf.length > AgentRunner.STDERR_BUFFER_MAX)
2010
+ buf.shift();
2011
+ }
2012
+ if (msg.includes('[ERROR]') || msg.includes('[WARN]') || msg.includes('Stream started')) {
2013
+ logger.info(`[Claude-stderr] ${trimmed}`);
2014
+ }
2015
+ else {
2016
+ logger.debug(`[Claude-stderr] ${trimmed}`);
2017
+ }
2018
+ },
2019
+ env: this.getAgentEnv(runtimeEnv, sessionId)
2020
+ };
2021
+ const createQuery = (promptInput, resumeSessionId, resumeAt) => {
2022
+ if (useSettingSources) {
2023
+ return query({
2024
+ prompt: promptInput,
2025
+ options: {
2026
+ ...commonOptions,
2027
+ ...(!resumeSessionId && callSessionTitle ? { title: callSessionTitle } : {}),
2028
+ settingSources: [...settingSources],
2029
+ systemPrompt: {
2030
+ type: 'preset',
2031
+ preset: 'claude_code',
2032
+ ...(excludeDynamic ? { excludeDynamicSections: true } : {}),
2033
+ ...(systemPromptAppend ? { append: systemPromptAppend } : {})
2034
+ },
2035
+ ...(resumeSessionId ? { resume: resumeSessionId } : {}),
2036
+ ...(resumeAt ? { resumeSessionAt: resumeAt } : {}),
2037
+ }
2038
+ });
2039
+ }
2040
+ else {
2041
+ const globalClaudeMd = (() => {
2042
+ try {
2043
+ const globalPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
2044
+ if (fs.existsSync(globalPath)) {
2045
+ return fs.readFileSync(globalPath, 'utf-8').trim();
2046
+ }
2047
+ }
2048
+ catch { }
2049
+ return '';
2050
+ })();
2051
+ const projectClaudeMds = [
2052
+ path.join(projectPath, 'CLAUDE.md'),
2053
+ path.join(projectPath, '.claude', 'CLAUDE.md'),
2054
+ ].map(p => {
2055
+ try {
2056
+ return fs.existsSync(p) ? fs.readFileSync(p, 'utf-8').trim() : '';
2057
+ }
2058
+ catch {
2059
+ return '';
2060
+ }
2061
+ }).filter(Boolean);
2062
+ const fullAppend = [
2063
+ ...projectClaudeMds,
2064
+ globalClaudeMd,
2065
+ systemPromptAppend,
2066
+ ].filter(Boolean).join('\n\n');
2067
+ return query({
2068
+ prompt: promptInput,
2069
+ options: {
2070
+ ...commonOptions,
2071
+ ...(!resumeSessionId && callSessionTitle ? { title: callSessionTitle } : {}),
2072
+ settingSources: [],
2073
+ ...(resumeSessionId ? { resume: resumeSessionId } : {}),
2074
+ ...(resumeAt ? { resumeSessionAt: resumeAt } : {}),
2075
+ ...(fullAppend ? {
2076
+ systemPrompt: {
2077
+ type: 'preset',
2078
+ preset: 'claude_code',
2079
+ append: fullAppend
2080
+ }
2081
+ } : {}),
2082
+ }
2083
+ });
2084
+ }
2085
+ };
2086
+ // 检查待处理的 resumeAt(由 /rewind N chat 设置)
2087
+ let resumeAt;
2088
+ if (sessionManager && agentSessionId) {
2089
+ try {
2090
+ const currentSession = await sessionManager.getSessionById?.(sessionId);
2091
+ if (currentSession?.metadata?.resumeAt) {
2092
+ resumeAt = currentSession.metadata.resumeAt;
2093
+ const newMeta = { ...currentSession.metadata };
2094
+ delete newMeta.resumeAt;
2095
+ await sessionManager.updateSession(sessionId, { metadata: newMeta });
2096
+ logger.info(`[AgentRunner] Consuming resumeAt: ${resumeAt}`);
2097
+ }
2098
+ }
2099
+ catch (err) {
2100
+ logger.warn('[AgentRunner] Failed to check resumeAt:', err);
2101
+ }
2102
+ }
2103
+ let sdkStream;
2104
+ const msgStream = new MessageStream();
2105
+ const inputId = modelOverride?.turn?.inputId ?? crypto.randomUUID();
2106
+ if (images && images.length > 0) {
2107
+ logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
2108
+ logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
2109
+ msgStream.push(prompt, images, inputId);
2110
+ msgStream.end();
2111
+ sdkStream = createQuery(msgStream);
2112
+ }
2113
+ else {
2114
+ logger.debug('[AgentRunner] Creating query with text only, agentSessionId:', initialClaudeSessionId);
2115
+ msgStream.push(prompt, undefined, inputId);
2116
+ sdkStream = createQuery(msgStream, agentSessionId, resumeAt);
2117
+ }
2118
+ this.activeMessageStreams.set(sessionId, msgStream);
2119
+ this.activeQueries.set(sessionId, sdkStream);
2120
+ const donePromise = new Promise(resolve => this.streamDoneResolvers.set(sessionId, resolve));
2121
+ this.streamDone.set(sessionId, donePromise);
2122
+ // 保存 interrupt 能力(不写 activeStreams,由 registerStream 管理活跃状态)
2123
+ if ('interrupt' in sdkStream && typeof sdkStream.interrupt === 'function') {
2124
+ this.interruptFns.set(sessionId, () => sdkStream.interrupt());
2125
+ }
2126
+ // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
2127
+ const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel);
2128
+ const self = this;
2129
+ return (async function* () {
2130
+ try {
2131
+ yield* transformed;
2132
+ }
2133
+ finally {
2134
+ self.streamDoneResolvers.get(sessionId)?.();
2135
+ self.streamDoneResolvers.delete(sessionId);
2136
+ self.streamDone.delete(sessionId);
2137
+ self.activeQueries.delete(sessionId);
2138
+ }
2139
+ })();
2140
+ }
2141
+ async interrupt(sessionId) {
2142
+ const fn = this.interruptFns.get(sessionId);
2143
+ const query = this.activeQueries.get(sessionId);
2144
+ const cancelledInputIds = [];
2145
+ let stillQueued;
2146
+ if (fn) {
2147
+ try {
2148
+ const receipt = await fn();
2149
+ stillQueued = Array.isArray(receipt?.still_queued) ? receipt.still_queued.filter((id) => typeof id === 'string') : undefined;
2150
+ if (stillQueued && query && typeof query.cancelAsyncMessage === 'function') {
2151
+ for (const inputId of stillQueued) {
2152
+ try {
2153
+ if (await query.cancelAsyncMessage(inputId))
2154
+ cancelledInputIds.push(inputId);
2155
+ }
2156
+ catch (error) {
2157
+ logger.debug(`[AgentRunner] Failed to cancel queued input ${inputId}: ${error}`);
2158
+ }
2159
+ }
2160
+ }
2161
+ logger.info(`[AgentRunner] Interrupted session: ${sessionId}`);
2162
+ }
2163
+ catch (error) {
2164
+ logger.warn(`[AgentRunner] Interrupt failed (transport closed): ${sessionId}`);
2165
+ }
2166
+ }
2167
+ this.activeMessageStreams.get(sessionId)?.end();
2168
+ if (query && typeof query.close === 'function')
2169
+ query.close();
2170
+ const done = this.streamDone.get(sessionId);
2171
+ if (done) {
2172
+ await Promise.race([done, new Promise(resolve => setTimeout(resolve, 5_000))]);
2173
+ }
2174
+ this.interruptFns.delete(sessionId);
2175
+ this.activeStreams.delete(sessionId);
2176
+ return { stillQueued, cancelledInputIds, closed: true };
2177
+ }
2178
+ hasActiveStream(sessionId) {
2179
+ return this.activeStreams.has(sessionId);
2180
+ }
2181
+ registerStream(key, stream) {
2182
+ this.activeStreams.set(key, stream);
2183
+ }
2184
+ cleanupStream(sessionId) {
2185
+ this.activeMessageStreams.get(sessionId)?.end();
2186
+ this.activeMessageStreams.delete(sessionId);
2187
+ this.activeStreams.delete(sessionId);
2188
+ this.interruptFns.delete(sessionId);
2189
+ this.activeQueries.delete(sessionId);
2190
+ this.recentStderr.delete(sessionId);
2191
+ }
2192
+ injectUserMessage(sessionId, text) {
2193
+ this.activeMessageStreams.get(sessionId)?.push(text);
2194
+ }
2195
+ updateSessionId(sessionId, agentSessionId) {
2196
+ logger.info(`[AgentRunner] updateSessionId called: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
2197
+ this.activeSessions.set(sessionId, agentSessionId);
2198
+ if (this.onSessionIdUpdate) {
2199
+ this.onSessionIdUpdate(sessionId, agentSessionId);
2200
+ }
2201
+ }
2202
+ runSessionCommand(prompt, agentSessionId, projectPath) {
2203
+ return query({
2204
+ prompt,
2205
+ options: {
2206
+ cwd: projectPath,
2207
+ model: resolveSdkModel(this.model, this.baseUrl),
2208
+ resume: agentSessionId,
2209
+ maxTurns: 1,
2210
+ tools: [],
2211
+ skills: [],
2212
+ mcpServers: {},
2213
+ strictMcpConfig: true,
2214
+ settingSources: [],
2215
+ managedSettings: buildClaudeManagedLockdownSettings(),
2216
+ permissionMode: this.toSdkPermissionMode(),
2217
+ env: this.getAgentEnv()
2218
+ }
2219
+ });
2220
+ }
2221
+ /**
2222
+ * 主动压缩会话上下文
2223
+ */
2224
+ async compactSession(sessionId, agentSessionId, projectPath) {
2225
+ try {
2226
+ logger.info(`[AgentRunner] Compacting session: ${agentSessionId}`);
2227
+ const stream = this.runSessionCommand('/compact', agentSessionId, projectPath);
2228
+ this.activeStreams.set(sessionId, stream);
2229
+ try {
2230
+ let receivedBoundary = false;
2231
+ for await (const event of stream) {
2232
+ if (event.type === 'system' && event.subtype === 'compact_boundary') {
2233
+ logger.info(`[AgentRunner] Compact completed, pre_tokens: ${event.compact_metadata?.pre_tokens}`);
2234
+ receivedBoundary = true;
2235
+ }
2236
+ }
2237
+ if (!receivedBoundary) {
2238
+ logger.warn(`[AgentRunner] Compact stream ended without compact_boundary event`);
2239
+ }
2240
+ return receivedBoundary;
2241
+ }
2242
+ finally {
2243
+ this.activeStreams.delete(sessionId);
2244
+ }
2245
+ }
2246
+ catch (error) {
2247
+ logger.error('[AgentRunner] Compact failed:', error);
2248
+ return false;
2249
+ }
2250
+ }
2251
+ /**
2252
+ * 通过 SDK /clear 命令清空会话历史
2253
+ */
2254
+ async clearSession(sessionId, agentSessionId, projectPath) {
2255
+ try {
2256
+ logger.info(`[AgentRunner] Clearing session via SDK: ${agentSessionId}`);
2257
+ const stream = this.runSessionCommand('/clear', agentSessionId, projectPath);
2258
+ this.activeStreams.set(sessionId, stream);
2259
+ try {
2260
+ let cleared = false;
2261
+ for await (const event of stream) {
2262
+ logger.debug(`[AgentRunner] Clear event: type=${event.type}, subtype=${event.subtype || 'none'}`);
2263
+ if (event.session_id && event.session_id !== agentSessionId) {
2264
+ cleared = true;
2265
+ }
2266
+ }
2267
+ if (cleared) {
2268
+ this.activeSessions.delete(sessionId);
2269
+ this.onSessionIdUpdate?.(sessionId, '');
2270
+ }
2271
+ else {
2272
+ logger.warn('[AgentRunner] Clear stream ended without session reset signal');
2273
+ }
2274
+ return cleared;
2275
+ }
2276
+ finally {
2277
+ this.activeStreams.delete(sessionId);
2278
+ }
2279
+ }
2280
+ catch (error) {
2281
+ logger.error('[AgentRunner] Clear session failed:', error);
2282
+ return false;
2283
+ }
2284
+ }
2285
+ async closeSession(sessionId) {
2286
+ const query = this.activeQueries.get(sessionId);
2287
+ if (query && typeof query.close === 'function')
2288
+ query.close();
2289
+ this.activeSessions.delete(sessionId);
2290
+ this.activeStreams.delete(sessionId);
2291
+ this.interruptFns.delete(sessionId);
2292
+ this.activeQueries.delete(sessionId);
2293
+ this.permissionContexts.delete(sessionId);
2294
+ }
2295
+ resolveSessionFile(agentSessionId, projectPath) {
2296
+ const encodedProjectPath = encodePath(projectPath);
2297
+ const sessionFile = path.join(os.homedir(), '.claude', 'projects', encodedProjectPath, `${agentSessionId}.jsonl`);
2298
+ return fs.existsSync(sessionFile) ? sessionFile : null;
2299
+ }
2300
+ async forkSession(agentSessionId, projectPath, title) {
2301
+ const result = await sdkForkSession(agentSessionId, { dir: projectPath, title });
2302
+ return result.sessionId;
2303
+ }
2304
+ async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title) {
2305
+ const result = await sdkForkSession(agentSessionId, {
2306
+ dir: projectPath,
2307
+ upToMessageId: assistantMessageId,
2308
+ title,
2309
+ });
2310
+ return result.sessionId;
2311
+ }
2312
+ async getSessionMessages(agentSessionId, projectPath) {
2313
+ return sdkGetSessionMessages(agentSessionId, { dir: projectPath });
2314
+ }
2315
+ async rewindFiles(agentSessionId, projectPath, userMessageId) {
2316
+ logger.info(`[RewindFiles] agentSessionId=${agentSessionId} userMessageId=${userMessageId}`);
2317
+ const stderrChunks = [];
2318
+ const tempQuery = query({
2319
+ prompt: '',
2320
+ options: {
2321
+ cwd: projectPath,
2322
+ resume: agentSessionId,
2323
+ enableFileCheckpointing: true,
2324
+ tools: [],
2325
+ skills: [],
2326
+ mcpServers: {},
2327
+ strictMcpConfig: true,
2328
+ settingSources: [],
2329
+ managedSettings: buildClaudeManagedLockdownSettings(),
2330
+ permissionMode: this.toSdkPermissionMode(),
2331
+ stderr: (data) => { stderrChunks.push(data); },
2332
+ env: this.getAgentEnv(),
2333
+ }
2334
+ });
2335
+ try {
2336
+ for await (const _msg of tempQuery) {
2337
+ const dryResult = await tempQuery.rewindFiles(userMessageId, { dryRun: true });
2338
+ logger.info('[RewindFiles] dryRun result:', JSON.stringify(dryResult));
2339
+ if (!dryResult.canRewind)
2340
+ return dryResult;
2341
+ const result = await tempQuery.rewindFiles(userMessageId);
2342
+ logger.info('[RewindFiles] rewind result:', JSON.stringify(result));
2343
+ return {
2344
+ ...result,
2345
+ filesChanged: dryResult.filesChanged ?? result.filesChanged,
2346
+ insertions: dryResult.insertions ?? result.insertions,
2347
+ deletions: dryResult.deletions ?? result.deletions,
2348
+ };
2349
+ }
2350
+ throw new Error('Query stream ended before rewindFiles could be called');
2351
+ }
2352
+ catch (error) {
2353
+ if (stderrChunks.length > 0) {
2354
+ logger.error('[RewindFiles] subprocess stderr:', stderrChunks.join(''));
2355
+ }
2356
+ throw error;
2357
+ }
2358
+ finally {
2359
+ tempQuery.close();
2360
+ }
2361
+ }
2362
+ }
2363
+ function usesUnattendedInteractions(context) {
2364
+ return context?.approvalInteractionPolicy === 'deny';
2365
+ }
2366
+ // Plugin implementation
2367
+ export class ClaudeAgentPlugin {
2368
+ name = 'claude';
2369
+ isEnabled(agent) {
2370
+ return !!agent.config.baseagents?.claude;
2371
+ }
2372
+ createAgent(agent, callbacks) {
2373
+ const override = agent.config.baseagents?.claude;
2374
+ const syntheticConfig = { agents: { claude: override } };
2375
+ const anthropic = resolveAnthropicConfig(syntheticConfig, override);
2376
+ const merged = {
2377
+ agents: { claude: { ...(override || {}), evolcoreAgentAid: agent.aid, evolcoreAgentConfig: agent.config } },
2378
+ };
2379
+ const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, callbacks.onSessionIdUpdate, anthropic.baseUrl, merged);
2380
+ if (anthropic.effort) {
2381
+ agentRunner.setEffort(anthropic.effort);
2382
+ }
2383
+ return { evolagentName: agent.name, baseagent: 'claude', agent: agentRunner };
2384
+ }
2385
+ }