@zhushanwen/pi-subagent-workflow 0.3.2 → 0.4.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 (33) hide show
  1. package/agents/context-builder.md +1 -0
  2. package/agents/explorer.md +2 -2
  3. package/agents/oracle.md +1 -0
  4. package/agents/orchestrator.md +7 -2
  5. package/agents/researcher.md +6 -3
  6. package/agents/reviewer.md +1 -0
  7. package/agents/worker.md +1 -0
  8. package/package.json +1 -1
  9. package/src/execution/__tests__/agent-registry.test.ts +19 -2
  10. package/src/execution/__tests__/format.test.ts +15 -1
  11. package/src/execution/__tests__/notifier-flush.test.ts +109 -1
  12. package/src/execution/__tests__/sdk-contract.test.ts +9 -11
  13. package/src/execution/__tests__/spawn-args.test.ts +18 -1
  14. package/src/execution/__tests__/subagent-service.test.ts +4 -1
  15. package/src/execution/__tests__/tool-action.test.ts +10 -5
  16. package/src/execution/model-resolver.ts +1 -1
  17. package/src/execution/notifier.ts +79 -8
  18. package/src/execution/subagent-service.ts +10 -1
  19. package/src/index.ts +3 -0
  20. package/src/interface/__tests__/detectors.test.ts +3 -28
  21. package/src/interface/__tests__/subagent-tool-prompt.test.ts +54 -11
  22. package/src/interface/__tests__/tool-render.test.ts +122 -0
  23. package/src/interface/__tests__/workflow-tool-prompt.test.ts +1 -1
  24. package/src/interface/format.ts +11 -7
  25. package/src/interface/subagent-actions.ts +16 -5
  26. package/src/interface/subagent-tool.ts +81 -98
  27. package/src/interface/tool-render.ts +9 -11
  28. package/src/interface/tool-workflow.ts +10 -90
  29. package/src/orchestration/error-recovery.ts +2 -2
  30. package/src/orchestration/models/ports.ts +3 -3
  31. package/src/orchestration/models/workflow-run.ts +3 -3
  32. package/src/orchestration/worker-script-builder.ts +1 -1
  33. package/src/orchestration/node-ops.ts +0 -194
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Workflow Extension — workflow tool(7 actions,FR-5 tool 收口)。
2
+ * Workflow Extension — workflow tool(5 actions,FR-5 tool 收口)。
3
3
  *
4
4
  * 合并原 tool-workflow.ts + tool-workflow-run.ts 为单 tool。
5
5
  *
@@ -9,12 +9,10 @@
9
9
  * - pause: 调 pauseRun
10
10
  * - resume: 调 resumeRun
11
11
  * - abort: 调 abortRun
12
- * - retry-node: 调 retryNode
13
- * - skip-node: 调 skipNode
14
12
  *
15
13
  * **restart 不包含**(D-9 废弃)。
16
14
  *
17
- * 层归属:Interface。依赖 Pi SDK + Engine lifecycle/node-ops/launcher + helpers。
15
+ * 层归属:Interface。依赖 Pi SDK + Engine lifecycle/launcher + helpers。
18
16
  *
19
17
  * 参考:domain-models.md §FR-5(tool 收口 4→2)。
20
18
  */
@@ -36,7 +34,6 @@ import type { LauncherDeps } from "../orchestration/launcher.ts";
36
34
  import { abortRun, pauseRun, resumeRun, runWorkflow } from "../orchestration/lifecycle.ts";
37
35
  import type { RunStore } from "../orchestration/models/ports.ts";
38
36
  import type { WorkflowRun } from "../orchestration/models/workflow-run.ts";
39
- import { retryNode, skipNode } from "../orchestration/node-ops.ts";
40
37
  import { mapRunIcon, mapRunStatus, toGuiCtx } from "./gui-mappers.ts";
