@zhushanwen/pi-subagent-workflow 8.14.3 → 8.14.4

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.
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Subagent Workflow Extension — subagents tool(批量派发入口,一跳扁平 schema)。
3
+ *
4
+ * ⚠️ 命名同名不同命名空间,互指防混淆:本文件是 **tool**(模型调用的批量派发面,
5
+ * 名为 `subagents`),`interface/subagents.ts` 是 **slash 命令**壳(`/subagents`:
6
+ * TUI list overlay + GUI 定向消息通道)。两者无共享状态、无调用关系——改本文件
7
+ * 不影响命令壳,反之亦然。
8
+ *
9
+ * 行为契约(设计 §3.1 终态 / §3.3 D1-D3、D8、D9):
10
+ * - 唯一批量入口:N 个已知独立任务一次派发;handler 确定性转译
11
+ * runWorkflow("fan-out")(执行管道唯一——collect 时代的批协调状态已退役,
12
+ * 不存在第二套批机制)。
13
+ * - 无 action 分发:status/abort 不复制,直接指路 workflow tool(runId 同体系)。
14
+ * - 无 args 嵌套:tasks/agents/aggregate/... 全在顶层(弱模型信任 schema 结构信号,
15
+ * 两跳转译是事故高发区——对照 workflow tool 的 name+args 形态)。
16
+ * - 批量成员是一次性成员:不可 message/续聊(workflow-origin record 由 messageHandler
17
+ * 拒绝),结果在 run 收口时以一条通知(notifyDone)到达。
18
+ * - 不构造 `details.__gui__`:GUI 挂载按 WORKFLOW_TOOL_NAMES 集合分流,批量块走
19
+ * workflow 块分支(恒折叠单行 + openWorkflowDrawer);`__gui__` 的渲染点在普通
20
+ * tool 分支(v-else)的展开区内,isWorkflow 分支无展开路径不消费——构造即死代码
21
+ * (D8 裁决;workflow tool 现状构造 `__gui__` 但块面同样不消费,本工具不复制该漂移)。
22
+ *
23
+ * 层归属:Interface。依赖 Pi SDK + core lifecycle/registry + reentry-guard。
24
+ */
25
+
26
+ import { StringEnum } from "@earendil-works/pi-ai";
27
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
28
+ import { Text } from "@earendil-works/pi-tui";
29
+ import { type Static, Type } from "typebox";
30
+
31
+ import { MAX_TIMER_DELAY_MS, SLUG_MAX_LENGTH, THINKING_ORDER } from "@zhushanwen/subagent-core";
32
+ import type { LauncherDeps } from "@zhushanwen/subagent-core";
33
+ import { assertEntryTimeBudget, assertSlugWithinLimit, runWorkflow } from "@zhushanwen/subagent-core";
34
+ import {
35
+ acquireReentryGuard,
36
+ REENTRY_BUSY_MESSAGE,
37
+ type ReentryGuardRef,
38
+ releaseReentryGuard,
39
+ } from "./reentry-guard.ts";
40
+ import {
41
+ assertNotAborted,
42
+ buildRunSpecFromScript,
43
+ formatAvailableWorkflowList,
44
+ optionSlugSuffix,
45
+ renderTextResult,
46
+ } from "./tool-shared.ts";
47
+ import type { RunStartDetails, WorkflowToolResult } from "./tool-result.ts";
48
+
49
+ // ── Constants ────────────────────────────────────────────────
50
+
51
+ /**
52
+ * 本工具的固定执行体(内置模板)脚本名。
53
+ *
54
+ * 常量而非参数:subagents tool 的契约就是「转译到 fan-out 模板」——参数化会让
55
+ * 工具的语义随脚本漂移(对照 D3「执行管道唯一」)。脚本按名经 registry 解析
56
+ * (内置名优先链与 workflow tool actionRun 同一条,见 plan U2「复刻既有加载路径」)。
57
+ */
58
+ export const FAN_OUT_SCRIPT_NAME = "fan-out";
59
+
60
+ /** 批量标签时间短码基数(Date.now() 的 36 进制形态:8 字符覆盖到 2059 年)。 */
61
+ const SLUG_TIME_RADIX = 36;
62
+
63
+ // ── Parameter schema(D2:一跳扁平,无 action、无嵌套 args)──
64
+
65
+ const SubagentsParams = Type.Object({
66
+ tasks: Type.Array(Type.String(), {
67
+ description:
68
+ "N complete, self-contained task descriptions — one array element = one dispatchable task prompt. " +
69
+ "Members run as independent one-shot subagents (they do NOT see your conversation), so each element must carry its own context and acceptance criteria. " +
70
+ "All tasks are dispatched in parallel; results arrive together.",
71
+ }),
72
+ agents: Type.Optional(Type.String({
73
+ description:
74
+ "Comma-separated absolute paths to agent .md files (use <location> from <available_subagents>). " +
75
+ "One path applies to every member; N paths map one-to-one onto tasks in the same order. " +
76
+ "When more than one path is given it MUST equal the number of tasks — a mismatch fails the run fast (never a silent persona swap). " +
77
+ "Omit for the default (general-purpose) executor.",
78
+ })),
79
+ aggregate: Type.Optional(Type.Boolean({
80
+ description:
81
+ "Default false. When true, one extra agent is appended at the end to reduce all results into a single conclusion (results are still returned in full).",
82
+ })),
83
+ slug: Type.Optional(Type.String({
84
+ description:
85
+ "Batch label (max 35 chars, kebab-case) shown on the conversation block and run list — provide a short label for multi-batch scenarios. " +
86
+ "If omitted, one is generated for the run state face only.",
87
+ maxLength: SLUG_MAX_LENGTH,
88
+ })),
89
+ model: Type.Optional(Type.String({
90
+ description:
91
+ "Run-level model override in 'provider/modelId' format; every member of the batch inherits it. " +
92
+ "Omit to inherit the main agent's model.",
93
+ })),
94
+ thinkingLevel: Type.Optional(StringEnum(THINKING_ORDER, {
95
+ description:
96
+ "Run-level thinking depth for every member. Omit to default each agent to its model's highest available level.",
97
+ })),
98
+ tokens: Type.Optional(Type.Number({ description: "Max token budget for the whole batch — ONLY set when the user explicitly requests a limit; omit = unlimited (default)" })),
99
+ time: Type.Optional(Type.Number({ description: `Max time budget in ms for the whole batch — ONLY set when the user explicitly requests a limit; omit = unlimited (default; hard ceiling ${MAX_TIMER_DELAY_MS} ms — larger values fail fast at entry)` })),
100
+ });
101
+
102
+ export type SubagentsToolParams = Static<typeof SubagentsParams>;
103
+
104
+ // ── Tool result types ────────────────────────────────────────
105
+
106
+ /**
107
+ * `subagents` tool 的 details。
108
+ *
109
+ * 单形态(无 action 判别式——本工具只有一个动作)。刻意不含 `__gui__`(D8):
110
+ * 批量块由集合分流走 workflow 块分支,不消费 GUI 描述符。
111
+ */
112
+ export interface SubagentsToolDetails extends RunStartDetails {
113
+ /** 启动即返回(后台运行),恒 "running"。 */
114
+ status: "running";
115
+ /** 执行体模板名(恒 FAN_OUT_SCRIPT_NAME,供程序化消费方核对)。 */
116
+ scriptName: string;
117
+ /** 生效标签(模型提供的 slug,或 handler 生成的 fan-out-<时间短码>)。 */
118
+ slug: string;
119
+ taskCount: number;
120
+ }
121
+
122
+ /** Result returned by the `subagents` tool's execute(公共骨架见 tool-result.ts)。 */
123
+ type SubagentsExecuteResult = WorkflowToolResult<SubagentsToolDetails>;
124
+
125
+ // ── helpers ──────────────────────────────────────────────────
126
+
127
+ /**
128
+ * 生成缺省批量标签 `fan-out-<时间短码>`(时间短码 = Date.now() 的 36 进制)。
129
+ *
130
+ * 长度:`fan-out-` (8) + 8 = 16 ≤ SLUG_MAX_LENGTH (35),数十年内形态稳定
131
+ * (36^8 = 2.8e12 ms ≈ 2059 年)——测试锁定该不变量。
132
+ * 空串与空白串按「未提供」处理(避免状态面出现空标签)。
133
+ */
134
+ export function generateBatchSlug(now: number = Date.now()): string {
135
+ return `${FAN_OUT_SCRIPT_NAME}-${now.toString(SLUG_TIME_RADIX)}`;
136
+ }
137
+
138
+ /** 启动返回文案(设计 §3.1 成功路径原文:一条通知 + 单次 status 恢复出口 + abort 指引)。 */
139
+ function subagentsStartupText(
140
+ slug: string,
141
+ runId: string,
142
+ taskCount: number,
143
+ scriptName: string,
144
+ ): string {
145
+ return [
146
+ `Started batch '${slug}' (${runId}) as workflow run '${scriptName}' — ${taskCount} subagents dispatched in parallel (allSettled).`,
147
+ "Results arrive as ONE notification when the run settles. Do NOT poll.",
148
+ `If no notification arrives well past the expected duration, make a SINGLE status check: workflow tool with runId ${runId} (recovery exit, not a poll loop).`,
149
+ `To abort: workflow tool, action abort, runId ${runId}`,
150
+ ].join("\n");
151
+ }
152
+
153
+ /**
154
+ * 批量运行体(execute 主体;导出供契约测试直接调用)。
155
+ *
156
+ * 失败形态与恢复动作见设计 §3.3 D9(本函数只做入口级 fail-fast:tasks 缺失/空、
157
+ * slug 超长、time 超上界;tasks 元素/agents 数量错配由模板入口 fail-fast——run 即
158
+ * 失败,两条路径不重复实现同一约束)。
159
+ */
160
+ export async function runSubagentsBatch(
161
+ params: SubagentsToolParams,
162
+ deps: LauncherDeps,
163
+ signal: AbortSignal | undefined,
164
+ ): Promise<SubagentsExecuteResult> {
165
+ // D9:tasks 缺失/空数组 → 入口 throw(pi 只对 execute throw 置 isError:true)。
166
+ // 文案带 Correct 示例:弱模型照抄即可自纠。
167
+ const tasks = params.tasks;
168
+ if (!Array.isArray(tasks) || tasks.length === 0) {
169
+ throw new Error(
170
+ 'tasks is required (non-empty string array). Correct: {"tasks":["...","..."]}',
171
+ );
172
+ }
173
+
174
+ // slug 运行时护栏(与 workflow tool actionRun 对称的纵深防御;schema maxLength 是第一道关卡)
175
+ const providedSlug = params.slug?.trim();
176
+ assertSlugWithinLimit(providedSlug, ["tri-review", "scan-docs"]);
177
+ // 缺省(或空白)时 handler 生成 fan-out-<时间短码>:受益面 = 状态面(drawer run header /
178
+ // run 投影名);对话流块面显示的是模型 input.slug(无 input 回写通路——D8 分面声明)。
179
+ const slug = providedSlug ? providedSlug : generateBatchSlug();
180
+
181
+ // OR-1 入口 fail-fast:schema 的 time 是 Type.Number 直通(无上界),超 setTimeout
182
+ // 安全域的值会穿透到 lifecycle 内层防线(assertSafeTimerDelay)——入口拦截让它永不
183
+ // 进入副作用链(判定与文案单点在 core shared/entry-guards)。
184
+ const time = params.time;
185
+ assertEntryTimeBudget(time);
186
+
187
+ // 执行体脚本按内置名解析(与 workflow tool actionRun 同一条链:registry.get 命中
188
+ // 内置/已保存名;本工具不允许换脚本,故不回落 getPath)。
189
+ const script = await deps.registry.get(FAN_OUT_SCRIPT_NAME);
190
+ if (!script || !script.available) {
191
+ const all = await deps.registry.loadAll();
192
+ const available = formatAvailableWorkflowList(all);
193
+ throw new Error(
194
+ `Built-in workflow '${FAN_OUT_SCRIPT_NAME}' is not available — the subagents tool runs it as its batch body. ` +
195
+ `Recovery: verify the @zhushanwen/subagent-core package ships workflows/${FAN_OUT_SCRIPT_NAME}.js (reinstall/repair it), then retry. ` +
196
+ `Workflows currently available:\n${available || " (none)"}`,
197
+ );
198
+ }
199
+
200
+ // D3 确定性转译:tasks/agents/aggregate 原样进 args(模板参数面,$ARGS);未提供的
201
+ // 缺省键不写入(模板侧 aggregate 缺省 false;空值不制造「都传/都缺」歧义形态)。
202
+ const args: Record<string, unknown> = { tasks };
203
+ if (params.agents !== undefined) args.agents = params.agents;
204
+ if (params.aggregate !== undefined) args.aggregate = params.aggregate;
205
+
206
+ const runId = await runWorkflow(
207
+ buildRunSpecFromScript(script, {
208
+ args,
209
+ budgetTokens: params.tokens,
210
+ budgetTimeMs: time,
211
+ slug,
212
+ model: params.model,
213
+ thinkingLevel: params.thinkingLevel,
214
+ }),
215
+ deps,
216
+ signal,
217
+ );
218
+
219
+ return {
220
+ content: [{ type: "text", text: subagentsStartupText(slug, runId, tasks.length, script.name) }],
221
+ details: {
222
+ runId,
223
+ status: "running",
224
+ scriptName: script.name,
225
+ slug,
226
+ taskCount: tasks.length,
227
+ stateFile: deps.store.stateFilePath(runId),
228
+ },
229
+ };
230
+ }
231
+
232
+ // ── Tool registration ────────────────────────────────────────
233
+
234
+ /**
235
+ * 注册 `subagents` tool(唯一批量派发入口)。
236
+ *
237
+ * @param pi ExtensionAPI
238
+ * @param deps LauncherDeps(LifecycleDeps + registry)
239
+ * @param reentryRef reentry guard。**与 workflow tool 共用同一实例**(index.ts
240
+ * factory 内创建的单例)——两者是同一条 runWorkflow 管道的入口,共用守卫避免
241
+ * 双 guard 语义漂移;workflow-script tool 的 isScriptRunning 是另一套 flag,
242
+ * 与本 guard 无关。
243
+ */
244
+ export function registerSubagentsTool(
245
+ pi: ExtensionAPI,
246
+ deps: LauncherDeps,
247
+ reentryRef: ReentryGuardRef,
248
+ ): void {
249
+ pi.registerTool({
250
+ name: "subagents",
251
+ label: "Subagents (batch)",
252
+ description:
253
+ "Spawn multiple subagents in ONE call and collect all results together — for N INDEPENDENT tasks.\n" +
254
+ "Each member is a one-shot batch member: you cannot message or continue it (re-dispatch instead). Results arrive as ONE notification when the whole batch settles (not per member).\n" +
255
+ "For a single subagent you can message later, use the `subagent` tool. For tasks that depend on each other's output (step 2 needs step 1's result), use the `workflow` tool (chain / map-reduce) instead — batch members never see each other's results.",
256
+ promptSnippet: "Dispatch N independent subagent tasks in one batch",
257
+ promptGuidelines: [
258
+ "PRIORITY: 2+ independent tasks in one dispatch (results combined by you afterwards) → call `subagents` ONCE with the tasks array — do NOT issue N separate `subagent` starts and do NOT hand-build a workflow.",
259
+ "One-shot batch members: message/fork-from a batch member is not supported; to redo or extend a member's work, dispatch a new task (run the failed/extra task alone in a second batch).",
260
+ "Do NOT poll after starting — the batch result arrives as a single completion notification carrying every member's summary (plus optional aggregate).",
261
+ "Call shape (JSON): {\"tasks\":[\"<task 1>\",\"<task 2>\"],\"agents\":\"<abs .md path or comma-separated list>\",\"aggregate\":false,\"slug\":\"<short label>\"}. tasks is required and every element must be a complete self-contained task prompt.",
262
+ "agents: one path applies to all tasks; N paths map one-to-one onto tasks and N must equal tasks.length (a mismatch fails the run). Omit for the default executor.",
263
+ "slug: pass a short kebab-case label when you run more than one batch — it identifies the batch on the conversation block and in the run list.",
264
+ "Budget: Do NOT set tokens/time unless the user explicitly requests a limit. Batches run unlimited by default.",
265
+ "Model/thinkingLevel: omit by default (inherit the main agent's model). Only set them when the user explicitly requests a specific model or thinking depth for this batch.",
266
+ "Anti-patterns: nesting the tasks under an 'args' key (they are top-level), dispatching dependent steps in one batch (chain them across messages or use the workflow tool), and treating batch results as verified without checking them.",
267
+ "Run control (status/abort) lives in the workflow tool — use the runId printed in this tool's output.",
268
+ ],
269
+ parameters: SubagentsParams,
270
+
271
+ async execute(
272
+ _toolCallId: string,
273
+ params: SubagentsToolParams,
274
+ signal: AbortSignal | undefined,
275
+ _onUpdate: unknown,
276
+ _ctx: ExtensionContext,
277
+ ): Promise<SubagentsExecuteResult> {
278
+ // throw(W4b 契约):pi 只对 execute throw 置 isError:true,返回值里的 isError
279
+ // 被 agent-loop 丢弃——错误一律 throw(abort 前置判定收敛在 tool-shared)。
280
+ assertNotAborted(signal);
281
+ // reentry guard:与 workflow tool 共用(acquire 失败时尚未持有 guard,throw 前无需 release)
282
+ if (!acquireReentryGuard(reentryRef)) {
283
+ throw new Error(REENTRY_BUSY_MESSAGE);
284
+ }
285
+ try {
286
+ return await runSubagentsBatch(params, deps, signal);
287
+ } finally {
288
+ releaseReentryGuard(reentryRef);
289
+ }
290
+ },
291
+
292
+ renderCall(args: Record<string, unknown>, theme: Theme, _context?: unknown) {
293
+ // 单行标题:subagents <N tasks> · <slug>(TUI 惯例同 workflow tool 的 renderCall)
294
+ const count = Array.isArray(args.tasks) ? args.tasks.length : 0;
295
+ const bulk = count > 0 ? ` ${count} tasks` : "";
296
+ const slug = optionSlugSuffix(args.slug, theme);
297
+ return new Text(
298
+ theme.fg("toolTitle", theme.bold("subagents ")) +
299
+ theme.fg("muted", bulk) +
300
+ slug,
301
+ 0,
302
+ 0,
303
+ );
304
+ },
305
+
306
+ renderResult(result: { content?: Array<{ type: string; text?: string }> }, _options: unknown, _theme: Theme, _context?: unknown) {
307
+ return renderTextResult(result);
308
+ },
309
+ });
310
+ }
@@ -17,8 +17,6 @@
17
17
  * 置 isError:true);AbortSignal 的 aborted 检查留宿主层(C4 偏差 #4 已声明)。
18
18
  *
19
19
  * 层归属:Interface。依赖 Pi SDK + engine script-lint + infra workflow-files。
20
- *
21
- * 参考:domain-models.md §FR-5(tool 收口 4→2)。
22
20
  */
23
21
 
24
22
  import { StringEnum } from "@earendil-works/pi-ai";
@@ -43,7 +41,7 @@ import {
43
41
  } from "@zhushanwen/subagent-core";
44
42
  import type { WorkflowScriptRegistry } from "@zhushanwen/subagent-core";
45
43
  import { toGuiCtx } from "./gui-mappers.ts";
46
- import { renderTextFallback } from "./format.ts";
44
+ import { assertNotAborted, renderTextResult } from "./tool-shared.ts";
47
45
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
48
46
 
49
47
  // ── Parameter schema ─────────────────────────────────────────
@@ -242,7 +240,7 @@ export function registerWorkflowScriptTool(
242
240
  _theme: Theme,
243
241
  _context?: unknown,
244
242
  ) {
245
- return new Text(renderTextFallback(result), 0, 0);
243
+ return renderTextResult(result);
246
244
  },
247
245
  });
