@zhushanwen/pi-subagent-workflow 7.4.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +15 -0
  2. package/package.json +7 -4
  3. package/scripts/rfl.mjs +308 -0
  4. package/skills/workflow-script-format/SKILL.md +1 -1
  5. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  6. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +10 -4
  7. package/src/execution/__tests__/before-agent-start-injection.test.ts +132 -0
  8. package/src/execution/__tests__/bg-notify-render.test.ts +15 -15
  9. package/src/execution/__tests__/chatmode-first-round-closure-service.test.ts +365 -0
  10. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  11. package/src/execution/__tests__/chatmode-round-notify-real-chain.test.ts +215 -0
  12. package/src/execution/__tests__/conversation-wiring.test.ts +198 -0
  13. package/src/execution/__tests__/crash-recovery.test.ts +8 -2
  14. package/src/execution/__tests__/delivery-methods.test.ts +385 -0
  15. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  16. package/src/execution/__tests__/execute-and-await-worktree.test.ts +49 -2
  17. package/src/execution/__tests__/execute-nesting.test.ts +20 -72
  18. package/src/execution/__tests__/execution-record.test.ts +199 -0
  19. package/src/execution/__tests__/finalize-record.test.ts +197 -15
  20. package/src/execution/__tests__/format.test.ts +131 -7
  21. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  22. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  23. package/src/execution/__tests__/helpers/spawn-mock.ts +37 -10
  24. package/src/execution/__tests__/index-session-start-identity.test.ts +371 -0
  25. package/src/execution/__tests__/index-session-start.test.ts +257 -5
  26. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  27. package/src/execution/__tests__/lifecycle-manager.test.ts +337 -0
  28. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  29. package/src/execution/__tests__/list-component.test.ts +59 -5
  30. package/src/execution/__tests__/list-fields.test.ts +109 -0
  31. package/src/execution/__tests__/model-resolver.test.ts +38 -1
  32. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  33. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  34. package/src/execution/__tests__/notifier-flush.test.ts +209 -7
  35. package/src/execution/__tests__/one-shot-upgrade.test.ts +205 -0
  36. package/src/execution/__tests__/parent-child-matrix.test.ts +336 -0
  37. package/src/execution/__tests__/record-store.test.ts +442 -54
  38. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +11 -12
  39. package/src/execution/__tests__/recursive-visibility-env.test.ts +18 -20
  40. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  41. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  42. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  43. package/src/execution/__tests__/run-spawn-edges.test.ts +18 -25
  44. package/src/execution/__tests__/run-spawn-integration.test.ts +29 -25
  45. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  46. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +14 -11
  47. package/src/execution/__tests__/session-pending.test.ts +61 -2
  48. package/src/execution/__tests__/session-reconstructor.test.ts +4 -4
  49. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  50. package/src/execution/__tests__/session-runner-schema-env.test.ts +15 -21
  51. package/src/execution/__tests__/session-start-reaper.test.ts +10 -8
  52. package/src/execution/__tests__/spawn-args.test.ts +127 -49
  53. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +1 -0
  54. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  55. package/src/execution/__tests__/status-refactor.test.ts +345 -0
  56. package/src/execution/__tests__/stdin-writer.test.ts +97 -0
  57. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  58. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  59. package/src/execution/__tests__/subagent-service.test.ts +49 -11
  60. package/src/execution/__tests__/timeout-integration.test.ts +27 -13
  61. package/src/execution/__tests__/tool-action.test.ts +12 -10
  62. package/src/execution/__tests__/truncline-snapshot.test.ts +81 -0
  63. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  64. package/src/execution/__tests__/worktree-manager.test.ts +300 -90
  65. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +17 -13
  66. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  67. package/src/execution/__tests__/worktree-registry.test.ts +72 -34
  68. package/src/execution/agent-result-mapper.ts +4 -1
  69. package/src/execution/argv-mirror.ts +21 -2
  70. package/src/execution/channel-registry-access.ts +3 -1
  71. package/src/execution/execution-record.ts +126 -9
  72. package/src/execution/finalize-record.ts +99 -13
  73. package/src/execution/idle-gc.ts +47 -0
  74. package/src/execution/lifecycle-manager.ts +491 -0
  75. package/src/execution/lifecycle-predicates.ts +65 -0
  76. package/src/execution/manifest-store.ts +61 -16
  77. package/src/execution/model-resolver.ts +26 -5
  78. package/src/execution/notifier.ts +69 -12
  79. package/src/execution/pi-invocation.ts +21 -1
  80. package/src/execution/record-entry.ts +118 -0
  81. package/src/execution/record-store.ts +844 -108
  82. package/src/execution/session-pending.ts +121 -49
  83. package/src/execution/session-reconstructor.ts +224 -7
  84. package/src/execution/session-runner.ts +713 -316
  85. package/src/execution/sessions-index.ts +304 -0
  86. package/src/execution/stdin-writer.ts +93 -7
  87. package/src/execution/stream-sink.ts +20 -3
  88. package/src/execution/subagent-service.ts +917 -138
  89. package/src/execution/temp-prompt.ts +8 -3
  90. package/src/execution/turn-limiter.ts +14 -0
  91. package/src/execution/types.ts +218 -19
  92. package/src/execution/worktree-manager.ts +449 -59
  93. package/src/execution/worktree-registry.ts +97 -29
  94. package/src/index.ts +318 -20
  95. package/src/injectors/subagent-list-injector.ts +26 -8
  96. package/src/injectors/workflow-list-injector.ts +25 -8
  97. package/src/interface/__tests__/subagent-tool-prompt.test.ts +17 -4
  98. package/src/interface/__tests__/tool-render.test.ts +10 -8
  99. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +103 -26
  100. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +179 -0
  101. package/src/interface/bg-notify-render.ts +32 -8
  102. package/src/interface/command-actions.ts +26 -7
  103. package/src/interface/commands.ts +21 -22
  104. package/src/interface/format.ts +53 -20
  105. package/src/interface/gui-mappers.ts +6 -8
  106. package/src/interface/helpers.ts +170 -10
  107. package/src/interface/list-component.ts +53 -14
  108. package/src/interface/subagent-actions.ts +235 -17
  109. package/src/interface/subagent-tool.ts +81 -16
  110. package/src/interface/subagents.ts +2 -1
  111. package/src/interface/tool-render.ts +21 -27
  112. package/src/interface/tool-workflow-script.ts +29 -33
  113. package/src/interface/tool-workflow.ts +67 -100
  114. package/src/interface/views/WorkflowsView.ts +89 -32
  115. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +264 -0
  116. package/src/interface/views/detail-content.ts +1 -1
  117. package/src/interface/views/format.ts +3 -3
  118. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +333 -0
  119. package/src/orchestration/__tests__/args-validator.test.ts +1 -1
  120. package/src/orchestration/__tests__/config-loader.test.ts +38 -0
  121. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +394 -4
  122. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +4 -4
  123. package/src/orchestration/__tests__/execute-agent-call.test.ts +144 -1
  124. package/src/orchestration/__tests__/jsonl-run-store-loadall-sources.test.ts +171 -0
  125. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +820 -19
  126. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +0 -2
  127. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  128. package/src/orchestration/__tests__/lifecycle.test.ts +332 -149
  129. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +1108 -19
  130. package/src/orchestration/__tests__/skill-discovery.test.ts +130 -61
  131. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  132. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  133. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +110 -0
  134. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +0 -2
  135. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  136. package/src/orchestration/__tests__/workflows-e2e.test.ts +38 -40
  137. package/src/orchestration/agent-opts-resolver.ts +4 -1
  138. package/src/orchestration/args-validator.ts +2 -2
  139. package/src/orchestration/config-loader.ts +30 -1
  140. package/src/orchestration/error-recovery.ts +137 -30
  141. package/src/orchestration/execute-agent-call.ts +44 -10
  142. package/src/orchestration/jsonl-run-store.ts +397 -71
  143. package/src/orchestration/launcher.ts +7 -1
  144. package/src/orchestration/lifecycle.ts +145 -133
  145. package/src/orchestration/models/__tests__/trace.test.ts +408 -0
  146. package/src/orchestration/models/budget.ts +1 -1
  147. package/src/orchestration/models/run-runtime.ts +15 -17
  148. package/src/orchestration/models/run-spec.ts +2 -2
  149. package/src/orchestration/models/run-state.ts +3 -3
  150. package/src/orchestration/models/trace.ts +95 -15
  151. package/src/orchestration/models/types.ts +8 -9
  152. package/src/orchestration/models/workflow-run.ts +50 -71
  153. package/src/orchestration/models/workflow-script.ts +32 -1
  154. package/src/orchestration/skill-discovery.ts +30 -0
  155. package/src/orchestration/worker-handle.ts +1 -1
  156. package/src/orchestration/worker-host.ts +1 -1
  157. package/src/orchestration/worker-script-builder.ts +38 -11
  158. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  159. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +280 -0
  160. package/src/shared/__tests__/resource-discovery.test.ts +79 -0
  161. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  162. package/src/shared/agent-ref.ts +22 -1
  163. package/src/shared/resource-discovery.ts +162 -59
  164. package/src/shared/schema-jsonify.ts +56 -0
  165. package/workflows/review-fix-loop-utils.cjs +542 -32
  166. package/workflows/review-fix-loop.js +462 -109
