@zhushanwen/subagent-core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-KXVBIVOU.js +16 -0
  3. package/dist/chunk-T2SYBW3I.js +24 -0
  4. package/dist/chunk-VURUAGMM.js +2305 -0
  5. package/dist/{types-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
  6. package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -668
  7. package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
  8. package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
  9. package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
  10. package/dist/execution/engine/engine-discovery-scan.js +20 -0
  11. package/dist/execution/engine/paths.cjs +6 -23
  12. package/dist/execution/engine/paths.d.cts +1 -8
  13. package/dist/execution/engine/paths.d.ts +1 -8
  14. package/dist/execution/engine/paths.js +1 -1
  15. package/dist/execution/relay-env.cjs +12 -28
  16. package/dist/execution/relay-env.d.cts +1 -33
  17. package/dist/execution/relay-env.d.ts +1 -33
  18. package/dist/execution/relay-env.js +5 -3
  19. package/dist/index.cjs +14045 -16059
  20. package/dist/index.d.cts +1035 -1579
  21. package/dist/index.d.ts +1035 -1579
  22. package/dist/index.js +9838 -13800
  23. package/dist.bundle/index.cjs +28875 -30323
  24. package/package.json +20 -26
  25. package/src/__tests__/record-store-last-line.test.ts +16 -7
  26. package/src/core/error-message.ts +7 -6
  27. package/src/core/host-services.ts +8 -2
  28. package/src/core/notify-ports.ts +17 -2
  29. package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
  30. package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
  31. package/src/execution/__tests__/collect-coordinator-service.test.ts +36 -47
  32. package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
  33. package/src/execution/__tests__/delivery-methods.test.ts +179 -237
  34. package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
  35. package/src/execution/__tests__/execute-nesting.test.ts +20 -13
  36. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +20 -33
  37. package/src/execution/__tests__/gc-timer.test.ts +7 -22
  38. package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
  39. package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
  40. package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
  41. package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
  42. package/src/execution/__tests__/idle-gc.test.ts +200 -0
  43. package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
  44. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
  45. package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
  46. package/src/execution/__tests__/record-store.test.ts +60 -6
  47. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
  48. package/src/execution/__tests__/round-supervisor.test.ts +293 -0
  49. package/src/execution/__tests__/session-pending.test.ts +119 -17
  50. package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
  51. package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -11
  52. package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
  53. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
  54. package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
  55. package/src/execution/__tests__/subagent-service.test.ts +2 -2
  56. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
  57. package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
  58. package/src/execution/__tests__/sync-collect-recovery.test.ts +21 -36
  59. package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
  60. package/src/execution/agent-registry.ts +6 -2
  61. package/src/execution/dialog-queue.ts +6 -73
  62. package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
  63. package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
  64. package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
  65. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
  66. package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
  67. package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
  68. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
  69. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
  70. package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
  71. package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
  72. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
  73. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
  74. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
  75. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +81 -54
  76. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
  77. package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
  78. package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
  79. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +7 -2
  80. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
  81. package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
  82. package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
  83. package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
  84. package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
  85. package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
  86. package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
  87. package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
  88. package/src/execution/engine/__tests__/registry.test.ts +206 -1
  89. package/src/execution/engine/__tests__/routing.test.ts +97 -0
  90. package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
  91. package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
  92. package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
  93. package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
  94. package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
  95. package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
  96. package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
  97. package/src/execution/engine/client/client-options.ts +103 -0
  98. package/src/execution/engine/client/engine-client.ts +687 -0
  99. package/src/execution/engine/client/mirror.ts +167 -0
  100. package/src/execution/engine/client/pid-file.ts +392 -0
  101. package/src/execution/engine/client/reaper.ts +95 -0
  102. package/src/execution/engine/client/remote-engine.ts +441 -0
  103. package/src/execution/engine/client/reverse-router.ts +255 -0
  104. package/src/execution/engine/common/capability-gate.ts +109 -22
  105. package/src/execution/engine/common/errors.ts +9 -25
  106. package/src/execution/engine/common/event-journal.ts +4 -50
  107. package/src/execution/engine/common/journal-replay.ts +3 -1
  108. package/src/execution/engine/common/kill-chain.ts +18 -251
  109. package/src/execution/engine/common/nesting-guard.ts +18 -114
  110. package/src/execution/engine/common/pool-manager.ts +4 -0
  111. package/src/execution/engine/common/session-view-service.ts +56 -84
  112. package/src/execution/engine/config.ts +151 -0
  113. package/src/execution/engine/d8-compat.ts +346 -0
  114. package/src/execution/engine/engine-discovery-roots.ts +107 -0
  115. package/src/execution/engine/engine-discovery-scan.ts +269 -0
  116. package/src/execution/engine/engine-discovery.ts +30 -7
  117. package/src/execution/engine/engine-inspect-package.ts +282 -0
  118. package/src/execution/engine/engine-manifest.ts +244 -0
  119. package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
  120. package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
  121. package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
  122. package/src/execution/engine/host/host-bridge.ts +150 -0
  123. package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
  124. package/src/execution/engine/host/pi-host-binding.ts +91 -0
  125. package/src/execution/engine/host/spawned-children.ts +139 -0
  126. package/src/execution/engine/host-task-spec.ts +29 -2
  127. package/src/execution/engine/model-validation.ts +55 -2
  128. package/src/execution/engine/paths.ts +16 -41
  129. package/src/execution/engine/port.ts +48 -2
  130. package/src/execution/engine/registry.ts +153 -34
  131. package/src/execution/engine/routing.ts +225 -52
  132. package/src/execution/engine/types.ts +25 -200
  133. package/src/execution/finalize-record.ts +25 -0
  134. package/src/execution/idle-gc.ts +89 -14
  135. package/src/execution/lifecycle-manager.ts +10 -7
  136. package/src/execution/lifecycle-predicates.ts +10 -8
  137. package/src/execution/record-store.ts +29 -2
  138. package/src/execution/relay-env.ts +19 -36
  139. package/src/execution/round-supervisor/domain.ts +89 -0
  140. package/src/execution/round-supervisor/index.ts +33 -0
  141. package/src/execution/round-supervisor/notify-accounting.ts +87 -0
  142. package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
  143. package/src/execution/round-supervisor/service-binding.test.ts +661 -0
  144. package/src/execution/round-supervisor/service-binding.ts +273 -0
  145. package/src/execution/round-supervisor/supervisor.ts +426 -0
  146. package/src/execution/session-pending.ts +68 -74
  147. package/src/execution/settled-watchdog.ts +39 -0
  148. package/src/execution/subagent-service.ts +746 -400
  149. package/src/execution/subprocess-agent-runner.ts +16 -8
  150. package/src/execution/types.ts +41 -117
  151. package/src/execution/ui-channels.ts +13 -214
  152. package/src/execution/ui-request-handler-factory.ts +7 -4
  153. package/src/execution/ui-request-observability.ts +2 -2
  154. package/src/execution/workflow-state-root.ts +67 -0
  155. package/src/execution/worktree-git-ops.ts +5 -0
  156. package/src/execution/worktree-manager.ts +11 -1
  157. package/src/index.ts +55 -26
  158. package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
  159. package/src/orchestration/execute-agent-call.ts +1 -1
  160. package/src/orchestration/file-run-store.ts +144 -59
  161. package/src/orchestration/models/types.ts +43 -42
  162. package/src/shared/zcode-model-ref.ts +31 -0
  163. package/dist/chunk-43ONBFZX.js +0 -240
  164. package/dist/chunk-A4IVZWVX.js +0 -31
  165. package/dist/chunk-APZY4IME.js +0 -27
  166. package/dist/chunk-V3VHZ2VX.js +0 -59
  167. package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
  168. package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
  169. package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
  170. package/dist/execution/engine/engines/zcode/constants.js +0 -54
  171. package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
  172. package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
  173. package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
  174. package/dist/execution/engine/engines/zcode/reader.js +0 -9
  175. package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
  176. package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
  177. package/src/__tests__/session-runner.test.ts +0 -69
  178. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
  179. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
  180. package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
  181. package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
  182. package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
  183. package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
  184. package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
  185. package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
  186. package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
  187. package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
  188. package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
  189. package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
  190. package/src/execution/__tests__/output-collector.test.ts +0 -469
  191. package/src/execution/__tests__/pi-invocation.test.ts +0 -166
  192. package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
  193. package/src/execution/__tests__/rpc-mode.test.ts +0 -89
  194. package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
  195. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
  196. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
  197. package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
  198. package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
  199. package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
  200. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
  201. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
  202. package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
  203. package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
  204. package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
  205. package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
  206. package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
  207. package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
  208. package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
  209. package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
  210. package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
  211. package/src/execution/__tests__/spawn-args.test.ts +0 -446
  212. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
  213. package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
  214. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
  215. package/src/execution/__tests__/spawned-children.test.ts +0 -236
  216. package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
  217. package/src/execution/__tests__/stdin-writer.test.ts +0 -464
  218. package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
  219. package/src/execution/__tests__/temp-prompt.test.ts +0 -53
  220. package/src/execution/__tests__/timeout-integration.test.ts +0 -616
  221. package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
  222. package/src/execution/__tests__/turn-limiter.test.ts +0 -65
  223. package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
  224. package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
  225. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
  226. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
  227. package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
  228. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
  229. package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
  230. package/src/execution/engine/__tests__/paths.test.ts +0 -39
  231. package/src/execution/engine/common/schema-emulation.ts +0 -189
  232. package/src/execution/engine/common/session-view-projection.ts +0 -51
  233. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
  234. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
  235. package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
  236. package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
  237. package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
  238. package/src/execution/engine/engines/pi/output-collector.ts +0 -317
  239. package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
  240. package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
  241. package/src/execution/engine/engines/pi/reader.ts +0 -59
  242. package/src/execution/engine/engines/pi/registration.ts +0 -37
  243. package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
  244. package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
  245. package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
  246. package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
  247. package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
  248. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
  249. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
  250. package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
  251. package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
  252. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
  253. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
  254. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
  255. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
  256. package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
  257. package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
  258. package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
  259. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
  260. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
  261. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
  262. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
  263. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
  264. package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
  265. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
  266. package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
  267. package/src/execution/engine/engines/zcode/connection.ts +0 -598
  268. package/src/execution/engine/engines/zcode/constants.ts +0 -176
  269. package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
  270. package/src/execution/engine/engines/zcode/parser.ts +0 -102
  271. package/src/execution/engine/engines/zcode/preparer.ts +0 -235
  272. package/src/execution/engine/engines/zcode/reader.ts +0 -381
  273. package/src/execution/engine/engines/zcode/registration.ts +0 -37
  274. package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
  275. package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
  276. package/src/execution/round-settlement.ts +0 -93
  277. package/src/execution/ui-request-queue.ts +0 -215
  278. package/src/shared/schema-env.ts +0 -44
@@ -0,0 +1,269 @@
1
+ // src/execution/engine/engine-discovery-scan.ts
2
+ //
3
+ // [W4] 引擎发现器:manifest 解析 + 三级搜索路径 + cli descriptor 装载。
4
+ // 设计权威源:docs/design/subagent-engine-protocolization.md §3.4 发现与注册;
5
+ // 实现级规格 impl-plan §2.4。
6
+ //
7
+ // 自注册模型(G1「新引擎零改 core」):引擎包在自己的 package.json 里声明
8
+ // `xyz-agent.subagentEngine` manifest,发现器扫描三级搜索路径命中即解析 manifest、
9
+ // 装配 cli descriptor(portFactory = W2 EngineClient + RemoteEngine)并经
10
+ // registerEngineDescriptor 装载进注册表——core 与本文件都不枚举引擎。
11
+ //
12
+ // 三级路径:
13
+ // L1 宿主发现根 = env `XYZ_AGENT_ENGINE_ROOTS`(path.delimiter 分隔)+
14
+ // HostServices.discoveryRoots().engines(宿主模块域,pi 壳在 pi-host.ts 提供);
15
+ // L2 node 解析 = 宿主进程模块解析域的 node_modules(require.resolve 候选目录链的
16
+ // 扫描化,见 deriveNodeModuleRoots);打包态与 zsw vendor 态自然为空(规格明确
17
+ // 这两态 L2 无效);
18
+ // L3 显式配置 = <agentDir>/subagents/config.json 的 engines{} 段(config.ts),
19
+ // 最后装载 = 用户/开发态覆盖(A12⑤ enabled:false 不进清单)。
20
+ //
21
+ // 发现时机(§3.4):session_start 扫描一次 + 装载(装载结果即缓存——registry
22
+ // descriptors);hasEngine() 未命中走 ensureEngineDiscovered 同步补扫(只读 manifest
23
+ // 不握手——portFactory 惰性,扫描绝不 spawn 引擎进程)。引擎清单投影(engines.json)
24
+ // 归 engine-discovery.ts syncEnginesFile。
25
+ //
26
+ // 冲突与失败(A12):manifest 缺必需字段/不可解析 → warn 跳过该包(不阻断其他引擎);
27
+ // 同 id 覆盖 → debug 留痕(core logger 三值无 info 级,debug 落宿主文件日志);bin
28
+ // 不存在/不可执行 → 标记不可用(不装载、不进清单);protocol 不兼容 → 同不可用。
29
+
30
+ import * as path from "node:path";
31
+
32
+ import { CONSERVATIVE_CAPABILITIES } from "./engine-manifest.ts";
33
+ import {
34
+ canExecute,
35
+ inspectEnginePackage,
36
+ type DiscoveredEngine,
37
+ } from "./engine-inspect-package.ts";
38
+
39
+ import { getLogger } from "../../core/logger.ts";
40
+ import {
41
+ ENGINE_ROOTS_ENV,
42
+ deriveNodeModuleRoots,
43
+ parseEngineRootsEnv,
44
+ scanEngineRoot,
45
+ } from "./engine-discovery-roots.ts";
46
+ import { getHostServices, type DiscoveryRoot } from "../../core/host-services.ts";
47
+ import { getEngineDataDir } from "./common/data-dir.ts";
48
+ import { readExplicitEngines } from "./config.ts";
49
+ import { EngineClient } from "./client/engine-client.ts";
50
+ import { RemoteEngine } from "./client/remote-engine.ts";
51
+ import { hasEngine, registerEngineDescriptor, type CliEngineDescriptor } from "./registry.ts";
52
+ import { getHostUiRequestEndpoint } from "./host/host-ui-endpoint.ts";
53
+ import type { EngineCapabilities } from "./types.ts";
54
+
55
+ const logger = getLogger("subagents");
56
+
57
+ export { ENGINE_ROOTS_ENV, parseEngineRootsEnv, deriveNodeModuleRoots };
58
+ export { inspectEnginePackage };
59
+ export type { DiscoveredEngine, PackageInspection } from "./engine-inspect-package.ts";
60
+
61
+ /** 扫描结果(诊断面:skip/unusable 逐包留痕原因,对应 A12 负面场景)。 */
62
+ export interface DiscoveryScanResult {
63
+ discovered: DiscoveredEngine[];
64
+ skipped: Array<{ pkgDir: string; reason: string }>;
65
+ unusable: Array<{ pkgDir: string; id: string | undefined; reason: string }>;
66
+ }
67
+
68
+ export interface DiscoverEnginesOptions {
69
+ /** 宿主种类(EngineClient pidfile 实例维度命名段:pi 壳 'pi'、runtime 'runtime')。 */
70
+ hostKind: string;
71
+ /** L3 显式配置目录(<dir>/subagents/config.json 的 engines 段)。缺省 = 无 L3。 */
72
+ agentDir?: string;
73
+ /** 引擎数据根(EngineClient dataDir;缺省 portFactory 执行期 getEngineDataDir 解析)。 */
74
+ dataDir?: string;
75
+ /** env 覆盖(L1 根读取;缺省 process.env——测试注入隔离宿主 env)。 */
76
+ env?: NodeJS.ProcessEnv;
77
+ /** L2 覆盖(测试注入;缺省 = 宿主入口上溯 node_modules 链)。 */
78
+ nodeModuleRoots?: string[];
79
+ /** 附加发现根(调用方/测试追加的 L1 根,语义同 HostServices.engines)。 */
80
+ extraRoots?: DiscoveryRoot[];
81
+ }
82
+
83
+ // ============================================================
84
+ // 扫描与装载
85
+ // ============================================================
86
+
87
+ /** 纯扫描(不装载注册表)。装载序 = L1 env → L1 宿主/附加根 → L2 → L3(后者覆盖前者)。 */
88
+ export function scanEngines(opts: DiscoverEnginesOptions): DiscoveryScanResult {
89
+ const env = opts.env ?? process.env;
90
+ const result: DiscoveryScanResult = { discovered: [], skipped: [], unusable: [] };
91
+ const present = (entry: DiscoveredEngine): void => {
92
+ const idx = result.discovered.findIndex((d) => d.id === entry.id);
93
+ if (idx >= 0) {
94
+ logger.debug(
95
+ `[engine-discovery] engine id '${entry.id}' from '${entry.source}' overrides earlier ` +
96
+ `discovery from '${result.discovered[idx].source}' — same-id override`,
97
+ );
98
+ result.discovered[idx] = entry;
99
+ return;
100
+ }
101
+ result.discovered.push(entry);
102
+ };
103
+
104
+ const visitManifestPackage = (pkgDir: string, source: string): void => {
105
+ const inspection = inspectEnginePackage(pkgDir, source, opts.hostKind, env);
106
+ if (inspection.status === "skip") {
107
+ // 非引擎包(无 manifest 段)的静默 skip 不留痕——扫描常态;其余 skip warn。
108
+ if (!inspection.reason.includes("not an engine package")) {
109
+ logger.warn(`[engine-discovery] skipping ${pkgDir} (${source}): ${inspection.reason}`);
110
+ result.skipped.push({ pkgDir, reason: inspection.reason });
111
+ }
112
+ return;
113
+ }
114
+ if (inspection.status === "unusable") {
115
+ logger.warn(`[engine-discovery] engine unavailable: ${inspection.reason}`);
116
+ result.unusable.push({ pkgDir, id: inspection.id, reason: inspection.reason });
117
+ return;
118
+ }
119
+ present(inspection.entry);
120
+ };
121
+
122
+ // L1:env 宿主发现根(打包态主通道,W9 注入)
123
+ for (const dir of parseEngineRootsEnv(env)) {
124
+ scanEngineRoot(dir, (pkgDir) => visitManifestPackage(pkgDir, ENGINE_ROOTS_ENV));
125
+ }
126
+ // L1:宿主发现根第二通道(HostServices.engines)+ 调用方附加根
127
+ const hostRoots = [
128
+ ...(getHostServices().discoveryRoots?.().engines ?? []),
129
+ ...(opts.extraRoots ?? []),
130
+ ];
131
+ for (const root of hostRoots) {
132
+ scanEngineRoot(root.dir, (pkgDir) => visitManifestPackage(pkgDir, root.source));
133
+ }
134
+ // L2:宿主 node_modules(node 解析域;opts.nodeModuleRoots 测试覆盖)
135
+ const nodeRoots = opts.nodeModuleRoots ?? deriveNodeModuleRoots();
136
+ for (const dir of nodeRoots) {
137
+ scanEngineRoot(dir, (pkgDir) => visitManifestPackage(pkgDir, "node-modules"));
138
+ }
139
+ // L3:显式配置(最后装载 = 用户/开发态覆盖)
140
+ if (opts.agentDir !== undefined) {
141
+ for (const [id, entry] of Object.entries(readExplicitEngines(opts.agentDir))) {
142
+ const command = resolveExplicitCommand(entry.command, env);
143
+ if (command === undefined) {
144
+ const reason =
145
+ `engine '${id}' (config.json engines.${id}): command '${entry.command}' not found or not executable`;
146
+ logger.warn(`[engine-discovery] ${reason} — not registering`);
147
+ result.unusable.push({ pkgDir: `config.json engines.${id}`, id, reason });
148
+ continue;
149
+ }
150
+ present(buildExplicitDescriptor(id, command, entry, opts));
151
+ }
152
+ }
153
+ return result;
154
+ }
155
+
156
+ /**
157
+ * 历史发现装载的引擎 id 集(模块级;供 syncEnginesFile 投影时从注册表快照剔除
158
+ * 历史装载——投影逻辑与装载逻辑同 bundle 闭包,无跨模块实例副本问题)。
159
+ */
160
+ const loadedDiscoveryIdSet = new Set<string>();
161
+
162
+ /**
163
+ * 扫描 + 装载:descriptor 经 registerEngineDescriptor 进注册表(幂等覆盖——重复扫描
164
+ * 安全)。装载覆盖既有注册(含过渡期 inproc pi/zcode——auto 模式下 cli descriptor
165
+ * 胜出即设计 D3 语义)时 debug 留痕。
166
+ */
167
+ export function discoverAndRegisterEngines(opts: DiscoverEnginesOptions): DiscoveryScanResult {
168
+ const result = scanEngines(opts);
169
+ for (const entry of result.discovered) {
170
+ const existed = hasEngine(entry.id);
171
+ registerEngineDescriptor(entry.id, entry.descriptor);
172
+ loadedDiscoveryIdSet.add(entry.id);
173
+ if (existed) {
174
+ logger.debug(
175
+ `[engine-discovery] engine '${entry.id}' descriptor overwritten in registry ` +
176
+ `(source '${entry.source}') — same-id override`,
177
+ );
178
+ }
179
+ }
180
+ return result;
181
+ }
182
+
183
+ /**
184
+ * 历史发现装载的引擎 id 快照(投影源计算消费面,见 engine-discovery.ts)。
185
+ */
186
+ export function loadedDiscoveryIds(): string[] {
187
+ return [...loadedDiscoveryIdSet];
188
+ }
189
+
190
+ /**
191
+ * hasEngine 的补扫面(§3.4 发现时机:快照未命中触发一次同步补扫,只读 manifest
192
+ * 不握手)。消费方 = agent 解析期/路由期的存在性校验接线(宿主侧注入 hasEngineFn
193
+ * 时用本函数替代裸 hasEngine)——「装了包 → 下次解析即可用」。
194
+ */
195
+ export function ensureEngineDiscovered(id: string, opts: DiscoverEnginesOptions): boolean {
196
+ if (hasEngine(id)) return true;
197
+ discoverAndRegisterEngines(opts);
198
+ return hasEngine(id);
199
+ }
200
+
201
+ /** L3 显式命令解析:含路径分隔符 → 按路径验证;裸命令名 → PATH 查找。失败 = 不可用。 */
202
+ function resolveExplicitCommand(command: string, env: NodeJS.ProcessEnv): string | undefined {
203
+ if (command.includes("/") || command.includes("\\")) {
204
+ const p = path.isAbsolute(command) ? command : path.resolve(command);
205
+ return canExecute(p) ? p : undefined;
206
+ }
207
+ const pathVar = env["PATH"] ?? env["Path"] ?? "";
208
+ for (const dir of pathVar.split(path.delimiter)) {
209
+ if (dir === "") continue;
210
+ const p = path.join(dir, command);
211
+ if (canExecute(p)) return p;
212
+ }
213
+ return undefined;
214
+ }
215
+
216
+ /**
217
+ * L3 显式配置的 descriptor:无 manifest 面(用户手工配置 command)→ capabilities 取
218
+ * 全保守值 + warn(gate 同步拦生成,运行期不踩未声明能力);无 modelCatalog / 无
219
+ * displayName(= id)。engineConfig 经 initialize.engineConfig 透传。
220
+ */
221
+ function buildExplicitDescriptor(
222
+ id: string,
223
+ command: string,
224
+ entry: { args: string[]; config?: Record<string, string>; cwd?: string },
225
+ opts: DiscoverEnginesOptions,
226
+ ): DiscoveredEngine {
227
+ const env = opts.env ?? process.env;
228
+ logger.debug(
229
+ `[engine-discovery] engine '${id}' registered from config.json engines section with conservative capabilities (no manifest)`,
230
+ );
231
+ const caps: EngineCapabilities = { ...CONSERVATIVE_CAPABILITIES };
232
+ const descriptor: CliEngineDescriptor = {
233
+ kind: "cli",
234
+ command,
235
+ args: entry.args,
236
+ capabilities: caps,
237
+ portFactory: () => {
238
+ // 惰性求值理由同 manifest descriptor portFactory(扫描期早于 configureCore)。
239
+ const dataDir = opts.dataDir ?? getEngineDataDir(env);
240
+ const client = new EngineClient({
241
+ engineId: id,
242
+ command,
243
+ args: entry.args,
244
+ ...(entry.cwd !== undefined ? { cwd: entry.cwd } : {}),
245
+ hostKind: opts.hostKind,
246
+ dataDir,
247
+ envPrefixes: [],
248
+ // [W6 R3 MF-A] 同上:host/askUser 应答端经壳侧登记处接线。
249
+ uiRequestHandler: getHostUiRequestEndpoint(),
250
+ ...(entry.config !== undefined ? { engineConfig: entry.config } : {}),
251
+ manifestDiagnostics: { capabilities: caps, models: null },
252
+ });
253
+ return new RemoteEngine({
254
+ engineId: id,
255
+ client,
256
+ manifest: { capabilities: caps },
257
+ dataDir,
258
+ hostKind: opts.hostKind,
259
+ ...(entry.config !== undefined ? { engineConfig: entry.config } : {}),
260
+ });
261
+ },
262
+ };
263
+ return {
264
+ id,
265
+ source: "config.json",
266
+ ...(entry.config !== undefined ? { engineConfig: entry.config } : {}),
267
+ descriptor,
268
+ };
269
+ }
@@ -1,13 +1,19 @@
1
1
  // src/execution/engine/engine-discovery.ts
2
2
  //
3
- // [U7] 引擎列表状态文件同步(registry <agentDir>/subagents/engines.json)。
3
+ // [U7 → W4] 引擎列表状态文件同步(→ <agentDir>/subagents/engines.json)。
4
4
  //
5
5
  // 为什么落文件而不是 session entry:Settings 引擎选择器在无活跃 session 时也要可开
6
6
  // (冷启动/全局设置),session entry 绑定会话生命周期兜不住;engines.json 是进程外
7
7
  // 可读的全局状态,extension(pi 进程)为唯一写入者,runtime RPC 读——数据所有权清晰。
8
8
  //
9
- // 动态性(用户要求):清单来自代码注册表(registerEngine 调用点),未来新增引擎
10
- // (AcpEngine 等)零改动出现在 GUI——本文件与前端都不枚举引擎。
9
+ // 动态性(W4 协议化):清单来自「三级发现的装载结果 ∪ 代码注册表」——发现器
10
+ // (engine-discovery-scan.ts)在同步前扫描(L1 env 根 / HostServices.engines /
11
+ // L2 node 解析 / L3 config.json),把引擎包 manifest 自注册的 cli descriptor 装载进
12
+ // 注册表;inproc 过渡注册(组合根 registerXxxEngine)与发现装载合流构成本次投影。
13
+ // 新引擎 = 装一个包(W5/W7 后),本文件与前端都不枚举引擎。
14
+ //
15
+ // 投影契约(设计 §3.4 投影面表):清单 = 已发现且可执行的 id 数组;契约
16
+ // {v:1, engines: string[]} 不改(A11 宿主表面不变量)。
11
17
 
12
18
  import * as fs from "node:fs";
13
19
  import * as path from "node:path";
@@ -15,6 +21,7 @@ import * as path from "node:path";
15
21
  import { SUBAGENTS_ENGINES_FILENAME, type SubagentEnginesFile } from "@xyz-agent/extension-protocol";
16
22
 
17
23
  import { listEngines } from "./registry.ts";
24
+ import { discoverAndRegisterEngines, loadedDiscoveryIds } from "./engine-discovery-scan.ts";
18
25
  import { writeAtomicFileSync } from "../../shared/atomic-write.ts";
19
26
 
20
27
  /** engines.json 落盘缩进(与 subagent-core 其他 JSON store 的 JSON_INDENT 约定一致)。 */
@@ -26,17 +33,33 @@ export function getEnginesFilePath(agentDir: string): string {
26
33
  }
27
34
 
28
35
  /**
29
- * 把注册表引擎列表同步到 engines.json(session_start 时调用)。
36
+ * 三级发现装载 + 把引擎清单同步到 engines.json(session_start 时调用)。
37
+ *
38
+ * 发现时机(W4 §2.4):与 syncEnginesFile 同点——扩展组合根的模块加载点与
39
+ * session_start 兜底点即本函数既有调用点(调用方零改动),扫描一次 + 装载进注册表
40
+ * (装载结果即缓存);只读 manifest 不握手(portFactory 惰性,扫描不 spawn 引擎)。
41
+ *
42
+ * 投影源(§3.4 投影面表「清单 = 已发现且可执行」)= 本次发现装载结果 ∪ inproc 过渡
43
+ * 注册快照(listEngines() 剔除历史发现装载的 id——registry 无撤销 API,引擎包卸载
44
+ * 后已装载 descriptor 会滞留,投影时经 loadedDiscoveryIds 排除即「卸载 → 下次扫描
45
+ * 自动消失」清理通道;A4 新引擎装包零改 core 出现在清单)。W11 删 inproc 后清单 =
46
+ * 纯发现结果(零命中 → 空清单,GUI 按既有语义给 engine_not_found + 安装指引)。
30
47
  *
31
48
  * 幂等:内容与现文件一致时零写入(mtime 不动——读侧无谓失效);写入走 tmp+rename
32
49
  * 原子替换(shared/atomic-write 统一原语,U6b 迁移——与池 config 同防线)。
33
- * fail-safe:任何 IO 异常吞掉(可发现性降级不阻塞
34
- * session 启动——GUI 兜底显示 ['pi'])。
50
+ * fail-safe:发现与 IO 异常都吞掉(可发现性降级不阻塞 session 启动——GUI 兜底显示
51
+ * 既有清单)。
35
52
  */
36
53
  export function syncEnginesFile(agentDir: string): void {
37
54
  try {
55
+ // inproc 过渡注册快照:注册表全集剔除历史发现装载的 id(清理通道语义,见头注释)
56
+ const loadedIds = new Set(loadedDiscoveryIds());
57
+ const inprocSnapshot = listEngines().filter((id) => !loadedIds.has(id));
58
+ // [W4] 三级发现装载(fail-safe 内——扫描失败不阻塞投影与 session 启动)。
59
+ const scan = discoverAndRegisterEngines({ hostKind: "pi", agentDir });
38
60
  const filePath = getEnginesFilePath(agentDir);
39
- const payload: SubagentEnginesFile = { v: 1, engines: listEngines(), updatedAt: Date.now() };
61
+ const engines = [...new Set([...inprocSnapshot, ...scan.discovered.map((e) => e.id)])];
62
+ const payload: SubagentEnginesFile = { v: 1, engines, updatedAt: Date.now() };
40
63
  const serialized = JSON.stringify(payload, null, JSON_INDENT);
41
64
  try {
42
65
  const existing = fs.readFileSync(filePath, "utf8");
@@ -0,0 +1,282 @@
1
+ // src/execution/engine/engine-inspect-package.ts
2
+ //
3
+ // [W4] 单引擎包检查管线(engine-discovery-scan.ts 拆出——max-lines 纪律 + 职责域
4
+ // 独立):读 package.json → manifest 字段级解析 → bin 可执行验证 → cli descriptor
5
+ // 装配。纯「候选包目录 → 三态检查产物」,无搜索路径/装载状态(留在 discovery-scan)。
6
+ // 设计权威源:docs/design/subagent-engine-protocolization.md §3.4 + impl-plan §2.4。
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+
11
+ import { isProtocolVersionCompatible, type ModelCatalogEntry } from "@zhushanwen/subagent-engine-sdk";
12
+
13
+ import {
14
+ describeValue,
15
+ parseCapabilities,
16
+ parseEnvPrefixes,
17
+ parseModelCatalog,
18
+ resolveManifestBin,
19
+ } from "./engine-manifest.ts";
20
+
21
+ import { getLogger } from "../../core/logger.ts";
22
+ import { getEngineDataDir } from "./common/data-dir.ts";
23
+ import { EngineClient } from "./client/engine-client.ts";
24
+ import { RemoteEngine, type RemoteEngineManifestSnapshot } from "./client/remote-engine.ts";
25
+ import type { CliEngineDescriptor } from "./registry.ts";
26
+ import { getHostUiRequestEndpoint } from "./host/host-ui-endpoint.ts";
27
+ import type { EngineCapabilities } from "./types.ts";
28
+
29
+ const logger = getLogger("subagents");
30
+
31
+ /** 单个引擎包的检查产物。 */
32
+ export type PackageInspection =
33
+ | { status: "ok"; entry: DiscoveredEngine }
34
+ | { status: "skip"; reason: string }
35
+ | { status: "unusable"; id: string | undefined; reason: string };
36
+
37
+ /** 发现成功的引擎条目(装载序;同 id 后者覆盖前者)。 */
38
+ export interface DiscoveredEngine {
39
+ id: string;
40
+ /** 发现源标签(env 名 / 宿主根 source / node-modules / config.json)。 */
41
+ source: string;
42
+ /** L3 显式 config(initialize.engineConfig 透传;L1/L2 manifest 发现无此项)。 */
43
+ engineConfig?: Record<string, string>;
44
+ descriptor: CliEngineDescriptor;
45
+ }
46
+
47
+ /** 检查管线中间步的失败产物(skip/unusable;ok 只由主管线终点产出)。 */
48
+ type ManifestInspectionFailure = Extract<PackageInspection, { status: "skip" | "unusable" }>;
49
+
50
+ /** 读 + JSON 解析 package.json(不可读/坏 JSON/非对象 → skip)。 */
51
+ function readEnginePackageJson(pkgDir: string): { pkg: Record<string, unknown> } | ManifestInspectionFailure {
52
+ let raw: string;
53
+ try {
54
+ raw = fs.readFileSync(path.join(pkgDir, "package.json"), "utf8");
55
+ } catch (err) {
56
+ return { status: "skip", reason: `package.json unreadable: ${errorMessage(err)}` };
57
+ }
58
+ let pkg: unknown;
59
+ try {
60
+ pkg = JSON.parse(raw);
61
+ } catch (err) {
62
+ return { status: "skip", reason: `package.json is not valid JSON: ${errorMessage(err)}` };
63
+ }
64
+ if (typeof pkg !== "object" || pkg === null) {
65
+ return { status: "skip", reason: "package.json is not an object" };
66
+ }
67
+ return { pkg: pkg as Record<string, unknown> };
68
+ }
69
+
70
+ /** xyz-agent.subagentEngine manifest 段提取(无段/形态坏 → 各自的 skip reason)。 */
71
+ function extractEngineManifest(pkg: Record<string, unknown>): { manifest: Record<string, unknown> } | { skipReason: string } {
72
+ // 无 manifest 段 = 不是引擎包:扫描常态(node_modules 大量无关包),静默跳过不 warn。
73
+ const agentNs = pkg["xyz-agent"];
74
+ if (typeof agentNs !== "object" || agentNs === null) {
75
+ return { skipReason: "no xyz-agent.subagentEngine manifest (not an engine package)" };
76
+ }
77
+ const manifest = (agentNs as Record<string, unknown>)["subagentEngine"];
78
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
79
+ return { skipReason: "xyz-agent.subagentEngine is not an object" };
80
+ }
81
+ return { manifest: manifest as Record<string, unknown> };
82
+ }
83
+
84
+ // ── 必需字段:id / bin / protocol(缺失或形态坏 → warn 跳过该包)──
85
+ function validateRequiredManifestFields(
86
+ m: Record<string, unknown>,
87
+ ): { id: string; manifestBin: string; protocol: number } | ManifestInspectionFailure {
88
+ const id = m["id"];
89
+ if (typeof id !== "string" || id.trim() === "") {
90
+ return { status: "skip", reason: "manifest id is required (non-empty string)" };
91
+ }
92
+ const manifestBin = m["bin"];
93
+ if (typeof manifestBin !== "string" || manifestBin.trim() === "") {
94
+ return { status: "skip", reason: `engine '${id}': manifest bin is required (non-empty string)` };
95
+ }
96
+ const protocol = m["protocol"];
97
+ if (typeof protocol !== "number" || !Number.isInteger(protocol)) {
98
+ return {
99
+ status: "skip",
100
+ reason: `engine '${id}': manifest protocol is required (integer; got ${describeValue(protocol)})`,
101
+ };
102
+ }
103
+ if (!isProtocolVersionCompatible(protocol)) {
104
+ return {
105
+ status: "unusable",
106
+ id,
107
+ reason:
108
+ `engine '${id}': manifest protocol ${protocol} is not compatible ` +
109
+ `(core supports >=1 <2) — upgrade the engine package or the host core`,
110
+ };
111
+ }
112
+ return { id, manifestBin, protocol };
113
+ }
114
+
115
+ /**
116
+ * bin 解析 + 可执行验证:manifest bin = package.json npm bin 的 key(设计 §3.4 示例
117
+ * 形态);package.json bin 为字符串时直接作相对路径。解析不出真实入口 = bin 缺失面。
118
+ */
119
+ function resolveVerifiedEngineBin(
120
+ pkgDir: string,
121
+ pkg: Record<string, unknown>,
122
+ manifestBin: string,
123
+ id: string,
124
+ ): { binPath: string } | ManifestInspectionFailure {
125
+ const binPath = resolveManifestBin(pkgDir, pkg, manifestBin);
126
+ if (binPath === undefined) {
127
+ return {
128
+ status: "skip",
129
+ reason: `engine '${id}': manifest bin '${manifestBin}' does not resolve via package.json bin`,
130
+ };
131
+ }
132
+ if (!canExecute(binPath)) {
133
+ return {
134
+ status: "unusable",
135
+ id,
136
+ reason: `engine '${id}': bin not found or not executable: ${binPath}`,
137
+ };
138
+ }
139
+ return { binPath };
140
+ }
141
+
142
+ /**
143
+ * displayName / description(可选;displayName 缺省 = id 由消费面兜底
144
+ * (listEnginesByDisplayName 的 `?? id`),快照只存显式值防「缺省填充」被误读;
145
+ * description 当前无消费面,仅做形态校验)。
146
+ */
147
+ function parseOptionalDisplayFields(m: Record<string, unknown>, id: string): string | undefined {
148
+ let displayName: string | undefined;
149
+ const rawDisplayName = m["displayName"];
150
+ if (rawDisplayName !== undefined) {
151
+ if (typeof rawDisplayName === "string" && rawDisplayName.trim() !== "") {
152
+ displayName = rawDisplayName;
153
+ } else {
154
+ logger.warn(`[engine-discovery] engine '${id}': displayName must be a non-empty string — ignoring`);
155
+ }
156
+ }
157
+ const rawDescription = m["description"];
158
+ if (rawDescription !== undefined && typeof rawDescription !== "string") {
159
+ logger.warn(`[engine-discovery] engine '${id}': description must be a string — ignoring`);
160
+ }
161
+ return displayName;
162
+ }
163
+
164
+ /** cli descriptor 装配(portFactory 惰性求值:EngineClient + RemoteEngine)。 */
165
+ function buildManifestCliDescriptor(params: {
166
+ id: string;
167
+ binPath: string;
168
+ hostKind: string;
169
+ env: NodeJS.ProcessEnv;
170
+ caps: EngineCapabilities;
171
+ envPrefixes: string[];
172
+ modelCatalog: { dynamic: boolean; models: ModelCatalogEntry[] } | null | undefined;
173
+ displayName: string | undefined;
174
+ manifestSnapshot: RemoteEngineManifestSnapshot;
175
+ }): CliEngineDescriptor {
176
+ const { id, binPath, hostKind, env, caps, envPrefixes, modelCatalog, displayName, manifestSnapshot } = params;
177
+ return {
178
+ kind: "cli",
179
+ command: binPath,
180
+ args: [],
181
+ capabilities: caps,
182
+ portFactory: () => {
183
+ // 惰性求值:portFactory 在 getEngine 首次取用时才执行(registry 惰性单例),
184
+ // 那时宿主已 configureCore(或宿主进程 env 已带数据根)——扫描期可能早于
185
+ // configureCore,直接调 getEngineDataDir 会触 core_host_not_configured。
186
+ const dataDir = getEngineDataDir(env);
187
+ const client = new EngineClient({
188
+ engineId: id,
189
+ command: binPath,
190
+ args: [],
191
+ hostKind,
192
+ dataDir,
193
+ envPrefixes,
194
+ // [W6 R3 MF-A] host/askUser 应答端:壳侧登记处在 portFactory 惰性执行期取值
195
+ //(晚于 session_start 注册,未注册 → undefined → 引擎收 {unsupported:true})。
196
+ uiRequestHandler: getHostUiRequestEndpoint(),
197
+ manifestDiagnostics: {
198
+ capabilities: caps,
199
+ models: modelCatalog === undefined || modelCatalog === null ? null : modelCatalog.models,
200
+ },
201
+ });
202
+ return new RemoteEngine({
203
+ engineId: id,
204
+ client,
205
+ manifest: manifestSnapshot,
206
+ dataDir,
207
+ hostKind,
208
+ });
209
+ },
210
+ manifest: {
211
+ ...(modelCatalog !== undefined ? { modelCatalog } : {}),
212
+ ...(displayName !== undefined ? { displayName } : {}),
213
+ },
214
+ };
215
+ }
216
+
217
+ /**
218
+ * 检查单个候选包目录(读 package.json → manifest 字段级解析 → bin 可执行验证)。
219
+ * 三态:ok(装载)/ skip(必需字段缺失/不可解析——warn 跳过该包)/ unusable
220
+ * (protocol 不兼容、bin 不可执行——标记不可用,不进清单)。
221
+ */
222
+ export function inspectEnginePackage(
223
+ pkgDir: string,
224
+ source: string,
225
+ hostKind: string,
226
+ env: NodeJS.ProcessEnv,
227
+ ): PackageInspection {
228
+ const pkgStep = readEnginePackageJson(pkgDir);
229
+ if (!("pkg" in pkgStep)) return pkgStep;
230
+ const manifestStep = extractEngineManifest(pkgStep.pkg);
231
+ if (!("manifest" in manifestStep)) return { status: "skip", reason: manifestStep.skipReason };
232
+ const m = manifestStep.manifest;
233
+ const fields = validateRequiredManifestFields(m);
234
+ if ("reason" in fields) return fields;
235
+ const { id, manifestBin } = fields;
236
+ const binStep = resolveVerifiedEngineBin(pkgDir, pkgStep.pkg, manifestBin, id);
237
+ if (!("binPath" in binStep)) return binStep;
238
+
239
+ // ── capabilities(必需;缺键取最保守值 + warn;未知键忽略 + warn)──
240
+ const caps = parseCapabilities(id, m["capabilities"]);
241
+
242
+ // ── envPrefixes(可选,缺省 [];非法条目丢弃该前缀 + warn,包仍可用——A12②)──
243
+ const envPrefixes = parseEnvPrefixes(id, m["envPrefixes"]);
244
+
245
+ // ── modelCatalog(可选;缺省 = 不注入保持 undefined——解析器不得把省略填成
246
+ // models: [],否则「无枚举面」语义不可达,RemoteEngine.listModels 会说谎)──
247
+ const modelCatalog = parseModelCatalog(id, m["modelCatalog"]);
248
+
249
+ const displayName = parseOptionalDisplayFields(m, id);
250
+
251
+ const manifestSnapshot: RemoteEngineManifestSnapshot = {
252
+ capabilities: caps,
253
+ ...(modelCatalog !== undefined ? { modelCatalog } : {}),
254
+ };
255
+
256
+ const descriptor = buildManifestCliDescriptor({
257
+ id,
258
+ binPath: binStep.binPath,
259
+ hostKind,
260
+ env,
261
+ caps,
262
+ envPrefixes,
263
+ modelCatalog,
264
+ displayName,
265
+ manifestSnapshot,
266
+ });
267
+ return { status: "ok", entry: { id, source, descriptor } };
268
+ }
269
+
270
+ /** 可执行探测(X_OK;ENOENT/EACCES/平台不支持 → false)。 */
271
+ export function canExecute(p: string): boolean {
272
+ try {
273
+ fs.accessSync(p, fs.constants.X_OK);
274
+ return true;
275
+ } catch {
276
+ return false;
277
+ }
278
+ }
279
+
280
+ function errorMessage(err: unknown): string {
281
+ return err instanceof Error ? err.message : String(err);
282
+ }