@zhushanwen/pi-subagent-workflow 8.14.7 → 9.0.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 (58) hide show
  1. package/README.md +6 -5
  2. package/package.json +11 -7
  3. package/relay/relay.mjs +45 -2
  4. package/skills/workflow-script-format/SKILL.md +2 -56
  5. package/src/host/__tests__/inflight-reporter.test.ts +26 -1
  6. package/src/host/__tests__/pi-host.test.ts +418 -4
  7. package/src/host/inflight-reporter.ts +13 -10
  8. package/src/host/pi-host.ts +205 -17
  9. package/src/index.ts +21 -72
  10. package/src/injectors/__tests__/engine-awareness.test.ts +236 -33
  11. package/src/injectors/__tests__/helpers/injector-test-mocks.ts +15 -7
  12. package/src/injectors/__tests__/workflow-list-injector.test.ts +447 -40
  13. package/src/injectors/resource-list-injector.ts +103 -32
  14. package/src/injectors/subagent-list-injector.ts +4 -28
  15. package/src/injectors/workflow-list-injector.ts +8 -4
  16. package/src/interface/__tests__/capture-tool.ts +56 -0
  17. package/src/interface/__tests__/commands-resume.test.ts +103 -0
  18. package/src/interface/__tests__/tool-prompt-contract.test.ts +361 -0
  19. package/src/interface/__tests__/tool-render.test.ts +105 -2
  20. package/src/interface/__tests__/tool-subagents.test.ts +24 -26
  21. package/src/interface/__tests__/tool-workflow-resume.test.ts +227 -0
  22. package/src/interface/__tests__/tool-workflow-script.test.ts +557 -0
  23. package/src/interface/__tests__/tool-workflow.test.ts +884 -0
  24. package/src/interface/bg-notify-render.ts +18 -9
  25. package/src/interface/command-actions.ts +22 -17
  26. package/src/interface/commands.ts +58 -12
  27. package/src/interface/format.ts +50 -16
  28. package/src/interface/id-preview.ts +4 -4
  29. package/src/interface/subagent-actions.ts +18 -50
  30. package/src/interface/subagent-tool.ts +26 -18
  31. package/src/interface/subagents.ts +5 -11
  32. package/src/interface/tool-render.ts +14 -30
  33. package/src/interface/tool-shared.ts +55 -12
  34. package/src/interface/tool-subagents.ts +21 -11
  35. package/src/interface/tool-workflow-script.ts +52 -72
  36. package/src/interface/tool-workflow.ts +345 -139
  37. package/src/interface/views/WorkflowsView.ts +10 -7
  38. package/src/interface/views/__tests__/{WorkflowsView-signature.test.ts → WorkflowsView.test.ts} +155 -11
  39. package/src/interface/views/detail-content.ts +3 -5
  40. package/src/jsonl-run-store.ts +656 -544
  41. package/src/model-events.ts +35 -0
  42. package/src/session-lifecycle.ts +376 -85
  43. package/src/subagents-events.ts +56 -0
  44. package/src/workflow-events.ts +285 -200
  45. package/src/workflow-notify.ts +456 -0
  46. package/src/injectors/__tests__/engine-section-stability.test.ts +0 -353
  47. package/src/injectors/__tests__/model-list-injector.test.ts +0 -136
  48. package/src/injectors/__tests__/subagent-list-injector.test.ts +0 -376
  49. package/src/interface/__tests__/detectors.test.ts +0 -161
  50. package/src/interface/__tests__/subagent-tool-prompt.test.ts +0 -183
  51. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +0 -219
  52. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +0 -251
  53. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +0 -215
  54. package/src/interface/__tests__/workflow-state-file-exposure.test.ts +0 -38
  55. package/src/interface/__tests__/workflow-tool-prompt.test.ts +0 -129
  56. package/src/interface/helpers.ts +0 -272
  57. package/src/interface/views/__tests__/detail-content-session-file.test.ts +0 -73
  58. package/src/interface/views/__tests__/record-progress.test.ts +0 -235
@@ -3,11 +3,12 @@
3
3
  // `subagent` LLM 工具。薄壳——参数解析 + 调 runtime.execute。
4
4
  // 不创建 state、不节流 onUpdate、不持久化(全部在 runtime 层统一)。
5
5
  //
6
- // 设计说明:renderCall/renderResult/execute 三个回调均抽成模块级 const +
7
- // 顶层 type alias。原因:stub 的 registerTool(tool: unknown) 参数是 unknown,
8
- // 在其对象字面量内直接标注从 pi-coding-agent 导入的泛型(AgentToolResult<X>、
9
- // Theme、ExtensionContext)会触发 TS2307 误报(probe5d/5f 验证)。
10
- // 抽到顶层后参数类型由 alias 提供,绕过该 quirk。
6
+ // 设计说明:execute / renderCall 两个回调抽成模块级 const + 顶层 type alias。
7
+ // 原因:stub 的 registerTool(tool: unknown) 参数是 unknown,在其对象字面量内直接
8
+ // 标注从 pi-coding-agent 导入的泛型(AgentToolResult<X>、Theme、ExtensionContext)
9
+ // 会触发 TS2307 误报(probe5d/5f 验证)。抽到顶层后参数类型由 alias 提供,绕过
10
+ // 该 quirk。renderResult 直接引用 tool-render.ts 导出的 renderSubagentResult
11
+ // 函数值——值引用不涉及类型标注,不受该 quirk 影响。
11
12
 
