@zhushanwen/pi-subagent-workflow 0.1.0 → 0.2.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 (76) hide show
  1. package/agents/context-builder.md +1 -3
  2. package/agents/oracle.md +2 -2
  3. package/agents/planner.md +1 -3
  4. package/agents/researcher.md +0 -2
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +13 -3
  7. package/agents/worker.md +0 -2
  8. package/package.json +5 -3
  9. package/skills/workflow-script-format/SKILL.md +6 -6
  10. package/src/execution/__tests__/agent-result-mapper.test.ts +24 -2
  11. package/src/execution/__tests__/concurrency-pool.test.ts +33 -0
  12. package/src/execution/__tests__/execute-options-mapper.test.ts +40 -8
  13. package/src/execution/__tests__/gui-mode-dispatch.test.ts +60 -0
  14. package/src/execution/__tests__/sdk-contract.test.ts +5 -2
  15. package/src/execution/__tests__/session-reconstructor.test.ts +20 -0
  16. package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
  17. package/src/execution/__tests__/subprocess-agent-runner.test.ts +72 -3
  18. package/src/execution/__tests__/tool-action.test.ts +26 -4
  19. package/src/execution/agent-result-mapper.ts +4 -1
  20. package/src/execution/concurrency-pool.ts +38 -6
  21. package/src/execution/execute-options-mapper.ts +21 -4
  22. package/src/execution/execution-record.ts +5 -0
  23. package/src/execution/record-store.ts +2 -0
  24. package/src/execution/session-reconstructor.ts +11 -0
  25. package/src/execution/session-runner.ts +12 -0
  26. package/src/execution/stream-sink.ts +83 -0
  27. package/src/execution/subagent-service.ts +68 -43
  28. package/src/execution/subprocess-agent-runner.ts +16 -4
  29. package/src/execution/types.ts +23 -3
  30. package/src/index.ts +15 -2
  31. package/src/interface/__tests__/subagent-tool-prompt.test.ts +84 -0
  32. package/src/interface/__tests__/workflow-state-file-exposure.test.ts +38 -0
  33. package/src/interface/__tests__/workflow-tool-prompt.test.ts +50 -0
  34. package/src/interface/command-actions.ts +77 -0
  35. package/src/interface/commands.ts +40 -4
  36. package/src/interface/gui-mappers.ts +83 -0
  37. package/src/interface/helpers.ts +52 -9
  38. package/src/interface/list-component.ts +3 -1
  39. package/src/interface/subagent-actions.ts +35 -22
  40. package/src/interface/subagent-tool.ts +54 -23
  41. package/src/interface/subagents.ts +45 -5
  42. package/src/interface/tool-render.ts +16 -5
  43. package/src/interface/tool-workflow-script.ts +113 -15
  44. package/src/interface/tool-workflow.ts +92 -34
  45. package/src/interface/views/WorkflowsView.ts +13 -4
  46. package/src/interface/views/__tests__/detail-content-session-file.test.ts +70 -0
  47. package/src/interface/views/detail-content.ts +20 -0
  48. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +208 -0
  49. package/src/orchestration/__tests__/agent-call-stream.test.ts +157 -0
  50. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +2 -0
  51. package/src/orchestration/__tests__/execute-agent-call.test.ts +171 -0
  52. package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +177 -0
  53. package/src/orchestration/__tests__/worker-script-builder.test.ts +15 -0
  54. package/src/orchestration/agent-opts-resolver.ts +11 -2
  55. package/src/orchestration/error-recovery.ts +131 -23
  56. package/src/orchestration/execute-agent-call.ts +12 -3
  57. package/src/orchestration/jsonl-run-store.ts +10 -0
  58. package/src/orchestration/lifecycle.ts +1 -1
  59. package/src/orchestration/models/agent-call.ts +7 -0
  60. package/src/orchestration/models/ports.ts +15 -2
  61. package/src/orchestration/models/run-spec.ts +6 -0
  62. package/src/orchestration/models/trace.ts +1 -0
  63. package/src/orchestration/models/types.ts +19 -0
  64. package/src/orchestration/node-ops.ts +2 -0
  65. package/src/orchestration/worker-script-builder.ts +1 -0
  66. package/workflows/README.md +58 -0
  67. package/workflows/chain.js +107 -0
  68. package/workflows/map-reduce.js +142 -0
  69. package/workflows/parallel.js +131 -0
  70. package/workflows/scatter-gather.js +146 -0
  71. package/examples/README.md +0 -43
  72. package/examples/chain.example.js +0 -92
  73. package/examples/map-reduce.example.js +0 -99
  74. package/examples/parallel.example.js +0 -82
  75. package/examples/scatter-gather.example.js +0 -106
  76. package/src/interface/gui-adapter.ts +0 -136
@@ -3,26 +3,66 @@
3
3
  // /subagents 命令。薄壳——打开 list overlay(等同原 /subagents list [<id>])。
4
4
  //
5
5
  // 解析:args[0] 直接作可选 <id>(聚焦该 record)。
6
+ // RPC 模式(xyz-agent GUI):解析 cancel action 直接执行,不打开 TUI。
6
7
 
