@zhushanwen/pi-subagent-workflow 0.3.3 → 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.
@@ -31,23 +31,6 @@ import { type RenderContext,renderSubagentCall, renderSubagentResult } from "./t
31
31
  * 无法从 SubagentParams schema 反向推断参数类型)。
32
32
  * action 与对应 param 不匹配时 handler 内 throw。
33
33
  */
34
- interface StartParam {
35
- task: string;
36
- /** 短标签(≤35 字符,kebab-case),必填。展示在 TUI 标题行/列表。 */
37
- slug: string;
38
- agent?: string;
39
- model?: string;
40
- thinkingLevel?: string;
41
- skillPath?: string;
42
- appendSystemPrompt?: string[];
43
- schema?: Record<string, unknown>;
44
- maxTurns?: number;
45
- graceTurns?: number;
46
- fork?: boolean;
47
- worktree?: boolean;
48
- cwd?: string;
49
- }
50
-
51
34
  interface ListParam {
52
35
  includeFinished?: boolean;
53
36
  limit?: number;
@@ -59,7 +42,22 @@ interface CancelParam {
59
42
 
60
43
  interface SubagentExecuteParams {
61
44
  action: "start" | "list" | "cancel";
62
- startParam?: StartParam;
45
+ // action:"start" 的 13 字段拍平到顶层(弱模型常省略 startParam 嵌套层导致调用失败)。
46
+ // 拍平后这些字段直接在顶层(全部 optional——schema flat 无法表达「action 条件必填」,
47
+ // 由 startHandler runtime 校验 task/slug 必填)。
48
+ task?: string;
49
+ slug?: string;
50
+ agent?: string;
51
+ model?: string;
52
+ thinkingLevel?: string;
53
+ skillPath?: string;
54
+ appendSystemPrompt?: string[];
55
+ schema?: Record<string, unknown>;
56
+ maxTurns?: number;
57
+ graceTurns?: number;
58
+ fork?: boolean;
59
+ worktree?: boolean;
60
+ cwd?: string;
63
61
  listParam?: ListParam;
64
62
  cancelParam?: CancelParam;
65
63
  }
@@ -89,54 +87,56 @@ type SubagentRenderResultCb = (
89
87
 
90
88
  // Params schema(模块内消费,未导出)。
91
89
  //
92
- // TODO(long-term, option-A): startParam/listParam/cancelParam 全标 Optional 是 flat
93
- // JSON Schema 表达「action 分发的条件必填」的妥协——required[] 只能表达静态必填,
94
- // 无法表达「action:"start" startParam 必填、action:"list" 时不需要」。长期方案是
95
- // 拆成 3 个独立 tool(subagent_start / subagent_list / subagent_cancel),让每个 tool
96
- // schema 真实反映必填性,消除全新上下文下的字段误判。当前靠 description 强标记 +
97
- // runtime guard(subagent-actions.ts startHandler/cancelHandler throw)兜底。
98
- // 勿在此基础上继续堆 action 条件逻辑——要加就拆 tool。
90
+ // action:"start" 的 13 字段(task/slug/agent/model/...)拍平在顶层,不再用 startParam
91
+ // 嵌套容器包。原因:弱模型(GLM/DeepSeek)信任 schema 结构信号 > 文本信号,经常省略
92
+ // startParam 嵌套层把 task/slug 直接平铺到顶层导致调用失败。拍平后 schema 结构与模型
93
+ // 的自然倾向一致,消除这层误用。task/slug 必填性由 startHandler runtime 校验(flat
94
+ // JSON Schema 无法表达「action 条件必填」)。
95
+ //
96
+ // TODO(long-term, option-A): listParam/cancelParam 仍标 Optional 也是 flat JSON Schema
97
+ // 表达「action 分发条件必填」的妥协——长期方案是拆成 3 个独立 tool
98
+ // (subagent_start / subagent_list / subagent_cancel),让每个 tool 的 schema 真实
99
+ // 反映必填性。勿在此基础上继续堆 action 条件逻辑——要加就拆 tool。
99
100
  const SubagentParams = Type.Object({
100
101
  action: StringEnum(["start", "list", "cancel"], {
101
102
  description: "Operation: 'start' runs a subagent, 'list' shows running subagents (optional includeFinished), 'cancel' stops a background subagent by id.",
102
103
  }),
103
- // action:"start" startParam REQUIRED. Missing/empty task or slug throws at runtime.
104
+ // ── action:"start" fields (flattened to top level). task/slug REQUIRED for start. ──
105
+ // Missing/empty task or slug throws at runtime (startHandler).
104
106
  // (flat JSON Schema can't express conditional requirement — see file-level TODO.)
105
- startParam: Type.Optional(Type.Object({
106
- task: Type.String({
107
- description: "REQUIRED for action:'start'. The task for the subagent to execute. Throws if missing or whitespace-only.",
108
- }),
109
- slug: Type.String({
110
- description:
111
- "REQUIRED for action:'start'. Short label (≤35 chars) for this subagent, e.g. 'fix-login', 'extract-urls'. " +
112
- "Shown in TUI to distinguish concurrent subagents.",
113
- maxLength: SLUG_MAX_LENGTH,
114
- }),
115
- agent: Type.Optional(Type.String({
116
- description: 'Agent name (system prompt + tools). If omitted, defaults to "general-purpose" — a generic agent that inherits the main agent\'s model and project context. Available: general-purpose (default fallback), worker, researcher, explorer, planner, reviewer, oracle, context-builder. Custom agents configurable.',
117
- })),
118
- model: Type.Optional(Type.String({
119
- description: 'Model override in "provider/modelId" format. Resolution order (top wins): (1) this param, (2) agent .md frontmatter model, (3) the main agent\'s current model (zero-config default). An explicit model (param or frontmatter) that is missing or unauthorized THROWS — there is no silent fallback to the main model. Omit this param to inherit the main model.',
120
- })),
121
- thinkingLevel: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high", "xhigh"] as const)),
122
- skillPath: Type.Optional(Type.String()),
123
- appendSystemPrompt: Type.Optional(Type.Array(Type.String())),
124
- schema: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
125
- maxTurns: Type.Optional(Type.Number({
126
- description: "Turn limit. The subagent is terminated via SIGTERM after maxTurns turn_end events + graceTurns of slack. There is no graceful wrap-up message — the process is killed. 0 or omitted = unlimited.",
127
- })),
128
- graceTurns: Type.Optional(Type.Number({
129
- description: "Extra turns allowed after maxTurns is reached before SIGTERM (default 2). Only meaningful when maxTurns is set.",
130
- })),
131
- fork: Type.Optional(Type.Boolean({
132
- 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. Use worktree:true (requires fork:true) for file-system isolation.",
133
- })),
134
- worktree: Type.Optional(Type.Boolean({
135
- description: "Worktree isolation (requires fork:true): run the subagent in a dedicated git worktree, providing file-system level isolation from the parent session. Prevents concurrent file-write conflicts between parent and subagent. Only takes effect when fork:true; passing worktree:true without fork:true throws an error.",
136
- })),
137
- cwd: Type.Optional(Type.String({
138
- description: 'Override the working directory for the subagent execution. Must be an absolute path. Defaults to the parent session\'s cwd.',
139
- })),
107
+ task: Type.Optional(Type.String({
108
+ description: "REQUIRED for action:'start'. The task for the subagent to execute. Throws if missing or whitespace-only.",
109
+ })),
110
+ slug: Type.Optional(Type.String({
111
+ description:
112
+ "REQUIRED for action:'start'. Short label (≤35 chars) for this subagent, e.g. 'fix-login', 'extract-urls'. " +
113
+ "Shown in TUI to distinguish concurrent subagents.",
114
+ maxLength: SLUG_MAX_LENGTH,
115
+ })),
116
+ agent: Type.Optional(Type.String({
117
+ description: 'Agent name (system prompt + tools). If omitted, defaults to "general-purpose" — a generic agent that inherits the main agent\'s model and project context. Available: general-purpose (default fallback), worker, researcher, explorer, planner, reviewer, oracle, context-builder, orchestrator. Custom agents configurable.',
118
+ })),
119
+ model: Type.Optional(Type.String({
120
+ description: 'Model override in "provider/modelId" format. Resolution order (top wins): (1) this param, (2) agent .md frontmatter model, (3) the main agent\'s current model (zero-config default). An explicit model (param or frontmatter) that is missing or unauthorized THROWS — there is no silent fallback to the main model. Omit this param to inherit the main model.',
121
+ })),
122
+ thinkingLevel: Type.Optional(StringEnum(["off", "minimal", "low", "medium", "high", "xhigh"] as const)),
123
+ skillPath: Type.Optional(Type.String()),
124
+ appendSystemPrompt: Type.Optional(Type.Array(Type.String())),
125
+ schema: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
126
+ maxTurns: Type.Optional(Type.Number({
127
+ description: "Turn limit. The subagent is terminated via SIGTERM after maxTurns turn_end events + graceTurns of slack. There is no graceful wrap-up message — the process is killed. 0 or omitted = unlimited.",
128
+ })),
129
+ graceTurns: Type.Optional(Type.Number({
130
+ description: "Extra turns allowed after maxTurns is reached before SIGTERM (default 2). Only meaningful when maxTurns is set.",
131
+ })),
132
+ fork: Type.Optional(Type.Boolean({
133
+ 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. Use worktree:true (requires fork:true) for file-system isolation.",
134
+ })),
135
+ worktree: Type.Optional(Type.Boolean({
136
+ description: "Worktree isolation (requires fork:true): run the subagent in a dedicated git worktree, providing file-system level isolation from the parent session. Prevents concurrent file-write conflicts between parent and subagent. Only takes effect when fork:true; passing worktree:true without fork:true throws an error.",
137
+ })),
138
+ cwd: Type.Optional(Type.String({
139
+ description: 'Override the working directory for the subagent execution. Must be an absolute path. Defaults to the parent session\'s cwd.',
140
140
  })),
141
141
  // action:"list" → listParam OPTIONAL (all fields optional, defaults apply). Ignored by other actions.
142
142
  listParam: Type.Optional(Type.Object({
@@ -171,22 +171,8 @@ function isModelOverrideObj(a: unknown): a is { model?: unknown; thinkingLevel?:
171
171
  return typeof a === "object" && a !== null;
172
172
  }
173
173
 
174
- /** unknown args 是否含 startParam(类型守卫,替代 `in` 后的 `as`)。 */
175
- function hasStartParam(a: unknown): a is { startParam?: unknown } {
176
- return typeof a === "object" && a !== null && "startParam" in a;
177
- }
178
-
179
- /** action:'start' 入参是否把 task/slug 平铺到顶层(弱模型常见误用:缺 startParam 嵌套)。 */
180
- /**
181
- * action:'start' 入参是否把 task/slug 平铺到顶层(弱模型常见误用:缺 startParam 嵌套)。
182
- * export 供 behavioral 测试(trigger/no-trigger),不改变运行时行为。
183
- */
184
- export function hasFlattenedStartFields(a: unknown): boolean {
185
- if (typeof a !== "object" || a === null) return false;
186
- return "task" in a || "slug" in a;
187
- }
188
-
189
- /** 从 unknown args 安全提取 model/thinkingLevel override(传给 resolveModel)。 */
174
+ /** unknown args 安全提取 model/thinkingLevel override(传给 resolveModel)。
175
+ * 拍平后 args 已是顶层平铺结构(model/thinkingLevel 直接在 args 上)。 */
190
176
  function extractModelOverride(args: unknown): { model?: string; thinkingLevel?: string } | undefined {
191
177
  if (!isModelOverrideObj(args)) return undefined;
192
178
  const override: { model?: string; thinkingLevel?: string } = {};
@@ -214,17 +200,17 @@ Delegate when the task needs a distinct role (researcher/worker), context isolat
214
200
 
215
201
  ## Actions
216
202
 
217
- - action:"start" — run a subagent. REQUIRED startParam: { task, slug, ... } (task and slug REQUIRED). Background only: returns a subagentId immediately, notifies on completion.
203
+ - action:"start" — run a subagent. Pass task and slug as top-level fields (REQUIRED). Optional: agent, model, thinkingLevel, skillPath, appendSystemPrompt, schema, maxTurns, graceTurns, fork, worktree, cwd. Background only: returns a subagentId immediately, notifies on completion.
218
204
  - action:"list" — list subagents. Pass listParam: { includeFinished?, limit? } (all optional). Read an item's sessionFile for full detail.
219
205
  - action:"cancel" — cancel a background subagent. REQUIRED cancelParam: { subagentId }.
220
206
 
221
207
  ## Examples
222
208
 
223
209
  \`\`\`
224
- {"action":"start","startParam":{"task":"<your task>","slug":"<kebab-case>"}}
225
- {"action":"start","startParam":{"task":"...","slug":"fix-login","agent":"worker","model":"anthropic/claude-3.5-sonnet","fork":true}}
210
+ {"action":"start","task":"<your task>","slug":"<kebab-case>"}
211
+ {"action":"start","task":"...","slug":"fix-login","agent":"worker","model":"anthropic/claude-3.5-sonnet","fork":true}
226
212
  {"action":"list","listParam":{"includeFinished":false,"limit":20}}
227
- {"action":"cancel","cancelParam":{"subagentId":"sa_abc123"}}
213
+ {"action":"cancel","cancelParam":{"subagentId":"sa-550e8400"}}
228
214
  \`\`\`
229
215
 
230
216
  ## After launching — do NOT wait
@@ -237,7 +223,8 @@ Completion auto-notifies you (steer wakes next turn, even mid-poll). So:
237
223
 
238
224
  ## Anti-patterns
239
225
 
240
- - Putting task/slug at the top level instead of inside startParamthe tool reads startParam.task, not a top-level task.
226
+ - Forgetting the REQUIRED top-level task/slug fields for action:"start"both must be present at the top level (not nested).
227
+ - Over-generalizing the flatten: ONLY start fields are top-level. list and cancel params stay nested under listParam / cancelParam (e.g. {"action":"list","listParam":{"includeFinished":true}}, NOT {"action":"list","includeFinished":true}).
241
228
  - Launching background, then sleeping/polling instead of working or stopping.
242
229
  - Treating subagent results as authoritative without verification.
243
230
  - Delegating trivial tasks you could do faster yourself.
@@ -278,9 +265,10 @@ const subagentRenderCall: SubagentRenderCallCb = (args, theme, ctx) => {
278
265
  // 主 agent model 由 ModelConfigService 缓存(session_start 注入,model_select 刷新),
279
266
  // 补偿 renderCall 的 ToolRenderContext 不含 model 的 SDK 限制。
280
267
  // service 未就绪 / 缓存为空 / 解析失败 → 降级不显示 model。
281
- const startParam = hasStartParam(args) ? args.startParam : undefined;
282
- const agent = extractAgentName(startParam);
283
- const override = extractModelOverride(startParam);
268
+ // 拍平后 args 已是顶层平铺结构(agent/model/thinkingLevel 直接在 args 上),
269
+ // extractAgentName / extractModelOverride 都是 unknown-safe 顶层读取,对平铺形态天然兼容。
270
+ const agent = extractAgentName(args);
271
+ const override = extractModelOverride(args);
284
272
  let resolved: { model: string; thinkingLevel?: string } | undefined;
285
273
  try {
286
274
  const service = getSubagentService();
@@ -309,7 +297,7 @@ const subagentRenderResult: SubagentRenderResultCb = (result, options, theme, ct
309
297
  * ║ service = getSubagentService() —— 未初始化 throw ║
310
298
  * ║ ║
311
299
  * ║ switch(params.action): ║
312
- * ║ "start" → startHandler(service, params.startParam, signal) → 领域对象
300
+ * ║ "start" → startHandler(service, params, signal) → 领域对象
313
301
  * ║ "list" → listHandler(service, params.listParam) → 领域对象 ║
314
302
  * ║ "cancel" → cancelHandler(service, params.cancelParam) → 领域对象║
315
303
  * ║ ║
@@ -317,6 +305,10 @@ const subagentRenderResult: SubagentRenderResultCb = (result, options, theme, ct
317
305
  * ║ return { content: [{text: JSON.stringify(result)}], details: result }║
318
306
  * ╚══════════════════════════════════════════════════════════════════╝
319
307
  *
308
+ * 拍平后 startHandler 直接接收顶层 params(13 字段已在顶层)。startHandler 的入参
309
+ * 类型 StartHandlerInput 是 SubagentExecuteParams 的子集(13 字段全 optional),
310
+ * 结构兼容——SubagentExecuteParams 多出的 action/listParam/cancelParam 被忽略。
311
+ *
320
312
  * handler 返回纯领域对象(不碰 {content, details}),adapter 唯一包装。
321
313
  * content(JSON 字符串)给 LLM,details(领域对象 + action)给 renderResult,同源。
322
314
  */
@@ -332,20 +324,11 @@ const executeSubagent: SubagentExecuteCb = async (
332
324
  const service = getSubagentService();
333
325
  if (!service) throw new Error("subagents runtime not initialized");
334
326
 
335
- // 弱模型常见误用:action:'start' 时把 task/slug 平铺到顶层(缺 startParam 嵌套层)。
336
- // schema 用 Type.Optional 表达条件必填(flat JSON Schema 无法表达),弱模型信任
337
- // 结构信号 > 文本信号,倾向省略嵌套层。这里在进 startHandler 之前拦截平铺形态,
338
- // throw 带 Correct 正例,让弱模型撞错后第二次能直接照抄。
339
- if (params.action === "start" && !params.startParam && hasFlattenedStartFields(params)) {
340
- throw new Error(
341
- "startParam is required for action:'start' — wrap task/slug inside startParam. " +
342
- "Correct: {\"action\":\"start\",\"startParam\":{\"task\":\"<your task>\",\"slug\":\"<kebab-case>\"}}",
343
- );
344
- }
345
-
346
327
  switch (params.action) {
347
328
  case "start":
348
- return adapter({ action: "start", domain: await startHandler(service, params.startParam, signal, _ctx?.model) }, toGuiCtx(_ctx));
329
+ // 拍平后直接传顶层 params(StartHandlerInput SubagentExecuteParams 子集,
330
+ // action/listParam/cancelParam 被忽略;task/slug 必填性由 startHandler 校验)。
331
+ return adapter({ action: "start", domain: await startHandler(service, params, signal, _ctx?.model) }, toGuiCtx(_ctx));
349
332
  case "list":
350
333
  return adapter({ action: "list", domain: listHandler(service, params.listParam) }, toGuiCtx(_ctx));
351
334
  case "cancel":
@@ -92,15 +92,13 @@ export function renderSubagentCall(
92
92
  resolved?: { model: string; thinkingLevel?: string },
93
93
  ): Component {
94
94
  const t = theme as ThemeLike;
95
- // args 结构:{ action:"start", startParam:{ agent, task, ... } }(见 subagent-tool.ts schema)。
96
- // startParam 提取 agent + task,对齐 nicobailon renderCall 多行布局。
97
- const startParam = typeof args === "object" && args !== null && "startParam" in args
98
- ? (args as { startParam?: unknown }).startParam
99
- : undefined;
100
- const agent = extractAgentName(startParam);
101
- // slug:从 startParam 提取(必填字段),非空时在 agent 后用 · 分隔展示。
102
- const slug = typeof startParam === "object" && startParam !== null && "slug" in startParam
103
- ? (startParam as { slug?: unknown }).slug
95
+ // args 结构(拍平后):{ action:"start", agent, task, slug, ... }(见 subagent-tool.ts schema)。
96
+ // 13 字段直接在顶层,extractAgentName / slug / task 都从 args 顶层提取,
97
+ // 对齐 nicobailon renderCall 多行布局。
98
+ const agent = extractAgentName(args);
99
+ // slug:从顶层 args 提取(必填字段),非空时在 agent 后用 · 分隔展示。
100
+ const slug = typeof args === "object" && args !== null && "slug" in args
101
+ ? (args as { slug?: unknown }).slug
104
102
  : undefined;
105
103
  const slugStr = typeof slug === "string" ? slug.trim() : "";
106
104
  const parts = slugStr
@@ -122,8 +120,8 @@ export function renderSubagentCall(
122
120
  // task preview 行——对齐 nicobailon:renderCall 输出多行(标题 + \n + task 预览)。
123
121
  // 实验假设:call 多行让首帧(无 result)与后续帧(有 result)的高度跳变模式
124
122
  // 与 nicobailon 一致,可能影响 pi diff 引擎的行对齐路径。preview 截断到 60 字符。
125
- const task = typeof startParam === "object" && startParam !== null && "task" in startParam
126
- ? (startParam as { task?: unknown }).task
123
+ const task = typeof args === "object" && args !== null && "task" in args
124
+ ? (args as { task?: unknown }).task
127
125
  : undefined;
128
126
  if (typeof task === "string" && task.length > 0) {
129
127
  // task 取首行——prompt 常含换行(多行指令),直接 slice 会保留 \n,
@@ -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);',