@@ -1,16 +1,15 @@
1
1
  /**
2
- * Workflow Extension — workflow tool(5 actions,FR-5 tool 收口)。
2
+ * Workflow Extension — workflow tool(3 actions,FR-5 tool 收口)。
3
3
  *
4
4
  * 合并原 tool-workflow.ts + tool-workflow-run.ts 为单 tool。
5
5
  *
6
6
  * Actions:
7
7
  * - run: registry.get → runWorkflow(直接启动,无需用户确认)
8
8
  * - status: 列出 runs(deps.runs)
9
- * - pause: 调 pauseRun
10
- * - resume: 调 resumeRun
11
9
  * - abort: 调 abortRun
12
10
  *
13
- * **restart 不包含**(D-9 废弃)。
11
+ * **restart 不包含**(D-9 废弃);**pause/resume 不包含**(一次性生命周期——run
12
+ * 不可挂起,提前停止用 abort,要新结果开新 run)。
14
13
  *
15
14
  * 层归属:Interface。依赖 Pi SDK + Engine lifecycle/launcher + helpers。
16
15
  *
@@ -35,8 +34,7 @@ import { type Static, Type } from "typebox";
35
34
  import { SLUG_MAX_LENGTH } from "../execution/execute-options-mapper.ts";
36
35
  import { THINKING_ORDER } from "../execution/model-resolver.ts";
37
36
  import type { LauncherDeps } from "../orchestration/launcher.ts";
38
- import { ArgsValidationError } from "../orchestration/args-validator.ts";
39
- import { abortRun, pauseRun, resumeRun, runWorkflow } from "../orchestration/lifecycle.ts";
37
+ import { abortRun, runWorkflow } from "../orchestration/lifecycle.ts";
40
38
  import type { RunStore } from "../orchestration/models/ports.ts";
41
39
  import type { WorkflowRun } from "../orchestration/models/workflow-run.ts";
42
40
  import { mapRunIcon, mapRunStatus, toGuiCtx } from "./gui-mappers.ts";
@@ -54,15 +52,11 @@ import { formatElapsed, renderTextFallback } from "./views/format.ts";
54
52
  export type WorkflowAction =
55
53
  | "run"
56
54
  | "status"
57
- | "pause"
58
- | "resume"
59
55
  | "abort";
60
56
 
61
57
  const WORKFLOW_ACTIONS: readonly WorkflowAction[] = [
62
58
  "run",
63
59
  "status",
64
- "pause",
65
- "resume",
66
60
  "abort",
67
61
  ];
68
62
 
@@ -80,7 +74,7 @@ const WorkflowParams = Type.Object({
80
74
  }),
81
75
  ),
82
76
  runId: Type.Optional(
83
- Type.String({ description: "Workflow run ID (pause/resume/abort)" }),
77
+ Type.String({ description: "Workflow run ID (abort action)" }),
84
78
  ),
85
79
  args: Type.Optional(
86
80
  Type.Record(Type.String(), Type.Unknown(), {
@@ -96,7 +90,7 @@ const WorkflowParams = Type.Object({
96
90
  description: "Run-level model override in 'provider/modelId' format. When set, all agents spawned by this run inherit it by default (unless a per-call agent() opts.model is set). Omit to inherit the main agent's model.",
97
91
  })),
98
92
  thinkingLevel: Type.Optional(StringEnum(THINKING_ORDER, {
99
- description: "Run-level thinkingLevel override (off/minimal/low/medium/high/xhigh/max). All agents in this run inherit it by default. Omit to inherit the main agent's thinking level.",
93
+ description: "Run-level thinkingLevel override (off/minimal/low/medium/high/xhigh/max). All agents in this run inherit it by default. Omit to default each agent to its model's highest available level.",
100
94
  })),
101
95
  });
102
96
 
@@ -229,7 +223,7 @@ interface RunSummary {
229
223
  export type WorkflowToolDetails =
230
224
  | { action: "run"; runId: string; status: "running" | "not_found" | "invalid_args"; name: string; slug?: string; stateFile?: string; __gui__?: GuiRenderResult }
231
225
  | { action: "status"; runs: RunSummary[]; __gui__?: GuiRenderResult }
232
- | { action: "pause" | "resume" | "abort"; runId: string; status: string; reason?: string; __gui__?: GuiRenderResult };
226
+ | { action: "abort"; runId: string; status: string; reason?: string; __gui__?: GuiRenderResult };
233
227
 
234
228
  /** Result returned by the `workflow` tool's execute. */
