@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.
- package/README.md +1 -1
- package/dist/chunk-KXVBIVOU.js +16 -0
- package/dist/chunk-T2SYBW3I.js +24 -0
- package/dist/chunk-VURUAGMM.js +2305 -0
- package/dist/{types-C3fE3R9x.d.cts → engine-discovery-scan-ocpM8FmI.d.cts} +770 -668
- package/dist/{types-C3fE3R9x.d.ts → engine-discovery-scan-ocpM8FmI.d.ts} +770 -668
- package/dist/execution/engine/engine-discovery-scan.cjs +2210 -0
- package/dist/execution/engine/engine-discovery-scan.d.cts +4 -0
- package/dist/execution/engine/engine-discovery-scan.d.ts +4 -0
- package/dist/execution/engine/engine-discovery-scan.js +20 -0
- package/dist/execution/engine/paths.cjs +6 -23
- package/dist/execution/engine/paths.d.cts +1 -8
- package/dist/execution/engine/paths.d.ts +1 -8
- package/dist/execution/engine/paths.js +1 -1
- package/dist/execution/relay-env.cjs +12 -28
- package/dist/execution/relay-env.d.cts +1 -33
- package/dist/execution/relay-env.d.ts +1 -33
- package/dist/execution/relay-env.js +5 -3
- package/dist/index.cjs +14045 -16059
- package/dist/index.d.cts +1035 -1579
- package/dist/index.d.ts +1035 -1579
- package/dist/index.js +9838 -13800
- package/dist.bundle/index.cjs +28875 -30323
- package/package.json +20 -26
- package/src/__tests__/record-store-last-line.test.ts +16 -7
- package/src/core/error-message.ts +7 -6
- package/src/core/host-services.ts +8 -2
- package/src/core/notify-ports.ts +17 -2
- package/src/execution/__tests__/chat-engine-routing.test.ts +62 -28
- package/src/execution/__tests__/chat-round-first-round-watchdog.test.ts +255 -0
- package/src/execution/__tests__/collect-coordinator-service.test.ts +36 -47
- package/src/execution/__tests__/collect-mixed-dispatch.test.ts +20 -53
- package/src/execution/__tests__/delivery-methods.test.ts +179 -237
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +0 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -13
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +20 -33
- package/src/execution/__tests__/gc-timer.test.ts +7 -22
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +20 -10
- package/src/execution/__tests__/helpers/fake-engine-port.ts +187 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +36 -9
- package/src/execution/__tests__/helpers/subagent-service-mocks.ts +1 -1
- package/src/execution/__tests__/idle-gc.test.ts +200 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +26 -21
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +5 -2
- package/src/execution/__tests__/reconcile-sweep.test.ts +200 -0
- package/src/execution/__tests__/record-store.test.ts +60 -6
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +20 -13
- package/src/execution/__tests__/round-supervisor.test.ts +293 -0
- package/src/execution/__tests__/session-pending.test.ts +119 -17
- package/src/execution/__tests__/stream-sink-retirement.test.ts +10 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +1 -11
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +33 -30
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +2 -9
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +59 -129
- package/src/execution/__tests__/subagent-service.test.ts +2 -2
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +109 -7
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +108 -36
- package/src/execution/__tests__/sync-collect-recovery.test.ts +21 -36
- package/src/execution/__tests__/workflow-state-root.test.ts +232 -0
- package/src/execution/agent-registry.ts +6 -2
- package/src/execution/dialog-queue.ts +6 -73
- package/src/execution/engine/__tests__/common/capability-gate.test.ts +115 -15
- package/src/execution/engine/__tests__/common/errors.test.ts +11 -18
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +7 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +9 -3
- package/src/execution/engine/__tests__/common/session-view-service.test.ts +90 -3
- package/src/execution/engine/__tests__/conformance/H9-test-disposition.md +116 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/fake-engine-protocol.mjs +377 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/recorded.fixture.json +331 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/engine-protocol/smoke-run.fixture.json +213 -0
- package/src/execution/engine/__tests__/conformance/chat-round-protocol.test.ts +230 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +69 -42
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +4 -81
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +67 -44
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +81 -54
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +133 -70
- package/src/execution/engine/__tests__/conformance/engine-crash.test.ts +143 -0
- package/src/execution/engine/__tests__/conformance/fake-engine-capabilities.ts +19 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +7 -2
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +6 -5
- package/src/execution/engine/__tests__/conformance/protocol-blackbox.test.ts +398 -0
- package/src/execution/engine/__tests__/conformance/registry-fork-filter.test.ts +221 -0
- package/src/execution/engine/__tests__/conformance/round-liveness-supervisor.test.ts +302 -0
- package/src/execution/engine/__tests__/conformance/static-spawn-guard.test.ts +58 -0
- package/src/execution/engine/__tests__/engine-discovery-scan.test.ts +558 -0
- package/src/execution/engine/__tests__/host-task-spec.test.ts +45 -0
- package/src/execution/engine/__tests__/model-ref-split.test.ts +74 -0
- package/src/execution/engine/__tests__/registry.test.ts +206 -1
- package/src/execution/engine/__tests__/routing.test.ts +97 -0
- package/src/execution/engine/__tests__/run-failure-worktree-cleanup.test.ts +97 -0
- package/src/execution/engine/client/__tests__/__fixtures__/fake-engine.mjs +294 -0
- package/src/execution/engine/client/__tests__/engine-client.test.ts +516 -0
- package/src/execution/engine/client/__tests__/mirror.test.ts +67 -0
- package/src/execution/engine/client/__tests__/pid-file.test.ts +257 -0
- package/src/execution/engine/client/__tests__/protocol-closure.test.ts +95 -0
- package/src/execution/engine/client/__tests__/remote-engine.test.ts +457 -0
- package/src/execution/engine/client/client-options.ts +103 -0
- package/src/execution/engine/client/engine-client.ts +687 -0
- package/src/execution/engine/client/mirror.ts +167 -0
- package/src/execution/engine/client/pid-file.ts +392 -0
- package/src/execution/engine/client/reaper.ts +95 -0
- package/src/execution/engine/client/remote-engine.ts +441 -0
- package/src/execution/engine/client/reverse-router.ts +255 -0
- package/src/execution/engine/common/capability-gate.ts +109 -22
- package/src/execution/engine/common/errors.ts +9 -25
- package/src/execution/engine/common/event-journal.ts +4 -50
- package/src/execution/engine/common/journal-replay.ts +3 -1
- package/src/execution/engine/common/kill-chain.ts +18 -251
- package/src/execution/engine/common/nesting-guard.ts +18 -114
- package/src/execution/engine/common/pool-manager.ts +4 -0
- package/src/execution/engine/common/session-view-service.ts +56 -84
- package/src/execution/engine/config.ts +151 -0
- package/src/execution/engine/d8-compat.ts +346 -0
- package/src/execution/engine/engine-discovery-roots.ts +107 -0
- package/src/execution/engine/engine-discovery-scan.ts +269 -0
- package/src/execution/engine/engine-discovery.ts +30 -7
- package/src/execution/engine/engine-inspect-package.ts +282 -0
- package/src/execution/engine/engine-manifest.ts +244 -0
- package/src/execution/engine/host/__tests__/host-askuser-endpoint.test.ts +123 -0
- package/src/execution/engine/host/__tests__/host-bridge.test.ts +135 -0
- package/src/execution/engine/host/__tests__/spawned-children.test.ts +73 -0
- package/src/execution/engine/host/host-bridge.ts +150 -0
- package/src/execution/engine/host/host-ui-endpoint.ts +47 -0
- package/src/execution/engine/host/pi-host-binding.ts +91 -0
- package/src/execution/engine/host/spawned-children.ts +139 -0
- package/src/execution/engine/host-task-spec.ts +29 -2
- package/src/execution/engine/model-validation.ts +55 -2
- package/src/execution/engine/paths.ts +16 -41
- package/src/execution/engine/port.ts +48 -2
- package/src/execution/engine/registry.ts +153 -34
- package/src/execution/engine/routing.ts +225 -52
- package/src/execution/engine/types.ts +25 -200
- package/src/execution/finalize-record.ts +25 -0
- package/src/execution/idle-gc.ts +89 -14
- package/src/execution/lifecycle-manager.ts +10 -7
- package/src/execution/lifecycle-predicates.ts +10 -8
- package/src/execution/record-store.ts +29 -2
- package/src/execution/relay-env.ts +19 -36
- package/src/execution/round-supervisor/domain.ts +89 -0
- package/src/execution/round-supervisor/index.ts +33 -0
- package/src/execution/round-supervisor/notify-accounting.ts +87 -0
- package/src/execution/round-supervisor/reconcile-sweep.ts +235 -0
- package/src/execution/round-supervisor/service-binding.test.ts +661 -0
- package/src/execution/round-supervisor/service-binding.ts +273 -0
- package/src/execution/round-supervisor/supervisor.ts +426 -0
- package/src/execution/session-pending.ts +68 -74
- package/src/execution/settled-watchdog.ts +39 -0
- package/src/execution/subagent-service.ts +746 -400
- package/src/execution/subprocess-agent-runner.ts +16 -8
- package/src/execution/types.ts +41 -117
- package/src/execution/ui-channels.ts +13 -214
- package/src/execution/ui-request-handler-factory.ts +7 -4
- package/src/execution/ui-request-observability.ts +2 -2
- package/src/execution/workflow-state-root.ts +67 -0
- package/src/execution/worktree-git-ops.ts +5 -0
- package/src/execution/worktree-manager.ts +11 -1
- package/src/index.ts +55 -26
- package/src/orchestration/__tests__/execute-agent-call.test.ts +1 -1
- package/src/orchestration/execute-agent-call.ts +1 -1
- package/src/orchestration/file-run-store.ts +144 -59
- package/src/orchestration/models/types.ts +43 -42
- package/src/shared/zcode-model-ref.ts +31 -0
- package/dist/chunk-43ONBFZX.js +0 -240
- package/dist/chunk-A4IVZWVX.js +0 -31
- package/dist/chunk-APZY4IME.js +0 -27
- package/dist/chunk-V3VHZ2VX.js +0 -59
- package/dist/execution/engine/engines/zcode/constants.cjs +0 -107
- package/dist/execution/engine/engines/zcode/constants.d.cts +0 -135
- package/dist/execution/engine/engines/zcode/constants.d.ts +0 -135
- package/dist/execution/engine/engines/zcode/constants.js +0 -54
- package/dist/execution/engine/engines/zcode/reader.cjs +0 -276
- package/dist/execution/engine/engines/zcode/reader.d.cts +0 -21
- package/dist/execution/engine/engines/zcode/reader.d.ts +0 -21
- package/dist/execution/engine/engines/zcode/reader.js +0 -9
- package/src/__tests__/append-system-prompt-assembly.test.ts +0 -243
- package/src/__tests__/fr4-get-state-handshake.test.ts +0 -125
- package/src/__tests__/session-runner.test.ts +0 -69
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +0 -490
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +0 -191
- package/src/execution/__tests__/descendant-sweep-guards.test.ts +0 -220
- package/src/execution/__tests__/descendant-sweep.test.ts +0 -269
- package/src/execution/__tests__/engine-model-validation.test.ts +0 -741
- package/src/execution/__tests__/epipe-fallback.test.ts +0 -248
- package/src/execution/__tests__/execution-runtime-face.test.ts +0 -272
- package/src/execution/__tests__/get-state-handshake.test.ts +0 -127
- package/src/execution/__tests__/helpers/session-runner-mocks.ts +0 -113
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +0 -280
- package/src/execution/__tests__/kill-all-escalation.test.ts +0 -196
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +0 -113
- package/src/execution/__tests__/output-collector.test.ts +0 -469
- package/src/execution/__tests__/pi-invocation.test.ts +0 -166
- package/src/execution/__tests__/recursive-visibility-env.test.ts +0 -149
- package/src/execution/__tests__/rpc-mode.test.ts +0 -89
- package/src/execution/__tests__/run-and-finalize-anchoring.test.ts +0 -387
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +0 -277
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +0 -254
- package/src/execution/__tests__/run-spawn-edges.test.ts +0 -1079
- package/src/execution/__tests__/run-spawn-integration.test.ts +0 -933
- package/src/execution/__tests__/run-spawn-resume.test.ts +0 -322
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +0 -196
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +0 -207
- package/src/execution/__tests__/service-kill-escalation.test.ts +0 -91
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +0 -110
- package/src/execution/__tests__/session-runner-close-prune.test.ts +0 -181
- package/src/execution/__tests__/session-runner-dispatch.test.ts +0 -489
- package/src/execution/__tests__/session-runner-epipe.test.ts +0 -179
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +0 -227
- package/src/execution/__tests__/session-runner-lifecycle-helpers.test.ts +0 -290
- package/src/execution/__tests__/session-runner-schema-env.test.ts +0 -352
- package/src/execution/__tests__/settled-watchdog.test.ts +0 -524
- package/src/execution/__tests__/spawn-args.test.ts +0 -446
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +0 -192
- package/src/execution/__tests__/spawn-event-adapter.test.ts +0 -167
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +0 -208
- package/src/execution/__tests__/spawned-children.test.ts +0 -236
- package/src/execution/__tests__/start-sync-model-guard.test.ts +0 -150
- package/src/execution/__tests__/stdin-writer.test.ts +0 -464
- package/src/execution/__tests__/subagent-service-message-close.test.ts +0 -693
- package/src/execution/__tests__/temp-prompt.test.ts +0 -53
- package/src/execution/__tests__/timeout-integration.test.ts +0 -616
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +0 -194
- package/src/execution/__tests__/turn-limiter.test.ts +0 -65
- package/src/execution/__tests__/ui-request-handler.test.ts +0 -205
- package/src/execution/__tests__/ui-request-queue.test.ts +0 -298
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +0 -230
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +0 -128
- package/src/execution/engine/__tests__/common/session-view-service-zcode-dbpath.test.ts +0 -149
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +0 -28
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +0 -128
- package/src/execution/engine/__tests__/paths.test.ts +0 -39
- package/src/execution/engine/common/schema-emulation.ts +0 -189
- package/src/execution/engine/common/session-view-projection.ts +0 -51
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +0 -615
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +0 -155
- package/src/execution/engine/engines/pi/__tests__/spawn-opts-direct.test.ts +0 -257
- package/src/execution/engine/engines/pi/argv-mirror.ts +0 -135
- package/src/execution/engine/engines/pi/get-state-handshake.ts +0 -179
- package/src/execution/engine/engines/pi/output-collector.ts +0 -317
- package/src/execution/engine/engines/pi/pi-engine.ts +0 -811
- package/src/execution/engine/engines/pi/pi-invocation.ts +0 -120
- package/src/execution/engine/engines/pi/reader.ts +0 -59
- package/src/execution/engine/engines/pi/registration.ts +0 -37
- package/src/execution/engine/engines/pi/session-runner.ts +0 -2840
- package/src/execution/engine/engines/pi/spawn-event-adapter.ts +0 -404
- package/src/execution/engine/engines/pi/stdin-writer.ts +0 -199
- package/src/execution/engine/engines/pi/temp-prompt.ts +0 -62
- package/src/execution/engine/engines/pi/turn-limiter.ts +0 -102
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +0 -301
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +0 -36
- package/src/execution/engine/engines/zcode/__tests__/appserver-launcher.test.ts +0 -321
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +0 -473
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +0 -43
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +0 -124
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +0 -71
- package/src/execution/engine/engines/zcode/__tests__/session-channel-dispose-harvest.test.ts +0 -247
- package/src/execution/engine/engines/zcode/__tests__/session-channel-turn-timers.test.ts +0 -451
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +0 -792
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +0 -782
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +0 -168
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-dispose.test.ts +0 -210
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-retry.test.ts +0 -353
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-status.test.ts +0 -282
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-timeout.test.ts +0 -372
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +0 -145
- package/src/execution/engine/engines/zcode/appserver-launcher.ts +0 -167
- package/src/execution/engine/engines/zcode/connection.ts +0 -598
- package/src/execution/engine/engines/zcode/constants.ts +0 -176
- package/src/execution/engine/engines/zcode/golden-sample.ts +0 -47
- package/src/execution/engine/engines/zcode/parser.ts +0 -102
- package/src/execution/engine/engines/zcode/preparer.ts +0 -235
- package/src/execution/engine/engines/zcode/reader.ts +0 -381
- package/src/execution/engine/engines/zcode/registration.ts +0 -37
- package/src/execution/engine/engines/zcode/session-channel.ts +0 -968
- package/src/execution/engine/engines/zcode/zcode-engine.ts +0 -1431
- package/src/execution/round-settlement.ts +0 -93
- package/src/execution/ui-request-queue.ts +0 -215
- package/src/shared/schema-env.ts +0 -44
|
@@ -0,0 +1,2305 @@
|
|
|
1
|
+
// src/execution/engine/engine-discovery-scan.ts
|
|
2
|
+
import * as path5 from "path";
|
|
3
|
+
|
|
4
|
+
// src/execution/engine/engine-manifest.ts
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
|
|
7
|
+
// src/core/host-services.ts
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
var DEFAULT_DATA_ROOT = join(homedir(), ".subagent-core");
|
|
11
|
+
var HOST_SLOT_KEY = /* @__PURE__ */ Symbol.for("@zhushanwen/subagent-core.host-services");
|
|
12
|
+
function getHostSlot() {
|
|
13
|
+
let slot = Reflect.get(globalThis, HOST_SLOT_KEY);
|
|
14
|
+
if (!slot) {
|
|
15
|
+
slot = { current: void 0 };
|
|
16
|
+
Reflect.set(globalThis, HOST_SLOT_KEY, slot);
|
|
17
|
+
}
|
|
18
|
+
return slot;
|
|
19
|
+
}
|
|
20
|
+
function configureCore(host) {
|
|
21
|
+
getHostSlot().current = host;
|
|
22
|
+
}
|
|
23
|
+
var NULL_HOST = {
|
|
24
|
+
dataRoot() {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"[subagent-core] core_host_not_configured: HostServices is not configured \u2014 the host shell must call configureCore(host) during initialization, before any core API that needs host services is consumed. Recovery: pi shell wires HostServices in src/host/pi-host.ts of @zhushanwen/pi-subagent-workflow; zsw shell: see the wiring example in the @zhushanwen/subagent-core package README."
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
log(level, component, message, data) {
|
|
30
|
+
const line = `[${component}] ${message}`;
|
|
31
|
+
if (level === "error") {
|
|
32
|
+
if (data === void 0) console.error(line);
|
|
33
|
+
else console.error(line, data);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (level === "warn") {
|
|
37
|
+
if (data === void 0) console.warn(line);
|
|
38
|
+
else console.warn(line, data);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// discoveryRoots 刻意不实现:可选端口缺席 = undefined,由调用方走缺省发现语义。
|
|
43
|
+
};
|
|
44
|
+
function getHostServices() {
|
|
45
|
+
return getHostSlot().current ?? NULL_HOST;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/core/logger.ts
|
|
49
|
+
var facadeCache = /* @__PURE__ */ new Map();
|
|
50
|
+
function getLogger(component) {
|
|
51
|
+
const existing = facadeCache.get(component);
|
|
52
|
+
if (existing) return existing;
|
|
53
|
+
const facade = {
|
|
54
|
+
debug(msg, data) {
|
|
55
|
+
getHostServices().log("debug", component, msg, data);
|
|
56
|
+
},
|
|
57
|
+
warn(msg, data) {
|
|
58
|
+
getHostServices().log("warn", component, msg, data);
|
|
59
|
+
},
|
|
60
|
+
error(msg, data) {
|
|
61
|
+
getHostServices().log("error", component, msg, data);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
facadeCache.set(component, facade);
|
|
65
|
+
return facade;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/execution/engine/engine-manifest.ts
|
|
69
|
+
var logger = getLogger("subagents");
|
|
70
|
+
var RESERVED_ENV_PREFIXES = ["XYZ_", "XYZ_AGENT_", "XYZ_SUBAGENT_"];
|
|
71
|
+
var ENV_PREFIX_PATTERN = /^[A-Za-z0-9_]+$/;
|
|
72
|
+
var CONSERVATIVE_CAPABILITIES = {
|
|
73
|
+
schemaEnforcement: "emulated",
|
|
74
|
+
steer: "unsupported",
|
|
75
|
+
conversation: "unsupported",
|
|
76
|
+
personaInjection: "prompt",
|
|
77
|
+
eventGranularity: "coarse",
|
|
78
|
+
sandbox: "none",
|
|
79
|
+
sessionRead: "outcome-only",
|
|
80
|
+
resume: "unsupported",
|
|
81
|
+
interrupt: "kill-only",
|
|
82
|
+
permissionMode: "ignored",
|
|
83
|
+
maxTurns: false
|
|
84
|
+
};
|
|
85
|
+
var CAPABILITY_ENUMS = {
|
|
86
|
+
schemaEnforcement: ["native", "emulated"],
|
|
87
|
+
steer: ["native", "emulated", "unsupported"],
|
|
88
|
+
conversation: ["native", "unsupported"],
|
|
89
|
+
personaInjection: ["file", "flag", "prompt"],
|
|
90
|
+
eventGranularity: ["stream", "coarse"],
|
|
91
|
+
sandbox: ["native", "emulated", "none"],
|
|
92
|
+
sessionRead: ["full", "partial", "outcome-only"],
|
|
93
|
+
resume: ["native", "cold", "unsupported"],
|
|
94
|
+
interrupt: ["native", "kill-only"],
|
|
95
|
+
permissionMode: ["native", "fixed", "ignored"]
|
|
96
|
+
};
|
|
97
|
+
function resolveManifestBin(pkgDir, pkg, manifestBin) {
|
|
98
|
+
const npmBin = pkg["bin"];
|
|
99
|
+
if (typeof npmBin === "string") return path.resolve(pkgDir, npmBin);
|
|
100
|
+
if (typeof npmBin === "object" && npmBin !== null) {
|
|
101
|
+
const rel = npmBin[manifestBin];
|
|
102
|
+
if (typeof rel === "string" && rel.trim() !== "") return path.resolve(pkgDir, rel);
|
|
103
|
+
}
|
|
104
|
+
return void 0;
|
|
105
|
+
}
|
|
106
|
+
function parseCapabilities(id, raw) {
|
|
107
|
+
if (raw === void 0 || raw === null) {
|
|
108
|
+
logger.warn(
|
|
109
|
+
`[engine-discovery] engine '${id}': manifest capabilities is required \u2014 falling back to most conservative values`
|
|
110
|
+
);
|
|
111
|
+
return { ...CONSERVATIVE_CAPABILITIES };
|
|
112
|
+
}
|
|
113
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
114
|
+
logger.warn(
|
|
115
|
+
`[engine-discovery] engine '${id}': manifest capabilities must be an object \u2014 falling back to most conservative values`
|
|
116
|
+
);
|
|
117
|
+
return { ...CONSERVATIVE_CAPABILITIES };
|
|
118
|
+
}
|
|
119
|
+
const v = raw;
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const [key, allowed] of Object.entries(CAPABILITY_ENUMS)) {
|
|
122
|
+
const value = v[key];
|
|
123
|
+
if (value === void 0) {
|
|
124
|
+
out[key] = CONSERVATIVE_CAPABILITIES[key];
|
|
125
|
+
logger.warn(
|
|
126
|
+
`[engine-discovery] engine '${id}': capabilities.${key} missing \u2014 conservative default '${String(out[key])}'`
|
|
127
|
+
);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (typeof value === "string" && allowed.includes(value)) {
|
|
131
|
+
out[key] = value;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
out[key] = CONSERVATIVE_CAPABILITIES[key];
|
|
135
|
+
logger.warn(
|
|
136
|
+
`[engine-discovery] engine '${id}': capabilities.${key}=${describeValue(value)} invalid (expected one of ${allowed.join("|")}) \u2014 conservative default '${String(out[key])}'`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const maxTurns = v["maxTurns"];
|
|
140
|
+
if (maxTurns === void 0) {
|
|
141
|
+
out["maxTurns"] = false;
|
|
142
|
+
logger.warn(`[engine-discovery] engine '${id}': capabilities.maxTurns missing \u2014 conservative default 'false'`);
|
|
143
|
+
} else if (typeof maxTurns !== "boolean") {
|
|
144
|
+
out["maxTurns"] = false;
|
|
145
|
+
logger.warn(
|
|
146
|
+
`[engine-discovery] engine '${id}': capabilities.maxTurns=${describeValue(maxTurns)} invalid (expected boolean) \u2014 conservative default 'false'`
|
|
147
|
+
);
|
|
148
|
+
} else {
|
|
149
|
+
out["maxTurns"] = maxTurns;
|
|
150
|
+
}
|
|
151
|
+
const knownKeys = /* @__PURE__ */ new Set([...Object.keys(CAPABILITY_ENUMS), "maxTurns"]);
|
|
152
|
+
const unknownKeys = Object.keys(v).filter((k) => !knownKeys.has(k));
|
|
153
|
+
if (unknownKeys.length > 0) {
|
|
154
|
+
logger.warn(
|
|
155
|
+
`[engine-discovery] engine '${id}': unknown capabilities key(s) ignored: ${unknownKeys.join(", ")}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
function parseEnvPrefixes(id, raw) {
|
|
161
|
+
if (raw === void 0) return [];
|
|
162
|
+
if (!Array.isArray(raw)) {
|
|
163
|
+
logger.warn(
|
|
164
|
+
`[engine-discovery] engine '${id}': envPrefixes must be an array \u2014 ignoring (engine remains usable)`
|
|
165
|
+
);
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
const out = [];
|
|
169
|
+
const seen = /* @__PURE__ */ new Set();
|
|
170
|
+
for (const item of raw) {
|
|
171
|
+
if (typeof item !== "string" || item === "" || item.includes("*") || !ENV_PREFIX_PATTERN.test(item)) {
|
|
172
|
+
logger.warn(
|
|
173
|
+
`[engine-discovery] engine '${id}': envPrefixes entry ${describeValue(item)} rejected (must match ^[A-Za-z0-9_]+$, non-empty, no '*') \u2014 dropping the prefix, engine remains usable`
|
|
174
|
+
);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const upper = item.toUpperCase();
|
|
178
|
+
if (RESERVED_ENV_PREFIXES.some((reserved) => upper.startsWith(reserved))) {
|
|
179
|
+
logger.warn(
|
|
180
|
+
`[engine-discovery] engine '${id}': envPrefixes entry '${item}' rejected (reserved host prefix) \u2014 dropping the prefix, engine remains usable`
|
|
181
|
+
);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (seen.has(upper)) continue;
|
|
185
|
+
seen.add(upper);
|
|
186
|
+
out.push(item);
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
function parseModelCatalog(id, raw) {
|
|
191
|
+
if (raw === void 0) return void 0;
|
|
192
|
+
if (raw === null) return null;
|
|
193
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
194
|
+
logger.warn(`[engine-discovery] engine '${id}': modelCatalog must be an object \u2014 ignoring (no model catalog)`);
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
const v = raw;
|
|
198
|
+
const dynamic = parseCatalogDynamic(id, v);
|
|
199
|
+
if (!Array.isArray(v["models"])) {
|
|
200
|
+
logger.warn(
|
|
201
|
+
`[engine-discovery] engine '${id}': modelCatalog.models must be an array \u2014 treating catalog as absent (null)`
|
|
202
|
+
);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const models = [];
|
|
206
|
+
for (const item of v["models"]) {
|
|
207
|
+
const entry = parseModelCatalogEntry(id, item);
|
|
208
|
+
if (entry === void 0) continue;
|
|
209
|
+
models.push(entry);
|
|
210
|
+
}
|
|
211
|
+
return { dynamic, models };
|
|
212
|
+
}
|
|
213
|
+
function parseCatalogDynamic(id, v) {
|
|
214
|
+
const rawDynamic = v["dynamic"];
|
|
215
|
+
if (rawDynamic === void 0) return true;
|
|
216
|
+
if (typeof rawDynamic === "boolean") return rawDynamic;
|
|
217
|
+
logger.warn(
|
|
218
|
+
`[engine-discovery] engine '${id}': modelCatalog.dynamic=${describeValue(rawDynamic)} invalid (expected boolean) \u2014 defaulting to true`
|
|
219
|
+
);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
function parseModelCatalogEntry(id, item) {
|
|
223
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
224
|
+
logger.warn(`[engine-discovery] engine '${id}': modelCatalog entry ${describeValue(item)} is not an object \u2014 dropping entry`);
|
|
225
|
+
return void 0;
|
|
226
|
+
}
|
|
227
|
+
const entryId = item["id"];
|
|
228
|
+
if (typeof entryId !== "string" || entryId.trim() === "") {
|
|
229
|
+
logger.warn(`[engine-discovery] engine '${id}': modelCatalog entry missing string id \u2014 dropping entry`);
|
|
230
|
+
return void 0;
|
|
231
|
+
}
|
|
232
|
+
const aliases = item["aliases"];
|
|
233
|
+
if (aliases !== void 0 && !Array.isArray(aliases)) {
|
|
234
|
+
logger.warn(`[engine-discovery] engine '${id}': modelCatalog entry '${entryId}' aliases must be an array \u2014 dropping aliases`);
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
id: entryId,
|
|
238
|
+
...Array.isArray(aliases) ? { aliases: aliases.filter((a) => typeof a === "string") } : {},
|
|
239
|
+
...item["canonicalRef"] !== void 0 ? { canonicalRef: item["canonicalRef"] } : {}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function describeValue(value) {
|
|
243
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
244
|
+
if (typeof value === "object" && value !== null) return Array.isArray(value) ? "array" : "object";
|
|
245
|
+
return String(value);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/execution/engine/engine-inspect-package.ts
|
|
249
|
+
import * as fs from "fs";
|
|
250
|
+
import * as path2 from "path";
|
|
251
|
+
import { isProtocolVersionCompatible as isProtocolVersionCompatible2 } from "@zhushanwen/subagent-engine-sdk";
|
|
252
|
+
|
|
253
|
+
// src/execution/engine/common/data-dir.ts
|
|
254
|
+
var logger2 = getLogger("subagents");
|
|
255
|
+
var XYZ_DATA_DIR_ENV = "XYZ_AGENT_DATA_DIR";
|
|
256
|
+
var warned = false;
|
|
257
|
+
function getEngineDataDir(env = process.env, warn = defaultWarn) {
|
|
258
|
+
const fromEnv = env[XYZ_DATA_DIR_ENV]?.trim();
|
|
259
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
260
|
+
const fallback = getHostServices().dataRoot();
|
|
261
|
+
if (!warned) {
|
|
262
|
+
warned = true;
|
|
263
|
+
warn(
|
|
264
|
+
`[engine-data-dir] ${XYZ_DATA_DIR_ENV} is not set; engine journal/pool fall back to the pi agent dir (${fallback}). The xyz-agent host normally injects this env \u2014 if you are running inside xyz-agent, check the runtime spawn env; standalone pi installs intentionally use the pi agent dir.`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return fallback;
|
|
268
|
+
}
|
|
269
|
+
function defaultWarn(msg) {
|
|
270
|
+
logger2.warn(msg);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/execution/engine/client/engine-client.ts
|
|
274
|
+
import { spawn } from "child_process";
|
|
275
|
+
import {
|
|
276
|
+
buildEngineChildEnv,
|
|
277
|
+
CANCEL_SETTLE_GRACE_MS,
|
|
278
|
+
CRASH_REBUILD_BACKOFF_MS,
|
|
279
|
+
CRASH_REBUILD_MAX_ATTEMPTS,
|
|
280
|
+
ENGINE_PROTOCOL_VERSION,
|
|
281
|
+
EngineSdkError,
|
|
282
|
+
HANDSHAKE_TIMEOUT_MS,
|
|
283
|
+
getLogger as getLogger7,
|
|
284
|
+
isProtocolVersionCompatible,
|
|
285
|
+
isResponseFrame,
|
|
286
|
+
isNotificationFrame,
|
|
287
|
+
isReverseRequestFrame,
|
|
288
|
+
STDERR_TAIL_CHARS,
|
|
289
|
+
engineProtocolMismatchError,
|
|
290
|
+
hostKindOf,
|
|
291
|
+
resolveEngineNodeLaunch
|
|
292
|
+
} from "@zhushanwen/subagent-engine-sdk";
|
|
293
|
+
|
|
294
|
+
// src/execution/engine/client/mirror.ts
|
|
295
|
+
import { getLogger as getLogger2 } from "@zhushanwen/subagent-engine-sdk";
|
|
296
|
+
var logger3 = getLogger2("subagents");
|
|
297
|
+
var SpawnedChildrenMirror = class {
|
|
298
|
+
entries = /* @__PURE__ */ new Map();
|
|
299
|
+
listeners = /* @__PURE__ */ new Set();
|
|
300
|
+
/** `host/childSpawned`:落项(此前该 record 无句柄——失效语义 1)。 */
|
|
301
|
+
recordSpawned(pid, recordId) {
|
|
302
|
+
this.entries.set(pid, {
|
|
303
|
+
pid,
|
|
304
|
+
recordId,
|
|
305
|
+
state: "running",
|
|
306
|
+
killed: false,
|
|
307
|
+
updatedAt: Date.now()
|
|
308
|
+
});
|
|
309
|
+
this.emit({ reason: "childSpawned", pid, recordId });
|
|
310
|
+
}
|
|
311
|
+
/** `host/childStateChanged`:更新项(载荷 killed 必含——类型层 required)。 */
|
|
312
|
+
recordStateChanged(patch) {
|
|
313
|
+
const existing = this.entries.get(patch.pid);
|
|
314
|
+
const entry = {
|
|
315
|
+
pid: patch.pid,
|
|
316
|
+
recordId: patch.recordId,
|
|
317
|
+
state: patch.state,
|
|
318
|
+
killed: patch.killed,
|
|
319
|
+
exitCode: patch.exitCode ?? existing?.exitCode,
|
|
320
|
+
signal: patch.signal ?? existing?.signal,
|
|
321
|
+
updatedAt: Date.now()
|
|
322
|
+
};
|
|
323
|
+
this.entries.set(patch.pid, entry);
|
|
324
|
+
this.emit({ reason: "childStateChanged", pid: patch.pid, recordId: patch.recordId });
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* 失效语义 2 + 3:全部镜像项整体置死(killed=true + state=exited)→ 逐项广播 →
|
|
328
|
+
* 清空 Map。调用点 = 引擎进程 exit / 重建 / dispose / killAll(EngineClient 统一编排)。
|
|
329
|
+
*/
|
|
330
|
+
killAll() {
|
|
331
|
+
if (this.entries.size === 0) {
|
|
332
|
+
this.emit({ reason: "killedAll" });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
for (const entry of this.entries.values()) {
|
|
336
|
+
entry.killed = true;
|
|
337
|
+
entry.state = "exited";
|
|
338
|
+
entry.updatedAt = Date.now();
|
|
339
|
+
this.emit({ reason: "killedAll", pid: entry.pid, recordId: entry.recordId });
|
|
340
|
+
}
|
|
341
|
+
this.entries.clear();
|
|
342
|
+
}
|
|
343
|
+
/** 未收 childSpawned / 已被整体置死清空后 → undefined(无句柄等价,失效语义 1)。 */
|
|
344
|
+
getChildByRecord(recordId) {
|
|
345
|
+
for (const entry of this.entries.values()) {
|
|
346
|
+
if (entry.recordId === recordId) return entry;
|
|
347
|
+
}
|
|
348
|
+
return void 0;
|
|
349
|
+
}
|
|
350
|
+
getEntry(pid) {
|
|
351
|
+
return this.entries.get(pid);
|
|
352
|
+
}
|
|
353
|
+
/** 句柄存活谓词:镜像有项且未被置死(`child !== undefined && !child.killed`)。 */
|
|
354
|
+
hasLiveProcessHandle(recordId) {
|
|
355
|
+
const entry = this.getChildByRecord(recordId);
|
|
356
|
+
return entry !== void 0 && !entry.killed;
|
|
357
|
+
}
|
|
358
|
+
/** isResumable 的镜像面(W6 lifecycle-predicates 改读本方法;语义同 hasLiveProcessHandle)。 */
|
|
359
|
+
isResumable(recordId) {
|
|
360
|
+
return this.hasLiveProcessHandle(recordId);
|
|
361
|
+
}
|
|
362
|
+
/** 快照(诊断/测试)。 */
|
|
363
|
+
snapshot() {
|
|
364
|
+
return [...this.entries.values()];
|
|
365
|
+
}
|
|
366
|
+
get size() {
|
|
367
|
+
return this.entries.size;
|
|
368
|
+
}
|
|
369
|
+
/** 订阅状态广播(W6 notify/谓词接线点)。返回退订函数。 */
|
|
370
|
+
onChange(listener) {
|
|
371
|
+
this.listeners.add(listener);
|
|
372
|
+
return () => {
|
|
373
|
+
this.listeners.delete(listener);
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
emit(event) {
|
|
377
|
+
for (const listener of this.listeners) {
|
|
378
|
+
try {
|
|
379
|
+
listener(event);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
logger3.debug(
|
|
382
|
+
`[spawned-children-mirror] listener threw for ${event.reason}: ${err instanceof Error ? err.message : String(err)}`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// src/execution/engine/client/pid-file.ts
|
|
390
|
+
import { spawnSync } from "child_process";
|
|
391
|
+
import { readdirSync, readFileSync, renameSync, rmSync, writeFileSync, mkdirSync } from "fs";
|
|
392
|
+
import { join as join2 } from "path";
|
|
393
|
+
import { getLogger as getLogger3, resolveEngineDir } from "@zhushanwen/subagent-engine-sdk";
|
|
394
|
+
var logger4 = getLogger3("subagents");
|
|
395
|
+
var CMDLINE_PROBE_TIMEOUT_MS = 3e3;
|
|
396
|
+
var PIDFILE_JSON_INDENT = 2;
|
|
397
|
+
function pidfilePath(engineDataDir, engineId, hostKind, hostPid) {
|
|
398
|
+
return join2(resolveEngineDir(engineDataDir, engineId), `engine.${hostKind}.${hostPid}.pid`);
|
|
399
|
+
}
|
|
400
|
+
function writePidfileAtomic(path6, content) {
|
|
401
|
+
mkdirSync(join2(path6, ".."), { recursive: true });
|
|
402
|
+
const tmp = `${path6}.${process.pid}.${Date.now()}.tmp`;
|
|
403
|
+
writeFileSync(tmp, `${JSON.stringify(content, null, PIDFILE_JSON_INDENT)}
|
|
404
|
+
`, "utf-8");
|
|
405
|
+
renameSync(tmp, path6);
|
|
406
|
+
}
|
|
407
|
+
function readPidfile(path6) {
|
|
408
|
+
try {
|
|
409
|
+
const raw = readFileSync(path6, "utf-8");
|
|
410
|
+
const parsed = JSON.parse(raw);
|
|
411
|
+
if (typeof parsed.enginePid !== "number" || typeof parsed.hostPid !== "number" || typeof parsed.engineId !== "string" || typeof parsed.hostKind !== "string") {
|
|
412
|
+
return void 0;
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
enginePid: parsed.enginePid,
|
|
416
|
+
hostPid: parsed.hostPid,
|
|
417
|
+
engineStartTime: parsed.engineStartTime === null || typeof parsed.engineStartTime === "string" ? parsed.engineStartTime : null,
|
|
418
|
+
engineId: parsed.engineId,
|
|
419
|
+
hostKind: parsed.hostKind,
|
|
420
|
+
writtenAt: typeof parsed.writtenAt === "string" ? parsed.writtenAt : ""
|
|
421
|
+
};
|
|
422
|
+
} catch (err) {
|
|
423
|
+
logger4.debug(`[pid-file] readPidfile(${path6}) failed, treating as stale: ${err instanceof Error ? err.message : String(err)}`);
|
|
424
|
+
return void 0;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function removePidfile(path6) {
|
|
428
|
+
try {
|
|
429
|
+
rmSync(path6, { force: true });
|
|
430
|
+
} catch (err) {
|
|
431
|
+
logger4.debug(`[pid-file] removePidfile(${path6}) failed (concurrent sweep = expected): ${err instanceof Error ? err.message : String(err)}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function registerEnginePidfile(reg) {
|
|
435
|
+
const path6 = pidfilePath(reg.engineDataDir, reg.engineId, reg.hostKind, reg.hostPid);
|
|
436
|
+
try {
|
|
437
|
+
writePidfileAtomic(path6, {
|
|
438
|
+
enginePid: reg.enginePid,
|
|
439
|
+
hostPid: reg.hostPid,
|
|
440
|
+
engineStartTime: readProcessStartTime(reg.enginePid) ?? null,
|
|
441
|
+
engineId: reg.engineId,
|
|
442
|
+
hostKind: reg.hostKind,
|
|
443
|
+
writtenAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
444
|
+
});
|
|
445
|
+
return path6;
|
|
446
|
+
} catch (err) {
|
|
447
|
+
logger4.warn(
|
|
448
|
+
`[pid-file] pidfile write failed for ${reg.engineId} (sweep coverage degraded): ${err instanceof Error ? err.message : String(err)}`
|
|
449
|
+
);
|
|
450
|
+
return void 0;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function sweepEnginePidfiles(args) {
|
|
454
|
+
try {
|
|
455
|
+
return sweepStalePidfiles(args);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
logger4.warn(
|
|
458
|
+
`[pid-file] pidfile sweep failed for ${args.engineId} (conservative skip): ${err instanceof Error ? err.message : String(err)}`
|
|
459
|
+
);
|
|
460
|
+
return { killed: [], removed: [], skipped: [] };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function isProcessAlive(pid) {
|
|
464
|
+
try {
|
|
465
|
+
process.kill(pid, 0);
|
|
466
|
+
return true;
|
|
467
|
+
} catch (err) {
|
|
468
|
+
const code = err.code;
|
|
469
|
+
if (code === "ESRCH") return false;
|
|
470
|
+
if (code === "EPERM") return void 0;
|
|
471
|
+
return void 0;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function readProcessCmdline(pid) {
|
|
475
|
+
try {
|
|
476
|
+
const r = spawnSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
477
|
+
encoding: "utf-8",
|
|
478
|
+
timeout: CMDLINE_PROBE_TIMEOUT_MS
|
|
479
|
+
});
|
|
480
|
+
if (r.error || r.status !== 0) return void 0;
|
|
481
|
+
const out = typeof r.stdout === "string" ? r.stdout.trim() : "";
|
|
482
|
+
return out.length > 0 ? out : void 0;
|
|
483
|
+
} catch {
|
|
484
|
+
return void 0;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function readProcessStartTime(pid) {
|
|
488
|
+
try {
|
|
489
|
+
const r = spawnSync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
490
|
+
encoding: "utf-8",
|
|
491
|
+
timeout: CMDLINE_PROBE_TIMEOUT_MS
|
|
492
|
+
});
|
|
493
|
+
if (r.error || r.status !== 0) return void 0;
|
|
494
|
+
const out = typeof r.stdout === "string" ? r.stdout.trim() : "";
|
|
495
|
+
return out.length > 0 ? out : void 0;
|
|
496
|
+
} catch {
|
|
497
|
+
return void 0;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function sweepStalePidfiles(opts) {
|
|
501
|
+
const result = { killed: [], removed: [], skipped: [] };
|
|
502
|
+
const platform = opts.platform ?? process.platform;
|
|
503
|
+
const dir = resolveEngineDir(opts.engineDataDir, opts.engineId);
|
|
504
|
+
let files;
|
|
505
|
+
try {
|
|
506
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".pid"));
|
|
507
|
+
} catch {
|
|
508
|
+
return result;
|
|
509
|
+
}
|
|
510
|
+
for (const file of files) {
|
|
511
|
+
sweepOnePidfile(join2(dir, file), file, opts, platform, result);
|
|
512
|
+
}
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
function sweepOnePidfile(path6, file, opts, platform, result) {
|
|
516
|
+
const content = readPidfile(path6);
|
|
517
|
+
if (content === void 0) {
|
|
518
|
+
removePidfile(path6);
|
|
519
|
+
result.removed.push({ file, reason: "unreadable-or-corrupt" });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (content.hostPid === opts.currentHostPid) {
|
|
523
|
+
result.skipped.push({ file, reason: "own-pidfile" });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const hostAlive = isProcessAlive(content.hostPid);
|
|
527
|
+
if (hostAlive === void 0) {
|
|
528
|
+
result.skipped.push({ file, reason: "host-pid-probe-uncertain" });
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (hostAlive) {
|
|
532
|
+
result.skipped.push({ file, reason: "host-pid-alive" });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
reapDeadHostEngine(path6, file, content, opts, platform, result);
|
|
536
|
+
}
|
|
537
|
+
function reapDeadHostEngine(path6, file, content, opts, platform, result) {
|
|
538
|
+
if (platform === "win32") {
|
|
539
|
+
removePidfile(path6);
|
|
540
|
+
result.removed.push({ file, reason: "stale (host dead); win32 never kills" });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const engineAlive = isProcessAlive(content.enginePid);
|
|
544
|
+
if (engineAlive === void 0) {
|
|
545
|
+
result.skipped.push({ file, reason: "engine-pid-probe-uncertain" });
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (!engineAlive) {
|
|
549
|
+
removePidfile(path6);
|
|
550
|
+
result.removed.push({ file, reason: "engine-pid-dead" });
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const cmdline = readProcessCmdline(content.enginePid);
|
|
554
|
+
if (cmdline === void 0) {
|
|
555
|
+
result.skipped.push({ file, reason: "engine-cmdline-probe-uncertain" });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (!opts.matchesEngineCmdline(cmdline)) {
|
|
559
|
+
removePidfile(path6);
|
|
560
|
+
result.removed.push({ file, reason: "cmdline-mismatch (pid reused by unrelated process)" });
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const startTime = readProcessStartTime(content.enginePid);
|
|
564
|
+
if (startTime === void 0) {
|
|
565
|
+
result.skipped.push({ file, reason: "engine-start-time-probe-uncertain" });
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (content.engineStartTime === null || content.engineStartTime !== startTime) {
|
|
569
|
+
removePidfile(path6);
|
|
570
|
+
result.removed.push({ file, reason: "engine-start-time-mismatch (pid reused)" });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const killEngine = opts.killEngine ?? defaultKillEngineProcessGroup;
|
|
574
|
+
killEngine(content.enginePid);
|
|
575
|
+
removePidfile(path6);
|
|
576
|
+
result.killed.push(content.enginePid);
|
|
577
|
+
result.removed.push({ file, reason: "swept after kill (stale orphan)" });
|
|
578
|
+
}
|
|
579
|
+
function defaultKillEngineProcessGroup(enginePid) {
|
|
580
|
+
try {
|
|
581
|
+
process.kill(-enginePid, "SIGKILL");
|
|
582
|
+
return;
|
|
583
|
+
} catch (groupErr) {
|
|
584
|
+
logger4.debug(`[pid-file] process-group kill(-${enginePid}) failed, falling back to single kill: ${groupErr instanceof Error ? groupErr.message : String(groupErr)}`);
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
process.kill(enginePid, "SIGKILL");
|
|
588
|
+
} catch (err) {
|
|
589
|
+
logger4.debug(`[pid-file] single kill(${enginePid}) failed (already dead = expected): ${err instanceof Error ? err.message : String(err)}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/execution/engine/client/reverse-router.ts
|
|
594
|
+
import {
|
|
595
|
+
getLogger as getLogger4,
|
|
596
|
+
REVERSE_CHANNEL_TIMEOUT_CLASS,
|
|
597
|
+
REVERSE_REQUEST_TIMEOUT_MS
|
|
598
|
+
} from "@zhushanwen/subagent-engine-sdk";
|
|
599
|
+
var logger5 = getLogger4("subagents");
|
|
600
|
+
function routeReverseRequest(deps, frame) {
|
|
601
|
+
if (frame.method === "host/askUser") {
|
|
602
|
+
handleInteractionRequest(deps, frame, deps.uiRequestHandler, (params) => params.request);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (frame.method === "host/permission") {
|
|
606
|
+
handleInteractionRequest(deps, frame, deps.permissionHandler, (params) => params);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const timeoutClass = REVERSE_CHANNEL_TIMEOUT_CLASS[frame.method];
|
|
610
|
+
if (timeoutClass === "data-plane") {
|
|
611
|
+
handleDataPlaneRequest(deps, frame);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
deps.sendResponse(frame.id, { unsupported: true });
|
|
615
|
+
}
|
|
616
|
+
function handleInteractionRequest(deps, frame, handler, extract) {
|
|
617
|
+
if (handler === void 0) {
|
|
618
|
+
deps.sendResponse(frame.id, { unsupported: true });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
deps.sendResponse(frame.id, { ack: true });
|
|
622
|
+
let input;
|
|
623
|
+
try {
|
|
624
|
+
input = extract(frame.params);
|
|
625
|
+
} catch {
|
|
626
|
+
deps.sendResponse(frame.id, { cancelled: true });
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
void (async () => {
|
|
630
|
+
try {
|
|
631
|
+
const result = await handler(input);
|
|
632
|
+
if (!deps.isDisposed()) deps.sendResponse(frame.id, result);
|
|
633
|
+
} catch (err) {
|
|
634
|
+
logger5.warn(
|
|
635
|
+
`[engine-client:${deps.engineId}] ${frame.method} handler failed, answering cancelled: ${err instanceof Error ? err.message : String(err)}`
|
|
636
|
+
);
|
|
637
|
+
if (!deps.isDisposed()) deps.sendResponse(frame.id, { cancelled: true });
|
|
638
|
+
}
|
|
639
|
+
})();
|
|
640
|
+
}
|
|
641
|
+
function handleDataPlaneRequest(deps, frame) {
|
|
642
|
+
let settled = false;
|
|
643
|
+
const guardTimer = setTimeout(() => {
|
|
644
|
+
if (settled) return;
|
|
645
|
+
logger5.error(
|
|
646
|
+
`[engine-client:${deps.engineId}] data-plane reverse request ${frame.method} unanswered for ${REVERSE_REQUEST_TIMEOUT_MS}ms \u2014 treating as engine fault`
|
|
647
|
+
);
|
|
648
|
+
void deps.failEngine(
|
|
649
|
+
`data-plane reverse request ${frame.method} unanswered for ${REVERSE_REQUEST_TIMEOUT_MS}ms (engine fault)`
|
|
650
|
+
);
|
|
651
|
+
}, REVERSE_REQUEST_TIMEOUT_MS);
|
|
652
|
+
void Promise.resolve().then(() => dispatchDataPlane(deps, frame.method, frame.params)).then(
|
|
653
|
+
() => {
|
|
654
|
+
settled = true;
|
|
655
|
+
clearTimeout(guardTimer);
|
|
656
|
+
deps.sendResponse(frame.id, { ok: true });
|
|
657
|
+
},
|
|
658
|
+
(err) => {
|
|
659
|
+
settled = true;
|
|
660
|
+
clearTimeout(guardTimer);
|
|
661
|
+
logger5.warn(
|
|
662
|
+
`[engine-client:${deps.engineId}] ${frame.method} dispatch failed (answering ok, host-side bug): ${err instanceof Error ? err.message : String(err)}`
|
|
663
|
+
);
|
|
664
|
+
deps.sendResponse(frame.id, { ok: true });
|
|
665
|
+
}
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
var DATA_PLANE_HANDLERS = {
|
|
669
|
+
"host/log": (deps, params) => dispatchLog(deps, params),
|
|
670
|
+
"host/streamDelta": (deps, params) => dispatchStreamDelta(deps, params),
|
|
671
|
+
"host/roundLifecycle": (deps, params) => dispatchRoundLifecycle(deps, params),
|
|
672
|
+
"host/poolResolved": (deps, params) => dispatchPoolResolved(deps, params),
|
|
673
|
+
"host/handleReady": (deps, params) => dispatchHandleReady(deps, params),
|
|
674
|
+
"host/childSpawned": (deps, params) => dispatchChildSpawned(deps, params),
|
|
675
|
+
"host/childStateChanged": (deps, params) => dispatchChildStateChanged(deps, params)
|
|
676
|
+
};
|
|
677
|
+
async function dispatchDataPlane(deps, method, params) {
|
|
678
|
+
await DATA_PLANE_HANDLERS[method]?.(deps, params);
|
|
679
|
+
}
|
|
680
|
+
function dispatchLog(deps, p) {
|
|
681
|
+
if (deps.log !== void 0) deps.log(p);
|
|
682
|
+
else logger5[p.level](`[engine:${p.component}] ${p.message}`, p.data);
|
|
683
|
+
}
|
|
684
|
+
async function dispatchStreamDelta(deps, p) {
|
|
685
|
+
if (p.recordId !== void 0) {
|
|
686
|
+
await deps.recordRoutes.get(p.recordId)?.onStreamDelta?.(p.delta);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
await deps.runRoutes.get(p.runId)?.onStreamDelta?.(p.delta);
|
|
690
|
+
}
|
|
691
|
+
async function dispatchRoundLifecycle(deps, p) {
|
|
692
|
+
if (p.recordId !== void 0) {
|
|
693
|
+
await deps.recordRoutes.get(p.recordId)?.onRoundLifecycle?.(p);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
await deps.runRoutes.get(p.runId)?.onRoundLifecycle?.(p);
|
|
697
|
+
}
|
|
698
|
+
async function dispatchPoolResolved(deps, p) {
|
|
699
|
+
await deps.runRoutes.get(p.runId)?.onPoolResolved?.(p.poolKey);
|
|
700
|
+
}
|
|
701
|
+
async function dispatchHandleReady(deps, p) {
|
|
702
|
+
deps.setPartialHandle({ sessionRef: p.sessionRef, poolKey: p.poolKey });
|
|
703
|
+
await deps.runRoutes.get(p.runId)?.onHandleReady?.({
|
|
704
|
+
sessionRef: p.sessionRef,
|
|
705
|
+
poolKey: p.poolKey
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function dispatchChildSpawned(deps, p) {
|
|
709
|
+
deps.mirror.recordSpawned(p.pid, p.recordId);
|
|
710
|
+
if (process.platform !== "win32" && typeof p.pid === "number") {
|
|
711
|
+
try {
|
|
712
|
+
process.kill(-p.pid, 0);
|
|
713
|
+
logger5.warn(
|
|
714
|
+
`[engine-client:${deps.engineId}] childSpawned pid ${p.pid} leads its own process group (not in the engine harvest group \u2014 one-generation children + in-group descendants only; engine-detached descendants are an accepted cost per design \xA73.9)`
|
|
715
|
+
);
|
|
716
|
+
} catch (err) {
|
|
717
|
+
logger5.debug(
|
|
718
|
+
`[engine-client:${deps.engineId}] childSpawned pid ${p.pid} group probe settled (${err instanceof Error ? err.message : String(err)})`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
function dispatchChildStateChanged(deps, p) {
|
|
724
|
+
deps.mirror.recordStateChanged({
|
|
725
|
+
pid: p.pid,
|
|
726
|
+
recordId: p.recordId,
|
|
727
|
+
state: p.state,
|
|
728
|
+
killed: p.killed,
|
|
729
|
+
exitCode: p.exitCode,
|
|
730
|
+
signal: p.signal
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/execution/engine/client/client-options.ts
|
|
735
|
+
import { basename } from "path";
|
|
736
|
+
import {
|
|
737
|
+
getLogger as getLogger5
|
|
738
|
+
} from "@zhushanwen/subagent-engine-sdk";
|
|
739
|
+
var logger6 = getLogger5("subagents");
|
|
740
|
+
function defaultEngineCmdlineMatcher(command) {
|
|
741
|
+
const base = basename(command);
|
|
742
|
+
return (cmdline) => {
|
|
743
|
+
if (cmdline.includes(command)) return true;
|
|
744
|
+
return cmdline.split(/\s+/).some((arg) => arg === command || arg.endsWith(`/${base}`));
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function warnOnManifestDiagnostics(engineId, diag, result) {
|
|
748
|
+
if (diag?.capabilities) {
|
|
749
|
+
const answered = JSON.stringify(result.capabilities ?? null);
|
|
750
|
+
const declared = JSON.stringify(diag.capabilities);
|
|
751
|
+
if (answered !== declared) {
|
|
752
|
+
logger6.warn(
|
|
753
|
+
`[engine-client:${engineId}] initialize capabilities differ from manifest (diagnostic only): manifest=${declared} answered=${answered}`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (diag && "models" in diag) {
|
|
758
|
+
const answered = JSON.stringify(result.models ?? null);
|
|
759
|
+
const declared = JSON.stringify(diag.models ?? null);
|
|
760
|
+
if (answered !== declared) {
|
|
761
|
+
logger6.warn(
|
|
762
|
+
`[engine-client:${engineId}] initialize models differ from manifest (diagnostic only): manifest=${declared} answered=${answered}`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/execution/engine/client/reaper.ts
|
|
769
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
770
|
+
import { getLogger as getLogger6 } from "@zhushanwen/subagent-engine-sdk";
|
|
771
|
+
var logger7 = getLogger6("subagents");
|
|
772
|
+
var KILL_GRACE_MS = 5e3;
|
|
773
|
+
function killProcessTree(pid, reason, opts) {
|
|
774
|
+
if (process.platform === "win32") {
|
|
775
|
+
const r = spawnSync2("taskkill", ["/PID", String(pid), "/T", "/F"], { timeout: 1e4 });
|
|
776
|
+
if (r.error || r.status !== 0) {
|
|
777
|
+
logger7.warn(
|
|
778
|
+
`[engine-client:${opts.engineId}] taskkill failed for pid ${pid} (${reason}): ${r.error?.message ?? `exit ${r.status}`}`
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const signalGroup = (signal) => {
|
|
784
|
+
try {
|
|
785
|
+
process.kill(-pid, signal);
|
|
786
|
+
return true;
|
|
787
|
+
} catch (err) {
|
|
788
|
+
logger7.debug(
|
|
789
|
+
`[engine-client:${opts.engineId}] process.kill(-${pid}, ${signal}) failed: ${err instanceof Error ? err.message : String(err)}`
|
|
790
|
+
);
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
if (!signalGroup("SIGTERM")) {
|
|
795
|
+
try {
|
|
796
|
+
process.kill(pid, "SIGTERM");
|
|
797
|
+
} catch (err) {
|
|
798
|
+
logger7.debug(
|
|
799
|
+
`[engine-client:${opts.engineId}] single SIGTERM to ${pid} failed (already dead = expected): ${err instanceof Error ? err.message : String(err)}`
|
|
800
|
+
);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const escalation = setTimeout(
|
|
805
|
+
() => {
|
|
806
|
+
if (!signalGroup("SIGKILL")) {
|
|
807
|
+
try {
|
|
808
|
+
process.kill(pid, "SIGKILL");
|
|
809
|
+
} catch (err) {
|
|
810
|
+
logger7.debug(
|
|
811
|
+
`[engine-client:${opts.engineId}] SIGKILL to ${pid} failed (already dead = expected): ${err instanceof Error ? err.message : String(err)}`
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
KILL_GRACE_MS
|
|
817
|
+
);
|
|
818
|
+
escalation.unref();
|
|
819
|
+
}
|
|
820
|
+
function waitForChildExit(child, timeoutMs) {
|
|
821
|
+
return new Promise((resolve4) => {
|
|
822
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
823
|
+
resolve4();
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const timer = setTimeout(() => resolve4(), timeoutMs);
|
|
827
|
+
child.once("exit", () => {
|
|
828
|
+
clearTimeout(timer);
|
|
829
|
+
resolve4();
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/execution/engine/client/engine-client.ts
|
|
835
|
+
var logger8 = getLogger7("subagents");
|
|
836
|
+
var DISPOSE_GRACE_MS = 3e3;
|
|
837
|
+
var SIGKILL_REAP_TIMEOUT_MS = 1e4;
|
|
838
|
+
var FRAME_ECHO_MAX_CHARS = 200;
|
|
839
|
+
function engineSpawnPlatformOptions() {
|
|
840
|
+
if (process.platform === "win32") {
|
|
841
|
+
return { detached: false, windowsHide: true };
|
|
842
|
+
}
|
|
843
|
+
return { detached: true, windowsHide: false };
|
|
844
|
+
}
|
|
845
|
+
function engineCrashedError(detail, stderrTail) {
|
|
846
|
+
const tail = stderrTail.length > 0 ? ` stderr tail: ${stderrTail}` : "";
|
|
847
|
+
return new EngineSdkError(
|
|
848
|
+
"engine_crashed",
|
|
849
|
+
`engine process exited unexpectedly: ${detail}.${tail}`,
|
|
850
|
+
"The engine will be rebuilt (up to 3 attempts with exponential backoff) on the next run. If it keeps crashing, inspect the engine package installation and its stderr diagnostics.",
|
|
851
|
+
{ stderrTail }
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
var EngineClient = class {
|
|
855
|
+
engineId;
|
|
856
|
+
/** spawnedChildren 状态镜像(host/childSpawned / childStateChanged 的落地面)。 */
|
|
857
|
+
mirror = new SpawnedChildrenMirror();
|
|
858
|
+
opts;
|
|
859
|
+
reverseRouterDeps;
|
|
860
|
+
state = "idle";
|
|
861
|
+
child;
|
|
862
|
+
nextRequestId = 1;
|
|
863
|
+
pending = /* @__PURE__ */ new Map();
|
|
864
|
+
runRoutes = /* @__PURE__ */ new Map();
|
|
865
|
+
/** [W3 v1.x] chat 轮次路由(recordId 键,D1-A——续聊轮无 runId);注册经
|
|
866
|
+
* RemoteEngine.registerChatRoundRoute,record 终态注销。 */
|
|
867
|
+
recordRoutes = /* @__PURE__ */ new Map();
|
|
868
|
+
stderrTail = "";
|
|
869
|
+
unavailableReason;
|
|
870
|
+
connectInFlight;
|
|
871
|
+
pidfileWritten;
|
|
872
|
+
pidfileSwept = false;
|
|
873
|
+
intentionalKill = false;
|
|
874
|
+
/** run 期 host/handleReady 的最近回填(崩溃时在途 run 的合成 handle 数据源)。 */
|
|
875
|
+
lastPartialHandle;
|
|
876
|
+
initializeDiagnostics;
|
|
877
|
+
stdoutBuffer = "";
|
|
878
|
+
constructor(opts) {
|
|
879
|
+
this.opts = opts;
|
|
880
|
+
this.engineId = opts.engineId;
|
|
881
|
+
this.reverseRouterDeps = {
|
|
882
|
+
engineId: opts.engineId,
|
|
883
|
+
uiRequestHandler: opts.uiRequestHandler,
|
|
884
|
+
permissionHandler: opts.permissionHandler,
|
|
885
|
+
log: opts.log,
|
|
886
|
+
runRoutes: this.runRoutes,
|
|
887
|
+
recordRoutes: this.recordRoutes,
|
|
888
|
+
mirror: this.mirror,
|
|
889
|
+
setPartialHandle: (partial) => {
|
|
890
|
+
this.lastPartialHandle = partial;
|
|
891
|
+
},
|
|
892
|
+
sendResponse: (id, result) => this.sendResponse(id, result),
|
|
893
|
+
failEngine: (reason) => this.killAll(reason),
|
|
894
|
+
isDisposed: () => this.disposed
|
|
895
|
+
};
|
|
896
|
+
this.mirror.onChange((event) => {
|
|
897
|
+
opts.onMirrorChanged?.(event);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
get currentState() {
|
|
901
|
+
return this.state;
|
|
902
|
+
}
|
|
903
|
+
/** disposed 判定(getter 形态——TS 不对跨 await 的字段窄化保持,字段直比会在长方法内误窄化)。 */
|
|
904
|
+
get disposed() {
|
|
905
|
+
return this.state === "disposed";
|
|
906
|
+
}
|
|
907
|
+
/** 引擎 CLI 进程 pid(未连接 = undefined)。 */
|
|
908
|
+
get enginePid() {
|
|
909
|
+
return this.child?.pid;
|
|
910
|
+
}
|
|
911
|
+
/** stderr 环形缓冲尾(engine_crashed 现场 / 诊断)。 */
|
|
912
|
+
get stderrTailText() {
|
|
913
|
+
return this.stderrTail;
|
|
914
|
+
}
|
|
915
|
+
/** 最近一次 host/handleReady 回填(RemoteEngine 崩溃合成 handle 用)。 */
|
|
916
|
+
getPartialHandle() {
|
|
917
|
+
return this.lastPartialHandle;
|
|
918
|
+
}
|
|
919
|
+
/** initialize 应答(诊断面;RemoteEngine 诊断留痕用)。 */
|
|
920
|
+
getInitializeDiagnostics() {
|
|
921
|
+
return this.initializeDiagnostics;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* 确保引擎 CLI 已连接(spawn + initialize 握手)。幂等;崩溃后按重建状态机重试:
|
|
925
|
+
* 初建失败 → 退避 1s/2s/4s 各重建一次(共 1 + CRASH_REBUILD_MAX_ATTEMPTS 次
|
|
926
|
+
* spawn 尝试),全败标记不可用直到宿主重启(unavailable 恒 throw)。
|
|
927
|
+
* 版本协商越界 → engine_protocol_mismatch + 直接标记不可用(不重试)。
|
|
928
|
+
*/
|
|
929
|
+
async ensureConnected() {
|
|
930
|
+
if (this.disposed) {
|
|
931
|
+
throw new EngineSdkError(
|
|
932
|
+
"engine_crashed",
|
|
933
|
+
`engine client for "${this.engineId}" is disposed`,
|
|
934
|
+
"Create a new engine client / RemoteEngine instance."
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
if (this.state === "unavailable") {
|
|
938
|
+
throw this.unavailableReason ?? new Error(`engine "${this.engineId}" is unavailable`);
|
|
939
|
+
}
|
|
940
|
+
if (this.state === "ready") return;
|
|
941
|
+
if (this.connectInFlight) return this.connectInFlight;
|
|
942
|
+
this.connectInFlight = this.connectWithRebuild();
|
|
943
|
+
try {
|
|
944
|
+
await this.connectInFlight;
|
|
945
|
+
} finally {
|
|
946
|
+
this.connectInFlight = void 0;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async connectWithRebuild() {
|
|
950
|
+
this.state = "connecting";
|
|
951
|
+
if (!this.pidfileSwept) {
|
|
952
|
+
this.pidfileSwept = true;
|
|
953
|
+
const sweep = sweepEnginePidfiles({
|
|
954
|
+
engineDataDir: this.opts.dataDir,
|
|
955
|
+
engineId: this.engineId,
|
|
956
|
+
currentHostPid: process.pid,
|
|
957
|
+
matchesEngineCmdline: this.opts.engineCmdlineMatcher ?? defaultEngineCmdlineMatcher(this.opts.command)
|
|
958
|
+
});
|
|
959
|
+
if (sweep.killed.length > 0) {
|
|
960
|
+
logger8.warn(
|
|
961
|
+
`[engine-client:${this.engineId}] startup sweep killed stale orphan engine pids: ${sweep.killed.join(",")}`
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
for (const removed of sweep.removed) {
|
|
965
|
+
logger8.debug(`[engine-client:${this.engineId}] swept stale pidfile ${removed.file}: ${removed.reason}`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
let attempt = 0;
|
|
969
|
+
while (attempt <= CRASH_REBUILD_MAX_ATTEMPTS) {
|
|
970
|
+
if (attempt > 0) {
|
|
971
|
+
const backoff = CRASH_REBUILD_BACKOFF_MS[attempt - 1];
|
|
972
|
+
logger8.warn(
|
|
973
|
+
`[engine-client:${this.engineId}] rebuild attempt ${attempt}/${CRASH_REBUILD_MAX_ATTEMPTS} after ${backoff}ms backoff`
|
|
974
|
+
);
|
|
975
|
+
await delay(backoff);
|
|
976
|
+
if (this.disposed) return;
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
await this.spawnAndInitialize();
|
|
980
|
+
this.state = "ready";
|
|
981
|
+
return;
|
|
982
|
+
} catch (err) {
|
|
983
|
+
this.teardownProcess("handshake failed");
|
|
984
|
+
if (err instanceof EngineSdkError && err.code === "engine_protocol_mismatch") {
|
|
985
|
+
this.markUnavailable(err);
|
|
986
|
+
throw err;
|
|
987
|
+
}
|
|
988
|
+
attempt += 1;
|
|
989
|
+
if (attempt > CRASH_REBUILD_MAX_ATTEMPTS) {
|
|
990
|
+
const failure = err instanceof Error ? err : new Error(String(err));
|
|
991
|
+
const unavailable = new EngineSdkError(
|
|
992
|
+
"engine_crashed",
|
|
993
|
+
`engine "${this.engineId}" failed to start after ${1 + CRASH_REBUILD_MAX_ATTEMPTS} attempts (initial + ${CRASH_REBUILD_MAX_ATTEMPTS} rebuilds): ${failure.message}`,
|
|
994
|
+
"Fix or reinstall the engine package, then restart the host. The engine stays unavailable until the next host start."
|
|
995
|
+
);
|
|
996
|
+
this.markUnavailable(unavailable);
|
|
997
|
+
throw unavailable;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
markUnavailable(reason) {
|
|
1003
|
+
this.state = "unavailable";
|
|
1004
|
+
this.unavailableReason = reason;
|
|
1005
|
+
logger8.error(`[engine-client:${this.engineId}] marked unavailable: ${reason.message}`);
|
|
1006
|
+
}
|
|
1007
|
+
/** spawn 引擎 CLI + initialize 握手 + 版本协商 + 诊断留痕。 */
|
|
1008
|
+
async spawnAndInitialize() {
|
|
1009
|
+
this.intentionalKill = false;
|
|
1010
|
+
const platformOpts = engineSpawnPlatformOptions();
|
|
1011
|
+
const baseEnv = this.opts.baseEnv ?? { ...process.env };
|
|
1012
|
+
const launch = await resolveEngineNodeLaunch({
|
|
1013
|
+
entryPath: this.opts.command,
|
|
1014
|
+
args: this.opts.args,
|
|
1015
|
+
hostKind: hostKindOf(this.opts.hostKind, baseEnv),
|
|
1016
|
+
env: baseEnv
|
|
1017
|
+
});
|
|
1018
|
+
const env = buildEngineChildEnv(baseEnv, {
|
|
1019
|
+
dataDir: this.opts.dataDir,
|
|
1020
|
+
engineNode: this.opts.engineNode ?? launch.command,
|
|
1021
|
+
electronRunAsNode: this.opts.electronRunAsNode ?? launch.electronRunAsNode,
|
|
1022
|
+
relay: this.opts.relay,
|
|
1023
|
+
identityEnv: this.opts.identityEnv,
|
|
1024
|
+
envPrefixes: this.opts.envPrefixes,
|
|
1025
|
+
processEnv: this.opts.processEnv
|
|
1026
|
+
});
|
|
1027
|
+
this.child = spawn(launch.command, launch.args, {
|
|
1028
|
+
cwd: this.opts.cwd,
|
|
1029
|
+
env,
|
|
1030
|
+
...platformOpts,
|
|
1031
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1032
|
+
});
|
|
1033
|
+
const child = this.child;
|
|
1034
|
+
child.on("error", (err) => {
|
|
1035
|
+
logger8.warn(`[engine-client:${this.engineId}] spawn error: ${err.message}`);
|
|
1036
|
+
this.appendStderrTail(`spawn error: ${err.message}
|
|
1037
|
+
`);
|
|
1038
|
+
});
|
|
1039
|
+
for (const pipe of ["stdin", "stdout", "stderr"]) {
|
|
1040
|
+
child[pipe]?.on("error", (err) => {
|
|
1041
|
+
logger8.debug(`[engine-client:${this.engineId}] ${pipe} pipe error (engine died concurrently = expected): ${err.message}`);
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
child.stdout?.setEncoding("utf-8");
|
|
1045
|
+
child.stdout?.on("data", (chunk) => this.onStdoutData(chunk));
|
|
1046
|
+
child.stderr?.setEncoding("utf-8");
|
|
1047
|
+
child.stderr?.on("data", (chunk) => this.appendStderrTail(chunk));
|
|
1048
|
+
child.on("exit", (code, signal) => this.onEngineExit(code, signal));
|
|
1049
|
+
if (child.pid !== void 0) {
|
|
1050
|
+
this.pidfileWritten = registerEnginePidfile({
|
|
1051
|
+
engineDataDir: this.opts.dataDir,
|
|
1052
|
+
engineId: this.engineId,
|
|
1053
|
+
hostKind: this.opts.hostKind,
|
|
1054
|
+
hostPid: process.pid,
|
|
1055
|
+
enginePid: child.pid
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
this.attachFrameReader();
|
|
1059
|
+
const initParams = {
|
|
1060
|
+
protocolVersion: ENGINE_PROTOCOL_VERSION,
|
|
1061
|
+
hostInfo: {
|
|
1062
|
+
name: this.opts.hostKind,
|
|
1063
|
+
version: this.opts.hostVersion ?? process.version,
|
|
1064
|
+
dataRoot: this.opts.dataDir
|
|
1065
|
+
},
|
|
1066
|
+
engineConfig: this.opts.engineConfig ?? {}
|
|
1067
|
+
};
|
|
1068
|
+
let result;
|
|
1069
|
+
try {
|
|
1070
|
+
result = await this.request("initialize", initParams, {
|
|
1071
|
+
timeoutMs: HANDSHAKE_TIMEOUT_MS
|
|
1072
|
+
});
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
if (err instanceof EngineSdkError && err.code === "engine_handshake_timeout") {
|
|
1075
|
+
throw new EngineSdkError(
|
|
1076
|
+
"engine_handshake_timeout",
|
|
1077
|
+
`engine "${this.engineId}" did not answer initialize within ${HANDSHAKE_TIMEOUT_MS}ms`,
|
|
1078
|
+
"Check that the engine package is executable and speaks engine-protocol v1. The engine is marked unavailable until it answers a handshake."
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
throw err;
|
|
1082
|
+
}
|
|
1083
|
+
if (!isProtocolVersionCompatible(result.protocolVersion)) {
|
|
1084
|
+
throw engineProtocolMismatchError(result.protocolVersion);
|
|
1085
|
+
}
|
|
1086
|
+
this.initializeDiagnostics = result;
|
|
1087
|
+
warnOnManifestDiagnostics(this.engineId, this.opts.manifestDiagnostics, result);
|
|
1088
|
+
}
|
|
1089
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1090
|
+
// stdout 行解析与帧路由
|
|
1091
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1092
|
+
attachFrameReader() {
|
|
1093
|
+
this.stdoutBuffer = "";
|
|
1094
|
+
}
|
|
1095
|
+
onStdoutData(chunk) {
|
|
1096
|
+
this.stdoutBuffer += chunk;
|
|
1097
|
+
let newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
1098
|
+
while (newlineIndex >= 0) {
|
|
1099
|
+
const line = this.stdoutBuffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
1100
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
1101
|
+
if (line.trim().length > 0) this.handleLine(line);
|
|
1102
|
+
newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
handleLine(line) {
|
|
1106
|
+
let frame;
|
|
1107
|
+
try {
|
|
1108
|
+
frame = JSON.parse(line);
|
|
1109
|
+
} catch {
|
|
1110
|
+
logger8.warn(
|
|
1111
|
+
`[engine-client:${this.engineId}] dropped non-NDJSON stdout line (protocol contract: stdout is NDJSON-only): ${truncate(line, FRAME_ECHO_MAX_CHARS)}`
|
|
1112
|
+
);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
if (isResponseFrame(frame)) {
|
|
1116
|
+
this.onResponseFrame(frame);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (isNotificationFrame(frame)) {
|
|
1120
|
+
this.onEventNotification(frame.params.runId, frame.params.event);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (isReverseRequestFrame(frame)) {
|
|
1124
|
+
this.onReverseRequest(frame);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
logger8.warn(
|
|
1128
|
+
`[engine-client:${this.engineId}] dropped unrecognized frame: ${truncate(line, FRAME_ECHO_MAX_CHARS)}`
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
onResponseFrame(frame) {
|
|
1132
|
+
if (typeof frame.id !== "number") return;
|
|
1133
|
+
const pending = this.pending.get(frame.id);
|
|
1134
|
+
if (!pending) return;
|
|
1135
|
+
this.pending.delete(frame.id);
|
|
1136
|
+
if (pending.timer !== void 0) clearTimeout(pending.timer);
|
|
1137
|
+
const error = frame.error;
|
|
1138
|
+
if (error !== void 0 && typeof error === "object") {
|
|
1139
|
+
pending.reject(
|
|
1140
|
+
new EngineSdkError(
|
|
1141
|
+
typeof error.code === "string" ? error.code : "engine_unknown",
|
|
1142
|
+
typeof error.message === "string" ? error.message : JSON.stringify(error),
|
|
1143
|
+
typeof error.recovery === "string" ? error.recovery : "Inspect the engine error and retry."
|
|
1144
|
+
)
|
|
1145
|
+
);
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
pending.resolve(frame.result);
|
|
1149
|
+
}
|
|
1150
|
+
onEventNotification(runId, event) {
|
|
1151
|
+
this.runRoutes.get(runId)?.onEvent?.(event);
|
|
1152
|
+
}
|
|
1153
|
+
/** 帧④入口(路由实现见 reverse-router.ts——超时域二分的 core 侧语义所在)。 */
|
|
1154
|
+
onReverseRequest(frame) {
|
|
1155
|
+
routeReverseRequest(this.reverseRouterDeps, frame);
|
|
1156
|
+
}
|
|
1157
|
+
// ── 正向请求 ────────────────────────────────────────────────────────────
|
|
1158
|
+
/**
|
|
1159
|
+
* 发帧①并等帧②。opts.timeoutMs = 数据面超时域(initialize 等);不传 = 任务级
|
|
1160
|
+
* 默认无超时(run,AGENTS.md 规则 19 / ADR-0047)。
|
|
1161
|
+
*/
|
|
1162
|
+
request(method, params, opts) {
|
|
1163
|
+
if (this.state === "disposed" || this.state === "unavailable") {
|
|
1164
|
+
return Promise.reject(
|
|
1165
|
+
this.unavailableReason ?? new EngineSdkError(
|
|
1166
|
+
"engine_crashed",
|
|
1167
|
+
`engine client for "${this.engineId}" is ${this.state}`,
|
|
1168
|
+
"Rebuild the connection via ensureConnected or recreate the client."
|
|
1169
|
+
)
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
const id = this.nextRequestId++;
|
|
1173
|
+
const frame = { id, method, params };
|
|
1174
|
+
return new Promise((resolve4, reject) => {
|
|
1175
|
+
const pending = { resolve: resolve4, reject, timer: void 0 };
|
|
1176
|
+
if (opts?.timeoutMs !== void 0) {
|
|
1177
|
+
pending.timer = setTimeout(() => {
|
|
1178
|
+
this.pending.delete(id);
|
|
1179
|
+
reject(
|
|
1180
|
+
new EngineSdkError(
|
|
1181
|
+
method === "initialize" ? "engine_handshake_timeout" : "engine_request_timeout",
|
|
1182
|
+
`engine "${this.engineId}" did not answer ${method} (id=${id}) within ${opts.timeoutMs}ms`,
|
|
1183
|
+
"Retry the operation; if it persists the engine process is faulted and will be rebuilt."
|
|
1184
|
+
)
|
|
1185
|
+
);
|
|
1186
|
+
}, opts.timeoutMs);
|
|
1187
|
+
}
|
|
1188
|
+
this.pending.set(id, pending);
|
|
1189
|
+
if (!this.writeFrame(frame)) {
|
|
1190
|
+
this.pending.delete(id);
|
|
1191
|
+
if (pending.timer !== void 0) clearTimeout(pending.timer);
|
|
1192
|
+
reject(
|
|
1193
|
+
engineCrashedError(
|
|
1194
|
+
`stdin write failed while sending ${method}`,
|
|
1195
|
+
this.stderrTail
|
|
1196
|
+
)
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
/** 注册 run 作用域反向通知路由;返回注销函数。 */
|
|
1202
|
+
registerRunRoute(runId, route) {
|
|
1203
|
+
this.runRoutes.set(runId, route);
|
|
1204
|
+
return () => {
|
|
1205
|
+
if (this.runRoutes.get(runId) === route) this.runRoutes.delete(runId);
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
/** [W3 v1.x] 注册 chat 轮次路由(recordId 键);分发见 reverse-router.ts
|
|
1209
|
+
* (与 runRoutes 同构,键空间互斥:runId=run 帧分配 / recordId=宿主铸造)。 */
|
|
1210
|
+
registerRecordRoute(recordId, route) {
|
|
1211
|
+
this.recordRoutes.set(recordId, route);
|
|
1212
|
+
return () => {
|
|
1213
|
+
if (this.recordRoutes.get(recordId) === route) this.recordRoutes.delete(recordId);
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
/** 健康检查(ADR-0047:静默 ≠ 卡死,不据此杀任务)。 */
|
|
1217
|
+
async ping() {
|
|
1218
|
+
await this.ensureConnected();
|
|
1219
|
+
await this.request("ping", {});
|
|
1220
|
+
}
|
|
1221
|
+
/** cancel 受理窗口 = CANCEL_SETTLE_GRACE_MS;终态收敛由调用方(RemoteEngine)等 run 应答。 */
|
|
1222
|
+
async cancelRun(runId, reason) {
|
|
1223
|
+
await this.request("cancel", { runId, reason }, { timeoutMs: CANCEL_SETTLE_GRACE_MS });
|
|
1224
|
+
}
|
|
1225
|
+
// ── 收割 / 停机 ──────────────────────────────────────────────────────────
|
|
1226
|
+
/**
|
|
1227
|
+
* 宿主收割入口(D8 killAllSpawnedChildren 落点):组杀引擎 CLI(POSIX 负 pid 组杀 /
|
|
1228
|
+
* Windows taskkill /T /F)+ 镜像整体置死 + pidfile 清理。在途请求以 engine_crashed
|
|
1229
|
+
* 失败。「零残留」断言范围 = 一代子进程 + 组内后代(引擎自身 detached 后代不覆盖,
|
|
1230
|
+
* impl-plan §7.2 R9-1 / 设计 §3.9 已接受代价)。
|
|
1231
|
+
*/
|
|
1232
|
+
async killAll(reason) {
|
|
1233
|
+
this.intentionalKill = true;
|
|
1234
|
+
const child = this.child;
|
|
1235
|
+
if (child !== void 0 && child.pid !== void 0 && child.exitCode === null) {
|
|
1236
|
+
killProcessTree(child.pid, reason, { engineId: this.engineId });
|
|
1237
|
+
await waitForChildExit(child, SIGKILL_REAP_TIMEOUT_MS);
|
|
1238
|
+
}
|
|
1239
|
+
this.teardownProcess(reason);
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* 协议 dispose:dispose 帧(3s 上界)→ 组杀兜底 → 清理。幂等。
|
|
1243
|
+
* dispose 后首个 run 的重建由新的连接状态机承担(EnginePort.dispose 契约)。
|
|
1244
|
+
*/
|
|
1245
|
+
async dispose() {
|
|
1246
|
+
if (this.state === "disposed") return;
|
|
1247
|
+
if (this.state === "ready" && this.child !== void 0 && this.child.exitCode === null) {
|
|
1248
|
+
try {
|
|
1249
|
+
await this.request("dispose", {}, { timeoutMs: DISPOSE_GRACE_MS });
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
logger8.debug(
|
|
1252
|
+
`[engine-client:${this.engineId}] dispose frame failed, falling back to kill chain: ${err instanceof Error ? err.message : String(err)}`
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
await this.killAll("dispose");
|
|
1257
|
+
this.state = "disposed";
|
|
1258
|
+
}
|
|
1259
|
+
/** 引擎进程 exit:镜像整体置死 + pidfile 清理 + 在途请求失败 + 状态回落。 */
|
|
1260
|
+
onEngineExit(code, signal) {
|
|
1261
|
+
const detail = signal !== null ? `signal ${signal}` : `exit code ${code}`;
|
|
1262
|
+
if (this.intentionalKill) {
|
|
1263
|
+
logger8.debug(`[engine-client:${this.engineId}] engine exited after intentional kill (${detail}) \u2014 expected`);
|
|
1264
|
+
} else {
|
|
1265
|
+
logger8.warn(`[engine-client:${this.engineId}] engine process exited unexpectedly (${detail})`);
|
|
1266
|
+
}
|
|
1267
|
+
this.teardownProcess(detail);
|
|
1268
|
+
this.state = "exited";
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* 进程死亡 / 失败后的统一清理:镜像整体置死(失效语义 2+3:killed=true 广播后
|
|
1272
|
+
* 清空)→ pidfile 删 → 在途请求 engine_crashed(附 stderr 尾)→ run 路由清空。
|
|
1273
|
+
*/
|
|
1274
|
+
teardownProcess(detail) {
|
|
1275
|
+
this.mirror.killAll();
|
|
1276
|
+
if (this.pidfileWritten !== void 0) {
|
|
1277
|
+
removePidfile(this.pidfileWritten);
|
|
1278
|
+
this.pidfileWritten = void 0;
|
|
1279
|
+
}
|
|
1280
|
+
const err = engineCrashedError(detail, this.stderrTail);
|
|
1281
|
+
for (const [, pending] of this.pending) {
|
|
1282
|
+
if (pending.timer !== void 0) clearTimeout(pending.timer);
|
|
1283
|
+
pending.reject(err);
|
|
1284
|
+
}
|
|
1285
|
+
this.pending.clear();
|
|
1286
|
+
this.runRoutes.clear();
|
|
1287
|
+
this.recordRoutes.clear();
|
|
1288
|
+
this.lastPartialHandle = void 0;
|
|
1289
|
+
if (this.child !== void 0) {
|
|
1290
|
+
const child = this.child;
|
|
1291
|
+
this.child = void 0;
|
|
1292
|
+
const alreadyDead = child.exitCode !== null || child.signalCode !== null;
|
|
1293
|
+
if (!alreadyDead && !this.intentionalKill && child.pid !== void 0) {
|
|
1294
|
+
killProcessTree(child.pid, detail, { engineId: this.engineId });
|
|
1295
|
+
}
|
|
1296
|
+
child.removeAllListeners();
|
|
1297
|
+
}
|
|
1298
|
+
if (this.state !== "unavailable" && this.state !== "disposed") {
|
|
1299
|
+
this.state = "exited";
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
// ── pidfile ──────────────────────────────────────────────────────────────
|
|
1303
|
+
// ── 帧写出口 ────────────────────────────────────────────────────────────
|
|
1304
|
+
writeFrame(frame) {
|
|
1305
|
+
const stdin = this.child?.stdin;
|
|
1306
|
+
if (stdin === void 0 || stdin === null || this.child === void 0 || this.child.exitCode !== null) {
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
try {
|
|
1310
|
+
stdin.write(`${JSON.stringify(frame)}
|
|
1311
|
+
`);
|
|
1312
|
+
return true;
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
logger8.debug(
|
|
1315
|
+
`[engine-client:${this.engineId}] stdin write failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1316
|
+
);
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
sendResponse(id, result) {
|
|
1321
|
+
return this.writeFrame({ id, result });
|
|
1322
|
+
}
|
|
1323
|
+
appendStderrTail(chunk) {
|
|
1324
|
+
this.stderrTail = (this.stderrTail + chunk).slice(-STDERR_TAIL_CHARS);
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
function delay(ms) {
|
|
1328
|
+
return new Promise((resolve4) => {
|
|
1329
|
+
setTimeout(resolve4, ms);
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
function truncate(text, max) {
|
|
1333
|
+
return text.length <= max ? text : `${text.slice(0, max)}...`;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/execution/engine/client/remote-engine.ts
|
|
1337
|
+
import {
|
|
1338
|
+
CANCEL_SETTLE_GRACE_MS as CANCEL_SETTLE_GRACE_MS2,
|
|
1339
|
+
EngineSdkError as EngineSdkError2
|
|
1340
|
+
} from "@zhushanwen/subagent-engine-sdk";
|
|
1341
|
+
|
|
1342
|
+
// src/execution/engine/common/capability-gate.ts
|
|
1343
|
+
import { engineConversationUnsupportedError } from "@zhushanwen/subagent-engine-sdk";
|
|
1344
|
+
|
|
1345
|
+
// src/execution/engine/common/errors.ts
|
|
1346
|
+
var EngineError = class extends Error {
|
|
1347
|
+
code;
|
|
1348
|
+
/** 恢复指引:指向具体下一步(命令 / 配置路径 / 替代方案),非安慰性文案。 */
|
|
1349
|
+
recovery;
|
|
1350
|
+
constructor(code, detail, recovery) {
|
|
1351
|
+
super(`${code}: ${detail}`);
|
|
1352
|
+
this.name = "EngineError";
|
|
1353
|
+
this.code = code;
|
|
1354
|
+
this.recovery = recovery;
|
|
1355
|
+
}
|
|
1356
|
+
/** 结构化投影(InteractResult.code/message 与 GUI 警告条共用形态)。 */
|
|
1357
|
+
toStructured() {
|
|
1358
|
+
return { code: this.code, message: this.message, recovery: this.recovery };
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
// src/execution/engine/common/capability-gate.ts
|
|
1363
|
+
var MANIFEST_RECOVERY_TAIL = "\u4FEE manifest capabilities / \u5347\u7EA7\u5F15\u64CE\u5305\uFF08\u82E5\u5F15\u64CE\u5B9E\u9645\u652F\u6301\u8BE5\u80FD\u529B\uFF09";
|
|
1364
|
+
function assertTaskShapeSupported(engineId, caps, task) {
|
|
1365
|
+
if (task.conversation === true && caps.conversation === "unsupported") {
|
|
1366
|
+
throw engineConversationUnsupportedError(engineId);
|
|
1367
|
+
}
|
|
1368
|
+
if (task.fork === true || task.forkFromSessionFile !== void 0) {
|
|
1369
|
+
if (caps.steer === "unsupported" && caps.conversation === "unsupported") {
|
|
1370
|
+
throw new EngineError(
|
|
1371
|
+
"engine_capability_unsupported",
|
|
1372
|
+
`engine '${engineId}' \u4E0D\u652F\u6301 fork${task.forkFromSessionFile !== void 0 ? "\uFF08fork-from \u540C\u4E3A\u7236 session \u4E0A\u4E0B\u6587\u7EE7\u627F\uFF09" : ""}\uFF08fork \u4F9D\u8D56\u7236 session \u4E0A\u4E0B\u6587\u7EE7\u627F\uFF0Ccapabilities.steer = '${caps.steer}' / conversation = '${caps.conversation}'\u2014\u2014\u5F15\u64CE\u65E0\u7236 session \u5206\u53C9\u901A\u9053\uFF09`,
|
|
1373
|
+
`\u628A\u6240\u9700\u7236\u4E0A\u4E0B\u6587\u5199\u8FDB task \u6B63\u6587\u540E\u4E0D\u4F20 fork\uFF0C\u6216 ${MANIFEST_RECOVERY_TAIL}`
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
if (task.maxTurns !== void 0 && caps.maxTurns === false) {
|
|
1378
|
+
throw new EngineError(
|
|
1379
|
+
"engine_capability_unsupported",
|
|
1380
|
+
`engine '${engineId}' \u4E0D\u652F\u6301 maxTurns\uFF08capabilities.maxTurns = false\uFF0C\u8F6E\u6570\u4E0A\u9650\u4F9D\u8D56 turn_end \u4E8B\u4EF6\u6D41\uFF0C\u672C\u5F15\u64CE\u65E0\u6B64\u8BED\u4E49\u2014\u2014\u9759\u9ED8\u4E22\u5F03\u4F1A\u9020\u6210\u300C\u4F20\u4E86\u4E0A\u9650\u5374\u5931\u63A7\u300D\u7684\u5047\u8C61\uFF09`,
|
|
1381
|
+
`\u53BB\u6389 maxTurns \u53C2\u6570\u91CD\u6D3E\uFF0C\u6216 ${MANIFEST_RECOVERY_TAIL}`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
if ((task.worktree === true || typeof task.worktree === "object") && caps.sandbox === "none") {
|
|
1385
|
+
throw new EngineError(
|
|
1386
|
+
"engine_capability_unsupported",
|
|
1387
|
+
`engine '${engineId}' \u4E0D\u652F\u6301 worktree \u9694\u79BB\uFF08capabilities.sandbox = 'none'\uFF0C\u5F15\u64CE\u672A\u63A5\u6587\u4EF6\u7CFB\u7EDF\u9694\u79BB\u5C42\uFF09`,
|
|
1388
|
+
`\u53BB\u6389 worktree \u53C2\u6570\uFF08\u5728 parent cwd \u6267\u884C\uFF09\uFF0C\u6216 ${MANIFEST_RECOVERY_TAIL}`
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
function assertGateCapabilitiesMatched(engineId, manifestCaps, answeredCaps) {
|
|
1393
|
+
if (manifestCaps.conversation !== "unsupported" && answeredCaps.conversation === "unsupported") {
|
|
1394
|
+
throw gateMismatchError(engineId, "conversation", manifestCaps.conversation, answeredCaps.conversation);
|
|
1395
|
+
}
|
|
1396
|
+
const manifestForkCapable = manifestCaps.steer !== "unsupported" || manifestCaps.conversation !== "unsupported";
|
|
1397
|
+
const answeredForkCapable = answeredCaps.steer !== "unsupported" || answeredCaps.conversation !== "unsupported";
|
|
1398
|
+
if (manifestForkCapable && !answeredForkCapable) {
|
|
1399
|
+
throw gateMismatchError(
|
|
1400
|
+
engineId,
|
|
1401
|
+
"fork\uFF08steer/conversation \u901A\u9053\u65CF\uFF09",
|
|
1402
|
+
`steer=${manifestCaps.steer}/conversation=${manifestCaps.conversation}`,
|
|
1403
|
+
`steer=${answeredCaps.steer}/conversation=${answeredCaps.conversation}`
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
if (manifestCaps.maxTurns === true && answeredCaps.maxTurns === false) {
|
|
1407
|
+
throw gateMismatchError(engineId, "maxTurns", String(manifestCaps.maxTurns), String(answeredCaps.maxTurns));
|
|
1408
|
+
}
|
|
1409
|
+
if (manifestCaps.sandbox !== "none" && answeredCaps.sandbox === "none") {
|
|
1410
|
+
throw gateMismatchError(engineId, "sandbox\uFF08worktree\uFF09", manifestCaps.sandbox, answeredCaps.sandbox);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
function gateMismatchError(engineId, cap, declared, answered) {
|
|
1414
|
+
return new EngineError(
|
|
1415
|
+
"engine_capability_mismatch",
|
|
1416
|
+
`engine '${engineId}' manifest \u58F0\u660E\u7684\u80FD\u529B\u4F4D '${cap}'\uFF08${declared}\uFF09\u4E0E\u5F15\u64CE initialize \u5E94\u7B54\u4E0D\u7B26\uFF08${answered}\uFF09\u2014\u2014manifest \u591A\u58F0\u660E\uFF0C\u4EFB\u52A1\u6309 gate \u5224\u636E\u653E\u884C\u540E\u5F15\u64CE\u65E0\u6CD5\u5151\u73B0`,
|
|
1417
|
+
`\u4FEE manifest capabilities\uFF08\u4E0E\u5F15\u64CE\u5B9E\u9645\u80FD\u529B\u5BF9\u9F50\uFF09\u6216\u5347\u7EA7\u5F15\u64CE\u5305\uFF1B\u82E5\u4EFB\u52A1\u4E0D\u518D\u9700\u8981\u8BE5\u80FD\u529B\uFF0C\u8C03\u6574\u4EFB\u52A1\u53C2\u6570\u540E\u91CD\u6D3E`
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// src/execution/engine/client/remote-engine.ts
|
|
1422
|
+
function matchCatalogEntry(entries, ref) {
|
|
1423
|
+
return entries.find(
|
|
1424
|
+
(entry) => entry.id === ref || entry.canonicalRef === ref || entry.aliases?.includes(ref) === true
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
var RemoteEngine = class {
|
|
1428
|
+
id;
|
|
1429
|
+
opts;
|
|
1430
|
+
constructor(opts) {
|
|
1431
|
+
this.opts = opts;
|
|
1432
|
+
this.id = opts.engineId;
|
|
1433
|
+
if (opts.manifest.modelCatalog === void 0 || opts.manifest.modelCatalog === null) {
|
|
1434
|
+
this.validateModel = void 0;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
/** 直读 manifest 注册期快照(无缓存——每次调用同值,快照不可变)。 */
|
|
1438
|
+
capabilities() {
|
|
1439
|
+
return this.opts.manifest.capabilities;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* listModels 三态映射(必写死):
|
|
1443
|
+
* 省略 modelCatalog / models null → 返回 null(buildCoreAlignedHint 语义);
|
|
1444
|
+
* 显式 `models: []` → 返回 [](buildEmptyModelsHint);
|
|
1445
|
+
* 数组 → 原样返回。
|
|
1446
|
+
*/
|
|
1447
|
+
listModels() {
|
|
1448
|
+
const catalog = this.opts.manifest.modelCatalog;
|
|
1449
|
+
if (catalog === void 0 || catalog === null) return null;
|
|
1450
|
+
return catalog.models;
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* validateModel 同源 manifest 判定(成员在 catalog 省略时已被构造器摘除):
|
|
1454
|
+
* 命中(id/canonicalRef/alias 全等)→ {canonicalRef: entry.canonicalRef ?? entry.id};
|
|
1455
|
+
* 未命中且 dynamic:false → throw engine_model_unknown(同步拒,record 不创建);
|
|
1456
|
+
* 未命中且 dynamic:true → 放行,返回原样 ref(运行期以引擎为权威
|
|
1457
|
+
* engine_model_mismatch;无斜杠 ref 的 core 侧拆分 = 契约变更④,归 W3)。
|
|
1458
|
+
* modelRef undefined(查引擎缺省)对静态目录恒属未命中:dynamic:true 放行回空串
|
|
1459
|
+
* (缺省模型无静态 canonical 形态,运行期自证);dynamic:false 同步拒。
|
|
1460
|
+
*/
|
|
1461
|
+
validateModel(modelRef) {
|
|
1462
|
+
const catalog = this.opts.manifest.modelCatalog;
|
|
1463
|
+
if (!catalog || modelRef === void 0 || modelRef.trim() === "") {
|
|
1464
|
+
if (catalog?.dynamic === false) {
|
|
1465
|
+
throw new EngineSdkError2(
|
|
1466
|
+
"engine_model_unknown",
|
|
1467
|
+
`engine '${this.id}' declares a static model catalog (dynamic:false) and has no engine-default entry; an explicit \`model\` ref from the catalog is required.`,
|
|
1468
|
+
"Retry with an exact model id from the engine's model list (engine listModels), or declare the engine-default entry in the manifest modelCatalog."
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
return { canonicalRef: modelRef ?? "" };
|
|
1472
|
+
}
|
|
1473
|
+
const entry = matchCatalogEntry(catalog.models, modelRef);
|
|
1474
|
+
if (entry !== void 0) {
|
|
1475
|
+
return { canonicalRef: entry.canonicalRef ?? entry.id };
|
|
1476
|
+
}
|
|
1477
|
+
if (catalog.dynamic === false) {
|
|
1478
|
+
throw new EngineSdkError2(
|
|
1479
|
+
"engine_model_unknown",
|
|
1480
|
+
`model '${modelRef}' is not in engine '${this.id}' static model catalog (dynamic:false)`,
|
|
1481
|
+
"Retry with an exact model id from the engine's model list (engine listModels), or fix the manifest modelCatalog / upgrade the engine package."
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
return { canonicalRef: modelRef };
|
|
1485
|
+
}
|
|
1486
|
+
async probe(probeOpts) {
|
|
1487
|
+
await this.opts.client.ensureConnected();
|
|
1488
|
+
const report = await this.opts.client.request("probe", {
|
|
1489
|
+
force: probeOpts?.force ?? false
|
|
1490
|
+
});
|
|
1491
|
+
return report;
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* 协议 run 映射。task 收窄为引擎面子集(model/schemaEnv/cwd/engineFallback 改挂
|
|
1495
|
+
* run.params.ctx,协议层单列——SDK AgentCallOpts 注释的字段裁决);事件经 run 作用域
|
|
1496
|
+
* 路由分发(event 通知 / streamDelta / poolResolved / handleReady);abort → cancel
|
|
1497
|
+
* 帧 + 3s 收敛窗口(超时杀链)。运行中失败不 reject——合成 error outcome + 正常
|
|
1498
|
+
* handle 返回(EnginePort 契约:record 必须收尾);run 帧发出前的失败(连接/握手)
|
|
1499
|
+
* reject。childSpawned/childStateChanged 镜像归 EngineClient(协议形态无
|
|
1500
|
+
* ChildProcess 实例,ctx.onChildSpawned 不调用——W6 起生命周期谓词读镜像)。
|
|
1501
|
+
*/
|
|
1502
|
+
async run(task, ctx) {
|
|
1503
|
+
await this.opts.client.ensureConnected();
|
|
1504
|
+
const answeredCaps = this.opts.client.getInitializeDiagnostics()?.capabilities;
|
|
1505
|
+
if (answeredCaps !== void 0) {
|
|
1506
|
+
assertGateCapabilitiesMatched(this.id, this.opts.manifest.capabilities, answeredCaps);
|
|
1507
|
+
}
|
|
1508
|
+
const runId = ctx.taskId;
|
|
1509
|
+
const runParams = buildRunParams(task, ctx, runId);
|
|
1510
|
+
const unregister = this.opts.client.registerRunRoute(runId, buildRunRouteHandlers(ctx));
|
|
1511
|
+
const abort = wireAbortSignal(this.opts.client, runId, ctx);
|
|
1512
|
+
try {
|
|
1513
|
+
const result = await this.opts.client.request("run", runParams);
|
|
1514
|
+
return { handle: { data: result.handle }, outcome: result.outcome };
|
|
1515
|
+
} catch (err) {
|
|
1516
|
+
if (abort.isCancelSent()) {
|
|
1517
|
+
return {
|
|
1518
|
+
handle: { data: this.synthesizeHandle(ctx.poolKey) },
|
|
1519
|
+
outcome: abortedRunOutcome(this.id, runId, err)
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
if (isTransientRunFailure(err)) {
|
|
1523
|
+
return {
|
|
1524
|
+
handle: { data: this.synthesizeHandle(ctx.poolKey) },
|
|
1525
|
+
outcome: transientRunOutcome(this.id, err)
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
throw err;
|
|
1529
|
+
} finally {
|
|
1530
|
+
abort.dispose();
|
|
1531
|
+
unregister();
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
async interact(handle, action) {
|
|
1535
|
+
await this.opts.client.ensureConnected();
|
|
1536
|
+
const result = await this.opts.client.request("interact", {
|
|
1537
|
+
handle: handle.data,
|
|
1538
|
+
action
|
|
1539
|
+
});
|
|
1540
|
+
return result;
|
|
1541
|
+
}
|
|
1542
|
+
/**
|
|
1543
|
+
* [W3 v1.x] chat 轮次反向通道路由注册(recordId 键,EnginePort 可选面实现):
|
|
1544
|
+
* interact 续聊轮的 streamDelta / roundLifecycle 分发目标。薄委托 EngineClient
|
|
1545
|
+
* 的 recordRoutes(键分发见 reverse-router.ts)。
|
|
1546
|
+
*/
|
|
1547
|
+
registerChatRoundRoute(recordId, route) {
|
|
1548
|
+
return this.opts.client.registerRecordRoute(recordId, route);
|
|
1549
|
+
}
|
|
1550
|
+
/** 协议 read(dataDir 必填——引擎数据根,构造注入)。 */
|
|
1551
|
+
async read(handle) {
|
|
1552
|
+
await this.opts.client.ensureConnected();
|
|
1553
|
+
const view = await this.opts.client.request("read", {
|
|
1554
|
+
handle: handle.data,
|
|
1555
|
+
dataDir: this.opts.dataDir
|
|
1556
|
+
});
|
|
1557
|
+
return view;
|
|
1558
|
+
}
|
|
1559
|
+
/** 协议 dispose(幂等)→ EngineClient 停机清理(镜像置死 + pidfile + 组杀兜底)。 */
|
|
1560
|
+
async dispose() {
|
|
1561
|
+
await this.opts.client.dispose();
|
|
1562
|
+
}
|
|
1563
|
+
/** 运行中失败的合成 handle:handleReady 回填优先,缺省回退请求期 ctx。 */
|
|
1564
|
+
synthesizeHandle(poolKey) {
|
|
1565
|
+
const partial = this.opts.client.getPartialHandle();
|
|
1566
|
+
const diag = this.opts.client.getInitializeDiagnostics();
|
|
1567
|
+
return {
|
|
1568
|
+
v: 1,
|
|
1569
|
+
engineId: this.id,
|
|
1570
|
+
sessionRef: partial?.sessionRef ?? {},
|
|
1571
|
+
poolKey: partial?.poolKey ?? poolKey,
|
|
1572
|
+
engineVersion: diag?.engineVersion,
|
|
1573
|
+
adapterVersion: diag?.adapterVersion ?? `remote-engine/${this.id}`
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
function isTransientRunFailure(err) {
|
|
1578
|
+
if (!(err instanceof EngineSdkError2)) return false;
|
|
1579
|
+
return err.code === "engine_crashed" || err.code === "engine_request_timeout" || err.code === "engine_handshake_timeout";
|
|
1580
|
+
}
|
|
1581
|
+
function buildRunParams(task, ctx, runId) {
|
|
1582
|
+
const ctxModelRef = ctx.ctxModel ? `${ctx.ctxModel.provider}/${ctx.ctxModel.id}` : void 0;
|
|
1583
|
+
return {
|
|
1584
|
+
runId,
|
|
1585
|
+
task: toSdkTaskSubset(task),
|
|
1586
|
+
ctx: {
|
|
1587
|
+
poolKey: ctx.poolKey,
|
|
1588
|
+
cwd: task.cwd ?? process.cwd(),
|
|
1589
|
+
model: task.model,
|
|
1590
|
+
schemaEnv: ctx.schemaEnv ?? task.schemaEnv,
|
|
1591
|
+
ctxModel: ctxModelRef,
|
|
1592
|
+
engineFallback: ctx.engineFallback,
|
|
1593
|
+
streamMode: ctx.stream !== void 0 ? "stream" : void 0
|
|
1594
|
+
},
|
|
1595
|
+
...ctx.chat !== void 0 ? { chat: ctx.chat } : {}
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
function buildRunRouteHandlers(ctx) {
|
|
1599
|
+
return {
|
|
1600
|
+
onEvent: (event) => ctx.onEvent?.(event),
|
|
1601
|
+
onStreamDelta: (delta) => ctx.stream?.onDelta(delta),
|
|
1602
|
+
onPoolResolved: (poolKey) => ctx.onPoolResolved?.(poolKey),
|
|
1603
|
+
onHandleReady: (partial) => ctx.onHandleReady?.(partial),
|
|
1604
|
+
// [W3 v1.x] 首轮(run 会话形态)轮次生命周期帧(runId 键)→ 宿主消费口。
|
|
1605
|
+
onRoundLifecycle: (phase) => ctx.onRoundLifecycle?.(phase)
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
function wireAbortSignal(client, runId, ctx) {
|
|
1609
|
+
let cancelSent = false;
|
|
1610
|
+
let settled = false;
|
|
1611
|
+
let settleTimer;
|
|
1612
|
+
const onAbort = () => {
|
|
1613
|
+
if (cancelSent || settled) return;
|
|
1614
|
+
cancelSent = true;
|
|
1615
|
+
void client.cancelRun(runId, "abort").catch(() => {
|
|
1616
|
+
});
|
|
1617
|
+
settleTimer = setTimeout(() => {
|
|
1618
|
+
if (!settled) {
|
|
1619
|
+
void client.killAll(`cancel did not settle within grace for run ${runId}`);
|
|
1620
|
+
}
|
|
1621
|
+
}, CANCEL_SETTLE_GRACE_MS2);
|
|
1622
|
+
};
|
|
1623
|
+
if (ctx.signal !== void 0) {
|
|
1624
|
+
if (ctx.signal.aborted) onAbort();
|
|
1625
|
+
else ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
1626
|
+
}
|
|
1627
|
+
return {
|
|
1628
|
+
isCancelSent: () => cancelSent,
|
|
1629
|
+
dispose: () => {
|
|
1630
|
+
settled = true;
|
|
1631
|
+
if (settleTimer !== void 0) clearTimeout(settleTimer);
|
|
1632
|
+
if (ctx.signal !== void 0) ctx.signal.removeEventListener("abort", onAbort);
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
function abortedRunOutcome(engineId, runId, err) {
|
|
1637
|
+
return {
|
|
1638
|
+
content: "",
|
|
1639
|
+
error: `engine_run_failed: run ${runId} aborted before terminal answer${err instanceof Error ? ` (${err.message})` : ""}`,
|
|
1640
|
+
exitCode: null,
|
|
1641
|
+
engineId
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
function transientRunOutcome(engineId, err) {
|
|
1645
|
+
return {
|
|
1646
|
+
content: "",
|
|
1647
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1648
|
+
exitCode: null,
|
|
1649
|
+
engineId
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
function toSdkTaskSubset(task) {
|
|
1653
|
+
return {
|
|
1654
|
+
prompt: task.prompt,
|
|
1655
|
+
schema: task.schema,
|
|
1656
|
+
thinkingLevel: task.thinkingLevel,
|
|
1657
|
+
scene: task.scene,
|
|
1658
|
+
maxTurns: task.maxTurns,
|
|
1659
|
+
graceTurns: task.graceTurns,
|
|
1660
|
+
skill: task.skill,
|
|
1661
|
+
skillPath: task.skillPath,
|
|
1662
|
+
description: task.description,
|
|
1663
|
+
agent: task.agent,
|
|
1664
|
+
appendSystemPrompt: task.appendSystemPrompt,
|
|
1665
|
+
fork: task.fork,
|
|
1666
|
+
// fork-from 源(fork-from 轮次 --fork 的协议载体,W3 断链修复):host-task-spec
|
|
1667
|
+
// 已把 ExecuteOptions.forkFromSessionFile 改名为 task.forkSource,此处同名透传。
|
|
1668
|
+
// 缺省 undefined 不落 wire(JSON 序列化丢弃,与相邻可选字段同语义)。
|
|
1669
|
+
forkSource: task.forkSource,
|
|
1670
|
+
worktree: task.worktree,
|
|
1671
|
+
conversation: task.conversation,
|
|
1672
|
+
idleTimeoutMs: task.idleTimeoutMs,
|
|
1673
|
+
denyTools: task.denyTools,
|
|
1674
|
+
permissionMode: task.permissionMode
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// src/execution/engine/host/host-ui-endpoint.ts
|
|
1679
|
+
var ENDPOINT_SLOT_KEY = /* @__PURE__ */ Symbol.for(
|
|
1680
|
+
"@zhushanwen/pi-subagent-workflow.hostUiRequestEndpoint"
|
|
1681
|
+
);
|
|
1682
|
+
function endpointSlot() {
|
|
1683
|
+
let slot = Reflect.get(globalThis, ENDPOINT_SLOT_KEY);
|
|
1684
|
+
if (!slot) {
|
|
1685
|
+
slot = { handler: void 0 };
|
|
1686
|
+
Reflect.set(globalThis, ENDPOINT_SLOT_KEY, slot);
|
|
1687
|
+
}
|
|
1688
|
+
return slot;
|
|
1689
|
+
}
|
|
1690
|
+
function setHostUiRequestEndpoint(handler) {
|
|
1691
|
+
endpointSlot().handler = handler;
|
|
1692
|
+
}
|
|
1693
|
+
function getHostUiRequestEndpoint() {
|
|
1694
|
+
return endpointSlot().handler;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// src/execution/engine/engine-inspect-package.ts
|
|
1698
|
+
var logger9 = getLogger("subagents");
|
|
1699
|
+
function readEnginePackageJson(pkgDir) {
|
|
1700
|
+
let raw;
|
|
1701
|
+
try {
|
|
1702
|
+
raw = fs.readFileSync(path2.join(pkgDir, "package.json"), "utf8");
|
|
1703
|
+
} catch (err) {
|
|
1704
|
+
return { status: "skip", reason: `package.json unreadable: ${errorMessage(err)}` };
|
|
1705
|
+
}
|
|
1706
|
+
let pkg;
|
|
1707
|
+
try {
|
|
1708
|
+
pkg = JSON.parse(raw);
|
|
1709
|
+
} catch (err) {
|
|
1710
|
+
return { status: "skip", reason: `package.json is not valid JSON: ${errorMessage(err)}` };
|
|
1711
|
+
}
|
|
1712
|
+
if (typeof pkg !== "object" || pkg === null) {
|
|
1713
|
+
return { status: "skip", reason: "package.json is not an object" };
|
|
1714
|
+
}
|
|
1715
|
+
return { pkg };
|
|
1716
|
+
}
|
|
1717
|
+
function extractEngineManifest(pkg) {
|
|
1718
|
+
const agentNs = pkg["xyz-agent"];
|
|
1719
|
+
if (typeof agentNs !== "object" || agentNs === null) {
|
|
1720
|
+
return { skipReason: "no xyz-agent.subagentEngine manifest (not an engine package)" };
|
|
1721
|
+
}
|
|
1722
|
+
const manifest = agentNs["subagentEngine"];
|
|
1723
|
+
if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
|
|
1724
|
+
return { skipReason: "xyz-agent.subagentEngine is not an object" };
|
|
1725
|
+
}
|
|
1726
|
+
return { manifest };
|
|
1727
|
+
}
|
|
1728
|
+
function validateRequiredManifestFields(m) {
|
|
1729
|
+
const id = m["id"];
|
|
1730
|
+
if (typeof id !== "string" || id.trim() === "") {
|
|
1731
|
+
return { status: "skip", reason: "manifest id is required (non-empty string)" };
|
|
1732
|
+
}
|
|
1733
|
+
const manifestBin = m["bin"];
|
|
1734
|
+
if (typeof manifestBin !== "string" || manifestBin.trim() === "") {
|
|
1735
|
+
return { status: "skip", reason: `engine '${id}': manifest bin is required (non-empty string)` };
|
|
1736
|
+
}
|
|
1737
|
+
const protocol = m["protocol"];
|
|
1738
|
+
if (typeof protocol !== "number" || !Number.isInteger(protocol)) {
|
|
1739
|
+
return {
|
|
1740
|
+
status: "skip",
|
|
1741
|
+
reason: `engine '${id}': manifest protocol is required (integer; got ${describeValue(protocol)})`
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
if (!isProtocolVersionCompatible2(protocol)) {
|
|
1745
|
+
return {
|
|
1746
|
+
status: "unusable",
|
|
1747
|
+
id,
|
|
1748
|
+
reason: `engine '${id}': manifest protocol ${protocol} is not compatible (core supports >=1 <2) \u2014 upgrade the engine package or the host core`
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
return { id, manifestBin, protocol };
|
|
1752
|
+
}
|
|
1753
|
+
function resolveVerifiedEngineBin(pkgDir, pkg, manifestBin, id) {
|
|
1754
|
+
const binPath = resolveManifestBin(pkgDir, pkg, manifestBin);
|
|
1755
|
+
if (binPath === void 0) {
|
|
1756
|
+
return {
|
|
1757
|
+
status: "skip",
|
|
1758
|
+
reason: `engine '${id}': manifest bin '${manifestBin}' does not resolve via package.json bin`
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
if (!canExecute(binPath)) {
|
|
1762
|
+
return {
|
|
1763
|
+
status: "unusable",
|
|
1764
|
+
id,
|
|
1765
|
+
reason: `engine '${id}': bin not found or not executable: ${binPath}`
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
return { binPath };
|
|
1769
|
+
}
|
|
1770
|
+
function parseOptionalDisplayFields(m, id) {
|
|
1771
|
+
let displayName;
|
|
1772
|
+
const rawDisplayName = m["displayName"];
|
|
1773
|
+
if (rawDisplayName !== void 0) {
|
|
1774
|
+
if (typeof rawDisplayName === "string" && rawDisplayName.trim() !== "") {
|
|
1775
|
+
displayName = rawDisplayName;
|
|
1776
|
+
} else {
|
|
1777
|
+
logger9.warn(`[engine-discovery] engine '${id}': displayName must be a non-empty string \u2014 ignoring`);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const rawDescription = m["description"];
|
|
1781
|
+
if (rawDescription !== void 0 && typeof rawDescription !== "string") {
|
|
1782
|
+
logger9.warn(`[engine-discovery] engine '${id}': description must be a string \u2014 ignoring`);
|
|
1783
|
+
}
|
|
1784
|
+
return displayName;
|
|
1785
|
+
}
|
|
1786
|
+
function buildManifestCliDescriptor(params) {
|
|
1787
|
+
const { id, binPath, hostKind, env, caps, envPrefixes, modelCatalog, displayName, manifestSnapshot } = params;
|
|
1788
|
+
return {
|
|
1789
|
+
kind: "cli",
|
|
1790
|
+
command: binPath,
|
|
1791
|
+
args: [],
|
|
1792
|
+
capabilities: caps,
|
|
1793
|
+
portFactory: () => {
|
|
1794
|
+
const dataDir = getEngineDataDir(env);
|
|
1795
|
+
const client = new EngineClient({
|
|
1796
|
+
engineId: id,
|
|
1797
|
+
command: binPath,
|
|
1798
|
+
args: [],
|
|
1799
|
+
hostKind,
|
|
1800
|
+
dataDir,
|
|
1801
|
+
envPrefixes,
|
|
1802
|
+
// [W6 R3 MF-A] host/askUser 应答端:壳侧登记处在 portFactory 惰性执行期取值
|
|
1803
|
+
//(晚于 session_start 注册,未注册 → undefined → 引擎收 {unsupported:true})。
|
|
1804
|
+
uiRequestHandler: getHostUiRequestEndpoint(),
|
|
1805
|
+
manifestDiagnostics: {
|
|
1806
|
+
capabilities: caps,
|
|
1807
|
+
models: modelCatalog === void 0 || modelCatalog === null ? null : modelCatalog.models
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
return new RemoteEngine({
|
|
1811
|
+
engineId: id,
|
|
1812
|
+
client,
|
|
1813
|
+
manifest: manifestSnapshot,
|
|
1814
|
+
dataDir,
|
|
1815
|
+
hostKind
|
|
1816
|
+
});
|
|
1817
|
+
},
|
|
1818
|
+
manifest: {
|
|
1819
|
+
...modelCatalog !== void 0 ? { modelCatalog } : {},
|
|
1820
|
+
...displayName !== void 0 ? { displayName } : {}
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
function inspectEnginePackage(pkgDir, source, hostKind, env) {
|
|
1825
|
+
const pkgStep = readEnginePackageJson(pkgDir);
|
|
1826
|
+
if (!("pkg" in pkgStep)) return pkgStep;
|
|
1827
|
+
const manifestStep = extractEngineManifest(pkgStep.pkg);
|
|
1828
|
+
if (!("manifest" in manifestStep)) return { status: "skip", reason: manifestStep.skipReason };
|
|
1829
|
+
const m = manifestStep.manifest;
|
|
1830
|
+
const fields = validateRequiredManifestFields(m);
|
|
1831
|
+
if ("reason" in fields) return fields;
|
|
1832
|
+
const { id, manifestBin } = fields;
|
|
1833
|
+
const binStep = resolveVerifiedEngineBin(pkgDir, pkgStep.pkg, manifestBin, id);
|
|
1834
|
+
if (!("binPath" in binStep)) return binStep;
|
|
1835
|
+
const caps = parseCapabilities(id, m["capabilities"]);
|
|
1836
|
+
const envPrefixes = parseEnvPrefixes(id, m["envPrefixes"]);
|
|
1837
|
+
const modelCatalog = parseModelCatalog(id, m["modelCatalog"]);
|
|
1838
|
+
const displayName = parseOptionalDisplayFields(m, id);
|
|
1839
|
+
const manifestSnapshot = {
|
|
1840
|
+
capabilities: caps,
|
|
1841
|
+
...modelCatalog !== void 0 ? { modelCatalog } : {}
|
|
1842
|
+
};
|
|
1843
|
+
const descriptor = buildManifestCliDescriptor({
|
|
1844
|
+
id,
|
|
1845
|
+
binPath: binStep.binPath,
|
|
1846
|
+
hostKind,
|
|
1847
|
+
env,
|
|
1848
|
+
caps,
|
|
1849
|
+
envPrefixes,
|
|
1850
|
+
modelCatalog,
|
|
1851
|
+
displayName,
|
|
1852
|
+
manifestSnapshot
|
|
1853
|
+
});
|
|
1854
|
+
return { status: "ok", entry: { id, source, descriptor } };
|
|
1855
|
+
}
|
|
1856
|
+
function canExecute(p) {
|
|
1857
|
+
try {
|
|
1858
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
1859
|
+
return true;
|
|
1860
|
+
} catch {
|
|
1861
|
+
return false;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
function errorMessage(err) {
|
|
1865
|
+
return err instanceof Error ? err.message : String(err);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// src/execution/engine/engine-discovery-roots.ts
|
|
1869
|
+
import * as fs2 from "fs";
|
|
1870
|
+
import * as path3 from "path";
|
|
1871
|
+
var logger10 = getLogger("subagents");
|
|
1872
|
+
var ENGINE_ROOTS_ENV = "XYZ_AGENT_ENGINE_ROOTS";
|
|
1873
|
+
function parseEngineRootsEnv(env) {
|
|
1874
|
+
const raw = env[ENGINE_ROOTS_ENV];
|
|
1875
|
+
if (raw === void 0 || raw.trim() === "") return [];
|
|
1876
|
+
const out = [];
|
|
1877
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1878
|
+
for (const part of raw.split(path3.delimiter)) {
|
|
1879
|
+
const dir = part.trim();
|
|
1880
|
+
if (dir === "") continue;
|
|
1881
|
+
if (!path3.isAbsolute(dir)) {
|
|
1882
|
+
logger10.warn(
|
|
1883
|
+
`[engine-discovery] ${ENGINE_ROOTS_ENV} entry '${dir}' is not an absolute path \u2014 dropping entry`
|
|
1884
|
+
);
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
if (seen.has(dir)) continue;
|
|
1888
|
+
seen.add(dir);
|
|
1889
|
+
out.push(dir);
|
|
1890
|
+
}
|
|
1891
|
+
return out;
|
|
1892
|
+
}
|
|
1893
|
+
function deriveNodeModuleRoots(argvEntry = process.argv[1]) {
|
|
1894
|
+
if (!argvEntry || argvEntry.trim() === "") return [];
|
|
1895
|
+
const roots = [];
|
|
1896
|
+
let dir = path3.dirname(path3.resolve(argvEntry));
|
|
1897
|
+
while (true) {
|
|
1898
|
+
const nm = path3.join(dir, "node_modules");
|
|
1899
|
+
if (isDirectory(nm)) roots.push(nm);
|
|
1900
|
+
const parent = path3.dirname(dir);
|
|
1901
|
+
if (parent === dir) break;
|
|
1902
|
+
dir = parent;
|
|
1903
|
+
}
|
|
1904
|
+
return roots;
|
|
1905
|
+
}
|
|
1906
|
+
function scanEngineRoot(root, visit) {
|
|
1907
|
+
let entries;
|
|
1908
|
+
try {
|
|
1909
|
+
entries = fs2.readdirSync(root, { withFileTypes: true });
|
|
1910
|
+
} catch {
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
for (const item of entries) {
|
|
1914
|
+
if (!item.isDirectory()) continue;
|
|
1915
|
+
const dir = path3.join(root, item.name);
|
|
1916
|
+
if (isFile(path3.join(dir, "package.json"))) {
|
|
1917
|
+
visit(dir);
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
let sub;
|
|
1921
|
+
try {
|
|
1922
|
+
sub = fs2.readdirSync(dir, { withFileTypes: true });
|
|
1923
|
+
} catch {
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
for (const child of sub) {
|
|
1927
|
+
if (!child.isDirectory()) continue;
|
|
1928
|
+
const subDir = path3.join(dir, child.name);
|
|
1929
|
+
if (isFile(path3.join(subDir, "package.json"))) visit(subDir);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
function isDirectory(p) {
|
|
1934
|
+
try {
|
|
1935
|
+
return fs2.statSync(p).isDirectory();
|
|
1936
|
+
} catch {
|
|
1937
|
+
return false;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function isFile(p) {
|
|
1941
|
+
try {
|
|
1942
|
+
return fs2.statSync(p).isFile();
|
|
1943
|
+
} catch {
|
|
1944
|
+
return false;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// src/execution/engine/config.ts
|
|
1949
|
+
import * as fs3 from "fs";
|
|
1950
|
+
import * as path4 from "path";
|
|
1951
|
+
var logger11 = getLogger("subagents");
|
|
1952
|
+
function readExplicitEngines(agentDir) {
|
|
1953
|
+
const configPath = path4.join(agentDir, "subagents", "config.json");
|
|
1954
|
+
let parsed;
|
|
1955
|
+
try {
|
|
1956
|
+
parsed = JSON.parse(fs3.readFileSync(configPath, "utf8"));
|
|
1957
|
+
} catch {
|
|
1958
|
+
return {};
|
|
1959
|
+
}
|
|
1960
|
+
if (typeof parsed !== "object" || parsed === null) return {};
|
|
1961
|
+
const engines = parsed["engines"];
|
|
1962
|
+
if (typeof engines !== "object" || engines === null || Array.isArray(engines)) {
|
|
1963
|
+
if (engines !== void 0) {
|
|
1964
|
+
logger11.warn(
|
|
1965
|
+
`[engine-discovery] config.json engines section must be an object keyed by engine id, got ${jsonKindOf(engines)} \u2014 ignoring L3 explicit engines`
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
return {};
|
|
1969
|
+
}
|
|
1970
|
+
const out = {};
|
|
1971
|
+
for (const [id, raw] of Object.entries(engines)) {
|
|
1972
|
+
const entry = sanitizeExplicitEntry(id, raw);
|
|
1973
|
+
if (entry !== void 0) out[id] = entry;
|
|
1974
|
+
}
|
|
1975
|
+
return out;
|
|
1976
|
+
}
|
|
1977
|
+
function sanitizeExplicitEntry(id, raw) {
|
|
1978
|
+
const v = requireEntryObject(id, raw);
|
|
1979
|
+
if (v === void 0) return void 0;
|
|
1980
|
+
if (v["enabled"] === false) return void 0;
|
|
1981
|
+
const command = requireCommand(id, v["command"]);
|
|
1982
|
+
if (command === void 0) return void 0;
|
|
1983
|
+
return {
|
|
1984
|
+
command,
|
|
1985
|
+
...normalizeArgs(id, v["args"]),
|
|
1986
|
+
...normalizeConfig(id, v["config"]),
|
|
1987
|
+
...normalizeCwd(id, v["cwd"])
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
function requireEntryObject(id, raw) {
|
|
1991
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
1992
|
+
logger11.warn(
|
|
1993
|
+
`[engine-discovery] config.json engines.${id} must be an object, got ${jsonKindOf(raw)} \u2014 skipping entry`
|
|
1994
|
+
);
|
|
1995
|
+
return void 0;
|
|
1996
|
+
}
|
|
1997
|
+
return raw;
|
|
1998
|
+
}
|
|
1999
|
+
function requireCommand(id, command) {
|
|
2000
|
+
if (typeof command !== "string" || command.trim() === "") {
|
|
2001
|
+
logger11.warn(
|
|
2002
|
+
`[engine-discovery] config.json engines.${id}.command is required (non-empty string) \u2014 skipping entry`
|
|
2003
|
+
);
|
|
2004
|
+
return void 0;
|
|
2005
|
+
}
|
|
2006
|
+
return command;
|
|
2007
|
+
}
|
|
2008
|
+
function normalizeArgs(id, args) {
|
|
2009
|
+
if (args !== void 0 && !isStringArray(args)) {
|
|
2010
|
+
logger11.warn(
|
|
2011
|
+
`[engine-discovery] config.json engines.${id}.args must be a string array \u2014 ignoring args`
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
2014
|
+
return { args: isStringArray(args) ? args : [] };
|
|
2015
|
+
}
|
|
2016
|
+
function normalizeConfig(id, config) {
|
|
2017
|
+
if (config !== void 0 && !isStringRecord(config)) {
|
|
2018
|
+
logger11.warn(
|
|
2019
|
+
`[engine-discovery] config.json engines.${id}.config must be a Record<string, string> \u2014 ignoring config`
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
return isStringRecord(config) ? { config } : {};
|
|
2023
|
+
}
|
|
2024
|
+
function normalizeCwd(id, cwd) {
|
|
2025
|
+
if (cwd !== void 0 && typeof cwd !== "string") {
|
|
2026
|
+
logger11.warn(
|
|
2027
|
+
`[engine-discovery] config.json engines.${id}.cwd must be a string \u2014 ignoring cwd`
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
return typeof cwd === "string" ? { cwd } : {};
|
|
2031
|
+
}
|
|
2032
|
+
function isStringArray(value) {
|
|
2033
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2034
|
+
}
|
|
2035
|
+
function isStringRecord(value) {
|
|
2036
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
2037
|
+
return Object.values(value).every((item) => typeof item === "string");
|
|
2038
|
+
}
|
|
2039
|
+
function jsonKindOf(value) {
|
|
2040
|
+
if (value === null) return "null";
|
|
2041
|
+
if (Array.isArray(value)) return "array";
|
|
2042
|
+
return typeof value;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// src/execution/engine/registry.ts
|
|
2046
|
+
var logger12 = getLogger("subagents");
|
|
2047
|
+
var DEFAULT_ENGINE_ID = "pi";
|
|
2048
|
+
function normalizeEngineId(engine) {
|
|
2049
|
+
return engine?.trim() || DEFAULT_ENGINE_ID;
|
|
2050
|
+
}
|
|
2051
|
+
var EngineNotFoundError = class extends Error {
|
|
2052
|
+
/** 结构化错误码(错误规格表的 code 列,供调用方程序化分流)。 */
|
|
2053
|
+
code = "engine_not_found";
|
|
2054
|
+
/** 请求的(未注册的)引擎 id。 */
|
|
2055
|
+
engineId;
|
|
2056
|
+
/** 请求时刻的已注册清单快照(防错误对象跨时间读 Map 的失真)。 */
|
|
2057
|
+
registered;
|
|
2058
|
+
/** 错误来源定位(agent .md 文件路径 / 配置键等;运行期 getEngine 无来源不传)。 */
|
|
2059
|
+
source;
|
|
2060
|
+
constructor(engineId, registered, source) {
|
|
2061
|
+
super(
|
|
2062
|
+
`engine_not_found: engine '${engineId}' is not registered. Registered engines: ${registered.length > 0 ? registered.join(", ") : "(none)"}. ` + (registered.length === 0 ? `No engine packages were discovered. Recovery: install an engine package (e.g. @zhushanwen/pi-subagent-cli / @zhushanwen/zcode-subagent-cli), or add its location to XYZ_AGENT_ENGINE_ROOTS / subagents/config.json engines section.` : `Recovery: check the engine id in the agent .md frontmatter (engine: field) or the global default engine setting, fix the typo, or install/register the engine first (registered engines are listed above).`) + (source !== void 0 ? ` Source: ${source}.` : "")
|
|
2063
|
+
);
|
|
2064
|
+
this.name = "EngineNotFoundError";
|
|
2065
|
+
this.engineId = engineId;
|
|
2066
|
+
this.registered = registered;
|
|
2067
|
+
this.source = source;
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
var ENGINE_REGISTRY_SLOT_KEY = /* @__PURE__ */ Symbol.for("@zhushanwen/pi-subagent-workflow.engineRegistry");
|
|
2071
|
+
function getRegistrySlot() {
|
|
2072
|
+
let slot = Reflect.get(globalThis, ENGINE_REGISTRY_SLOT_KEY);
|
|
2073
|
+
if (!slot) {
|
|
2074
|
+
slot = { descriptors: /* @__PURE__ */ new Map(), singletons: /* @__PURE__ */ new Map() };
|
|
2075
|
+
Reflect.set(globalThis, ENGINE_REGISTRY_SLOT_KEY, slot);
|
|
2076
|
+
}
|
|
2077
|
+
return slot;
|
|
2078
|
+
}
|
|
2079
|
+
function portFactoryOf(descriptor) {
|
|
2080
|
+
return descriptor.kind === "inproc" ? descriptor.factory : descriptor.portFactory;
|
|
2081
|
+
}
|
|
2082
|
+
function triggerEngineDispose(engine, source) {
|
|
2083
|
+
if (typeof engine.dispose !== "function") return;
|
|
2084
|
+
try {
|
|
2085
|
+
engine.dispose().then(void 0, (err) => {
|
|
2086
|
+
logger12.warn(
|
|
2087
|
+
`[engine-registry] engine '${engine.id}' dispose rejected (${source}, best-effort continue): ${err instanceof Error ? err.message : String(err)}`
|
|
2088
|
+
);
|
|
2089
|
+
});
|
|
2090
|
+
} catch (err) {
|
|
2091
|
+
logger12.warn(
|
|
2092
|
+
`[engine-registry] engine '${engine.id}' dispose threw synchronously (${source}, best-effort continue): ${err instanceof Error ? err.message : String(err)}`
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
function registerEngineDescriptor(id, descriptor) {
|
|
2097
|
+
const slot = getRegistrySlot();
|
|
2098
|
+
const previous = slot.singletons.get(id);
|
|
2099
|
+
if (previous) triggerEngineDispose(previous, `registerEngineDescriptor('${id}') overwrite`);
|
|
2100
|
+
slot.descriptors.set(id, descriptor);
|
|
2101
|
+
slot.singletons.delete(id);
|
|
2102
|
+
}
|
|
2103
|
+
function getEngine(id) {
|
|
2104
|
+
const slot = getRegistrySlot();
|
|
2105
|
+
const cached = slot.singletons.get(id);
|
|
2106
|
+
if (cached) return cached;
|
|
2107
|
+
const descriptor = slot.descriptors.get(id);
|
|
2108
|
+
if (!descriptor) {
|
|
2109
|
+
throw new EngineNotFoundError(id, listEngines());
|
|
2110
|
+
}
|
|
2111
|
+
const engine = portFactoryOf(descriptor)();
|
|
2112
|
+
slot.singletons.set(id, engine);
|
|
2113
|
+
return engine;
|
|
2114
|
+
}
|
|
2115
|
+
function hasEngine(id) {
|
|
2116
|
+
return getRegistrySlot().descriptors.has(id);
|
|
2117
|
+
}
|
|
2118
|
+
function listEngines() {
|
|
2119
|
+
return [...getRegistrySlot().descriptors.keys()];
|
|
2120
|
+
}
|
|
2121
|
+
function listEnginesByDisplayName() {
|
|
2122
|
+
const slot = getRegistrySlot();
|
|
2123
|
+
const keyed = [...slot.descriptors.entries()].map(([id, descriptor]) => {
|
|
2124
|
+
const displayName = descriptor.kind === "cli" ? descriptor.manifest?.displayName ?? id : id;
|
|
2125
|
+
return { id, displayName };
|
|
2126
|
+
});
|
|
2127
|
+
keyed.sort(
|
|
2128
|
+
(a, b) => a.displayName !== b.displayName ? a.displayName < b.displayName ? -1 : 1 : a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
|
2129
|
+
);
|
|
2130
|
+
return keyed.map((k) => k.id);
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// src/execution/engine/engine-discovery-scan.ts
|
|
2134
|
+
var logger13 = getLogger("subagents");
|
|
2135
|
+
function scanEngines(opts) {
|
|
2136
|
+
const env = opts.env ?? process.env;
|
|
2137
|
+
const result = { discovered: [], skipped: [], unusable: [] };
|
|
2138
|
+
const present = (entry) => {
|
|
2139
|
+
const idx = result.discovered.findIndex((d) => d.id === entry.id);
|
|
2140
|
+
if (idx >= 0) {
|
|
2141
|
+
logger13.debug(
|
|
2142
|
+
`[engine-discovery] engine id '${entry.id}' from '${entry.source}' overrides earlier discovery from '${result.discovered[idx].source}' \u2014 same-id override`
|
|
2143
|
+
);
|
|
2144
|
+
result.discovered[idx] = entry;
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
result.discovered.push(entry);
|
|
2148
|
+
};
|
|
2149
|
+
const visitManifestPackage = (pkgDir, source) => {
|
|
2150
|
+
const inspection = inspectEnginePackage(pkgDir, source, opts.hostKind, env);
|
|
2151
|
+
if (inspection.status === "skip") {
|
|
2152
|
+
if (!inspection.reason.includes("not an engine package")) {
|
|
2153
|
+
logger13.warn(`[engine-discovery] skipping ${pkgDir} (${source}): ${inspection.reason}`);
|
|
2154
|
+
result.skipped.push({ pkgDir, reason: inspection.reason });
|
|
2155
|
+
}
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
if (inspection.status === "unusable") {
|
|
2159
|
+
logger13.warn(`[engine-discovery] engine unavailable: ${inspection.reason}`);
|
|
2160
|
+
result.unusable.push({ pkgDir, id: inspection.id, reason: inspection.reason });
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
present(inspection.entry);
|
|
2164
|
+
};
|
|
2165
|
+
for (const dir of parseEngineRootsEnv(env)) {
|
|
2166
|
+
scanEngineRoot(dir, (pkgDir) => visitManifestPackage(pkgDir, ENGINE_ROOTS_ENV));
|
|
2167
|
+
}
|
|
2168
|
+
const hostRoots = [
|
|
2169
|
+
...getHostServices().discoveryRoots?.().engines ?? [],
|
|
2170
|
+
...opts.extraRoots ?? []
|
|
2171
|
+
];
|
|
2172
|
+
for (const root of hostRoots) {
|
|
2173
|
+
scanEngineRoot(root.dir, (pkgDir) => visitManifestPackage(pkgDir, root.source));
|
|
2174
|
+
}
|
|
2175
|
+
const nodeRoots = opts.nodeModuleRoots ?? deriveNodeModuleRoots();
|
|
2176
|
+
for (const dir of nodeRoots) {
|
|
2177
|
+
scanEngineRoot(dir, (pkgDir) => visitManifestPackage(pkgDir, "node-modules"));
|
|
2178
|
+
}
|
|
2179
|
+
if (opts.agentDir !== void 0) {
|
|
2180
|
+
for (const [id, entry] of Object.entries(readExplicitEngines(opts.agentDir))) {
|
|
2181
|
+
const command = resolveExplicitCommand(entry.command, env);
|
|
2182
|
+
if (command === void 0) {
|
|
2183
|
+
const reason = `engine '${id}' (config.json engines.${id}): command '${entry.command}' not found or not executable`;
|
|
2184
|
+
logger13.warn(`[engine-discovery] ${reason} \u2014 not registering`);
|
|
2185
|
+
result.unusable.push({ pkgDir: `config.json engines.${id}`, id, reason });
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
present(buildExplicitDescriptor(id, command, entry, opts));
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
return result;
|
|
2192
|
+
}
|
|
2193
|
+
var loadedDiscoveryIdSet = /* @__PURE__ */ new Set();
|
|
2194
|
+
function discoverAndRegisterEngines(opts) {
|
|
2195
|
+
const result = scanEngines(opts);
|
|
2196
|
+
for (const entry of result.discovered) {
|
|
2197
|
+
const existed = hasEngine(entry.id);
|
|
2198
|
+
registerEngineDescriptor(entry.id, entry.descriptor);
|
|
2199
|
+
loadedDiscoveryIdSet.add(entry.id);
|
|
2200
|
+
if (existed) {
|
|
2201
|
+
logger13.debug(
|
|
2202
|
+
`[engine-discovery] engine '${entry.id}' descriptor overwritten in registry (source '${entry.source}') \u2014 same-id override`
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return result;
|
|
2207
|
+
}
|
|
2208
|
+
function loadedDiscoveryIds() {
|
|
2209
|
+
return [...loadedDiscoveryIdSet];
|
|
2210
|
+
}
|
|
2211
|
+
function ensureEngineDiscovered(id, opts) {
|
|
2212
|
+
if (hasEngine(id)) return true;
|
|
2213
|
+
discoverAndRegisterEngines(opts);
|
|
2214
|
+
return hasEngine(id);
|
|
2215
|
+
}
|
|
2216
|
+
function resolveExplicitCommand(command, env) {
|
|
2217
|
+
if (command.includes("/") || command.includes("\\")) {
|
|
2218
|
+
const p = path5.isAbsolute(command) ? command : path5.resolve(command);
|
|
2219
|
+
return canExecute(p) ? p : void 0;
|
|
2220
|
+
}
|
|
2221
|
+
const pathVar = env["PATH"] ?? env["Path"] ?? "";
|
|
2222
|
+
for (const dir of pathVar.split(path5.delimiter)) {
|
|
2223
|
+
if (dir === "") continue;
|
|
2224
|
+
const p = path5.join(dir, command);
|
|
2225
|
+
if (canExecute(p)) return p;
|
|
2226
|
+
}
|
|
2227
|
+
return void 0;
|
|
2228
|
+
}
|
|
2229
|
+
function buildExplicitDescriptor(id, command, entry, opts) {
|
|
2230
|
+
const env = opts.env ?? process.env;
|
|
2231
|
+
logger13.debug(
|
|
2232
|
+
`[engine-discovery] engine '${id}' registered from config.json engines section with conservative capabilities (no manifest)`
|
|
2233
|
+
);
|
|
2234
|
+
const caps = { ...CONSERVATIVE_CAPABILITIES };
|
|
2235
|
+
const descriptor = {
|
|
2236
|
+
kind: "cli",
|
|
2237
|
+
command,
|
|
2238
|
+
args: entry.args,
|
|
2239
|
+
capabilities: caps,
|
|
2240
|
+
portFactory: () => {
|
|
2241
|
+
const dataDir = opts.dataDir ?? getEngineDataDir(env);
|
|
2242
|
+
const client = new EngineClient({
|
|
2243
|
+
engineId: id,
|
|
2244
|
+
command,
|
|
2245
|
+
args: entry.args,
|
|
2246
|
+
...entry.cwd !== void 0 ? { cwd: entry.cwd } : {},
|
|
2247
|
+
hostKind: opts.hostKind,
|
|
2248
|
+
dataDir,
|
|
2249
|
+
envPrefixes: [],
|
|
2250
|
+
// [W6 R3 MF-A] 同上:host/askUser 应答端经壳侧登记处接线。
|
|
2251
|
+
uiRequestHandler: getHostUiRequestEndpoint(),
|
|
2252
|
+
...entry.config !== void 0 ? { engineConfig: entry.config } : {},
|
|
2253
|
+
manifestDiagnostics: { capabilities: caps, models: null }
|
|
2254
|
+
});
|
|
2255
|
+
return new RemoteEngine({
|
|
2256
|
+
engineId: id,
|
|
2257
|
+
client,
|
|
2258
|
+
manifest: { capabilities: caps },
|
|
2259
|
+
dataDir,
|
|
2260
|
+
hostKind: opts.hostKind,
|
|
2261
|
+
...entry.config !== void 0 ? { engineConfig: entry.config } : {}
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
return {
|
|
2266
|
+
id,
|
|
2267
|
+
source: "config.json",
|
|
2268
|
+
...entry.config !== void 0 ? { engineConfig: entry.config } : {},
|
|
2269
|
+
descriptor
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
export {
|
|
2274
|
+
DEFAULT_DATA_ROOT,
|
|
2275
|
+
configureCore,
|
|
2276
|
+
getHostServices,
|
|
2277
|
+
getLogger,
|
|
2278
|
+
EngineError,
|
|
2279
|
+
DEFAULT_ENGINE_ID,
|
|
2280
|
+
normalizeEngineId,
|
|
2281
|
+
EngineNotFoundError,
|
|
2282
|
+
registerEngineDescriptor,
|
|
2283
|
+
getEngine,
|
|
2284
|
+
hasEngine,
|
|
2285
|
+
listEngines,
|
|
2286
|
+
listEnginesByDisplayName,
|
|
2287
|
+
CONSERVATIVE_CAPABILITIES,
|
|
2288
|
+
resolveManifestBin,
|
|
2289
|
+
parseCapabilities,
|
|
2290
|
+
parseEnvPrefixes,
|
|
2291
|
+
parseModelCatalog,
|
|
2292
|
+
getEngineDataDir,
|
|
2293
|
+
EngineClient,
|
|
2294
|
+
assertTaskShapeSupported,
|
|
2295
|
+
RemoteEngine,
|
|
2296
|
+
setHostUiRequestEndpoint,
|
|
2297
|
+
inspectEnginePackage,
|
|
2298
|
+
ENGINE_ROOTS_ENV,
|
|
2299
|
+
parseEngineRootsEnv,
|
|
2300
|
+
deriveNodeModuleRoots,
|
|
2301
|
+
scanEngines,
|
|
2302
|
+
discoverAndRegisterEngines,
|
|
2303
|
+
loadedDiscoveryIds,
|
|
2304
|
+
ensureEngineDiscovered
|
|
2305
|
+
};
|