@zhushanwen/pi-subagent-workflow 2.0.1 → 3.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/package.json +3 -2
- package/src/execution/__tests__/channel-registry-handshake.test.ts +18 -8
- package/src/execution/__tests__/execute-and-await-worktree.test.ts +219 -0
- package/src/execution/__tests__/finalize-record.test.ts +19 -6
- package/src/execution/__tests__/stdin-writer.test.ts +18 -5
- package/src/execution/__tests__/ui-request-observability.test.ts +21 -8
- package/src/execution/agent-registry.ts +10 -2
- package/src/execution/best-effort.ts +13 -5
- package/src/execution/channel-registry-access.ts +7 -3
- package/src/execution/execute-options-mapper.ts +2 -0
- package/src/execution/finalize-record.ts +5 -1
- package/src/execution/record-store.ts +10 -4
- package/src/execution/session-runner.ts +8 -2
- package/src/execution/stdin-writer.ts +8 -2
- package/src/execution/subagent-service.ts +56 -5
- package/src/execution/ui-request-handler-factory.ts +10 -10
- package/src/execution/ui-request-observability.ts +6 -2
- package/src/execution/ui-request-queue.ts +7 -1
- package/src/index.ts +21 -8
- package/src/interface/subagent-tool.ts +10 -11
- package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +22 -7
- package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
- package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +105 -1
- package/src/orchestration/__tests__/worker-script-builder.test.ts +91 -0
- package/src/orchestration/error-recovery.ts +23 -15
- package/src/orchestration/lifecycle.ts +6 -2
- package/src/orchestration/models/types.ts +18 -0
- package/src/orchestration/worker-script-builder.ts +32 -5
|
@@ -99,6 +99,10 @@ interface RunOptions {
|
|
|
99
99
|
args?: Record<string, unknown>;
|
|
100
100
|
/** 按 agent-call 顺序回发的 parsedOutput(默认每个回发 {ok:true})。 */
|
|
101
101
|
agentResults?: unknown[];
|
|
102
|
+
/** 按 agent-call 顺序回发的完整 agent-result 对象(覆盖 agentResults 的 content/parsedOutput 默认形)。
|
|
103
|
+
* 用于 returnMeta 测试:回发包含 sessionFile/worktreePath/error 的完整 result,
|
|
104
|
+
* worker handler 会原样取这些字段 resolve。未提供该项的索引退回 {content:"fallback", parsedOutput: agentResults[idx]}。 */
|
|
105
|
+
agentResultObjects?: Record<string, unknown>[];
|
|
102
106
|
/** 主线程对收到的 workflow-call 的处理:回发 workflow-result。 */
|
|
103
107
|
handleWorkflowCall?: (msg: WorkflowCallMsg) => unknown;
|
|
104
108
|
/** 是否在收到首个 agent-call 后立即发 abort(测 abort 路径)。 */
|
|
@@ -156,11 +160,14 @@ function runWorker(userScript: string, opts: RunOptions = {}): Promise<RunResult
|
|
|
156
160
|
return;
|
|
157
161
|
}
|
|
158
162
|
const parsed = opts.agentResults?.[agentCallIdx] ?? { ok: true };
|
|
163
|
+
// returnMeta 测试路径:若调用方提供完整 result 对象(含 sessionFile/worktreePath/error),
|
|
164
|
+
// 原样回发;否则用既有 {content, parsedOutput} 默认形。
|
|
165
|
+
const fullResult = opts.agentResultObjects?.[agentCallIdx];
|
|
159
166
|
agentCallIdx++;
|
|
160
167
|
worker.postMessage({
|
|
161
168
|
type: "agent-result",
|
|
162
169
|
callId: raw.callId,
|
|
163
|
-
result: { content: "fallback", parsedOutput: parsed },
|
|
170
|
+
result: fullResult ?? { content: "fallback", parsedOutput: parsed },
|
|
164
171
|
cached: false,
|
|
165
172
|
});
|
|
166
173
|
} else if (isWorkflowCall(raw)) {
|
|
@@ -345,3 +352,100 @@ describe("buildWorkerScript runtime — 之前缺失的路径覆盖", () => {
|
|
|
345
352
|
expect(res.exitCode).not.toBe(1);
|
|
346
353
|
});
|
|
347
354
|
});
|
|
355
|
+
|
|
356
|
+
// ── W2: agent() returnMeta 模式运行时验证 ───────────────────────────
|
|
357
|
+
// 现有 worker-script-builder.test.ts 的 16 条 returnMeta 断言全是字符串 toContain,
|
|
358
|
+
// 无法验证「真实 Worker 线程执行时 agent({returnMeta:true}) resolve 出对象、
|
|
359
|
+
// 不设 returnMeta resolve 单值」。此处补运行时验证(对称 handler 9b 分支)。
|
|
360
|
+
|
|
361
|
+
describe("buildWorkerScript runtime — W2 agent() returnMeta mode", () => {
|
|
362
|
+
it("agent({prompt, returnMeta:true}) resolve 出 {value, sessionFile, worktreePath, error}(非单值)", async () => {
|
|
363
|
+
// returnMeta:true → worker handler 走 9b 分支,resolve 包含 4 字段的对象。
|
|
364
|
+
// 主线程回发的 result 带 sessionFile/worktreePath/error,验证它们被原样透传。
|
|
365
|
+
const script = `
|
|
366
|
+
const r = await agent({ prompt: "with-meta", returnMeta: true });
|
|
367
|
+
return r;
|
|
368
|
+
`;
|
|
369
|
+
const res = await runWorker(script, {
|
|
370
|
+
agentResultObjects: [
|
|
371
|
+
{
|
|
372
|
+
content: "raw-text",
|
|
373
|
+
parsedOutput: { ok: true },
|
|
374
|
+
sessionFile: "/tmp/sess-1.jsonl",
|
|
375
|
+
worktreePath: "/tmp/wt-abc",
|
|
376
|
+
error: undefined,
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
expect(res.agentCalls).toHaveLength(1);
|
|
381
|
+
// agent-call 消息应透传 returnMeta(验证 m1 修复:prompt 分支本就透传)
|
|
382
|
+
expect(res.agentCalls[0]!.opts.returnMeta).toBe(true);
|
|
383
|
+
expect(res.workerError).toBeUndefined();
|
|
384
|
+
// value = parsedOutput ?? content = {ok:true}(结构化输出优先)
|
|
385
|
+
expect(res.returnValue).toEqual({
|
|
386
|
+
value: { ok: true },
|
|
387
|
+
sessionFile: "/tmp/sess-1.jsonl",
|
|
388
|
+
worktreePath: "/tmp/wt-abc",
|
|
389
|
+
error: undefined,
|
|
390
|
+
});
|
|
391
|
+
expect(res.exitCode).not.toBe(1);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("不设 returnMeta 时 agent() resolve 单值(向后兼容)", async () => {
|
|
395
|
+
// 无 returnMeta → handler 走 else 分支,resolve 裸 _value(向后兼容)。
|
|
396
|
+
const script = `
|
|
397
|
+
const r = await agent({ prompt: "no-meta" });
|
|
398
|
+
return r;
|
|
399
|
+
`;
|
|
400
|
+
const res = await runWorker(script, {
|
|
401
|
+
agentResultObjects: [
|
|
402
|
+
{
|
|
403
|
+
content: "raw-text",
|
|
404
|
+
parsedOutput: { ok: true },
|
|
405
|
+
sessionFile: "/tmp/sess-2.jsonl",
|
|
406
|
+
worktreePath: "/tmp/wt-def",
|
|
407
|
+
},
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
expect(res.agentCalls).toHaveLength(1);
|
|
411
|
+
expect(res.agentCalls[0]!.opts.returnMeta).toBeUndefined();
|
|
412
|
+
expect(res.workerError).toBeUndefined();
|
|
413
|
+
// 单值:parsedOutput 优先({ok:true}),sessionFile/worktreePath 被丢弃
|
|
414
|
+
expect(res.returnValue).toEqual({ ok: true });
|
|
415
|
+
expect(res.exitCode).not.toBe(1);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("agent({task, returnMeta:true}) task/agent 快捷分支也透传 returnMeta(m1 修复)", async () => {
|
|
419
|
+
// m1 修复:task/agent 快捷分支现在透传 returnMeta(之前丢弃)。
|
|
420
|
+
// 用 task 而非 prompt 触发快捷分支,验证 returnMeta 生效。
|
|
421
|
+
const script = `
|
|
422
|
+
const r = await agent({ task: "via-task-branch", returnMeta: true });
|
|
423
|
+
return r;
|
|
424
|
+
`;
|
|
425
|
+
const res = await runWorker(script, {
|
|
426
|
+
agentResultObjects: [
|
|
427
|
+
{
|
|
428
|
+
content: "task-raw",
|
|
429
|
+
parsedOutput: "task-value",
|
|
430
|
+
sessionFile: "/tmp/sess-3.jsonl",
|
|
431
|
+
worktreePath: "/tmp/wt-ghi",
|
|
432
|
+
error: "soft-fail-msg",
|
|
433
|
+
},
|
|
434
|
+
],
|
|
435
|
+
});
|
|
436
|
+
expect(res.agentCalls).toHaveLength(1);
|
|
437
|
+
// 快捷分支透传 returnMeta(m1 修复点)
|
|
438
|
+
expect(res.agentCalls[0]!.opts.returnMeta).toBe(true);
|
|
439
|
+
expect(res.agentCalls[0]!.opts.prompt).toBe("via-task-branch");
|
|
440
|
+
expect(res.workerError).toBeUndefined();
|
|
441
|
+
// returnMeta 生效 → resolve 对象(非单值)
|
|
442
|
+
expect(res.returnValue).toEqual({
|
|
443
|
+
value: "task-value",
|
|
444
|
+
sessionFile: "/tmp/sess-3.jsonl",
|
|
445
|
+
worktreePath: "/tmp/wt-ghi",
|
|
446
|
+
error: "soft-fail-msg",
|
|
447
|
+
});
|
|
448
|
+
expect(res.exitCode).not.toBe(1);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
@@ -131,3 +131,94 @@ describe("buildWorkerScript — W1 postMessage defense & parallel degrade", () =
|
|
|
131
131
|
});
|
|
132
132
|
});
|
|
133
133
|
});
|
|
134
|
+
|
|
135
|
+
// ── W2: agent() returnMeta 模式 ──
|
|
136
|
+
// 验证 returnMeta 标志:known fields 放行 + handler resolve 分支 + 缓存重放分支,
|
|
137
|
+
// 且两处分支结构对称(returnMeta===true → {value, sessionFile, worktreePath, error})。
|
|
138
|
+
|
|
139
|
+
describe("buildWorkerScript — W2 agent() returnMeta mode", () => {
|
|
140
|
+
const script = buildWorkerScript("// noop user script");
|
|
141
|
+
|
|
142
|
+
describe("returnMeta known field whitelist", () => {
|
|
143
|
+
it("includes returnMeta in _knownFields Set", () => {
|
|
144
|
+
expect(script).toContain('"returnMeta"');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("includes returnMeta in unknown-fields warning text", () => {
|
|
148
|
+
expect(script).toContain("cwd, fork, worktree, returnMeta");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("does NOT warn about returnMeta when only returnMeta is passed (parity with fork/worktree)", () => {
|
|
152
|
+
// fork/worktree 已是 known fields;returnMeta 加入后不应触发 warn 文案中的 returnMeta。
|
|
153
|
+
// 简化断言:warning 文案里 Known 列表含 returnMeta(即被识别)。
|
|
154
|
+
expect(script).toMatch(/Known fields:.*returnMeta/);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("_pendingCalls stores returnMeta flag", () => {
|
|
159
|
+
it("stores returnMeta: opts.returnMeta === true in _pendingCalls.set", () => {
|
|
160
|
+
expect(script).toContain(
|
|
161
|
+
"returnMeta: opts.returnMeta === true",
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("agent-result handler resolve branch (改动 9b)", () => {
|
|
167
|
+
it("branches on pending.returnMeta", () => {
|
|
168
|
+
expect(script).toContain("if (pending.returnMeta)");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("resolve branch returns {value, sessionFile, worktreePath, error}", () => {
|
|
172
|
+
const handlerBlock = script.match(
|
|
173
|
+
/if \(pending\.returnMeta\) \{[\s\S]*?\} else \{[\s\S]*?pending\.resolve\(_value\);[\s\S]*?\}/,
|
|
174
|
+
);
|
|
175
|
+
expect(handlerBlock).toBeTruthy();
|
|
176
|
+
expect(handlerBlock![0]).toContain("value: _value");
|
|
177
|
+
expect(handlerBlock![0]).toContain("sessionFile: msg.result.sessionFile");
|
|
178
|
+
expect(handlerBlock![0]).toContain("worktreePath: msg.result.worktreePath");
|
|
179
|
+
expect(handlerBlock![0]).toContain("error: msg.result.error");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("fallback resolve single value uses msg.result.parsedOutput ?? msg.result.content", () => {
|
|
183
|
+
expect(script).toContain(
|
|
184
|
+
"const _value = msg.result.parsedOutput ?? msg.result.content;",
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("cache replay returnMeta branch (改动 9c)", () => {
|
|
190
|
+
it("early-returns undefined when !cached", () => {
|
|
191
|
+
expect(script).toContain("if (!cached) return undefined;");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("branches on opts.returnMeta === true", () => {
|
|
195
|
+
expect(script).toContain("if (opts.returnMeta === true)");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("cache replay returns {value, sessionFile, worktreePath, error} from cached", () => {
|
|
199
|
+
const cacheBlock = script.match(
|
|
200
|
+
/if \(opts\.returnMeta === true\) \{[\s\S]*?return _cachedValue;/,
|
|
201
|
+
);
|
|
202
|
+
expect(cacheBlock).toBeTruthy();
|
|
203
|
+
expect(cacheBlock![0]).toContain("value: _cachedValue");
|
|
204
|
+
expect(cacheBlock![0]).toContain("sessionFile: cached.sessionFile");
|
|
205
|
+
expect(cacheBlock![0]).toContain("worktreePath: cached.worktreePath");
|
|
206
|
+
expect(cacheBlock![0]).toContain("error: cached.error");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("cache replay fallback uses cached.parsedOutput ?? cached.content", () => {
|
|
210
|
+
expect(script).toContain(
|
|
211
|
+
"const _cachedValue = cached.parsedOutput ?? cached.content;",
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe("handler (9b) and cache replay (9c) branch symmetry", () => {
|
|
217
|
+
it("both expose worktreePath field (CL-1/DEC-2 core)", () => {
|
|
218
|
+
// handler 读 msg.result.worktreePath;cache replay 读 cached.worktreePath。
|
|
219
|
+
expect(script).toContain("msg.result.worktreePath");
|
|
220
|
+
expect(script).toContain("cached.worktreePath");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* 参考:domain-models.md §失败处理矩阵。
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
29
|
+
|
|
28
30
|
import { SLUG_MAX_LENGTH } from "../execution/execute-options-mapper.ts";
|
|
29
31
|
import { createRecord, updateFromEvent } from "../execution/execution-record.ts";
|
|
30
32
|
import { SubagentStream } from "../execution/stream-sink.ts";
|
|
@@ -40,6 +42,8 @@ import type { AgentCallOpts, AgentResult, ExecutionTraceNode } from "./models/ty
|
|
|
40
42
|
import type { WorkflowRun } from "./models/workflow-run.ts";
|
|
41
43
|
import type { WorkerHandle } from "./worker-handle.ts";
|
|
42
44
|
|
|
45
|
+
const logger = getLogger("subagents");
|
|
46
|
+
|
|
43
47
|
// ── 常量 ─────────────────────────────────────────────────────
|
|
44
48
|
|
|
45
49
|
/**
|
|
@@ -225,7 +229,7 @@ function dispatchAgentCall(
|
|
|
225
229
|
if (typeof msg.callId !== "number" || !Number.isFinite(msg.callId) ||
|
|
226
230
|
typeof msg.opts !== "object" || msg.opts === null ||
|
|
227
231
|
typeof msg.opts.prompt !== "string") {
|
|
228
|
-
|
|
232
|
+
logger.error(`[workflow] malformed agent-call message: callId=${JSON.stringify(msg.callId)}, opts=${JSON.stringify(msg.opts)?.slice(0, 200)}`);
|
|
229
233
|
return;
|
|
230
234
|
}
|
|
231
235
|
|
|
@@ -299,7 +303,7 @@ function dispatchAgentCall(
|
|
|
299
303
|
});
|
|
300
304
|
postAgentResult(run, msg.callId, errorResult, false);
|
|
301
305
|
deps.store.save(run).catch((e: unknown) => {
|
|
302
|
-
|
|
306
|
+
logger.error(`[workflow] store.save failed (resolveAgentOpts): ${e instanceof Error ? e.message : String(e)}`);
|
|
303
307
|
});
|
|
304
308
|
return;
|
|
305
309
|
}
|
|
@@ -352,7 +356,7 @@ function dispatchAgentCall(
|
|
|
352
356
|
postBudgetUpdate(run);
|
|
353
357
|
deps.store.save(run).catch((e: unknown) => {
|
|
354
358
|
const m = e instanceof Error ? e.message : String(e);
|
|
355
|
-
|
|
359
|
+
logger.error(`[workflow] store.save failed (agent call ${msg.callId}): ${m}`);
|
|
356
360
|
});
|
|
357
361
|
|
|
358
362
|
// C-2:budget 超限 → 终止整个 run(避免继续 spawn 烧预算)
|
|
@@ -374,7 +378,7 @@ function dispatchAgentCall(
|
|
|
374
378
|
if (transitioned) {
|
|
375
379
|
deps.store.save(run).catch((e: unknown) => {
|
|
376
380
|
const m = e instanceof Error ? e.message : String(e);
|
|
377
|
-
|
|
381
|
+
logger.error(`[workflow] store.save failed (budget done): ${m}`);
|
|
378
382
|
});
|
|
379
383
|
deps.log?.("debug", "workflow:error-recovery", "run saved after budget done", { runId: run.runId, reason: run.state.reason });
|
|
380
384
|
// M12: onRunDone/emit 单独 try——这些是真实副作用,错误不应被静默吞掉
|
|
@@ -385,7 +389,7 @@ function dispatchAgentCall(
|
|
|
385
389
|
deps.onRunDone?.(run);
|
|
386
390
|
} catch (err) {
|
|
387
391
|
const m = err instanceof Error ? err.message : String(err);
|
|
388
|
-
|
|
392
|
+
logger.error(`[workflow] onRunDone/emit failed (budget done): ${m}`);
|
|
389
393
|
}
|
|
390
394
|
}
|
|
391
395
|
}
|
|
@@ -394,7 +398,7 @@ function dispatchAgentCall(
|
|
|
394
398
|
// withSlot 在 queued + signal-aborted 时 reject AbortError——预期,不记错。
|
|
395
399
|
if (err instanceof Error && err.name === "AbortError") return;
|
|
396
400
|
const message = err instanceof Error ? err.message : String(err);
|
|
397
|
-
|
|
401
|
+
logger.error(`[workflow] agent call ${msg.callId} failed: ${message}`);
|
|
398
402
|
// 兜底回发:executeAgentCall 抛非 Abort 异常时(如 runner undefined 的 TypeError、
|
|
399
403
|
// gate.withSlot 内部 bug)原 catch 仅 console.error,worker 内对 callId 的 pending
|
|
400
404
|
// Promise 永不 resolve → agent() 永久 await → worker 脚本挂死。构造 failed AgentResult
|
|
@@ -422,7 +426,7 @@ function dispatchAgentCall(
|
|
|
422
426
|
// S2: 与 .then 对称——catch 路径也同步 worker $BUDGET(幂等)
|
|
423
427
|
postBudgetUpdate(run);
|
|
424
428
|
deps.store.save(run).catch((e: unknown) => {
|
|
425
|
-
|
|
429
|
+
logger.error(`[workflow] store.save failed (catch fallback): ${e instanceof Error ? e.message : String(e)}`);
|
|
426
430
|
});
|
|
427
431
|
});
|
|
428
432
|
}
|
|
@@ -432,8 +436,12 @@ function dispatchAgentCall(
|
|
|
432
436
|
*
|
|
433
437
|
* postResult(workflow-call)与 postAgentResult(agent-call)各自前缀不同,故 prefix 参数化,
|
|
434
438
|
* 共享返回类型与构造逻辑,避免字面量重复导致形状漂移。
|
|
439
|
+
*
|
|
440
|
+
* W2 防御关键纯函数——export 供独立单测(error-recovery-serialize-failed-result.test.ts)验证
|
|
441
|
+
* 返回 shape `{content:"", error:"<prefix>: <errMsg>"}`,确保两条 fallback 路径(workflow-call /
|
|
442
|
+
* agent-call)共享同一构造逻辑不漂移。
|
|
435
443
|
*/
|
|
436
|
-
function makeSerializeFailedResult(
|
|
444
|
+
export function makeSerializeFailedResult(
|
|
437
445
|
prefix: string,
|
|
438
446
|
errMsg: string,
|
|
439
447
|
): { content: string; error: string } {
|
|
@@ -456,7 +464,7 @@ function dispatchWorkflowCall(
|
|
|
456
464
|
if (typeof msg.callId !== "number" || !Number.isFinite(msg.callId) ||
|
|
457
465
|
typeof msg.name !== "string" ||
|
|
458
466
|
typeof msg.args !== "object" || msg.args === null) {
|
|
459
|
-
|
|
467
|
+
logger.error(`[workflow] malformed workflow-call message: callId=${JSON.stringify(msg.callId)}, name=${JSON.stringify(msg.name)}`);
|
|
460
468
|
return;
|
|
461
469
|
}
|
|
462
470
|
|
|
@@ -474,7 +482,7 @@ function dispatchWorkflowCall(
|
|
|
474
482
|
});
|
|
475
483
|
} catch (err) {
|
|
476
484
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
477
|
-
|
|
485
|
+
logger.error(`[workflow] postResult (workflow-call callId=${msg.callId}) failed: ${errMsg}. Sending error fallback.`);
|
|
478
486
|
// 回发纯字符串 fallback result(必可克隆),让 worker pending resolve
|
|
479
487
|
try {
|
|
480
488
|
run.runtime?.worker.postMessage({
|
|
@@ -484,7 +492,7 @@ function dispatchWorkflowCall(
|
|
|
484
492
|
});
|
|
485
493
|
} catch {
|
|
486
494
|
// fallback 也失败——worker 此 callId 的 pending 只能靠 timeout 兜底
|
|
487
|
-
|
|
495
|
+
logger.error(`[workflow] postResult fallback also failed (callId=${msg.callId}): worker pending will hang until timeout`);
|
|
488
496
|
}
|
|
489
497
|
}
|
|
490
498
|
};
|
|
@@ -515,7 +523,7 @@ function dispatchWorkflowCall(
|
|
|
515
523
|
* postMessage 同步抛 DataCloneError。若冒泡到 dispatchAgentCall 的 .then 回调,会中断
|
|
516
524
|
* 后续 postBudgetUpdate/store.save/budget 检查,run 卡在 running。故内部 try/catch:
|
|
517
525
|
* 失败时记录诊断 + 回发纯字符串 fallback result(必可克隆),让 worker pending resolve。
|
|
518
|
-
*
|
|
526
|
+
* 函数签名不变(所有调用点无需改动),仅用共享 logger 记日志(deps 不在手边)。
|
|
519
527
|
*/
|
|
520
528
|
function postAgentResult(
|
|
521
529
|
run: WorkflowRun,
|
|
@@ -527,7 +535,7 @@ function postAgentResult(
|
|
|
527
535
|
run.runtime?.worker.postMessage({ type: "agent-result", callId, result, cached });
|
|
528
536
|
} catch (err) {
|
|
529
537
|
const msg = err instanceof Error ? err.message : String(err);
|
|
530
|
-
|
|
538
|
+
logger.error(`[workflow] postAgentResult failed (callId=${callId}): ${msg}. Result likely contains non-cloneable value.`);
|
|
531
539
|
// 回发纯字符串 fallback result(必可克隆),让 worker pending resolve(避免永久挂起)
|
|
532
540
|
try {
|
|
533
541
|
run.runtime?.worker.postMessage({
|
|
@@ -539,7 +547,7 @@ function postAgentResult(
|
|
|
539
547
|
});
|
|
540
548
|
} catch {
|
|
541
549
|
// fallback 也失败——worker 此 callId 的 pending 只能靠 timeout/exit 兜底
|
|
542
|
-
|
|
550
|
+
logger.error(`[workflow] postAgentResult fallback also failed (callId=${callId}): worker pending will hang until timeout`);
|
|
543
551
|
}
|
|
544
552
|
}
|
|
545
553
|
}
|
|
@@ -565,7 +573,7 @@ export function postBudgetUpdate(run: WorkflowRun): void {
|
|
|
565
573
|
const msg = err instanceof Error ? err.message : String(err);
|
|
566
574
|
// budget 是纯 number 不太可能失败,但防御性兜底——budget 同步非关键(worker 仍可
|
|
567
575
|
// 基于 $BUDGET.spent() 自行累计),失败仅记日志,不中断调用方流程。
|
|
568
|
-
|
|
576
|
+
logger.error(`[workflow] postBudgetUpdate failed: ${msg}. Budget sync to worker skipped (non-critical).`);
|
|
569
577
|
}
|
|
570
578
|
}
|
|
571
579
|
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
* 参考:domain-models.md §1(聚合根状态机)。
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
+
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
34
|
+
|
|
33
35
|
import { ConcurrencyGate, DEFAULT_CONCURRENCY } from "./concurrency-gate.ts";
|
|
34
36
|
import {
|
|
35
37
|
handleWorkerError,
|
|
@@ -45,6 +47,8 @@ import type { DoneReason } from "./models/types.ts";
|
|
|
45
47
|
import { WorkflowRun } from "./models/workflow-run.ts";
|
|
46
48
|
import type { WorkerHandle } from "./worker-handle.ts";
|
|
47
49
|
|
|
50
|
+
const logger = getLogger("subagents");
|
|
51
|
+
|
|
48
52
|
// ── 常量 ─────────────────────────────────────────────────────
|
|
49
53
|
|
|
50
54
|
/** runId 生成:wf-<timestamp>-<base36 random 6 chars>。 */
|
|
@@ -115,7 +119,7 @@ export function scheduleTimeBudget(
|
|
|
115
119
|
void abortRun(runId, deps, "Time budget exceeded", "time_limited").catch(
|
|
116
120
|
(err: unknown) => {
|
|
117
121
|
const msg = err instanceof Error ? err.message : String(err);
|
|
118
|
-
|
|
122
|
+
logger.error(`[workflow] time budget abort failed: ${msg}`);
|
|
119
123
|
},
|
|
120
124
|
);
|
|
121
125
|
}, budgetTimeMs);
|
|
@@ -174,7 +178,7 @@ export async function runWorkflow(
|
|
|
174
178
|
() => {
|
|
175
179
|
void abortRun(runId, deps, "External signal aborted").catch((err: unknown) => {
|
|
176
180
|
const msg = err instanceof Error ? err.message : String(err);
|
|
177
|
-
|
|
181
|
+
logger.error(`[workflow] abortRun on signal failed: ${msg}`);
|
|
178
182
|
});
|
|
179
183
|
},
|
|
180
184
|
{ once: true },
|
|
@@ -135,6 +135,13 @@ export interface AgentCallOpts {
|
|
|
135
135
|
* undefined 时 spawn 继承 workflow 进程的 cwd(向后兼容)。
|
|
136
136
|
*/
|
|
137
137
|
cwd?: string;
|
|
138
|
+
/** Inherit parent session context (fork mode). Required when worktree isolation is enabled. */
|
|
139
|
+
fork?: boolean;
|
|
140
|
+
/** Filesystem isolation: when true, creates a new git worktree for the agent (requires fork: true). */
|
|
141
|
+
worktree?: boolean;
|
|
142
|
+
/** When true, agent() resolves {value, sessionFile, worktreePath, error} instead of a bare value.
|
|
143
|
+
* Worker-layer flag only — not forwarded to ExecuteOptions (mapToExecuteOptions drops it). */
|
|
144
|
+
returnMeta?: boolean;
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
/**
|
|
@@ -194,6 +201,17 @@ export interface AgentResult {
|
|
|
194
201
|
* 窗口期内可能 undefined(session 尚未创建成功)。
|
|
195
202
|
*/
|
|
196
203
|
sessionFile?: string;
|
|
204
|
+
/**
|
|
205
|
+
* Absolute path of the git worktree used for filesystem isolation (set when
|
|
206
|
+
* worktree isolation is active). Injected by executeAndAwait from record.worktreeHandle.path.
|
|
207
|
+
*
|
|
208
|
+
* ⚠️ Diagnostic only, may not exist: executeAndAwait's finalizeRecord cleans up the
|
|
209
|
+
* worktree (git worktree remove --force) before returning, so by the time this field
|
|
210
|
+
* reaches the caller the directory has typically been deleted. Use it only for log/trace
|
|
211
|
+
* correlation (e.g. attributing a session jsonl to its worktree origin) — never as a cwd
|
|
212
|
+
* for a subsequent agent or filesystem operation (would ENOENT).
|
|
213
|
+
*/
|
|
214
|
+
worktreePath?: string;
|
|
197
215
|
/** All tool calls collected from JSONL stream (FR-7). */
|
|
198
216
|
toolCalls?: ToolCallEntry[];
|
|
199
217
|
}
|
|
@@ -141,7 +141,18 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
141
141
|
' // 不丢失。失败 resolve 为空字符串是既定容错策略。',
|
|
142
142
|
' // parsedOutput: validated data object from structured-output execute().',
|
|
143
143
|
' // Fallback to content (raw text) when no schema was requested or on error.',
|
|
144
|
-
'
|
|
144
|
+
' // W2 改动 9(b):returnMeta===true 时 resolve {value,sessionFile,worktreePath,error}(对称缓存重放 9c);否则单值。',
|
|
145
|
+
' const _value = msg.result.parsedOutput ?? msg.result.content;',
|
|
146
|
+
' if (pending.returnMeta) {',
|
|
147
|
+
' pending.resolve({',
|
|
148
|
+
' value: _value,',
|
|
149
|
+
' sessionFile: msg.result.sessionFile,',
|
|
150
|
+
' worktreePath: msg.result.worktreePath,',
|
|
151
|
+
' error: msg.result.error,',
|
|
152
|
+
' });',
|
|
153
|
+
' } else {',
|
|
154
|
+
' pending.resolve(_value);',
|
|
155
|
+
' }',
|
|
145
156
|
' }',
|
|
146
157
|
' } else if (msg.type === "workflow-result") {',
|
|
147
158
|
' const pending = _pendingCalls.get(msg.callId);',
|
|
@@ -185,6 +196,8 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
185
196
|
' if (firstArg.prompt) {',
|
|
186
197
|
' opts = firstArg;',
|
|
187
198
|
' } else if (firstArg.task || firstArg.agent) {',
|
|
199
|
+
' // fork/worktree/returnMeta forwarded here (parity with agent({prompt,...}) and direct-pass',
|
|
200
|
+
' // branches). Previously this shortcut dropped them (w1 only noted, not wired).',
|
|
188
201
|
' opts = {',
|
|
189
202
|
' prompt: firstArg.task || firstArg.prompt || "",',
|
|
190
203
|
' description: firstArg.label || firstArg.description,',
|
|
@@ -195,6 +208,9 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
195
208
|
' skill: firstArg.skill,',
|
|
196
209
|
' timeoutMs: firstArg.timeoutMs,',
|
|
197
210
|
' cwd: firstArg.cwd,',
|
|
211
|
+
' fork: firstArg.fork,',
|
|
212
|
+
' worktree: firstArg.worktree,',
|
|
213
|
+
' returnMeta: firstArg.returnMeta,',
|
|
198
214
|
' };',
|
|
199
215
|
' } else {',
|
|
200
216
|
' opts = firstArg;',
|
|
@@ -204,10 +220,10 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
204
220
|
' }',
|
|
205
221
|
'',
|
|
206
222
|
' // Validate known agent() fields to catch API misuse early',
|
|
207
|
-
' const _knownFields = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd"]);',
|
|
223
|
+
' const _knownFields = new Set(["prompt", "description", "schema", "model", "scene", "label", "task", "agent", "phase", "skill", "timeoutMs", "cwd", "fork", "worktree", "returnMeta"]);',
|
|
208
224
|
' const _unknownFields = Object.keys(opts).filter((k) => !_knownFields.has(k));',
|
|
209
225
|
' if (_unknownFields.length > 0) {',
|
|
210
|
-
' _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, cwd"]);',
|
|
226
|
+
' _pushWorkerLog("warn", ["[workflow] agent() received unknown fields: " + _unknownFields.join(", ") + ". Known fields: prompt, description, schema, model, scene, label, task, agent, phase, skill, timeoutMs, cwd, fork, worktree, returnMeta"]);',
|
|
211
227
|
' }',
|
|
212
228
|
'',
|
|
213
229
|
' const callId = _callIdCounter;',
|
|
@@ -217,7 +233,18 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
217
233
|
' const cached = _callCache.get(callId);',
|
|
218
234
|
' // 与 live handler 对齐:失败也 resolve(回退 content),不 throw。',
|
|
219
235
|
' // 见 agent-result 消息处理的注释:拒绝传播失败到 agent 外部。',
|
|
220
|
-
'
|
|
236
|
+
' // W2 改动 9(c):returnMeta 分支(对称 handler 9b),opts 仍在闭包作用域。',
|
|
237
|
+
' if (!cached) return undefined;',
|
|
238
|
+
' const _cachedValue = cached.parsedOutput ?? cached.content;',
|
|
239
|
+
' if (opts.returnMeta === true) {',
|
|
240
|
+
' return {',
|
|
241
|
+
' value: _cachedValue,',
|
|
242
|
+
' sessionFile: cached.sessionFile,',
|
|
243
|
+
' worktreePath: cached.worktreePath,',
|
|
244
|
+
' error: cached.error,',
|
|
245
|
+
' };',
|
|
246
|
+
' }',
|
|
247
|
+
' return _cachedValue;',
|
|
221
248
|
' }',
|
|
222
249
|
'',
|
|
223
250
|
' const _effectivePhase = opts.phase || _currentPhase;\n' +
|
|
@@ -227,7 +254,7 @@ export function buildWorkerScript(userScript: string): string {
|
|
|
227
254
|
' return Promise.reject(new Error("postMessage failed for agent-call (callId=" + callId + "): see workerLogs"));',
|
|
228
255
|
' }',
|
|
229
256
|
' return new Promise((resolve, reject) => {',
|
|
230
|
-
' _pendingCalls.set(callId, { resolve, reject });',
|
|
257
|
+
' _pendingCalls.set(callId, { resolve, reject, returnMeta: opts.returnMeta === true });',
|
|
231
258
|
' });',
|
|
232
259
|
' }',
|
|
233
260
|
'',
|