12
13
  import { isAbsolute } from "node:path";
13
14
 
@@ -18,9 +19,17 @@ import type { Static } from "typebox";
18
19
 
19
20
  import { getSubagentService } from "@zhushanwen/subagent-core";
20
21
  import type { SubagentToolResult } from "@zhushanwen/subagent-core";
22
+ import {
23
+ cancelHandler,
24
+ closeHandler,
25
+ forkFromHandler,
26
+ listHandler,
27
+ messageHandler,
28
+ startHandler,
29
+ } from "@zhushanwen/subagent-core";
21
30
  import { extractAgentName } from "./format.ts";
22
31
  import { toGuiCtx } from "./gui-mappers.ts";
23
- import { adapter, cancelHandler, closeHandler, forkFromHandler, listHandler, messageHandler, startHandler } from "./subagent-actions.ts";
32
+ import { adapter } from "./subagent-actions.ts";
24
33
  import { SubagentParams } from "./subagent-tool-schema.ts";
25
34
  import { type RenderContext,renderSubagentCall, renderSubagentResult } from "./tool-render.ts";
26
35
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
@@ -48,13 +57,6 @@ type SubagentExecuteCb = (
48
57
 
49
58
  type SubagentRenderCallCb = (args: unknown, theme: Theme, ctx: RenderContext) => Component;
50
59
 
51
- type SubagentRenderResultCb = (
52
- result: AgentToolResult<SubagentToolResult>,
53
- options: { expanded: boolean; isPartial: boolean },
54
- theme: Theme,
55
- ctx: RenderContext,
56
- ) => Component;
57
-
58
60
  // ============================================================
59
61
  // renderCall 预解析 helper
60
62
  // ============================================================
@@ -221,7 +223,7 @@ Do NOT recurse when: the work is linear/flat; the child needs your context; or y
221
223
  executionMode: "sequential",
222
224
  parameters: SubagentParams,
223
225
  renderCall: subagentRenderCall,
224
- renderResult: subagentRenderResult,
226
+ renderResult: renderSubagentResult,
225
227
  execute: executeSubagent,
226
228
  });
227
229
  }
@@ -260,9 +262,6 @@ const subagentRenderCall: SubagentRenderCallCb = (args, theme, ctx) => {
260
262
  return renderSubagentCall(args, theme, ctx, resolved);
261
263
  };
262
264
 
263
- const subagentRenderResult: SubagentRenderResultCb = (result, options, theme, ctx) =>
264
- renderSubagentResult(result, options, theme, ctx);
265
-
266
265
  /**
267
266
  * execute 实现(action 路由 + adapter)。
268
267
  *
@@ -295,7 +294,16 @@ const executeSubagent: SubagentExecuteCb = async (
295
294
  // background 模式:execute 立即返回,detached 运行不向 tool 层回流 onUpdate
296
295
  //(完成由 notify 驱动新 turn)。onUpdate 参数保留以兼容 SDK 回调签名,但不消费。
297
296
  const service = getSubagentService();
298
- if (!service) throw new Error("subagents runtime not initialized");
297
+ if (!service) {
298
+ // [C2] 错误带恢复动作(对齐 workflow-events getWorkflowDeps 的 store unavailable
299
+ // 闭环风格):service 缺席的 root cause 是 session_start 装配链失败(围栏 catch
300
+ // 只留 extension 日志),此处指引 reload + 查日志,接通「现象 → 根因」链路。
301
+ throw new Error(
302
+ "subagents runtime not initialized (session_start assembly failed). " +
303
+ "Recovery: reload this session or restart pi to re-run initialization, " +
304
+ "and check the subagents extension logs (session_start failure) for the root cause.",
305
+ );
306
+ }
299
307
 
300
308
  // typebox v1 的 StringEnum 在 Static 投影下退化为 string,此处类型守卫收窄回
301
309
  // 字面量联合,恢复 switch 的 exhaustiveness 约束(default 分支 = never)。
@@ -13,23 +13,17 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
13
13
  import { getSubagentService } from "@zhushanwen/subagent-core";
14
14
  import type { SubagentService } from "@zhushanwen/subagent-core";
15
15
  import { displayAgentName } from "@zhushanwen/subagent-core";
16
- import { messageHandler, startHandler } from "./subagent-actions.ts";
16
+ import { messageHandler, startHandler } from "@zhushanwen/subagent-core";
17
+ import { SUBAGENT_DIRECTIVE_CUSTOM_TYPE } from "@zhushanwen/extension-protocol";
17
18
  import { parseSubagentRpcCommand } from "./command-actions.ts";
18
19
  import type { SubagentRpcAction } from "./command-actions.ts";
19
20
  import { LIST_LIMIT } from "./list-shared.ts";
20
21
  import { createSubagentsView } from "./list-view.ts";
21
22
  import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
22
23
 
23
- /**
24
- * subagent-directive custom_message 的 customType。
25
- *
26
- * 定向消息留痕载体(设计 §3.3.3):message/start 成功派发后落主 session 的
27
- * custom_message entry,一 entry 双消费——
28
- * 1. 主 agent 上下文(custom_message 进 context,主 agent 下次 turn 可见定向对话)
29
- * 2. renderer 定向气泡渲染源(§3.3.3a live/reload 双链路,后续 wave 消费)
30
- * 字段形状是 GUI 契约,改动需与 renderer 侧同步。
31
- */
32
- export const SUBAGENT_DIRECTIVE_CUSTOM_TYPE = "subagent-directive";
24
+ // subagent-directive customType 经 extension-protocol 单源(与 shared/runtime 消费侧
25
+ // 同源;等值锁 = src/__tests__/contract.notify-custom-types.test.ts)。定向消息留痕
26
+ // 载体语义见下方 emitSubagentDirective 注释。
33
27
 
