@zhushanwen/pi-subagent-workflow 9.4.1 → 9.5.1

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/README.md CHANGED
@@ -2,14 +2,28 @@
2
2
 
3
3
  Pi 的 subagent + workflow 合并包:任务委派 + 多 agent 编排(chain / parallel / scatter-gather / map-reduce),单包统一执行链 + 分层配额(ADR-030)。
4
4
 
5
- ## 工具面(3 tools)
5
+ ## 工具面(7 tools)
6
6
 
7
7
  | 工具 | action 集合 | 关键参数 |
8
8
  |------|------------|---------|
9
- | `subagent` | `start` / `list` / `cancel` / `message` / `close` / `fork-from`(6 个) | start:`task` + `slug`(≤35 字符)必填,`agent`(.md 绝对路径,缺省 general-purpose)、`model`、`engine`、`fork`、`worktree`、`collect`、`maxTurns`、`idleTimeoutMs` 等 16 字段拍平顶层;`message` / `close` / `cancel` / `fork-from`:`subagentId` 必填(`message` 另需 `text`) |
10
- | `workflow` | `run` / `status` / `abort`(3 个) | 见「Workflow 生命周期」节 |
9
+ | `subagent_start` | —(单动作) | `task` + `slug`(≤35 字符)必填,`agent`(.md 绝对路径,缺省 general-purpose)、`model`、`engine`、`fork`、`worktree`、`maxTurns`、`idleTimeoutMs` 等 start 场景可选字段 |
10
+ | `subagent_manage` | `list` / `cancel` / `close`(3 个) | `cancel` / `close`:`subagentId` 必填 |
11
+ | `subagent_message` | `message` / `fork-from`(2 个) | `message`:`subagentId` + `text` 必填;`fork-from`:`sourceSubagentId` 必填 |
12
+ | `subagents` | —(单动作,批量派发,deferred 延迟加载) | `tasks` 必填(每元素一个自包含任务 prompt,全部并行、结果一条通知收口),可选 `agents`(逗号分隔 .md 绝对路径,单值或与 tasks 一一对应)、`aggregate`、`slug`、`model` 等;run 级状态控制走 `workflow_control` |
13
+ | `workflow_run` | —(单动作) | `name`(`.js` 绝对路径)+ 可选 `args` / 预算,见「Workflow 生命周期」节 |
14
+ | `workflow_control` | `status` / `abort` / `resume`(3 个) | `abort` / `resume`:`runId` 必填,见「Workflow 生命周期」节 |
11
15
  | `workflow-script` | `generate` / `lint` / `save` / `delete` / `list`(5 个) | `generate`:`script` + `name`(可选 `description`);`save`:`name`(可 `newName` 改名);`lint` / `delete`:`name` |
12
16
 
17
+ ## 工具激活(deferred)
18
+
19
+ `subagents` / `workflow-script` 两工具注册为 `exposure: "deferred"`——不占常驻工具声明,由 pi 内置 `tool_search` 工具检索命中后按需激活。taiji 宿主内开箱即用(宿主启动时已激活 `tool_search`);独立 pi 使用需在 `<agentDir>/settings.json`(默认 `~/.pi/agent/settings.json`)开启:
20
+
21
+ ```json
22
+ { "defaultTools": ["+tool_search"] }
23
+ ```
24
+
25
+ `+` 前缀是增量修饰(保留 read/bash/edit/write 默认集)。未开启 `tool_search` 时这两个工具对模型不可见。
26
+
13
27
  ## 内置 Agents
14
28
 
15
29
  按「读/写 × 视角」正交切分,10 个角色零重叠(C1 起随 `@zhushanwen/subagent-core` 的 `agents/` 资产分发,`<available_subagents>` 的 `<location>` 指向 core 包 `agents/` 内各 agent `.md` 的绝对路径):
@@ -44,7 +58,7 @@ orchestrator 是纯协调器角色:拆解任务 → 委派 subagent → 汇总
44
58
  **工具约束(C1/D-5)**:内置模板不携带 `tools:` frontmatter 白名单,subagent 不以 `--tools` 白名单启动——工具约束回归宿主默认工具面,orchestrator 靠角色职责(职责边界段)约束自身只做协调。想要白名单的用户在 `<workspace>/.agents/agents/` 放同名 `.md` 覆写(发现优先级 project 级最高,稳定遮蔽内置),或临时用 pi CLI 的 `--tools` 白名单验证:
45
59
 
46
60
  ```bash
47
- pi --tools todo,goal_control,workflow,subagent,ask_user
61
+ pi --tools todo,goal_control,workflow_run,subagent_start,ask_user
48
62
  ```
49
63
 
50
64
  > **依赖**:需先安装本包及相关扩展
@@ -61,7 +75,7 @@ pi --tools todo,goal_control,workflow,subagent,ask_user
61
75
 
62
76
  ## Workflow 生命周期
63
77
 
64
- Workflow run 有两类结束形态:**终局**(`done`,outcome 区分 done / failed / cancelled / time_limited——成败已定)与**中断**(`interrupted` 暂停态——执行被打断但可续跑)。`workflow` tool 共 4 个 action:`run` / `status` / `abort` / `resume`。run 的 `name` 接受 `<available_workflows>` 列出的 workflow 名(内置 chain / parallel / map-reduce / scatter-gather / review-fix-loop 或已保存脚本)或 `.js` 绝对路径。
78
+ Workflow run 有两类结束形态:**终局**(`done`,outcome 区分 done / failed / cancelled / time_limited——成败已定)与**中断**(`interrupted` 暂停态——执行被打断但可续跑)。`workflow_run` 启动 run;`workflow_control` 共 3 个 action:`status` / `abort` / `resume`。run 的 `name` 仅接受 `.js` 绝对路径(通常是 `<available_workflows>` 条目的 `<location>`;任意声明 `@pi-meta kind:"workflow"` 的绝对 `.js` 路径均可),裸名 / 相对路径拒绝(`not_found`,拒单文案列全部可用条目及 location,按 location 重试即可)。
65
79
 
