@zhushanwen/pi-subagent-workflow 7.0.1 → 7.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/package.json +1 -1
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +4 -27
- package/src/execution/__tests__/subagent-service.test.ts +27 -28
- package/src/execution/notifier.ts +1 -1
- package/src/execution/subagent-service.ts +3 -22
- package/src/execution/types.ts +5 -5
- package/src/interface/bg-notify-render.ts +2 -2
- package/src/interface/subagent-tool.ts +2 -2
- package/src/orchestration/models/types.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-subagent-workflow",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// src/__tests__/execute-and-await-worktree.test.ts
|
|
2
2
|
//
|
|
3
|
-
// executeAndAwait 的 worktree
|
|
3
|
+
// executeAndAwait 的 worktree 失败收尾测试(W1 code review 修复回归锁)。
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// 2. worktreeManager.create 抛错时 record 被 finalizeFailed(status→failed)且原错外抛
|
|
5
|
+
// 覆盖:worktreeManager.create 抛错时 record 被 finalizeFailed(status→failed)且原错外抛。
|
|
6
|
+
// (worktree 与 fork 解耦后,worktree:true+fork:false 的解耦验证见 subagent-service.test.ts。)
|
|
8
7
|
//
|
|
9
8
|
// ── mock 策略 ──
|
|
10
9
|
//
|
|
@@ -153,33 +152,11 @@ function getStore(service: SubagentService): RecordStore {
|
|
|
153
152
|
return Reflect.get(service, "store") as RecordStore;
|
|
154
153
|
}
|
|
155
154
|
|
|
156
|
-
describe("executeAndAwait worktree
|
|
155
|
+
describe("executeAndAwait worktree 失败收尾", () => {
|
|
157
156
|
afterEach(() => {
|
|
158
157
|
vi.restoreAllMocks();
|
|
159
158
|
});
|
|
160
159
|
|
|
161
|
-
// ============================================================
|
|
162
|
-
// [MF#7] worktree:true && !fork → fail-fast 抛错(任何副作用之前)
|
|
163
|
-
// ============================================================
|
|
164
|
-
it("[MF#7] worktree:true 且 fork 未设时抛 'worktree:true requires fork:true'", async () => {
|
|
165
|
-
const { service } = setup();
|
|
166
|
-
|
|
167
|
-
// guard 在 BC-12 深度检查之后、步骤 1 之前——无需 fork:true,worktree:true 即触发。
|
|
168
|
-
// 传入完整 ExecuteOptions(补全 slug 必填字段),不再用 `as` 掩盖缺失字段——让缺字段在类型层可见。
|
|
169
|
-
await expect(
|
|
170
|
-
service.executeAndAwait({
|
|
171
|
-
task: "needs worktree without fork",
|
|
172
|
-
slug: "mf7-worktree-without-fork",
|
|
173
|
-
worktree: true,
|
|
174
|
-
fork: undefined,
|
|
175
|
-
ctxModel,
|
|
176
|
-
}),
|
|
177
|
-
).rejects.toThrow(/worktree:true requires fork:true/);
|
|
178
|
-
|
|
179
|
-
// 无副作用:guard 在 createRecordForMode 之前 → store 无 running record。
|
|
180
|
-
expect(getStore(service).listRunning()).toHaveLength(0);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
160
|
// ============================================================
|
|
184
161
|
// worktreeManager.create 抛错 → record 收尾为 failed + 原错外抛
|
|
185
162
|
// ============================================================
|
|
@@ -385,24 +385,22 @@ describe("SubagentService", () => {
|
|
|
385
385
|
});
|
|
386
386
|
|
|
387
387
|
// ============================================================
|
|
388
|
-
// execute() worktree
|
|
388
|
+
// execute() worktree 路径(worktree 与 fork 解耦后)
|
|
389
389
|
// ============================================================
|
|
390
390
|
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
// 1. worktree:true + fork:false →
|
|
395
|
-
// 2. worktree:true + fork:true →
|
|
396
|
-
// 3. worktree:false + fork:false →
|
|
391
|
+
// worktree(文件隔离)与 fork(上下文继承)已解耦:worktree:true 可独立于 fork 工作
|
|
392
|
+
// (worktreeManager.create 只看 opts.worktree,不读 fork)。此组验证三种 fork/worktree
|
|
393
|
+
// 组合下 worktree 路径的行为(均不应抛 'requires fork'——该 guard 已移除):
|
|
394
|
+
// 1. worktree:true + fork:false → 解耦后正常(创建 worktree 路径,不抛 requires fork)
|
|
395
|
+
// 2. worktree:true + fork:true → 创建 worktree 路径(测试环境 git 失败,抛非 requires fork 错)
|
|
396
|
+
// 3. worktree:false + fork:false → 默认路径(不创建 worktree)
|
|
397
397
|
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
describe("execute() worktree fail-fast 校验 [MF#7]", () => {
|
|
398
|
+
// 本文件不 mock spawn(保持与文件头声明一致——execute 集成测试在 execute-nesting /
|
|
399
|
+
// run-spawn-integration),因此 case 验证「不抛 requires fork」而非「执行完成」:
|
|
400
|
+
// 执行越过 worktree 创建后在后续步骤(worktreeManager.create 调 git / runSpawn 调
|
|
401
|
+
// spawn)抛与 fork/worktree 无关的错。用 try/catch 断言抛出的不是 requires fork。
|
|
402
|
+
|
|
403
|
+
describe("execute() worktree 路径(worktree 与 fork 解耦)", () => {
|
|
406
404
|
/** 构造已就绪的 service(initSession + initModel 注入 ctxModel,使 resolveIdentity 不因 model 拗错)。 */
|
|
407
405
|
function makeReadyService(): SubagentService {
|
|
408
406
|
const service = new SubagentService({ cwd: agentDir, modelService });
|
|
@@ -422,24 +420,25 @@ describe("SubagentService", () => {
|
|
|
422
420
|
return service;
|
|
423
421
|
}
|
|
424
422
|
|
|
425
|
-
it("worktree:true + fork:false →
|
|
423
|
+
it("worktree:true + fork:false → 解耦后不抛 'requires fork'(worktree 独立于 fork)", async () => {
|
|
426
424
|
const service = makeReadyService();
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
service.execute({
|
|
430
|
-
task: "worktree without fork",
|
|
425
|
+
// 解耦后 worktree:true+fork:false 不再 throw requires fork(worktreeManager.create 只看 worktree)
|
|
426
|
+
try {
|
|
427
|
+
await service.execute({
|
|
428
|
+
task: "worktree without fork (decoupled)",
|
|
431
429
|
worktree: true,
|
|
432
430
|
fork: false,
|
|
433
431
|
ctxModel: { id: "ctx-model", name: "Ctx", provider: "p", reasoning: false },
|
|
434
|
-
})
|
|
435
|
-
)
|
|
436
|
-
|
|
437
|
-
|
|
432
|
+
});
|
|
433
|
+
} catch (err) {
|
|
434
|
+
// 解耦后绝不抛 requires fork(执行继续到 worktreeManager.create/spawn 才可能抛其他错)
|
|
435
|
+
expect((err as Error).message).not.toMatch(/requires fork/);
|
|
436
|
+
}
|
|
438
437
|
});
|
|
439
438
|
|
|
440
|
-
it("worktree:true + fork:true →
|
|
439
|
+
it("worktree:true + fork:true → 创建 worktree 路径(不抛 'requires fork')", async () => {
|
|
441
440
|
const service = makeReadyService();
|
|
442
|
-
//
|
|
441
|
+
// 执行继续:先创建 record,然后 worktreeManager.create 调 git(测试环境无 repo → 抛与 fork 无关的错)
|
|
443
442
|
try {
|
|
444
443
|
await service.execute({
|
|
445
444
|
task: "worktree with fork",
|
|
@@ -454,9 +453,9 @@ describe("SubagentService", () => {
|
|
|
454
453
|
}
|
|
455
454
|
});
|
|
456
455
|
|
|
457
|
-
it("worktree:false + fork:false →
|
|
456
|
+
it("worktree:false + fork:false → 默认路径(不创建 worktree,不抛 'requires fork')", async () => {
|
|
458
457
|
const service = makeReadyService();
|
|
459
|
-
//
|
|
458
|
+
// 默认路径:runSpawn 调 child_process.spawn(测试环境无真实 pi → 抛与 fork 无关的错)
|
|
460
459
|
try {
|
|
461
460
|
await service.execute({
|
|
462
461
|
task: "default path",
|
|
@@ -19,7 +19,7 @@ export interface BgNotifyRecord {
|
|
|
19
19
|
error?: string;
|
|
20
20
|
startedAt: number;
|
|
21
21
|
endedAt: number | undefined;
|
|
22
|
-
/** [MF#1]
|
|
22
|
+
/** [MF#1] worktree 模式下子 agent 改动的 patch 路径(worktree 外,cleanup 后留存)。
|
|
23
23
|
* done 时通知文本显式提示 `git apply`,否则 background 子 agent 在隔离 worktree 的改动
|
|
24
24
|
* 会静默丢失——父 LLM 不知 patch 路径,无法应用。 */
|
|
25
25
|
patchFile?: string;
|
|
@@ -421,16 +421,6 @@ export class SubagentService {
|
|
|
421
421
|
);
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
// [MF#7] worktree:true 需要 fork:true——否则下面三个 worktree 分支都不命中,
|
|
425
|
-
// worktreeHandle 恒 undefined → 子 agent 零文件隔离且零报错(静默 no-op)。此处在
|
|
426
|
-
// 任何副作用(record 创建 / worktree 创建)之前 fail-fast,不吞误用。
|
|
427
|
-
if (opts.worktree === true && !opts.fork) {
|
|
428
|
-
throw new Error(
|
|
429
|
-
"worktree:true requires fork:true (worktree isolation only applies to forked sessions). " +
|
|
430
|
-
"Set fork:true together with worktree:true.",
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
424
|
// mode 固定 background(sync 模式已删除)
|
|
435
425
|
const mode: ExecutionMode = "background";
|
|
436
426
|
const ctx = this.buildSessionRunnerContext(opts.cwd);
|
|
@@ -451,7 +441,7 @@ export class SubagentService {
|
|
|
451
441
|
// 传入的是已创建的 WorktreeHandle
|
|
452
442
|
worktreeHandle = opts.worktree;
|
|
453
443
|
} else if (opts.worktree === true) {
|
|
454
|
-
// worktree===true(显式要求)——创建新 worktree
|
|
444
|
+
// worktree===true(显式要求)——创建新 worktree。与 fork 正交(worktree 文件隔离不依赖 fork 上下文继承)。
|
|
455
445
|
try {
|
|
456
446
|
worktreeHandle = this.worktreeManager.create(this.cwd, record.id);
|
|
457
447
|
record.worktreeHandle = worktreeHandle;
|
|
@@ -524,15 +514,6 @@ export class SubagentService {
|
|
|
524
514
|
);
|
|
525
515
|
}
|
|
526
516
|
|
|
527
|
-
// [MF#7] worktree:true requires fork:true — symmetric with execute() guard.
|
|
528
|
-
// Fails fast before any side effect (record creation / worktree creation).
|
|
529
|
-
if (opts.worktree === true && !opts.fork) {
|
|
530
|
-
throw new Error(
|
|
531
|
-
"worktree:true requires fork:true (worktree isolation only applies to forked sessions). " +
|
|
532
|
-
"Set fork:true together with worktree:true.",
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
517
|
// ── 步骤 1: IDENTITY 解析 ──
|
|
537
518
|
const identity = await this.resolveIdentity(opts);
|
|
538
519
|
|
|
@@ -543,8 +524,8 @@ export class SubagentService {
|
|
|
543
524
|
// ── 步骤 2.5: worktree creation (only worktree===true; handle injection is execute()'s path) ──
|
|
544
525
|
// Workflow path receives boolean only (AgentCallOpts.worktree: boolean) — WorktreeHandle is a
|
|
545
526
|
// main-thread non-serializable object that cannot cross worker postMessage, so no object branch
|
|
546
|
-
// here (unlike execute() :445-447 which serves the subagent-tool path).
|
|
547
|
-
//
|
|
527
|
+
// here (unlike execute() :445-447 which serves the subagent-tool path).
|
|
528
|
+
// On create failure, finalizeFailed cleans up the record, then
|
|
548
529
|
// throw lets SAR.run() convert it to an AgentResult.error (not return-handle like execute()).
|
|
549
530
|
let worktreeHandle: WorktreeHandle | undefined;
|
|
550
531
|
if (opts.worktree === true) {
|
package/src/execution/types.ts
CHANGED
|
@@ -231,7 +231,7 @@ export interface AgentResult {
|
|
|
231
231
|
*/
|
|
232
232
|
/**
|
|
233
233
|
* worktree handle 值对象。仅 worktree:true 时持有——worktree 是独立维度,
|
|
234
|
-
*
|
|
234
|
+
* 需显式开启,fork alone 不创建 worktree。
|
|
235
235
|
* Object.freeze 守卫保证不可变。
|
|
236
236
|
*/
|
|
237
237
|
export interface WorktreeHandle {
|
|
@@ -339,7 +339,7 @@ export interface ExecutionRecord {
|
|
|
339
339
|
/** session jsonl 文件名。session 创建成功后由 session-runner.run() 回填(窗口期内 undefined)。 */
|
|
340
340
|
sessionFile?: string;
|
|
341
341
|
|
|
342
|
-
/** [MF#3]
|
|
342
|
+
/** [MF#3] worktree 模式下子 agent 改动的 patch 文件路径(worktree 外,供调用方应用)。 */
|
|
343
343
|
patchFile?: string;
|
|
344
344
|
|
|
345
345
|
/** worktree 隔离时的 handle(仅 worktree:true 时存在;fork alone 无此字段)。 */
|
|
@@ -383,7 +383,7 @@ export interface SubagentToolDetails {
|
|
|
383
383
|
parsedOutput?: unknown;
|
|
384
384
|
/** session jsonl 文件名(不含目录)。窗口期内可能 undefined(session 尚未创建成功)。 */
|
|
385
385
|
sessionFile?: string;
|
|
386
|
-
/** [MF#3]
|
|
386
|
+
/** [MF#3] worktree 模式下子 agent 改动的 patch 文件路径(worktree 外,供调用方应用)。 */
|
|
387
387
|
patchFile?: string;
|
|
388
388
|
}
|
|
389
389
|
|
|
@@ -419,7 +419,7 @@ export interface ExecuteOptions {
|
|
|
419
419
|
onComplete?: (record: RecordSnapshot) => void;
|
|
420
420
|
/** 是否继承父会话上下文(fork 模式,只继承上下文)。 */
|
|
421
421
|
fork?: boolean;
|
|
422
|
-
/** 文件系统隔离:true=创建新 git worktree
|
|
422
|
+
/** 文件系统隔离:true=创建新 git worktree,WorktreeHandle=复用外部已创建的;undefined=不隔离(parent cwd)。 */
|
|
423
423
|
worktree?: boolean | WorktreeHandle;
|
|
424
424
|
/** 覆盖执行 cwd(默认 mainCwd)。 */
|
|
425
425
|
cwd?: string;
|
|
@@ -527,7 +527,7 @@ export interface SubagentRecord {
|
|
|
527
527
|
result?: string;
|
|
528
528
|
error?: string;
|
|
529
529
|
sessionFile?: string;
|
|
530
|
-
/** [MF#3]
|
|
530
|
+
/** [MF#3] worktree 模式下子 agent 改动的 patch 文件路径(worktree 外,供调用方应用)。 */
|
|
531
531
|
patchFile?: string;
|
|
532
532
|
/** 外部 Pi 实例(进程隔离模式下由外部启动的子进程)。 */
|
|
533
533
|
externalInstance?: AliveMarker;
|
|
@@ -57,7 +57,7 @@ interface BgNotifyRecord {
|
|
|
57
57
|
model?: string;
|
|
58
58
|
result?: string;
|
|
59
59
|
error?: string;
|
|
60
|
-
/** [MF#1]
|
|
60
|
+
/** [MF#1] worktree background 完成通知携带的 patch 文件路径。 */
|
|
61
61
|
patchFile?: string;
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -275,7 +275,7 @@ function extractBgNotifyRecord(details: unknown): BgNotifyRecord | undefined {
|
|
|
275
275
|
model: typeof d.model === "string" ? d.model : undefined,
|
|
276
276
|
result: typeof d.result === "string" ? d.result : undefined,
|
|
277
277
|
error: typeof d.error === "string" ? d.error : undefined,
|
|
278
|
-
// [MF#1] 提取 patchFile(
|
|
278
|
+
// [MF#1] 提取 patchFile(worktree background 完成通知携带)。
|
|
279
279
|
patchFile: typeof d.patchFile === "string" ? d.patchFile : undefined,
|
|
280
280
|
};
|
|
281
281
|
}
|
|
@@ -105,10 +105,10 @@ const SubagentParams = Type.Object({
|
|
|
105
105
|
description: "Extra turns allowed after maxTurns is reached before SIGTERM (default 2). Only meaningful when maxTurns is set.",
|
|
106
106
|
})),
|
|
107
107
|
fork: Type.Optional(Type.Boolean({
|
|
108
|
-
description: "Fork mode: inherit the parent's conversation context. When true, the subagent receives the parent's session file via --fork and builds a branched conversation (it sees prior turns/messages). The subagent still runs in a separate spawned child process (process isolation) — fork is about context inheritance, not process sharing
|
|
108
|
+
description: "Fork mode: inherit the parent's conversation context. When true, the subagent receives the parent's session file via --fork and builds a branched conversation (it sees prior turns/messages). The subagent still runs in a separate spawned child process (process isolation) — fork is about context inheritance, not process sharing; independent of worktree (file-system isolation, see worktree param). When to use: only when the task extends from the parent and genuinely needs key information from the parent's conversation history that a self-contained task prompt cannot carry — most tasks a plain prompt can describe do NOT need fork, so keep false by default and enable only when the user explicitly asks or the task truly depends on seeing prior turns. Caveat: fork drags in the parent's dispatch records and unrelated task context, polluting the subagent (it cannot tell 'context meant for me' from 'parent dispatching me'); when state lives in an external store the subagent can query (e.g., cw handoff), prefer that over fork.",
|
|
109
109
|
})),
|
|
110
110
|
worktree: Type.Optional(Type.Boolean({
|
|
111
|
-
description: "Worktree isolation
|
|
111
|
+
description: "Worktree isolation: run the subagent in a dedicated git worktree, providing file-system level isolation from the parent session (prevents concurrent file-write conflicts). Independent of fork — worktree may be combined with fork:false (file isolation does not require context inheritance). When to use: parallel development scenarios where multiple agents write files concurrently and need isolated working directories (each gets its own checkout; merge later); leave false for single-agent or read-only tasks.",
|
|
112
112
|
})),
|
|
113
113
|
cwd: Type.Optional(Type.String({
|
|
114
114
|
description: 'Override the working directory for the subagent execution. Must be an absolute path. Defaults to the parent session\'s cwd.',
|
|
@@ -142,9 +142,9 @@ export interface AgentCallOpts {
|
|
|
142
142
|
* undefined 时 spawn 继承 workflow 进程的 cwd(向后兼容)。
|
|
143
143
|
*/
|
|
144
144
|
cwd?: string;
|
|
145
|
-
/** Inherit parent session context (fork mode).
|
|
145
|
+
/** Inherit parent session context (fork mode). Independent of worktree (file isolation). */
|
|
146
146
|
fork?: boolean;
|
|
147
|
-
/** Filesystem isolation: when true, creates a new git worktree for the agent
|
|
147
|
+
/** Filesystem isolation: when true, creates a new git worktree for the agent. Independent of fork. */
|
|
148
148
|
worktree?: boolean;
|
|
149
149
|
/** When true, agent() resolves {value, sessionFile, worktreePath, error} instead of a bare value.
|
|
150
150
|
* Worker-layer flag only — not forwarded to ExecuteOptions (mapToExecuteOptions drops it). */
|