34
28
  /** subagent-directive entry 的 details 形状(GUI 定向气泡渲染契约)。 */
35
29
  export interface SubagentDirectiveDetails {
@@ -32,8 +32,8 @@ import {
32
32
  formatElapsedSeconds,
33
33
  statusGlyph,
34
34
  type ThemeLike,
35
- truncLine,
36
35
  } from "./format.ts";
36
+ import { truncLine } from "./tui-kit.ts";
37
37
 
38
38
  // ============================================================
39
39
  // 常量
@@ -65,14 +65,14 @@ function getTermWidth(): number {
65
65
  // ============================================================
66
66
 
67
67
  /**
68
- * renderResult 的 context(SDK ToolRenderContext 的有意子集——只读 state/invalidate)。
69
- * SDK 实际传入更完整的 { args, toolCallId, cwd, executionStarted, argsComplete, isPartial,
70
- * expanded, showImages, isError, lastComponent, ... },本组件结构兼容只取需要的字段。
68
+ * renderResult 的 context(SDK ToolRenderContext 的有意子集——结构兼容只声明
69
+ * 本组件需要的字段)。SDK 实际传入更完整的 { args, toolCallId, cwd,
70
+ * executionStarted, argsComplete, isPartial, expanded, showImages, isError,
71
+ * lastComponent, ... }。
71
72
  *
72
73
  * 注意:不使用 lastComponent。每次 renderResult 返回新 Container(参照 nicobailon)。
73
74
  */
74
75
  export interface RenderContext {
75
- state: Record<string, never>;
76
76
  invalidate(): void;
77
77
  }
78
78
 
@@ -228,7 +228,10 @@ function buildCompactLines(d: SubagentToolResult, theme: ThemeLike): string[] {
228
228
  }
229
229
 
230
230
  /**
231
- * 展开视图行内容生成。完整 eventLog + 交付物。
231
+ * 展开视图行内容生成。除下列两个特例外与 compact 逐字节相同(一次性 block 无
232
+ * 细节可展开),测试锁字节等价:
233
+ * - list:compact 结果之上每 item 追加 sessionFile 路径行(renderListExpanded)
234
+ * - fork-from:源文件用完整路径(compact 用 basename 短标签)
232
235
  */
233
236
  function buildExpandedLines(d: SubagentToolResult, theme: ThemeLike): string[] {
234
237
  const width = getTermWidth();
@@ -236,34 +239,15 @@ function buildExpandedLines(d: SubagentToolResult, theme: ThemeLike): string[] {
236
239
  if (d.action === "list" && d.listResponse) {
237
240
  return renderListExpanded(d.listResponse, theme, width);
238
241
  }
239
- if (d.action === "cancel" && d.cancelResponse) {
242
+ if (d.action === "fork-from" && d.forkFromResponse) {
240
243
  return [truncLine(
241
- `${theme.fg("muted", "■")} ${theme.fg("dim", "cancelled ")}${theme.fg("accent", d.subagentId ?? "?")}`,
244
+ `${theme.fg("accent", "⑂")} ${theme.fg("dim", "forked-from ")}${theme.fg("accent", d.forkFromResponse.sourceSessionFile)}` +
245
+ ` ${theme.fg("dim", "→ new: ")}${theme.fg("accent", d.subagentId ?? "?")}`,
242
246
  width,
243
247
  )];
244
248
  }
245
- if (d.action === "fork-from" && d.forkFromResponse) {
246
- // expanded 与 compact 同形(一次性 block 无细节可展开),另附源文件完整路径行。
247
- return [
248
- truncLine(
249
- `${theme.fg("accent", "⑂")} ${theme.fg("dim", "forked-from ")}${theme.fg("accent", d.forkFromResponse.sourceSessionFile)}` +
250
- ` ${theme.fg("dim", "→ new: ")}${theme.fg("accent", d.subagentId ?? "?")}`,
251
- width,
252
- ),
253
- ];
254
- }
255
-
256
- const lines: string[] = [];
257
- // bg 占位 expanded 与 compact 同(一次性 block 无细节可展开)
258
- if ("bgResponse" in d) {
259
- lines.push(truncLine(
260
- `${theme.fg("accent", "●")} ${theme.fg("dim", "background: ")}${theme.fg("accent", d.subagentId ?? "?")}`
261
- + ` ${theme.fg("dim", "· detached")}`,
262
- width,
263
- ));
264
- return lines;
265
- }
266
- return [truncLine(theme.fg("warning", "(subagent: no response)"), width)];
249
+ // cancel / start(bg) / fallback:expanded 与 compact 逐字节相同
250
+ return buildCompactLines(d, theme);
267
251
  }
268
252
 
269
253
  // ============================================================
@@ -4,8 +4,16 @@
4
4
  * 抽取边界(findings g11a-F1/F2/F3):
5
5
  * - `assertNotAborted` / `optionSlugSuffix` / `renderTextResult`:三处逐字重复的
6
6
  * 入口前置与渲染片段(同粒度小函数)。
7
- * - `buildRunSpecFromScript` / `formatAvailableWorkflowList`:RunSpec 组装字面量与
8
- * 「可用脚本清单」串——RunSpec 是 core 启动契约,两份字面量漏改即静默丢字段。
7
+ * - `buildRunSpecFromScript`:RunSpec 组装字面量——RunSpec 是 core 启动契约,
8
+ * 字面量漏改即静默丢字段。
9
+ * - `withGuiAttach`:RPC 模式给 details 附加 `__gui__` 的唯一实现(workflow-script /
10
+ * subagent 两个 tool 的 attach 点收敛于此,组件构造回调由调用方提供;workflow /
11
+ * subagents 走 isWorkflow 块分支不消费 `__gui__`,不构造——D8)。
12
+ * - `throwPrefixed`:catch 内 `<前缀>: <msg>` 重抛(pi 的 execute-throw 契约,
13
+ * toErrorMessage 规整单点)。
14
+ *
15
+ * 「可用脚本清单」串不在本文件:已并入 core 单源
16
+ *(@zhushanwen/subagent-core 的 formatAvailableWorkflowRefs,经 barrel 消费)。
9
17
  *
10
18
  * **不**把 execute 包成 HOF:reentry-guard.ts 文件头已裁决(HOF 包装会破坏 union
11
19
  * 返回类型推断),本文件只放同粒度小函数,guard 的 check → try/finally release
@@ -17,7 +25,10 @@
17
25
  import type { Theme } from "@earendil-works/pi-coding-agent";
18
26
  import { Text } from "@earendil-works/pi-tui";
19
27
 
28
+ import type { GuiComponent, GuiContext, GuiRenderResult } from "@zhushanwen/extension-protocol";
29
+ import { guiResult, isGuiCapable } from "@zhushanwen/extension-protocol";
20
30
  import type { RunSpec, WorkflowScript } from "@zhushanwen/subagent-core";
31
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
21
32
  import { renderTextFallback } from "./format.ts";
22
33
 
23
34
  /** renderResult 回调的宽入参形态(content 可缺省,由 renderTextFallback 兜底)。 */
@@ -93,17 +104,49 @@ export function buildRunSpecFromScript(
93
104
  };
94
105
  }
95
106
 
107
+ /** withGuiAttach 的 details 约束:已声明 `__gui__?`(消费 attach 的 details union 均满足)。 */
108
+ interface GuiAttachableDetails {
109
+ __gui__?: GuiRenderResult;
110
+ }
111
+
112
+ /**
113
+ * GUI attach 单点:RPC 模式(isGuiCapable,仅 mode==="rpc")下为 details 附加
114
+ * `__gui__`,其余模式原样返回;details 为 undefined 时原样返回(过载签名区分
115
+ * 可空/非空入参,非空入参的返回不带 undefined——调用方无需收窄)。
116
+ *
117
+ * 组件构造是各 tool 特有逻辑,经 build 回调注入——回调只在 attach 分支被调用
118
+ * (非 RPC 模式零构造成本)。union 各成员已声明 `__gui__?`,spread + 补字段类型
119
+ * 安全,无需强转。
120
+ */
121
+ export function withGuiAttach<Details extends GuiAttachableDetails>(
122
+ details: Details,
123
+ ctx: GuiContext | undefined,
124
+ build: (details: Details) => GuiComponent,
125
+ ): Details;
126
+ export function withGuiAttach<Details extends GuiAttachableDetails>(
127
+ details: Details | undefined,
128
+ ctx: GuiContext | undefined,
129
+ build: (details: Details) => GuiComponent,
130
+ ): Details | undefined;
131
+ export function withGuiAttach<Details extends GuiAttachableDetails>(
132
+ details: Details | undefined,
133
+ ctx: GuiContext | undefined,
134
+ build: (details: Details) => GuiComponent,
135
+ ): Details | undefined {
136
+ if (!details) return details;
137
+ if (ctx && isGuiCapable(ctx)) {
138
+ return { ...details, __gui__: guiResult(build(details)) };
139
+ }
140
+ return details;
141
+ }
142
+
96
143
  /**
97
- * 「可用脚本清单」串(无可用项 → 空串,调用方自行补 `|| " (none)"`)。
144
+ * catch 内重抛单点:`<prefix>: <msg>`(msg 经 toErrorMessage 规整)。
98
145
  *
99
- * 每项两行(name + description,缩进 location 绝对路径)——弱模型按清单里的名字
100
- * / 路径重试的自救主路径,两个 tool 的文案必须同源。
146
+ * pi 只对 execute throw 置 isError:true——返回值里的 isError 被 agent-loop 丢弃
147
+ * (agent-loop.js:453-483),错误一律 throw,本函数是四个 catch 点共享的唯一包装
148
+ * 形态;前缀是 LLM 可见文案,调用方逐字保持既有形态。
101
149
  */
102
- export function formatAvailableWorkflowList(all: readonly WorkflowScript[]): string {
103
- return all
104
- .filter((wf) => wf.available)
105
- .map(
106
- (wf) => ` - ${wf.name}: ${wf.meta.description || "(no description)"}\n location: ${wf.path}`,
107
- )
108
- .join("\n");
150
+ export function throwPrefixed(prefix: string, err: unknown): never {
151
+ throw new Error(`${prefix}: ${toErrorMessage(err)}`);
109
152
  }
@@ -10,6 +10,11 @@
10
10
  * - 唯一批量入口:N 个已知独立任务一次派发;handler 确定性转译
11
11
  * runWorkflow("fan-out")(执行管道唯一——collect 时代的批协调状态已退役,
12
12
  * 不存在第二套批机制)。
13
+ * - 批量派发的等价性契约:tasks[] 经 handler 确定性转译为内置 fan-out 模板脚本,
14
+ * 走 runWorkflow 完整管道执行——模板仅由 tasks[] 确定性生成、不含任何批协调
15
+ * 状态,执行管道唯一(不存在第二套批机制);走管道使批量派发继承 workflow 的
16
+ * 持久化、journal、GUI 投影与断点恢复(resume)全套能力,这是刻意保留间接层
17
+ * 的原因。
13
18
  * - 无 action 分发:status/abort 不复制,直接指路 workflow tool(runId 同体系)。
14
19
  * - 无 args 嵌套:tasks/agents/aggregate/... 全在顶层(弱模型信任 schema 结构信号,
15
20
  * 两跳转译是事故高发区——对照 workflow tool 的 name+args 形态)。
@@ -18,7 +23,7 @@
18
23
  * - 不构造 `details.__gui__`:GUI 挂载按 WORKFLOW_TOOL_NAMES 集合分流,批量块走
19
24
  * workflow 块分支(恒折叠单行 + openWorkflowDrawer);`__gui__` 的渲染点在普通
20
25
  * tool 分支(v-else)的展开区内,isWorkflow 分支无展开路径不消费——构造即死代码
21
- * (D8 裁决;workflow tool 现状构造 `__gui__` 但块面同样不消费,本工具不复制该漂移)。
26
+ * (D8 裁决;workflow tool 的 `__gui__` 死构造已随之删除,isWorkflow 集合内工具零消费)。
22
27
  *
23
28
  * 层归属:Interface。依赖 Pi SDK + core lifecycle/registry + reentry-guard。
24
29
  */
@@ -30,7 +35,13 @@ import { type Static, Type } from "typebox";
30
35
 
31
36
  import { MAX_TIMER_DELAY_MS, SLUG_MAX_LENGTH, THINKING_ORDER } from "@zhushanwen/subagent-core";
32
37
  import type { LauncherDeps } from "@zhushanwen/subagent-core";
33
- import { assertEntryTimeBudget, assertSlugWithinLimit, runWorkflow } from "@zhushanwen/subagent-core";
38
+ // formatAvailableWorkflowRefs:拒单可用清单单源(core launcher,副本已并入)。
39
+ import {
40
+ assertEntryTimeBudget,
41
+ assertSlugWithinLimit,
42
+ formatAvailableWorkflowRefs,
43
+ runWorkflow,
44
+ } from "@zhushanwen/subagent-core";
34
45
  import {
35
46
  acquireReentryGuard,
36
47
  REENTRY_BUSY_MESSAGE,
@@ -40,7 +51,6 @@ import {
40
51
  import {
41
52
  assertNotAborted,
42
53
  buildRunSpecFromScript,
43
- formatAvailableWorkflowList,
44
54
  optionSlugSuffix,
45
55
  renderTextResult,
46
56
  } from "./tool-shared.ts";
@@ -163,11 +173,12 @@ export async function runSubagentsBatch(
163
173
  signal: AbortSignal | undefined,
164
174
  ): Promise<SubagentsExecuteResult> {
165
175
  // D9:tasks 缺失/空数组 → 入口 throw(pi 只对 execute throw 置 isError:true)。
166
- // 文案带 Correct 示例:弱模型照抄即可自纠。
176
+ // 文案与其他 tool 的必填参数拒单同模板(<subject> requires '<param>' parameter.
177
+ // Correct: <最小正确调用例>):本工具无 action,主语用工具名。
167
178
  const tasks = params.tasks;
168
179
  if (!Array.isArray(tasks) || tasks.length === 0) {
169
180
  throw new Error(
170
- 'tasks is required (non-empty string array). Correct: {"tasks":["...","..."]}',
181
+ 'subagents requires \'tasks\' parameter (non-empty string array). Correct: {"tasks":["...","..."]}',
171
182
  );
172
183
  }
173
184
 
@@ -184,12 +195,13 @@ export async function runSubagentsBatch(
184
195
  const time = params.time;
185
196
  assertEntryTimeBudget(time);
186
197
 
187
- // 执行体脚本按内置名解析(与 workflow tool actionRun 同一条链:registry.get 命中
188
- // 内置/已保存名;本工具不允许换脚本,故不回落 getPath)。
198
+ // 执行体脚本按固定内置名解析(registry.get 精确名匹配;脚本名是工具自带常量
199
+ // FAN_OUT_SCRIPT_NAME 而非用户参数——workflow tool actionRun 的按名解析已退役
200
+ // 走 getPath 单通道,本工具不允许换脚本,无 getPath 回落需求)。
189
201
  const script = await deps.registry.get(FAN_OUT_SCRIPT_NAME);
190
202
  if (!script || !script.available) {
191
203
  const all = await deps.registry.loadAll();
192
- const available = formatAvailableWorkflowList(all);
204
+ const available = formatAvailableWorkflowRefs(all);
193
205
  throw new Error(
194
206
  `Built-in workflow '${FAN_OUT_SCRIPT_NAME}' is not available — the subagents tool runs it as its batch body. ` +
195
207
  `Recovery: verify the @zhushanwen/subagent-core package ships workflows/${FAN_OUT_SCRIPT_NAME}.js (reinstall/repair it), then retry. ` +
@@ -303,8 +315,6 @@ export function registerSubagentsTool(
303
315
  );
304
316
  },
305
317
 
306
- renderResult(result: { content?: Array<{ type: string; text?: string }> }, _options: unknown, _theme: Theme, _context?: unknown) {
307
- return renderTextResult(result);
308
- },
318
+ renderResult: renderTextResult,
309
319
  });
310
320
  }
@@ -14,7 +14,8 @@
14
14
  * generateWorkflowScript(报错文案逐字平移,CA2 前提);save/delete 改调 core
15
15
  * barrel(第三可选目录参数缺省即 pi 布局 .pi/workflows——pi 现两参调用形态零变化)。
16
16
  * 结构化 {ok}|{error} → pi 的 execute-throw 契约转换在本层(pi 只对 execute throw
17
- * 置 isError:true);AbortSignal 的 aborted 检查留宿主层(C4 偏差 #4 已声明)。
17
+ * 置 isError:true);AbortSignal 的 aborted 检查在 execute 入口统一拦截(与
18
+ * workflow / subagents 两 tool 同源,tool-shared assertNotAborted;C4 偏差 #4)。
18
19
  *
19
20
  * 层归属:Interface。依赖 Pi SDK + engine script-lint + infra workflow-files。
20
21
  */
@@ -26,23 +27,21 @@ import { type Static, Type } from "typebox";
26
27
 
27
28
  import {
28
29
  guiComponent,
29
- type GuiContext,
30
30
  type GuiRenderResult,
31
- guiResult,
32
- isGuiCapable,
33
31
  } from "@zhushanwen/extension-protocol";
34
32
  // C5②/C5⑦:创作闭环统一走 core barrel(generateWorkflowScript/saveWorkflow/
35
33
  // deleteWorkflow/lintScript 均为 barrel 导出面;深路径在 npm/vendored 形态不可达)
36
34
  import {
37
35
  deleteWorkflow,
36
+ formatAvailableWorkflowRefs,
38
37
  generateWorkflowScript,
39
38
  lintScript,
40
39
  saveWorkflow,
41
40
  } from "@zhushanwen/subagent-core";
42
41
  import type { WorkflowScriptRegistry } from "@zhushanwen/subagent-core";
43
42
  import { toGuiCtx } from "./gui-mappers.ts";
44
- import { assertNotAborted, renderTextResult } from "./tool-shared.ts";
45
- import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
43
+ import type { WorkflowToolResult } from "./tool-result.ts";
44
+ import { assertNotAborted, renderTextResult, throwPrefixed, withGuiAttach } from "./tool-shared.ts";
46
45
 
47
46
  // ── Parameter schema ─────────────────────────────────────────
48
47
 
@@ -82,17 +81,13 @@ export type WorkflowScriptToolDetails =
82
81
  | { action: "save"; name: string; ok: boolean; __gui__?: GuiRenderResult }
83
82
  | { action: "delete"; name: string; ok: boolean; __gui__?: GuiRenderResult };
84
83
 
85
- /** Result returned by the `workflow-script` tool's execute. */
86
- export interface TextContent {
87
- content: Array<{ type: "text"; text: string }>;
88
- details: WorkflowScriptToolDetails | undefined;
89
- isError?: boolean;
90
- }
84
+ /** Result returned by the `workflow-script` tool's execute(公共骨架见 tool-result.ts)。 */
85
+ export type WorkflowScriptExecuteResult = WorkflowToolResult<WorkflowScriptToolDetails | undefined>;
91
86
 
92
87
  // ── GUI 协议 helpers ───────────────────────────────────────
93
88
 
94
89
  /**
95
- * 为 details 附加 __gui__(RPC 模式下)。
90
+ * 按 WorkflowScriptToolDetails 构造 stats-line GuiComponent。
96
91
  *
97
92
  * 所有 5 个 action 都映射到 stats-line(单行统计,无复杂结构):
98
93
  * - generate: 显示生成的脚本名
@@ -100,20 +95,6 @@ export interface TextContent {
100
95
  * - list: 脚本数量
101
96
  * - save/delete: ok/warn
102
97
  */
103
- function withScriptGui(
104
- result: TextContent,
105
- ctx?: GuiContext,
106
- ): TextContent {
107
- if (!ctx || !isGuiCapable(ctx) || !result.details) return result;
108
- const details = result.details;
109
- // union 各成员已声明 __gui__?,spread + 补字段类型安全,无需强转
110
- return {
111
- ...result,
112
- details: { ...details, __gui__: guiResult(buildScriptGui(details)) },
113
- };
114
- }
115
-
116
- /** 按 WorkflowScriptToolDetails 构造 stats-line GuiComponent。 */
117
98
  export function buildScriptGui(details: WorkflowScriptToolDetails) {
118
99
  switch (details.action) {
119
100
  case "generate":
@@ -199,11 +180,15 @@ export function registerWorkflowScriptTool(
199
180
  signal: AbortSignal | undefined,
200
181
  _onUpdate: unknown,
201
182
  ctx: ExtensionContext,
202
- ): Promise<TextContent> {
203
- let result: TextContent;
183
+ ): Promise<WorkflowScriptExecuteResult> {
184
+ // P1-2 abort 前置(三 tool 对称):5 个 action 统一在 execute 入口拦截——
185
+ // 原先只在 generate 内检查,lint/save/delete/list 四路径漏拦。throw(W4b):
186
+ // pi 只对 execute throw 置 isError:true,返回值里的 isError 被 agent-loop 丢弃。
187
+ assertNotAborted(signal);
188
+ let result: WorkflowScriptExecuteResult;
204
189
  switch (params.action) {
205
190
  case "generate":
206
- result = actionGenerate(params, signal);
191
+ result = actionGenerate(params);
207
192
  break;
208
193
  case "lint":
209
194
  result = await actionLint(params, registry);
@@ -222,8 +207,11 @@ export function registerWorkflowScriptTool(
222
207
  // isError:true,返回值里的 isError 被 agent-loop 丢弃(agent-loop.js:453-483)。
223
208
  throw new Error(`Unknown action: ${String(params.action)}`);
224
209
  }
225
- // GUI 协议:RPC 模式下附加 __gui__ 到 details
226
- return withScriptGui(result, toGuiCtx(ctx));
210
+ // GUI 协议:RPC 模式下附加 __gui__ 到 details(attach 单点在 tool-shared)
211
+ return {
212
+ ...result,
213
+ details: withGuiAttach(result.details, toGuiCtx(ctx), buildScriptGui),
214
+ };
227
215
  },
228
216
 
229
217
  renderCall(args: ScriptParams, theme: Theme, _context?: unknown) {
@@ -234,23 +222,16 @@ export function registerWorkflowScriptTool(
234
222
  return new Text(text, 0, 0);
235
223
  },
236
224
 
237
- renderResult(
238
- result: { content?: Array<{ type: string; text?: string }> },
239
- _options: unknown,
240
- _theme: Theme,
241
- _context?: unknown,
242
- ) {
243
- return renderTextResult(result);
244
- },
225
+ renderResult: renderTextResult,
245
226
  });
246
227
  }
247
228
 
248
229
  // ── generate action ──────────────────────────────────────────
249
230
 
250
- export function actionGenerate(params: ScriptParams, signal: AbortSignal | undefined): TextContent {
231
+ export function actionGenerate(params: ScriptParams): WorkflowScriptExecuteResult {
251
232
  // throw(W4b):pi 只对 execute throw 置 isError:true(返回值 isError 被丢弃)。
252
- // AbortSignal 是 pi tool 契约层关注——core 管线不含 signal 检查,宿主自留(C4 偏差 #4)
253
- assertNotAborted(signal);
233
+ // abort 前置在 execute 入口统一拦截(三 tool 同源,tool-shared assertNotAborted),
234
+ // 本函数不再自带 signal 检查。
254
235
  const name = params.name ?? "";
255
236
  const script = params.script ?? "";
256
237
 
@@ -281,18 +262,20 @@ export function actionGenerate(params: ScriptParams, signal: AbortSignal | undef
281
262
  async function actionLint(
282
263
  params: ScriptParams,
283
264
  registry: WorkflowScriptRegistry,
284
- ): Promise<TextContent> {
265
+ ): Promise<WorkflowScriptExecuteResult> {
285
266
  const name = params.name;
286
267
  if (!name) {
287
- throw new Error("lint requires 'name' parameter");
268
+ throw new Error(
269
+ "lint requires 'name' parameter. Correct: {\"action\":\"lint\",\"name\":\"<script>\"}",
270
+ );
288
271
  }
289
272
  const source = await loadScriptSource(name, registry);
290
273
  if (!source) {
274
+ // 清单 item 走 core formatAvailableWorkflowRefs 缺省形态(有意统一:与 run 拒单
275
+ // 一致带 location 行,自纠指引同构);"Available:" 标题与 (none) 空态是本调用点
276
+ // 的语境文案。
291
277
  const all = await registry.loadAll();
292
- const available = all.filter((wf) => wf.available);
293
- const suggestions = available
294
- .map((wf) => ` - ${wf.name}: ${wf.meta.description || "(no description)"}`)
295
- .join("\n");
278
+ const suggestions = formatAvailableWorkflowRefs(all);
296
279
  throw new Error(
297
280
  `Workflow '${name}' not found or not available.\nAvailable:\n${suggestions || " (none)"}`,
298
281
  );
@@ -334,10 +317,12 @@ async function loadScriptSource(
334
317
 
335
318
  // ── save action ──────────────────────────────────────────────
336
319
 
337
- async function actionSave(params: ScriptParams): Promise<TextContent> {
320
+ async function actionSave(params: ScriptParams): Promise<WorkflowScriptExecuteResult> {
338
321
  const name = params.name;
339
322
  if (!name) {
340
- throw new Error("save requires 'name' parameter (tmp script name)");
323
+ throw new Error(
324
+ "save requires 'name' parameter (tmp script name). Correct: {\"action\":\"save\",\"name\":\"<tmp-script>\"}",
325
+ );
341
326
  }
342
327
  try {
343
328
  const result = await saveWorkflow(name, params.newName);
@@ -346,10 +331,7 @@ async function actionSave(params: ScriptParams): Promise<TextContent> {
346
331
  details: { action: "save", name, ok: true },
347
332
  };
348
333
  } catch (err: unknown) {
349
- // throw(W4):pi 只对 execute throw 置 isError:true(返回值里的 isError 被
350
- // agent-loop 丢弃,agent-loop.js:453-483)——文案原样进 toolResult。
351
- const msg = toErrorMessage(err);
352
- throw new Error(`Save failed: ${msg}`);
334
+ throwPrefixed("Save failed", err);
353
335
  }
354
336
  }
355
337
 
@@ -359,10 +341,12 @@ function actionDelete(
359
341
  params: ScriptParams,
360
342
  registry: WorkflowScriptRegistry,
361
343
  isRunning: (name: string) => boolean,
362
- ): TextContent {
344
+ ): WorkflowScriptExecuteResult {
363
345
  const name = params.name;
364
346
  if (!name) {
365
- throw new Error("delete requires 'name' parameter");
347
+ throw new Error(
348
+ "delete requires 'name' parameter. Correct: {\"action\":\"delete\",\"name\":\"<script>\"}",
349
+ );
366
350
  }
367
351
  // deleteWorkflow 内部检查 isRunning(防止删运行中脚本)
368
352
  try {
@@ -374,32 +358,28 @@ function actionDelete(
374
358
  details: { action: "delete", name, ok: true },
375
359
  };
376
360
  } catch (err: unknown) {
377
- // throw(W4):同 save——pi 契约只有 throw 才置 isError:true。
378
- const msg = toErrorMessage(err);
379
- throw new Error(`Delete failed: ${msg}`);
361
+ throwPrefixed("Delete failed", err);
380
362
  }
381
363
  }
382
364
 
383
365
  // ── list action ──────────────────────────────────────────────
384
366
 
385
- async function actionList(registry: WorkflowScriptRegistry): Promise<TextContent> {
367
+ async function actionList(registry: WorkflowScriptRegistry): Promise<WorkflowScriptExecuteResult> {
386
368
  try {
387
369
  const all = await registry.loadAll();
388
- const available = all.filter((wf) => wf.available);
389
- if (available.length === 0) {
370
+ // item 行走 core formatAvailableWorkflowRefs(includeSource 保留 [saved]/[tmp]
371
+ // 标签、去 location 行、分隔符统一为 ":"——与拒单 item 模板单源);标题与空态
372
+ // 是本调用点的语境文案。
373
+ const lines = formatAvailableWorkflowRefs(all, { includeSource: true, includeLocation: false });
374
+ if (lines === "") {
390
375
  return textResult("No workflow scripts available.");
391
376
  }
392
- const lines = available.map(
393
- (wf) => ` [${wf.source}] ${wf.name} — ${wf.meta.description || "(no description)"}`,
394
- );
395
377
  return {
396
- content: [{ type: "text", text: `Available workflows:\n${lines.join("\n")}` }],
397
- details: { action: "list", count: available.length },
378
+ content: [{ type: "text", text: `Available workflows:\n${lines}` }],
379
+ details: { action: "list", count: all.filter((wf) => wf.available).length },
398
380
  };
399
381
  } catch (err: unknown) {
400
- // throw(W4b):list 失败改 throw(原 return isError 被 pi 丢弃),文案保持
401
- const msg = toErrorMessage(err);
402
- throw new Error(`List failed: ${msg}`);
382
+ throwPrefixed("List failed", err);
403
383
  }
404
384
  }
405
385
 
@@ -410,7 +390,7 @@ async function actionList(registry: WorkflowScriptRegistry): Promise<TextContent
410
390
  * isError:true,返回值里的 isError 被 agent-loop 丢弃(agent-loop.js:453-483),
411
391
  * 错误一律 throw,编译器兜底防回潮)。
412
392
  */
413
- function textResult(text: string): TextContent {
393
+ function textResult(text: string): WorkflowScriptExecuteResult {
414
394
  return {
415
395
  content: [{ type: "text", text }],
416
396
  details: undefined,