66
80
  - **abort(主动取消)**:`{"action":"abort","runId":"<id>"}`(可选 `"error":"<reason>"`)→ `cancelled` 终局。要新结果就重新 run
67
81
  - **resume(断点续跑)**:`{"action":"resume","runId":"<id>"}`(可选 args 校验一致性与时间预算)——对 interrupted run 从断点恢复:已完成的调用回放缓存零 token、未完成的按三档恢复(会话完整 → 捞回结果;请求未完成 → 同会话续写;会话不在 → 整跑);`/workflows resume <id>` 同能力
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-subagent-workflow",
3
- "version": "9.4.1",
3
+ "version": "9.5.1",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
@@ -58,22 +58,22 @@
58
58
  ]
59
59
  },
60
60
  "dependencies": {
61
- "@zhushanwen/extension-protocol": "0.20.0",
62
61
  "@zhushanwen/session-delivery": "1.1.0",
62
+ "@zhushanwen/extension-protocol": "0.20.0",
63
63
  "@zhushanwen/pi-ext-guards": "0.4.4",
64
64
  "@zhushanwen/pi-extension-logger": "0.6.2",
65
- "@zhushanwen/pi-notify-ledger-host": "0.3.2",
66
- "@zhushanwen/subagent-core": "1.5.0",
67
- "@zhushanwen/pi-subagent-cli": "0.7.1",
68
- "@zhushanwen/zcode-subagent-cli": "0.7.0"
65
+ "@zhushanwen/pi-notify-ledger-host": "0.3.3",
66
+ "@zhushanwen/subagent-core": "1.5.1",
67
+ "@zhushanwen/pi-subagent-cli": "0.7.2",
68
+ "@zhushanwen/zcode-subagent-cli": "0.8.1"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "@earendil-works/pi-ai": "^1.0.0",
72
72
  "@earendil-works/pi-coding-agent": "^1.0.0",
73
73
  "@earendil-works/pi-tui": "^1.0.0",
74
74
  "typebox": "1.3.27",
75
- "@zhushanwen/pi-pending-notifications": "0.8.4",
76
- "@zhushanwen/pi-structured-output": "6.1.1"
75
+ "@zhushanwen/pi-pending-notifications": "0.8.5",
76
+ "@zhushanwen/pi-structured-output": "6.1.2"
77
77
  },
