@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
|
@@ -111,6 +111,10 @@ interface RunOptions {
|
|
|
111
111
|
timeoutMs?: number;
|
|
112
112
|
/** workerData.callCache 预填(测缓存命中路径)。 */
|
|
113
113
|
callCache?: Map<number, unknown>;
|
|
114
|
+
/** Run 级 model override(透传到 workerData.model → $MODEL global)。 */
|
|
115
|
+
model?: string;
|
|
116
|
+
/** Run 级 thinkingLevel override(透传到 workerData.thinkingLevel → $THINKING_LEVEL global)。 */
|
|
117
|
+
thinkingLevel?: string;
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
/**
|
|
@@ -132,6 +136,9 @@ function runWorker(userScript: string, opts: RunOptions = {}): Promise<RunResult
|
|
|
132
136
|
callCache: opts.callCache instanceof Map
|
|
133
137
|
? Object.fromEntries(opts.callCache)
|
|
134
138
|
: opts.callCache ?? {},
|
|
139
|
+
// Option B run-level override:透传到 $MODEL/$THINKING_LEVEL worker global
|
|
140
|
+
model: opts.model,
|
|
141
|
+
thinkingLevel: opts.thinkingLevel,
|
|
135
142
|
},
|
|
136
143
|
});
|
|
137
144
|
// S8:创建后立即登记,afterEach 兜底清理(防止 promise 泄漏导致 Worker 未终止)
|
|
@@ -449,3 +456,65 @@ describe("buildWorkerScript runtime — W2 agent() returnMeta mode", () => {
|
|
|
449
456
|
});
|
|
450
457
|
});
|
|
451
458
|
|
|
459
|
+
// ── P3/P4 runtime: run-level model/thinkingLevel override 真实注入(L0→L1 升级)──
|
|
460
|
+
// worker-script-builder.test.ts 的 P3/P4 block 全是源码字符串断言(L0:验证「生成的
|
|
461
|
+
// 源码含 $MODEL 注入行」)。本组起真实 Worker 线程,传 workerData.model/thinkingLevel,
|
|
462
|
+
// 断言 agent() 三分支产出的 agent-call 消息 opts.model/opts.thinkingLevel 真实继承
|
|
463
|
+
// $MODEL/$THINKING_LEVEL global(L1:验证「运行时 agent-call 携带正确 override」)。
|
|
464
|
+
// 对应 docs/testing/ 断言价值层级 L1(真实 Worker 产物,非源码字符串)。
|
|
465
|
+
|
|
466
|
+
describe("buildWorkerScript runtime — P3/P4 run-level model/thinkingLevel override 真实注入", () => {
|
|
467
|
+
it("workerData.model 经 $MODEL global 注入到 object 分支 agent() opts.model", async () => {
|
|
468
|
+
const script = `await agent({ prompt: "hi" }); return { done: true };`;
|
|
469
|
+
const res = await runWorker(script, { model: "anthropic/claude-sonnet-4-5" });
|
|
470
|
+
expect(res.agentCalls).toHaveLength(1);
|
|
471
|
+
expect(res.agentCalls[0]!.opts.model).toBe("anthropic/claude-sonnet-4-5");
|
|
472
|
+
expect(res.workerError).toBeUndefined();
|
|
473
|
+
expect(res.exitCode).not.toBe(1);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("workerData.thinkingLevel 经 $THINKING_LEVEL global 注入到 opts.thinkingLevel", async () => {
|
|
477
|
+
const script = `await agent({ prompt: "hi" }); return { done: true };`;
|
|
478
|
+
const res = await runWorker(script, { thinkingLevel: "high" });
|
|
479
|
+
expect(res.agentCalls).toHaveLength(1);
|
|
480
|
+
expect(res.agentCalls[0]!.opts.thinkingLevel).toBe("high");
|
|
481
|
+
expect(res.workerError).toBeUndefined();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("per-call model 优先于 $MODEL global(显式传 model 时不被 override 覆盖)", async () => {
|
|
485
|
+
const script = `await agent({ prompt: "hi", model: "openai/gpt-4o" }); return {};`;
|
|
486
|
+
const res = await runWorker(script, { model: "anthropic/claude-sonnet-4-5" });
|
|
487
|
+
expect(res.agentCalls).toHaveLength(1);
|
|
488
|
+
expect(res.agentCalls[0]!.opts.model).toBe("openai/gpt-4o");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("agent() 三分支一致继承 $MODEL(string / task / object.prompt)", async () => {
|
|
492
|
+
const script = `
|
|
493
|
+
await agent("str-branch");
|
|
494
|
+
await agent({ task: "t" });
|
|
495
|
+
await agent({ prompt: "obj" });
|
|
496
|
+
return {};
|
|
497
|
+
`;
|
|
498
|
+
const res = await runWorker(script, {
|
|
499
|
+
model: "X/Y",
|
|
500
|
+
agentResults: [{}, {}, {}],
|
|
501
|
+
});
|
|
502
|
+
expect(res.agentCalls).toHaveLength(3);
|
|
503
|
+
expect(res.agentCalls.map((c) => c.opts.model)).toEqual(["X/Y", "X/Y", "X/Y"]);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("model+thinkingLevel 同时注入(Option B 对称验证)", async () => {
|
|
507
|
+
const script = `await agent({ prompt: "hi" }); return {};`;
|
|
508
|
+
const res = await runWorker(script, { model: "p/m", thinkingLevel: "max" });
|
|
509
|
+
expect(res.agentCalls[0]!.opts.model).toBe("p/m");
|
|
510
|
+
expect(res.agentCalls[0]!.opts.thinkingLevel).toBe("max");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("不传 model 时 opts.model 为 undefined(零配置默认继承主 agent,不误注入)", async () => {
|
|
514
|
+
const script = `await agent({ prompt: "hi" }); return {};`;
|
|
515
|
+
const res = await runWorker(script);
|
|
516
|
+
expect(res.agentCalls).toHaveLength(1);
|
|
517
|
+
expect(res.agentCalls[0]!.opts.model).toBeUndefined();
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
@@ -237,7 +237,7 @@ describe("buildWorkerScript — agent() thinkingLevel passthrough (3 branches)",
|
|
|
237
237
|
const stringBranch = script.match(/typeof firstArg === "string"[\s\S]*?\};/);
|
|
238
238
|
expect(stringBranch).toBeTruthy();
|
|
239
239
|
expect(stringBranch![0]).toContain(
|
|
240
|
-
"thinkingLevel: (secondArg && typeof secondArg === \"object\" && secondArg.thinkingLevel) ||
|
|
240
|
+
"thinkingLevel: (secondArg && typeof secondArg === \"object\" && secondArg.thinkingLevel) || $THINKING_LEVEL",
|
|
241
241
|
);
|
|
242
242
|
});
|
|
243
243
|
|
|
@@ -257,3 +257,53 @@ describe("buildWorkerScript — agent() thinkingLevel passthrough (3 branches)",
|
|
|
257
257
|
});
|
|
258
258
|
});
|
|
259
259
|
|
|
260
|
+
// ── P3/P4: run-level model/thinkingLevel override global injection ──
|
|
261
|
+
// Option B 对称单路径:workflow 顶层 model/thinkingLevel 经 workerData →
|
|
262
|
+
// $MODEL/$THINKING_LEVEL worker global → agent() 三分支 fallback。
|
|
263
|
+
// 验证生成的 worker 源码字符串包含 global 注入与三分支 fallback 逻辑。
|
|
264
|
+
// 运行时真生效需 worker 集成 harness(设计文档 §6 降级为源码字符串断言)。
|
|
265
|
+
|
|
266
|
+
describe("P3/P4 run-level model/thinkingLevel global injection + agent() fallback", () => {
|
|
267
|
+
const script = buildWorkerScript("// noop user script");
|
|
268
|
+
|
|
269
|
+
it("injects $MODEL global from workerData.model", () => {
|
|
270
|
+
expect(script).toContain('const $MODEL = (workerData.model && typeof workerData.model === "string") ? workerData.model : undefined;');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("injects $THINKING_LEVEL global from workerData.thinkingLevel", () => {
|
|
274
|
+
expect(script).toContain('const $THINKING_LEVEL = (workerData.thinkingLevel && typeof workerData.thinkingLevel === "string") ? workerData.thinkingLevel : undefined;');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("string branch falls back to $MODEL when secondArg.model omitted", () => {
|
|
278
|
+
const stringBranch = script.match(/typeof firstArg === "string"[\s\S]*?\};/);
|
|
279
|
+
expect(stringBranch).toBeTruthy();
|
|
280
|
+
expect(stringBranch![0]).toContain('secondArg.model) || $MODEL');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("string branch falls back to $THINKING_LEVEL when secondArg.thinkingLevel omitted", () => {
|
|
284
|
+
const stringBranch = script.match(/typeof firstArg === "string"[\s\S]*?\};/);
|
|
285
|
+
expect(stringBranch).toBeTruthy();
|
|
286
|
+
expect(stringBranch![0]).toContain('secondArg.thinkingLevel) || $THINKING_LEVEL');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("task/agent branch falls back to $MODEL when firstArg.model omitted", () => {
|
|
290
|
+
const taskAgentBranch = script.match(/firstArg\.task \|\| firstArg\.agent[\s\S]*?\};/);
|
|
291
|
+
expect(taskAgentBranch).toBeTruthy();
|
|
292
|
+
expect(taskAgentBranch![0]).toContain('model: firstArg.model || $MODEL');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("task/agent branch falls back to $THINKING_LEVEL when firstArg.thinkingLevel omitted", () => {
|
|
296
|
+
const taskAgentBranch = script.match(/firstArg\.task \|\| firstArg\.agent[\s\S]*?\};/);
|
|
297
|
+
expect(taskAgentBranch).toBeTruthy();
|
|
298
|
+
expect(taskAgentBranch![0]).toContain('thinkingLevel: firstArg.thinkingLevel || $THINKING_LEVEL');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("object branch injects $MODEL when opts.model omitted (guard fallback)", () => {
|
|
302
|
+
expect(script).toContain('if (!opts.model && $MODEL) opts.model = $MODEL;');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("object branch injects $THINKING_LEVEL when opts.thinkingLevel omitted (guard fallback)", () => {
|
|
306
|
+
expect(script).toContain('if (!opts.thinkingLevel && $THINKING_LEVEL) opts.thinkingLevel = $THINKING_LEVEL;');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
@@ -122,12 +122,12 @@ function makeDoneChildRun(opts: {
|
|
|
122
122
|
function makeDeps(opts: {
|
|
123
123
|
script?: WorkflowScript;
|
|
124
124
|
childRun?: WorkflowRun;
|
|
125
|
-
registry?: {
|
|
125
|
+
registry?: { getPath: ReturnType<typeof vi.fn> };
|
|
126
126
|
} = {}): LauncherDeps {
|
|
127
127
|
const runs = new Map<string, WorkflowRun>();
|
|
128
128
|
if (opts.childRun) runs.set(MOCK_RUN_ID, opts.childRun);
|
|
129
129
|
const registry = opts.registry ?? {
|
|
130
|
-
|
|
130
|
+
getPath: vi.fn(async () => opts.script),
|
|
131
131
|
};
|
|
132
132
|
return {
|
|
133
133
|
registry,
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* 内容 + 手动构造 WorkflowScript 对象,包装为一个满足 WorkflowScriptRegistry 接口
|
|
25
25
|
* 的自定义 registry(loadWorkflowsFromDir)。
|
|
26
26
|
*/
|
|
27
|
-
import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
27
|
+
import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
28
28
|
import { tmpdir } from "node:os";
|
|
29
29
|
import { dirname, join } from "node:path";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
@@ -33,6 +33,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
33
33
|
|
|
34
34
|
import { JsonlRunStore } from "../jsonl-run-store.ts";
|
|
35
35
|
import { type LauncherDeps,runAndWait } from "../launcher.ts";
|
|
36
|
+
import { actionRun } from "../../interface/tool-workflow.ts";
|
|
36
37
|
import type { LifecycleDeps } from "../models/ports.ts";
|
|
37
38
|
import type { AgentRunner } from "../models/ports.ts";
|
|
38
39
|
import type { AgentResult, AgentUsage } from "../models/types.ts";
|
|
@@ -41,6 +42,8 @@ import {
|
|
|
41
42
|
WorkflowScript,
|
|
42
43
|
type WorkflowSource,
|
|
43
44
|
} from "../models/workflow-script.ts";
|
|
45
|
+
import { parseResourceMeta } from "../../shared/meta-parser.ts";
|
|
46
|
+
import { normalizeRef } from "../../shared/agent-ref.ts";
|
|
44
47
|
import type { WorkflowScriptRegistry } from "../models/workflow-script-registry.ts";
|
|
45
48
|
import { WorkerHostImpl } from "../worker-host.ts";
|
|
46
49
|
|
|
@@ -49,6 +52,10 @@ import { WorkerHostImpl } from "../worker-host.ts";
|
|
|
49
52
|
// 即 __dirname → .. (orchestration) → .. (src) → .. (subagent-workflow) → workflows
|
|
50
53
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
51
54
|
const WORKFLOWS_DIR = join(__dirname, "..", "..", "..", "workflows");
|
|
55
|
+
const AGENTS_DIR = join(__dirname, "..", "..", "..", "agents");
|
|
56
|
+
// S2:workflowRef/agentRef = 绝对路径(注入段 <location> 同源)
|
|
57
|
+
const wf = (name: string): string => join(WORKFLOWS_DIR, name + ".js");
|
|
58
|
+
const agentMd = (name: string): string => join(AGENTS_DIR, name + ".md");
|
|
52
59
|
|
|
53
60
|
// ── 临时 session 目录(RunStore 持久化根),每用例重建 ──────────────────
|
|
54
61
|
let sessionDir: string;
|
|
@@ -147,25 +154,12 @@ function makeMockRunner(): AgentRunner & { run: ReturnType<typeof vi.fn> } {
|
|
|
147
154
|
* 同语义,避免执行用户代码)。失败时回落到 name=文件名 stem 的空 meta。
|
|
148
155
|
*/
|
|
149
156
|
function extractMeta(source: string, fallbackName: string): WorkflowMeta {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (obj && typeof obj === "object" && typeof obj.name === "string") {
|
|
157
|
-
return {
|
|
158
|
-
name: obj.name,
|
|
159
|
-
description: typeof obj.description === "string" ? obj.description : "",
|
|
160
|
-
phases: Array.isArray(obj.phases) ? obj.phases : [],
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
} catch (e) {
|
|
164
|
-
// meta 提取失败(非法 JS / regex 不匹配)→ 回落 fallback name,非测试关注点
|
|
165
|
-
void e;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return { name: fallbackName, description: "", phases: [] };
|
|
157
|
+
// m2 exec-review MINOR-1:旧 const meta regex + new Function 随 m2 迁移已失效(恒走空
|
|
158
|
+
// fallback,不再验证真实 meta 提取),改调 IF1 parseResourceMeta(与
|
|
159
|
+
// builtin-workflows-structure.test 一致)。失败时回落到 name=文件名 stem 的空 meta。
|
|
160
|
+
const meta = parseResourceMeta(source, "workflow");
|
|
161
|
+
if (meta && meta.kind === "workflow") return meta;
|
|
162
|
+
return { kind: "workflow", name: fallbackName, description: "", phases: [] };
|
|
169
163
|
}
|
|
170
164
|
|
|
171
165
|
/**
|
|
@@ -208,6 +202,29 @@ function loadWorkflowsFromDir(dir: string): Map<string, WorkflowScript> {
|
|
|
208
202
|
function makeRegistry(scripts: Map<string, WorkflowScript>): WorkflowScriptRegistry {
|
|
209
203
|
return {
|
|
210
204
|
get: async (name: string) => scripts.get(name),
|
|
205
|
+
// S2:按路径加载(任意路径 .js,不限扫描目录)——路径未预扫则直接读文件
|
|
206
|
+
getPath: async (ref: string) => {
|
|
207
|
+
const normalized = normalizeRef(ref, ".js");
|
|
208
|
+
if (normalized === null) return undefined;
|
|
209
|
+
for (const script of scripts.values()) {
|
|
210
|
+
if (script.path === normalized) return script;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const sourceCode = readFileSync(normalized, "utf-8");
|
|
214
|
+
const stem = basename(normalized, ".js");
|
|
215
|
+
const meta = extractMeta(sourceCode, stem);
|
|
216
|
+
return new WorkflowScript({
|
|
217
|
+
name: meta.name,
|
|
218
|
+
source: "saved",
|
|
219
|
+
path: normalized,
|
|
220
|
+
sourceCode,
|
|
221
|
+
meta,
|
|
222
|
+
available: true,
|
|
223
|
+
});
|
|
224
|
+
} catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
},
|
|
211
228
|
loadAll: async () => Array.from(scripts.values()),
|
|
212
229
|
invalidate: () => {},
|
|
213
230
|
};
|
|
@@ -292,7 +309,7 @@ describe("内置 workflow E2E(真实 worker thread + mock LLM runner)", () =
|
|
|
292
309
|
async () => {
|
|
293
310
|
const deps = makeDeps();
|
|
294
311
|
const result = await runAndWait(
|
|
295
|
-
"parallel",
|
|
312
|
+
wf("parallel"),
|
|
296
313
|
{ target: "src/auth/login.ts" },
|
|
297
314
|
deps,
|
|
298
315
|
undefined,
|
|
@@ -315,7 +332,7 @@ describe("内置 workflow E2E(真实 worker thread + mock LLM runner)", () =
|
|
|
315
332
|
async () => {
|
|
316
333
|
const deps = makeDeps();
|
|
317
334
|
const result = await runAndWait(
|
|
318
|
-
"chain",
|
|
335
|
+
wf("chain"),
|
|
319
336
|
{ task: "把这段需求文档拆成技术任务" },
|
|
320
337
|
deps,
|
|
321
338
|
undefined,
|
|
@@ -338,7 +355,7 @@ describe("内置 workflow E2E(真实 worker thread + mock LLM runner)", () =
|
|
|
338
355
|
async () => {
|
|
339
356
|
const deps = makeDeps();
|
|
340
357
|
const result = await runAndWait(
|
|
341
|
-
"map-reduce",
|
|
358
|
+
wf("map-reduce"),
|
|
342
359
|
{ operation: "审查代码风格", items: ["file1.ts", "file2.ts"] },
|
|
343
360
|
deps,
|
|
344
361
|
undefined,
|
|
@@ -363,7 +380,7 @@ describe("内置 workflow E2E(真实 worker thread + mock LLM runner)", () =
|
|
|
363
380
|
async () => {
|
|
364
381
|
const deps = makeDeps();
|
|
365
382
|
const result = await runAndWait(
|
|
366
|
-
"scatter-gather",
|
|
383
|
+
wf("scatter-gather"),
|
|
367
384
|
{ task: "重构认证模块,涉及 session/jwt/oauth 三块" },
|
|
368
385
|
deps,
|
|
369
386
|
undefined,
|
|
@@ -383,4 +400,140 @@ describe("内置 workflow E2E(真实 worker thread + mock LLM runner)", () =
|
|
|
383
400
|
},
|
|
384
401
|
RUN_TIMEOUT_MS,
|
|
385
402
|
);
|
|
403
|
+
|
|
404
|
+
it(
|
|
405
|
+
"TC10: runAndWait 参数校验失败 → reason=invalid_args + runId='' + info 指引(chokepoint 先拦)",
|
|
406
|
+
async () => {
|
|
407
|
+
// review-fix-loop 有 parameters schema(唯一带 schema 的内置 workflow)——
|
|
408
|
+
// 缺 required targetType/target → chokepoint 在 worker 启动前拦截。
|
|
409
|
+
const deps = makeDeps();
|
|
410
|
+
const result = await runAndWait(
|
|
411
|
+
wf("review-fix-loop"),
|
|
412
|
+
{ batch1: agentMd("code-reviewer") },
|
|
413
|
+
deps,
|
|
414
|
+
undefined,
|
|
415
|
+
RUN_TIMEOUT_MS,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
expect(result.status).toBe("done");
|
|
419
|
+
expect(result.reason).toBe("invalid_args");
|
|
420
|
+
expect(result.runId).toBe("");
|
|
421
|
+
expect(result.error).toContain("Invalid args for workflow 'review-fix-loop'");
|
|
422
|
+
expect(result.error).toContain("targetType");
|
|
423
|
+
expect(result.error).toContain("Read the workflow script file");
|
|
424
|
+
},
|
|
425
|
+
RUN_TIMEOUT_MS,
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
it(
|
|
429
|
+
"TC12: coerce 结果到达 worker(chokepoint 原地 coerce → $ARGS 收到 boolean false)",
|
|
430
|
+
async () => {
|
|
431
|
+
// fixture 必须过 lint 入口检查(含 agent( 调用)+ typecheckMeta(name/description/phases)
|
|
432
|
+
const fixtureDir = mkdtempSync(join(tmpdir(), "wf-tc12-"));
|
|
433
|
+
try {
|
|
434
|
+
writeFileSync(
|
|
435
|
+
join(fixtureDir, "args-probe.js"),
|
|
436
|
+
[
|
|
437
|
+
"/* @pi-meta",
|
|
438
|
+
"name: args-probe",
|
|
439
|
+
"description: TC12 coerce probe fixture",
|
|
440
|
+
"phases: [a]",
|
|
441
|
+
"parameters:",
|
|
442
|
+
" type: object",
|
|
443
|
+
" properties:",
|
|
444
|
+
" autoCommit: { type: boolean }",
|
|
445
|
+
" required: [autoCommit]",
|
|
446
|
+
"*/",
|
|
447
|
+
'module.exports.execute = async ({ agent, $ARGS }) => {',
|
|
448
|
+
' await agent({ prompt: "x" });',
|
|
449
|
+
" return { t: typeof $ARGS.autoCommit, v: $ARGS.autoCommit };",
|
|
450
|
+
"};",
|
|
451
|
+
"",
|
|
452
|
+
].join("\n"),
|
|
453
|
+
"utf-8",
|
|
454
|
+
);
|
|
455
|
+
const scripts = loadWorkflowsFromDir(fixtureDir);
|
|
456
|
+
const deps = { ...makeDeps(), registry: makeRegistry(scripts) };
|
|
457
|
+
|
|
458
|
+
const result = await runAndWait(
|
|
459
|
+
join(fixtureDir, "args-probe.js"),
|
|
460
|
+
{ autoCommit: "false" },
|
|
461
|
+
deps,
|
|
462
|
+
undefined,
|
|
463
|
+
RUN_TIMEOUT_MS,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
expect(result.reason).toBe("completed");
|
|
467
|
+
const outcome = result.scriptResult as { t: string; v: unknown };
|
|
468
|
+
expect(outcome.t).toBe("boolean");
|
|
469
|
+
expect(outcome.v).toBe(false);
|
|
470
|
+
} finally {
|
|
471
|
+
rmSync(fixtureDir, { recursive: true, force: true });
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
RUN_TIMEOUT_MS,
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
it("TC4: actionRun 顺序——not_found 优先(平铺 + 不存在名);slug 护栏保留;chain 平铺 Correct 文案", async () => {
|
|
478
|
+
const deps = makeDeps();
|
|
479
|
+
// 平铺 + 不存在 name → not_found 优先(m6:registry.get 先于平铺检测)
|
|
480
|
+
const notFound = await actionRun(
|
|
481
|
+
{ action: "run", name: wf("nope"), task: "x" },
|
|
482
|
+
deps,
|
|
483
|
+
undefined,
|
|
484
|
+
);
|
|
485
|
+
expect(notFound.isError).toBe(true);
|
|
486
|
+
expect(notFound.content![0]!.text).toContain("not found"); // DoD 用户可见断言
|
|
487
|
+
// R5(D7):workflow not_found 文案含 <available_workflows> <location> 恢复指引
|
|
488
|
+
expect(notFound.content![0]!.text).toContain("<available_workflows>");
|
|
489
|
+
expect(notFound.content![0]!.text).toContain("<location>");
|
|
490
|
+
// chain 平铺 task → isError Correct 正例(动态集来自 registry)
|
|
491
|
+
const flat = await actionRun(
|
|
492
|
+
{ action: "run", name: wf("chain"), task: "x" },
|
|
493
|
+
deps,
|
|
494
|
+
undefined,
|
|
495
|
+
);
|
|
496
|
+
expect(flat.isError).toBe(true);
|
|
497
|
+
expect(flat.content![0]!.text).toContain("Detected task at top level");
|
|
498
|
+
expect(flat.content![0]!.text).toContain("Correct:");
|
|
499
|
+
// slug 护栏保留(m6 顺序调整后仍在 runWorkflow 前)
|
|
500
|
+
const slug = await actionRun(
|
|
501
|
+
{ action: "run", name: wf("chain"), args: { task: "x" }, slug: "a".repeat(40) },
|
|
502
|
+
deps,
|
|
503
|
+
undefined,
|
|
504
|
+
);
|
|
505
|
+
expect(slug.isError).toBe(true);
|
|
506
|
+
expect(slug.content![0]!.text).toContain("slug exceeds");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("TC6: 跨 workflow 平铺语义——review-fix-loop 平铺 task(非其参数)走 args-validator", async () => {
|
|
510
|
+
const deps = makeDeps();
|
|
511
|
+
// task 是 chain 参数非 review-fix-loop 参数 → 不报平铺 → args 缺 targetType
|
|
512
|
+
// → m3 chokepoint invalid_args(错误更准——评审 m-5 语义锁定)
|
|
513
|
+
const result = await actionRun(
|
|
514
|
+
{ action: "run", name: wf("review-fix-loop"), task: "x" },
|
|
515
|
+
deps,
|
|
516
|
+
undefined,
|
|
517
|
+
);
|
|
518
|
+
expect(result.isError).toBe(true);
|
|
519
|
+
expect(result.content![0]!.text).toContain("Invalid args for workflow 'review-fix-loop'");
|
|
520
|
+
expect(result.content![0]!.text).toContain("targetType");
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("TC9: actionRun 参数校验失败 → isError ToolResult + §5.3 指引", async () => {
|
|
524
|
+
const deps = makeDeps();
|
|
525
|
+
const result = await actionRun(
|
|
526
|
+
{ action: "run", name: wf("review-fix-loop"), args: { batch1: agentMd("code-reviewer") } },
|
|
527
|
+
deps,
|
|
528
|
+
undefined,
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
expect(result.isError).toBe(true);
|
|
532
|
+
const text = result.content?.[0]?.text ?? "";
|
|
533
|
+
expect(text).toContain("Invalid args for workflow 'review-fix-loop'");
|
|
534
|
+
expect(text).toContain("targetType");
|
|
535
|
+
expect(text).toContain("Read the workflow script file");
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
|
|
386
539
|
});
|
|
@@ -1,83 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent options resolver — resolves
|
|
3
|
-
*
|
|
2
|
+
* Agent options resolver — resolves skill / schema to append-system-prompt
|
|
3
|
+
* content + env vars on every dispatch (BL-1).
|
|
4
4
|
*
|
|
5
|
-
* BL-1:解析 workflow 脚本里 `agent({
|
|
6
|
-
* 否则 pi 子进程只收到原始 prompt,没有 --append-system-prompt /
|
|
7
|
-
*
|
|
5
|
+
* BL-1:解析 workflow 脚本里 `agent({skill,schema})` 的 inline override,
|
|
6
|
+
* 否则 pi 子进程只收到原始 prompt,没有 --append-system-prompt /
|
|
7
|
+
* --skill / PI_WORKFLOW_SCHEMA。
|
|
8
|
+
*
|
|
9
|
+
* 职责范围(M2 修正):仅处理 schema SO 指令(内容直传 appendSystemPrompt)+ skill。
|
|
10
|
+
* agent ref 处理(systemPrompt/model/thinkingLevel)已移交 resolveIdentity(execution 层,
|
|
11
|
+
* 经 getAgentConfig + resolveModel 完整覆盖),消除双重注入与 model 层级混乱。
|
|
8
12
|
*
|
|
9
13
|
* 调用方:engine/error-recovery.ts dispatchAgentCall(每次 agent-call 消息)。
|
|
10
|
-
* - agent → AgentRegistry.resolve → systemPrompt 写临时文件 → systemPromptFiles(--append-system-prompt)
|
|
11
14
|
* - skill → resolveSkillPath → skillPath(--skill)
|
|
12
|
-
* - schema →
|
|
15
|
+
* - schema → 结构化输出指令内容直传 appendSystemPrompt(--append-system-prompt)+ schemaEnv(PI_WORKFLOW_SCHEMA)
|
|
13
16
|
*
|
|
14
|
-
*
|
|
17
|
+
* M2 bug 修正:旧实现把 schema 指令写成临时文件、push 文件路径(而非内容)给下游,
|
|
18
|
+
* 下游 mapper/session-runner 把路径当文本拼进最终 append 文件,导致 schema 指令从未
|
|
19
|
+
* 进入子进程。改为指令内容直传(不写盘、无临时文件)。
|
|
15
20
|
*/
|
|
16
21
|
|
|
17
|
-
import { randomUUID } from "node:crypto";
|
|
18
|
-
import * as fs from "node:fs";
|
|
19
|
-
import * as path from "node:path";
|
|
20
|
-
|
|
21
22
|
import type { AgentCallOpts } from "./models/types.ts";
|
|
22
|
-
import type { AgentRegistry } from "../execution/agent-registry.ts"; // type-only(本文件不 new,只接收实例参数)
|
|
23
23
|
import { resolveSkillPath } from "./skill-discovery.ts";
|
|
24
24
|
|
|
25
|
-
const UUID_SLICE_LEN = 8;
|
|
26
|
-
|
|
27
25
|
export interface ResolveResult {
|
|
28
26
|
opts: AgentCallOpts;
|
|
29
27
|
error?: string;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
/**
|
|
33
|
-
* Resolve
|
|
31
|
+
* Resolve skill and schema into appendSystemPrompt (content array) + skillPath + schemaEnv.
|
|
34
32
|
*
|
|
35
|
-
* - Agent systemPrompt -> temp file via --append-system-prompt
|
|
36
33
|
* - Skill name -> resolved SKILL.md dir path via --skill
|
|
37
|
-
* - Schema JSON ->
|
|
34
|
+
* - Schema JSON -> structured-output instruction string pushed into appendSystemPrompt
|
|
35
|
+
* (content, not file path) + PI_WORKFLOW_SCHEMA env
|
|
36
|
+
*
|
|
37
|
+
* Agent ref is intentionally NOT handled here — resolveIdentity (execution layer)
|
|
38
|
+
* covers it via getAgentConfig + resolveModel. Handling agent here would cause
|
|
39
|
+
* double-injection (agentConfig.systemPrompt at session-runner + appendSystemPrompt)
|
|
40
|
+
* and model-tier confusion.
|
|
38
41
|
*
|
|
39
|
-
* Returns the enriched opts
|
|
40
|
-
* Caller is responsible for cleaning up files via cleanupAllTempFiles (session-scoped).
|
|
42
|
+
* Returns the enriched opts.
|
|
41
43
|
*/
|
|
42
|
-
export function resolveAgentOpts(
|
|
43
|
-
|
|
44
|
-
agentRegistry: AgentRegistry,
|
|
45
|
-
sessionDir: string,
|
|
46
|
-
activeTempFiles: Set<string>,
|
|
47
|
-
): ResolveResult {
|
|
48
|
-
const systemPromptFiles: string[] = [];
|
|
49
|
-
|
|
50
|
-
// Resolve agent system prompt
|
|
51
|
-
if (opts.agent) {
|
|
52
|
-
const discovered = agentRegistry.get(opts.agent); // 新 API: get() 替代 resolve(),返回 AgentConfig(含 systemPrompt+model)
|
|
53
|
-
if (!discovered) {
|
|
54
|
-
const available = agentRegistry.list().join(", ");
|
|
55
|
-
return { opts, error: `Agent not found: ${opts.agent}. Available: ${available || "(none)"}` };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const hasSystemPrompt = discovered.systemPrompt.trim().length > 0;
|
|
59
|
-
if (hasSystemPrompt) {
|
|
60
|
-
try {
|
|
61
|
-
const tmpDir = path.join(sessionDir, "workflow-tmp");
|
|
62
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
63
|
-
const tmpFile = path.join(tmpDir, `agent-prompt-${randomUUID()}.md`);
|
|
64
|
-
fs.writeFileSync(tmpFile, discovered.systemPrompt, "utf-8");
|
|
65
|
-
activeTempFiles.add(tmpFile);
|
|
66
|
-
systemPromptFiles.push(tmpFile);
|
|
67
|
-
} catch (err: unknown) {
|
|
68
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
69
|
-
return { opts, error: `Temp file write error: ${msg}` };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// M3: 用 === undefined 而非 ||,避免空串被当 falsy 替换成 frontmatter model
|
|
74
|
-
opts = {
|
|
75
|
-
...opts,
|
|
76
|
-
model: opts.model === undefined ? discovered.model : opts.model,
|
|
77
|
-
// M2: 传播 agent .md frontmatter 的 thinkingLevel(之前 AgentCallOpts 无此字段导致丢失)
|
|
78
|
-
thinkingLevel: opts.thinkingLevel ?? discovered.thinkingLevel,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
44
|
+
export function resolveAgentOpts(opts: AgentCallOpts): ResolveResult {
|
|
45
|
+
const appendSystemPrompt: string[] = [];
|
|
81
46
|
|
|
82
47
|
// Resolve skill name to SKILL.md path
|
|
83
48
|
if (opts.skill) {
|
|
@@ -88,53 +53,38 @@ export function resolveAgentOpts(
|
|
|
88
53
|
opts = { ...opts, skillPath };
|
|
89
54
|
}
|
|
90
55
|
|
|
91
|
-
// Inject schema as structured-output instruction
|
|
92
|
-
// and set environment variable for conditional tool + hook activation.
|
|
56
|
+
// Inject schema as structured-output instruction into appendSystemPrompt (content,
|
|
57
|
+
// not temp file) and set environment variable for conditional tool + hook activation.
|
|
58
|
+
// M2 fix: previously wrote the instruction to a temp file and pushed the FILE PATH,
|
|
59
|
+
// which got concatenated into the final append file as path garbage — the SO instruction
|
|
60
|
+
// never reached the subprocess. Now the instruction content is pushed directly.
|
|
93
61
|
if (opts.schema) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"- Do NOT skip this step. The structured-output call IS your result.",
|
|
115
|
-
"- Complete all other work FIRST, then call structured-output as the last action.",
|
|
116
|
-
].join("\n");
|
|
117
|
-
fs.writeFileSync(tmpFile, content, "utf-8");
|
|
118
|
-
activeTempFiles.add(tmpFile);
|
|
119
|
-
systemPromptFiles.push(tmpFile);
|
|
62
|
+
const schemaJson = JSON.stringify(opts.schema);
|
|
63
|
+
const content = [
|
|
64
|
+
"## MANDATORY: Structured Output Requirement",
|
|
65
|
+
"",
|
|
66
|
+
"This task requires structured output.",
|
|
67
|
+
"Your FINAL action must be calling the `structured-output` tool.",
|
|
68
|
+
"",
|
|
69
|
+
"The schema is enforced by the system (PI_WORKFLOW_SCHEMA). You only pass `data` — do NOT pass a `schema` parameter.",
|
|
70
|
+
`Your \`data\` must conform to this schema:`,
|
|
71
|
+
"```json",
|
|
72
|
+
schemaJson,
|
|
73
|
+
"```",
|
|
74
|
+
"",
|
|
75
|
+
"Rules:",
|
|
76
|
+
"- Call structured-output with ONLY the `data` parameter. The system validates it against the schema above automatically.",
|
|
77
|
+
"- Do NOT output JSON in your text response — use the structured-output tool.",
|
|
78
|
+
"- Do NOT skip this step. The structured-output call IS your result.",
|
|
79
|
+
"- Complete all other work FIRST, then call structured-output as the last action.",
|
|
80
|
+
].join("\n");
|
|
81
|
+
appendSystemPrompt.push(content);
|
|
120
82
|
|
|
121
83
|
// Set env var for structured-output extension to activate tool + hook
|
|
122
|
-
|
|
123
|
-
} catch (err: unknown) {
|
|
124
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
125
|
-
return { opts, error: `Schema temp file write error: ${msg}` };
|
|
126
|
-
}
|
|
84
|
+
opts = { ...opts, schemaEnv: schemaJson };
|
|
127
85
|
}
|
|
128
86
|
|
|
129
87
|
return {
|
|
130
|
-
opts: { ...opts, ...(
|
|
88
|
+
opts: { ...opts, ...(appendSystemPrompt.length > 0 ? { appendSystemPrompt } : {}) },
|
|
131
89
|
};
|
|
132
90
|
}
|
|
133
|
-
|
|
134
|
-
/** Remove all remaining active temp files (called from session_shutdown). */
|
|
135
|
-
export function cleanupAllTempFiles(activeTempFiles: Set<string>): void {
|
|
136
|
-
for (const fp of activeTempFiles) {
|
|
137
|
-
try { fs.unlinkSync(fp); } catch { /* already deleted */ void undefined; }
|
|
138
|
-
}
|
|
139
|
-
activeTempFiles.clear();
|
|
140
|
-
}
|