7
8
  import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
8
9
 
9
10
  import { getSubagentService } from "../execution/subagent-service.ts";
11
+ import { parseSubagentRpcCommand } from "./command-actions.ts";
10
12
  import { createSubagentsView } from "./list-view.ts";
11
13
 
12
14
  /** 注册 /subagents 命令(= list overlay)。 */
13
15
  export function registerSubagentsCommand(pi: ExtensionAPI): void {
14
16
  pi.registerCommand("subagents", {
15
- description: "Subagents: /subagents [<id>]",
17
+ description: "Subagents: /subagents [<id>] | /subagents cancel <id>",
16
18
  handler: async (argsStr: string, ctx: ExtensionCommandContext) => {
17
- if (!ctx.hasUI) {
18
- ctx.ui.notify("/subagents requires an interactive UI", "error");
19
- return;
20
- }
21
19
  const service = getSubagentService();
22
20
  if (!service) {
23
21
  ctx.ui.notify("subagents execution runtime not ready (session not started)", "error");
24
22
  return;
25
23
  }
24
+
25
+ // ── RPC 模式(xyz-agent GUI):解析 action 直接执行,不打开 TUI ──
26
+ // hasUI 在 TUI 和 RPC 都为 true,不能用于区分;用 ctx.mode === "rpc" 判定 GUI 通道。
27
+ if (ctx.mode === "rpc") {
28
+ const parsed = parseSubagentRpcCommand(argsStr);
29
+ switch (parsed.action) {
30
+ case "cancel": {
31
+ try {
32
+ const ok = service.cancel(parsed.recordId);
33
+ ctx.ui.notify(
34
+ ok ? `Cancelled subagent ${parsed.recordId}` : `Subagent ${parsed.recordId} not found or already finished`,
35
+ ok ? "info" : "warning",
36
+ );
37
+ } catch (err) {
38
+ // service.cancel 内部 assertReady 在 session_shutdown 并发 dispose 时会抛
39
+ const msg = err instanceof Error ? err.message : String(err);
40
+ ctx.ui.notify(`Failed to cancel subagent ${parsed.recordId}: ${msg}`, "warning");
41
+ }
42
+ return;
43
+ }
44
+ case "cancel-missing-id":
45
+ ctx.ui.notify("Usage: /subagents cancel <id>", "warning");
46
+ return;
47
+ case "noop":
48
+ // 无 action 或未知 action:GUI 端已屏蔽此 command 入口,此处兜底
49
+ ctx.ui.notify("View subagents in the sidebar Agents tab", "info");
50
+ return;
51
+ default: {
52
+ // exhaustiveness 断言:未来新增 action verb 忘加 case 时 tsc 报错
53
+ const _exhaustive: never = parsed;
54
+ throw new Error(`Unhandled subagent RPC action: ${String(_exhaustive)}`);
55
+ }
56
+ }
57
+ }
58
+
59
+ // ── print/json 模式(headless):不可交互 ──
60
+ if (ctx.mode !== "tui") {
61
+ ctx.ui.notify("/subagents requires interactive mode", "error");
62
+ return;
63
+ }
64
+
65
+ // ── TUI 模式:打开 list overlay(原逻辑不变)──
26
66
  const args = argsStr.trim().split(/\s+/).filter(Boolean);
27
67
  await createSubagentsView(service, ctx.ui.theme, ctx, args[0]);
28
68
  },
@@ -98,7 +98,14 @@ export function renderSubagentCall(
98
98
  ? (args as { startParam?: unknown }).startParam
99
99
  : undefined;
100
100
  const agent = extractAgentName(startParam);
101
- const parts = [`${t.fg("toolTitle", t.bold("subagent "))}${t.fg("accent", agent)}`];
101
+ // slug:从 startParam 提取(必填字段),非空时在 agent 后用 · 分隔展示。
102
+ const slug = typeof startParam === "object" && startParam !== null && "slug" in startParam
103
+ ? (startParam as { slug?: unknown }).slug
104
+ : undefined;
105
+ const slugStr = typeof slug === "string" ? slug.trim() : "";
106
+ const parts = slugStr
107
+ ? [`${t.fg("toolTitle", t.bold("subagent "))}${t.fg("accent", agent)}${t.fg("dim", " · ")}${t.fg("accent", slugStr)}`]
108
+ : [`${t.fg("toolTitle", t.bold("subagent "))}${t.fg("accent", agent)}`];
102
109
 
103
110
  // model + thinking——完整 provider/model(accent 色),thinking 保持 dim。
104
111
  // 不去 provider 前缀——provider 是模型来源的关键信息,感知「用错模型」需要完整路径。
@@ -210,8 +217,9 @@ function buildCompactLines(d: SubagentToolResult, theme: ThemeLike): string[] {
210
217
  }
211
218
  // ── start 分支:background ──
212
219
  if ("bgResponse" in d) {
220
+ const slugPart = d.slug ? `${theme.fg("dim", " · ")}${theme.fg("accent", d.slug)}` : "";
213
221
  return [truncLine(
214
- `${theme.fg("accent", "●")} ${theme.fg("dim", "background: ")}${theme.fg("accent", d.subagentId ?? "?")}`
222
+ `${theme.fg("accent", "●")} ${theme.fg("dim", "background: ")}${theme.fg("accent", d.subagentId ?? "?")}${slugPart}`
215
223
  + ` ${theme.fg("dim", "· running detached · will notify on completion")}`,
216
224
  width,
217
225
  )];
@@ -238,8 +246,9 @@ function buildExpandedLines(d: SubagentToolResult, theme: ThemeLike): string[] {
238
246
  const lines: string[] = [];
239
247
  // bg 占位 expanded 与 compact 同(一次性 block 无细节可展开)
240
248
  if ("bgResponse" in d) {
249
+ const slugPart = d.slug ? `${theme.fg("dim", " · ")}${theme.fg("accent", d.slug)}` : "";
241
250
  lines.push(truncLine(
242
- `${theme.fg("accent", "●")} ${theme.fg("dim", "background: ")}${theme.fg("accent", d.subagentId ?? "?")}`,
251
+ `${theme.fg("accent", "●")} ${theme.fg("dim", "background: ")}${theme.fg("accent", d.subagentId ?? "?")}${slugPart}`,
243
252
  width,
244
253
  ));
245
254
  return lines;
@@ -302,7 +311,7 @@ function firstLineSanitized(text?: string): string {
302
311
  // list 渲染 helper(action:"list" 分支)
303
312
  // ============================================================
304
313
 
305
- /** list compact:标题行 + 每行一个 item 摘要(glyph + agent + mode + status + duration)。 */
314
+ /** list compact:标题行 + 每行一个 item 摘要(glyph + agent + slug + mode + status + duration)。 */
306
315
  function renderListCompact(resp: ListResponse, theme: ThemeLike, width: number): string[] {
307
316
  if (resp.items.length === 0) {
308
317
  return [truncLine(theme.fg("dim", `No subagents (running: ${resp.running})`), width)];
@@ -314,7 +323,9 @@ function renderListCompact(resp: ListResponse, theme: ThemeLike, width: number):
314
323
  const glyph = statusGlyph(it.status);
315
324
  const icon = glyph.icon ?? "●";
316
325
  const mode = "bg";
317
- const line = `${theme.fg(glyph.color, icon)} ${theme.fg("accent", it.agent)}`
326
+ // slug 非空时在 agent 后展示(· 分隔),空串时省略。
327
+ const slugPart = it.slug ? `${theme.fg("dim", " · ")}${theme.fg("accent", it.slug)}` : "";
328
+ const line = `${theme.fg(glyph.color, icon)} ${theme.fg("accent", it.agent)}${slugPart}`
318
329
  + ` ${theme.fg("dim", `· ${mode} · ${it.status} · ${formatElapsedSeconds(it.duration)}`)}`;
319
330
  lines.push(truncLine(`${STREAM_PREFIX}${line}`, width));
320
331
  }
@@ -23,9 +23,17 @@ import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-cod
23
23
  import { Text } from "@mariozechner/pi-tui";
24
24
  import { type Static, Type } from "typebox";
25
25
 
26
+ import {
27
+ guiComponent,
28
+ type GuiContext,
29
+ type GuiRenderResult,
30
+ guiResult,
31
+ isGuiCapable,
32
+ } from "@xyz-agent/extension-protocol";
26
33
  import type { WorkflowScriptRegistry } from "../orchestration/models/workflow-script-registry.ts";
27
34
  import { lintScript } from "../orchestration/script-lint.ts";
28
35
  import { deleteWorkflow, saveWorkflow } from "../orchestration/workflow-files.ts";
36
+ import { toGuiCtx } from "./gui-mappers.ts";
29
37
  import { renderTextFallback } from "./views/format.ts";
30
38
 
31
39
  // ── Parameter schema ─────────────────────────────────────────
@@ -60,11 +68,11 @@ type ScriptParams = Static<typeof WorkflowScriptParams>;
60
68
  * can distinguish error shape from success.
61
69
  */
62
70
  export type WorkflowScriptToolDetails =
63
- | { action: "generate"; path: string; name: string; status: "ready" }
64
- | { action: "lint"; name: string; valid: boolean; findingCount: number }
65
- | { action: "list"; count: number }
66
- | { action: "save"; name: string; ok: boolean }
67
- | { action: "delete"; name: string; ok: boolean };
71
+ | { action: "generate"; path: string; name: string; status: "ready"; __gui__?: GuiRenderResult }
72
+ | { action: "lint"; name: string; valid: boolean; findingCount: number; __gui__?: GuiRenderResult }
73
+ | { action: "list"; count: number; __gui__?: GuiRenderResult }
74
+ | { action: "save"; name: string; ok: boolean; __gui__?: GuiRenderResult }
75
+ | { action: "delete"; name: string; ok: boolean; __gui__?: GuiRenderResult };
68
76
 
69
77
  /** Result returned by the `workflow-script` tool's execute. */
70
78
  export interface TextContent {
@@ -73,6 +81,71 @@ export interface TextContent {
73
81
  isError?: boolean;
74
82
  }
75
83
 
84
+ // ── GUI 协议 helpers ───────────────────────────────────────
85
+
86
+ /**
87
+ * 为 details 附加 __gui__(RPC 模式下)。
88
+ *
89
+ * 所有 5 个 action 都映射到 stats-line(单行统计,无复杂结构):
90
+ * - generate: 显示生成的脚本名
91
+ * - lint: passed / N findings
92
+ * - list: 脚本数量
93
+ * - save/delete: ok/warn
94
+ */
95
+ function withScriptGui(
96
+ result: TextContent,
97
+ ctx?: GuiContext,
98
+ ): TextContent {
99
+ if (!ctx || !isGuiCapable(ctx) || !result.details) return result;
100
+ const details = result.details;
101
+ // union 各成员已声明 __gui__?,spread + 补字段类型安全,无需强转
102
+ return {
103
+ ...result,
104
+ details: { ...details, __gui__: guiResult(buildScriptGui(details)) },
105
+ };
106
+ }
107
+
108
+ /** 按 WorkflowScriptToolDetails 构造 stats-line GuiComponent。 */
109
+ export function buildScriptGui(details: WorkflowScriptToolDetails) {
110
+ switch (details.action) {
111
+ case "generate":
112
+ return guiComponent("stats-line", {
113
+ items: [{ label: "generated", value: details.name, severity: "ok" }],
114
+ });
115
+ case "lint":
116
+ return guiComponent("stats-line", {
117
+ items: [
118
+ {
119
+ label: "lint",
120
+ value: details.valid ? "passed" : `${details.findingCount} findings`,
121
+ severity: details.valid ? "ok" : "warn",
122
+ },
123
+ ],
124
+ });
125
+ case "list":
126
+ return guiComponent("stats-line", {
127
+ items: [{ label: "scripts", value: String(details.count), severity: "ok" }],
128
+ });
129
+ case "save":
130
+ case "delete":
131
+ return guiComponent("stats-line", {
132
+ items: [
133
+ {
134
+ label: details.action,
135
+ value: details.name,
136
+ severity: details.ok ? "ok" : "warn",
137
+ },
138
+ ],
139
+ });
140
+ default:
141
+ // 防御性兜底:action 是有限联合类型,理论不可达。
142
+ // 若未来新增 action 忘了更新此 switch,返回中性 stats-line 而非 undefined。
143
+ return guiComponent("stats-line", {
144
+ items: [{ label: "action", value: String((details as { action: string }).action), severity: "warn" }],
145
+ });
146
+ }
147
+ }
148
+
76
149
  // ── Tool registration ────────────────────────────────────────
77
150
 
78
151
  /**
@@ -92,14 +165,22 @@ export function registerWorkflowScriptTool(
92
165
  label: "Workflow Script",
93
166
  description:
94
167
  "Manage workflow scripts: generate (AI creates tmp script), lint (static check), " +
95
- "save (tmp→permanent), delete, list. Replaces workflow-generate + workflow-lint tools.",
168
+ "save (tmp→permanent), delete, list. Before generating a new script, use action:list " +
169
+ "to check if a built-in workflow (chain/parallel/scatter-gather/map-reduce) already " +
170
+ "covers the use case. Replaces workflow-generate + workflow-lint tools.",
96
171
  promptSnippet: "Generate, lint, save, delete, or list workflow scripts",
97
172
  promptGuidelines: [
98
173
  "generate: AI writes a tmp workflow script to .pi/workflows/.tmp/. Script can be run immediately via the workflow tool.",
99
174
  "lint: Statically check a script for common API misuse (outputSchema, result.output, file state).",
100
175
  "save: Promote a tmp script to permanent (.pi/workflows/).",
101
176
  "delete: Remove a script (blocked if a run is active).",
102
- "list: Show all available workflow scripts with source tags.",
177
+ "list: Show all available workflow scripts with source tags. " +
178
+ "Use this to discover built-in workflows (chain/parallel/scatter-gather/map-reduce) " +
179
+ "and user-generated scripts before starting a run. After listing, start a script via " +
180
+ "the workflow tool with action:run and the script name.",
181
+ "ANTI-PATTERN: Do NOT generate a new script for chain/parallel/scatter-gather/map-reduce " +
182
+ "orchestration — these are built-in. Call action:list first. Generate only when no built-in " +
183
+ "matches the use case.",
103
184
  ],
104
185
  parameters: WorkflowScriptParams,
105
186
 
@@ -108,22 +189,31 @@ export function registerWorkflowScriptTool(
108
189
  params: ScriptParams,
109
190
  signal: AbortSignal | undefined,
110
191
  _onUpdate: unknown,
111
- _ctx: ExtensionContext,
192
+ ctx: ExtensionContext,
112
193
  ): Promise<TextContent> {
194
+ let result: TextContent;
113
195
  switch (params.action) {
114
196
  case "generate":
115
- return actionGenerate(params, signal);
197
+ result = actionGenerate(params, signal);
198
+ break;
116
199
  case "lint":
117
- return actionLint(params, registry);
200
+ result = await actionLint(params, registry);
201
+ break;
118
202
  case "save":
119
- return actionSave(params);
203
+ result = await actionSave(params);
204
+ break;
120
205
  case "delete":
121
- return actionDelete(params, registry, isRunning);
206
+ result = actionDelete(params, registry, isRunning);
207
+ break;
122
208
  case "list":
123
- return await actionList(registry);
209
+ result = await actionList(registry);
210
+ break;
124
211
  default:
125
- return textResult(`Unknown action: ${String(params.action)}`, true);
212
+ result = textResult(`Unknown action: ${String(params.action)}`, true);
213
+ break;
126
214
  }
215
+ // GUI 协议:RPC 模式下附加 __gui__ 到 details
216
+ return withScriptGui(result, toGuiCtx(ctx));
127
217
  },
128
218
 
129
219
  renderCall(args: ScriptParams, theme: Theme, _context?: unknown) {
@@ -228,7 +318,15 @@ async function actionLint(
228
318
  }
229
319
  const source = await loadScriptSource(name, registry);
230
320
  if (!source) {
231
- return textResult(`Workflow '${name}' not found or not available.`, true);
321
+ const all = await registry.loadAll();
322
+ const available = all.filter((wf) => wf.available);
323
+ const suggestions = available
324
+ .map((wf) => ` - ${wf.name}: ${wf.meta.description || "(no description)"}`)
325
+ .join("\n");
326
+ return textResult(
327
+ `Workflow '${name}' not found or not available.\nAvailable:\n${suggestions || " (none)"}`,
328
+ true,
329
+ );
232
330
  }
233
331
 
234
332
  const result = lintScript(source);
@@ -26,14 +26,17 @@ import { type Static, Type } from "typebox";
26
26
 
27
27
  import type { LauncherDeps } from "../orchestration/launcher.ts";
28
28
  import { abortRun, pauseRun, resumeRun, runWorkflow } from "../orchestration/lifecycle.ts";
29
+ import type { RunStore } from "../orchestration/models/ports.ts";
29
30
  import type { WorkflowRun } from "../orchestration/models/workflow-run.ts";
30
31
  import { retryNode, skipNode } from "../orchestration/node-ops.ts";
31
32
  import {
32
33
  guiComponent,
33
34
  type GuiContext,
35
+ type GuiRenderResult,
34
36
  guiResult,
35
37
  isGuiCapable,
36
- } from "./gui-adapter.ts";
38
+ } from "@xyz-agent/extension-protocol";
39
+ import { mapRunIcon, mapRunStatus, toGuiCtx } from "./gui-mappers.ts";
37
40
  import {
38
41
  acquireReentryGuard,
39
42
  REENTRY_BUSY_MESSAGE,
@@ -69,6 +72,14 @@ const WorkflowParams = Type.Object({
69
72
  name: Type.Optional(
70
73
  Type.String({ description: "Workflow name (run action)" }),
71
74
  ),
75
+ slug: Type.Optional(
76
+ Type.String({
77
+ description:
78
+ "Short label (max 20 chars) for this run, shown in the TUI to distinguish concurrent runs. " +
79
+ "If omitted, defaults to the script name.",
80
+ maxLength: 20,
81
+ }),
82
+ ),
72
83
  runId: Type.Optional(
73
84
  Type.String({ description: "Workflow run ID (pause/resume/abort/retry-node/skip-node)" }),
74
85
  ),
@@ -99,11 +110,15 @@ const RUNID_SHORT = 8;
99
110
  interface RunSummary {
100
111
  runId: string;
101
112
  name: string;
113
+ /** Run 级 slug(可选,旧 run 缺失为 undefined)。 */
114
+ slug?: string;
102
115
  status: string;
103
116
  reason?: string;
104
117
  startedAt?: string;
105
118
  completedAt?: string;
106
119
  error?: string;
120
+ /** Run 状态快照文件绝对路径(<sessionDir>/workflow-state/<runId>.jsonl)。 */
121
+ stateFile?: string;
107
122
  }
108
123
 
109
124
  // ── Tool result types ──
@@ -112,14 +127,14 @@ interface RunSummary {
112
127
  * Discriminated union of `workflow` tool `details` payloads.
113
128
  *
114
129
  * Discriminant: `action`. Each action's details shape is explicitly typed so
115
- * downstream consumers (GUI task-list renderer, structured-output) can narrow
130
+ * downstream consumers (GUI list-tree renderer, structured-output) can narrow
116
131
  * without unsafe casts.
117
132
  */
118
133
  export type WorkflowToolDetails =
119
- | { action: "run"; runId: string; status: "running" | "not_found"; name: string }
120
- | { action: "status"; runs: RunSummary[] }
121
- | { action: "pause" | "resume" | "abort"; runId: string; status: string; reason?: string }
122
- | { action: "retry-node" | "skip-node"; runId: string; callId: number };
134
+ | { action: "run"; runId: string; status: "running" | "not_found"; name: string; slug?: string; stateFile?: string; __gui__?: GuiRenderResult }
135
+ | { action: "status"; runs: RunSummary[]; __gui__?: GuiRenderResult }
136
+ | { action: "pause" | "resume" | "abort"; runId: string; status: string; reason?: string; __gui__?: GuiRenderResult }
137
+ | { action: "retry-node" | "skip-node"; runId: string; callId: number; __gui__?: GuiRenderResult };
123
138
 
124
139
  /** Result returned by the `workflow` tool's execute. */
125
140
  export interface ToolResult {
@@ -130,42 +145,57 @@ export interface ToolResult {
130
145
 
131
146
  // ── GUI 协议 helpers ───────────────────────────────────────
132
147
 
133
- /** 为 details 附加 __gui__(RPC 模式下)。 */
134
- function withGui<T extends WorkflowToolDetails | undefined>(
135
- details: T,
148
+ /** 为 details 附加 __gui__(RPC 模式下)。union 各成员已声明 __gui__?,无需强转。 */
149
+ function withGui(
150
+ details: WorkflowToolDetails | undefined,
136
151
  ctx?: GuiContext,
137
- ): Record<string, unknown> {
138
- const out: Record<string, unknown> = details ? { ...details } : {};
139
- if (ctx && isGuiCapable(ctx) && details) {
140
- out.__gui__ = guiResult(buildWorkflowGui(details));
152
+ ): WorkflowToolDetails | undefined {
153
+ if (!details) return undefined;
154
+ if (ctx && isGuiCapable(ctx)) {
155
+ return { ...details, __gui__: guiResult(buildWorkflowGui(details)) };
141
156
  }
142
- return out;
157
+ return details;
143
158
  }
144
159
 
145
160
  /** 按 WorkflowToolDetails 构造对应的 GuiComponent。 */
146
- function buildWorkflowGui(details: WorkflowToolDetails) {
161
+ export function buildWorkflowGui(details: WorkflowToolDetails) {
147
162
  if (details.action === "run") {
148
- return guiComponent("workflow-runs", {
149
- runs: [{ runId: details.runId, name: details.name, status: details.status }],
163
+ // not_found 是脚本未找到的逻辑错误(isError:true),不能走通用 mapper 的 done/check 成功映射。
164
+ // 短路为 danger severity stats-line,与 isError 文案一致。
165
+ if (details.status === "not_found") {
166
+ return guiComponent("stats-line", {
167
+ items: [{ label: "run", value: "not found", severity: "danger" as const }],
168
+ });
169
+ }
170
+ const statusStr = details.status;
171
+ return guiComponent("list-tree", {
172
+ items: [{
173
+ label: [details.name, details.slug, details.runId.slice(0, 8)].filter(Boolean).join(" "),
174
+ status: mapRunStatus(statusStr),
175
+ icon: mapRunIcon(statusStr),
176
+ }],
150
177
  });
151
178
  }
152
179
  if (details.action === "status") {
153
- return guiComponent("workflow-runs", {
154
- runs: details.runs.map((r) => ({
155
- runId: r.runId,
156
- name: r.name,
157
- status: r.status,
158
- reason: r.reason,
159
- error: r.error,
160
- })),
180
+ return guiComponent("list-tree", {
181
+ items: details.runs.map((r) => {
182
+ const statusStr = r.reason ? `${r.status} (${r.reason})` : r.status;
183
+ return {
184
+ label: [r.name, r.slug, r.runId.slice(0, 8)].filter(Boolean).join(" "),
185
+ status: mapRunStatus(statusStr),
186
+ icon: mapRunIcon(statusStr),
187
+ };
188
+ }),
161
189
  });
162
190
  }
163
191
  // pause/resume/abort/retry-node/skip-node
192
+ // abort 是破坏性终止、pause 是挂起(非成功完成),用 warn 区分;resume/retry/skip 保留 ok
193
+ const severity = details.action === "abort" || details.action === "pause" ? "warn" as const : "ok" as const;
164
194
  return guiComponent("stats-line", {
165
195
  items: [{
166
196
  label: details.action,
167
197
  value: details.runId.slice(0, 8),
168
- severity: "ok" as const,
198
+ severity,
169
199
  }],
170
200
  });
171
201
  }
@@ -200,6 +230,15 @@ export function registerWorkflowTool(
200
230
  promptSnippet: "Run, pause, resume, abort, or check workflow status",
201
231
  promptGuidelines: [
202
232
  "PRIORITY: When user says 'workflow', 'run workflow', try run action FIRST.",
233
+ "BUILT-IN workflows (ready to use, no script generation needed): " +
234
+ "chain (analyze→transform→synthesize sequential; args: task), " +
235
+ "parallel (multi-perspective analysis; args: target, optional perspectives), " +
236
+ "scatter-gather (split→parallel process→merge; args: task), " +
237
+ "map-reduce (parallel map→reduce; args: items/itemsJson + operation). " +
238
+ "Example: workflow run chain --args task=\"<description>\".",
239
+ "DISCOVERY: If unsure what workflows exist, call the workflow-script tool with " +
240
+ "action:list first — it returns all available scripts (built-in + user-generated) " +
241
+ "with source tags and descriptions. Then use this tool's run action to start one.",
203
242
  "run: discover by name/description, then start in background (no user confirmation needed).",
204
243
  "Do NOT poll status after starting — results appear automatically via notifyDone.",
205
244
  "retry-node/skip-node: for specific failed agent calls (requires runId + callId). " +
@@ -260,7 +299,7 @@ export function registerWorkflowTool(
260
299
  // GUI 协议:RPC 模式下附加 __gui__ 到 details
261
300
  return {
262
301
  ...result,
263
- details: withGui(result.details, _ctx as GuiContext) as unknown as WorkflowToolDetails,
302
+ details: withGui(result.details, toGuiCtx(_ctx)),
264
303
  };
265
304
  } finally {
266
305
  releaseReentryGuard(reentryRef);
@@ -270,11 +309,16 @@ export function registerWorkflowTool(
270
309
  renderCall(args: Record<string, unknown>, theme: Theme, _context?: unknown) {
271
310
  const action = String(args.action ?? "");
272
311
  const name = args.name ? ` ${String(args.name)}` : "";
312
+ // run action 可选 slug:在 name 后追加 · slug(accent 色)
313
+ const slug = typeof args.slug === "string" && args.slug.trim()
314
+ ? `${theme.fg("dim", " · ")}${theme.fg("accent", String(args.slug))}`
315
+ : "";
273
316
  const runId = args.runId ? ` ${String(args.runId).slice(0, RUNID_SHORT)}` : "";
274
317
  return new Text(
275
318
  theme.fg("toolTitle", theme.bold("workflow ")) +
276
319
  theme.fg("muted", action) +
277
320
  theme.fg("accent", name) +
321
+ slug +
278
322
  theme.fg("dim", runId),
279
323
  0,
280
324
  0,
@@ -330,6 +374,7 @@ async function actionRun(
330
374
  budgetTokens: tokens,
331
375
  budgetTimeMs: time,
332
376
  scriptName: script.name,
377
+ slug: params.slug,
333
378
  scriptPath: script.path,
334
379
  description: script.meta.description,
335
380
  },
@@ -341,10 +386,12 @@ async function actionRun(
341
386
  content: [
342
387
  {
343
388
  type: "text",
344
- text: `Started workflow '${script.name}' (${runId}). Running in background — do NOT poll status.`,
389
+ text: params.slug
390
+ ? `Started workflow '${script.name}' · ${params.slug} (${runId}). Running in background — do NOT poll status.`
391
+ : `Started workflow '${script.name}' (${runId}). Running in background — do NOT poll status.`,
345
392
  },
346
393
  ],
347
- details: { action: "run", runId, status: "running", name: script.name },
394
+ details: { action: "run", runId, status: "running", name: script.name, slug: params.slug, stateFile: deps.store.stateFilePath(runId) },
348
395
  };
349
396
  }
350
397
 
@@ -358,7 +405,7 @@ function actionStatus(deps: LauncherDeps): ToolResult {
358
405
  details: { action: "status", runs: [] },
359
406
  };
360
407
  }
361
- const summaries = runs.map(toRunSummary);
408
+ const summaries = runs.map((r) => toRunSummary(r, deps.store));
362
409
  const lines = summaries.map((s) => {
363
410
  const duration = s.startedAt ? ` (${formatElapsed(s.startedAt)})` : "";
364
411
  const reasonSuffix = s.reason && s.reason !== "completed" ? ` [${s.reason}]` : "";
@@ -383,7 +430,10 @@ async function actionLifecycle(
383
430
  }
384
431
  const run = deps.runs.get(runId);
385
432
  if (!run) {
386
- return textResult(`Workflow '${runId}' not found`, true);
433
+ return textResult(
434
+ `Workflow '${runId}' not found. Use action:status to list active runs and their runIds.`,
435
+ true,
436
+ );
387
437
  }
388
438
  try {
389
439
  const oldStatus = run.state.status;
@@ -421,7 +471,10 @@ async function actionRetryNode(params: WorkflowToolParams, deps: LauncherDeps):
421
471
  }
422
472
  const run = deps.runs.get(runId);
423
473
  if (!run) {
424
- return textResult(`Workflow '${runId}' not found`, true);
474
+ return textResult(
475
+ `Workflow '${runId}' not found. Use action:status to list active runs and their runIds.`,
476
+ true,
477
+ );
425
478
  }
426
479
  try {
427
480
  await retryNode(run, callId, deps);
@@ -445,7 +498,10 @@ async function actionSkipNode(params: WorkflowToolParams, deps: LauncherDeps): P
445
498
  }
446
499
  const run = deps.runs.get(runId);
447
500
  if (!run) {
448
- return textResult(`Workflow '${runId}' not found`, true);
501
+ return textResult(
502
+ `Workflow '${runId}' not found. Use action:status to list active runs and their runIds.`,
503
+ true,
504
+ );
449
505
  }
450
506
  try {
451
507
  await skipNode(run, callId, deps);
@@ -464,15 +520,17 @@ async function actionSkipNode(params: WorkflowToolParams, deps: LauncherDeps): P
464
520
  // ── helpers ──────────────────────────────────────────────────
465
521
 
466
522
  /** WorkflowRun → 摘要(status action 用)。 */
467
- function toRunSummary(run: WorkflowRun): RunSummary {
523
+ function toRunSummary(run: WorkflowRun, store: RunStore): RunSummary {
468
524
  return {
469
525
  runId: run.runId,
470
526
  name: run.spec.scriptName,
527
+ slug: run.spec.slug,
471
528
  status: run.state.status,
472
529
  reason: run.state.reason,
473
530
  startedAt: run.meta.startedAt,
474
531
  completedAt: run.meta.completedAt,
475
532
  error: run.state.error,
533
+ stateFile: store.stateFilePath(run.runId),
476
534
  };
477
535
  }
478
536
 
@@ -188,6 +188,7 @@ export function createWorkflowsView(
188
188
  theme: ThemeLike,
189
189
  ctx: ExtensionContext,
190
190
  actions: ViewActions,
191
+ runStateFile?: string,
191
192
  ): Promise<void> {
192
193
  return ctx.ui.custom<void>((_tui: unknown, _t: unknown, _kb: unknown, done: (result: void) => void) => {
193
194
  const state = createInitialState();
@@ -467,7 +468,7 @@ export function createWorkflowsView(
467
468
  // 缺陷 #1 修复:每次 render 从 run.state.trace 实时读(toArray 返回内部数组引用,
468
469
  // 后续 trace.append 会反映到 view),不再用 factory 时的冻结快照。
469
470
  const liveGroups = buildPhaseGroups([...run.state.trace.toArray()]);
470
- const raw = renderLayout(run, state, liveGroups, theme, width, height);
471
+ const raw = renderLayout(run, state, liveGroups, theme, width, height, runStateFile);
471
472
  // Pad to terminal height so the overlay fills the screen (matches main 行为)
472
473
  const lines = raw.length < height
473
474
  ? [...raw, ...Array.from({ length: height - raw.length }, () => "")]
@@ -517,12 +518,13 @@ function renderLayout(
517
518
  theme: ThemeLike,
518
519
  screenWidth: number,
519
520
  screenHeight: number,
521
+ runStateFile?: string,
520
522
  ): string[] {
521
523
  const lines: string[] = [];
522
524
  const contentWidth = screenWidth - BOX_BORDER_CHARS;
523
525
  const mainWidth = contentWidth - SIDEBAR_WIDTH - 1; // -1 for the │ divider
524
526
 
525
- renderHeader(lines, run, theme, contentWidth);
527
+ renderHeader(lines, run, theme, contentWidth, runStateFile);
526
528
 
527
529
  const phase = phaseGroups[state.phaseIdx] ?? phaseGroups[0];
528
530
  const agents = phase?.nodes ?? [];
@@ -564,12 +566,13 @@ function renderLayout(
564
566
  return lines;
565
567
  }
566
568
 
567
- /** Header:╭─╮ + name(bold) + 右侧 status/agents/elapsed/budget */
569
+ /** Header:╭─╮ + name(bold) + 右侧 status/agents/elapsed/budget + 可选 state 路径行。 */
568
570
  function renderHeader(
569
571
  lines: string[],
570
572
  run: WorkflowRun,
571
573
  theme: ThemeLike,
572
574
  contentWidth: number,
575
+ runStateFile?: string,
573
576
  ): void {
574
577
  const traceArr = run.state.trace.toArray();
575
578
  const completed = traceArr.filter((n) => n.status === "completed").length;
@@ -579,7 +582,9 @@ function renderHeader(
579
582
  const budget = run.state.budget;
580
583
  const budgetStr = `${Math.round(budget.usedTokens / BUDGET_TOKENS_DIVISOR)}k/${budget.maxTokens ? `${Math.round(budget.maxTokens / BUDGET_TOKENS_DIVISOR)}k` : "∞"} tok · $${budget.usedCost.toFixed(BUDGET_COST_DECIMALS)}`;
581
584
 
582
- const nameLine = theme.bold(run.spec.scriptName);
585
+ const nameLine = run.spec.slug
586
+ ? `${theme.bold(run.spec.scriptName)}${theme.fg("dim", " · ")}${theme.fg("accent", run.spec.slug)}`
587
+ : theme.bold(run.spec.scriptName);
583
588
  const rightPart = theme.fg("muted", `${headerRight} · ${budgetStr}`);
584
589
 
585
590
  lines.push(plainBorder(theme, "╭", "╮", contentWidth));
@@ -596,6 +601,10 @@ function renderHeader(
596
601
  } else {
597
602
  lines.push(walled(theme, rightPart, contentWidth));
598
603
  }
604
+ // run 状态快照文件路径(可选——无 store 注入时不渲染)
605
+ if (runStateFile) {
606
+ lines.push(walled(theme, theme.fg("dim", `state: ${runStateFile}`.slice(0, contentWidth)), contentWidth));
607
+ }
599
608
  lines.push(plainBorder(theme, "├", "┤", contentWidth));
600
609
  }
601
610