248
246
  }
@@ -250,11 +248,9 @@ export function registerWorkflowScriptTool(
250
248
  // ── generate action ──────────────────────────────────────────
251
249
 
252
250
  export function actionGenerate(params: ScriptParams, signal: AbortSignal | undefined): TextContent {
253
- if (signal?.aborted) {
254
- // throw(W4b):pi 只对 execute throw isError:true(返回值 isError 被丢弃)。
255
- // AbortSignal 是 pi tool 契约层关注——core 管线不含 signal 检查,宿主自留(C4 偏差 #4)
256
- throw new Error("Operation aborted before start");
257
- }
251
+ // throw(W4b):pi 只对 execute throw 置 isError:true(返回值 isError 被丢弃)。
252
+ // AbortSignal 是 pi tool 契约层关注——core 管线不含 signal 检查,宿主自留(C4 偏差 #4)
253
+ assertNotAborted(signal);
258
254
  const name = params.name ?? "";
259
255
  const script = params.script ?? "";
260
256
 
@@ -12,8 +12,6 @@
12
12
  * 不可挂起,提前停止用 abort,要新结果开新 run)。
13
13
  *
14
14
  * 层归属:Interface。依赖 Pi SDK + Engine lifecycle/launcher + helpers。
15
- *
16
- * 参考:domain-models.md §FR-5(tool 收口 4→2)。
17
15
  */
18
16
 
19
17
  import { StringEnum } from "@earendil-works/pi-ai";
@@ -44,15 +42,25 @@ import {
44
42
  findFlattenedArgKeys,
45
43
  MAX_TIMER_DELAY_MS,
46
44
  } from "@zhushanwen/subagent-core";
45
+ import { assertEntryTimeBudget, assertSlugWithinLimit } from "@zhushanwen/subagent-core";
47
46
  import { runSummary } from "@zhushanwen/subagent-core";
48
47
  import { mapRunIcon, mapRunStatus, toGuiCtx } from "./gui-mappers.ts";
48
+ import { ID_PREVIEW_LENGTH } from "./id-preview.ts";
49
+ import type { RunStartDetails, WorkflowToolResult } from "./tool-result.ts";
49
50
  import {
50
51
  acquireReentryGuard,
51
52
  REENTRY_BUSY_MESSAGE,
52
53
  type ReentryGuardRef,
53
54
  releaseReentryGuard,
54
55
  } from "./reentry-guard.ts";
55
- import { formatRunStatusElapsed, renderTextFallback } from "./format.ts";
56
+ import { formatRunStatusElapsed } from "./format.ts";
57
+ import {
58
+ assertNotAborted,
59
+ buildRunSpecFromScript,
60
+ formatAvailableWorkflowList,
61
+ optionSlugSuffix,
62
+ renderTextResult,
63
+ } from "./tool-shared.ts";
56
64
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
57
65
 
58
66
  // ── Parameter schema ─────────────────────────────────────────
@@ -107,9 +115,6 @@ type WorkflowToolParams = Static<typeof WorkflowParams>;
107
115
 
108
116
  // ── Constants ────────────────────────────────────────────────
109
117
 
110
- /** runId 截断长度(显示用)。 */
111
- const RUNID_SHORT = 8;
112
-
113
118
  /**
114
119
  * tool 自身顶层键(workflow params schema 键)——workflow 参数名与 tool 键撞名时
115
120
  * (如 workflow 声明参数 name),顶层同名键是 tool 参数而非平铺(m6 评审 M-3)。
@@ -169,16 +174,12 @@ interface RunSummary {
169
174
  * without unsafe casts.
170
175
  */
171
176
  export type WorkflowToolDetails =
172
- | { action: "run"; runId: string; status: "running" | "not_found" | "invalid_args"; name: string; slug?: string; stateFile?: string; __gui__?: GuiRenderResult }
177
+ | ({ action: "run"; name: string; __gui__?: GuiRenderResult } & RunStartDetails)
173
178
  | { action: "status"; runs: RunSummary[]; __gui__?: GuiRenderResult }
174
179
  | { action: "abort"; runId: string; status: string; reason?: string; __gui__?: GuiRenderResult };
175
180
 
176
- /** Result returned by the `workflow` tool's execute. */
177
- export interface ToolResult {
178
- content: Array<{ type: "text"; text: string }>;
179
- details: WorkflowToolDetails | undefined;
180
- isError?: boolean;
181
- }
181
+ /** Result returned by the `workflow` tool's execute(公共骨架见 tool-result.ts)。 */
182
+ type WorkflowExecuteResult = WorkflowToolResult<WorkflowToolDetails | undefined>;
182
183
 
183
184
  // ── GUI 协议 helpers ───────────────────────────────────────
184
185
 
@@ -208,7 +209,7 @@ export function buildWorkflowGui(details: WorkflowToolDetails) {
208
209
  const statusStr = details.status;
209
210
  return guiComponent("list-tree", {
210
211
  items: [{
211
- label: [details.name, details.slug, details.runId.slice(0, RUNID_SHORT)].filter(Boolean).join(" "),
212
+ label: [details.name, details.slug, details.runId.slice(0, ID_PREVIEW_LENGTH)].filter(Boolean).join(" "),
212
213
  status: mapRunStatus(statusStr),
213
214
  icon: mapRunIcon(statusStr),
214
215
  }],
@@ -219,7 +220,7 @@ export function buildWorkflowGui(details: WorkflowToolDetails) {
219
220
  items: details.runs.map((r) => {
220
221
  const statusStr = r.reason ? `${r.status} (${r.reason})` : r.status;
221
222
  return {
222
- label: [r.name, r.slug, r.runId.slice(0, RUNID_SHORT)].filter(Boolean).join(" "),
223
+ label: [r.name, r.slug, r.runId.slice(0, ID_PREVIEW_LENGTH)].filter(Boolean).join(" "),
223
224
  status: mapRunStatus(statusStr),
224
225
  icon: mapRunIcon(statusStr),
225
226
  };
@@ -230,7 +231,7 @@ export function buildWorkflowGui(details: WorkflowToolDetails) {
230
231
  return guiComponent("stats-line", {
231
232
  items: [{
232
233
  label: details.action,
233
- value: details.runId.slice(0, RUNID_SHORT),
234
+ value: details.runId.slice(0, ID_PREVIEW_LENGTH),
234
235
  severity: "warn" as const,
235
236
  }],
236
237
  });
@@ -270,7 +271,7 @@ export function registerWorkflowTool(
270
271
  "script file (script header has @pi-meta parameters + usage + phases). Do NOT use " +
271
272
  "workflow-script generate for patterns already covered by available workflows.",
272
273
  "run: pass the workflow ref as name — the listed <name> (builtin/saved workflow) or its <location> absolute .js path from <available_workflows> — then start in background (no user confirmation needed).",
273
- "Do NOT poll status after starting — results appear automatically via notifyDone.",
274
+ "DO NOT bash sleep or poll status after starting — results appear automatically via notifyDone.",
274
275
  "Runs are one-shot: there is no pause/resume — to stop a run early use abort; for a fresh result start a new run.",
275
276
  "Call shapes (JSON): " +
276
277
  "- run: {\"action\":\"run\",\"name\":\"<script>\",\"args\":{...},\"tokens\":N,\"time\":N,\"model\":\"<provider/modelId>\",\"thinkingLevel\":\"<level>\"}. " +
@@ -291,19 +292,18 @@ export function registerWorkflowTool(
291
292
  signal: AbortSignal | undefined,
292
293
  _onUpdate: unknown,
293
294
  _ctx: ExtensionContext,
294
- ): Promise<ToolResult> {
295
+ ): Promise<WorkflowExecuteResult> {
295
296
  // P1-2: Honor abort signal up-front
296
- if (signal?.aborted) {
297
- // throw(W4b):pi 只对 execute throw 置 isError:true,返回值里的 isError
298
- // agent-loop 丢弃(agent-loop.js:453-483)——文案原样进 toolResult。
299
- throw new Error("Operation aborted before start");
300
- }
297
+ // throw(W4b):pi 只对 execute throw 置 isError:true,返回值里的 isError
298
+ // agent-loop 丢弃(agent-loop.js:453-483)——文案原样进 toolResult。
299
+ // abort 前置判定收敛在 tool-shared(三处 tool 同源)。
300
+ assertNotAborted(signal);
301
301
  // P1-6: Reentry guard(acquire 失败时尚未持有 guard,throw 前无需 release)
302
302
  if (!acquireReentryGuard(reentryRef)) {
303
303
  throw new Error(REENTRY_BUSY_MESSAGE);
304
304
  }
305
305
  try {
306
- let result: ToolResult;
306
+ let result: WorkflowExecuteResult;
307
307
  // 断言为 WorkflowAction 联合——typebox Static 推断为 any,显式标注让 default
308
308
  // 分支的 never 穷尽检查生效(新增 action 时 tsc 报错强制补 case)。
309
309
  const action = params.action as WorkflowAction;
@@ -337,10 +337,8 @@ export function registerWorkflowTool(
337
337
  const action = String(args.action ?? "");
338
338
  const name = args.name ? ` ${String(args.name)}` : "";
339
339
  // run action 可选 slug:在 name 后追加 · slug(accent 色)
340
- const slug = typeof args.slug === "string" && args.slug.trim()
341
- ? `${theme.fg("dim", " · ")}${theme.fg("accent", String(args.slug))}`
342
- : "";
343
- const runId = args.runId ? ` ${String(args.runId).slice(0, RUNID_SHORT)}` : "";
340
+ const slug = optionSlugSuffix(args.slug, theme);
341
+ const runId = args.runId ? ` ${String(args.runId).slice(0, ID_PREVIEW_LENGTH)}` : "";
344
342
  return new Text(
345
343
  theme.fg("toolTitle", theme.bold("workflow ")) +
346
344
  theme.fg("muted", action) +
@@ -353,7 +351,7 @@ export function registerWorkflowTool(
353
351
  },
354
352
 
355
353
  renderResult(result: { content?: Array<{ type: string; text?: string }> }, _options: unknown, _theme: Theme, _context?: unknown) {
356
- return new Text(renderTextFallback(result), 0, 0);
354
+ return renderTextResult(result);
357
355
  },
358
356
  });
359
357
  }
@@ -364,7 +362,7 @@ export async function actionRun(
364
362
  params: WorkflowToolParams,
365
363
  deps: LauncherDeps,
366
364
  signal: AbortSignal | undefined,
367
- ): Promise<ToolResult> {
365
+ ): Promise<WorkflowExecuteResult> {
368
366
  const name = params.name;
369
367
  if (!name) {
370
368
  throw new Error("run requires 'name' parameter (absolute .js path from <available_workflows> <location>). Correct: {\"action\":\"run\",\"name\":\"<ref>\",\"args\":{...}}");
@@ -391,10 +389,7 @@ export async function actionRun(
391
389
  // 模糊匹配建议。throw(W4):pi 只对 execute throw 置 isError:true,
392
390
  // 返回值里的 isError 被 agent-loop 丢弃(agent-loop.js:453-483)——文案原样进 toolResult。
393
391
  const all = await deps.registry.loadAll();
394
- const available = all.filter((wf) => wf.available);
395
- const suggestions = available
396
- .map((wf) => ` - ${wf.name}: ${wf.meta.description || "(no description)"}\n location: ${wf.path}`)
397
- .join("\n");
392
+ const suggestions = formatAvailableWorkflowList(all);
398
393
  // [按名解析自救指引] 摘要逐条附绝对路径 location:run 的 name 形参最贴近的
399
394
  // 读取面就是本清单(<available_workflows> 注入面在 start 时已过时/可能不在
400
395
  // 上下文)——模型按清单里的名字重试(8.6.0 实装 getPath-only 时代的实测失败
@@ -429,42 +424,28 @@ export async function actionRun(
429
424
  );
430
425
  }
431
426
  // slug 运行时护栏(与 subagent startHandler 对称的纵深防御;schema maxLength 是第一道关卡)
432
- if (params.slug !== undefined && params.slug.length > SLUG_MAX_LENGTH) {
433
- throw new Error(
434
- `slug exceeds ${SLUG_MAX_LENGTH} chars (got ${params.slug.length}). Shorten to a kebab-case label, e.g. "fix-login", "extract-urls".`,
435
- );
436
- }
427
+ assertSlugWithinLimit(params.slug, ["fix-login", "extract-urls"]);
437
428
  const args = params.args ?? {};
438
429
  const tokens = params.tokens;
439
430
  const time = params.time;
440
431
  // OR-1 入口 fail-fast(crash-forensics-and-watchdog.md 附录 E(原 unbounded-wait-audit §7.2 T3①)):schema 的 time 是
441
432
  // Type.Number 直通(无上界)——超 setTimeout 安全域的值会穿透到 lifecycle 内层
442
- // 防线(assertSafeTimerDelay),而入口拦截让它永不进入副作用链。错误带合法上限
443
- // 与实际传入值,LLM 可据消息自纠(clamp 或省略走 unlimited 语义)。
444
- if (time !== undefined && time > MAX_TIMER_DELAY_MS) {
445
- throw new Error(
446
- `time budget ${time} ms exceeds the maximum of ${MAX_TIMER_DELAY_MS} ms (~24.8 days). ` +
447
- `Retry with a smaller "time", or omit it for unlimited.`,
448
- );
449
- }
433
+ // 防线(assertSafeTimerDelay),而入口拦截让它永不进入副作用链(判定与文案单点在
434
+ // core shared/entry-guards;LLM 可据消息自纠:clamp 或省略走 unlimited 语义)。
435
+ assertEntryTimeBudget(time);
450
436
 
451
437
  // 构建 RunSpec + 启动(m3:parameters 从 script.meta 拷贝——chokepoint 校验用;
452
438
  // 校验失败 → ArgsValidationError 直接 throw 给 pi(W4:err.message 含 §5.3 指引,
453
439
  // pi catch 后原文案进 toolResult content 并置 isError:true),其他错误保持传播)
454
440
  const runId = await runWorkflow(
455
- {
456
- scriptSource: script.toExecutable(),
441
+ buildRunSpecFromScript(script, {
457
442
  args,
458
443
  budgetTokens: tokens,
459
444
  budgetTimeMs: time,
460
- scriptName: script.name,
461
445
  slug: params.slug,
462
- scriptPath: script.path,
463
- description: script.meta.description,
464
- parameters: script.meta.parameters,
465
446
  model: params.model,
466
447
  thinkingLevel: params.thinkingLevel,
467
- },
448
+ }),
468
449
  deps,
469
450
  signal,
470
451
  );
@@ -474,8 +455,8 @@ export async function actionRun(
474
455
  {
475
456
  type: "text",
476
457
  text: params.slug
477
- ? `Started workflow '${script.name}' · ${params.slug} (${runId}). Running in background — do NOT poll status.`
478
- : `Started workflow '${script.name}' (${runId}). Running in background — do NOT poll status.`,
458
+ ? `Started workflow '${script.name}' · ${params.slug} (${runId}). Running in background — DO NOT bash sleep or poll status; results are auto-delivered via notifyDone.`
459
+ : `Started workflow '${script.name}' (${runId}). Running in background — DO NOT bash sleep or poll status; results are auto-delivered via notifyDone.`,
479
460
  },
480
461
  ],
481
462
  details: { action: "run", runId, status: "running", name: script.name, slug: params.slug, stateFile: deps.store.stateFilePath(runId) },
@@ -485,7 +466,7 @@ export async function actionRun(
485
466
 
486
467
  // ── status action ────────────────────────────────────────────
487
468
 
488
- function actionStatus(deps: LauncherDeps): ToolResult {
469
+ function actionStatus(deps: LauncherDeps): WorkflowExecuteResult {
489
470
  const runs = Array.from(deps.runs.values());
490
471
  if (runs.length === 0) {
491
472
  return {
@@ -499,7 +480,7 @@ function actionStatus(deps: LauncherDeps): ToolResult {
499
480
  // now 基准),不再随每次 status 查询的墙钟增长。
500
481
  const duration = s.startedAt ? ` (${formatRunStatusElapsed(s.startedAt, s.completedAt)})` : "";
501
482
  const reasonSuffix = s.reason && s.reason !== "completed" ? ` [${s.reason}]` : "";
502
- return `[${s.status}${reasonSuffix}] ${s.name} (${s.runId.slice(0, RUNID_SHORT)})${duration}${s.error ? ` error: ${s.error}` : ""}`;
483
+ return `[${s.status}${reasonSuffix}] ${s.name} (${s.runId.slice(0, ID_PREVIEW_LENGTH)})${duration}${s.error ? ` error: ${s.error}` : ""}`;
503
484
  });
504
485
  return {
505
486
  content: [{ type: "text", text: lines.join("\n") }],
@@ -515,7 +496,7 @@ async function actionLifecycle(
515
496
  action: "abort",
516
497
  params: WorkflowToolParams,
517
498
  deps: LauncherDeps,
518
- ): Promise<ToolResult> {
499
+ ): Promise<WorkflowExecuteResult> {
519
500
  const runId = params.runId;
520
501
  if (!runId) {
521
502
  throw new Error(`'runId' is required for ${action}. Correct: {"action":"${action}","runId":"<id>"} (use action:"status" to find runId)`);