@zhushanwen/subagent-core 0.2.0 → 0.4.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 +45 -2
- package/agents/analyst.md +60 -0
- package/agents/coder.md +69 -0
- package/agents/debugger.md +66 -0
- package/agents/doc-reviewer.md +49 -0
- package/agents/explorer.md +63 -0
- package/agents/general-purpose.md +32 -0
- package/agents/orchestrator.md +61 -0
- package/agents/planner.md +53 -0
- package/agents/researcher.md +67 -0
- package/agents/reviewer.md +73 -0
- package/dist/chunk-4KN4TTG7.js +240 -0
- package/dist/chunk-APZY4IME.js +27 -0
- package/dist/chunk-X7SZ5HLQ.js +66 -0
- package/dist/execution/engine/engines/zcode/constants.cjs +65 -0
- package/dist/execution/engine/engines/zcode/constants.d.cts +93 -1
- package/dist/execution/engine/engines/zcode/constants.d.ts +93 -1
- package/dist/execution/engine/engines/zcode/constants.js +43 -1
- package/dist/execution/engine/engines/zcode/reader.d.cts +1 -1
- package/dist/execution/engine/engines/zcode/reader.d.ts +1 -1
- package/dist/execution/engine/engines/zcode/reader.js +4 -234
- package/dist/execution/engine/paths.js +7 -19
- package/dist/index.cjs +14709 -100
- package/dist/index.d.cts +4053 -200
- package/dist/index.d.ts +4053 -200
- package/dist/index.js +14358 -102
- package/dist/{types-BxyAidGf.d.cts → types-DpUO16pj.d.cts} +464 -1
- package/dist/{types-BxyAidGf.d.ts → types-DpUO16pj.d.ts} +464 -1
- package/package.json +6 -3
- package/src/__tests__/review-fix-loop-script.test.ts +90 -2
- package/src/__tests__/review-fix-loop-utils.test.ts +21 -0
- package/src/__tests__/smoke.test.ts +9 -2
- package/src/execution/__tests__/agent-profile.test.ts +232 -0
- package/src/execution/__tests__/agents-assembly.test.ts +219 -0
- package/src/execution/__tests__/chat-engine-routing.test.ts +71 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +1 -0
- package/src/execution/__tests__/create-concurrency-pool.test.ts +194 -0
- package/src/execution/__tests__/delivery-methods.test.ts +2 -0
- package/src/execution/__tests__/descendant-sweep.test.ts +269 -0
- package/src/execution/__tests__/dialog-queue.test.ts +199 -1
- package/src/execution/__tests__/epipe-fallback.test.ts +2 -0
- package/src/execution/__tests__/execution-runtime-face.test.ts +272 -0
- package/src/execution/__tests__/finalize-record.test.ts +106 -0
- package/src/execution/__tests__/gc-timer.test.ts +2 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +2 -0
- package/src/execution/__tests__/get-state-handshake.test.ts +127 -0
- package/src/execution/__tests__/keep-alive-no-progress.test.ts +378 -0
- package/src/execution/__tests__/kill-all-escalation.test.ts +196 -0
- package/src/execution/__tests__/lifecycle-manager-idle-timer-identity.test.ts +117 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +39 -0
- package/src/execution/__tests__/manifest-store-tmp-recovery.test.ts +99 -0
- package/src/execution/__tests__/max-turns-to-watchdog-ms.test.ts +113 -0
- package/src/execution/__tests__/notify-ledger.test.ts +294 -0
- package/src/execution/__tests__/record-store-orphan-revive.test.ts +159 -0
- package/src/execution/__tests__/record-store.test.ts +83 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +4 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +1 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +407 -17
- package/src/execution/__tests__/run-spawn-integration.test.ts +1 -1
- package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +12 -4
- package/src/execution/__tests__/service-kill-escalation.test.ts +89 -0
- package/src/execution/__tests__/session-pending.test.ts +228 -0
- package/src/execution/__tests__/session-runner-branch-cache-lru.test.ts +110 -0
- package/src/execution/__tests__/session-runner-close-prune.test.ts +181 -0
- package/src/execution/__tests__/session-runner-epipe.test.ts +1 -0
- package/src/execution/__tests__/session-runner-heartbeat-idle-fallback.test.ts +226 -0
- package/src/execution/__tests__/settled-watchdog.test.ts +289 -0
- package/src/execution/__tests__/spawned-children.test.ts +153 -10
- package/src/execution/__tests__/subagent-actions-core.test.ts +997 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +71 -10
- package/src/execution/__tests__/subagent-service-multiproc-guard.test.ts +174 -0
- package/src/execution/__tests__/subagent-service-notify-gate.test.ts +266 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +3 -0
- package/src/execution/__tests__/subagent-service-recovery-bounds.test.ts +316 -0
- package/src/execution/__tests__/timeout-integration.test.ts +7 -7
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +95 -1
- package/src/execution/__tests__/ui-request-queue.test.ts +25 -0
- package/src/execution/__tests__/worktree-git-ops.test.ts +571 -0
- package/src/execution/__tests__/worktree-reconcile-aging.test.ts +204 -0
- package/src/execution/agent-registry.ts +225 -1
- package/src/execution/agents-assembly.ts +88 -0
- package/src/execution/concurrency-pool.ts +71 -9
- package/src/execution/dialog-queue.ts +101 -3
- package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +49 -2
- package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +87 -1
- package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +26 -0
- package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +83 -1
- package/src/execution/engine/__tests__/conformance/zcode-appserver-harness.ts +131 -0
- package/src/execution/engine/__tests__/registry.test.ts +90 -1
- package/src/execution/engine/engine-discovery.ts +12 -16
- package/src/execution/engine/engines/pi/task-spec-mapper.ts +3 -3
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/fake-appserver.mjs +274 -0
- package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-appserver.json +36 -0
- package/src/execution/engine/engines/zcode/__tests__/connection.test.ts +472 -0
- package/src/execution/engine/engines/zcode/__tests__/preparer-appserver.test.ts +387 -0
- package/src/execution/engine/engines/zcode/__tests__/session-channel.test.ts +780 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-appserver.test.ts +815 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine-degrade.test.ts +462 -0
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +119 -1
- package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +136 -6
- package/src/execution/engine/engines/zcode/appserver-home.ts +442 -0
- package/src/execution/engine/engines/zcode/appserver-probe.ts +141 -0
- package/src/execution/engine/engines/zcode/connection.ts +585 -0
- package/src/execution/engine/engines/zcode/constants.ts +129 -0
- package/src/execution/engine/engines/zcode/golden-sample.ts +54 -8
- package/src/execution/engine/engines/zcode/preparer.ts +25 -22
- package/src/execution/engine/engines/zcode/session-channel.ts +656 -0
- package/src/execution/engine/engines/zcode/zcode-engine.ts +909 -41
- package/src/execution/engine/host-task-spec.ts +3 -3
- package/src/execution/engine/port.ts +41 -1
- package/src/execution/engine/registry.ts +53 -0
- package/src/execution/finalize-record.ts +63 -5
- package/src/execution/get-state-handshake.ts +85 -10
- package/src/execution/lifecycle-manager.ts +27 -3
- package/src/execution/manifest-store.ts +53 -62
- package/src/execution/notifier.ts +10 -10
- package/src/execution/notify-ledger.ts +117 -16
- package/src/execution/record-entry.ts +8 -2
- package/src/execution/record-store.ts +29 -15
- package/src/execution/session-pending.ts +213 -62
- package/src/execution/session-runner.ts +974 -128
- package/src/execution/sessions-index.ts +10 -55
- package/src/execution/settled-watchdog.ts +99 -0
- package/src/execution/subagent-actions-core.ts +686 -0
- package/src/execution/subagent-service.ts +396 -49
- package/src/execution/ui-request-handler-factory.ts +27 -4
- package/src/execution/worktree-git-ops.ts +397 -0
- package/src/execution/worktree-manager.ts +72 -10
- package/src/execution/worktree-registry.ts +6 -12
- package/src/index.ts +413 -6
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +42 -3
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +2 -4
- package/src/orchestration/__tests__/args-meta.test.ts +358 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +1 -8
- package/src/orchestration/__tests__/error-recovery-rebuild-failure.test.ts +312 -0
- package/src/orchestration/__tests__/error-recovery-terminal-hardening.test.ts +382 -0
- package/src/orchestration/__tests__/file-run-store-prune.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store-throttle.test.ts +168 -0
- package/src/orchestration/__tests__/file-run-store.test.ts +389 -0
- package/src/orchestration/__tests__/helpers/flush-microtasks.ts +13 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +34 -0
- package/src/orchestration/__tests__/lifecycle-abort-broadcast-signal.test.ts +357 -0
- package/src/orchestration/__tests__/lifecycle-recover-crashed.test.ts +289 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +77 -0
- package/src/orchestration/__tests__/run-snapshot.test.ts +353 -0
- package/src/orchestration/__tests__/script-generate.test.ts +314 -0
- package/src/orchestration/__tests__/worker-pending-timeout-abort.test.ts +275 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +78 -1
- package/src/orchestration/__tests__/workflow-files.test.ts +186 -0
- package/src/orchestration/__tests__/workflow-run-summary.test.ts +119 -0
- package/src/orchestration/__tests__/workflow-script-registry-impl.test.ts +124 -0
- package/src/orchestration/agent-opts-resolver.ts +7 -7
- package/src/orchestration/args-meta.ts +198 -0
- package/src/orchestration/error-recovery.ts +416 -146
- package/src/orchestration/execute-agent-call.ts +9 -9
- package/src/orchestration/file-run-store.ts +327 -0
- package/src/orchestration/launcher.ts +35 -26
- package/src/orchestration/lifecycle.ts +355 -79
- package/src/orchestration/models/agent-call.ts +7 -7
- package/src/orchestration/models/budget.ts +5 -5
- package/src/orchestration/models/run-runtime.ts +13 -13
- package/src/orchestration/models/trace.ts +8 -8
- package/src/orchestration/models/workflow-run.ts +27 -27
- package/src/orchestration/models/workflow-script.ts +4 -4
- package/src/orchestration/run-snapshot.ts +266 -0
- package/src/orchestration/script-generate.ts +154 -0
- package/src/orchestration/script-lint.ts +33 -33
- package/src/orchestration/worker-handle.ts +10 -10
- package/src/orchestration/worker-host.ts +10 -10
- package/src/orchestration/worker-script-builder.ts +401 -346
- package/src/orchestration/workflow-files.ts +37 -11
- package/src/orchestration/workflow-run-summary.ts +69 -0
- package/src/orchestration/workflow-script-registry-impl.ts +31 -9
- package/src/shared/__tests__/agent-ref.test.ts +209 -2
- package/src/shared/__tests__/atomic-write.test.ts +267 -0
- package/src/shared/__tests__/bounded-serialize.test.ts +236 -0
- package/src/shared/__tests__/injection-render.test.ts +518 -0
- package/src/shared/__tests__/resource-discovery-host-roots.test.ts +474 -0
- package/src/shared/__tests__/resource-discovery.test.ts +3 -2
- package/src/shared/agent-ref.ts +143 -2
- package/src/shared/atomic-write.ts +320 -0
- package/src/shared/bounded-serialize.ts +154 -0
- package/src/shared/injection-render.ts +279 -0
- package/src/shared/meta-parser.ts +41 -1
- package/src/shared/resource-discovery.ts +104 -37
- package/src/shared/resource-meta.ts +14 -0
- package/src/shared/xml-injection.ts +9 -9
- package/workflows/README.md +9 -9
- package/workflows/chain.js +4 -2
- package/workflows/map-reduce.js +5 -3
- package/workflows/parallel.js +5 -3
- package/workflows/review-fix-loop-utils.cjs +14 -3
- package/workflows/review-fix-loop.js +18 -7
- package/workflows/scatter-gather.js +4 -2
- package/dist/chunk-3VOERJPJ.js +0 -22
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// worktree-reconcile-aging.test.ts —— 对账「歧义跳过」老化升级(PS-12 措施⑤)。
|
|
2
|
+
//
|
|
3
|
+
// 环境模式对齐 worktree-reconcile.integration.test.ts:真实 git repo + TMPDIR
|
|
4
|
+
// 重定向到测试私有目录(物理面扫描根 os.tmpdir()/pi-subagents 跟随 TMPDIR);
|
|
5
|
+
// 本文件额外 mock core logger 以断言升级 warn(消息含磁盘路径与人工清理指引)。
|
|
6
|
+
//
|
|
7
|
+
// 语义锁定:
|
|
8
|
+
// - 阈值前(连续 N-1 周期内):保持既有低信息聚合 warn,无升级、不清理(宁延迟勿误删);
|
|
9
|
+
// - 第 N 周期:升级 warn——消息含 checkout 磁盘路径 + git worktree list 查看、
|
|
10
|
+
// worktree remove --force / branch -D 手动清理、无主残留 rm -rf 指引;
|
|
11
|
+
// 结构化字段带 branch/checkout/repo/skippedCycles;升级是提醒不是动作(资源不动);
|
|
12
|
+
// - 重置语义:路径「出现对应」(自愈补写)后计数清零,重现歧义需重新连续 N 周期。
|
|
13
|
+
|
|
14
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as os from "node:os";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
|
|
19
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
20
|
+
|
|
21
|
+
const { loggerMock } = vi.hoisted(() => ({
|
|
22
|
+
loggerMock: {
|
|
23
|
+
debug: vi.fn(),
|
|
24
|
+
warn: vi.fn(),
|
|
25
|
+
error: vi.fn(),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
vi.mock("../../core/logger.ts", () => ({ getLogger: () => loggerMock }));
|
|
29
|
+
|
|
30
|
+
import { encodeCwd } from "../path-encoding.ts";
|
|
31
|
+
import { WorktreeRegistry } from "../worktree-registry.ts";
|
|
32
|
+
import { RECONCILE_SKIP_ESCALATION_CYCLES, WorktreeManager } from "../worktree-manager.ts";
|
|
33
|
+
|
|
34
|
+
/** 原始 TMPDIR(beforeEach 重定向、afterEach 还原)。 */
|
|
35
|
+
const ORIG_TMPDIR = os.tmpdir();
|
|
36
|
+
|
|
37
|
+
/** git 辅助:repo 内执行(输出 trim)。 */
|
|
38
|
+
function git(repo: string, ...args: string[]): string {
|
|
39
|
+
return execFileSync("git", ["-C", repo, ...args], { encoding: "utf-8" }).trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 升级 warn 调用(按消息锚文本过滤)。 */
|
|
43
|
+
function escalationCalls(): Array<{ msg: string; data: Record<string, unknown> }> {
|
|
44
|
+
return loggerMock.warn.mock.calls
|
|
45
|
+
.filter((c) => typeof c[0] === "string" && c[0].includes("manual cleanup may be needed"))
|
|
46
|
+
.map((c) => ({ msg: c[0] as string, data: (c[1] ?? {}) as Record<string, unknown> }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("WorktreeManager 对账歧义跳过老化(PS-12 措施⑤)", { timeout: 30_000 }, () => {
|
|
50
|
+
let outerDir: string;
|
|
51
|
+
let agentDir: string;
|
|
52
|
+
let repo: string;
|
|
53
|
+
let enc: string;
|
|
54
|
+
let registry: WorktreeRegistry;
|
|
55
|
+
let mgr: WorktreeManager;
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
outerDir = fs.mkdtempSync(path.join(ORIG_TMPDIR, "wt-reconcile-aging-"));
|
|
59
|
+
process.env.TMPDIR = outerDir;
|
|
60
|
+
|
|
61
|
+
agentDir = path.join(outerDir, "agent");
|
|
62
|
+
repo = path.join(outerDir, "repo");
|
|
63
|
+
fs.mkdirSync(repo, { recursive: true });
|
|
64
|
+
git(repo, "init", "-q");
|
|
65
|
+
git(repo, "config", "user.email", "test@test.local");
|
|
66
|
+
git(repo, "config", "user.name", "test");
|
|
67
|
+
fs.writeFileSync(path.join(repo, "a.txt"), "init\n", "utf-8");
|
|
68
|
+
git(repo, "add", "-A");
|
|
69
|
+
git(repo, "commit", "-q", "-m", "init");
|
|
70
|
+
|
|
71
|
+
enc = encodeCwd(repo);
|
|
72
|
+
registry = new WorktreeRegistry(agentDir);
|
|
73
|
+
mgr = new WorktreeManager(agentDir);
|
|
74
|
+
loggerMock.warn.mockClear();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
process.env.TMPDIR = ORIG_TMPDIR;
|
|
79
|
+
fs.rmSync(outerDir, { recursive: true, force: true });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/** 建一个物理 worktree(绕过 WorktreeManager.create 的脏树校验,路径布局同 create)。 */
|
|
83
|
+
function makePhysicalWorktree(branch: string): string {
|
|
84
|
+
const checkout = path.join(outerDir, "pi-subagents", enc, branch);
|
|
85
|
+
git(repo, "worktree", "add", "-q", "-b", branch, checkout, "HEAD");
|
|
86
|
+
return checkout;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 在 <agentDir>/subagents/<enc>/sessions/ 下写一个 .alive marker。 */
|
|
90
|
+
function writeAliveMarker(sessionId: string, pid: number): void {
|
|
91
|
+
const sessions = path.join(agentDir, "subagents", enc, "sessions");
|
|
92
|
+
fs.mkdirSync(sessions, { recursive: true });
|
|
93
|
+
fs.writeFileSync(
|
|
94
|
+
path.join(sessions, `${sessionId}.jsonl.alive`),
|
|
95
|
+
JSON.stringify({ pid, id: sessionId, startedAt: Date.now() }),
|
|
96
|
+
"utf-8",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 真实子进程等待:等 pid 死透(isProcessAlive 翻 false,SIGKILL 后的短暂窗口)。 */
|
|
101
|
+
async function waitPidDead(pid: number): Promise<void> {
|
|
102
|
+
for (let i = 0; i < 50; i++) {
|
|
103
|
+
try {
|
|
104
|
+
process.kill(pid, 0);
|
|
105
|
+
} catch {
|
|
106
|
+
return; // ESRCH = 已死
|
|
107
|
+
}
|
|
108
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
it("阈值前保持普通 warn,第 N 周期升级含磁盘路径与清理指引(资源不动)", async () => {
|
|
113
|
+
const c1 = makePhysicalWorktree("pi-sub-a1");
|
|
114
|
+
const c2 = makePhysicalWorktree("pi-sub-a2");
|
|
115
|
+
const sleeper = spawn("sleep", ["60"]);
|
|
116
|
+
try {
|
|
117
|
+
expect(sleeper.pid).toBeTruthy();
|
|
118
|
+
writeAliveMarker("s-a1", process.pid);
|
|
119
|
+
writeAliveMarker("s-a2", sleeper.pid as number);
|
|
120
|
+
|
|
121
|
+
// 阈值前(N-1 周期):无升级消息,资源原样(宁延迟勿误删语义不变)
|
|
122
|
+
for (let i = 0; i < RECONCILE_SKIP_ESCALATION_CYCLES - 1; i++) {
|
|
123
|
+
await mgr.scan();
|
|
124
|
+
}
|
|
125
|
+
expect(escalationCalls()).toEqual([]);
|
|
126
|
+
expect(fs.existsSync(c1)).toBe(true);
|
|
127
|
+
expect(fs.existsSync(c2)).toBe(true);
|
|
128
|
+
|
|
129
|
+
// 第 N 周期:升级 warn——每个残留路径一条,含路径与人工清理指引
|
|
130
|
+
await mgr.scan();
|
|
131
|
+
const calls = escalationCalls();
|
|
132
|
+
expect(calls.length).toBe(2); // c1 / c2 各一条
|
|
133
|
+
for (const checkout of [c1, c2]) {
|
|
134
|
+
const hit = calls.find((c) => c.msg.includes(checkout));
|
|
135
|
+
expect(hit).toBeDefined();
|
|
136
|
+
expect(hit?.msg).toContain("worktree list");
|
|
137
|
+
expect(hit?.msg).toContain("worktree remove --force");
|
|
138
|
+
expect(hit?.msg).toContain("branch -D");
|
|
139
|
+
expect(hit?.msg).toContain("rm -rf");
|
|
140
|
+
expect(hit?.msg).toContain(`skipped for ${RECONCILE_SKIP_ESCALATION_CYCLES} consecutive cycles`);
|
|
141
|
+
}
|
|
142
|
+
// 结构化字段:branch/checkout/skippedCycles 可机读
|
|
143
|
+
// (repo 是 git .git 指针解析产物,macOS 上为 realpath 形态 /private/var/...)
|
|
144
|
+
const data1 = calls.find((c) => c.data.checkout === c1)?.data;
|
|
145
|
+
expect(data1?.branch).toBe("pi-sub-a1");
|
|
146
|
+
expect(data1?.repo).toBe(fs.realpathSync(repo));
|
|
147
|
+
expect(data1?.skippedCycles).toBe(RECONCILE_SKIP_ESCALATION_CYCLES);
|
|
148
|
+
|
|
149
|
+
// 升级 warn 是提醒不是清理动作:物理资源与注册表未动
|
|
150
|
+
expect(fs.existsSync(c1)).toBe(true);
|
|
151
|
+
expect(fs.existsSync(c2)).toBe(true);
|
|
152
|
+
expect(registry.load()).toEqual([]);
|
|
153
|
+
} finally {
|
|
154
|
+
sleeper.kill("SIGKILL");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("重置语义:自愈补写(出现对应)后计数清零,重现歧义需重新连续 N 周期", async () => {
|
|
159
|
+
const c1 = makePhysicalWorktree("pi-sub-r1");
|
|
160
|
+
const c2 = makePhysicalWorktree("pi-sub-r2");
|
|
161
|
+
const sleeper = spawn("sleep", ["60"]);
|
|
162
|
+
try {
|
|
163
|
+
writeAliveMarker("s-r1", process.pid);
|
|
164
|
+
writeAliveMarker("s-r2", sleeper.pid as number);
|
|
165
|
+
|
|
166
|
+
// 累计 N-1 周期歧义(未达阈值)
|
|
167
|
+
for (let i = 0; i < RECONCILE_SKIP_ESCALATION_CYCLES - 1; i++) {
|
|
168
|
+
await mgr.scan();
|
|
169
|
+
}
|
|
170
|
+
expect(escalationCalls()).toEqual([]);
|
|
171
|
+
} finally {
|
|
172
|
+
sleeper.kill("SIGKILL");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 出现对应:s-r2 pid 死 + c2 删除 → 1 活 pid ↔ 1 残留 → 自愈补写 c1
|
|
176
|
+
await waitPidDead(sleeper.pid as number);
|
|
177
|
+
fs.rmSync(c2, { recursive: true, force: true });
|
|
178
|
+
await mgr.scan();
|
|
179
|
+
expect(registry.load().map((e) => e.branch)).toEqual(["pi-sub-r1"]);
|
|
180
|
+
expect(escalationCalls()).toEqual([]);
|
|
181
|
+
|
|
182
|
+
// 重现歧义:c1 退出注册表 + 新残留 c3 + 新活 pid。
|
|
183
|
+
// c1 的历史歧义计数已被自愈轮清零——若未清零(陈账 N-1),下面第 1 轮就达
|
|
184
|
+
// 阈值升级;断言 N-1 轮内无升级即证明重置生效。
|
|
185
|
+
await registry.remove("pi-sub-r1");
|
|
186
|
+
const c3 = makePhysicalWorktree("pi-sub-r3");
|
|
187
|
+
const sleeper2 = spawn("sleep", ["60"]);
|
|
188
|
+
try {
|
|
189
|
+
writeAliveMarker("s-r3", sleeper2.pid as number);
|
|
190
|
+
for (let i = 0; i < RECONCILE_SKIP_ESCALATION_CYCLES - 1; i++) {
|
|
191
|
+
await mgr.scan();
|
|
192
|
+
}
|
|
193
|
+
expect(escalationCalls()).toEqual([]);
|
|
194
|
+
|
|
195
|
+
// 第 N 轮:升级(新计数从 1 起步走满阈值)
|
|
196
|
+
await mgr.scan();
|
|
197
|
+
const msgs = escalationCalls().map((c) => c.msg);
|
|
198
|
+
expect(msgs.some((m) => m.includes(c1))).toBe(true);
|
|
199
|
+
expect(msgs.some((m) => m.includes(c3))).toBe(true);
|
|
200
|
+
} finally {
|
|
201
|
+
sleeper2.kill("SIGKILL");
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { normalizeRef, AGENT_REF_EXT } from "../shared/agent-ref.ts";
|
|
19
19
|
import { parseResourceMeta } from "../shared/meta-parser.ts";
|
|
20
20
|
import { lintAgentMeta } from "../orchestration/script-lint.ts";
|
|
21
|
-
import type { AgentMeta } from "../shared/resource-meta.ts";
|
|
21
|
+
import type { AgentMeta, RoutingExample } from "../shared/resource-meta.ts";
|
|
22
22
|
import type { AgentConfig } from "./model-resolver.ts";
|
|
23
23
|
import { hasEngine, listEngines, EngineNotFoundError } from "./engine/registry.ts";
|
|
24
24
|
|
|
@@ -138,6 +138,230 @@ function extractYamlField(yaml: string, key: string): string | undefined {
|
|
|
138
138
|
return value || undefined;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// ============================================================
|
|
142
|
+
// 宽容解析(sink 设计 D3 / U2)
|
|
143
|
+
// ============================================================
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* agent .md 的宽容解析结果(执行消费面全字段投影)。
|
|
147
|
+
*
|
|
148
|
+
* 双轨语义(D3 定稿):本结构是**执行可用性**面(可用即跑),严格路由面仍是
|
|
149
|
+
* AgentMeta(注入清单可见性:缺 name/description 不进清单,由 meta !== null 表达)。
|
|
150
|
+
* 两轨分离是两宿主既有设计,本类型只是把执行侧第三份手写实现(zsw mini parser)
|
|
151
|
+
* 收敛到 core 单点。
|
|
152
|
+
*/
|
|
153
|
+
export interface AgentProfile {
|
|
154
|
+
/** 宽容 name:frontmatter name,缺省文件 stem(宽松语义不拒)。 */
|
|
155
|
+
name: string;
|
|
156
|
+
/** 宽容 description:frontmatter description,缺省空串(宽松语义不拒)。 */
|
|
157
|
+
description: string;
|
|
158
|
+
/** frontmatter 后正文(trim)——即 systemPrompt。 */
|
|
159
|
+
body: string;
|
|
160
|
+
/** 路由提示(严格层投影;IF1 未通过时经 legacy fallback 也不取——该资产不进清单)。 */
|
|
161
|
+
when?: string;
|
|
162
|
+
examples?: RoutingExample[];
|
|
163
|
+
// ── 执行字段全量(与 AgentConfig / AgentMeta 同名同语义)──
|
|
164
|
+
model?: string;
|
|
165
|
+
tools?: string[];
|
|
166
|
+
/** 执行引擎 id(D9 per-agent)。 */
|
|
167
|
+
engine?: string;
|
|
168
|
+
thinkingLevel?: string;
|
|
169
|
+
defaultBackground?: boolean;
|
|
170
|
+
/** turn 预算上限(D3 可选执行字段;消费优先级:显式参数 > 本字段 > 缺省)。 */
|
|
171
|
+
maxTurns?: number;
|
|
172
|
+
/** tool denylist(D3 可选执行字段,与 tools allowlist 正交)。 */
|
|
173
|
+
disallowedTools?: string[];
|
|
174
|
+
/** agent 声明依赖的 skill 名清单(D3 可选执行字段)。 */
|
|
175
|
+
skills?: string[];
|
|
176
|
+
/**
|
|
177
|
+
* IF1 严格层 meta:null = 无 frontmatter / 未闭合 / 严格校验未通过。
|
|
178
|
+
* 注入清单可见性判定归它(discoverAgents 只放行 meta 非 null 的条目——
|
|
179
|
+
* 与 pi 现装配循环口径等值)。
|
|
180
|
+
*/
|
|
181
|
+
meta: AgentMeta | null;
|
|
182
|
+
/**
|
|
183
|
+
* 宽容解析降级说明(legacy fallback 生效等)。宽容语义不抛——资产异常时
|
|
184
|
+
* 返回尽力解析结果 + warnings,调用方决定呈现(错误规格表:`{ name: stem, body,
|
|
185
|
+
* warnings[] }` 宽容降级)。
|
|
186
|
+
*/
|
|
187
|
+
warnings: string[];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* agent .md 宽容解析(D3/U2 定稿):无 frontmatter 不拒、name 缺省 stem、
|
|
192
|
+
* description 缺省空串、返回 body 与执行字段全量,**永不抛**。
|
|
193
|
+
*
|
|
194
|
+
* 实现基础(D3 字段形态覆盖矩阵):
|
|
195
|
+
* - 主路径 = parseResourceMeta(eemeli/yaml 全量解析):原生支持单行 key:value、
|
|
196
|
+
* 行内数组、block-scalar、多行 `- item` 列表;
|
|
197
|
+
* - meta=null 时(无 frontmatter / 未闭合 / yaml 整体解析失败 / IF1 严格校验未通过)
|
|
198
|
+
* 经 extractYamlField legacy fallback(仅单行 key:value 形态)兜底取执行字段,
|
|
199
|
+
* 保证「可用即跑」不因资产格式瑕疵丢失配置(先例:parseAgentWithMeta 的 MF-3
|
|
200
|
+
* fallback)。fallback 触达时写入 warnings(warn 可见,调用方决定呈现)。
|
|
201
|
+
*
|
|
202
|
+
* 与 parseAgentWithMeta 的关系:本函数是新增导出面(执行消费面),既有
|
|
203
|
+
* parseAgentWithMeta/loadByPath 语义零改动(含 engine 未注册 throw 的解析期校验)。
|
|
204
|
+
* 本函数宽容语义不抛,故不做 engine 注册校验(执行校验归执行路径)。
|
|
205
|
+
*/
|
|
206
|
+
export function parseAgentProfile(text: string, filePath: string): AgentProfile {
|
|
207
|
+
const stem = path.basename(filePath, ".md");
|
|
208
|
+
const warnings: string[] = [];
|
|
209
|
+
|
|
210
|
+
// 阶段 1:结构切分(无 frontmatter / 未闭合 / 闭合三态)
|
|
211
|
+
const fm = splitAgentFrontmatter(text);
|
|
212
|
+
// 无 frontmatter → 整个内容作为 body(宽容:README 等普通 .md 也是合法 profile)
|
|
213
|
+
if (fm.kind === "none") {
|
|
214
|
+
return {
|
|
215
|
+
name: stem,
|
|
216
|
+
description: "",
|
|
217
|
+
body: text.trim(),
|
|
218
|
+
meta: null,
|
|
219
|
+
warnings,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (fm.kind === "unclosed") {
|
|
223
|
+
// 未闭合 frontmatter:name 经 legacy fallback,其余全文作 body(与 parseAgentWithMeta 同判)
|
|
224
|
+
warnings.push(
|
|
225
|
+
`[agent-registry] ${filePath}: frontmatter 未闭合——name 经 legacy fallback(仅单行 key:value),全文作 body`,
|
|
226
|
+
);
|
|
227
|
+
return {
|
|
228
|
+
name: extractYamlField(fm.yamlBlock, "name") ?? stem,
|
|
229
|
+
description: "",
|
|
230
|
+
body: text.trim(),
|
|
231
|
+
meta: null,
|
|
232
|
+
warnings,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 阶段 2:主路径 = IF1 严格层(eemeli/yaml 全量解析)
|
|
237
|
+
const meta = parseResourceMeta(text, "agent");
|
|
238
|
+
const agentMeta = meta?.kind === "agent" ? meta : null;
|
|
239
|
+
if (agentMeta !== null) {
|
|
240
|
+
return profileFromStrictMeta(agentMeta, fm.yamlBlock, fm.body, warnings);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 阶段 3:meta=null(yaml 解析失败或 IF1 未通过)→ legacy fallback 仅单行 key:value,
|
|
244
|
+
// 执行字段不静默丢失(MF-3 先例收敛)。宽松语义:name 缺省 stem、description 缺省空串。
|
|
245
|
+
warnings.push(
|
|
246
|
+
`[agent-registry] ${filePath}: agent frontmatter 未通过严格校验(IF1:yaml 解析失败或缺 name/description)` +
|
|
247
|
+
"——执行字段经 legacy fallback(仅单行 key:value 形态)生效,结构化路由不可见,建议补 name/description",
|
|
248
|
+
);
|
|
249
|
+
return profileFromLegacyFallback(fm.yamlBlock, fm.body, stem, filePath, warnings);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** 宽容解析阶段 1 的切分产物(frontmatter 结构三态)。 */
|
|
253
|
+
type AgentFrontmatterSlice =
|
|
254
|
+
| { kind: "none" }
|
|
255
|
+
| { kind: "unclosed"; yamlBlock: string }
|
|
256
|
+
| { kind: "closed"; yamlBlock: string; body: string };
|
|
257
|
+
|
|
258
|
+
/** frontmatter 结构切分(与 parseAgentWithMeta 同款定位逻辑,产出三态供分派)。 */
|
|
259
|
+
function splitAgentFrontmatter(text: string): AgentFrontmatterSlice {
|
|
260
|
+
if (!text.startsWith(FM_DELIM)) return { kind: "none" };
|
|
261
|
+
const closeIdx = text.indexOf(FM_DELIM, FM_DELIM.length);
|
|
262
|
+
if (closeIdx === -1) {
|
|
263
|
+
return { kind: "unclosed", yamlBlock: text.slice(FM_DELIM.length) };
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
kind: "closed",
|
|
267
|
+
yamlBlock: text.slice(FM_DELIM.length, closeIdx),
|
|
268
|
+
body: text.slice(closeIdx + FM_DELIM.length).trim(),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** 严格层(IF1 通过)的 profile 投影:AgentMeta 路由/执行字段 + 单行提取的执行配置。 */
|
|
273
|
+
function profileFromStrictMeta(
|
|
274
|
+
agentMeta: AgentMeta,
|
|
275
|
+
yamlBlock: string,
|
|
276
|
+
body: string,
|
|
277
|
+
warnings: string[],
|
|
278
|
+
): AgentProfile {
|
|
279
|
+
// thinkingLevel/defaultBackground 是执行配置(非 AgentMeta 字段),与
|
|
280
|
+
// parseAgentWithMeta 同位经单行提取(严格层资产同样携带这两字段的现实先例)
|
|
281
|
+
const thinkingLevelRaw = extractYamlField(yamlBlock, "thinkingLevel");
|
|
282
|
+
const defaultBackgroundRaw = extractYamlField(yamlBlock, "defaultBackground");
|
|
283
|
+
return {
|
|
284
|
+
name: agentMeta.name,
|
|
285
|
+
description: agentMeta.description,
|
|
286
|
+
body,
|
|
287
|
+
...(agentMeta.when !== undefined ? { when: agentMeta.when } : {}),
|
|
288
|
+
...(agentMeta.examples !== undefined ? { examples: agentMeta.examples } : {}),
|
|
289
|
+
...(agentMeta.model !== undefined ? { model: agentMeta.model } : {}),
|
|
290
|
+
...nonEmptyArraySpread("tools", agentMeta.tools),
|
|
291
|
+
...(agentMeta.engine !== undefined ? { engine: agentMeta.engine } : {}),
|
|
292
|
+
...(thinkingLevelRaw !== undefined ? { thinkingLevel: thinkingLevelRaw } : {}),
|
|
293
|
+
...(defaultBackgroundRaw === "true" ? { defaultBackground: true } : {}),
|
|
294
|
+
...(agentMeta.maxTurns !== undefined ? { maxTurns: agentMeta.maxTurns } : {}),
|
|
295
|
+
...nonEmptyArraySpread("disallowedTools", agentMeta.disallowedTools),
|
|
296
|
+
...nonEmptyArraySpread("skills", agentMeta.skills),
|
|
297
|
+
meta: agentMeta,
|
|
298
|
+
warnings,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** legacy fallback 的 profile 投影(仅单行 key:value 提取;warnings 同引用收集)。 */
|
|
303
|
+
function profileFromLegacyFallback(
|
|
304
|
+
yamlBlock: string,
|
|
305
|
+
body: string,
|
|
306
|
+
stem: string,
|
|
307
|
+
filePath: string,
|
|
308
|
+
warnings: string[],
|
|
309
|
+
): AgentProfile {
|
|
310
|
+
const nameFallback = extractYamlField(yamlBlock, "name") ?? stem;
|
|
311
|
+
const modelFallback = extractYamlField(yamlBlock, "model");
|
|
312
|
+
const toolsFallback = parseCommaListFallback(extractYamlField(yamlBlock, "tools"));
|
|
313
|
+
const maxTurnsFallback = parseNumberFallback(extractYamlField(yamlBlock, "maxTurns"), filePath, "maxTurns", warnings);
|
|
314
|
+
const disallowedToolsFallback = parseCommaListFallback(extractYamlField(yamlBlock, "disallowedTools"));
|
|
315
|
+
const skillsFallback = parseCommaListFallback(extractYamlField(yamlBlock, "skills"));
|
|
316
|
+
const thinkingLevelFallback = extractYamlField(yamlBlock, "thinkingLevel");
|
|
317
|
+
const defaultBackgroundRaw = extractYamlField(yamlBlock, "defaultBackground");
|
|
318
|
+
const engineFallback = extractYamlField(yamlBlock, "engine");
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
name: nameFallback,
|
|
322
|
+
description: "",
|
|
323
|
+
body,
|
|
324
|
+
...(modelFallback !== undefined ? { model: modelFallback } : {}),
|
|
325
|
+
...nonEmptyArraySpread("tools", toolsFallback),
|
|
326
|
+
...(engineFallback !== undefined ? { engine: engineFallback } : {}),
|
|
327
|
+
...(thinkingLevelFallback !== undefined ? { thinkingLevel: thinkingLevelFallback } : {}),
|
|
328
|
+
...(defaultBackgroundRaw === "true" ? { defaultBackground: true } : {}),
|
|
329
|
+
...(maxTurnsFallback !== undefined ? { maxTurns: maxTurnsFallback } : {}),
|
|
330
|
+
...nonEmptyArraySpread("disallowedTools", disallowedToolsFallback),
|
|
331
|
+
...nonEmptyArraySpread("skills", skillsFallback),
|
|
332
|
+
meta: null,
|
|
333
|
+
warnings,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** 可选数组字段非空才带键(tools/disallowedTools/skills 的条件展开收敛,键序随展开位不变)。 */
|
|
338
|
+
function nonEmptyArraySpread<T>(key: string, v: T[] | undefined): Record<string, T[]> {
|
|
339
|
+
return v !== undefined && v.length > 0 ? { [key]: v } : {};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** legacy fallback 的逗号分隔列表解析(`tools: read, bash` 约定,与 parseAgentWithMeta 同构)。 */
|
|
343
|
+
function parseCommaListFallback(raw: string | undefined): string[] | undefined {
|
|
344
|
+
return raw
|
|
345
|
+
? raw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
346
|
+
: undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** legacy fallback 的数字解析(extractYamlField 只出字符串,`maxTurns: 2` 需换算)。 */
|
|
350
|
+
function parseNumberFallback(
|
|
351
|
+
raw: string | undefined,
|
|
352
|
+
filePath: string,
|
|
353
|
+
field: string,
|
|
354
|
+
warnings: string[],
|
|
355
|
+
): number | undefined {
|
|
356
|
+
if (raw === undefined) return undefined;
|
|
357
|
+
const n = Number(raw);
|
|
358
|
+
if (!Number.isFinite(n)) {
|
|
359
|
+
warnings.push(`[agent-registry] ${filePath}: ${field} 值 "${raw}" 不是有限数字,忽略`);
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
return n;
|
|
363
|
+
}
|
|
364
|
+
|
|
141
365
|
// ============================================================
|
|
142
366
|
// AgentRegistry
|
|
143
367
|
// ============================================================
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/execution/agents-assembly.ts
|
|
2
|
+
//
|
|
3
|
+
// agent 装配函数(sink 设计 U2 / A6):发现 → parseAgentProfile → frontmatter name
|
|
4
|
+
// 去重 → 码点序,warn 口径内聚。workflow 侧 discoverWorkflows(config-loader)的
|
|
5
|
+
// 对称面——第三宿主仅凭该函数即可「列 agents」(G3/S5),无需复刻装配循环。
|
|
6
|
+
//
|
|
7
|
+
// 与 pi 壳装配循环(subagent-list-injector.discoverAllAgents)的等值口径:
|
|
8
|
+
// - 清单可见性 = IF1 严格层(profile.meta !== null)——缺 name/description 的资产
|
|
9
|
+
// 不进清单(注入投影「路由可见性」双轨语义,D3 定稿;宽容解析只服务执行消费面);
|
|
10
|
+
// - warn 口径内聚:仅「有 frontmatter 但严格校验未通过」才 warn——README 等无
|
|
11
|
+
// frontmatter 的 .md 静默跳过(每 turn 扫描不刷屏,pi m5 评审先例);
|
|
12
|
+
// - 扫描 / 遮蔽语义归 shared/resource-discovery(ADR-031),本文件零扫描逻辑。
|
|
13
|
+
|
|
14
|
+
import type { AgentEntry } from "../shared/injection-render.ts";
|
|
15
|
+
import { sortByCodepoint } from "../shared/injection-render.ts";
|
|
16
|
+
import { discoverResources, getCachedFileContent } from "../shared/resource-discovery.ts";
|
|
17
|
+
import type { DiscoveryRoot } from "../core/host-services.ts";
|
|
18
|
+
import { getLogger } from "../core/logger.ts";
|
|
19
|
+
import { parseAgentProfile } from "./agent-registry.ts";
|
|
20
|
+
|
|
21
|
+
const logger = getLogger("agents-assembly");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 发现并列出全部可用 agent(U2/A6 装配函数)。
|
|
25
|
+
*
|
|
26
|
+
* 流程:discoverResources(按优先级低→高,last-writer-wins)→ 逐文件
|
|
27
|
+
* parseAgentProfile → 仅 IF1 严格层通过(profile.meta !== null)的条目进清单 →
|
|
28
|
+
* 按 frontmatter name 去重(高优先级靠后覆盖,Map 后写胜)→ name 码点序输出
|
|
29
|
+
* (KV-cache 契约:顺序与 readdir 枚举序解耦,重建结果逐字节一致)。
|
|
30
|
+
*
|
|
31
|
+
* 抛错面(如实声明):单文件读失败仅记日志跳过;目录不存在返回空列表;但底层
|
|
32
|
+
* discoverResources 的不可恢复扫描错误会原样向上传播(readdir 遇权限拒绝、
|
|
33
|
+
* EMFILE 竞态等——resource-discovery 自述 "Throws on unrecoverable scan errors",
|
|
34
|
+
* Promise.all 首个 reject 即整体拒绝),调用方需自行兜底。
|
|
35
|
+
*
|
|
36
|
+
* @param workspaceRoot 项目根(findWorkspaceRoot 推导结果)
|
|
37
|
+
* @param hostRoots 宿主注入发现根(pi 壳 = getAgentDir 三根;无则传 []——
|
|
38
|
+
* user-agents/project-agents 硬编码槽仍生效,与 ScanConfig 语义一致)。
|
|
39
|
+
* 注意:hostRoots 之外还有四个硬编码根恒进入扫描,无法经参数关闭——
|
|
40
|
+
* `~/.agents/agents`、`<workspaceRoot>/.pi/agents`、
|
|
41
|
+
* `<workspaceRoot>/.agents/agents` 与 `XYZ_EXTENSION_PATHS`
|
|
42
|
+
* 环境变量展开的扩展源码路径(resource-discovery buildScanTargets
|
|
43
|
+
* 固定槽位,注入 hostRoots 只是增列而非替换扫描面)
|
|
44
|
+
*/
|
|
45
|
+
export async function discoverAgents(
|
|
46
|
+
workspaceRoot: string,
|
|
47
|
+
hostRoots: DiscoveryRoot[],
|
|
48
|
+
): Promise<AgentEntry[]> {
|
|
49
|
+
const resources = await discoverResources({
|
|
50
|
+
kind: "agents",
|
|
51
|
+
workspaceRoot,
|
|
52
|
+
hostRoots,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const agentMap = new Map<string, AgentEntry>();
|
|
56
|
+
for (const resource of resources) {
|
|
57
|
+
// manifest 校验失败的包整体占位(available=false),不进清单
|
|
58
|
+
if (!resource.available) continue;
|
|
59
|
+
|
|
60
|
+
const content = getCachedFileContent(resource.path);
|
|
61
|
+
if (content === null) {
|
|
62
|
+
// 单文件读失败不阻断整条 agent 列表装配
|
|
63
|
+
logger.error(`[agents-assembly] skip unreadable agent file ${resource.path}`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 宽容解析(执行消费面单点);清单可见性按严格层 meta 判定
|
|
68
|
+
const profile = parseAgentProfile(content, resource.path);
|
|
69
|
+
if (profile.meta !== null) {
|
|
70
|
+
agentMap.set(profile.name, {
|
|
71
|
+
name: profile.name,
|
|
72
|
+
description: profile.description,
|
|
73
|
+
...(profile.when !== undefined ? { when: profile.when } : {}),
|
|
74
|
+
...(profile.examples !== undefined ? { examples: profile.examples } : {}),
|
|
75
|
+
path: resource.path,
|
|
76
|
+
});
|
|
77
|
+
} else if (content.trimStart().startsWith("---")) {
|
|
78
|
+
// 仅「有 frontmatter 但严格校验未通过」才 warn——无 frontmatter 的 README 等
|
|
79
|
+
// 普通 .md 静默跳过(pi m5 评审先例:缺 name/description 或单条 examples 非法
|
|
80
|
+
// 致整体 reject 时作者需要知道,普通文档不需要)。
|
|
81
|
+
logger.warn(
|
|
82
|
+
`[agents-assembly] ${resource.path}: agent frontmatter 解析失败(IF1 校验不通过)——agent 未进清单`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return sortByCodepoint([...agentMap.values()], (a) => a.name);
|
|
88
|
+
}
|
|
@@ -14,9 +14,25 @@ interface QueueEntry {
|
|
|
14
14
|
signal?: AbortSignal;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* 排队策略(D7/U4):release 时从等待队列放行哪个条目。策略差异是宿主声明的
|
|
19
|
+
* 有意决策(pi=priority / zsw=strict-fifo),保留为参数而非消灭(见 sink 设计
|
|
20
|
+
* subagent-core-sink-design.md §3.3 D7)。
|
|
21
|
+
*
|
|
22
|
+
* - `"priority"`(缺省):priority 值最小(0=最高)者优先,同优先级按入队序(FIFO)。
|
|
23
|
+
* 与 pi 既有行为等值。
|
|
24
|
+
* - `"strict-fifo"`:忽略 priority,严格按入队顺序(seq)放行。纯 FIFO 语义,
|
|
25
|
+
* 供 zsw 侧等需要「先到先得、不许插队」语义的宿主消费。
|
|
26
|
+
*/
|
|
27
|
+
export type QueuePolicy = "priority" | "strict-fifo";
|
|
28
|
+
|
|
17
29
|
/** 并发池接口(可注入,便于测试 mock)。 */
|
|
18
30
|
export interface ConcurrencyPool {
|
|
19
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* 排队获取槽位(priority 0=最高;"strict-fifo" 策略下该值仅记录不参与出队选择,
|
|
33
|
+
* 见 createConcurrencyPool)。可选 effectiveMaxConcurrent 覆盖实例级默认配额。
|
|
34
|
+
* 可选 AbortSignal 在 abort 时 reject 排队条目。
|
|
35
|
+
*/
|
|
20
36
|
acquire(priority: number, effectiveMaxConcurrent?: number, signal?: AbortSignal): Promise<void>;
|
|
21
37
|
/** 归还槽位。必须无条件执行(finally)。 */
|
|
22
38
|
release(): void;
|
|
@@ -27,27 +43,40 @@ export interface ConcurrencyPool {
|
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
/**
|
|
30
|
-
* 默认实现:maxConcurrent 槽位 +
|
|
46
|
+
* 默认实现:maxConcurrent 槽位 + 可策略化排队队列。
|
|
31
47
|
*
|
|
32
48
|
* acquire(priority, effectiveMaxConcurrent?):
|
|
33
49
|
* effective = effectiveMaxConcurrent ?? maxConcurrent
|
|
34
50
|
* active < effective → active++, resolve
|
|
35
|
-
* 否则 → 入队 { priority, resolve, seq }
|
|
51
|
+
* 否则 → 入队 { priority, resolve, seq }
|
|
36
52
|
*
|
|
37
53
|
* release():
|
|
38
|
-
* queue 非空 →
|
|
54
|
+
* queue 非空 → 按 queuePolicy 选一个条目出队 resolve(active 不变):
|
|
55
|
+
* - "priority":priority 升序 + 同优先级 seq FIFO
|
|
56
|
+
* - "strict-fifo":纯 seq FIFO(priority 仅记录)
|
|
39
57
|
* queue 空 → active--(防下溢)
|
|
58
|
+
*
|
|
59
|
+
* 已知契约边界(审查 S-2 登记):分层配额(effectiveMaxConcurrent)仅在
|
|
60
|
+
* acquire 时点判定;release 授予排队条目时不复查其 effective——即活跃数
|
|
61
|
+
* 仍 ≥ 条目 effective 时,单次释放也可能授予该条目(pi 运行时既有行为,
|
|
62
|
+
* concurrency-pool.test T-A2 锚定)。需要严格分层隔离的宿主应在条目侧
|
|
63
|
+
* 自行保证,勿依赖 release 时点复查。
|
|
64
|
+
*
|
|
65
|
+
* 类保留内部消费(subagent-service 深路径);对外导出面走 createConcurrencyPool
|
|
66
|
+
* 工厂(对象参数 + 策略枚举,不暴露本类名与位置参数构造——D7/U4 裁决)。
|
|
40
67
|
*/
|
|
41
68
|
export class DefaultConcurrencyPool implements ConcurrencyPool {
|
|
42
69
|
private _active = 0;
|
|
43
70
|
private readonly queue: QueueEntry[] = [];
|
|
44
71
|
private seqCounter = 0;
|
|
72
|
+
private readonly queuePolicy: QueuePolicy;
|
|
45
73
|
|
|
46
74
|
/** 下限 1——maxConcurrent=0 会让 acquire 永久排队死锁(C3 修复)。 */
|
|
47
75
|
readonly maxConcurrent: number;
|
|
48
76
|
|
|
49
|
-
constructor(maxConcurrent: number) {
|
|
77
|
+
constructor(maxConcurrent: number, queuePolicy: QueuePolicy = "priority") {
|
|
50
78
|
this.maxConcurrent = Math.max(1, maxConcurrent);
|
|
79
|
+
this.queuePolicy = queuePolicy;
|
|
51
80
|
}
|
|
52
81
|
|
|
53
82
|
acquire(priority: number, effectiveMaxConcurrent?: number, signal?: AbortSignal): Promise<void> {
|
|
@@ -86,14 +115,24 @@ export class DefaultConcurrencyPool implements ConcurrencyPool {
|
|
|
86
115
|
});
|
|
87
116
|
}
|
|
88
117
|
|
|
118
|
+
/**
|
|
119
|
+
* 出队候选比较:cur 是否优于 best(release 时选谁获得释放的槽位)。
|
|
120
|
+
* 策略单一分派点——新增策略只需在此分支,acquire/abort/clamp 逻辑策略无关。
|
|
121
|
+
*/
|
|
122
|
+
private isBetterCandidate(cur: QueueEntry, best: QueueEntry): boolean {
|
|
123
|
+
if (this.queuePolicy === "strict-fifo") {
|
|
124
|
+
// 纯 FIFO:seq 单调递增,先入队者 seq 必更小——priority 完全不参与
|
|
125
|
+
return cur.seq < best.seq;
|
|
126
|
+
}
|
|
127
|
+
// priority:取优先级最高(priority 最小)的;同优先级 FIFO(seq 最小)
|
|
128
|
+
return cur.priority < best.priority || (cur.priority === best.priority && cur.seq < best.seq);
|
|
129
|
+
}
|
|
130
|
+
|
|
89
131
|
release(): void {
|
|
90
132
|
if (this.queue.length > 0) {
|
|
91
|
-
// 取优先级最高(priority 最小)的;同优先级 FIFO(seq 最小)
|
|
92
133
|
let bestIdx = 0;
|
|
93
134
|
for (let i = 1; i < this.queue.length; i++) {
|
|
94
|
-
|
|
95
|
-
const best = this.queue[bestIdx];
|
|
96
|
-
if (cur.priority < best.priority || (cur.priority === best.priority && cur.seq < best.seq)) {
|
|
135
|
+
if (this.isBetterCandidate(this.queue[i], this.queue[bestIdx])) {
|
|
97
136
|
bestIdx = i;
|
|
98
137
|
}
|
|
99
138
|
}
|
|
@@ -114,3 +153,26 @@ export class DefaultConcurrencyPool implements ConcurrencyPool {
|
|
|
114
153
|
return this._active;
|
|
115
154
|
}
|
|
116
155
|
}
|
|
156
|
+
|
|
157
|
+
/** createConcurrencyPool 选项(对象参数构造——导出面禁止位置参数歧义,D7/U4)。 */
|
|
158
|
+
export interface CreateConcurrencyPoolOptions {
|
|
159
|
+
/** 实例级最大并发配额。0/负数 clamp 到 1(防 acquire 永久排队死锁,C3 修复语义)。 */
|
|
160
|
+
maxConcurrent: number;
|
|
161
|
+
/**
|
|
162
|
+
* 排队策略(见 QueuePolicy)。缺省 `"priority"`——与 pi 既有消费
|
|
163
|
+
* (`new DefaultConcurrencyPool(n)`)行为逐点等值,缺省即零回归。
|
|
164
|
+
*/
|
|
165
|
+
queuePolicy?: QueuePolicy;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 并发池工厂(D7/U4 导出面):宿主经此创建并发池,无需感知实现类。
|
|
170
|
+
*
|
|
171
|
+
* 返回 ConcurrencyPool 接口而非 DefaultConcurrencyPool——barrel 导出面只认
|
|
172
|
+
* 「对象参数 + 策略枚举」,实现类可内部替换,策略差异(pi=priority /
|
|
173
|
+
* zsw=strict-fifo)显式化为参数而非两份复刻实现(深度分层公式与下限常量单源,
|
|
174
|
+
* 公式见类注释与 slots 消费方)。
|
|
175
|
+
*/
|
|
176
|
+
export function createConcurrencyPool(options: CreateConcurrencyPoolOptions): ConcurrencyPool {
|
|
177
|
+
return new DefaultConcurrencyPool(options.maxConcurrent, options.queuePolicy ?? "priority");
|
|
178
|
+
}
|