@zhushanwen/subagent-core 0.2.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 +114 -0
- package/dist/chunk-3VOERJPJ.js +22 -0
- package/dist/chunk-A4IVZWVX.js +31 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
- package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
- package/dist/execution/engine/engines/zcode/constants.js +22 -0
- package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
- package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
- package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
- package/dist/execution/engine/engines/zcode/reader.js +239 -0
- package/dist/execution/engine/paths.cjs +55 -0
- package/dist/execution/engine/paths.d.cts +8 -0
- package/dist/execution/engine/paths.d.ts +8 -0
- package/dist/execution/engine/paths.js +26 -0
- package/dist/execution/relay-env.cjs +62 -0
- package/dist/execution/relay-env.d.cts +33 -0
- package/dist/execution/relay-env.d.ts +33 -0
- package/dist/execution/relay-env.js +20 -0
- package/dist/index.cjs +1852 -0
- package/dist/index.d.cts +1184 -0
- package/dist/index.d.ts +1184 -0
- package/dist/index.js +1810 -0
- package/dist/types-BxyAidGf.d.cts +875 -0
- package/dist/types-BxyAidGf.d.ts +875 -0
- package/package.json +99 -0
- package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
- package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
- package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
- package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
- package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
- package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
- package/src/__tests__/m2-append-content-probe.test.ts +73 -0
- package/src/__tests__/manifest-store.test.ts +368 -0
- package/src/__tests__/record-store-cache.test.ts +335 -0
- package/src/__tests__/record-store-index.test.ts +560 -0
- package/src/__tests__/record-store-last-line.test.ts +129 -0
- package/src/__tests__/review-fix-loop-script.test.ts +288 -0
- package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
- package/src/__tests__/robustness-low-batch1.test.ts +286 -0
- package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
- package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
- package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
- package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
- package/src/__tests__/session-runner.test.ts +69 -0
- package/src/__tests__/smoke.test.ts +9 -0
- package/src/__tests__/stream-sink.test.ts +203 -0
- package/src/core/__tests__/host-services.test.ts +144 -0
- package/src/core/__tests__/logger.test.ts +78 -0
- package/src/core/__tests__/notify-ports.test.ts +104 -0
- package/src/core/host-services.ts +97 -0
- package/src/core/logger.ts +47 -0
- package/src/core/notify-ports.ts +126 -0
- package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
- package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
- package/src/execution/__tests__/config.test.ts +302 -0
- package/src/execution/__tests__/delivery-methods.test.ts +424 -0
- package/src/execution/__tests__/dialog-queue.test.ts +299 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
- package/src/execution/__tests__/execute-nesting.test.ts +323 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
- package/src/execution/__tests__/execution-record.test.ts +1394 -0
- package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
- package/src/execution/__tests__/finalize-record.test.ts +367 -0
- package/src/execution/__tests__/finalized-marker.test.ts +104 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
- package/src/execution/__tests__/host-mode.test.ts +87 -0
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
- package/src/execution/__tests__/model-resolver.test.ts +465 -0
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/output-collector.test.ts +358 -0
- package/src/execution/__tests__/path-encoding.test.ts +104 -0
- package/src/execution/__tests__/pi-invocation.test.ts +134 -0
- package/src/execution/__tests__/record-store.test.ts +1057 -0
- package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
- package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
- package/src/execution/__tests__/relay-env.test.ts +42 -0
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/rpc-mode.test.ts +89 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +293 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
- package/src/execution/__tests__/spawn-args.test.ts +445 -0
- package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
- package/src/execution/__tests__/stdin-writer.test.ts +462 -0
- package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
- package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +788 -0
- package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +615 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/ui-channels.test.ts +187 -0
- package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
- package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
- package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
- package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
- package/src/execution/__tests__/worktree-manager.test.ts +633 -0
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
- package/src/execution/__tests__/worktree-registry.test.ts +199 -0
- package/src/execution/agent-registry.ts +199 -0
- package/src/execution/agent-result-mapper.ts +90 -0
- package/src/execution/alive-store.ts +119 -0
- package/src/execution/argv-mirror.ts +113 -0
- package/src/execution/best-effort.ts +38 -0
- package/src/execution/channel-registry-access.ts +144 -0
- package/src/execution/concurrency-pool.ts +116 -0
- package/src/execution/config.ts +165 -0
- package/src/execution/dialog-queue.ts +330 -0
- package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
- package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
- package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
- package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
- package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
- package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
- package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
- package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
- package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
- package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
- package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
- package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
- package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
- package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
- package/src/execution/engine/__tests__/paths.test.ts +39 -0
- package/src/execution/engine/__tests__/registry.test.ts +120 -0
- package/src/execution/engine/__tests__/routing.test.ts +231 -0
- package/src/execution/engine/common/data-dir.ts +66 -0
- package/src/execution/engine/common/errors.ts +183 -0
- package/src/execution/engine/common/event-journal.ts +254 -0
- package/src/execution/engine/common/journal-replay.ts +62 -0
- package/src/execution/engine/common/kill-chain.ts +221 -0
- package/src/execution/engine/common/nesting-guard.ts +50 -0
- package/src/execution/engine/common/persona-router.ts +108 -0
- package/src/execution/engine/common/pool-manager.ts +226 -0
- package/src/execution/engine/common/schema-emulation.ts +189 -0
- package/src/execution/engine/common/session-view-projection.ts +51 -0
- package/src/execution/engine/engine-discovery.ts +65 -0
- package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
- package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
- package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
- package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
- package/src/execution/engine/engines/pi/reader.ts +48 -0
- package/src/execution/engine/engines/pi/registration.ts +35 -0
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
- package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
- package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
- package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
- package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
- package/src/execution/engine/engines/zcode/constants.ts +43 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
- package/src/execution/engine/engines/zcode/launcher.ts +161 -0
- package/src/execution/engine/engines/zcode/parser.ts +436 -0
- package/src/execution/engine/engines/zcode/preparer.ts +363 -0
- package/src/execution/engine/engines/zcode/reader.ts +381 -0
- package/src/execution/engine/engines/zcode/registration.ts +37 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
- package/src/execution/engine/host-task-spec.ts +47 -0
- package/src/execution/engine/model-prompt.ts +158 -0
- package/src/execution/engine/paths.ts +42 -0
- package/src/execution/engine/port.ts +153 -0
- package/src/execution/engine/registry.ts +133 -0
- package/src/execution/engine/routing.ts +218 -0
- package/src/execution/engine/types.ts +309 -0
- package/src/execution/execute-options-mapper.ts +115 -0
- package/src/execution/execution-record.ts +984 -0
- package/src/execution/finalize-record.ts +254 -0
- package/src/execution/finalized-marker.ts +70 -0
- package/src/execution/get-state-handshake.ts +104 -0
- package/src/execution/host-mode.ts +56 -0
- package/src/execution/idle-gc.ts +47 -0
- package/src/execution/lifecycle-manager.ts +513 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +256 -0
- package/src/execution/model-config-service.ts +250 -0
- package/src/execution/model-resolver.ts +248 -0
- package/src/execution/notifier.ts +372 -0
- package/src/execution/notify-ledger.ts +580 -0
- package/src/execution/output-collector.ts +228 -0
- package/src/execution/path-encoding.ts +60 -0
- package/src/execution/pi-invocation.ts +120 -0
- package/src/execution/record-entry.ts +132 -0
- package/src/execution/record-store.ts +1265 -0
- package/src/execution/relay-env.ts +37 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +120 -0
- package/src/execution/session-pending.ts +170 -0
- package/src/execution/session-reconstructor.ts +678 -0
- package/src/execution/session-runner.ts +1789 -0
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/spawn-event-adapter.ts +363 -0
- package/src/execution/stdin-writer.ts +198 -0
- package/src/execution/stream-sink.ts +126 -0
- package/src/execution/subagent-service.ts +2268 -0
- package/src/execution/subprocess-agent-runner.ts +317 -0
- package/src/execution/temp-prompt.ts +62 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +102 -0
- package/src/execution/types.ts +1023 -0
- package/src/execution/ui-channels.ts +216 -0
- package/src/execution/ui-interaction-model.ts +48 -0
- package/src/execution/ui-request-handler-factory.ts +243 -0
- package/src/execution/ui-request-observability.ts +81 -0
- package/src/execution/ui-request-queue.ts +184 -0
- package/src/execution/worktree-manager.ts +688 -0
- package/src/execution/worktree-registry.ts +279 -0
- package/src/index.ts +87 -0
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
- package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
- package/src/orchestration/__tests__/args-validator.test.ts +143 -0
- package/src/orchestration/__tests__/config-loader.test.ts +503 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
- package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
- package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
- package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
- package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
- package/src/orchestration/__tests__/script-lint.test.ts +831 -0
- package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
- package/src/orchestration/__tests__/test-mocks.ts +197 -0
- package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
- package/src/orchestration/__tests__/worker-host.test.ts +120 -0
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/agent-opts-resolver.ts +174 -0
- package/src/orchestration/args-validator.ts +127 -0
- package/src/orchestration/config-loader.ts +316 -0
- package/src/orchestration/error-recovery.ts +1018 -0
- package/src/orchestration/execute-agent-call.ts +256 -0
- package/src/orchestration/launcher.ts +455 -0
- package/src/orchestration/lifecycle.ts +399 -0
- package/src/orchestration/models/__tests__/budget.test.ts +307 -0
- package/src/orchestration/models/agent-call.ts +83 -0
- package/src/orchestration/models/budget.ts +118 -0
- package/src/orchestration/models/ports.ts +164 -0
- package/src/orchestration/models/run-runtime.ts +104 -0
- package/src/orchestration/models/run-spec.ts +83 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +183 -0
- package/src/orchestration/models/types.ts +301 -0
- package/src/orchestration/models/workflow-run.ts +254 -0
- package/src/orchestration/models/workflow-script-registry.ts +35 -0
- package/src/orchestration/models/workflow-script.ts +117 -0
- package/src/orchestration/script-lint.ts +766 -0
- package/src/orchestration/skill-discovery.ts +135 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +98 -0
- package/src/orchestration/worker-script-builder.ts +421 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +133 -0
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/meta-parser.test.ts +304 -0
- package/src/shared/__tests__/model-ref.test.ts +306 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
- package/src/shared/__tests__/resource-discovery.test.ts +749 -0
- package/src/shared/__tests__/resource-meta.test.ts +51 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/__tests__/timer-delay.test.ts +61 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/agent-ref.ts +57 -0
- package/src/shared/meta-parser.ts +261 -0
- package/src/shared/model-ref.ts +286 -0
- package/src/shared/resource-discovery.ts +835 -0
- package/src/shared/resource-meta.ts +65 -0
- package/src/shared/schema-env.ts +44 -0
- package/src/shared/schema-jsonify.ts +58 -0
- package/src/shared/timer-delay.ts +54 -0
- package/src/shared/xml-injection.ts +35 -0
- package/workflows/README.md +81 -0
- package/workflows/_shared/agent-refs.cjs +40 -0
- package/workflows/chain.js +137 -0
- package/workflows/map-reduce.js +180 -0
- package/workflows/parallel.js +154 -0
- package/workflows/review-fix-loop-utils.cjs +1542 -0
- package/workflows/review-fix-loop.js +1605 -0
- package/workflows/scatter-gather.js +184 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// src/execution/engine/common/kill-chain.ts
|
|
2
|
+
//
|
|
3
|
+
// 超时杀链与 abort 两级中断(P2 公共降级层)。设计权威源:
|
|
4
|
+
// docs/architecture/subagent-engine-abstraction.md D1(abort 分级:引擎原生中断 →
|
|
5
|
+
// 公共杀链兜底;CLI-only 引擎直接杀链,杀死后宿主合成终态)+ §3.3.3 engine_timeout /
|
|
6
|
+
// engine_run_failed 行 + 附录 A「CLI 超时」行(6/6 引擎全缺 → 宿主公共层全补)。
|
|
7
|
+
//
|
|
8
|
+
// 为什么放公共层:没有任何引擎有 CLI 级超时(§2.2 表),杀链写一次全引擎复用(D4)。
|
|
9
|
+
|
|
10
|
+
import { getLogger } from "../../../core/logger.ts";
|
|
11
|
+
|
|
12
|
+
import { engineTimeoutDetail } from "./errors.ts";
|
|
13
|
+
import type { AgentOutcome, AgentTaskSpec } from "../types.ts";
|
|
14
|
+
import { DEFAULT_ENGINE_ID } from "../registry.ts";
|
|
15
|
+
|
|
16
|
+
const logger = getLogger("subagents");
|
|
17
|
+
|
|
18
|
+
// ============================================================
|
|
19
|
+
// 杀链(SIGTERM → grace → SIGKILL)
|
|
20
|
+
// ============================================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 可杀子进程的结构形状(Node ChildProcess 的结构子集)。
|
|
24
|
+
* 用结构接口而非 ChildProcess 类型:测试可注入 fake(ChildProcess 全字段构造过重),
|
|
25
|
+
* 且未来 driver host 的常驻进程句柄只要满足此形状即可复用杀链。
|
|
26
|
+
*/
|
|
27
|
+
export interface KillableChild {
|
|
28
|
+
/** 非 null = 进程已退出(自然退出码)。 */
|
|
29
|
+
readonly exitCode: number | null;
|
|
30
|
+
/** 非 null = 进程被信号杀死。exitCode/signalCode 任一非 null 即已退出。 */
|
|
31
|
+
readonly signalCode: string | null;
|
|
32
|
+
/** 发信号。返回 false = 进程已不存在(kill no-op)。 */
|
|
33
|
+
kill(signal?: NodeJS.Signals | number): boolean;
|
|
34
|
+
once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** SIGTERM 优雅窗口默认值(ms)。实测校准点:设计 §5 待验证检查点⑤(zcode 对 SIGTERM 的响应时序)。 */
|
|
38
|
+
export const DEFAULT_KILL_GRACE_MS = 5_000;
|
|
39
|
+
|
|
40
|
+
/** SIGKILL 发出后的收尸等待上限(ms)——SIGKILL 后进程必死(D 状态罕见),有界等待防挂死。 */
|
|
41
|
+
const SIGKILL_REAP_TIMEOUT_MS = 10_000;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 宿主超时 abort 的 signal.reason 标记(对齐点④)。mergeTimeoutSignal(SAR 侧超时
|
|
45
|
+
* 合并链)产出;引擎 abort 合成终态时判别「超时 vs 用户 cancel」——超时统一走
|
|
46
|
+
* synthesizeTimeoutOutcome(engine_timeout),cancel 维持中止标记(engine_run_failed)。
|
|
47
|
+
*/
|
|
48
|
+
export const HOST_TIMEOUT_ABORT_REASON = "agent-call-timeout";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 杀链:SIGTERM → 等待 graceMs → 仍存活则 SIGKILL。
|
|
52
|
+
*
|
|
53
|
+
* @returns 'terminated' = SIGTERM 优雅退出(或进程已自行退出);'killed' = 走了 SIGKILL。
|
|
54
|
+
*/
|
|
55
|
+
export async function killChain(
|
|
56
|
+
child: KillableChild,
|
|
57
|
+
opts: { graceMs: number },
|
|
58
|
+
): Promise<"terminated" | "killed"> {
|
|
59
|
+
// 已退出(自然/已被杀)→ 无需发信号,按优雅终止口径返回
|
|
60
|
+
if (child.exitCode !== null || child.signalCode !== null) return "terminated";
|
|
61
|
+
|
|
62
|
+
const exited = waitForExit(child);
|
|
63
|
+
safeKill(child, "SIGTERM");
|
|
64
|
+
|
|
65
|
+
const graceful = await raceTimeout(exited, opts.graceMs);
|
|
66
|
+
if (graceful === "settled") return "terminated";
|
|
67
|
+
// grace 超时后进程可能恰好在检查前一刻退出——复核退出态,避免误杀已死进程
|
|
68
|
+
if (child.exitCode !== null || child.signalCode !== null) return "terminated";
|
|
69
|
+
|
|
70
|
+
safeKill(child, "SIGKILL");
|
|
71
|
+
// 有界收尸:无论等到与否都返回 'killed'(信号已发出,返回值表达「走了 SIGKILL」)
|
|
72
|
+
await raceTimeout(exited, SIGKILL_REAP_TIMEOUT_MS);
|
|
73
|
+
return "killed";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 发信号兜底包裹:进程恰在退出态检查与 kill 之间自退时,ChildProcess.kill 可能抛
|
|
78
|
+
* (zsub 实测经验)——幂等吞掉并 debug 留痕,不阻断杀链语义(对已退进程信号本就是
|
|
79
|
+
* no-op)。收口自 zcode launcher 的内联实现(对齐点②:单一权威)。
|
|
80
|
+
*/
|
|
81
|
+
function safeKill(child: KillableChild, signal: NodeJS.Signals): void {
|
|
82
|
+
try {
|
|
83
|
+
child.kill(signal);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
logger.debug(
|
|
86
|
+
`[kill-chain] ${signal} on exited process: ${err instanceof Error ? err.message : String(err)}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================
|
|
92
|
+
// 超时终态合成
|
|
93
|
+
// ============================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 合成 engine_timeout 终态(宿主超时杀链走完后的 AgentOutcome)。
|
|
97
|
+
* 含 stdout 尾部 2000 字 + 「可用 engine: pi 重跑」建议(§3.3.3 第 6 行);
|
|
98
|
+
* exitCode: null = 被信号杀死(AgentOutcome 的杀链判据,§3.3.5)。
|
|
99
|
+
*/
|
|
100
|
+
export function synthesizeTimeoutOutcome(
|
|
101
|
+
task: AgentTaskSpec,
|
|
102
|
+
stdoutTail: string,
|
|
103
|
+
engineId: string = DEFAULT_ENGINE_ID,
|
|
104
|
+
): AgentOutcome {
|
|
105
|
+
return {
|
|
106
|
+
content: "",
|
|
107
|
+
// slug 进错误信息:单看 outcome(record 之外)也能定位是哪个任务超时
|
|
108
|
+
error: `engine_timeout: [slug=${task.slug}] ${engineTimeoutDetail(stdoutTail)}`,
|
|
109
|
+
// 被信号杀死:退出码语义为 null(§3.3.5 AgentOutcome.exitCode 注释)
|
|
110
|
+
exitCode: null,
|
|
111
|
+
engineId,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============================================================
|
|
116
|
+
// abort 两级编排
|
|
117
|
+
// ============================================================
|
|
118
|
+
|
|
119
|
+
/** 原生中断后的宽限窗口默认值(ms):中断指令送达 → 引擎自行收尾的等待上限。 */
|
|
120
|
+
export const DEFAULT_NATIVE_INTERRUPT_GRACE_MS = 3_000;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* abort 两级编排 helper(D1 abort 分级的公共实现):
|
|
124
|
+
* signal abort 时 ①先调引擎原生中断(tryNativeInterrupt,若有)并给宽限窗口,
|
|
125
|
+
* 窗口内未停(或无原生中断——CLI-only 引擎)则 ②走杀链兜底。
|
|
126
|
+
*
|
|
127
|
+
* 进程自然退出时以 'terminated' settle(挂在 exit 事件上,杀链对已退进程是 no-op),
|
|
128
|
+
* 调用方可安全 await;signal 永不 abort 且进程已退时也会 settle,不悬挂。
|
|
129
|
+
*/
|
|
130
|
+
export function abortWithFallback(
|
|
131
|
+
child: KillableChild,
|
|
132
|
+
signal: AbortSignal,
|
|
133
|
+
tryNativeInterrupt?: () => Promise<void>,
|
|
134
|
+
opts?: { graceMs?: number; nativeGraceMs?: number },
|
|
135
|
+
): Promise<"terminated" | "killed"> {
|
|
136
|
+
const graceMs = opts?.graceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
137
|
+
const nativeGraceMs = opts?.nativeGraceMs ?? DEFAULT_NATIVE_INTERRUPT_GRACE_MS;
|
|
138
|
+
|
|
139
|
+
return new Promise<"terminated" | "killed">((resolve) => {
|
|
140
|
+
let settled = false;
|
|
141
|
+
// 三个 handler 互相引用(finish 移除 onAbort / onAbort 触发 runChain / runChain
|
|
142
|
+
// 兜底 finish),用函数声明 + 提升消解声明环,避免 use-before-define。
|
|
143
|
+
function finish(result: "terminated" | "killed"): void {
|
|
144
|
+
if (settled) return;
|
|
145
|
+
settled = true;
|
|
146
|
+
signal.removeEventListener("abort", onAbort);
|
|
147
|
+
resolve(result);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function onExit(): void {
|
|
151
|
+
finish("terminated");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function runChain(): void {
|
|
155
|
+
void (async () => {
|
|
156
|
+
if (tryNativeInterrupt) {
|
|
157
|
+
try {
|
|
158
|
+
await tryNativeInterrupt();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
// 原生中断失败(协议错/管道断)→ 直接落杀链,中断失败不阻断兜底。
|
|
161
|
+
// debug 级留诊断线索:这不是错误终态,只是该引擎优雅中断不可用
|
|
162
|
+
logger.debug(
|
|
163
|
+
`[kill-chain] native interrupt failed, falling back to kill chain: ${
|
|
164
|
+
err instanceof Error ? err.message : String(err)
|
|
165
|
+
}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const stopped = await raceTimeout(waitForExit(child), nativeGraceMs);
|
|
169
|
+
if (stopped === "settled" || settled) return; // 原生中断生效,进程已停
|
|
170
|
+
}
|
|
171
|
+
finish(await killChain(child, { graceMs }));
|
|
172
|
+
})();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function onAbort(): void {
|
|
176
|
+
runChain();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 进程自然退出(含原生中断生效)→ 优雅终止口径。
|
|
180
|
+
// once listener 不显式移除:触发时 finish 已幂等,单 child 单 listener 无堆积。
|
|
181
|
+
child.once("exit", onExit);
|
|
182
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
183
|
+
finish("terminated");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (signal.aborted) runChain();
|
|
188
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── 内部工具 ────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/** 等 child 退出(已退出立即 settle;否则挂一次性 exit listener)。 */
|
|
195
|
+
function waitForExit(child: KillableChild): Promise<void> {
|
|
196
|
+
return new Promise<void>((resolve) => {
|
|
197
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
198
|
+
resolve();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
child.once("exit", () => resolve());
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** promise vs 超时:超时先到返回 'timeout'(promise 继续但被放弃等待)。 */
|
|
206
|
+
function raceTimeout(p: Promise<void>, ms: number): Promise<"settled" | "timeout"> {
|
|
207
|
+
return new Promise<"settled" | "timeout">((resolve) => {
|
|
208
|
+
const timer = setTimeout(() => resolve("timeout"), ms);
|
|
209
|
+
p.then(
|
|
210
|
+
() => {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
resolve("settled");
|
|
213
|
+
},
|
|
214
|
+
() => {
|
|
215
|
+
// 被 await 的 exit promise 不会 reject;防御分支保持语义完整(超时口径胜出前出错按 settled 处理)
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
resolve("settled");
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// src/execution/engine/common/nesting-guard.ts
|
|
2
|
+
//
|
|
3
|
+
// 嵌套防护(P2 公共降级层)。设计权威源:
|
|
4
|
+
// docs/architecture/subagent-engine-abstraction.md D8(嵌套防护双层):
|
|
5
|
+
// 统一 XYZ_AGENT_SUBAGENT=1 标记(所有引擎 spawn 都注入,引擎 adapter 检测到即拒绝
|
|
6
|
+
// 递归派发)+ 剥离各引擎原生标记(CC 的 CLAUDECODE / zsub 的 ZSW_NESTED / pi 的
|
|
7
|
+
// PI_SUBAGENT_*)防继承泄漏——子代理环境的旧标记会让孙代理误判自己已在嵌套层。
|
|
8
|
+
//
|
|
9
|
+
// 为什么 env 标记是唯一跨引擎可靠手段(被否方案见设计):「隔离目录里不装扩展」
|
|
10
|
+
// 依赖配置洁癖,且 opencode/CC 会吃项目级配置;env 由宿主显式控制,随 spawn 必达。
|
|
11
|
+
|
|
12
|
+
import { nestedSpawnRejectedError } from "./errors.ts";
|
|
13
|
+
|
|
14
|
+
/** 统一嵌套标记 env 名(D8)。值恒 '1'。 */
|
|
15
|
+
export const NESTED_SPAWN_ENV = "XYZ_AGENT_SUBAGENT";
|
|
16
|
+
|
|
17
|
+
/** 需剥离的引擎原生嵌套标记(精确名)。 */
|
|
18
|
+
const NATIVE_NESTED_KEYS: readonly string[] = ["CLAUDECODE", "ZSW_NESTED"];
|
|
19
|
+
|
|
20
|
+
/** 需剥离的引擎原生嵌套标记前缀(pi 家族)。 */
|
|
21
|
+
const NATIVE_NESTED_PREFIXES: readonly string[] = ["PI_SUBAGENT_"];
|
|
22
|
+
|
|
23
|
+
/** env 对象形状(NodeJS.ProcessEnv 的结构子集,测试可传普通对象)。 */
|
|
24
|
+
export type SpawnEnv = Record<string, string | undefined>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 构造子代理 spawn env:注入 XYZ_AGENT_SUBAGENT=1 + 剥离引擎原生嵌套标记。
|
|
28
|
+
* 返回新对象,不改入参(spawn env 组装链中的多层 spread 安全)。
|
|
29
|
+
*/
|
|
30
|
+
export function buildNestedSpawnEnv(baseEnv: SpawnEnv): SpawnEnv {
|
|
31
|
+
const env: SpawnEnv = {};
|
|
32
|
+
for (const [key, value] of Object.entries(baseEnv)) {
|
|
33
|
+
if (NATIVE_NESTED_KEYS.includes(key)) continue;
|
|
34
|
+
if (NATIVE_NESTED_PREFIXES.some((prefix) => key.startsWith(prefix))) continue;
|
|
35
|
+
env[key] = value;
|
|
36
|
+
}
|
|
37
|
+
env[NESTED_SPAWN_ENV] = "1";
|
|
38
|
+
return env;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 嵌套 spawn 防护断言:检测到统一标记(本进程已是 subagent)抛
|
|
43
|
+
* EngineError(nested_spawn_rejected)——文案说明防护规则、指向 task 内自行完成。
|
|
44
|
+
* 调用点:subagent 工具入口(进程创建前拒绝,D11 处置三级)。
|
|
45
|
+
*/
|
|
46
|
+
export function assertNotNestedSpawn(env: SpawnEnv): void {
|
|
47
|
+
if (env[NESTED_SPAWN_ENV] === "1") {
|
|
48
|
+
throw nestedSpawnRejectedError();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// src/execution/engine/common/persona-router.ts
|
|
2
|
+
//
|
|
3
|
+
// persona 路由与 argv 预算(P2 公共降级层)。设计权威源:
|
|
4
|
+
// docs/architecture/subagent-engine-abstraction.md D4(persona 路由按 capabilities
|
|
5
|
+
// 选择 file/flag/prompt 三策略,兼作 argv 长度分流)+ D5(preparer 在 spawn 前估算
|
|
6
|
+
// argv 总长度:仅 argv 投递的引擎超长时在 prepare 期报 prompt_too_large,禁止 spawn
|
|
7
|
+
// 后撞 E2BIG 才失败)+ §3.3.3 prompt_too_large 行。
|
|
8
|
+
//
|
|
9
|
+
// 路由产出按 capabilities.personaInjection 三分(中立层只决策通道与内容,具体 flag
|
|
10
|
+
// 组装归各引擎 launcher):
|
|
11
|
+
// - 'file' → fileCandidate(人设全文落盘为文件,argv 只带文件路径——kimi
|
|
12
|
+
// --agent-file 形态);promptSegment 为空(人设不进 prompt)。
|
|
13
|
+
// - 'flag' → promptSegment 为人设全文(launcher 组 --system-prompt 类 flag 直传)。
|
|
14
|
+
// - 'prompt'→ promptSegment 为带引导结构的人设段(拼进最终 prompt——zcode 形态)。
|
|
15
|
+
|
|
16
|
+
import { promptTooLargeError } from "./errors.ts";
|
|
17
|
+
import type { EngineCapabilities, PersonaSpec } from "../types.ts";
|
|
18
|
+
|
|
19
|
+
// ============================================================
|
|
20
|
+
// persona 三策略路由
|
|
21
|
+
// ============================================================
|
|
22
|
+
|
|
23
|
+
/** applyPersona 的返回:按通道产出人设载体。 */
|
|
24
|
+
export interface PersonaRouting {
|
|
25
|
+
/** 进 prompt / flag 通道的人设文本(file 通道为空串)。 */
|
|
26
|
+
promptSegment: string;
|
|
27
|
+
/** file 通道的人设文件落盘候选(suggestedPath 相对池目录,preparer 负责落位)。 */
|
|
28
|
+
fileCandidate?: { suggestedPath: string; content: string };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** file 通道的人设文件建议名(相对池目录——preparer 知道池根,router 不感知)。 */
|
|
32
|
+
const PERSONA_FILE_NAME = "persona.md";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 按 capabilities.personaInjection 路由 persona 的注入通道。
|
|
36
|
+
*
|
|
37
|
+
* router 不解析 agent .md / skill 文件内容(身份解析归宿主 resolveIdentity)——
|
|
38
|
+
* 只把 PersonaSpec 声明的内容组装成通道载体:appendSystemPrompt 是人设正文,
|
|
39
|
+
* agentRef/skillPath 以引用提示行进文本(prompt 通道)或正文头(file 通道)。
|
|
40
|
+
*/
|
|
41
|
+
export function applyPersona(persona: PersonaSpec, capabilities: EngineCapabilities): PersonaRouting {
|
|
42
|
+
switch (capabilities.personaInjection) {
|
|
43
|
+
case "file": {
|
|
44
|
+
const content = buildPersonaBody(persona);
|
|
45
|
+
// 全空 persona 走 file 通道无意义(空文件白占 argv 一段)——降级为无载体
|
|
46
|
+
if (content === "") return { promptSegment: "" };
|
|
47
|
+
return { promptSegment: "", fileCandidate: { suggestedPath: PERSONA_FILE_NAME, content } };
|
|
48
|
+
}
|
|
49
|
+
case "flag":
|
|
50
|
+
// flag 直传:纯正文(launcher 把它作为 --append-system-prompt 类 flag 的值)
|
|
51
|
+
return { promptSegment: buildPersonaBody(persona) };
|
|
52
|
+
case "prompt":
|
|
53
|
+
return { promptSegment: buildPromptSegment(persona) };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 人设正文:appendSystemPrompt join + agentRef/skillPath 头部引用行。 */
|
|
58
|
+
function buildPersonaBody(persona: PersonaSpec): string {
|
|
59
|
+
const parts: string[] = [];
|
|
60
|
+
if (persona.agentRef !== undefined) parts.push(`# Agent: ${persona.agentRef}`);
|
|
61
|
+
if (persona.skillPath !== undefined) parts.push(`Skill context: ${persona.skillPath}`);
|
|
62
|
+
const body = persona.appendSystemPrompt?.join("\n") ?? "";
|
|
63
|
+
if (body !== "") parts.push(body);
|
|
64
|
+
return parts.join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** prompt 通道的人设段(带结构头,拼进最终 prompt)。 */
|
|
68
|
+
function buildPromptSegment(persona: PersonaSpec): string {
|
|
69
|
+
const body = buildPersonaBody(persona);
|
|
70
|
+
if (body === "") return "";
|
|
71
|
+
return `## Persona\n${body}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ============================================================
|
|
75
|
+
// argv 长度预算(prepare 期前置拦截)
|
|
76
|
+
// ============================================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* argv 预算默认上限(128KB)。依据:POSIX ARG_MAX 通常 ≥ 256KB(Linux 单字符串
|
|
80
|
+
* MAX_ARG_STRLEN 128KB 是更紧的硬顶)——预算取 128KB 留出参数个数与 env 余量,
|
|
81
|
+
* spawn 前拦截,绝不 spawn 后撞 E2BIG(D5)。可经 assertArgvBudget 第二参覆盖。
|
|
82
|
+
*/
|
|
83
|
+
export const DEFAULT_ARGV_BUDGET_BYTES =
|
|
84
|
+
// eslint-disable-next-line no-magic-numbers -- 128KB = 128 * 1024 bytes 预算换算常数
|
|
85
|
+
128 * 1024;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 估算 argv 总字节数:各参数 UTF-8 字节数之和 + 每参数一个 NUL 分隔符。
|
|
89
|
+
* execve 按字节计算参数区,估算口径与内核一致(字符数会低估多字节文本)。
|
|
90
|
+
*/
|
|
91
|
+
export function estimateArgvBytes(argv: string[]): number {
|
|
92
|
+
let total = 0;
|
|
93
|
+
for (const arg of argv) {
|
|
94
|
+
total += Buffer.byteLength(arg, "utf8") + 1;
|
|
95
|
+
}
|
|
96
|
+
return total;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* argv 预算断言:估算超限抛 EngineError(prompt_too_large)(恢复指引三条:缩短 task /
|
|
101
|
+
* persona 移 file 通道 / 换 stdin 引擎)。调用点在 preparer(进程创建前,D5)。
|
|
102
|
+
*/
|
|
103
|
+
export function assertArgvBudget(argv: string[], limitBytes: number = DEFAULT_ARGV_BUDGET_BYTES): void {
|
|
104
|
+
const actual = estimateArgvBytes(argv);
|
|
105
|
+
if (actual > limitBytes) {
|
|
106
|
+
throw promptTooLargeError(actual, limitBytes);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// src/execution/engine/common/pool-manager.ts
|
|
2
|
+
//
|
|
3
|
+
// 隔离目录池管理(P2 公共降级层)。设计权威源:
|
|
4
|
+
// docs/architecture/subagent-engine-abstraction.md D5(隔离目录池化保留,随 record
|
|
5
|
+
// 生命周期回收,清理只做到池粒度)+ §3.3.9(目录布局 / refs 方案)。
|
|
6
|
+
//
|
|
7
|
+
// P2 口径(与 §3.3.9 完整设计的差异,签名所限):引用计数是**进程内注册表**——
|
|
8
|
+
// acquire/release 的任务书签名无 taskId,refs.json 文件形态(登记 taskId + ts)留给
|
|
9
|
+
// 后续 wave 在 preparer/host 接线时落地(宿主是唯一写者,升级为文件形态不改变本模块
|
|
10
|
+
// 的 acquire/release 语义边界)。
|
|
11
|
+
//
|
|
12
|
+
// 删除边界(D5 三条硬规则):
|
|
13
|
+
// 1. 只删引擎原生状态(隔离 HOME / config / db.sqlite,均可由 preparer 重建);
|
|
14
|
+
// 2. journal-*.jsonl 不随池删——生命周期跟随 record,避免「池删导致仍存 record 的
|
|
15
|
+
// 历史从②级静默跌③级」;
|
|
16
|
+
// 3. 删除失败置 .pool-cleanup-failed 标记文件(可观测不静默),启动期扫描该标记告警。
|
|
17
|
+
|
|
18
|
+
import { mkdir, readdir, rm, rmdir, writeFile } from "node:fs/promises";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
import { getLogger } from "../../../core/logger.ts";
|
|
22
|
+
|
|
23
|
+
import { resolvePoolDir } from "../paths.ts";
|
|
24
|
+
|
|
25
|
+
const logger = getLogger("subagents");
|
|
26
|
+
|
|
27
|
+
/** 清理失败标记文件名(置于池目录内;内容为 JSON 失败清单)。 */
|
|
28
|
+
export const POOL_CLEANUP_FAILED_MARKER = ".pool-cleanup-failed";
|
|
29
|
+
|
|
30
|
+
/** readdir withFileTypes 的条目结构子集。 */
|
|
31
|
+
interface DirEntryLike {
|
|
32
|
+
name: string;
|
|
33
|
+
isDirectory(): boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 池管理的文件系统依赖面(结构接口:测试注入 fake,免 vi.mock 整个 fs 模块)。 */
|
|
37
|
+
export interface PoolFsDeps {
|
|
38
|
+
mkdir(path: string, opts: { recursive: boolean }): Promise<unknown>;
|
|
39
|
+
readdir(path: string): Promise<DirEntryLike[]>;
|
|
40
|
+
rm(path: string, opts: { recursive: boolean; force: boolean }): Promise<void>;
|
|
41
|
+
rmdir(path: string): Promise<void>;
|
|
42
|
+
writeFile(path: string, data: string): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================
|
|
46
|
+
// 进程内池注册表(引用计数)
|
|
47
|
+
// ============================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* key = 池目录绝对路径 → 活跃引用计数。进程级单例状态,用 globalThis[Symbol.for]
|
|
51
|
+
* 持有防 jiti 双路径加载分裂(development-guide §7.5),不用模块级 const。
|
|
52
|
+
*/
|
|
53
|
+
const POOL_REFS_SLOT_KEY = Symbol.for("@zhushanwen/pi-subagent-workflow.poolRefs");
|
|
54
|
+
|
|
55
|
+
function getPoolRefs(): Map<string, number> {
|
|
56
|
+
// globalThis 无 symbol 索引签名,但运行时支持 symbol 键——用 Reflect 安全读写,
|
|
57
|
+
// 避免双重断言(同 model-config-service.ts 先例)。
|
|
58
|
+
let refs = Reflect.get(globalThis, POOL_REFS_SLOT_KEY) as Map<string, number> | undefined;
|
|
59
|
+
if (!refs) {
|
|
60
|
+
refs = new Map();
|
|
61
|
+
Reflect.set(globalThis, POOL_REFS_SLOT_KEY, refs);
|
|
62
|
+
}
|
|
63
|
+
return refs;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 测试隔离专用:清空进程内注册表(生产禁用——会丢失活跃引用计数)。 */
|
|
67
|
+
export function resetPoolRegistryForTests(): void {
|
|
68
|
+
getPoolRefs().clear();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================
|
|
72
|
+
// acquire / release
|
|
73
|
+
// ============================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 获取池目录:mkdir -p(幂等)+ 进程内引用计数 +1,返回池目录绝对路径。
|
|
77
|
+
* 路径一律经 paths.ts resolvePoolDir 派生(双端同源,禁自拼)。
|
|
78
|
+
*/
|
|
79
|
+
export async function acquirePool(
|
|
80
|
+
dataDir: string,
|
|
81
|
+
engineId: string,
|
|
82
|
+
poolKey: string,
|
|
83
|
+
fs: Pick<PoolFsDeps, "mkdir"> = nodeFs,
|
|
84
|
+
): Promise<string> {
|
|
85
|
+
const poolDir = resolvePoolDir(dataDir, engineId, poolKey);
|
|
86
|
+
await fs.mkdir(poolDir, { recursive: true });
|
|
87
|
+
const poolRefs = getPoolRefs();
|
|
88
|
+
poolRefs.set(poolDir, (poolRefs.get(poolDir) ?? 0) + 1);
|
|
89
|
+
return poolDir;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 释放一次池引用(record GC / 删除时调用):计数递减;归零删整池——只删引擎原生
|
|
94
|
+
* 状态,journal-*.jsonl 保留(D5);删除失败置标记文件并 warn,不静默不 throw
|
|
95
|
+
* (清理失败不是 record GC 的失败)。
|
|
96
|
+
*/
|
|
97
|
+
export async function releasePoolRef(
|
|
98
|
+
dataDir: string,
|
|
99
|
+
engineId: string,
|
|
100
|
+
poolKey: string,
|
|
101
|
+
fs: PoolFsDeps = nodeFs,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const poolDir = resolvePoolDir(dataDir, engineId, poolKey);
|
|
104
|
+
const poolRefs = getPoolRefs();
|
|
105
|
+
const current = poolRefs.get(poolDir);
|
|
106
|
+
// 无计数(进程重启后释放 / 未 acquire 的释放)→ 保守不删池:删池决策必须有本进程
|
|
107
|
+
// 内的归零证据,凭空删除可能误删其他进程正在使用的池
|
|
108
|
+
if (current === undefined || current <= 0) {
|
|
109
|
+
poolRefs.delete(poolDir);
|
|
110
|
+
logger.debug(`[pool-manager] release without live ref, skip pool deletion: ${poolDir}`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (current > 1) {
|
|
114
|
+
poolRefs.set(poolDir, current - 1);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// 归零:删引擎原生状态(journal 保留)
|
|
118
|
+
poolRefs.delete(poolDir);
|
|
119
|
+
await deletePoolNativeState(poolDir, fs);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** journal 文件名判定(与 paths.ts 的 journal-<taskId>.jsonl 命名约定一致)。 */
|
|
123
|
+
function isJournalFile(name: string): boolean {
|
|
124
|
+
return name.startsWith("journal-") && name.endsWith(".jsonl");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 删池内引擎原生状态:逐条目删除(跳过 journal),目录清空后移除目录本身。 */
|
|
128
|
+
async function deletePoolNativeState(poolDir: string, fs: PoolFsDeps): Promise<void> {
|
|
129
|
+
const failures: string[] = [];
|
|
130
|
+
let entries: DirEntryLike[];
|
|
131
|
+
try {
|
|
132
|
+
entries = await fs.readdir(poolDir);
|
|
133
|
+
} catch {
|
|
134
|
+
// 池目录不存在(从未创建 / 已删)= 无原生状态可清,视为成功
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (isJournalFile(entry.name)) continue;
|
|
139
|
+
try {
|
|
140
|
+
await fs.rm(join(poolDir, entry.name), { recursive: true, force: true });
|
|
141
|
+
} catch (err) {
|
|
142
|
+
failures.push(`${entry.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (failures.length > 0) {
|
|
146
|
+
await markCleanupFailed(poolDir, failures, fs);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
// 目录内只剩 journal(或已空):只剩 journal 时保留目录(journal 生命周期跟 record)
|
|
151
|
+
const remaining = await fs.readdir(poolDir);
|
|
152
|
+
if (remaining.every((e) => isJournalFile(e.name))) return;
|
|
153
|
+
await fs.rmdir(poolDir);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
await markCleanupFailed(
|
|
156
|
+
poolDir,
|
|
157
|
+
[`(rmdir): ${err instanceof Error ? err.message : String(err)}`],
|
|
158
|
+
fs,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 置 .pool-cleanup-failed 标记(失败清单 JSON)+ warn——可观测不静默(D5)。 */
|
|
164
|
+
async function markCleanupFailed(poolDir: string, failures: string[], fs: PoolFsDeps): Promise<void> {
|
|
165
|
+
const marker = join(poolDir, POOL_CLEANUP_FAILED_MARKER);
|
|
166
|
+
const payload = JSON.stringify({ ts: Date.now(), failures });
|
|
167
|
+
logger.warn(
|
|
168
|
+
`[pool-manager] pool cleanup failed for ${poolDir} (${failures.length} item(s)); ` +
|
|
169
|
+
`marker written to ${marker} — re-run cleanup after fixing the underlying error`,
|
|
170
|
+
);
|
|
171
|
+
try {
|
|
172
|
+
await fs.writeFile(marker, `${payload}\n`);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
// 标记本身也写不进(目录只读/磁盘满)——error 级留痕是最后防线,不再上抛
|
|
175
|
+
logger.warn(
|
|
176
|
+
`[pool-manager] failed to write cleanup-failed marker ${marker}: ` +
|
|
177
|
+
`${err instanceof Error ? err.message : String(err)}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================================
|
|
183
|
+
// 单次性产物清理(spawnedFiles)
|
|
184
|
+
// ============================================================
|
|
185
|
+
|
|
186
|
+
/** cleanupSpawnedFiles 选项。 */
|
|
187
|
+
export interface SpawnedFilesCleanupOpts {
|
|
188
|
+
/**
|
|
189
|
+
* resume 场景保留:true = 全部保留(no-op)。resume 续接原 session,prompt 文件
|
|
190
|
+
* 等单次性产物不再重写(§3.3.7 preparer resume 语义),清理留给 record 终局。
|
|
191
|
+
*/
|
|
192
|
+
keepForResume: boolean;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 清理单次性产物(preparer 的 spawnedFiles:临时 prompt / persona 文件)。
|
|
197
|
+
* 任务结束即清理(D5);单条失败收集 warn 不 throw(部分清理优于整体失败),ENOENT
|
|
198
|
+
* 幂等忽略。
|
|
199
|
+
*/
|
|
200
|
+
export async function cleanupSpawnedFiles(
|
|
201
|
+
paths: string[],
|
|
202
|
+
opts: SpawnedFilesCleanupOpts,
|
|
203
|
+
fs: Pick<PoolFsDeps, "rm"> = nodeFs,
|
|
204
|
+
): Promise<void> {
|
|
205
|
+
if (opts.keepForResume) return;
|
|
206
|
+
for (const p of paths) {
|
|
207
|
+
try {
|
|
208
|
+
await fs.rm(p, { recursive: true, force: true });
|
|
209
|
+
} catch (err) {
|
|
210
|
+
logger.warn(
|
|
211
|
+
`[pool-manager] spawned file cleanup failed for ${p}: ` +
|
|
212
|
+
`${err instanceof Error ? err.message : String(err)}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── 默认 fs 实现 ────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
const nodeFs: PoolFsDeps = {
|
|
221
|
+
mkdir: (p, o) => mkdir(p, o),
|
|
222
|
+
readdir: (p) => readdir(p, { withFileTypes: true }) as Promise<DirEntryLike[]>,
|
|
223
|
+
rm: (p, o) => rm(p, o),
|
|
224
|
+
rmdir: (p) => rmdir(p),
|
|
225
|
+
writeFile: (p, d) => writeFile(p, d, "utf8"),
|
|
226
|
+
};
|