@zhushanwen/pi-subagent-workflow 7.4.0 → 8.1.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 +15 -0
- package/package.json +7 -4
- package/scripts/rfl.mjs +308 -0
- package/skills/workflow-script-format/SKILL.md +1 -1
- package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
- package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
- package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
- package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
- package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
- package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
- package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
- package/src/execution/__tests__/crash-recovery.test.ts +8 -2
- package/src/execution/__tests__/delivery-methods.test.ts +385 -0
- package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
- package/src/execution/__tests__/execute-nesting.test.ts +20 -72
- package/src/execution/__tests__/execution-record.test.ts +199 -0
- package/src/execution/__tests__/finalize-record.test.ts +197 -15
- package/src/execution/__tests__/format.test.ts +131 -7
- package/src/execution/__tests__/gc-timer.test.ts +184 -0
- package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
- package/src/execution/__tests__/helpers/spawn-mock.ts +37 -10
- package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
- package/src/execution/__tests__/index-session-start.test.ts +257 -5
- package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
- package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
- package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
- package/src/execution/__tests__/list-component.test.ts +59 -5
- package/src/execution/__tests__/list-fields.test.ts +109 -0
- package/src/execution/__tests__/model-resolver.test.ts +38 -1
- package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
- package/src/execution/__tests__/nested-visibility.test.ts +325 -0
- package/src/execution/__tests__/notifier-flush.test.ts +209 -7
- package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
- package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
- package/src/execution/__tests__/record-store.test.ts +442 -54
- package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
- package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
- package/src/execution/__tests__/resource-policy.test.ts +109 -0
- package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
- package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
- package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
- package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
- package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
- package/src/execution/__tests__/session-pending.test.ts +61 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
- package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
- package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
- package/src/execution/__tests__/spawn-args.test.ts +127 -49
- package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
- package/src/execution/__tests__/spawned-children.test.ts +92 -0
- package/src/execution/__tests__/status-refactor.test.ts +345 -0
- package/src/execution/__tests__/stdin-writer.test.ts +97 -0
- package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
- package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
- package/src/execution/__tests__/subagent-service.test.ts +49 -11
- package/src/execution/__tests__/timeout-integration.test.ts +27 -13
- package/src/execution/__tests__/tool-action.test.ts +12 -10
- package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
- package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
- package/src/execution/__tests__/worktree-manager.test.ts +300 -90
- package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +17 -13
- package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
- package/src/execution/__tests__/worktree-registry.test.ts +72 -34
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/argv-mirror.ts +21 -2
- package/src/execution/channel-registry-access.ts +3 -1
- package/src/execution/execution-record.ts +126 -9
- package/src/execution/finalize-record.ts +99 -13
- package/src/execution/idle-gc.ts +47 -0
- package/src/execution/lifecycle-manager.ts +491 -0
- package/src/execution/lifecycle-predicates.ts +65 -0
- package/src/execution/manifest-store.ts +61 -16
- package/src/execution/model-resolver.ts +26 -5
- package/src/execution/notifier.ts +69 -12
- package/src/execution/pi-invocation.ts +21 -1
- package/src/execution/record-entry.ts +118 -0
- package/src/execution/record-store.ts +844 -108
- package/src/execution/session-pending.ts +121 -49
- package/src/execution/session-reconstructor.ts +224 -7
- package/src/execution/session-runner.ts +713 -316
- package/src/execution/sessions-index.ts +304 -0
- package/src/execution/stdin-writer.ts +93 -7
- package/src/execution/stream-sink.ts +20 -3
- package/src/execution/subagent-service.ts +917 -138
- package/src/execution/temp-prompt.ts +8 -3
- package/src/execution/turn-limiter.ts +14 -0
- package/src/execution/types.ts +218 -19
- package/src/execution/worktree-manager.ts +449 -59
- package/src/execution/worktree-registry.ts +97 -29
- package/src/index.ts +318 -20
- package/src/injectors/subagent-list-injector.ts +26 -8
- package/src/injectors/workflow-list-injector.ts +25 -8
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
- package/src/interface/__tests__/tool-render.test.ts +10 -8
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +103 -26
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +179 -0
- package/src/interface/bg-notify-render.ts +32 -8
- package/src/interface/command-actions.ts +26 -7
- package/src/interface/commands.ts +21 -22
- package/src/interface/format.ts +53 -20
- package/src/interface/gui-mappers.ts +6 -8
- package/src/interface/helpers.ts +170 -10
- package/src/interface/list-component.ts +53 -14
- package/src/interface/subagent-actions.ts +235 -17
- package/src/interface/subagent-tool.ts +81 -16
- package/src/interface/subagents.ts +2 -1
- package/src/interface/tool-render.ts +21 -27
- package/src/interface/tool-workflow-script.ts +29 -33
- package/src/interface/tool-workflow.ts +67 -100
- package/src/interface/views/WorkflowsView.ts +89 -32
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
- package/src/interface/views/detail-content.ts +1 -1
- package/src/interface/views/format.ts +3 -3
- package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +333 -0
- package/src/orchestration/__tests__/args-validator.test.ts +1 -1
- package/src/orchestration/__tests__/config-loader.test.ts +38 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
- package/src/orchestration/__tests__/execute-agent-call.test.ts +144 -1
- package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +820 -19
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
- package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
- package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +1108 -19
- package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
- package/src/orchestration/__tests__/test-mocks.ts +197 -0
- package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
- package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +110 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
- package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
- package/src/orchestration/__tests__/workflows-e2e.test.ts +38 -40
- package/src/orchestration/agent-opts-resolver.ts +4 -1
- package/src/orchestration/args-validator.ts +2 -2
- package/src/orchestration/config-loader.ts +30 -1
- package/src/orchestration/error-recovery.ts +137 -30
- package/src/orchestration/execute-agent-call.ts +44 -10
- package/src/orchestration/jsonl-run-store.ts +397 -71
- package/src/orchestration/launcher.ts +7 -1
- package/src/orchestration/lifecycle.ts +145 -133
- package/src/orchestration/models/__tests__/trace.test.ts +408 -0
- package/src/orchestration/models/budget.ts +1 -1
- package/src/orchestration/models/run-runtime.ts +15 -17
- package/src/orchestration/models/run-spec.ts +2 -2
- package/src/orchestration/models/run-state.ts +3 -3
- package/src/orchestration/models/trace.ts +95 -15
- package/src/orchestration/models/types.ts +8 -9
- package/src/orchestration/models/workflow-run.ts +50 -71
- package/src/orchestration/models/workflow-script.ts +32 -1
- package/src/orchestration/skill-discovery.ts +30 -0
- package/src/orchestration/worker-handle.ts +1 -1
- package/src/orchestration/worker-host.ts +1 -1
- package/src/orchestration/worker-script-builder.ts +38 -11
- package/src/shared/__tests__/agent-ref.test.ts +34 -0
- package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
- package/src/shared/__tests__/resource-discovery.test.ts +79 -0
- package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
- package/src/shared/agent-ref.ts +22 -1
- package/src/shared/resource-discovery.ts +162 -59
- package/src/shared/schema-jsonify.ts +56 -0
- package/workflows/review-fix-loop-utils.cjs +542 -32
- package/workflows/review-fix-loop.js +462 -109
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buildWorkerScript 模板 hoisting(IF5/IF6)— byte-identical 快照锚定。
|
|
3
|
+
*
|
|
4
|
+
* ES8:快照先行——fixture 由「改造前实现」生成落盘(本文件同 commit),
|
|
5
|
+
* 断言重构后输出与 fixture 逐字节一致,锁定 PRE/POST 边界换行与 userScript
|
|
6
|
+
* 缩进语义(AC-4「逐字保留」不变式)。IF6(_KNOWN_FIELDS 提升至生成源
|
|
7
|
+
* module scope)落地时基线已按设计在同一 commit 内更新为最终形态。
|
|
8
|
+
*
|
|
9
|
+
* 样例脚本覆盖 $ARGS / schema / parallel / pipeline / workflow / phase / log 特性。
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { describe, expect, it } from "vitest";
|
|
16
|
+
|
|
17
|
+
import { buildWorkerScript } from "../worker-script-builder.ts";
|
|
18
|
+
|
|
19
|
+
const sampleScript = [
|
|
20
|
+
"// sample exercising template features",
|
|
21
|
+
'const name = $ARGS.name ?? "default";',
|
|
22
|
+
'phase("build");',
|
|
23
|
+
'const spec = await agent("build it", { label: "builder", schema: { type: "object" } });',
|
|
24
|
+
"const results = await parallel([",
|
|
25
|
+
' agent({ task: "unit tests", agent: "./tester.md" }),',
|
|
26
|
+
' agent({ task: "lint", agent: "./linter.md", skill: "lint-skill" }),',
|
|
27
|
+
"]);",
|
|
28
|
+
"await pipeline([(x) => x, (x) => x]);",
|
|
29
|
+
'await workflow("deploy", { env: "prod" });',
|
|
30
|
+
'log("done " + name + " " + $WORKSPACE + " " + $BUDGET.remaining());',
|
|
31
|
+
"module.exports = { execute: async (ctx) => ctx.agent(\"finalize\") };",
|
|
32
|
+
].join("\n");
|
|
33
|
+
|
|
34
|
+
function loadFixture(): string {
|
|
35
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
return readFileSync(join(here, "__fixtures__", "worker-template.snapshot.txt"), "utf8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("buildWorkerScript — 模板 hoisting byte-identical 快照(IF5)", () => {
|
|
40
|
+
it("特性全量样例的输出与 fixture 逐字节一致", () => {
|
|
41
|
+
const out = buildWorkerScript(sampleScript);
|
|
42
|
+
expect(out).toBe(loadFixture());
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("首尾边界锚定:use strict 开头 + error handler 结尾 + userScript 缩进注入", () => {
|
|
46
|
+
const out = buildWorkerScript(sampleScript);
|
|
47
|
+
// PRE 边界:第一行 use strict(无前导换行)
|
|
48
|
+
expect(out.startsWith('"use strict";\n')).toBe(true);
|
|
49
|
+
// userScript 段:两空格缩进注入(AC-4 拼接语义)
|
|
50
|
+
expect(out).toContain("\n // sample exercising template features\n");
|
|
51
|
+
// userScript 段结束后一个空行再接 auto-invoke 段(POST 首部换行语义)
|
|
52
|
+
expect(out).toContain("ctx.agent(\"finalize\") };\n\n // ── Auto-invoke execute() for module.exports pattern ──");
|
|
53
|
+
// POST 边界:以 error handler 的 "});" 结尾(无尾随换行)
|
|
54
|
+
expect(out.endsWith('_safePost({ type: "error", runId, error: err.message || String(err), workerLogs: _workerLogs }, "error");\n});')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("空 userScript 与任意脚本的拼接语义稳定(PRE/POST 不含 userScript 状态)", () => {
|
|
58
|
+
const a = buildWorkerScript("x");
|
|
59
|
+
const b = buildWorkerScript("y");
|
|
60
|
+
// 除 userScript 段外完全一致:以 userScript 行为界切割比对
|
|
61
|
+
const [preA] = a.split("\n x\n");
|
|
62
|
+
const [preB] = b.split("\n y\n");
|
|
63
|
+
expect(preA).toBe(preB);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("A8 rfl 仪表透传字段字面量锚定(tier-1 §7.1):live resolve 与缓存重放两对称点均含三字段", () => {
|
|
67
|
+
const out = buildWorkerScript("// noop");
|
|
68
|
+
// live resolve 分支(agent-result 消息处理)
|
|
69
|
+
expect(out).toContain("usage: msg.result.usage,");
|
|
70
|
+
expect(out).toContain("durationMs: msg.result.durationMs,");
|
|
71
|
+
expect(out).toContain("sessionId: msg.result.sessionId,");
|
|
72
|
+
// 缓存重放分支(_callCache 命中重建,对称点)
|
|
73
|
+
expect(out).toContain("usage: cached.usage,");
|
|
74
|
+
expect(out).toContain("durationMs: cached.durationMs,");
|
|
75
|
+
expect(out).toContain("sessionId: cached.sessionId,");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("buildWorkerScript — _KNOWN_FIELDS module scope 提升(IF6)", () => {
|
|
80
|
+
const script = buildWorkerScript("// noop user script");
|
|
81
|
+
|
|
82
|
+
it("生成源含 module 级 _KNOWN_FIELDS 声明(与 _workerLogs 同级,IIFE 外)", () => {
|
|
83
|
+
// 声明在 IIFE 开始之前(module scope):const _KNOWN_FIELDS 必须出现在 "(async () => {" 前
|
|
84
|
+
const iifePos = script.indexOf("(async () => {");
|
|
85
|
+
const knownPos = script.indexOf("const _KNOWN_FIELDS = new Set([");
|
|
86
|
+
expect(knownPos).toBeGreaterThan(-1);
|
|
87
|
+
expect(knownPos).toBeLessThan(iifePos);
|
|
88
|
+
// 紧随 _workerLogs 声明之后(design IF6:与 _workerLogs/:62 同级)
|
|
89
|
+
expect(script.indexOf("const _workerLogs = [];")).toBeLessThan(knownPos);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("agent() 体内不再重建 Set(引用 _KNOWN_FIELDS,无 new Set)", () => {
|
|
93
|
+
// agent() 函数体(到 parallel 声明前)内不得有 new Set 构造
|
|
94
|
+
const agentPos = script.indexOf("async function agent(firstArg, secondArg)");
|
|
95
|
+
const parallelPos = script.indexOf("async function parallel(calls)");
|
|
96
|
+
expect(agentPos).toBeGreaterThan(-1);
|
|
97
|
+
expect(parallelPos).toBeGreaterThan(agentPos);
|
|
98
|
+
const agentBody = script.slice(agentPos, parallelPos);
|
|
99
|
+
expect(agentBody).not.toContain("new Set(");
|
|
100
|
+
expect(agentBody).toContain("_KNOWN_FIELDS.has(k)");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("字段集合内容逐字段一致(16 known fields 不丢失)", () => {
|
|
104
|
+
expect(script).toContain(
|
|
105
|
+
'const _KNOWN_FIELDS = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta", "thinkingLevel"]);',
|
|
106
|
+
);
|
|
107
|
+
// unknown-fields 警告文案不变(known 列表仍全量)
|
|
108
|
+
expect(script).toMatch(/Known fields:.*returnMeta.*thinkingLevel/);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -21,8 +21,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
21
21
|
vi.mock("../lifecycle.ts", () => ({
|
|
22
22
|
runWorkflow: vi.fn(),
|
|
23
23
|
abortRun: vi.fn(async () => {}),
|
|
24
|
-
pauseRun: vi.fn(async () => {}),
|
|
25
|
-
resumeRun: vi.fn(async () => {}),
|
|
26
24
|
scheduleTimeBudget: vi.fn(() => undefined),
|
|
27
25
|
}));
|
|
28
26
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowScript.validate lint memo(IF9/#15,TC9/DM2)— 引用相等键缓存测试。
|
|
3
|
+
*
|
|
4
|
+
* vi.mock script-lint 模块统计 lintScript 调用次数(design IF9 测试面):
|
|
5
|
+
* - 同 path + sourceCode 相等(含等值异字面量)→ 二次 validate 只 1 次 lint 调用
|
|
6
|
+
* - 【设计修正】design 原文「异引用等值 miss」在 JS 不可构造:字符串 === 是值相等
|
|
7
|
+
* (实测等值异字面量命中)。lintScript 是纯函数,值相等 ⟹ 结果相同,值键 memo
|
|
8
|
+
* 语义严格正确且是引用键意图的超集(registry 等值重建实例同样命中)。
|
|
9
|
+
* - 同 path 新内容(值不等)→ 重 lint 并更新条目
|
|
10
|
+
* - clearLintMemo → 重 lint(config-loader.invalidateCache 挂载点)
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
13
|
+
|
|
14
|
+
const lintCalls = vi.hoisted(() => ({ count: 0 }));
|
|
15
|
+
|
|
16
|
+
vi.mock("../script-lint.ts", async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal<typeof import("../script-lint.ts")>();
|
|
18
|
+
const realLintScript = actual.lintScript;
|
|
19
|
+
const countingLintScript = ((source: string) => {
|
|
20
|
+
lintCalls.count++;
|
|
21
|
+
return realLintScript(source);
|
|
22
|
+
}) as typeof actual.lintScript;
|
|
23
|
+
return { ...actual, lintScript: countingLintScript };
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
import { WorkflowScript, clearLintMemo, type WorkflowMeta } from "../models/workflow-script.ts";
|
|
27
|
+
|
|
28
|
+
const meta: WorkflowMeta = {
|
|
29
|
+
kind: "workflow",
|
|
30
|
+
name: "w",
|
|
31
|
+
description: "",
|
|
32
|
+
phases: [],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function makeScript(path: string, sourceCode: string): WorkflowScript {
|
|
36
|
+
return new WorkflowScript({
|
|
37
|
+
name: "w",
|
|
38
|
+
source: "saved",
|
|
39
|
+
path,
|
|
40
|
+
sourceCode,
|
|
41
|
+
meta,
|
|
42
|
+
available: true,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const VALID = '/* @pi-meta name: w */\nagent({ prompt: "x" });\n';
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
clearLintMemo();
|
|
50
|
+
lintCalls.count = 0;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("WorkflowScript.validate — lintMemo(IF9)", () => {
|
|
54
|
+
it("同 sourceCode 引用:二次 validate 只 1 次 lint 调用", () => {
|
|
55
|
+
const src = VALID;
|
|
56
|
+
const a = makeScript("/ws/.pi/workflows/w.js", src);
|
|
57
|
+
const b = makeScript("/ws/.pi/workflows/w.js", src); // registry 重建实例场景
|
|
58
|
+
|
|
59
|
+
const ra = a.validate();
|
|
60
|
+
const rb = b.validate();
|
|
61
|
+
|
|
62
|
+
expect(lintCalls.count).toBe(1);
|
|
63
|
+
expect(rb).toEqual(ra); // 缓存返回同一 lint 结果
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("等值异字面量同样命中(JS === 值相等;lintScript 纯函数 ⟹ 值键 memo 正确)", () => {
|
|
67
|
+
const a = makeScript("/ws/.pi/workflows/w.js", VALID);
|
|
68
|
+
const b = makeScript("/ws/.pi/workflows/w.js", `/* @pi-meta name: w */\nagent({ prompt: "x" });\n`);
|
|
69
|
+
|
|
70
|
+
a.validate();
|
|
71
|
+
b.validate();
|
|
72
|
+
|
|
73
|
+
// 设计原文预期「异引用 miss → 2 次」,但字符串 === 是值相等,等值必命中;
|
|
74
|
+
// lintScript(source) 纯函数,同值同结果,缓存正确性不受影响(实测锚定)。
|
|
75
|
+
expect(lintCalls.count).toBe(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("同 path 新内容(值不等):重 lint 并更新条目", () => {
|
|
79
|
+
const path = "/ws/.pi/workflows/w.js";
|
|
80
|
+
const old = makeScript(path, VALID);
|
|
81
|
+
old.validate();
|
|
82
|
+
expect(lintCalls.count).toBe(1);
|
|
83
|
+
|
|
84
|
+
const edited = makeScript(path, '/* @pi-meta name: w */\nagent({ prompt: "y", outputSchema: {} });\n');
|
|
85
|
+
edited.validate();
|
|
86
|
+
expect(lintCalls.count).toBe(2); // 新引用 → miss → 重 lint
|
|
87
|
+
|
|
88
|
+
// 新条目已覆写:再以 edited 同引用 validate 命中缓存
|
|
89
|
+
const replay = makeScript(path, edited.sourceCode);
|
|
90
|
+
replay.validate();
|
|
91
|
+
expect(lintCalls.count).toBe(2);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("clearLintMemo 后重 lint(invalidateCache 挂载点)", () => {
|
|
95
|
+
const a = makeScript("/ws/.pi/workflows/w.js", VALID);
|
|
96
|
+
a.validate();
|
|
97
|
+
expect(lintCalls.count).toBe(1);
|
|
98
|
+
|
|
99
|
+
clearLintMemo();
|
|
100
|
+
a.validate();
|
|
101
|
+
expect(lintCalls.count).toBe(2);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("不同 path 互不共享条目", () => {
|
|
105
|
+
const src = VALID;
|
|
106
|
+
makeScript("/ws/.pi/workflows/a.js", src).validate();
|
|
107
|
+
makeScript("/ws/.pi/workflows/b.js", src).validate();
|
|
108
|
+
expect(lintCalls.count).toBe(2);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -279,7 +279,7 @@ interface ScriptOutcome {
|
|
|
279
279
|
/**
|
|
280
280
|
* 断言 runAndWait 返回成功完成的终态。
|
|
281
281
|
*
|
|
282
|
-
* - result.status === "
|
|
282
|
+
* - result.status === "closed"(runAndWait 恒 done)
|
|
283
283
|
* - result.reason === "completed"(脚本正常 return,非 failed/aborted/time_limited)
|
|
284
284
|
* - result.scriptResult.status 是 "ok" 或 "partial"(脚本层 outcome,非 "error")
|
|
285
285
|
* - result.error 为 undefined
|
|
@@ -476,63 +476,61 @@ describe("内置 workflow E2E(真实 worker thread + mock LLM runner)", () =
|
|
|
476
476
|
|
|
477
477
|
it("TC4: actionRun 顺序——not_found 优先(平铺 + 不存在名);slug 护栏保留;chain 平铺 Correct 文案", async () => {
|
|
478
478
|
const deps = makeDeps();
|
|
479
|
-
// 平铺 + 不存在 name → not_found 优先(m6:registry.get
|
|
480
|
-
|
|
479
|
+
// 平铺 + 不存在 name → not_found 优先(m6:registry.get 先于平铺检测)。
|
|
480
|
+
// W4:not_found 改 throw(pi 只对 execute throw 置 isError:true,返回值里的
|
|
481
|
+
// isError 被 agent-loop 丢弃)——文案含 <available_workflows> <location> 恢复指引,
|
|
482
|
+
// pi catch 后原样进 toolResult content。
|
|
483
|
+
const notFoundErr: Error = await actionRun(
|
|
481
484
|
{ action: "run", name: wf("nope"), task: "x" },
|
|
482
485
|
deps,
|
|
483
486
|
undefined,
|
|
484
|
-
);
|
|
485
|
-
expect(
|
|
486
|
-
expect(
|
|
487
|
-
|
|
488
|
-
expect(
|
|
489
|
-
|
|
490
|
-
//
|
|
491
|
-
|
|
487
|
+
).catch((e: unknown) => e as Error);
|
|
488
|
+
expect(notFoundErr).toBeInstanceOf(Error);
|
|
489
|
+
expect(notFoundErr.message).toContain("not found"); // DoD 用户可见断言
|
|
490
|
+
expect(notFoundErr.message).toContain("<available_workflows>");
|
|
491
|
+
expect(notFoundErr.message).toContain("<location>");
|
|
492
|
+
// chain 平铺 task → throw Correct 正例(动态集来自 registry;W4b:原 textResult
|
|
493
|
+
// 间接 isError 返回改 throw——pi 只对 execute throw 置 isError:true,
|
|
494
|
+
// 返回值里的 isError 被 agent-loop 丢弃)
|
|
495
|
+
const flatErr: Error = await actionRun(
|
|
492
496
|
{ action: "run", name: wf("chain"), task: "x" },
|
|
493
497
|
deps,
|
|
494
498
|
undefined,
|
|
495
|
-
);
|
|
496
|
-
expect(
|
|
497
|
-
expect(
|
|
498
|
-
expect(
|
|
499
|
-
// slug 护栏保留(m6 顺序调整后仍在 runWorkflow
|
|
500
|
-
const
|
|
499
|
+
).catch((e: unknown) => e as Error);
|
|
500
|
+
expect(flatErr).toBeInstanceOf(Error);
|
|
501
|
+
expect(flatErr.message).toContain("Detected task at top level");
|
|
502
|
+
expect(flatErr.message).toContain("Correct:");
|
|
503
|
+
// slug 护栏保留(m6 顺序调整后仍在 runWorkflow 前;W4b 同改 throw)
|
|
504
|
+
const slugErr: Error = await actionRun(
|
|
501
505
|
{ action: "run", name: wf("chain"), args: { task: "x" }, slug: "a".repeat(40) },
|
|
502
506
|
deps,
|
|
503
507
|
undefined,
|
|
504
|
-
);
|
|
505
|
-
expect(
|
|
506
|
-
expect(
|
|
508
|
+
).catch((e: unknown) => e as Error);
|
|
509
|
+
expect(slugErr).toBeInstanceOf(Error);
|
|
510
|
+
expect(slugErr.message).toContain("slug exceeds");
|
|
507
511
|
});
|
|
508
512
|
|
|
509
513
|
it("TC6: 跨 workflow 平铺语义——review-fix-loop 平铺 task(非其参数)走 args-validator", async () => {
|
|
510
514
|
const deps = makeDeps();
|
|
511
515
|
// task 是 chain 参数非 review-fix-loop 参数 → 不报平铺 → args 缺 targetType
|
|
512
|
-
// → m3 chokepoint invalid_args(错误更准——评审 m-5
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
deps,
|
|
516
|
-
|
|
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");
|
|
516
|
+
// → m3 chokepoint invalid_args(错误更准——评审 m-5 语义锁定)。
|
|
517
|
+
// W4:ArgsValidationError 直接 throw(message 原文即 §5.3 指引)
|
|
518
|
+
await expect(
|
|
519
|
+
actionRun({ action: "run", name: wf("review-fix-loop"), task: "x" }, deps, undefined),
|
|
520
|
+
).rejects.toThrow(/Invalid args for workflow 'review-fix-loop'[\s\S]*targetType/);
|
|
521
521
|
});
|
|
522
522
|
|
|
523
|
-
it("TC9: actionRun 参数校验失败 →
|
|
523
|
+
it("TC9: actionRun 参数校验失败 → throw + §5.3 指引(W4)", async () => {
|
|
524
524
|
const deps = makeDeps();
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
525
|
+
await expect(
|
|
526
|
+
actionRun(
|
|
527
|
+
{ action: "run", name: wf("review-fix-loop"), args: { batch1: agentMd("code-reviewer") } },
|
|
528
|
+
deps,
|
|
529
|
+
undefined,
|
|
530
|
+
),
|
|
531
|
+
).rejects.toThrow(
|
|
532
|
+
/Invalid args for workflow 'review-fix-loop'[\s\S]*targetType[\s\S]*Read the workflow script file/,
|
|
529
533
|
);
|
|
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
534
|
});
|
|
537
535
|
|
|
538
536
|
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import type { AgentCallOpts } from "./models/types.ts";
|
|
23
23
|
import { resolveSkillPath } from "./skill-discovery.ts";
|
|
24
|
+
import { stringifySchemaCached } from "../shared/schema-jsonify.ts";
|
|
24
25
|
|
|
25
26
|
export interface ResolveResult {
|
|
26
27
|
opts: AgentCallOpts;
|
|
@@ -59,7 +60,9 @@ export function resolveAgentOpts(opts: AgentCallOpts): ResolveResult {
|
|
|
59
60
|
// which got concatenated into the final append file as path garbage — the SO instruction
|
|
60
61
|
// never reached the subprocess. Now the instruction content is pushed directly.
|
|
61
62
|
if (opts.schema) {
|
|
62
|
-
|
|
63
|
+
// IF7(#13):同 schema 对象引用的 compact stringify 走 WeakMap 缓存
|
|
64
|
+
// (与 session-runner formatSchemaInstruction 的 pretty 版共享缓存条目)
|
|
65
|
+
const schemaJson = stringifySchemaCached(opts.schema, "compact");
|
|
63
66
|
const content = [
|
|
64
67
|
"## MANDATORY: Structured Output Requirement",
|
|
65
68
|
"",
|
|
@@ -62,8 +62,8 @@ function formatMessage(name: string, errors: readonly unknown[]): string {
|
|
|
62
62
|
* 校验 spec.parameters(JSON Schema draft-07)对 spec.args 的约束。
|
|
63
63
|
*
|
|
64
64
|
* - spec.parameters === undefined → 跳过(安全退化:漏拷 parameters 退化是「不校验」非「校验错」)
|
|
65
|
-
* - coerceTypes 原地规范化 spec.args(worker 启动 +
|
|
66
|
-
* run.spec === spec
|
|
65
|
+
* - coerceTypes 原地规范化 spec.args(worker 启动 + 崩溃重建重跑共用同一对象,
|
|
66
|
+
* run.spec === spec,保证重跑路径参数一致)
|
|
67
67
|
* - 失败 throw ArgsValidationError(非原始 ajv 错误)
|
|
68
68
|
*
|
|
69
69
|
* @throws ArgsValidationError 参数不合法或 schema 无效
|
|
@@ -16,6 +16,7 @@ import { resolve } from "node:path";
|
|
|
16
16
|
|
|
17
17
|
// WorkflowMeta 规范来源是 shared/resource-meta.ts(m1 DM1);WorkflowSource 来自 workflow-script
|
|
18
18
|
import type { WorkflowSource } from "./models/workflow-script.ts";
|
|
19
|
+
import { clearLintMemo } from "./models/workflow-script.ts";
|
|
19
20
|
import type { WorkflowMeta } from "../shared/resource-meta.ts";
|
|
20
21
|
import { getCachedFile, getCachedFileContent, clearFileCache } from "../shared/resource-discovery.ts";
|
|
21
22
|
import { parseResourceMeta } from "../shared/meta-parser.ts";
|
|
@@ -93,6 +94,20 @@ function toWorkflowSource(source: ResourceSource): WorkflowSource {
|
|
|
93
94
|
return source === "project-pi-tmp" ? "tmp" : "saved";
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 按路径推导 ResourceSource(IF4/外部 #5 残留)。
|
|
99
|
+
*
|
|
100
|
+
* getWorkflowByPath 可加载任意绝对路径(不限扫描源),此前硬编码 "user-pi"
|
|
101
|
+
* 使 workspaceRoot/.pi/workflows/.tmp/ 下的临时脚本按路径加载被错标 saved
|
|
102
|
+
* (discoverWorkflows 对同文件正确标 tmp,两路径不一致)。修正:路径落在
|
|
103
|
+
* tmp 目录前缀内 → "project-pi-tmp",其余 → "user-pi"(非 tmp 路径输出不变,DS4)。
|
|
104
|
+
*/
|
|
105
|
+
function deriveResourceSource(filePath: string, workspaceRoot: string): ResourceSource {
|
|
106
|
+
const tmpDir = resolve(workspaceRoot, ".pi/workflows/.tmp");
|
|
107
|
+
const normalized = resolve(filePath);
|
|
108
|
+
return normalized === tmpDir || normalized.startsWith(`${tmpDir}/`) ? "project-pi-tmp" : "user-pi";
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
// ── 单文件 → CachedWorkflowMeta(IF1 parseResourceMeta + 整对象透传)──
|
|
97
112
|
|
|
98
113
|
/**
|
|
@@ -264,11 +279,23 @@ export async function getWorkflow(name: string): Promise<CachedWorkflowMeta | un
|
|
|
264
279
|
* - ~/ 前缀展开;相对路径/非 .js 引用返回 undefined(引用唯一形态 = 绝对路径)
|
|
265
280
|
* - 任意路径(不限扫描源):内置包内脚本、用户任意位置脚本均可执行
|
|
266
281
|
* - meta 提取失败/文件不可读 → available=false(fail-safe,不抛)
|
|
282
|
+
* - [perf] 与 getWorkflow(name) 对称走 bucket 缓存(key=绝对路径,与 stem 名不冲突),
|
|
283
|
+
* 消除 workflow tool 主路径每次 run 的全文 regex + YAML.parse(mtime 判变失效)
|
|
267
284
|
*/
|
|
268
285
|
export async function getWorkflowByPath(ref: string): Promise<CachedWorkflowMeta | undefined> {
|
|
269
286
|
const filePath = normalizeRef(ref, WORKFLOW_REF_EXT);
|
|
270
287
|
if (filePath === null) return undefined;
|
|
271
|
-
|
|
288
|
+
const workspaceRoot = findWorkspaceRoot();
|
|
289
|
+
const bucket = getCacheBucket(workspaceRoot);
|
|
290
|
+
const cached = bucket.get(filePath);
|
|
291
|
+
if (cached && isCacheValid(cached)) {
|
|
292
|
+
return cached.meta;
|
|
293
|
+
}
|
|
294
|
+
// IF4:source 按路径推导——.tmp 前缀 → project-pi-tmp(→ "tmp"),其余 user-pi 维持现值
|
|
295
|
+
const meta = await toCachedMeta(filePath, deriveResourceSource(filePath, workspaceRoot));
|
|
296
|
+
const file = getCachedFile(filePath);
|
|
297
|
+
if (file) bucket.set(filePath, { meta, mtimeMs: file.mtimeMs });
|
|
298
|
+
return meta;
|
|
272
299
|
}
|
|
273
300
|
|
|
274
301
|
/**
|
|
@@ -277,6 +304,8 @@ export async function getWorkflowByPath(ref: string): Promise<CachedWorkflowMeta
|
|
|
277
304
|
*/
|
|
278
305
|
export function invalidateCache(): void {
|
|
279
306
|
// m5:清统一 mtime 缓存层 + bucket(测试隔离 + mtime 漏判场景手动刷新兜底)
|
|
307
|
+
// IF9:连带清 workflow-script validate 的 lint memo(同源文件缓存,测试隔离统一走它)
|
|
280
308
|
clearFileCache();
|
|
281
309
|
cache.clear();
|
|
310
|
+
clearLintMemo();
|
|
282
311
|
}
|