235
229
  export interface ToolResult {
@@ -255,8 +249,9 @@ function withGui(
255
249
  /** 按 WorkflowToolDetails 构造对应的 GuiComponent。 */
256
250
  export function buildWorkflowGui(details: WorkflowToolDetails) {
257
251
  if (details.action === "run") {
258
- // not_found 是脚本未找到的逻辑错误(isError:true),不能走通用 mapper done/check 成功映射。
259
- // 短路为 danger severity stats-line,与 isError 文案一致。
252
+ // not_found 曾是「isError:true + not_found details」的错误形态(W4 前返回值 isError 被
253
+ // pi 丢弃);W4 后该错误改为 throw(details 不再产出此形态),本分支保留消费历史
254
+ // session entry / 防御性渲染,不能走通用 mapper 的 done/check 成功映射。
260
255
  if (details.status === "not_found") {
261
256
  return guiComponent("stats-line", {
262
257
  items: [{ label: "run", value: "not found", severity: "danger" as const }],
@@ -283,14 +278,12 @@ export function buildWorkflowGui(details: WorkflowToolDetails) {
283
278
  }),
284
279
  });
285
280
  }
286
- // pause/resume/abort
287
- // abort 是破坏性终止、pause 是挂起(非成功完成),用 warn 区分;resume 保留 ok
288
- const severity = details.action === "abort" || details.action === "pause" ? "warn" as const : "ok" as const;
281
+ // abort(唯一 lifecycle action):破坏性终止非成功完成,用 warn 与成功区分
289
282
  return guiComponent("stats-line", {
290
283
  items: [{
291
284
  label: details.action,
292
285
  value: details.runId.slice(0, RUNID_SHORT),
293
- severity,
286
+ severity: "warn" as const,
294
287
  }],
295
288
  });
296
289
  }
@@ -298,7 +291,8 @@ export function buildWorkflowGui(details: WorkflowToolDetails) {
298
291
  // ── Tool registration ────────────────────────────────────────
299
292
 
300
293
  /**
301
- * 注册 workflow tool(5 actions: run / status / pause / resume / abort)。
294
+ * 注册 workflow tool(3 actions: run / status / abort;pause/resume 已随一次性
295
+ * 生命周期移除——enum 拒绝由 pi 核心校验拦截,见 F3)。
302
296
  *
303
297
  * @param pi ExtensionAPI
304
298
  * @param deps LauncherDeps(LifecycleDeps + registry)
@@ -318,9 +312,9 @@ export function registerWorkflowTool(
318
312
  name: "workflow",
319
313
  label: "Workflow",
320
314
  description:
321
- "Execute and control workflows: run (start), status, pause, resume, abort.\n" +
315
+ "Execute and control workflows: run (start), status, abort.\n" +
322
316
  "Replaces workflow + workflow-run tools.",
323
- promptSnippet: "Run, pause, resume, abort, or check workflow status",
317
+ promptSnippet: "Run, abort, or check workflow status",
324
318
  promptGuidelines: [
325
319
  "PRIORITY: When user says 'workflow', 'run workflow', try run action FIRST.",
326
320
  "All listed workflows run DIRECTLY with action:run — refs/descriptions come from " +
@@ -329,10 +323,11 @@ export function registerWorkflowTool(
329
323
  "workflow-script generate for patterns already covered by available workflows.",
330
324
  "run: pass the absolute .js path from <available_workflows> <location> as name, then start in background (no user confirmation needed).",
331
325
  "Do NOT poll status after starting — results appear automatically via notifyDone.",
326
+ "Runs are one-shot: there is no pause/resume — to stop a run early use abort; for a fresh result start a new run.",
332
327
  "Call shapes (JSON): " +
333
328
  "- run: {\"action\":\"run\",\"name\":\"<script>\",\"args\":{...},\"tokens\":N,\"time\":N,\"model\":\"<provider/modelId>\",\"thinkingLevel\":\"<level>\"}. " +
334
329
  "- status: {\"action\":\"status\"}. " +
335
- "- pause/resume/abort: {\"action\":\"pause\",\"runId\":\"<id>\"} (abort optional: ,\"error\":\"<reason>\"}).",
330
+ "- abort: {\"action\":\"abort\",\"runId\":\"<id>\"} (optional: {\"error\":\"<reason>\"}).",
336
331
  "Budget: Do NOT set tokens/time unless the user explicitly requests a limit. Built-in workflows run unlimited by default.",
337
332
  "Model/thinkingLevel: omit by default (inherit main agent's model). Only set model/thinkingLevel when the user explicitly requests a specific model or thinking depth for this run.",
338
333
  "Anti-patterns: Flattening args sub-fields (task/items/...) to the top level — they belong inside args. Calling {\"action\":\"run\"} without name.",
@@ -351,11 +346,13 @@ export function registerWorkflowTool(
351
346
  ): Promise<ToolResult> {
352
347
  // P1-2: Honor abort signal up-front
353
348
  if (signal?.aborted) {
354
- return textResult("Operation aborted before start", true);
349
+ // throw(W4b):pi 只对 execute throw 置 isError:true,返回值里的 isError
350
+ // 被 agent-loop 丢弃(agent-loop.js:453-483)——文案原样进 toolResult。
351
+ throw new Error("Operation aborted before start");
355
352
  }
356
- // P1-6: Reentry guard
353
+ // P1-6: Reentry guard(acquire 失败时尚未持有 guard,throw 前无需 release)
357
354
  if (!acquireReentryGuard(reentryRef)) {
358
- return textResult(REENTRY_BUSY_MESSAGE, true);
355
+ throw new Error(REENTRY_BUSY_MESSAGE);
359
356
  }
360
357
  try {
361
358
  let result: ToolResult;
@@ -369,19 +366,13 @@ export function registerWorkflowTool(
369
366
  case "status":
370
367
  result = actionStatus(deps);
371
368
  break;
372
- case "pause":
373
- result = await actionLifecycle("pause", params, deps);
374
- break;
375
- case "resume":
376
- result = await actionLifecycle("resume", params, deps);
377
- break;
378
369
  case "abort":
379
370
  result = await actionLifecycle("abort", params, deps);
380
371
  break;
381
372
  default: {
382
373
  // Exhaustiveness check — 新增 WorkflowAction 成员时未补 case,tsc 在此报错。
383
374
  const _exhaustive: never = action;
384
- return textResult(`Unknown action: ${String(_exhaustive)}`, true);
375
+ throw new Error(`Unknown action: ${String(_exhaustive)}`);
385
376
  }
386
377
  }
387
378
  // GUI 协议:RPC 模式下附加 __gui__ 到 details
@@ -428,30 +419,27 @@ export async function actionRun(
428
419
  ): Promise<ToolResult> {
429
420
  const name = params.name;
430
421
  if (!name) {
431
- return textResult("run requires 'name' parameter (absolute .js path from <available_workflows> <location>). Correct: {\"action\":\"run\",\"name\":\"<ref>\",\"args\":{...}}", true);
422
+ throw new Error("run requires 'name' parameter (absolute .js path from <available_workflows> <location>). Correct: {\"action\":\"run\",\"name\":\"<ref>\",\"args\":{...}}");
432
423
  }
433
424
  // 弱模型常见误用(P0 静默失败):把 task/items 等 args 子字段平铺到 workflow params
434
425
  // 顶层(缺 args 嵌套)。args ?? {} 会静默 args={},启动缺参 run 不报错——比 subagent
435
426
  // 平铺事故更严重。m6:先 registry.getPath(动态参数集来源——schema 即 SSOT),
436
427
  // not_found 优先返回;平铺检测报错带 Correct 正例纠正。
437
428
  const script = await deps.registry.getPath(name);
438
- if (!script) {
439
- // 模糊匹配建议
429
+ // W4c:config-loader 的 toCachedMeta 对不可读/不存在文件返回 available:false 的
430
+ // stub(非 undefined),仅判 !script 会绕过 not_found → 空 sourceCode 假启动
431
+ //(W4b verifier 探针实测复现)。与下方 suggestions 分支的 wf.available 过滤口径对齐。
432
+ if (!script || !script.available) {
433
+ // 模糊匹配建议。throw(W4):pi 只对 execute throw 置 isError:true,
434
+ // 返回值里的 isError 被 agent-loop 丢弃(agent-loop.js:453-483)——文案原样进 toolResult。
440
435
  const all = await deps.registry.loadAll();
441
436
  const available = all.filter((wf) => wf.available);
442
437
  const suggestions = available
443
438
  .map((wf) => ` - ${wf.name}: ${wf.meta.description || "(no description)"}`)
444
439
  .join("\n");
445
- return {
446
- content: [
447
- {
448
- type: "text",
449
- text: `Workflow '${name}' not found. Available:\n${suggestions || " (none)"}\nUse <location> from <available_workflows> for the absolute .js path.`,
450
- },
451
- ],
452
- details: { action: "run", runId: "", status: "not_found", name },
453
- isError: true,
454
- };
440
+ throw new Error(
441
+ `Workflow '${name}' not found. Available:\n${suggestions || " (none)"}\nUse <location> from <available_workflows> for the absolute .js path.`,
442
+ );
455
443
  }
456
444
 
457
445
  // m6:动态参数集(schema 即 SSOT)→ 平铺检测;无 parameters → 单次 warn + 跳过
@@ -466,17 +454,15 @@ export async function actionRun(
466
454
  }
467
455
  const flattened = findFlattenedArgKeys(params, knownKeys, knownPatterns);
468
456
  if (flattened.length > 0) {
469
- return textResult(
457
+ throw new Error(
470
458
  `Detected ${flattened.join(", ")} at top level — they belong inside 'args'. ` +
471
459
  `Correct: {"action":"run","name":"${name}","args":{${flattened.map((k) => `"${k}": "<value>"`).join(", ")}}}`,
472
- true,
473
460
  );
474
461
  }
475
462
  // slug 运行时护栏(与 subagent startHandler 对称的纵深防御;schema maxLength 是第一道关卡)
476
463
  if (params.slug !== undefined && params.slug.length > SLUG_MAX_LENGTH) {
477
- return textResult(
464
+ throw new Error(
478
465
  `slug exceeds ${SLUG_MAX_LENGTH} chars (got ${params.slug.length}). Shorten to a kebab-case label, e.g. "fix-login", "extract-urls".`,
479
- true,
480
466
  );
481
467
  }
482
468
  const args = params.args ?? {};
@@ -484,36 +470,25 @@ export async function actionRun(
484
470
  const time = params.time;
485
471
 
486
472
  // 构建 RunSpec + 启动(m3:parameters 从 script.meta 拷贝——chokepoint 校验用;
487
- // 校验失败 → isError ToolResult §5.3 指引,非 ArgsValidationError 保持传播)
488
- let runId: string;
489
- try {
490
- runId = await runWorkflow(
491
- {
492
- scriptSource: script.toExecutable(),
493
- args,
494
- budgetTokens: tokens,
495
- budgetTimeMs: time,
496
- scriptName: script.name,
497
- slug: params.slug,
498
- scriptPath: script.path,
499
- description: script.meta.description,
500
- parameters: script.meta.parameters,
501
- model: params.model,
502
- thinkingLevel: params.thinkingLevel,
503
- },
504
- deps,
505
- signal,
506
- );
507
- } catch (err) {
508
- if (err instanceof ArgsValidationError) {
509
- return {
510
- content: [{ type: "text", text: err.message }],
511
- details: { action: "run", runId: "", status: "invalid_args", name: script.name },
512
- isError: true,
513
- };
514
- }
515
- throw err;
516
- }
473
+ // 校验失败 → ArgsValidationError 直接 throw 给 pi(W4:err.message 含 §5.3 指引,
474
+ // pi catch 后原文案进 toolResult content 并置 isError:true),其他错误保持传播)
475
+ const runId = await runWorkflow(
476
+ {
477
+ scriptSource: script.toExecutable(),
478
+ args,
479
+ budgetTokens: tokens,
480
+ budgetTimeMs: time,
481
+ scriptName: script.name,
482
+ slug: params.slug,
483
+ scriptPath: script.path,
484
+ description: script.meta.description,
485
+ parameters: script.meta.parameters,
486
+ model: params.model,
487
+ thinkingLevel: params.thinkingLevel,
488
+ },
489
+ deps,
490
+ signal,
491
+ );
517
492
 
518
493
  return {
519
494
  content: [
@@ -551,33 +526,28 @@ function actionStatus(deps: LauncherDeps): ToolResult {
551
526
  };
552
527
  }
553
528
 
554
- // ── pause/resume/abort lifecycle actions ─────────────────────
529
+ // ── abort lifecycle action ────────────────────────────────────
555
530
 
531
+ // 一次性生命周期:abort 是唯一的提前停止方式(pause/resume 已随 D-2 移除),
532
+ // action 参数保留字面量类型与 WorkflowAction 单成员分发对齐。
556
533
  async function actionLifecycle(
557
- action: "pause" | "resume" | "abort",
534
+ action: "abort",
558
535
  params: WorkflowToolParams,
559
536
  deps: LauncherDeps,
560
537
  ): Promise<ToolResult> {
561
538
  const runId = params.runId;
562
539
  if (!runId) {
563
- return textResult(`'runId' is required for ${action}. Correct: {"action":"${action}","runId":"<id>"} (use action:"status" to find runId)`, true);
540
+ throw new Error(`'runId' is required for ${action}. Correct: {"action":"${action}","runId":"<id>"} (use action:"status" to find runId)`);
564
541
  }
565
542
  const run = deps.runs.get(runId);
566
543
  if (!run) {
567
- return textResult(
544
+ throw new Error(
568
545
  `Workflow '${runId}' not found. Use action:status to list active runs and their runIds.`,
569
- true,
570
546
  );
571
547
  }
572
548
  try {
573
549
  const oldStatus = run.state.status;
574
- if (action === "pause") {
575
- await pauseRun(runId, deps);
576
- } else if (action === "resume") {
577
- await resumeRun(runId, deps);
578
- } else {
579
- await abortRun(runId, deps, params.error);
580
- }
550
+ await abortRun(runId, deps, params.error);
581
551
  const newStatus = run.state.status;
582
552
  const reasonSuffix = run.state.reason ? ` (${run.state.reason})` : "";
583
553
  return {
@@ -590,8 +560,9 @@ async function actionLifecycle(
590
560
  details: { action, runId, status: newStatus, reason: run.state.reason },
591
561
  };
592
562
  } catch (err) {
563
+ // throw(W4b):abortRun 失败改 throw(原 return isError 被 pi 丢弃),"Error: " 前缀保持
593
564
  const msg = err instanceof Error ? err.message : String(err);
594
- return textResult(`Error: ${msg}`, true);
565
+ throw new Error(`Error: ${msg}`);
595
566
  }
596
567
  }
597
568
 
@@ -612,10 +583,6 @@ function toRunSummary(run: WorkflowRun, store: RunStore): RunSummary {
612
583
  };
613
584
  }
614
585
 
615
- function textResult(text: string, isError = false): ToolResult {
616
- return {
617
- content: [{ type: "text", text }],
618
- details: undefined,
619
- isError: isError || undefined,
620
- };
621
- }
586
+ // W4b:原 textResult(text, isError) helper 已删除——23 处错误路径全部改 throw
587
+ // (pi 只对 execute throw 置 isError:true,返回值里的 isError 被 agent-loop 丢弃,
588
+ // agent-loop.js:453-483),且本文件无非错误纯文本结果用途,无残留调用方。
@@ -17,9 +17,9 @@
17
17
  *
18
18
  * 关键实现点:(a) overlay 第二参数必须传,否则 view 不全屏;(b) trace 必须 per-render
19
19
  * 重读(run.state.trace.toArray 返回内部数组引用,trace.append 后下次 render 可见),
20
- * 1s tick invalidate + requestRender 保证运行中刷新;(c) 's' save 模式无条件可用
21
- * (saveWorkflow 内部对非 tmp workflow 返回错误消息);(d) pause/resume/abort 失败时
22
- * notify 反馈,不静默吞。
20
+ * 配条件失效 tick(签名比对,IF11)+ requestRender 保证运行中刷新;(c) 's' save 模式无条件可用
21
+ * (saveWorkflow 内部对非 tmp workflow 返回错误消息);(d) abort 失败时 notify 反馈,
22
+ * 不静默吞(run 一次性生命周期——无 pause/resume,abort 是唯一提前停止方式)。
23
23
  */
24
24
 
25
25
  import { promises as fsPromises } from "node:fs";
@@ -30,12 +30,14 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
30
30
  import { Key, matchesKey } from "@earendil-works/pi-tui";
31
31
 
32
32
  import {
33
+ countAllToolCalls,
33
34
  getAllToolCalls,
34
35
  projectLiveProgress,
35
36
  } from "../../execution/execution-record.ts";
36
37
  import type { ExecutionTraceNode } from "../../orchestration/models/types.ts";
37
38
  import type { WorkflowRun } from "../../orchestration/models/workflow-run.ts";
38
39
  import { saveWorkflow } from "../../orchestration/workflow-files.ts";
40
+ import { displayAgentName } from "../../shared/agent-ref.ts";
39
41
  import {
40
42
  BOX_BORDER_CHARS,
41
43
  BUDGET_TOKENS_DIVISOR,
@@ -122,14 +124,75 @@ interface TuiLike {
122
124
 
123
125
  /**
124
126
  * view 可触发的 lifecycle 操作(由调用方注入,避免 view 直接依赖 Engine 函数)。
125
- * 每个 action 接收 runId;调用方绑到 pauseRun/resumeRun/abortRun
127
+ * 每个 action 接收 runId;调用方绑到 abortRun(一次性生命周期——仅 abort)。
126
128
  */
127
129
  export interface ViewActions {
128
- pause: (runId: string) => Promise<void>;
129
- resume: (runId: string) => Promise<void>;
130
130
  abort: (runId: string) => Promise<void>;
131
131
  }
132
132
 
133
+ // ── 渲染签名(IF11/TC7/DM6:tick 条件失效的判据,渲染动态字段 SSOT)──────
134
+
135
+ /**
136
+ * computeRenderSignature — 渲染输出中全部动态决定字段的签名(now 参数化,供测试)。
137
+ *
138
+ * 纯度说明:now 是显式参数,但 live 节点的 elapsedSeconds 经 projectLiveProgress
139
+ * 内的 computeElapsedSeconds 现算(record.endedAt 缺省时取 Date.now())——即使两次
140
+ * 调用传同一 now,跨秒的真实时钟也会使签名变。该内含实时源只朝「多失效」方向
141
+ * 偏离(多失效、不多绘、更不漏绘),对「签名同 → 跳过重绘」的单向判据安全。
142
+ *
143
+ * 用途:200ms tick 用它与上次签名比对——相同则该帧内容无可见变化,跳过清缓存 +
144
+ * requestRender(纯等待场景零重建零重绘);不同则走现状失效路径。可见行为逐帧
145
+ * 等价的前提 = 签名字段集覆盖当前渲染的全部动态字段(漏字段 = 该字段渲染滞后一拍,
146
+ * ES7;签名取原始值时粒度细于显示量化值,只多失效不多绘不漏绘)。
147
+ *
148
+ * **字段核对表**(每字段 ↔ 渲染消费点 file:line,完备性唯一证据——测试只能证
149
+ * 「已入字段变 → 签名变」,不能证无漏字段):
150
+ *
151
+ * | 签名字段 | 消费点 |
152
+ * |---|---|
153
+ * | run.state.status | WorkflowsView.ts renderHeader :652 / detail-content.ts :75(statusDotStr+statusLabel)/ renderFooter :695-699 |
154
+ * | 秒桶 Math.floor(now/1000) | renderHeader :651 formatElapsed(现算 elapsed)+ detail-content.ts :69-72(now 参与未完成节点 elapsed)|
155
+ * | completed/total(节点 status 推导)| renderHeader :649-652 |
156
+ * | budget 量化值(tokens round-k + cost toFixed(4)=BUDGET_COST_DECIMALS,与 :654 同精度——第 3-4 位小数变化是可见变化)| renderHeader :654 / saveTraceToFile :942 |
157
+ * | run.state.errorLogs 指纹 = length+末条 level:message(errorLogs 仅经 push + slice(-MAX_ERROR_LOGS) 变异——append-only + 前向淘汰、条目不可变;封顶后 length 不变内容移,单取 length 会漏失效,而任何可见变化必伴随 length 变或末条变,故 length+末条是完备且最小的指纹)| detail-content.ts :174-191(renderWorkerLogSection:total 标签 + 末 20 条)|
158
+ * | 节点 stepIndex | nodeParts 首字段(trace 数组序参与拼接,节点重排→签名变)/ saveTraceToFile :947(trace 导出 `### [#stepIndex]` 标题)|
159
+ * | 节点 sessionFile | detail-content.ts :314-317(renderSessionSection)|
160
+ * | 节点 status | 节点行 :832 statusDotStr / detail :75 |
161
+ * | live.totalTokens | 节点行 :836 / detail :79 / :275 |
162
+ * | 工具计数(签名路径用 countAllToolCalls 免克隆计数,与 getAllToolCalls(node.live).length 恒等——projectLiveProgress 投影无 toolCallCount 字段,不得引用)| 节点行 :837 / detail :80 / :223 |
163
+ * | live.elapsedSeconds | 节点行 :838 / detail :81 / :276 |
164
+ * | live.turns | detail :224 / :276 |
165
+ * | live.eventLog.length(append-only,长度变化即尾部窗口右移出新事件)| detail :222(filter turn_end)+ :231-235 |
166
+ * | live.currentActivity(type+label 均入签名)| detail :227-228 |
167
+ * | live.lastError(内容入签名,防同存在性不同文本漏绘)| detail :277-278 |
168
+ *
169
+ * 维护约定(DS8):WorkflowsView/detail-content 新增任何动态展示字段必须同步
170
+ * 并入本签名并更新上表,否则该字段渲染滞后一拍。本文件编辑会使表内 WorkflowsView
171
+ * 行号漂移——改动核对表下方代码后必须 grep 实际位置刷新本表。
172
+ */
173
+ /** 秒桶宽度(ms):签名取 Math.floor(now / SECOND_MS),秒桶推进 = 可见 elapsed 变化。 */
174
+ const SECOND_MS = 1000;
175
+
176
+ export function computeRenderSignature(run: WorkflowRun, now: number): string {
177
+ const traceArr = run.state.trace.toArray();
178
+ const completed = traceArr.filter((n) => n.status === "completed").length;
179
+ const budget = run.state.budget;
180
+ // budget 量化串与 renderHeader budgetStr 同源同精度(round-k token / toFixed(4) cost)
181
+ const budgetPart = `${Math.round(budget.usedTokens / BUDGET_TOKENS_DIVISOR)}k/${budget.maxTokens ? `${Math.round(budget.maxTokens / BUDGET_TOKENS_DIVISOR)}k` : "∞"}::$${budget.usedCost.toFixed(BUDGET_COST_DECIMALS)}`;
182
+ // errorLogs 指纹:detail 只渲染 total 标签 + 末 20 条(renderWorkerLogSection),
183
+ // 而 errorLogs 仅经 push + slice(-MAX_ERROR_LOGS) 变异(append-only + 前向淘汰、
184
+ // 条目不可变)——封顶后 length 不变内容移,单取 length 漏失效;任何可见变化必
185
+ // 伴随「length 变」或「末条变」,故 length+末条(level:message)是完备最小指纹。
186
+ const logs = run.state.errorLogs;
187
+ const lastLog = logs && logs.length > 0 ? `${logs[logs.length - 1].level}:${logs[logs.length - 1].message}` : "-";
188
+ const errorLogsPart = `${logs?.length ?? 0}:${lastLog}`;
189
+ const nodeParts = traceArr.map((n) => {
190
+ const live = n.live ? projectLiveProgress(n.live) : undefined;
191
+ return `${n.stepIndex}:${n.status}:${n.sessionFile ?? "-"}:${live?.totalTokens ?? -1}:${n.live ? countAllToolCalls(n.live) : -1}:${live?.elapsedSeconds ?? -1}:${live?.turns ?? -1}:${live?.eventLog.length ?? -1}:${live?.currentActivity ? `${live.currentActivity.type}:${live.currentActivity.label}` : "-"}:${live?.lastError ?? "-"}`;
192
+ });
193
+ return [run.state.status, Math.floor(now / SECOND_MS), `${completed}/${traceArr.length}`, budgetPart, errorLogsPart, ...nodeParts].join("|");
194
+ }
195
+
133
196
  // ── View state ────────────────────────────────────────────────
134
197
 
135
198
  interface ViewState {
@@ -181,7 +244,7 @@ function createInitialState(): ViewState {
181
244
  * @param run WorkflowRun 聚合根(读 state.status/spec/trace/meta)
182
245
  * @param theme ThemeLike(避免直接 import Pi runtime)
183
246
  * @param ctx ExtensionContext(调 ui.custom 渲染 + ui.notify 错误反馈)
184
- * @param actions lifecycle 操作(pause/resume/abort),由调用方注入
247
+ * @param actions lifecycle 操作(abort),由调用方注入
185
248
  */
186
249
  export function createWorkflowsView(
187
250
  run: WorkflowRun,
@@ -212,10 +275,17 @@ export function createWorkflowsView(
212
275
  const requestRender = () => tui.requestRender();
213
276
 
214
277
  // ── 轮询 tick:engine 无事件推送,view 自轮询 trace 变化 ──
215
- // 每 200ms 重绘,保证 header 动态数据(elapsed/tokens)实时更新。
216
- // 行数固定后,diff-redraw 引擎能正确逐行对比,不会出现残影。
278
+ // 每 200ms 条件失效(IF11/TC7):先算渲染签名(computeRenderSignature,
279
+ // 覆盖 header/节点行/L2 detail 全部动态字段),与上次相同 → 该帧内容无可见
280
+ // 变化,直接 return(不清 cache、不 requestRender——纯等待场景零重建零重绘);
281
+ // 不同 → 现状路径(清缓存 + requestRender)。lastSignature 初始 undefined,
282
+ // 首 tick 必失效(保证首绘)。签名计算 O(N)(N=trace 节点数)远低于全量 render。
283
+ let lastSignature: string | undefined;
217
284
  const tick = setInterval(() => {
218
285
  if (state.disposed) return;
286
+ const signature = computeRenderSignature(run, Date.now());
287
+ if (signature === lastSignature) return;
288
+ lastSignature = signature;
219
289
  cache.key = undefined;
220
290
  cache.lines = undefined;
221
291
  requestRender();
@@ -358,21 +428,9 @@ export function createWorkflowsView(
358
428
  return;
359
429
  }
360
430
 
361
- // ── Lifecycle shortcuts (no restart per D-9) ──
362
- if (data === "p") {
363
- if (run.state.status === "running") {
364
- void actions.pause(run.runId)
365
- .then(() => { cache.key = undefined; requestRender(); })
366
- .catch((err: Error) => ctx.ui.notify(`Pause failed: ${err.message}`, "error"));
367
- } else if (run.state.status === "paused") {
368
- void actions.resume(run.runId)
369
- .then(() => { cache.key = undefined; requestRender(); })
370
- .catch((err: Error) => ctx.ui.notify(`Resume failed: ${err.message}`, "error"));
371
- }
372
- return;
373
- }
431
+ // ── Lifecycle shortcuts (no restart per D-9; no pause/resume — one-shot) ──
374
432
  if (data === "a") {
375
- if (run.state.status === "running" || run.state.status === "paused") {
433
+ if (run.state.status === "running") {
376
434
  void actions.abort(run.runId)
377
435
  .then(() => { cache.key = undefined; requestRender(); })
378
436
  .catch((err: Error) => ctx.ui.notify(`Abort failed: ${err.message}`, "error"));
@@ -498,7 +556,7 @@ export function createWorkflowsView(
498
556
  // │ ● 2 deploy 0/1 │ ● tester model ... │
499
557
  // │ (pad) │ (pad) │
500
558
  // ╰───────────────────────────────────────────────────╯
501
- // ↑↓ phase · ⏎ enter · p pause · a abort · s save · esc back ← footer (框外)
559
+ // ↑↓ phase · ⏎ enter · a abort · s save · esc back ← footer (框外)
502
560
  //
503
561
  // body = sidebar(SIDEBAR_WIDTH) │ main(rest)
504
562
  // save overlay 活跃时居中覆盖 body。
@@ -621,10 +679,8 @@ function renderFooter(
621
679
  ? "↑↓ agent · ⏎ detail"
622
680
  : "↑↓ agent · ⏎ prompt · PgUp/PgDn scroll";
623
681
  const actionParts: string[] = [];
624
- const status = run.state.status;
625
- if (status === "running" || status === "paused") {
682
+ if (run.state.status === "running") {
626
683
  actionParts.push("a abort");
627
- actionParts.push(status === "paused" ? "p resume" : "p pause");
628
684
  }
629
685
  actionParts.push("s save");
630
686
  actionParts.push("S trace");
@@ -765,7 +821,7 @@ function renderLevel1(
765
821
  const tokStr = live.totalTokens > 0 ? `${Math.round(live.totalTokens / BUDGET_TOKENS_DIVISOR)}k tok` : "";
766
822
  const tcCount = getAllToolCalls(node.live).length;
767
823
  const elapsed = formatElapsedSeconds(live.elapsedSeconds);
768
- rightLines.push(`${pointer}${dot} ${node.agent} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`);
824
+ rightLines.push(`${pointer}${dot} ${displayAgentName(node.agent)} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`);
769
825
  } else {
770
826
  const elapsed = formatElapsed(
771
827
  node.startedAt,
@@ -774,7 +830,7 @@ function renderLevel1(
774
830
  const tok = node.result?.usage;
775
831
  const tokStr = tok ? `${Math.round((tok.input + tok.output) / BUDGET_TOKENS_DIVISOR)}k tok` : "";
776
832
  const tcCount = node.result?.toolCalls?.length ?? 0;
777
- rightLines.push(`${pointer}${dot} ${node.agent} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`);
833
+ rightLines.push(`${pointer}${dot} ${displayAgentName(node.agent)} ${node.model} ${tokStr} · ${tcCount} tools · ${elapsed}`);
778
834
  }
779
835
  }
780
836
  state.agentScrollOffset = agentStart;
@@ -808,9 +864,10 @@ function renderLevel2(
808
864
  const a = agents[i];
809
865
  const pointer = i === state.agentIdx ? "❯ " : " ";
810
866
  const maxNameWidth = SIDEBAR_WIDTH - AGENT_NAME_BUDGET;
811
- const agentName = visibleLen(a.agent) > maxNameWidth
812
- ? a.agent.slice(0, maxNameWidth - 1) + ELLIPSIS
813
- : a.agent;
867
+ const agentRef = displayAgentName(a.agent);
868
+ const agentName = visibleLen(agentRef) > maxNameWidth
869
+ ? agentRef.slice(0, maxNameWidth - 1) + ELLIPSIS
870
+ : agentRef;
814
871
  leftLines.push(`${pointer}${agentName}`);
815
872
  }
816
873