@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.
Files changed (28) hide show
  1. package/package.json +3 -2
  2. package/src/execution/__tests__/channel-registry-handshake.test.ts +18 -8
  3. package/src/execution/__tests__/execute-and-await-worktree.test.ts +219 -0
  4. package/src/execution/__tests__/finalize-record.test.ts +19 -6
  5. package/src/execution/__tests__/stdin-writer.test.ts +18 -5
  6. package/src/execution/__tests__/ui-request-observability.test.ts +21 -8
  7. package/src/execution/agent-registry.ts +10 -2
  8. package/src/execution/best-effort.ts +13 -5
  9. package/src/execution/channel-registry-access.ts +7 -3
  10. package/src/execution/execute-options-mapper.ts +2 -0
  11. package/src/execution/finalize-record.ts +5 -1
  12. package/src/execution/record-store.ts +10 -4
  13. package/src/execution/session-runner.ts +8 -2
  14. package/src/execution/stdin-writer.ts +8 -2
  15. package/src/execution/subagent-service.ts +56 -5
  16. package/src/execution/ui-request-handler-factory.ts +10 -10
  17. package/src/execution/ui-request-observability.ts +6 -2
  18. package/src/execution/ui-request-queue.ts +7 -1
  19. package/src/index.ts +21 -8
  20. package/src/interface/subagent-tool.ts +10 -11
  21. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +22 -7
  22. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
  23. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +105 -1
  24. package/src/orchestration/__tests__/worker-script-builder.test.ts +91 -0
  25. package/src/orchestration/error-recovery.ts +23 -15
  26. package/src/orchestration/lifecycle.ts +6 -2
  27. package/src/orchestration/models/types.ts +18 -0
  28. 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
- console.error(`[workflow] malformed agent-call message: callId=${JSON.stringify(msg.callId)}, opts=${JSON.stringify(msg.opts)?.slice(0, 200)}`);
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
- console.error(`[workflow] store.save failed (resolveAgentOpts): ${e instanceof Error ? e.message : String(e)}`);
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
- console.error(`[workflow] store.save failed (agent call ${msg.callId}): ${m}`);
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
- console.error(`[workflow] store.save failed (budget done): ${m}`);
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
- console.error(`[workflow] onRunDone/emit failed (budget done): ${m}`);
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
- console.error(`[workflow] agent call ${msg.callId} failed: ${message}`);
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
- console.error(`[workflow] store.save failed (catch fallback): ${e instanceof Error ? e.message : String(e)}`);
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
- console.error(`[workflow] malformed workflow-call message: callId=${JSON.stringify(msg.callId)}, name=${JSON.stringify(msg.name)}`);
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
- console.error(`[workflow] postResult (workflow-call callId=${msg.callId}) failed: ${errMsg}. Sending error fallback.`);
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
- console.error(`[workflow] postResult fallback also failed (callId=${msg.callId}): worker pending will hang until timeout`);
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
- * 函数签名不变(所有调用点无需改动),仅用 console.error 记日志(deps 不在手边)。
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
- console.error(`[workflow] postAgentResult failed (callId=${callId}): ${msg}. Result likely contains non-cloneable value.`);
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
- console.error(`[workflow] postAgentResult fallback also failed (callId=${callId}): worker pending will hang until timeout`);
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
- console.error(`[workflow] postBudgetUpdate failed: ${msg}. Budget sync to worker skipped (non-critical).`);
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
- console.error(`[workflow] time budget abort failed: ${msg}`);
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
- console.error(`[workflow] abortRun on signal failed: ${msg}`);
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
- ' pending.resolve(msg.result.parsedOutput ?? msg.result.content);',
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
- ' return cached ? (cached.parsedOutput ?? cached.content) : undefined;',
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
  '',