@zhushanwen/pi-subagent-workflow 0.4.0 → 0.4.2
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/package.json +6 -6
- package/src/execution/__tests__/bg-notify-render.test.ts +1 -1
- package/src/execution/__tests__/crash-recovery.test.ts +3 -3
- package/src/execution/__tests__/execute-nesting.test.ts +16 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +1 -1
- package/src/execution/__tests__/index-session-start.test.ts +4 -4
- package/src/execution/__tests__/sdk-contract.test.ts +1 -1
- package/src/execution/__tests__/session-start-reaper.test.ts +3 -3
- package/src/execution/__tests__/ui-request-handler-factory.test.ts +1 -1
- package/src/execution/host-mode.ts +1 -1
- package/src/execution/session-runner.ts +1 -1
- package/src/execution/subagent-service.ts +1 -1
- package/src/execution/ui-request-handler-factory.ts +2 -2
- package/src/execution/ui-request-observability.ts +1 -1
- package/src/index.ts +3 -3
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +13 -2
- package/src/interface/bg-notify-render.ts +1 -1
- package/src/interface/commands.ts +1 -1
- package/src/interface/helpers.ts +1 -1
- package/src/interface/list-view.ts +1 -1
- package/src/interface/subagent-actions.ts +1 -1
- package/src/interface/subagent-tool.ts +2 -2
- package/src/interface/subagents.ts +1 -1
- package/src/interface/tool-render.ts +1 -1
- package/src/interface/tool-workflow-script.ts +7 -6
- package/src/interface/tool-workflow.ts +10 -7
- package/src/interface/views/WorkflowsView.ts +2 -2
- package/src/interface/views/detail-content.ts +1 -1
- package/src/interface/views/format.ts +1 -1
- package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +390 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +74 -0
- package/src/orchestration/config-loader.ts +1 -1
- package/src/orchestration/error-recovery.ts +77 -13
- package/src/orchestration/jsonl-run-store.ts +2 -2
- package/src/orchestration/worker-script-builder.ts +49 -8
- package/workflows/README.md +5 -3
- package/workflows/chain.js +2 -2
- package/workflows/map-reduce.js +10 -6
- package/workflows/parallel.js +17 -27
- package/workflows/scatter-gather.js +15 -7
|
@@ -69,6 +69,17 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
69
69
|
' console.error = function (...args) { _pushWorkerLog("error", args); };',
|
|
70
70
|
' console.info = function (...args) { _pushWorkerLog("info", args); };',
|
|
71
71
|
'',
|
|
72
|
+
' // ── safePostMessage wrapper: 统一 postMessage 防御(DataCloneError 等)──',
|
|
73
|
+
' function _safePost(msg, context) {',
|
|
74
|
+
' try { parentPort.postMessage(msg); return true; }',
|
|
75
|
+
' catch (e) {',
|
|
76
|
+
' const errMsg = e && e.message ? e.message : String(e);',
|
|
77
|
+
' const stack = e && e.stack ? e.stack : "";',
|
|
78
|
+
' _pushWorkerLog("error", ["[postMessage failed:" + context + "]", errMsg, stack]);',
|
|
79
|
+
' return false;',
|
|
80
|
+
' }',
|
|
81
|
+
' }',
|
|
82
|
+
'',
|
|
72
83
|
' // ── Internal state ──',
|
|
73
84
|
' let _callIdCounter = 0;',
|
|
74
85
|
' let _agentCallCount = 0;',
|
|
@@ -201,7 +212,9 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
201
212
|
' const _effectivePhase = opts.phase || _currentPhase;\n' +
|
|
202
213
|
' delete opts.phase;\n' +
|
|
203
214
|
'\n' +
|
|
204
|
-
'
|
|
215
|
+
' if (!_safePost({ type: "agent-call", callId, opts, phase: _effectivePhase }, "agent-call")) {',
|
|
216
|
+
' return Promise.reject(new Error("postMessage failed for agent-call (callId=" + callId + "): see workerLogs"));',
|
|
217
|
+
' }',
|
|
205
218
|
' return new Promise((resolve, reject) => {',
|
|
206
219
|
' _pendingCalls.set(callId, { resolve, reject });',
|
|
207
220
|
' });',
|
|
@@ -219,7 +232,21 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
219
232
|
' if (typeof c === "object" && c !== null && (c.task || c.agent)) { return agent(c); }',
|
|
220
233
|
' return agent(c);',
|
|
221
234
|
' }));',
|
|
222
|
-
' return settled.map((r) =>
|
|
235
|
+
' return settled.map((r) => {',
|
|
236
|
+
' if (r.status === "fulfilled") {',
|
|
237
|
+
' const v = r.value;',
|
|
238
|
+
' if (v !== null && typeof v === "object" && !Array.isArray(v)) {',
|
|
239
|
+
' // 主线程 fallback(postAgentResult/postResult serialization failed)回发的对象含 error 字段',
|
|
240
|
+
' // → 归一化为 failed 形状,与脚本侧 r.status === "failed" 检查统一',
|
|
241
|
+
' if (typeof v.error === "string" && v.error.length > 0) return { status: "failed", error: v.error };',
|
|
242
|
+
' return v;',
|
|
243
|
+
' }',
|
|
244
|
+
' return { status: "failed", error: "agent returned non-object result (type=" + typeof v + ")" };',
|
|
245
|
+
' }',
|
|
246
|
+
' const reason = r.reason;',
|
|
247
|
+
' const errMsg = reason instanceof Error ? reason.message : String(reason);',
|
|
248
|
+
' return { status: "failed", error: errMsg };',
|
|
249
|
+
' });',
|
|
223
250
|
' }',
|
|
224
251
|
'',
|
|
225
252
|
// ── pipeline global ──
|
|
@@ -227,19 +254,31 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
227
254
|
' // Single-arg mode: pipeline([stage1, stage2, ...])',
|
|
228
255
|
' if (Array.isArray(firstArg) && restStages.length === 0) {',
|
|
229
256
|
' let result;',
|
|
230
|
-
' for (
|
|
257
|
+
' for (let i = 0; i < firstArg.length; i++) {',
|
|
258
|
+
' try { result = await firstArg[i](result); }',
|
|
259
|
+
' catch (e) {',
|
|
260
|
+
' const msg = e && e.message ? e.message : String(e);',
|
|
261
|
+
' _pushWorkerLog("error", ["[pipeline stage " + i + " failed]", msg]);',
|
|
262
|
+
' throw e;',
|
|
263
|
+
' }',
|
|
264
|
+
' }',
|
|
231
265
|
' return result;',
|
|
232
266
|
' }',
|
|
233
267
|
' // Cartesian product mode: pipeline([items], stage1, stage2, ...)',
|
|
234
268
|
' if (Array.isArray(firstArg) && restStages.length > 0 && typeof restStages[0] === "function") {',
|
|
235
269
|
' const results = [];',
|
|
236
|
-
' for (
|
|
270
|
+
' for (let idx = 0; idx < firstArg.length; idx++) {',
|
|
271
|
+
' const item = firstArg[idx];',
|
|
237
272
|
' let val = item;',
|
|
238
273
|
' let failed = false;',
|
|
239
274
|
' for (const stage of restStages) {',
|
|
240
275
|
' if (failed) break;',
|
|
241
276
|
' try { val = await stage(val); }',
|
|
242
|
-
' catch (e) {
|
|
277
|
+
' catch (e) {',
|
|
278
|
+
' const msg = e && e.message ? e.message : String(e);',
|
|
279
|
+
' _pushWorkerLog("error", ["[pipeline cartesian stage failed for item " + (idx + 1) + "]", msg]);',
|
|
280
|
+
' val = null; failed = true;',
|
|
281
|
+
' }',
|
|
243
282
|
' }',
|
|
244
283
|
' results.push(val);',
|
|
245
284
|
' }',
|
|
@@ -256,7 +295,9 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
256
295
|
' const workflowArgs = (typeof args === "object" && args !== null) ? args : {};',
|
|
257
296
|
' const callId = _callIdCounter;',
|
|
258
297
|
' _callIdCounter++;',
|
|
259
|
-
'
|
|
298
|
+
' if (!_safePost({ type: "workflow-call", callId, name, args: workflowArgs }, "workflow-call")) {',
|
|
299
|
+
' return Promise.reject(new Error("postMessage failed for workflow-call (name=" + name + "): see workerLogs"));',
|
|
300
|
+
' }',
|
|
260
301
|
' return new Promise((resolve, reject) => {',
|
|
261
302
|
' _pendingCalls.set(callId, { resolve, reject });',
|
|
262
303
|
' });',
|
|
@@ -272,11 +313,11 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
272
313
|
'})().then((result) => {',
|
|
273
314
|
' const { parentPort, workerData } = require("node:worker_threads");',
|
|
274
315
|
' const runId = (workerData.args && typeof workerData.args === "object" && workerData.args._runId) || "";',
|
|
275
|
-
'
|
|
316
|
+
' _safePost({ type: "return", runId, result, workerLogs: _workerLogs }, "return");',
|
|
276
317
|
'}).catch((err) => {',
|
|
277
318
|
' const { parentPort, workerData } = require("node:worker_threads");',
|
|
278
319
|
' const runId = (workerData.args && typeof workerData.args === "object" && workerData.args._runId) || "";',
|
|
279
|
-
'
|
|
320
|
+
' _safePost({ type: "error", runId, error: err.message || String(err), workerLogs: _workerLogs }, "error");',
|
|
280
321
|
'});',
|
|
281
322
|
].join("\n");
|
|
282
323
|
}
|
package/workflows/README.md
CHANGED
|
@@ -28,7 +28,9 @@ workflow run parallel --args target="src/auth/login.ts"
|
|
|
28
28
|
workflow run parallel --args target="..." --args 'perspectives=["security","readability"]'
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
`perspectives` 默认 `["security","performance","maintainability"]`。每个视角一个并行 agent
|
|
31
|
+
`perspectives` 默认 `["security","performance","maintainability"]`。每个视角一个并行 agent,各自返回评分+发现的问题,最后纯代码拼接各视角的 findings。
|
|
32
|
+
|
|
33
|
+
> **Note (breaking)**: `outcome.aggregate` is now a concatenated string of each perspective's findings (format: `[perspective] finding1; finding2`, joined by newlines). Previously it was an LLM-produced object `{overallScore, topIssues, consensus}`. If you have generated workflows or downstream tools parsing the old object shape, update them to read `outcome.per_perspective` for structured per-perspective scores/findings, or treat `outcome.aggregate` as plain text.
|
|
32
34
|
|
|
33
35
|
### scatter-gather — 分发-收集
|
|
34
36
|
|
|
@@ -36,7 +38,7 @@ workflow run parallel --args target="..." --args 'perspectives=["security","read
|
|
|
36
38
|
workflow run scatter-gather --args task="重构认证模块,涉及 session/jwt/oauth 三块"
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
三段:第一个 agent 把大任务拆成 2-4 个可并行子任务 → `parallel()` 并行处理每个子任务 →
|
|
41
|
+
三段:第一个 agent 把大任务拆成 2-4 个可并行子任务 → `parallel()` 并行处理每个子任务 → gather 阶段用 `agent()` 把各子任务结果合并成最终结论(LLM 合并,非纯代码拼接)。
|
|
40
42
|
|
|
41
43
|
### map-reduce — 映射-归约
|
|
42
44
|
|
|
@@ -45,7 +47,7 @@ workflow run map-reduce --args 'items=["file1.ts","file2.ts","file3.ts"]' --args
|
|
|
45
47
|
workflow run map-reduce --args itemsJson=/path/to/items.json --args operation="..."
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
`items` 直接传 JSON 数组,或 `itemsJson` 传 JSON 文件路径(二选一)。`parallel()` 对每个 item 并行执行 `operation` →
|
|
50
|
+
`items` 直接传 JSON 数组,或 `itemsJson` 传 JSON 文件路径(二选一)。`parallel()` 对每个 item 并行执行 `operation` → reduce 阶段用 `agent()` 把各 item 的 map 结果归约成单一结论(LLM 归约,非纯代码拼接)。
|
|
49
51
|
|
|
50
52
|
## 编排 API
|
|
51
53
|
|
package/workflows/chain.js
CHANGED
|
@@ -19,8 +19,8 @@ const meta = {
|
|
|
19
19
|
|
|
20
20
|
// ── 入参($ARGS)──────────────────────────────────────────────────
|
|
21
21
|
const task = $ARGS.task;
|
|
22
|
-
if (
|
|
23
|
-
throw new Error("chain 缺少必需参数 task
|
|
22
|
+
if (typeof task !== "string" || task.trim() === "") {
|
|
23
|
+
throw new Error("chain 缺少必需参数 task(非空字符串)。用法:workflow run chain --args task=\"<描述>\"");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
log("chain 开始,task=" + task);
|
package/workflows/map-reduce.js
CHANGED
|
@@ -82,12 +82,12 @@ try {
|
|
|
82
82
|
let mapFailed = 0;
|
|
83
83
|
for (let i = 0; i < mappedRaw.length; i++) {
|
|
84
84
|
const r = mappedRaw[i];
|
|
85
|
-
if (!r || r.error) {
|
|
85
|
+
if (!r || r.status === "failed" || r.error) {
|
|
86
86
|
mapped.push({
|
|
87
87
|
itemIndex: i,
|
|
88
88
|
item: items[i],
|
|
89
89
|
status: "failed",
|
|
90
|
-
error: r ? r.error : "agent 无返回",
|
|
90
|
+
error: r ? (r.error || "agent 返回 failed 状态") : "agent 无返回",
|
|
91
91
|
});
|
|
92
92
|
mapFailed++;
|
|
93
93
|
} else {
|
|
@@ -95,7 +95,7 @@ try {
|
|
|
95
95
|
itemIndex: i,
|
|
96
96
|
item: items[i],
|
|
97
97
|
status: "ok",
|
|
98
|
-
mapped: r.mapped,
|
|
98
|
+
mapped: (typeof r.mapped === "string" ? r.mapped : "(无结果)"),
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -104,10 +104,11 @@ try {
|
|
|
104
104
|
}
|
|
105
105
|
log("map 完成:ok=" + (items.length - mapFailed) + " failed=" + mapFailed);
|
|
106
106
|
|
|
107
|
-
// ── 段 2:reduce(agent
|
|
107
|
+
// ── 段 2:reduce(agent 归约所有 map 结果)──────────────────────
|
|
108
108
|
phase("reduce");
|
|
109
109
|
currentPhase = "reduce";
|
|
110
|
-
|
|
110
|
+
|
|
111
|
+
const reducedResult = await agent({
|
|
111
112
|
prompt:
|
|
112
113
|
"以下是对 " + items.length + " 个 item 执行「" + operation + "」的结果,请归约成单一结论:\n\n" +
|
|
113
114
|
JSON.stringify(mapped, null, 2),
|
|
@@ -127,7 +128,10 @@ try {
|
|
|
127
128
|
phases_run: ["map", "reduce"],
|
|
128
129
|
items_total: items.length,
|
|
129
130
|
items_mapped: items.length - mapFailed,
|
|
130
|
-
reduced: {
|
|
131
|
+
reduced: {
|
|
132
|
+
reduced: (reducedResult?.reduced ?? "(归约无结果)"),
|
|
133
|
+
stats: (reducedResult?.stats ?? "(归约无结果)"),
|
|
134
|
+
},
|
|
131
135
|
message: "map-reduce 完成:map " + items.length + " 项(失败 " + mapFailed + ")→ reduce",
|
|
132
136
|
};
|
|
133
137
|
} catch (err) {
|
package/workflows/parallel.js
CHANGED
|
@@ -28,6 +28,9 @@ if (!target) {
|
|
|
28
28
|
const perspectives = Array.isArray($ARGS.perspectives) && $ARGS.perspectives.length > 0
|
|
29
29
|
? $ARGS.perspectives
|
|
30
30
|
: ["security", "performance", "maintainability"];
|
|
31
|
+
if (perspectives.some((p) => typeof p !== "string")) {
|
|
32
|
+
throw new Error("parallel 参数 perspectives 必须是字符串数组,实际含非字符串元素");
|
|
33
|
+
}
|
|
31
34
|
|
|
32
35
|
log("parallel 开始,target=" + target + " perspectives=" + JSON.stringify(perspectives));
|
|
33
36
|
|
|
@@ -68,15 +71,20 @@ try {
|
|
|
68
71
|
let failedCount = 0;
|
|
69
72
|
for (let i = 0; i < perPerspectiveRaw.length; i++) {
|
|
70
73
|
const r = perPerspectiveRaw[i];
|
|
71
|
-
if (!r || r.error) {
|
|
74
|
+
if (!r || r.status === "failed" || r.error) {
|
|
72
75
|
perPerspective.push({
|
|
73
76
|
perspective: perspectives[i],
|
|
74
77
|
status: "failed",
|
|
75
|
-
error: r ? r.error : "agent 无返回",
|
|
78
|
+
error: r ? (r.error || "agent 返回 failed 状态") : "agent 无返回",
|
|
76
79
|
});
|
|
77
80
|
failedCount++;
|
|
78
81
|
} else {
|
|
79
|
-
perPerspective.push({
|
|
82
|
+
perPerspective.push({
|
|
83
|
+
perspective: perspectives[i],
|
|
84
|
+
status: "ok",
|
|
85
|
+
score: typeof r.score === "number" ? r.score : undefined,
|
|
86
|
+
findings: Array.isArray(r.findings) ? r.findings : [],
|
|
87
|
+
});
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
90
|
if (failedCount === perspectives.length) {
|
|
@@ -87,36 +95,18 @@ try {
|
|
|
87
95
|
// ── 段 2:aggregate(汇总多视角结果)────────────────────────────
|
|
88
96
|
phase("aggregate");
|
|
89
97
|
currentPhase = "aggregate";
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
type: "object",
|
|
96
|
-
properties: {
|
|
97
|
-
overallScore: { type: "number", description: "综合评分 0-10" },
|
|
98
|
-
topIssues: {
|
|
99
|
-
type: "array",
|
|
100
|
-
items: { type: "string" },
|
|
101
|
-
description: "最关键的问题(按严重度排序)",
|
|
102
|
-
},
|
|
103
|
-
consensus: { type: "string", description: "多视角共识总结" },
|
|
104
|
-
},
|
|
105
|
-
required: ["overallScore", "topIssues", "consensus"],
|
|
106
|
-
},
|
|
107
|
-
description: "parallel-aggregate",
|
|
108
|
-
});
|
|
98
|
+
|
|
99
|
+
// 纯代码合并:拼接各视角发现的问题(不调用 LLM)
|
|
100
|
+
const aggregateResult = perPerspective
|
|
101
|
+
.map((p) => "[" + (p.perspective || "?") + "] " + (p.findings ? p.findings.join("; ") : "(no findings)"))
|
|
102
|
+
.join("\n");
|
|
109
103
|
|
|
110
104
|
outcome = {
|
|
111
105
|
status: failedCount > 0 ? "partial" : "ok",
|
|
112
106
|
phases_run: ["parallel-analyze", "aggregate"],
|
|
113
107
|
perspectives_analyzed: perspectives.length,
|
|
114
108
|
per_perspective: perPerspective,
|
|
115
|
-
aggregate:
|
|
116
|
-
overallScore: (aggregate?.overallScore ?? "(聚合无结果)"),
|
|
117
|
-
topIssues: (aggregate?.topIssues ?? []),
|
|
118
|
-
consensus: (aggregate?.consensus ?? "(聚合无结果)"),
|
|
119
|
-
},
|
|
109
|
+
aggregate: aggregateResult,
|
|
120
110
|
message: "parallel 完成:" + perspectives.length + " 视角(失败 " + failedCount + ")→ 聚合",
|
|
121
111
|
};
|
|
122
112
|
} catch (err) {
|
|
@@ -62,6 +62,9 @@ try {
|
|
|
62
62
|
if (subtasks.length === 0) {
|
|
63
63
|
throw new Error("scatter 返回的 subtasks 为空");
|
|
64
64
|
}
|
|
65
|
+
if (subtasks.some((s) => !s || typeof s.name !== "string")) {
|
|
66
|
+
throw new Error("scatter 返回的 subtasks 每项需含 name 字符串字段");
|
|
67
|
+
}
|
|
65
68
|
log("scatter 出 " + subtasks.length + " 个子任务");
|
|
66
69
|
|
|
67
70
|
// ── 段 2:process(parallel 并行处理每个子任务)──────────────────
|
|
@@ -89,15 +92,19 @@ try {
|
|
|
89
92
|
let failedCount = 0;
|
|
90
93
|
for (let i = 0; i < processedRaw.length; i++) {
|
|
91
94
|
const r = processedRaw[i];
|
|
92
|
-
if (!r || r.error) {
|
|
95
|
+
if (!r || r.status === "failed" || r.error) {
|
|
93
96
|
processed.push({
|
|
94
97
|
subtask: subtasks[i].name,
|
|
95
98
|
status: "failed",
|
|
96
|
-
error: r ? r.error : "agent 无返回",
|
|
99
|
+
error: r ? (r.error || "agent 返回 failed 状态") : "agent 无返回",
|
|
97
100
|
});
|
|
98
101
|
failedCount++;
|
|
99
102
|
} else {
|
|
100
|
-
processed.push({
|
|
103
|
+
processed.push({
|
|
104
|
+
subtask: subtasks[i].name,
|
|
105
|
+
status: "ok",
|
|
106
|
+
result: (typeof r.result === "string" ? r.result : "(无结果)"),
|
|
107
|
+
});
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
110
|
if (failedCount === subtasks.length) {
|
|
@@ -105,10 +112,11 @@ try {
|
|
|
105
112
|
}
|
|
106
113
|
log("process 完成:ok=" + (subtasks.length - failedCount) + " failed=" + failedCount);
|
|
107
114
|
|
|
108
|
-
// ── 段 3:gather
|
|
115
|
+
// ── 段 3:gather(agent 合并所有子任务结果)─────────────────────
|
|
109
116
|
phase("gather");
|
|
110
117
|
currentPhase = "gather";
|
|
111
|
-
|
|
118
|
+
|
|
119
|
+
const gatheredResult = await agent({
|
|
112
120
|
prompt:
|
|
113
121
|
"以下是各子任务的处理结果,请合并成一个完整、一致的最终结果:\n\n" +
|
|
114
122
|
JSON.stringify(processed, null, 2),
|
|
@@ -129,8 +137,8 @@ try {
|
|
|
129
137
|
subtasks_total: subtasks.length,
|
|
130
138
|
subtasks_processed: subtasks.length - failedCount,
|
|
131
139
|
gathered: {
|
|
132
|
-
mergedResult: (
|
|
133
|
-
completeness: (
|
|
140
|
+
mergedResult: (gatheredResult?.mergedResult ?? "(合并无结果)"),
|
|
141
|
+
completeness: (gatheredResult?.completeness ?? "(合并无结果)"),
|
|
134
142
|
},
|
|
135
143
|
message: "scatter-gather 完成:split " + subtasks.length + " → process(失败 " + failedCount + ")→ merge",
|
|
136
144
|
};
|