41
38
  import {
42
39
  acquireReentryGuard,
@@ -54,9 +51,7 @@ export type WorkflowAction =
54
51
  | "status"
55
52
  | "pause"
56
53
  | "resume"
57
- | "abort"
58
- | "retry-node"
59
- | "skip-node";
54
+ | "abort";
60
55
 
61
56
  const WORKFLOW_ACTIONS: readonly WorkflowAction[] = [
62
57
  "run",
@@ -64,8 +59,6 @@ const WORKFLOW_ACTIONS: readonly WorkflowAction[] = [
64
59
  "pause",
65
60
  "resume",
66
61
  "abort",
67
- "retry-node",
68
- "skip-node",
69
62
  ];
70
63
 
71
64
  const WorkflowParams = Type.Object({
@@ -82,10 +75,7 @@ const WorkflowParams = Type.Object({
82
75
  }),
83
76
  ),
84
77
  runId: Type.Optional(
85
- Type.String({ description: "Workflow run ID (pause/resume/abort/retry-node/skip-node)" }),
86
- ),
87
- callId: Type.Optional(
88
- Type.Number({ description: "Agent call ID (retry-node/skip-node)" }),
78
+ Type.String({ description: "Workflow run ID (pause/resume/abort)" }),
89
79
  ),
90
80
  args: Type.Optional(
91
81
  Type.Record(Type.String(), Type.Unknown(), {
@@ -151,8 +141,7 @@ interface RunSummary {
151
141
  export type WorkflowToolDetails =
152
142
  | { action: "run"; runId: string; status: "running" | "not_found"; name: string; slug?: string; stateFile?: string; __gui__?: GuiRenderResult }
153
143
  | { action: "status"; runs: RunSummary[]; __gui__?: GuiRenderResult }
154
- | { action: "pause" | "resume" | "abort"; runId: string; status: string; reason?: string; __gui__?: GuiRenderResult }
155
- | { action: "retry-node" | "skip-node"; runId: string; callId: number; __gui__?: GuiRenderResult };
144
+ | { action: "pause" | "resume" | "abort"; runId: string; status: string; reason?: string; __gui__?: GuiRenderResult };
156
145
 
157
146
  /** Result returned by the `workflow` tool's execute. */
158
147
  export interface ToolResult {
@@ -206,8 +195,8 @@ export function buildWorkflowGui(details: WorkflowToolDetails) {
206
195
  }),
207
196
  });
208
197
  }
209
- // pause/resume/abort/retry-node/skip-node
210
- // abort 是破坏性终止、pause 是挂起(非成功完成),用 warn 区分;resume/retry/skip 保留 ok
198
+ // pause/resume/abort
199
+ // abort 是破坏性终止、pause 是挂起(非成功完成),用 warn 区分;resume 保留 ok
211
200
  const severity = details.action === "abort" || details.action === "pause" ? "warn" as const : "ok" as const;
212
201
  return guiComponent("stats-line", {
213
202
  items: [{
@@ -221,7 +210,7 @@ export function buildWorkflowGui(details: WorkflowToolDetails) {
221
210
  // ── Tool registration ────────────────────────────────────────
222
211
 
223
212
  /**
224
- * 注册 workflow tool(7 actions)。
213
+ * 注册 workflow tool(5 actions: run / status / pause / resume / abort)。
225
214
  *
226
215
  * @param pi ExtensionAPI
227
216
  * @param deps LauncherDeps(LifecycleDeps + registry)
@@ -241,9 +230,7 @@ export function registerWorkflowTool(
241
230
  name: "workflow",
242
231
  label: "Workflow",
243
232
  description:
244
- "Execute and control workflows: run (start), status, pause, resume, abort, " +
245
- "retry-node (re-run a failed agent call to refresh its trace; does NOT resume the " +
246
- "workflow script or change its output — see promptGuidelines), skip-node (mark a call as skipped).\n" +
233
+ "Execute and control workflows: run (start), status, pause, resume, abort.\n" +
247
234
  "Replaces workflow + workflow-run tools.",
248
235
  promptSnippet: "Run, pause, resume, abort, or check workflow status",
249
236
  promptGuidelines: [
@@ -259,15 +246,10 @@ export function registerWorkflowTool(
259
246
  "with source tags and descriptions. Then use this tool's run action to start one.",
260
247
  "run: discover by name/description, then start in background (no user confirmation needed).",
261
248
  "Do NOT poll status after starting — results appear automatically via notifyDone.",
262
- "retry-node/skip-node: for specific failed agent calls (requires runId + callId). " +
263
- "retry-node only re-runs the call and refreshes the trace — the workflow script has " +
264
- "already moved past the failed call, so the new result does NOT feed back into the " +
265
- "script flow. Use retry-node for diagnostics, not to resume the workflow.",
266
249
  "Call shapes (JSON): " +
267
250
  "- run: {\"action\":\"run\",\"name\":\"<script>\",\"args\":{...},\"tokens\":N,\"time\":N}. " +
268
251
  "- status: {\"action\":\"status\"}. " +
269
- "- pause/resume/abort: {\"action\":\"pause\",\"runId\":\"<id>\"} (abort optional: ,\"error\":\"<reason>\"}). " +
270
- "- retry-node/skip-node: {\"action\":\"retry-node\",\"runId\":\"<id>\",\"callId\":N}.",
252
+ "- pause/resume/abort: {\"action\":\"pause\",\"runId\":\"<id>\"} (abort optional: ,\"error\":\"<reason>\"}).",
271
253
  "Anti-patterns: Flattening args sub-fields (task/items/...) to the top level — they belong inside args. Calling {\"action\":\"run\"} without name.",
272
254
  ],
273
255
  parameters: WorkflowParams,
@@ -308,12 +290,6 @@ export function registerWorkflowTool(
308
290
  case "abort":
309
291
  result = await actionLifecycle("abort", params, deps);
310
292
  break;
311
- case "retry-node":
312
- result = await actionRetryNode(params, deps);
313
- break;
314
- case "skip-node":
315
- result = await actionSkipNode(params, deps);
316
- break;
317
293
  default: {
318
294
  // Exhaustiveness check — 新增 WorkflowAction 成员时未补 case,tsc 在此报错。
319
295
  const _exhaustive: never = action;
@@ -503,62 +479,6 @@ async function actionLifecycle(
503
479
  }
504
480
  }
505
481
 
506
- // ── retry-node / skip-node ───────────────────────────────────
507
-
508
- async function actionRetryNode(params: WorkflowToolParams, deps: LauncherDeps): Promise<ToolResult> {
509
- const runId = params.runId;
510
- const callId = params.callId;
511
- if (!runId || callId === undefined) {
512
- return textResult("retry-node requires 'runId' and 'callId'. Correct: {\"action\":\"retry-node\",\"runId\":\"<id>\",\"callId\":<number>}", true);
513
- }
514
- const run = deps.runs.get(runId);
515
- if (!run) {
516
- return textResult(
517
- `Workflow '${runId}' not found. Use action:status to list active runs and their runIds.`,
518
- true,
519
- );
520
- }
521
- try {
522
- await retryNode(run, callId, deps);
523
- return {
524
- content: [
525
- { type: "text", text: `Retried call ${callId} in run ${runId.slice(0, RUNID_SHORT)}.` },
526
- ],
527
- details: { action: "retry-node", runId, callId },
528
- };
529
- } catch (err) {
530
- const msg = err instanceof Error ? err.message : String(err);
531
- return textResult(`Error: ${msg}`, true);
532
- }
533
- }
534
-
535
- async function actionSkipNode(params: WorkflowToolParams, deps: LauncherDeps): Promise<ToolResult> {
536
- const runId = params.runId;
537
- const callId = params.callId;
538
- if (!runId || callId === undefined) {
539
- return textResult("skip-node requires 'runId' and 'callId'. Correct: {\"action\":\"skip-node\",\"runId\":\"<id>\",\"callId\":<number>}", true);
540
- }
541
- const run = deps.runs.get(runId);
542
- if (!run) {
543
- return textResult(
544
- `Workflow '${runId}' not found. Use action:status to list active runs and their runIds.`,
545
- true,
546
- );
547
- }
548
- try {
549
- await skipNode(run, callId, deps);
550
- return {
551
- content: [
552
- { type: "text", text: `Skipped call ${callId} in run ${runId.slice(0, RUNID_SHORT)}.` },
553
- ],
554
- details: { action: "skip-node", runId, callId },
555
- };
556
- } catch (err) {
557
- const msg = err instanceof Error ? err.message : String(err);
558
- return textResult(`Error: ${msg}`, true);
559
- }
560
- }
561
-
562
482
  // ── helpers ──────────────────────────────────────────────────
563
483
 
564
484
  /** WorkflowRun → 摘要(status action 用)。 */
@@ -492,8 +492,8 @@ function postAgentResult(
492
492
  * 更新 spent()/remaining())。每次 agent 调用消费 usage 后发送,保持 worker 内 $BUDGET
493
493
  * 与主线程 Budget 值对象同步。
494
494
  *
495
- * D-12 regression fix (round-2 #1):重建 budget-update 发送方。被 error-recovery(dispatch
496
- * 后)和 node-opsretry/skip 后)共用——单一实现,避免消息形状漂移。
495
+ * D-12 regression fix (round-2 #1):重建 budget-update 发送方。被 error-recovery 主路径调用
496
+ * (dispatch 后同步 worker $BUDGET)——单一实现,避免消息形状漂移。
497
497
  */
498
498
  export function postBudgetUpdate(run: WorkflowRun): void {
499
499
  run.runtime?.worker.postMessage({
@@ -5,7 +5,7 @@
5
5
  * 是真需要 mock 测试的依赖(子进程/文件系统/线程)。
6
6
  *
7
7
  * 编排层共享类型(WorkerHandlers / LifecycleDeps)——打破 lifecycle ↔
8
- * error-recovery ↔ node-ops 循环依赖:3 个 engine 函数文件各自独立,共用同一组
8
+ * error-recovery 循环依赖:2 个 engine 函数文件各自独立,共用同一组
9
9
  * 依赖签名(D-12)。
10
10
  *
11
11
  * 层归属:Engine。零 infra 依赖(AC-1)。
@@ -72,7 +72,7 @@ export interface WorkerHost {
72
72
 
73
73
  /**
74
74
  * Worker 线程事件回调集合——WorkerHost.start 的入参,由 lifecycle
75
- * 构造并注入。3 个 engine 文件(lifecycle / error-recovery / node-ops)共用此签名,
75
+ * 构造并注入。2 个 engine 文件(lifecycle / error-recovery)共用此签名,
76
76
  * 避免各自定义形状不一致的 handler bag(打破循环依赖)。
77
77
  *
78
78
  * 所有回调返回 Promise——允许 engine 层在回调内做 await persistState 等异步操作。
@@ -89,7 +89,7 @@ export interface WorkerHandlers {
89
89
  // ── 编排层共享类型 2: LifecycleDeps ────────────────────────────
90
90
 
91
91
  /**
92
- * lifecycle / error-recovery / node-ops 3 个 engine 函数文件的共同依赖 bag。
92
+ * lifecycle / error-recovery 2 个 engine 函数文件的共同依赖 bag。
93
93
  *
94
94
  * 取代旧 4 个 Context factory(errorHandlerContext / agentCallContext /
95
95
  * budgetCallbacks / 旧 terminate bag,AC-2 目标)。函数签名 `(deps: LifecycleDeps, ...)`
@@ -23,7 +23,7 @@
23
23
  * (runtime=undefined)。AbortController 一次性无法复用。
24
24
  * - resume 走 assignRuntime(new RunRuntime(...)),重建 worker/gate/controller。
25
25
  *
26
- * retryNode / worker-error-retry(G5-001 + G6-001):
26
+ * worker-error-retry(G5-001 + G6-001):
27
27
  * - replaceRuntime(newRt): 前置 status==="running"(G6-001),原子释放前一个 runtime
28
28
  * + 绑定新 runtime,全程保持不变式 I1(中间不经过 runtime===undefined 的可见状态)。
29
29
  * - paused 状态下 retry 被拒(要 retry 先 resume)。
@@ -243,11 +243,11 @@ export class WorkflowRun {
243
243
  this.runtime.release("pause");
244
244
  this.runtime = undefined;
245
245
  // 不改 status——调用方(transition)负责。独立调用时调用方需自行确保
246
- // status 一致(如 retryNode 用 replaceRuntime 而非 release+assign)。
246
+ // status 一致(如 worker-error-retry 用 replaceRuntime 而非 release+assign)。
247
247
  }
248
248
 
249
249
  /**
250
- * 原地替换 runtime(G5-001:retryNode / worker-error-retry)。
250
+ * 原地替换 runtime(G5-001:worker-error-retry)。
251
251
  *
252
252
  * 前置:status==="running"(G6-001:paused 下拒绝,要 retry 先 resume)。
253
253
  * 原子地:释放旧 runtime(worker.terminate + abort)+ 绑定新 runtime,
@@ -116,7 +116,7 @@ export function buildWorkerScript(userScript: string): string {
116
116
  ' // 把单点失败放大成整批崩溃。改为始终 resolve(错误时回退到 content 文本),',
117
117
  ' // 让 parallel() 下的脚本容错循环(parseResult → null → skip)自然接管。',
118
118
  ' // 错误原因已由主线程 executeAgentCall → trace.update(result.error) 保留在 trace/TUI,',
119
- ' // 不丢失。skipNode 的 SKIP_PLACEHOLDER(无 error,resolve 为 "")已确立此先例。',
119
+ ' // 不丢失。失败 resolve 为空字符串是既定容错策略。',
120
120
  ' // parsedOutput: validated data object from structured-output execute().',
121
121
  ' // Fallback to content (raw text) when no schema was requested or on error.',
122
122
  ' pending.resolve(msg.result.parsedOutput ?? msg.result.content);',
@@ -1,194 +0,0 @@
1
- /**
2
- * Workflow Extension — node-ops
3
- *
4
- * 单节点操作 free functions(D-12)。
5
- *
6
- * 2 个导出函数:
7
- * - retryNode(run, callId, deps) — 重置 call + 主线程重跑(不 replaceRuntime)
8
- * - skipNode(run, callId, deps) — 标记 call done + 占位 result
9
- *
10
- * **D.5(方案 A)**:retryNode 的语义是「重试单个失败 call」——只重置 call 状态 +
11
- * 主线程直接调 executeAgentCall,worker 不重启,已完成调用不受影响(worker 重启是
12
- * worker-error-retry handleWorkerError 的语义,不在本职责内)。
13
- *
14
- * **retryNode 不影响脚本流程**:worker 在首次失败结果被 postAgentResult 投递后即
15
- * resolve 并删除该 callId 的 pending Promise(worker-script-builder agent-result 分支)。
16
- * retryNode 的二次 postMessage 因此被 worker 丢弃——新结果只更新 trace/TUI,
17
- * 回不到脚本(脚本早已带着首次结果往下走)。这是 D.5「不重启 worker」的直接后果:
18
- * 要让新结果回到脚本必须重启 worker 重跑整个脚本(旧 orchestrator.ts 语义),
19
- * 与「不干扰已完成调用」的设计意图冲突。故 retryNode 定位为「失败节点的诊断性重跑
20
- * + trace 刷新」,不承诺改变脚本输出。tool-workflow 的描述已如实声明此语义。
21
- *
22
- * **G6-001**:retryNode 前置 status==="running"(paused 下拒绝,要 retry 先 resume)。
23
- *
24
- * 层归属:Engine。依赖 LifecycleDeps + WorkflowRun + executeAgentCall。
25
- *
26
- * 参考:domain-models.md §失败处理矩阵(retryNode 语义)、clarification.md D.5/G6-001。
27
- */
28
-
29
- import { postBudgetUpdate } from "./error-recovery.ts";
30
- import { executeAgentCall } from "./execute-agent-call.ts";
31
- import type { LifecycleDeps } from "./models/ports.ts";
32
- import type { AgentResult } from "./models/types.ts";
33
- import type { WorkflowRun } from "./models/workflow-run.ts";
34
-
35
- // ── skipNode 占位结果 ────────────────────────────────────────
36
-
37
- /** skipNode 注入的占位结果(零 usage,避免污染 budget)。 */
38
- const SKIP_PLACEHOLDER: AgentResult = {
39
- content: "",
40
- usage: {
41
- input: 0,
42
- output: 0,
43
- cacheRead: 0,
44
- cacheWrite: 0,
45
- cost: 0,
46
- contextTokens: 0,
47
- turns: 0,
48
- },
49
- };
50
-
51
- // ── retryNode ────────────────────────────────────────────────
52
-
53
- /**
54
- * 重试单个失败 agent call(诊断性重跑 + trace 刷新,不影响脚本流程)。
55
- *
56
- * **D.5 修复**:不 replaceRuntime、不重启 worker。只重置 call 状态(status=pending,
57
- * attempts=0, result=undefined)+ 同步 trace 节点 + 主线程直接调 executeAgentCall。
58
- * worker 仍在运行,已完成调用不受影响。
59
- *
60
- * **结果不回到脚本**:见文件头说明——worker 在首次失败结果投递后已 resolve 并删除
61
- * 该 callId 的 pending Promise,本函数末尾的 postMessage 通常被 worker 丢弃。新结果
62
- * 只反映在 trace/TUI,不改变脚本输出(若需让脚本拿新结果,须重启 worker 重跑整个
63
- * 脚本,与 D.5 冲突,未采用)。
64
- *
65
- * 与 worker-error-retry的区别:
66
- * - handleWorkerError:worker 本身崩溃 → replaceRuntime 重启整个 worker
67
- * - retryNode:单个 call 失败 → 主线程重跑该 call,worker 不动
68
- *
69
- * **G6-001**:前置 status==="running"。paused 下抛错(要 retry 先 resume)。
70
- *
71
- * @param run WorkflowRun 聚合根
72
- * @param callId 要重试的 call id(必须已存在于 run.state.calls)
73
- * @param deps LifecycleDeps(runner 用于重跑 call)
74
- * @throws run.state.status !== "running"(G6-001)
75
- * @throws callId 不存在
76
- */
77
- export async function retryNode(
78
- run: WorkflowRun,
79
- callId: number,
80
- deps: LifecycleDeps,
81
- ): Promise<void> {
82
- // G6-001:前置 status==="running"
83
- if (run.state.status !== "running") {
84
- throw new Error(
85
- `retryNode: requires status==="running" (current: ${run.state.status}, runId=${run.runId})`,
86
- );
87
- }
88
-
89
- const call = run.state.calls.get(callId);
90
- if (!call) {
91
- throw new Error(`retryNode: call ${callId} not found in run ${run.runId}`);
92
- }
93
-
94
- // 重置 call 状态:done → pending(绕过 AgentCall 状态机守卫,因为是显式 reset 语义)
95
- call.status = "pending";
96
- call.attempts = 0;
97
- call.result = undefined;
98
- call.sessionId = undefined;
99
- call.sessionFile = undefined;
100
-
101
- // 同步 trace 节点:回退到 pending
102
- run.state.trace.update(callId, {
103
- status: "pending",
104
- result: undefined,
105
- error: undefined,
106
- completedAt: undefined,
107
- sessionId: undefined,
108
- sessionFile: undefined,
109
- });
110
-
111
- // 主线程重跑(不重启 worker)——executeAgentCall 内部 markRunning + runner.run
112
- // G6-001 保证 status==="running" ⟺ runtime defined;retryNode 已守 status==="running"
113
- // 前置,故 run.runtime 必存在。非空断言,不再用 fallback 掩盖不变式违反。
114
- const signal = run.runtime!.controller.signal;
115
- await executeAgentCall(call, deps.runner, run.state.budget, signal, run.state.trace);
116
-
117
- // 回发结果给 worker(best-effort:worker 通常已在首次失败结果投递后 resolve 并删除
118
- // 该 callId 的 pending Promise,故本 postMessage 多被丢弃——见文件头 D.5 说明)。
119
- // 保留是为覆盖「executeAgentCall 已完成但 dispatchAgentCall.then 尚未投递结果」的
120
- // 极窄竞态窗口,以及与 skipNode 的回发路径对称。结果无论如何都已写入 trace/TUI。
121
- if (call.result) {
122
- run.runtime?.worker.postMessage({
123
- type: "agent-result",
124
- callId,
125
- result: call.result,
126
- cached: false,
127
- });
128
- }
129
-
130
- // D-12 regression fix (round-2 #1):retry 重跑消费 usage 后同步 worker $BUDGET
131
- postBudgetUpdate(run);
132
-
133
- await deps.store.save(run);
134
- }
135
-
136
- // ── skipNode ─────────────────────────────────────────────────
137
-
138
- /**
139
- * 跳过单个 agent call(注入占位 result)。
140
- *
141
- * 标记 call.status="done" + 写入 SKIP_PLACEHOLDER result + 同步 trace 节点为 completed。
142
- * 若 worker 仍活着,立即回发 agent-result(解锁 worker pending await)。
143
- *
144
- * 与 retryNode 的区别:skipNode 不重跑——直接用占位结果「假装完成」。
145
- * 用于用户显式跳过失败节点继续执行的场景。
146
- *
147
- * 不要求 status==="running"——paused 下也可 skip(标记后 resume 时该 call 走 callCache
148
- * replay)。但若 worker 已 terminate(runtime undefined),只标记不回发。
149
- *
150
- * @param run WorkflowRun 聚合根
151
- * @param callId 要跳过的 call id(若不存在,仅注入到 calls Map 占位)
152
- * @param deps LifecycleDeps(store 持久化)
153
- */
154
- export async function skipNode(
155
- run: WorkflowRun,
156
- callId: number,
157
- deps: LifecycleDeps,
158
- ): Promise<void> {
159
- const call = run.state.calls.get(callId);
160
-
161
- if (call) {
162
- // 已有 call:标记 done + 占位 result(绕过状态机守卫,显式 skip 语义)
163
- call.status = "done";
164
- call.result = SKIP_PLACEHOLDER;
165
- }
166
-
167
- // 同步 trace 节点
168
- run.state.trace.update(callId, {
169
- status: "completed",
170
- result: SKIP_PLACEHOLDER,
171
- completedAt: new Date().toISOString(),
172
- });
173
-
174
- // 若 worker 仍活着,回发 agent-result(解锁 worker pending await)
175
- if (run.runtime) {
176
- try {
177
- run.runtime.worker.postMessage({
178
- type: "agent-result",
179
- callId,
180
- result: SKIP_PLACEHOLDER,
181
- cached: true,
182
- });
183
- } catch (err) {
184
- // P1-8: worker 可能在 has 与 postMessage 间 exit——预期竞态,不恢复
185
- void err;
186
- }
187
- }
188
-
189
- // D-12 regression fix (round-2 #1):skip 后同步 worker $BUDGET(占位 result 零 usage,
190
- // 值不变,但保持 $BUDGET 与主线程一致)
191
- postBudgetUpdate(run);
192
-
193
- await deps.store.save(run);
194
- }