78
78
  "peerDependenciesMeta": {
79
79
  "@earendil-works/pi-coding-agent": {
package/src/index.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  * subagent-workflow Extension — Factory(extension 装配点)
3
3
  *
4
4
  * 合并 @zhushanwen/pi-subagents + @zhushanwen/pi-workflow 为统一包。
5
- * 注册项:4 tool(subagent + subagents + workflow + workflow-script)+ 3 command(subagents + workflows + subagent-model)
5
+ * 注册项:7 tool(subagent_start / subagent_manage / subagent_message + subagents
6
+ * + workflow_run / workflow_control + workflow-script)+ 3 command(subagents + workflows + subagent-model)
6
7
  * + messageRenderer(subagent-bg-notify)+ session 事件。
7
8
  *
8
9
  * 包内结构(执行运行时已迁 packages/subagent-core,本包只留注册面与宿主适配):
@@ -229,7 +230,13 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
229
230
  process.on("beforeExit", reapSpawnedChildrenOnShutdown);
230
231
 
231
232
  // ════════════════════════════════════════════════════════════
232
- // Tools(3 个)+ Commands(3 个)—— 注册面
233
+ // Tools(7 个)+ Commands(3 个)—— 注册面盘点
234
+ //
235
+ // 全包 7 tool:本段注册 4(workflow_run + workflow_control 经 registerWorkflowTool、
236
+ // subagents 经 registerSubagentsTool、workflow-script 经 registerWorkflowScriptTool);
237
+ // 其余 3(subagent_start / subagent_manage / subagent_message 经 registerSubagentTool)
238
+ // 在上方「subagents 域」段(factory 期)注册。3 command:subagents / subagent-model
239
+ // 同在 subagents 域段,workflows 在本段段尾。
233
240
  //
234
241
  // lazyDeps / isScriptRunning / registry 由 workflow-events.ts 装配结果提供。
235
242
  // guard:workflow tool 与 subagents(批量派发入口)共用同一 guard——两者是同一条
@@ -36,7 +36,7 @@ import {
36
36
  // 被测工厂 + 生产模块导出(parse/guide/单例接线)
37
37
  import { createResourceListInjector } from "../resource-list-injector.ts";
38
38
  import { parseWorkflowMeta, setupWorkflowListInjector, WORKFLOW_LIST_GUIDE } from "../workflow-list-injector";
39
- import { SUBAGENT_LIST_GUIDE } from "../subagent-list-injector";
39
+ import { SUBAGENT_DISPATCH_SHARED_RULES, SUBAGENT_LIST_GUIDE } from "../subagent-list-injector";
40
40
 
41
41
  // ── 稳定 spy(vi.hoisted 保证 resetModules 后引用不变)──
42
42
  const spies = vi.hoisted(() => ({ discoverResources: vi.fn(), getCachedFileContent: vi.fn() }));
@@ -75,6 +75,8 @@ function makeAgentInjector() {
75
75
  format: (agents, _invalids) => formatAgentList(agents, { guide: SUBAGENT_LIST_GUIDE }),
76
76
  assemble: (workspaceRoot) =>
77
77
  discoverAgents(workspaceRoot, getHostServices().discoveryRoots?.()?.agents ?? []),
78
+ // 空态纪律行(dmg-r1-6):与生产 config 同款接线——空态断言经本复刻驱动
79
+ emptyGuide: SUBAGENT_DISPATCH_SHARED_RULES,
78
80
  });
79
81
  }
80
82
 
@@ -192,7 +194,7 @@ describe("formatWorkflowList", () => {
192
194
  // guide 段硬编码(锁文本):期望值不插值生产常量——改写 WORKFLOW_LIST_GUIDE
193
195
  // 即红灯,避免「两侧同源插值」下文案漂移无守卫
194
196
  expect(out).toBe(
195
- `\n\n<available_workflows>\nThe following workflows are available. Do NOT call list to discover available workflows — they are listed below; use list only for running state. All listed workflows run directly via action:run — do NOT use workflow-script generate for any listed workflow. For parameter details, read the <location> script file (script header has @pi-meta parameters + usage). For 2+ independent tasks dispatched together, prefer the \`subagents\` tool — it drives the fan-out workflow for you.\n <workflow><name>chain</name><description>三步链</description><location>/workflows/chain.js</location></workflow>\n</available_workflows>`,
197
+ `\n\n<available_workflows>\nThe following workflows are available. Do NOT call list to discover available workflows — they are listed below; use list only for running state. All listed workflows run directly via the workflow_run tool — do NOT use workflow-script generate for any listed workflow. For parameter details, read the <location> script file (script header has @pi-meta parameters + usage). For 2+ independent tasks dispatched together, prefer the \`subagents\` tool — it drives the fan-out workflow for you.\n <workflow><name>chain</name><description>三步链</description><location>/workflows/chain.js</location></workflow>\n</available_workflows>`,
196
198
  );
197
199
  });
198
200
 
@@ -204,7 +206,7 @@ describe("formatWorkflowList", () => {
204
206
 
205
207
  it("引导语通用化:不写死内置 workflow 名,含 info 回收指引", () => {
206
208
  const out = formatWorkflowList([{ name: "chain", description: "d", path: "/workflows/chain.js" }], { guide: WORKFLOW_LIST_GUIDE });
207
- expect(out).toContain("All listed workflows run directly via action:run");
209
+ expect(out).toContain("All listed workflows run directly via the workflow_run tool");
208
210
  expect(out).toContain("read the <location> script file");
209
211
  // 通用化约束:引导语不点名具体 workflow(名字由 @pi-meta 动态注入,
210
212
  // 写死内置名会在新增/移除 workflow 时与列表漂移)
@@ -243,7 +245,7 @@ describe("formatAgentList CA2 快照(agent guide 壳自有文案唯一字节
243
245
  // guide 段硬编码(锁文本):期望值不插值生产常量——改写 SUBAGENT_LIST_GUIDE
244
246
  // 即红灯,避免「两侧同源插值」下文案漂移无守卫
245
247
  expect(out).toBe(
246
- `\n\n<available_subagents>\nThe following subagents are available. PRIORITY: when a task involves reading 3+ files, writing 100+ lines, parallel research, or specialized review, delegate to a matching subagent FIRST instead of doing it yourself — this keeps your context focused on orchestration. Do NOT call list to discover available subagents; use list only for running state. When using the subagent tool, pass the <location> path from this list as the agent param (usually the right choice); an absolute .md path outside this list also works if the file is a readable agent definition. If no agent matches your task, omit agent (a general-purpose agent is used) and put all role-specific instructions in the task text.\n${expectedItems.join("\n")}\n</available_subagents>`,
248
+ `\n\n<available_subagents>\nThe following subagents are available. PRIORITY: when a task involves reading 3+ files, writing 100+ lines, parallel research, or specialized review, delegate to a matching subagent FIRST instead of doing it yourself — this keeps your context focused on orchestration. Do NOT call list to discover available subagents; use list only for running state. When using the subagent_start tool, pass the <location> path from this list as the agent param (usually the right choice); an absolute .md path outside this list also works if the file is a readable agent definition. If no agent matches your task, omit agent (a general-purpose agent is used) and put all role-specific instructions in the task text.\n${expectedItems.join("\n")}\n</available_subagents>`,
247
249
  );
248
250
 
249
251
  // location 前缀替换(资产来源迁移:pi-sw 安装目录 → core 包)后其余字节零变化
@@ -480,6 +482,11 @@ describe("agent injector session 级缓存(assemble 路径工厂直测,与 w
480
482
  expect(r1?.systemPrompt).toContain("<available_subagents>");
481
483
  expect(r1?.systemPrompt).toContain("(none discovered; roots: ");
482
484
  expect(r1?.systemPrompt).toContain("/ws/.agents/agents");
485
+ // dmg-r1-6:零 agent 环境下 D1 单源化的跨工具调度纪律仍随空态段注入
486
+ // (direct 派发不经清单路由,纪律承载点不能随清单消失)
487
+ expect(r1?.systemPrompt).toContain("do NOT bash sleep, busy-wait, or poll");
488
+ expect(r1?.systemPrompt).toContain("omit unless the user explicitly requests a limit");
489
+ expect(r1?.systemPrompt).toContain("inherit the main agent's model");
483
490
  expect(r2?.systemPrompt).toBe(r1?.systemPrompt);
484
491
  });
485
492
 
@@ -14,16 +14,17 @@
14
14
  * 不阻断、缓存保持 null);before_agent_start 读缓存渲染注入、miss(session_start
15
15
  * 未触发/缓存被清)则 fallback 重新发现+赋值,空发现渲染空态段(D4-2:(none
16
16
  * discovered; roots: ...),不再整段消失;invalids 随段具名呈现——D4-3 损坏文件
17
- * 不静默),任何异常被吞掉(记日志)不阻断 agent turn;session_shutdown 清缓存。
17
+ * 不静默;config.emptyGuide 随段注入——纪律行与清单在场与否无关),任何异常被吞掉(记日志)不阻断 agent turn;session_shutdown 清缓存。
18
18
  * pi 支持 async handler,同一 event 多 handler 链式(前者返回的 systemPrompt 作
19
19
  * 后者输入)。
20
20
  *
21
21
  * 两实例的真差异经 config 参数化承载:kind(发现种类 + discoveryRoots 宿主槽位)/
22
22
  * parse(单文件内容 → entry;workflow 侧 description 截断内聚于其 parse)/ format
23
- * (entries → XML 注入段;guide 文案差异内聚于各自 format)/ includeTmp(workflow 侧
24
- * 覆盖 .pi/workflows/.tmp/ generate 产物)/ logTag(错误日志前缀,保持既有可检索性);
25
- * assemble(agent 侧真差异:装配循环覆写——U11 下沉 core discoverAgents 后工厂不重复
26
- * 装配语义,parse 不参与)。
23
+ * (entries → XML 注入段;guide 文案差异内聚于各自 format)/ emptyGuide(空态段
24
+ * 纪律行;agent 侧传 SUBAGENT_DISPATCH_SHARED_RULES,workflow 侧不传)/ includeTmp
25
+ * (workflow 侧覆写 .pi/workflows/.tmp/ generate 产物)/ logTag(错误日志前缀,
26
+ * 保持既有可检索性);assemble(agent 侧真差异:装配循环覆写——U11 下沉 core
27
+ * discoverAgents 后工厂不重复装配语义,parse 不参与)。
27
28
  *
28
29
  * model-list-injector 不参与本工厂:数据源是 ModelRegistry 内存快照(真差异,无文件
29
30
  * 发现与缓存生命周期),见该文件头注释。
@@ -90,6 +91,14 @@ export interface ResourceListInjectorConfig<TEntry extends ResourceListEntry> {
90
91
  * 实例的 format 决定(workflow 实例透传 core formatWorkflowList 的 invalids 选项)。
91
92
  */
92
93
  format(entries: TEntry[], invalids: readonly InvalidResource[]): string;
94
+ /**
95
+ * 空态段纪律行(可选,宿主注入):零条目时空态段在 roots 行后渲染的
96
+ * 引导文案——承载与清单在场与否无关、但工具仍可用就必须注入的跨工具
97
+ * 调度纪律(agent 侧 = SUBAGENT_DISPATCH_SHARED_RULES:direct 派发不经
98
+ * 清单路由,零 agent 环境下纪律仍需有承载点)。缺省空态段保持纯
99
+ * 「没有 + 去哪找」形态(workflow 侧不传)。
100
+ */
101
+ emptyGuide?: string;
93
102
  /** workflow 侧真差异:包含 .pi/workflows/.tmp/(workflow-script generate 产物)。 */
94
103
  includeTmp?: boolean;
95
104
  }
@@ -137,8 +146,8 @@ export function createResourceListInjector<TEntry extends ResourceListEntry>(
137
146
  * 不消费)。空发现(format 返回空串——core formatXxxList 空列表的判据契约)
138
147
  * 不再让注入段整段消失,改渲染空态段(D4-2):agent 可区分「功能关闭」与
139
148
  * 「确实没有」,roots 清单供自救;invalids 随空态段具名呈现(D4-3——条目为零
140
- * 与存在损坏文件两个事实同段)。非空路径 format 输出原样缓存(invalids 缺省
141
- * 逐字节零变更)。
149
+ * 与存在损坏文件两个事实同段);emptyGuide(若有)随空态段注入——纪律与
150
+ * 清单在场与否无关。非空路径 format 输出原样缓存(invalids 缺省逐字节零变更)。
142
151
  */
143
152
  function setCache(
144
153
  result: ResourceDiscoveryResult<TEntry> | null,
@@ -156,6 +165,7 @@ export function createResourceListInjector<TEntry extends ResourceListEntry>(
156
165
  config.kind,
157
166
  discoveryRootDirs(workspaceRoot, config.kind, config.includeTmp === true),
158
167
  result.invalids,
168
+ config.emptyGuide,
159
169
  );
160
170
  }
161
171
 
@@ -43,7 +43,23 @@ import { createResourceListInjector } from "./resource-list-injector.ts";
43
43
  * 动态指引经 task 文本 / appendSystemPrompt 参数承载。
44
44
  */
45
45
  export const SUBAGENT_LIST_GUIDE =
46
- "The following subagents are available. PRIORITY: when a task involves reading 3+ files, writing 100+ lines, parallel research, or specialized review, delegate to a matching subagent FIRST instead of doing it yourself — this keeps your context focused on orchestration. Do NOT call list to discover available subagents; use list only for running state. When using the subagent tool, pass the <location> path from this list as the agent param (usually the right choice); an absolute .md path outside this list also works if the file is a readable agent definition. If no agent matches your task, omit agent (a general-purpose agent is used) and put all role-specific instructions in the task text.";
46
+ "The following subagents are available. PRIORITY: when a task involves reading 3+ files, writing 100+ lines, parallel research, or specialized review, delegate to a matching subagent FIRST instead of doing it yourself — this keeps your context focused on orchestration. Do NOT call list to discover available subagents; use list only for running state. When using the subagent_start tool, pass the <location> path from this list as the agent param (usually the right choice); an absolute .md path outside this list also works if the file is a readable agent definition. If no agent matches your task, omit agent (a general-purpose agent is used) and put all role-specific instructions in the task text.";
47
+
48
+ /**
49
+ * 跨工具通用调度纪律(tool-declaration-slimming D1 信息单源化:这些规则曾重复出现在
50
+ * subagent 组(subagent_start/manage/message)/ subagents / workflow 组(workflow_run/
51
+ * workflow_control)的 description / schema 字段说明 / promptGuidelines 里——「勿 poll
52
+ * 等待」讲三遍、「tokens/time 仅用户明确要求才设」写四份、「model 缺省继承主 agent」
53
+ * 三处。现收敛到本常量,随 <available_subagents> 段每 turn 注入说一次;工具声明文案
54
+ * 零残留由契约测试锁定(tool-prompt-contract.test.ts 单源化终态断言)。bash 的
55
+ * 「DO NOT bash sleep」族是 taiji 自写契约锚词(功能契约非教学冗余),不属本收敛面、
56
+ * 保留在 bash 工具文案内。
57
+ */
58
+ export const SUBAGENT_DISPATCH_SHARED_RULES =
59
+ "Cross-tool dispatch rules (subagent_start, subagent_manage, subagent_message, subagents, workflow_run, workflow_control tools): " +
60
+ "background dispatches auto-deliver results via a completion notification that wakes your next turn — do NOT bash sleep, busy-wait, or poll while waiting; do useful non-overlapping work, otherwise stop. " +
61
+ "tokens/time budgets: omit unless the user explicitly requests a limit (default = unlimited). " +
62
+ "model/thinkingLevel: omit by default (inherit the main agent's model; thinkingLevel defaults to each model's highest available level) — set them only when the user explicitly requests a specific model or thinking depth.";
47
63
 
48
64
  /**
49
65
  * agent 清单实例:缓存生命周期 / 三 handler 经工厂骨架;装配循环经 assemble 覆写
@@ -56,12 +72,21 @@ const injector = createResourceListInjector<AgentEntry>({
56
72
  logTag: "[subagent-list-injector]",
57
73
  // agents 段无 invalid 产出面(assemble 路径 invalids 恒空),第二参忽略——
58
74
  // 签名对齐工厂 config.format(P5 D4-3 invalid 上报仅 workflow 域接线)。
59
- format: (agents, _invalids) => formatAgentList(agents, { guide: SUBAGENT_LIST_GUIDE }),
75
+ format: (agents, _invalids) =>
76
+ formatAgentList(agents, {
77
+ // guide = agent 路由引导 + 跨工具通用调度纪律(D1 单源化,见常量头注);
78
+ // "\n" 分行保持 guide 段内两条职责的文本边界。
79
+ guide: SUBAGENT_LIST_GUIDE + "\n" + SUBAGENT_DISPATCH_SHARED_RULES,
80
+ }),
60
81
  assemble: (workspaceRoot) =>
61
82
  discoverAgents(
62
83
  workspaceRoot,
63
84
  getHostServices().discoveryRoots?.()?.agents ?? [],
64
85
  ),
86
+ // 空态也承载调度纪律:direct 派发不经清单路由(缺省 general-purpose agent),
87
+ // 零 agent 环境下 D1 单源化的纪律(勿 poll 等待/预算缺省)仍需注入点——
88
+ // 只传纪律不传路由引导(「以下 subagent 可用」在零条目下是假话)。
89
+ emptyGuide: SUBAGENT_DISPATCH_SHARED_RULES,
65
90
  });
66
91
 
67
92
  /** 注册 session 生命周期 handler,注入 `<available_subagents>` 段(三 handler 语义见工厂)。 */
@@ -44,7 +44,7 @@ import { createResourceListInjector } from "./resource-list-injector.ts";
44
44
  * 而不进模板 meta——zsw 宿主清单注入不会照搬本句。
45
45
  */
46
46
  export const WORKFLOW_LIST_GUIDE =
47
- "The following workflows are available. Do NOT call list to discover available workflows — they are listed below; use list only for running state. All listed workflows run directly via action:run — do NOT use workflow-script generate for any listed workflow. For parameter details, read the <location> script file (script header has @pi-meta parameters + usage). For 2+ independent tasks dispatched together, prefer the `subagents` tool — it drives the fan-out workflow for you.";
47
+ "The following workflows are available. Do NOT call list to discover available workflows — they are listed below; use list only for running state. All listed workflows run directly via the workflow_run tool — do NOT use workflow-script generate for any listed workflow. For parameter details, read the <location> script file (script header has @pi-meta parameters + usage). For 2+ independent tasks dispatched together, prefer the `subagents` tool — it drives the fan-out workflow for you.";
48
48
 
49
49
  /**
50
50
  * 解析 workflow .js 文件的 meta(name + description),经 IF1 parseResourceMeta。
@@ -6,8 +6,10 @@
6
6
  // list 投影)在 core @zhushanwen/subagent-core/execution/assembly/subagent-actions-core,
7
7
  // 六 handler + 领域类型消费方经 core barrel import;行为快照等值锚定见 core
8
8
  // __tests__/subagent-actions-core.test.ts(期望值硬编码自迁移前实测输出,含错误
9
- // 文案逐字锚)。参数提取位于 subagent-tool.ts executeSubagent(action 路由 /
10
- // listParam / cancelParam 解包 / skillPath-cwd 路径检查)。本壳只做渲染:
9
+ // 文案逐字锚)。参数提取位于 subagent-tool.ts 三入口 execute(D3 拆分终态):
10
+ // executeSubagentStart(无 action 路由;skillPath-cwd 路径检查)/ executeSubagentManage
11
+ //(isManageAction 守卫 + listParam/cancelParam/closeParam 解包)/ executeSubagentMessage
12
+ //(isMessageAction 守卫 + messageParam/forkFromParam 解包)。本壳只做渲染:
11
13
  // adapter(领域对象 → {content, details})+ buildGuiComponent(GUI 协议组件)。
12
14
  //
13
15
  // content(JSON 字符串)给 LLM,details(SubagentToolResult)给 renderResult,同源同处生成。
@@ -1,8 +1,8 @@
1
1
  /**
2
- * 注册层黑盒测试共享捕获 helper:fake pi 收集 registerTool 入参,断言恰好注册
3
- * 一个 tool(可锁定期望名)并返回首个。此前 7 个测试文件各自内联同构拷贝
4
- * (fake pi + tools 数组 + name 断言 + CapturedTool 类型),收敛至此单点;
5
- * 差异面(注册函数、pi 之后的实参、deps 形状)留在调用方闭包。
2
+ * 注册层黑盒测试共享捕获 helper:fake pi 收集 registerTool 入参,支持两种形态——
3
+ * ①恰好注册一个 tool(可锁定期望名);②一次注册多个工具(D3 拆分:registerSubagentTool
4
+ * 注册三工具、registerWorkflowTool 注册两工具)按名捕获。差异面(注册函数、pi 之后
5
+ * 的实参、deps 形状)留在调用方闭包。
6
6
  */
7
7
 
8
8
  /** 注册层捕获到的 tool 定义最小形态(各文件按需用泛型传窄 view 类型)。 */
@@ -32,25 +32,46 @@ export interface ScriptResultToolView extends CapturedTool { // oe-exempt:202609
32
32
  }>;
33
33
  }
34
34
 
35
+ /** 注册并捕获全部 tool 定义,按名索引(D3 拆分多工具注册形态)。 */
36
+ export function captureToolsByName(
37
+ register: (pi: { registerTool: (tool: unknown) => void }) => void,
38
+ ): Record<string, CapturedTool> {
39
+ const tools: unknown[] = [];
40
+ register({ registerTool: (tool) => tools.push(tool) });
41
+ const byName: Record<string, CapturedTool> = {};
42
+ for (const tool of tools) {
43
+ const named = tool as CapturedTool;
44
+ byName[named.name] = named;
45
+ }
46
+ if (tools.length === 0) {
47
+ throw new Error("expected at least one registered tool, got 0");
48
+ }
49
+ return byName;
50
+ }
51
+
35
52
  /**
36
53
  * 注册并捕获 tool 定义。泛型 T 供需要窄 execute 返回类型的调用方传 view
37
54
  * 类型(方法参数双变 + 返回协变,窄形态天然 extends CapturedTool,零 cast)。
38
55
  *
39
56
  * @param register 拿 fake pi 执行注册(调用方闭包内传齐注册函数与其余实参)
40
- * @param expectedName 期望的 tool 名(缺省不锁定——仅断言恰好注册一个)
57
+ * @param expectedName 期望的 tool 名——给定:多工具注册中按名取出(D3 拆分形态);
58
+ * 缺省:断言恰好注册一个并返回首个(单工具注册形态)
41
59
  */
42
60
  export function captureTool<T extends CapturedTool = CapturedTool>(
43
61
  register: (pi: { registerTool: (tool: unknown) => void }) => void,
44
62
  expectedName?: string,
45
63
  ): T {
46
- const tools: unknown[] = [];
47
- register({ registerTool: (tool) => tools.push(tool) });
48
- const first = tools[0] as T | undefined;
49
- if (first === undefined || tools.length !== 1) {
50
- throw new Error(`expected exactly one registered tool, got ${tools.length}`);
64
+ const byName = captureToolsByName(register);
65
+ if (expectedName !== undefined) {
66
+ const named = byName[expectedName] as T | undefined;
67
+ if (named === undefined) {
68
+ throw new Error(`expected registered tool '${expectedName}', got names: ${Object.keys(byName).join(", ")}`);
69
+ }
70
+ return named;
51
71
  }
52
- if (expectedName !== undefined && first.name !== expectedName) {
53
- throw new Error(`expected registered tool '${expectedName}', got '${String(first.name)}'`);
72
+ const names = Object.keys(byName);
73
+ if (names.length !== 1) {
74
+ throw new Error(`expected exactly one registered tool, got ${names.length}`);
54
75
  }
55
- return first;
76
+ return byName[names[0]] as T;
56
77
  }
@@ -1,13 +1,15 @@
1
1
  /**
2
- * subagent tool 非 start 通道 + renderCall 预解析测试(与 path-guard 的 start 守卫面
3
- * 互补,覆盖 executeSubagent 的 action 路由全分支与 renderCall 的 model 预解析降级链)。
2
+ * subagent 工具组非 start 通道 + renderCall 预解析测试(与 path-guard 的 start 守卫面
3
+ * 互补,覆盖 subagent_manage / subagent_message 的 action 路由全分支、subagent_start
4
+ * 的顶层透传与 renderCall 的 model 预解析降级链)。
4
5
  *
5
- * 覆盖面:
6
- * - execute:service 缺席 → 带恢复动作的错误;未知 action → immediate throw;
7
- * list/cancel/message/close/fork-from 六通道 handler 接线 + adapter 包装
8
- * (content JSON 给 LLM / details 领域对象给 renderResult)
9
- * - renderCall:model override 三层解析(override 在场 → resolved 注入标题;
10
- * resolveModel 抛错 → 降级不崩;override 缺省 → resolved undefined)
6
+ * 覆盖面(tool-declaration-slimming D3 拆分后):
7
+ * - subagent_start:service 缺席 → 带恢复动作的错误;顶层 params 原样透传
8
+ * - subagent_manage:list/cancel/close 三通道 handler 接线 + adapter 包装;
9
+ * 未知 action → immediate throw
10
+ * - subagent_message:message/fork-from 两通道接线;未知 action → immediate throw
11
+ * - renderCall(subagent_start):model override 三层解析(override 在场 → resolved
12
+ * 注入标题;resolveModel 抛错 → 降级不崩;override 缺省 → resolved undefined)
11
13
  *
12
14
  * mock 策略:六 handler 深路径 stub(领域内核行为由 core 侧
13
15
  * subagent-actions-core.test.ts 锁定,此处只测接线);pi-ai/typebox 用共享桩
@@ -65,15 +67,18 @@ interface SubagentToolView { // oe-exempt:20260930:test:test double shape for to
65
67
  renderCall: (args: unknown, theme: unknown, ctx: unknown) => { render(width: number): string[] };
66
68
  }
67
69
 
68
- function captureTool(): SubagentToolView {
70
+ /** 按名捕获注册的 tool(D3 拆分:一次注册三个)。 */
71
+ function captureToolByName(toolName: string): SubagentToolView {
69
72
  let captured: SubagentToolView | undefined;
70
73
  const pi = mockExtensionApi({
71
74
  registerTool: (tool: unknown) => {
72
- captured = tool as SubagentToolView;
75
+ if ((tool as { name?: string }).name === toolName) {
76
+ captured = tool as SubagentToolView;
77
+ }
73
78
  },
74
79
  });
75
80
  registerSubagentTool(pi);
76
- if (!captured) throw new Error("subagent tool not registered");
81
+ if (!captured) throw new Error(`${toolName} tool not registered`);
77
82
  return captured;
78
83
  }
79
84
 
@@ -97,29 +102,47 @@ afterEach(() => {
97
102
  resetServiceSlot();
98
103
  });
99
104
 
100
- describe("subagent tool execute:守卫与缺席面", () => {
105
+ describe("注册面:三工具名(D3 拆分终态)", () => {
106
+ it("registerSubagentTool 注册 subagent_start / subagent_manage / subagent_message 三工具", () => {
107
+ const names: string[] = [];
108
+ const pi = mockExtensionApi({
109
+ registerTool: (tool: unknown) => names.push((tool as { name: string }).name),
110
+ });
111
+ registerSubagentTool(pi);
112
+ expect(names.sort()).toEqual(["subagent_manage", "subagent_message", "subagent_start"]);
113
+ });
114
+ });
115
+
116
+ describe("subagent_start execute:守卫与缺席面", () => {
101
117
  it("service 缺席 → immediate throw,错误带恢复动作指引", async () => {
102
- const tool = captureTool(); // 槽未注入
118
+ const tool = captureToolByName("subagent_start"); // 槽未注入
103
119
  await expect(
104
- tool.execute("c1", { action: "list" }, undefined, undefined, undefined),
120
+ tool.execute("c1", { task: "t", slug: "s" }, undefined, undefined, undefined),
105
121
  ).rejects.toThrow(/subagents runtime not initialized.*Recovery: reload this session/s);
106
122
  });
123
+ });
107
124
 
108
- it("未知 action → immediate throw(Unknown subagent action)", async () => {
125
+ describe("subagent_manage execute:三 action 通道接线", () => {
126
+ /** 注入最小 fake service 并捕获 manage 工具(三通道共用基座)。 */
127
+ function withService(): SubagentToolView {
109
128
  setSubagentService({ execute: vi.fn() } as never);
110
- const tool = captureTool();
129
+ return captureToolByName("subagent_manage");
130
+ }
131
+
132
+ it("service 缺席 → immediate throw(manage 同文案单源)", async () => {
133
+ const tool = captureToolByName("subagent_manage");
111
134
  await expect(
112
- tool.execute("c2", { action: "pause" }, undefined, undefined, undefined),
113
- ).rejects.toThrow("Unknown subagent action: pause");
135
+ tool.execute("c0", { action: "list" }, undefined, undefined, undefined),
136
+ ).rejects.toThrow(/subagents runtime not initialized/);
114
137
  });
115
- });
116
138
 
117
- describe("subagent tool execute:六 action 通道接线", () => {
118
- /** 注入最小 fake service 并捕获 tool(六通道共用基座)。 */
119
- function withService(): SubagentToolView {
139
+ it("未知 action → immediate throw(Unknown subagent_manage action)", async () => {
120
140
  setSubagentService({ execute: vi.fn() } as never);
121
- return captureTool();
122
- }
141
+ const tool = withService();
142
+ await expect(
143
+ tool.execute("c2", { action: "pause" }, undefined, undefined, undefined),
144
+ ).rejects.toThrow("Unknown subagent_manage action: pause");
145
+ });
123
146
 
124
147
  it("list:listParam 解包透传,adapter 包装 listResponse", async () => {
125
148
  vi.mocked(listHandler).mockReturnValue({ response: { items: [], total: 0 } } as never);
@@ -140,15 +163,6 @@ describe("subagent tool execute:六 action 通道接线", () => {
140
163
  expect(JSON.parse(result.content[0]!.text)).toMatchObject({ action: "cancel", subagentId: "sa-9" });
141
164
  });
142
165
 
143
- it("message:messageParam 解包透传", async () => {
144
- vi.mocked(messageHandler).mockResolvedValue({ subagentId: "sa-7", response: { queued: true } } as never);
145
- const tool = withService();
146
- const params = { action: "message", messageParam: { subagentId: "sa-7", text: "continue" } };
147
- const result = await tool.execute("c5", params, undefined, undefined, undefined);
148
- expect(vi.mocked(messageHandler)).toHaveBeenCalledWith(expect.anything(), params.messageParam);
149
- expect(JSON.parse(result.content[0]!.text)).toMatchObject({ action: "message", subagentId: "sa-7" });
150
- });
151
-
152
166
  it("close:closeParam 解包透传", async () => {
153
167
  vi.mocked(closeHandler).mockResolvedValue({ subagentId: "sa-6", response: { archived: true } } as never);
154
168
  const tool = withService();
@@ -157,6 +171,23 @@ describe("subagent tool execute:六 action 通道接线", () => {
157
171
  expect(vi.mocked(closeHandler)).toHaveBeenCalledWith(expect.anything(), { subagentId: "sa-6" });
158
172
  expect(JSON.parse(result.content[0]!.text)).toMatchObject({ action: "close", subagentId: "sa-6" });
159
173
  });
174
+ });
175
+
176
+ describe("subagent_message execute:两 action 通道接线", () => {
177
+ /** 注入最小 fake service 并捕获 message 工具(两通道共用基座)。 */
178
+ function withService(): SubagentToolView {
179
+ setSubagentService({ execute: vi.fn() } as never);
180
+ return captureToolByName("subagent_message");
181
+ }
182
+
183
+ it("message:messageParam 解包透传", async () => {
184
+ vi.mocked(messageHandler).mockResolvedValue({ subagentId: "sa-7", response: { queued: true } } as never);
185
+ const tool = withService();
186
+ const params = { action: "message", messageParam: { subagentId: "sa-7", text: "continue" } };
187
+ const result = await tool.execute("c5", params, undefined, undefined, undefined);
188
+ expect(vi.mocked(messageHandler)).toHaveBeenCalledWith(expect.anything(), params.messageParam);
189
+ expect(JSON.parse(result.content[0]!.text)).toMatchObject({ action: "message", subagentId: "sa-7" });
190
+ });
160
191
 
161
192
  it("fork-from:forkFromParam 解包透传", async () => {
162
193
  vi.mocked(forkFromHandler).mockResolvedValue({
@@ -170,7 +201,17 @@ describe("subagent tool execute:六 action 通道接线", () => {
170
201
  expect(JSON.parse(result.content[0]!.text)).toMatchObject({ action: "fork-from", subagentId: "sa-new" });
171
202
  });
172
203
 
173
- it("start:合法绝对路径放行,顶层 params 原样透传(含 signal/model override)", async () => {
204
+ it("未知 action → immediate throw(Unknown subagent_message action)", async () => {
205
+ setSubagentService({ execute: vi.fn() } as never);
206
+ const tool = withService();
207
+ await expect(
208
+ tool.execute("c2m", { action: "pause" }, undefined, undefined, undefined),
209
+ ).rejects.toThrow("Unknown subagent_message action: pause");
210
+ });
211
+ });
212
+
213
+ describe("subagent_start execute:顶层透传", () => {
214
+ it("start:合法绝对路径放行,顶层 params 原样透传(无 action 字段;signal/model ctx 透传)", async () => {
174
215
  vi.mocked(startHandler).mockResolvedValue({
175
216
  subagentId: "sa-1",
176
217
  slug: "demo",
@@ -178,9 +219,8 @@ describe("subagent tool execute:六 action 通道接线", () => {
178
219
  response: { mode: "background" },
179
220
  } as never);
180
221
  setSubagentService({ execute: vi.fn() } as never);
181
- const tool = captureTool();
222
+ const tool = captureToolByName("subagent_start");
182
223
  const params = {
183
- action: "start",
184
224
  task: "t",
185
225
  slug: "demo",
186
226
  skillPath: "/abs/skills/x.md",
@@ -193,12 +233,12 @@ describe("subagent tool execute:六 action 通道接线", () => {
193
233
  });
194
234
  });
195
235
 
196
- describe("subagent tool renderCall:model 预解析", () => {
236
+ describe("subagent_start renderCall:model 预解析", () => {
197
237
  it("override 在场 + resolveModel 成功 → resolved 注入标题(provider/model · thinking)", () => {
198
238
  setSubagentService({
199
239
  resolveModel: () => ({ model: { provider: "p", id: "m1" }, thinkingLevel: "high" }),
200
240
  } as never);
201
- const tool = captureTool();
241
+ const tool = captureToolByName("subagent_start");
202
242
  const comp = tool.renderCall({ agent: "coder", model: "p/m1", thinkingLevel: "high", slug: "demo" }, markingTheme, {});
203
243
  const text = comp.render(200).join("");
204
244
  expect(text).toContain("p/m1");
@@ -212,7 +252,7 @@ describe("subagent tool renderCall:model 预解析", () => {
212
252
  throw new Error("registry cold");
213
253
  },
214
254
  } as never);
215
- const tool = captureTool();
255
+ const tool = captureToolByName("subagent_start");
216
256
  const comp = tool.renderCall({ agent: "coder", model: "p/m1" }, markingTheme, {});
217
257
  const text = comp.render(80).join("");
218
258
  expect(text).toContain("subagent");
@@ -220,7 +260,7 @@ describe("subagent tool renderCall:model 预解析", () => {
220
260
  });
221
261
 
222
262
  it("service 缺席 → resolved undefined 降级;无 agent 字段兜底默认名", () => {
223
- const tool = captureTool(); // 槽空
263
+ const tool = captureToolByName("subagent_start"); // 槽空
224
264
  const comp = tool.renderCall({ task: "t" }, markingTheme, {});
225
265
  const text = comp.render(80).join("");
226
266
  expect(text).toContain("subagent");
@@ -231,8 +271,32 @@ describe("subagent tool renderCall:model 预解析", () => {
231
271
  setSubagentService({
232
272
  resolveModel: () => ({ model: { provider: "p", id: "m1" }, thinkingLevel: "high" }),
233
273
  } as never);
234
- const tool = captureTool();
274
+ const tool = captureToolByName("subagent_start");
235
275
  const comp = tool.renderCall(null, markingTheme, {});
236
276
  expect(comp.render(80).join("")).toContain("subagent");
237
277
  });
238
278
  });
279
+
280
+ describe("subagent_manage / subagent_message renderCall:轻量标题行", () => {
281
+ it("manage:标题含工具名与 action", () => {
282
+ const tool = captureToolByName("subagent_manage");
283
+ const comp = tool.renderCall({ action: "list", listParam: {} }, markingTheme, {});
284
+ const text = comp.render(200).join("");
285
+ expect(text).toContain("subagent_manage");
286
+ expect(text).toContain("list");
287
+ });
288
+
289
+ it("message:标题含工具名与 action", () => {
290
+ const tool = captureToolByName("subagent_message");
291
+ const comp = tool.renderCall({ action: "fork-from", forkFromParam: {} }, markingTheme, {});
292
+ const text = comp.render(200).join("");
293
+ expect(text).toContain("subagent_message");
294
+ expect(text).toContain("fork-from");
295
+ });
296
+
297
+ it("args 缺 action → 不崩,标题仍渲染工具名", () => {
298
+ const tool = captureToolByName("subagent_manage");
299
+ const comp = tool.renderCall(null, markingTheme, {});
300
+ expect(comp.render(200).join("")).toContain("subagent_manage");
301
+ });
302
+ });