@zhushanwen/pi-subagent-workflow 5.0.2 → 7.0.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/agents/{reviewer.md → code-reviewer.md} +20 -3
- package/agents/context-builder.md +5 -0
- package/agents/doc-reviewer.md +9 -2
- package/agents/explorer.md +5 -0
- package/agents/general-purpose.md +5 -0
- package/agents/oracle.md +20 -4
- package/agents/orchestrator.md +6 -1
- package/agents/planner.md +5 -0
- package/agents/researcher.md +5 -0
- package/agents/worker.md +5 -0
- package/package.json +6 -4
- package/src/execution/__tests__/agent-registry.test.ts +189 -119
- package/src/execution/__tests__/crash-recovery.test.ts +0 -1
- package/src/execution/__tests__/execute-options-mapper.test.ts +4 -4
- package/src/execution/__tests__/index-session-start.test.ts +0 -1
- package/src/execution/__tests__/model-resolver.test.ts +20 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +0 -2
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +1 -1
- package/src/execution/agent-registry.ts +92 -169
- package/src/execution/execute-options-mapper.ts +2 -2
- package/src/execution/model-config-service.ts +13 -34
- package/src/execution/model-resolver.ts +5 -3
- package/src/execution/subagent-service.ts +9 -6
- package/src/execution/subprocess-agent-runner.ts +3 -2
- package/src/index.ts +4 -25
- package/src/injectors/__tests__/subagent-list-injector.test.ts +266 -14
- package/src/injectors/__tests__/workflow-list-injector.test.ts +236 -32
- package/src/injectors/subagent-list-injector.ts +99 -48
- package/src/injectors/workflow-list-injector.ts +65 -50
- package/src/interface/__tests__/detectors.test.ts +100 -43
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +8 -12
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +163 -0
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +55 -9
- package/src/interface/subagent-tool.ts +7 -4
- package/src/interface/tool-workflow-script.ts +27 -10
- package/src/interface/tool-workflow.ts +174 -81
- package/src/orchestration/__tests__/args-validator.test.ts +143 -0
- package/src/orchestration/__tests__/config-loader.test.ts +124 -40
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +33 -2
- package/src/orchestration/__tests__/lifecycle.test.ts +59 -2
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +116 -51
- package/src/orchestration/__tests__/script-lint.test.ts +167 -1
- package/src/orchestration/__tests__/worker-host.test.ts +120 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +69 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +51 -1
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +2 -2
- package/src/orchestration/__tests__/workflows-e2e.test.ts +177 -24
- package/src/orchestration/agent-opts-resolver.ts +51 -101
- package/src/orchestration/args-validator.ts +127 -0
- package/src/orchestration/config-loader.ts +63 -94
- package/src/orchestration/error-recovery.ts +6 -9
- package/src/orchestration/launcher.ts +77 -41
- package/src/orchestration/lifecycle.ts +7 -0
- package/src/orchestration/models/ports.ts +0 -14
- package/src/orchestration/models/run-spec.ts +24 -1
- package/src/orchestration/models/types.ts +15 -8
- package/src/orchestration/models/workflow-script-registry.ts +3 -0
- package/src/orchestration/models/workflow-script.ts +10 -14
- package/src/orchestration/script-lint.ts +159 -0
- package/src/orchestration/worker-host.ts +5 -0
- package/src/orchestration/worker-script-builder.ts +15 -4
- package/src/orchestration/workflow-script-registry-impl.ts +34 -29
- package/src/shared/__tests__/meta-parser.test.ts +304 -0
- package/src/shared/__tests__/resource-discovery.test.ts +167 -7
- package/src/shared/__tests__/resource-meta.test.ts +51 -0
- package/src/shared/agent-ref.ts +36 -0
- package/src/shared/meta-parser.ts +257 -0
- package/src/shared/resource-discovery.ts +88 -2
- package/src/shared/resource-meta.ts +60 -0
- package/workflows/README.md +2 -2
- package/workflows/_shared/agent-refs.cjs +40 -0
- package/workflows/chain.js +30 -6
- package/workflows/map-reduce.js +33 -5
- package/workflows/parallel.js +34 -7
- package/workflows/review-fix-loop-utils.cjs +23 -103
- package/workflows/review-fix-loop.js +121 -58
- package/workflows/scatter-gather.js +30 -6
|
@@ -115,13 +115,13 @@ describe("mapToExecuteOptions (D-A2)", () => {
|
|
|
115
115
|
expect(result.thinkingLevel).toBeUndefined();
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
it("
|
|
119
|
-
const opts: AgentCallOpts = { ...baseOpts,
|
|
118
|
+
it("appendSystemPrompt → appendSystemPrompt 透传", () => {
|
|
119
|
+
const opts: AgentCallOpts = { ...baseOpts, appendSystemPrompt: ["SO instruction content"] };
|
|
120
120
|
const result = mapToExecuteOptions(opts);
|
|
121
|
-
expect(result.appendSystemPrompt).toEqual(["
|
|
121
|
+
expect(result.appendSystemPrompt).toEqual(["SO instruction content"]);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
it("
|
|
124
|
+
it("appendSystemPrompt 不传 → appendSystemPrompt undefined", () => {
|
|
125
125
|
const result = mapToExecuteOptions(baseOpts);
|
|
126
126
|
expect(result.appendSystemPrompt).toBeUndefined();
|
|
127
127
|
});
|
|
@@ -101,7 +101,6 @@ vi.mock("../subagent-service.ts", () => ({
|
|
|
101
101
|
vi.mock("../model-config-service.ts", () => ({
|
|
102
102
|
ModelConfigService: class {
|
|
103
103
|
initModel = vi.fn();
|
|
104
|
-
getAgentRegistry = () => ({ get: () => undefined, list: () => [] });
|
|
105
104
|
setCtxModel = vi.fn();
|
|
106
105
|
},
|
|
107
106
|
getModelConfigService: () => null,
|
|
@@ -278,6 +278,20 @@ describe("resolveModel — not-found error suggests similar models (B)", () => {
|
|
|
278
278
|
expect(msg).toMatch(/models\.json/);
|
|
279
279
|
expect(msg).not.toMatch(/Similar available models/); // auth 错误不列 model
|
|
280
280
|
});
|
|
281
|
+
|
|
282
|
+
it("P1: resolves model passed with ':max' suffix (THINKING_ORDER includes 'max')", () => {
|
|
283
|
+
const m = makeModel({ id: "mock-model", provider: "p1", reasoning: true, thinkingLevelMap: { max: 4 } });
|
|
284
|
+
const reg = makeRegistry([m]);
|
|
285
|
+
const r = resolveModel(
|
|
286
|
+
{ name: "worker", systemPrompt: "" },
|
|
287
|
+
reg,
|
|
288
|
+
{ model: "p1/mock-model:max" },
|
|
289
|
+
ctxModel,
|
|
290
|
+
);
|
|
291
|
+
expect(r.model).toBeTruthy();
|
|
292
|
+
expect(r.model.id).toBe("mock-model");
|
|
293
|
+
expect(r.thinkingLevel).toBe("max");
|
|
294
|
+
});
|
|
281
295
|
});
|
|
282
296
|
|
|
283
297
|
describe("availableThinkingLevels", () => {
|
|
@@ -292,6 +306,12 @@ describe("availableThinkingLevels", () => {
|
|
|
292
306
|
availableThinkingLevels({ reasoning: true, thinkingLevelMap: { off: 0, high: 2, low: 1, xhigh: 3 } }),
|
|
293
307
|
).toEqual(["off", "low", "high", "xhigh"]);
|
|
294
308
|
});
|
|
309
|
+
|
|
310
|
+
it("P2: includes 'max' as the last level (THINKING_ORDER SSOT derived)", () => {
|
|
311
|
+
const levels = availableThinkingLevels({ reasoning: true, thinkingLevelMap: { off: 0, low: 1, high: 2, xhigh: 3, max: 4 } });
|
|
312
|
+
expect(levels).toEqual(["off", "low", "high", "xhigh", "max"]);
|
|
313
|
+
expect(levels.slice(-3)).toEqual(["high", "xhigh", "max"]);
|
|
314
|
+
});
|
|
295
315
|
});
|
|
296
316
|
|
|
297
317
|
// ============================================================
|
|
@@ -72,8 +72,6 @@ const { mockInitModel, mockInitSession, mockSetUiRequestHandler, mockSetModelCon
|
|
|
72
72
|
vi.mock("../model-config-service.ts", () => ({
|
|
73
73
|
ModelConfigService: class {
|
|
74
74
|
initModel = mockInitModel;
|
|
75
|
-
// F-4/D-003: index.ts 复用 modelService.getAgentRegistry(),stub 返回最小结构
|
|
76
|
-
getAgentRegistry = () => ({ get: () => undefined, list: () => [] });
|
|
77
75
|
},
|
|
78
76
|
getModelConfigService: () => null,
|
|
79
77
|
setModelConfigService: mockSetModelConfigService,
|
|
@@ -7,77 +7,28 @@
|
|
|
7
7
|
//
|
|
8
8
|
// builtin agent(包内 agents/*.md)走 pi.agents manifest(与 npm 包内发现规则一致)。
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
12
|
|
|
14
13
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
15
14
|
|
|
16
15
|
import {
|
|
17
|
-
|
|
18
|
-
discoverResourcesSync,
|
|
19
|
-
type ScanConfig,
|
|
16
|
+
getCachedFile,
|
|
20
17
|
} from "../shared/resource-discovery.ts";
|
|
18
|
+
import { normalizeRef, AGENT_REF_EXT } from "../shared/agent-ref.ts";
|
|
19
|
+
import { parseResourceMeta } from "../shared/meta-parser.ts";
|
|
20
|
+
import { lintAgentMeta } from "../orchestration/script-lint.ts";
|
|
21
|
+
import type { AgentMeta } from "../shared/resource-meta.ts";
|
|
21
22
|
import type { AgentConfig } from "./model-resolver.ts";
|
|
22
23
|
|
|
23
24
|
const logger = getLogger("subagents");
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
-
export interface BuiltinAgentRegistry {
|
|
27
|
-
get(name: string): AgentConfig | undefined;
|
|
28
|
-
list(): string[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 包内自带 agents(与 src/ 同级的 agents/ 目录)。
|
|
33
|
-
*
|
|
34
|
-
* 走 pi.agents manifest(package.json 的 pi.agents 字段),与 npm 包内发现规则一致。
|
|
35
|
-
* manifest 缺失时 fallback 扫约定目录 agents/。
|
|
36
|
-
*
|
|
37
|
-
* [HISTORICAL] 此前 discoverAll 从未被调用,agentRegistry 永远为空——包内
|
|
38
|
-
* agents/*.md(worker/reviewer/explorer 等)pi install 后开箱不可用。修复:构造时扫描
|
|
39
|
-
* 包内 agents/ 作为 builtin(优先级最低,被用户同名文件覆盖)。
|
|
40
|
-
*/
|
|
41
|
-
export function createPackageBuiltinRegistry(): BuiltinAgentRegistry {
|
|
42
|
-
const packageRoot = path.resolve(
|
|
43
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
44
|
-
"..",
|
|
45
|
-
"..",
|
|
46
|
-
);
|
|
47
|
-
const cache = new Map<string, AgentConfig>();
|
|
48
|
-
try {
|
|
49
|
-
const config = discoverPackageAgentsSync(packageRoot);
|
|
50
|
-
for (const resource of config) {
|
|
51
|
-
if (!resource.available) continue;
|
|
52
|
-
try {
|
|
53
|
-
const raw = fs.readFileSync(resource.path, "utf-8");
|
|
54
|
-
const agentConfig = parseAgentFrontmatter(resource.path, raw);
|
|
55
|
-
if (agentConfig) cache.set(agentConfig.name, agentConfig);
|
|
56
|
-
} catch (err) {
|
|
57
|
-
// 单个 builtin agent 文件损坏不影响其他——降级跳过该文件。
|
|
58
|
-
void err;
|
|
59
|
-
logger.warn(`[subagents] skip malformed builtin agent: ${resource.path}`, {
|
|
60
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
} catch (err) {
|
|
65
|
-
// agents/ 目录不存在(打包遗漏)→ 空 builtin,不崩。
|
|
66
|
-
void err;
|
|
67
|
-
logger.warn("[subagents] builtin agents/ directory unreadable, falling back to empty set", {
|
|
68
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
return {
|
|
72
|
-
get: (name) => cache.get(name),
|
|
73
|
-
list: () => [...cache.keys()],
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** mtime 缓存条目(跨 discoverAll 保留,靠 mtime 判失效)。 */
|
|
26
|
+
/** mtime 缓存条目(跨 loadByPath 保留,靠 mtime 判失效)。 */
|
|
78
27
|
interface FileCacheEntry {
|
|
79
28
|
mtimeMs: number;
|
|
80
29
|
config: AgentConfig;
|
|
30
|
+
/** AgentMeta(W4 lint 用——cache-miss 时 lint 一次,warn 随 mtime 变化刷)。 */
|
|
31
|
+
meta: AgentMeta | null;
|
|
81
32
|
}
|
|
82
33
|
|
|
83
34
|
// ============================================================
|
|
@@ -92,11 +43,22 @@ const FM_DELIM = "---";
|
|
|
92
43
|
* 兼容简单 YAML(key: value 单行格式)。body 作为 systemPrompt。
|
|
93
44
|
*/
|
|
94
45
|
export function parseAgentFrontmatter(filePath: string, content: string): AgentConfig {
|
|
46
|
+
return parseAgentWithMeta(filePath, content).config;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 解析 agent .md → { config, meta } 二元组(m5 T2:W4 lint 需要 AgentMeta——
|
|
51
|
+
* AgentConfig 投影时 examples 被丢弃,meta 供 lintAgentMeta 使用)。
|
|
52
|
+
*/
|
|
53
|
+
export function parseAgentWithMeta(
|
|
54
|
+
filePath: string,
|
|
55
|
+
content: string,
|
|
56
|
+
): { config: AgentConfig; meta: AgentMeta | null } {
|
|
95
57
|
const name = path.basename(filePath, ".md");
|
|
96
58
|
|
|
97
59
|
// 无 frontmatter → 整个内容作为 systemPrompt
|
|
98
60
|
if (!content.startsWith(FM_DELIM)) {
|
|
99
|
-
return { name, systemPrompt: content.trim() };
|
|
61
|
+
return { config: { name, systemPrompt: content.trim() }, meta: null };
|
|
100
62
|
}
|
|
101
63
|
|
|
102
64
|
const closeIdx = content.indexOf(FM_DELIM, FM_DELIM.length);
|
|
@@ -104,28 +66,53 @@ export function parseAgentFrontmatter(filePath: string, content: string): AgentC
|
|
|
104
66
|
// 未闭合 frontmatter:提取 name,其余作为 systemPrompt
|
|
105
67
|
const yamlBlock = content.slice(FM_DELIM.length);
|
|
106
68
|
return {
|
|
107
|
-
|
|
108
|
-
|
|
69
|
+
config: {
|
|
70
|
+
name: extractYamlField(yamlBlock, "name") ?? name,
|
|
71
|
+
systemPrompt: content.trim(),
|
|
72
|
+
},
|
|
73
|
+
meta: null,
|
|
109
74
|
};
|
|
110
75
|
}
|
|
111
76
|
|
|
112
77
|
const yamlBlock = content.slice(FM_DELIM.length, closeIdx);
|
|
113
78
|
const body = content.slice(closeIdx + FM_DELIM.length).trim();
|
|
114
79
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
80
|
+
// m2:结构化路由字段(name/model/tools)经 IF1 parseResourceMeta(统一 parser),
|
|
81
|
+
// 消灭本地 frontmatter parser 与 subagent-list-injector 的重复。thinkingLevel/defaultBackground
|
|
82
|
+
// 是执行配置(非 AgentMeta 路由字段),仍用 extractYamlField 取。
|
|
83
|
+
const meta = parseResourceMeta(content, "agent");
|
|
84
|
+
const agentMeta = meta?.kind === "agent" ? meta : null;
|
|
85
|
+
// MF-3 regression fix:agentMeta=null(IF1 要求 name/description 必填,缺则 parseResourceMeta
|
|
86
|
+
// 返 null)时,model/tools 不能静默丢失——fallback 用 extractYamlField 取(保留重构前行为)。
|
|
87
|
+
// 注入路由可见性由 discovery/injector 按 available 标志决定,与本处(direct-path loadByPath)无关。
|
|
88
|
+
const modelFallback = extractYamlField(yamlBlock, "model");
|
|
89
|
+
const toolsFallbackRaw = extractYamlField(yamlBlock, "tools");
|
|
90
|
+
const toolsFallback = toolsFallbackRaw
|
|
91
|
+
? toolsFallbackRaw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
118
92
|
: undefined;
|
|
119
|
-
|
|
93
|
+
if (!agentMeta && /^model:|^tools:/m.test(yamlBlock)) {
|
|
94
|
+
// m2 exec-review MINOR-2 + MF-3:IF1 未通过(缺 name/description)→ model/tools 经 legacy
|
|
95
|
+
// fallback 取(direct-path loadByPath 不丢配置,与重构前一致);但结构化路由注入不可见,
|
|
96
|
+
// 建议 agent 作者补 description 以满足 IF1。
|
|
97
|
+
logger.warn(
|
|
98
|
+
`[agent-registry] ${filePath}: agent frontmatter 缺 name/description(IF1 必填),` +
|
|
99
|
+
"model/tools 经 legacy fallback 生效(直接路径不丢配置),但结构化路由不可见——请补充 description",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
120
102
|
const defaultBackgroundRaw = extractYamlField(yamlBlock, "defaultBackground");
|
|
121
103
|
|
|
122
104
|
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
105
|
+
config: {
|
|
106
|
+
name: agentMeta?.name ?? name,
|
|
107
|
+
systemPrompt: body,
|
|
108
|
+
model: agentMeta?.model ?? modelFallback ?? undefined,
|
|
109
|
+
thinkingLevel: extractYamlField(yamlBlock, "thinkingLevel") ?? undefined,
|
|
110
|
+
tools: agentMeta?.tools && agentMeta.tools.length > 0
|
|
111
|
+
? agentMeta.tools
|
|
112
|
+
: (toolsFallback && toolsFallback.length > 0 ? toolsFallback : undefined),
|
|
113
|
+
defaultBackground: defaultBackgroundRaw === "true" ? true : undefined,
|
|
114
|
+
},
|
|
115
|
+
meta: agentMeta,
|
|
129
116
|
};
|
|
130
117
|
}
|
|
131
118
|
|
|
@@ -145,116 +132,52 @@ function extractYamlField(yaml: string, key: string): string | undefined {
|
|
|
145
132
|
// AgentRegistry
|
|
146
133
|
// ============================================================
|
|
147
134
|
|
|
148
|
-
/**
|
|
149
|
-
* 发现配置:用于统一资源发现的扫描参数。
|
|
150
|
-
*/
|
|
151
|
-
export interface AgentDiscoveryConfig {
|
|
152
|
-
/** 项目根目录(findWorkspaceRoot 推导结果) */
|
|
153
|
-
workspaceRoot: string;
|
|
154
|
-
/** agent 配置目录(getAgentDir() 结果) */
|
|
155
|
-
agentDir: string;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* agent 注册表。通过统一资源发现(shared/resource-discovery)扫描所有源。
|
|
160
|
-
* hot-reload:每次 discoverAll 重扫,mtime 未变的文件跳过 read+parse。
|
|
161
|
-
*
|
|
162
|
-
* 优先级(低→高):user .pi/agent → user .agents → npm global → npm dev →
|
|
163
|
-
* project .pi → project .agents。builtin(包内)优先级最低。
|
|
164
|
-
* 详见 ADR-031。
|
|
165
|
-
*/
|
|
166
135
|
export class AgentRegistry {
|
|
167
|
-
|
|
168
|
-
/** 文件级 mtime 缓存(key=绝对路径,跨 discoverAll 保留)。 */
|
|
136
|
+
/** 文件级 mtime 缓存(key=绝对路径,跨 loadByPath 保留)。 */
|
|
169
137
|
private readonly fileCache = new Map<string, FileCacheEntry>();
|
|
170
|
-
/** 本轮扫描到的路径集(清理已删除文件的缓存)。 */
|
|
171
|
-
private currentScanPaths = new Set<string>();
|
|
172
|
-
|
|
173
|
-
constructor(private readonly discoveryConfig: AgentDiscoveryConfig) {}
|
|
174
138
|
|
|
175
|
-
/**
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
this.currentScanPaths.add(resource.path);
|
|
190
|
-
try {
|
|
191
|
-
const config = this.loadWithMtimeCache(resource.path);
|
|
192
|
-
if (config) this.cache.set(config.name, config);
|
|
193
|
-
} catch (_err) {
|
|
194
|
-
// 有意吞掉:文件不可读/解析失败 → 跳过(不阻断其他 agent 发现)
|
|
195
|
-
void _err;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// builtin 优先级最低(先写入,被文件 agent 覆盖)
|
|
200
|
-
for (const agentName of builtin.list()) {
|
|
201
|
-
if (!this.cache.has(agentName)) {
|
|
202
|
-
const config = builtin.get(agentName);
|
|
203
|
-
if (config) this.cache.set(agentName, config);
|
|
139
|
+
/**
|
|
140
|
+
* 按绝对路径加载 agent(agentRef 统一解析入口——S2 路径统一)。
|
|
141
|
+
*
|
|
142
|
+
* - ~/ 前缀展开;相对路径/非 .md 引用返回 undefined(引用唯一形态 = 绝对路径)
|
|
143
|
+
* - 文件不可读/不存在 → 驱逐缓存 + 返回 undefined(调用方给错误指引)
|
|
144
|
+
* - mtime 未变复用 config 缓存;cache-miss 时 W4 lint 一次
|
|
145
|
+
*/
|
|
146
|
+
loadByPath(ref: string, require?: boolean): AgentConfig | undefined {
|
|
147
|
+
const filePath = normalizeRef(ref, AGENT_REF_EXT);
|
|
148
|
+
if (filePath === null) {
|
|
149
|
+
if (require) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Invalid agent ref: ${ref}. Agent refs must be absolute paths to .md files (use <location> from <available_subagents>).`,
|
|
152
|
+
);
|
|
204
153
|
}
|
|
154
|
+
return undefined;
|
|
205
155
|
}
|
|
206
156
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
157
|
+
const file = getCachedFile(filePath);
|
|
158
|
+
if (file === null) {
|
|
159
|
+
// ENOENT/不可读 → 驱逐(mtime 缓存下文件删除不自愈的修复)
|
|
160
|
+
this.fileCache.delete(filePath);
|
|
161
|
+
if (require) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Agent file not found or unreadable: ${filePath}. Use an absolute path from <available_subagents> <location>.`,
|
|
164
|
+
);
|
|
211
165
|
}
|
|
166
|
+
return undefined;
|
|
212
167
|
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** 按 name 查找。require=false 时找不到返回 undefined;true 时抛错。 */
|
|
216
|
-
get(name: string, require?: boolean): AgentConfig | undefined {
|
|
217
|
-
const config = this.cache.get(name);
|
|
218
|
-
if (!config && require) {
|
|
219
|
-
throw new Error(
|
|
220
|
-
`Agent "${name}" not found. Discovered: ${[...this.cache.keys()].join(", ") || "(none)"}`,
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
return config;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** 列出所有已发现 agent 名(诊断/wizard 用)。 */
|
|
227
|
-
list(): string[] {
|
|
228
|
-
return [...this.cache.keys()];
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ── 内部 ──────────────────────────────────────────────────
|
|
232
168
|
|
|
233
|
-
/** 带 mtime 缓存的单文件加载。mtime 未变复用缓存,否则 read+parse。 */
|
|
234
|
-
private loadWithMtimeCache(filePath: string): AgentConfig | undefined {
|
|
235
|
-
const stat = fs.statSync(filePath);
|
|
236
|
-
const mtimeMs = stat.mtimeMs;
|
|
237
169
|
const cached = this.fileCache.get(filePath);
|
|
238
|
-
if (cached && cached.mtimeMs === mtimeMs) {
|
|
170
|
+
if (cached && cached.mtimeMs === file.mtimeMs) {
|
|
239
171
|
return cached.config;
|
|
240
172
|
}
|
|
241
|
-
|
|
242
|
-
const config =
|
|
243
|
-
|
|
173
|
+
|
|
174
|
+
const { config, meta } = parseAgentWithMeta(filePath, file.content);
|
|
175
|
+
// W4 lint:cache-miss(parse 时一次)——warn 只随 mtime 变化刷
|
|
176
|
+
const lintFindings = meta ? lintAgentMeta(meta) : [];
|
|
177
|
+
for (const finding of lintFindings) {
|
|
178
|
+
logger.warn(`[agent-registry] ${filePath}: ${finding.message}`);
|
|
179
|
+
}
|
|
180
|
+
this.fileCache.set(filePath, { mtimeMs: file.mtimeMs, config, meta });
|
|
244
181
|
return config;
|
|
245
182
|
}
|
|
246
183
|
}
|
|
247
|
-
|
|
248
|
-
// ============================================================
|
|
249
|
-
// 包内 agent 发现(builtin 用,走 pi.agents manifest)
|
|
250
|
-
// ============================================================
|
|
251
|
-
|
|
252
|
-
import { processPackageSync } from "../shared/resource-discovery.ts";
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* 发现包内 agent 文件(走 pi.agents manifest 或约定目录 agents/)。
|
|
256
|
-
* builtin 专用——不参与优先级合并,优先级最低。
|
|
257
|
-
*/
|
|
258
|
-
function discoverPackageAgentsSync(packageRoot: string): DiscoveredResource[] {
|
|
259
|
-
return processPackageSync(packageRoot, "agents");
|
|
260
|
-
}
|
|
@@ -34,7 +34,7 @@ export const SLUG_MAX_LENGTH = 35;
|
|
|
34
34
|
* ctxModel → ctxModel(完整 ModelInfo 对象透传,让 resolveModel 走第 3 层透明传递)
|
|
35
35
|
* skillPath → skillPath
|
|
36
36
|
* thinkingLevel → thinkingLevel(M1: 否则下游 subagent-service 读到 undefined)
|
|
37
|
-
*
|
|
37
|
+
* appendSystemPrompt → appendSystemPrompt(内容数组,同名同义透传)
|
|
38
38
|
*
|
|
39
39
|
* 忽略字段(委托后由 executeAndAwait 内部机制替代):
|
|
40
40
|
* timeoutMs —— mergeTimeoutSignal 单独处理
|
|
@@ -61,7 +61,7 @@ export function mapToExecuteOptions(
|
|
|
61
61
|
ctxModel,
|
|
62
62
|
skillPath: opts.skillPath,
|
|
63
63
|
thinkingLevel: opts.thinkingLevel,
|
|
64
|
-
appendSystemPrompt: opts.
|
|
64
|
+
appendSystemPrompt: opts.appendSystemPrompt,
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
// 上游:SubagentService.execute 内部调 resolveModel。
|
|
7
7
|
// session_start 时经 initModel 注入 modelRegistry。
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import { AgentRegistry, createPackageBuiltinRegistry } from "./agent-registry.ts";
|
|
9
|
+
import { AgentRegistry } from "./agent-registry.ts";
|
|
11
10
|
import {
|
|
12
11
|
loadGlobalConfig,
|
|
13
12
|
} from "./config.ts";
|
|
@@ -69,25 +68,15 @@ export class ModelConfigService {
|
|
|
69
68
|
private globalConfig: SubagentsGlobalConfig;
|
|
70
69
|
private readonly agentRegistry: AgentRegistry;
|
|
71
70
|
private readonly agentRegistryDir: string;
|
|
72
|
-
private readonly workspaceRoot: string;
|
|
73
71
|
private modelRegistry: ModelRegistryLike | null = null;
|
|
74
72
|
private _sessionId: string | undefined;
|
|
75
73
|
/** 主 agent 当前 model 缓存(session_start 注入,model_select 刷新)。 */
|
|
76
74
|
private _ctxModel: ModelInfo | undefined;
|
|
77
75
|
|
|
78
|
-
/** 包内 builtin agent(agents/*.md,优先级最低,被用户覆盖)。 */
|
|
79
|
-
private readonly builtinRegistry = createPackageBuiltinRegistry();
|
|
80
|
-
|
|
81
76
|
constructor(init: ModelConfigServiceInit) {
|
|
82
77
|
this.agentRegistryDir = init.agentDir;
|
|
83
|
-
this.workspaceRoot = findWorkspaceRoot(init.cwd);
|
|
84
78
|
this.globalConfig = loadGlobalConfig(init.agentDir);
|
|
85
|
-
|
|
86
|
-
this.agentRegistry = new AgentRegistry({
|
|
87
|
-
workspaceRoot: this.workspaceRoot,
|
|
88
|
-
agentDir: this.agentRegistryDir,
|
|
89
|
-
});
|
|
90
|
-
this.agentRegistry.discoverAll(this.builtinRegistry);
|
|
79
|
+
this.agentRegistry = new AgentRegistry();
|
|
91
80
|
}
|
|
92
81
|
|
|
93
82
|
// ── 生命周期(index.ts 调)──────────────────────────────
|
|
@@ -99,9 +88,8 @@ export class ModelConfigService {
|
|
|
99
88
|
* 3. setSessionId
|
|
100
89
|
*/
|
|
101
90
|
initModel(init: ModelServiceSessionInit): void {
|
|
102
|
-
// 1.
|
|
91
|
+
// 1. 重载配置(agent 按需 loadByPath,无预热扫描)
|
|
103
92
|
this.globalConfig = loadGlobalConfig(this.agentRegistryDir);
|
|
104
|
-
this.agentRegistry.discoverAll(this.builtinRegistry);
|
|
105
93
|
|
|
106
94
|
// 2. modelRegistry(fail-fast)
|
|
107
95
|
if (init.modelRegistry === null) {
|
|
@@ -127,24 +115,26 @@ export class ModelConfigService {
|
|
|
127
115
|
/**
|
|
128
116
|
* 解析 agent 的模型(三层:override → agentConfig → 主 agent model)。
|
|
129
117
|
*
|
|
130
|
-
* @param
|
|
131
|
-
* @param override
|
|
132
|
-
* @param ctxModel
|
|
118
|
+
* @param agentRef agent 引用(.md 绝对路径;查 agentConfig 的 model override)
|
|
119
|
+
* @param override 调用方显式 override(最高优先级)
|
|
120
|
+
* @param ctxModel 主 agent 当前模型(兜底,直接透传)
|
|
133
121
|
*/
|
|
134
122
|
resolveModel(
|
|
135
|
-
|
|
123
|
+
agentRef: string,
|
|
136
124
|
override?: { model?: string; thinkingLevel?: string },
|
|
137
125
|
ctxModel?: ModelInfo,
|
|
126
|
+
/** 已解析的 agent 配置(调用方已加载时复用,避免同一 agentRef 二次 loadByPath)。 */
|
|
127
|
+
agentConfig?: AgentConfig,
|
|
138
128
|
): ResolvedModel {
|
|
139
129
|
this.assertReady();
|
|
140
|
-
const
|
|
130
|
+
const config = agentConfig ?? (agentRef ? this.agentRegistry.loadByPath(agentRef) : undefined);
|
|
141
131
|
// ctxModel 优先用显式传入(execute 路径),其次用 session 缓存(renderCall 路径)
|
|
142
|
-
return resolveModel(
|
|
132
|
+
return resolveModel(config, this.modelRegistry!, override, ctxModel ?? this._ctxModel);
|
|
143
133
|
}
|
|
144
134
|
|
|
145
135
|
/** 查询 agent 配置(SubagentService 内部判定 defaultBackground 用)。 */
|
|
146
|
-
getAgentConfig(
|
|
147
|
-
return
|
|
136
|
+
getAgentConfig(agentRef?: string): AgentConfig | undefined {
|
|
137
|
+
return agentRef ? this.agentRegistry.loadByPath(agentRef) : undefined;
|
|
148
138
|
}
|
|
149
139
|
|
|
150
140
|
// ── 配置读取(subagent-service 调)────────────────────────
|
|
@@ -164,17 +154,6 @@ export class ModelConfigService {
|
|
|
164
154
|
return this.agentRegistryDir;
|
|
165
155
|
}
|
|
166
156
|
|
|
167
|
-
/**
|
|
168
|
-
* AgentRegistry 实例(workflow 域复用,F-4/D-003 统一)。
|
|
169
|
-
*
|
|
170
|
-
* workflow 域(resolveAgentOpts via LifecycleDeps.agentRegistry)需要与 subagents 域
|
|
171
|
-
* 完全相同的 agent 名→config 解析,复用本实例避免二次扫描。registry 在 initModel
|
|
172
|
-
* (每次 session_start)调 discoverAll hot-reload,两域共享同一份发现结果。
|
|
173
|
-
*/
|
|
174
|
-
getAgentRegistry(): AgentRegistry {
|
|
175
|
-
return this.agentRegistry;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
157
|
/** modelRegistry(SubagentService 构造 factoryCtx 时读)。已注入保证非 null。 */
|
|
179
158
|
getModelRegistry(): ModelRegistryLike {
|
|
180
159
|
if (this.modelRegistry === null) {
|
|
@@ -60,8 +60,10 @@ export interface ResolvedModel {
|
|
|
60
60
|
// 常量
|
|
61
61
|
// ============================================================
|
|
62
62
|
|
|
63
|
-
/** thinking level 支持顺序(低→高),用于 clamp 到 model 可用级别。
|
|
64
|
-
|
|
63
|
+
/** thinking level 支持顺序(低→高),用于 clamp 到 model 可用级别。
|
|
64
|
+
* SSOT(单一权威源):新增 thinking 级别只改此处,subagent-tool 的 thinkingLevel 枚举
|
|
65
|
+
* 从本常量派生,避免两处硬编码不同步。 */
|
|
66
|
+
export const THINKING_ORDER = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
65
67
|
|
|
66
68
|
/** 解析失败时错误信息列出的可用模型上限(防超长错误信息)。 */
|
|
67
69
|
const MODEL_LIST_LIMIT = 20;
|
|
@@ -102,7 +104,7 @@ export function resolveModel(
|
|
|
102
104
|
if (paramOverride?.model) {
|
|
103
105
|
return lookupAndResolve(
|
|
104
106
|
paramOverride.model,
|
|
105
|
-
paramOverride.thinkingLevel,
|
|
107
|
+
paramOverride.thinkingLevel ?? agentConfig?.thinkingLevel,
|
|
106
108
|
modelRegistry,
|
|
107
109
|
"paramOverride",
|
|
108
110
|
);
|
|
@@ -394,8 +394,9 @@ export class SubagentService {
|
|
|
394
394
|
agent: string,
|
|
395
395
|
override?: { model?: string; thinkingLevel?: string },
|
|
396
396
|
ctxModel?: ModelInfo,
|
|
397
|
+
agentConfig?: AgentConfig,
|
|
397
398
|
): ResolvedModel {
|
|
398
|
-
return this.modelService.resolveModel(agent, override, ctxModel);
|
|
399
|
+
return this.modelService.resolveModel(agent, override, ctxModel, agentConfig);
|
|
399
400
|
}
|
|
400
401
|
|
|
401
402
|
/**
|
|
@@ -599,7 +600,7 @@ export class SubagentService {
|
|
|
599
600
|
// - **不可作为后续 agent 的 cwd**——目录已删,复用会 ENOENT。
|
|
600
601
|
// - wave 内 worktree 复用(spec-w §2 "wave 内 8 action 共享 worktree")在 pi 当前架构下
|
|
601
602
|
// 不可行:worktree 绑定单次 agent() record,每次 executeAndAwait 结束 finalizeRecord
|
|
602
|
-
// 无条件 cleanup,worktree 无法跨 action 存活。wave 改用主 cwd
|
|
603
|
+
// 无条件 cleanup,worktree 无法跨 action 存活。wave 改用主 cwd。
|
|
603
604
|
wfResult.worktreePath = record.worktreeHandle?.path;
|
|
604
605
|
return wfResult;
|
|
605
606
|
}
|
|
@@ -626,15 +627,17 @@ export class SubagentService {
|
|
|
626
627
|
|
|
627
628
|
/** 步骤 1:身份解析。agentConfig → resolveModel(三层:override → agentConfig → 主 agent model)。 */
|
|
628
629
|
private async resolveIdentity(opts: ExecuteOptions): Promise<ResolvedIdentity> {
|
|
629
|
-
//
|
|
630
|
-
//
|
|
630
|
+
// agentRef 语义(S2):agent 参数 = .md 绝对路径;不传 = 不加载 agentConfig,
|
|
631
|
+
// 直接用 override → 主 agent model。DEFAULT_AGENT_NAME 仅作 record 显示名
|
|
632
|
+
// (TUI 层 extractAgentName 共用,保证显示一致)。
|
|
631
633
|
const agent = opts.agent ?? DEFAULT_AGENT_NAME;
|
|
632
|
-
const agentConfig = this.modelService.getAgentConfig(agent);
|
|
634
|
+
const agentConfig = opts.agent ? this.modelService.getAgentConfig(opts.agent) : undefined;
|
|
633
635
|
|
|
634
636
|
const resolved = this.modelService.resolveModel(
|
|
635
|
-
agent,
|
|
637
|
+
opts.agent ?? "",
|
|
636
638
|
{ model: opts.model, thinkingLevel: opts.thinkingLevel },
|
|
637
639
|
opts.ctxModel,
|
|
640
|
+
agentConfig,
|
|
638
641
|
);
|
|
639
642
|
|
|
640
643
|
return { agent, agentConfig, resolved };
|
|
@@ -32,8 +32,9 @@ import type { ExecuteOptions } from "./types.ts";
|
|
|
32
32
|
* - subagentService: 进程单例(getSubagentService()),委托目标
|
|
33
33
|
* - ctxModel: 当前 session 主 agent 模型(D-008 model 填底,opts.model 空时用此)
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* resolveAgentOpts 在 orchestration 层完成(M2 修正后单参数,只处理 schema SO 指令 + skill),
|
|
36
|
+
* 结果填进 AgentCallOpts.skillPath/schemaEnv/appendSystemPrompt,SAR 收已解析的 opts。
|
|
37
|
+
* 不含 agent/session 目录/临时文件集合依赖(agent ref 交 resolveIdentity,无临时文件)。
|
|
37
38
|
*/
|
|
38
39
|
export interface SubprocessAgentRunnerDeps {
|
|
39
40
|
subagentService: SubagentService;
|