@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.
- package/agents/context-builder.md +1 -3
- package/agents/oracle.md +2 -2
- package/agents/planner.md +1 -3
- package/agents/researcher.md +0 -2
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +13 -3
- package/agents/worker.md +0 -2
- package/package.json +5 -3
- package/skills/workflow-script-format/SKILL.md +6 -6
- package/src/execution/__tests__/agent-result-mapper.test.ts +24 -2
- package/src/execution/__tests__/concurrency-pool.test.ts +33 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +40 -8
- package/src/execution/__tests__/gui-mode-dispatch.test.ts +60 -0
- package/src/execution/__tests__/sdk-contract.test.ts +5 -2
- package/src/execution/__tests__/session-reconstructor.test.ts +20 -0
- package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +72 -3
- package/src/execution/__tests__/tool-action.test.ts +26 -4
- package/src/execution/agent-result-mapper.ts +4 -1
- package/src/execution/concurrency-pool.ts +38 -6
- package/src/execution/execute-options-mapper.ts +21 -4
- package/src/execution/execution-record.ts +5 -0
- package/src/execution/record-store.ts +2 -0
- package/src/execution/session-reconstructor.ts +11 -0
- package/src/execution/session-runner.ts +12 -0
- package/src/execution/stream-sink.ts +83 -0
- package/src/execution/subagent-service.ts +68 -43
- package/src/execution/subprocess-agent-runner.ts +16 -4
- package/src/execution/types.ts +23 -3
- package/src/index.ts +15 -2
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +84 -0
- package/src/interface/__tests__/workflow-state-file-exposure.test.ts +38 -0
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +50 -0
- package/src/interface/command-actions.ts +77 -0
- package/src/interface/commands.ts +40 -4
- package/src/interface/gui-mappers.ts +83 -0
- package/src/interface/helpers.ts +52 -9
- package/src/interface/list-component.ts +3 -1
- package/src/interface/subagent-actions.ts +35 -22
- package/src/interface/subagent-tool.ts +54 -23
- package/src/interface/subagents.ts +45 -5
- package/src/interface/tool-render.ts +16 -5
- package/src/interface/tool-workflow-script.ts +113 -15
- package/src/interface/tool-workflow.ts +92 -34
- package/src/interface/views/WorkflowsView.ts +13 -4
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +70 -0
- package/src/interface/views/detail-content.ts +20 -0
- package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +208 -0
- package/src/orchestration/__tests__/agent-call-stream.test.ts +157 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +2 -0
- package/src/orchestration/__tests__/execute-agent-call.test.ts +171 -0
- package/src/orchestration/__tests__/jsonl-run-store-session-file.test.ts +177 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +15 -0
- package/src/orchestration/agent-opts-resolver.ts +11 -2
- package/src/orchestration/error-recovery.ts +131 -23
- package/src/orchestration/execute-agent-call.ts +12 -3
- package/src/orchestration/jsonl-run-store.ts +10 -0
- package/src/orchestration/lifecycle.ts +1 -1
- package/src/orchestration/models/agent-call.ts +7 -0
- package/src/orchestration/models/ports.ts +15 -2
- package/src/orchestration/models/run-spec.ts +6 -0
- package/src/orchestration/models/trace.ts +1 -0
- package/src/orchestration/models/types.ts +19 -0
- package/src/orchestration/node-ops.ts +2 -0
- package/src/orchestration/worker-script-builder.ts +1 -0
- package/workflows/README.md +58 -0
- package/workflows/chain.js +107 -0
- package/workflows/map-reduce.js +142 -0
- package/workflows/parallel.js +131 -0
- package/workflows/scatter-gather.js +146 -0
- package/examples/README.md +0 -43
- package/examples/chain.example.js +0 -92
- package/examples/map-reduce.example.js +0 -99
- package/examples/parallel.example.js +0 -82
- package/examples/scatter-gather.example.js +0 -106
- 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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
192
|
+
ctx: ExtensionContext,
|
|
112
193
|
): Promise<TextContent> {
|
|
194
|
+
let result: TextContent;
|
|
113
195
|
switch (params.action) {
|
|
114
196
|
case "generate":
|
|
115
|
-
|
|
197
|
+
result = actionGenerate(params, signal);
|
|
198
|
+
break;
|
|
116
199
|
case "lint":
|
|
117
|
-
|
|
200
|
+
result = await actionLint(params, registry);
|
|
201
|
+
break;
|
|
118
202
|
case "save":
|
|
119
|
-
|
|
203
|
+
result = await actionSave(params);
|
|
204
|
+
break;
|
|
120
205
|
case "delete":
|
|
121
|
-
|
|
206
|
+
result = actionDelete(params, registry, isRunning);
|
|
207
|
+
break;
|
|
122
208
|
case "list":
|
|
123
|
-
|
|
209
|
+
result = await actionList(registry);
|
|
210
|
+
break;
|
|
124
211
|
default:
|
|
125
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
|
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
|
|
135
|
-
details:
|
|
148
|
+
/** 为 details 附加 __gui__(RPC 模式下)。union 各成员已声明 __gui__?,无需强转。 */
|
|
149
|
+
function withGui(
|
|
150
|
+
details: WorkflowToolDetails | undefined,
|
|
136
151
|
ctx?: GuiContext,
|
|
137
|
-
):
|
|
138
|
-
|
|
139
|
-
if (ctx && isGuiCapable(ctx)
|
|
140
|
-
|
|
152
|
+
): WorkflowToolDetails | undefined {
|
|
153
|
+
if (!details) return undefined;
|
|
154
|
+
if (ctx && isGuiCapable(ctx)) {
|
|
155
|
+
return { ...details, __gui__: guiResult(buildWorkflowGui(details)) };
|
|
141
156
|
}
|
|
142
|
-
return
|
|
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
|
-
|
|
149
|
-
|
|
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("
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|