@zhushanwen/pi-subagent-workflow 9.4.0 → 9.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.0",
3
+ "version": "9.5.0",
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.19.0",
61
+ "@zhushanwen/extension-protocol": "0.20.0",
62
62
  "@zhushanwen/session-delivery": "1.1.0",
63
- "@zhushanwen/pi-ext-guards": "0.4.3",
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.1",
66
- "@zhushanwen/pi-subagent-cli": "0.7.0",
67
- "@zhushanwen/zcode-subagent-cli": "0.6.0",
68
- "@zhushanwen/subagent-core": "1.4.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.0"
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-structured-output": "6.1.0",
76
- "@zhushanwen/pi-pending-notifications": "0.8.3"
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": {
@@ -87,7 +87,7 @@
87
87
  "@types/node": "^24.0.0",
88
88
  "@vitest/coverage-v8": "^4.1.9",
89
89
  "vitest": "^4.1.8",
90
- "@zhushanwen/subagent-engine-sdk": "0.10.0"
90
+ "@zhushanwen/subagent-engine-sdk": "0.10.1"
91
91
  },
92
92
  "scripts": {
93
93
  "typecheck": "npx tsc --noEmit",
@@ -107,12 +107,15 @@ export function corePackageNpmRoot(moduleUrl: string = import.meta.url): string
107
107
  * `<Resources>/extensions/@zhushanwen/pi-subagent-workflow/index.js`),其**父目录**
108
108
  * 正是 `@zhushanwen` scope 目录——npm 槽语义("一级子项 = 包目录")恰好成立,
109
109
  * 于是可直接当 npm 槽根用:约定目录扫描即命中
110
- * `pi-subagent-workflow/{workflows,agents}`(两目录由 `scripts/bundle-extensions.mjs`
111
- * 随包拷贝;`pi.agents`/`pi.workflows` manifest 约定及其"声明路径不存在 → 整包失败
112
- * 占位"语义是 subagent-core resource-discovery 的 taiji 自建规则,权威源 =
113
- * resource-discovery.ts processPackage——pi 上游 manifest 只有
114
- * extensions/skills/prompts/themes 四字段,无 agents/workflows 概念。已核仓内无任何
115
- * 包声明该字段,故扫描面内所有包都走约定目录分支,不进 manifest 失败分支)。
110
+ * `pi-subagent-workflow/{workflows,agents}`(两目录由 `scripts/bundle-extensions.mjs`
111
+ * 随包拷贝;`pi.agents`/`pi.workflows` manifest 约定及其"声明路径不存在 → 整包失败
112
+ * 占位"语义是 subagent-core resource-discovery 的 taiji 自建规则,权威源 =
113
+ * resource-discovery.ts processPackage——pi 上游 manifest 只有
114
+ * extensions/skills/prompts/themes 四字段(锚 = pi-coding-agent
115
+ * dist/core/pi-manifest.js:3 RESOURCE_FIELDS,pi@1.0.0 已核;同源登记见
116
+ * docs/extensions/extension-conventions.md「readPiManifest」条),无 agents/workflows
117
+ * 概念。已核仓内无任何包声明该字段,故扫描面内所有包都走约定目录分支,不进
118
+ * manifest 失败分支)。
116
119
  *
117
120
  * 与 dev/npm 形态的既有语义一致:npm 槽根扫的就是 scope 下全部包目录(因此本包自带
118
121
  * 的 workflows/agents 在 dev/npm 形态同样入发现面),非新增语义。
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。
@@ -51,6 +51,13 @@ const STATUS_ORDER: Record<string, number> = {
51
51
  /** 未知 status 的默认排序权重(排在已知 status 之后)。 */
52
52
  const UNKNOWN_STATUS_WEIGHT = 9;
53
53
 
54
+ /** RPC 非 lifecycle 通知 action 词表(abort/resume 之外的三个纯 notify 分支——
55
+ * rpcSideNotify 的穷举域;abort/resume 在 handleRpcMode 内 if 早退)。 */
56
+ type WorkflowRpcSideAction = Extract<
57
+ WorkflowRpcAction,
58
+ { action: "lifecycle-removed" | "lifecycle-missing-id" | "noop" }
59
+ >;
60
+
54
61
  // ── /workflows command ───────────────────────────────────────
55
62
 
56
63
  /**
@@ -129,41 +136,7 @@ export function registerWorkflowsCommand(
129
136
  return;
130
137
  }
131
138
 
132
- // TUI 动词命令:/workflows resume <runId>——执行断点续跑后 notify,不打开面板
133
- // (abort 动词在 TUI 侧无既有通道,不在此顺手扩——resume 是本批接入面)
134
- const trimmedArgs = args.trim();
135
- const [verb, ...rest] = trimmedArgs.split(/\s+/).filter(Boolean);
136
- if (verb === "resume") {
137
- const runId = rest[0];
138
- if (!runId) {
139
- ctx.ui.notify("Usage: /workflows resume <runId> [model]", "warning");
140
- return;
141
- }
142
- // 可选第二参数 = 显式模型 canonical ref('provider/modelId[:thinkingLevel]')——
143
- // 落统一覆盖记账(F1-26 后续项:与 setModel 补切同通道,resume 合一步表达)。
144
- const model = rest[1];
145
- try {
146
- if (model !== undefined) assertResumeModelInCatalog(model);
147
- await resumeRun(runId, deps, ...(model !== undefined ? [{ model }] : []));
148
- ctx.ui.notify(
149
- `Workflow ${runId}: resuming${model !== undefined ? ` with model ${model}` : ""} — completed calls replay at zero token cost, unfinished calls re-dispatched`,
150
- "info",
151
- );
152
- } catch (err) {
153
- ctx.ui.notify(`Failed to resume workflow ${runId}: ${toErrorMessage(err)}`, "warning");
154
- }
155
- return;
156
- }
157
-
158
- // 直接按 runId / 前缀匹配打开
159
- const directRunId = trimmedArgs;
160
- if (directRunId) {
161
- await openByRunId(directRunId, getRuns, ctx, deps);
162
- return;
163
- }
164
-
165
- // 无参——列表选择
166
- await openFromList(getRuns, ctx, deps);
139
+ await handleTuiCommand(args, ctx, getRuns, deps);
167
140
  },
168
141
  });
169
142
  }
@@ -182,38 +155,36 @@ export function registerWorkflowsCommand(
182
155
  * (F3 定稿——提示语义优先于 missing-id:run 一次性生命周期后不可挂起)
183
156
  * - lifecycle-missing-id → Usage 提示
184
157
  * - noop → 无 action 或未知 action:GUI 端已屏蔽此 command 入口,此处兜底
158
+ *
159
+ * 分派形态:两个 lifecycle 动作 if 早退(控制流收窄,零断言),其余三个纯
160
+ * notify action 收敛进 rpcSideNotify 的穷举 switch。
185
161
  */
186
162
  async function handleRpcMode(
187
163
  parsed: WorkflowRpcAction,
188
164
  ctx: ExtensionCommandContext,
189
165
  deps: LauncherDeps,
190
166
  ): Promise<void> {
167
+ if (parsed.action === "abort") {
168
+ await abortWithNotify(parsed.runId, deps, ctx);
169
+ return;
170
+ }
171
+ if (parsed.action === "resume") {
172
+ await resumeWithNotify(parsed.runId, parsed.model, deps, ctx);
173
+ return;
174
+ }
175
+ rpcSideNotify(parsed, ctx);
176
+ }
177
+
178
+ /**
179
+ * 非 lifecycle RPC action(lifecycle-removed / lifecycle-missing-id / noop)的
180
+ * 穷举 notify 分派。default 保留 exhaustiveness 断言:未来新增 action verb 忘加
181
+ * case 时 tsc 报错。
182
+ */
183
+ function rpcSideNotify(
184
+ parsed: WorkflowRpcSideAction,
185
+ ctx: ExtensionCommandContext,
186
+ ): void {
191
187
  switch (parsed.action) {
192
- case "abort": {
193
- try {
194
- await abortRun(parsed.runId, deps);
195
- // abort 单态后 pastTense 固定(原三态拼接随 pause/resume 删除)
196
- ctx.ui.notify(`Workflow ${parsed.runId}: aborted`, "info");
197
- } catch (err) {
198
- const msg = toErrorMessage(err);
199
- ctx.ui.notify(`Failed to abort workflow ${parsed.runId}: ${msg}`, "warning");
200
- }
201
- return;
202
- }
203
- case "resume": {
204
- try {
205
- if (parsed.model !== undefined) assertResumeModelInCatalog(parsed.model);
206
- await resumeRun(parsed.runId, deps, ...(parsed.model !== undefined ? [{ model: parsed.model }] : []));
207
- ctx.ui.notify(
208
- `Workflow ${parsed.runId}: resuming${parsed.model !== undefined ? ` with model ${parsed.model}` : ""} — completed calls replay at zero token cost, unfinished calls re-dispatched`,
209
- "info",
210
- );
211
- } catch (err) {
212
- const msg = toErrorMessage(err);
213
- ctx.ui.notify(`Failed to resume workflow ${parsed.runId}: ${msg}`, "warning");
214
- }
215
- return;
216
- }
217
188
  case "lifecycle-removed":
218
189
  ctx.ui.notify(
219
190
  `Workflow ${parsed.verb} has been removed — runs are one-shot. To stop a run early: /workflows abort <runId>`,
@@ -223,12 +194,7 @@ async function handleRpcMode(
223
194
  case "lifecycle-missing-id":
224
195
  // Usage 按 verb 区分(与 TUI verb 分支同文案——两模式统一):resume 的可选
225
196
  // [model] 参数要出现在提示里,GUI 侧缺 runId 时得到的指引不指向旧用法。
226
- ctx.ui.notify(
227
- parsed.verb === "resume"
228
- ? "Usage: /workflows resume <runId> [model]"
229
- : `Usage: /workflows ${parsed.verb} <runId>`,
230
- "warning",
231
- );
197
+ ctx.ui.notify(missingIdUsageText(parsed.verb), "warning");
232
198
  return;
233
199
  case "noop":
234
200
  ctx.ui.notify("View workflows in the composer task tray", "info");
@@ -241,6 +207,100 @@ async function handleRpcMode(
241
207
  }
242
208
  }
243
209
 
210
+ /** lifecycle-missing-id 的 Usage 文案:resume 带可选 [model] 参数,其余 verb 仅 runId。 */
211
+ function missingIdUsageText(verb: "abort" | "resume"): string {
212
+ return verb === "resume" ? "Usage: /workflows resume <runId> [model]" : `Usage: /workflows ${verb} <runId>`;
213
+ }
214
+
215
+ /** abort 执行 + 结果通知(成功/失败均 notify,不向上抛——RPC 通道唯一 abort 入口)。 */
216
+ async function abortWithNotify(
217
+ runId: string,
218
+ deps: LauncherDeps,
219
+ ctx: ExtensionCommandContext,
220
+ ): Promise<void> {
221
+ try {
222
+ await abortRun(runId, deps);
223
+ // abort 单态后 pastTense 固定(原三态拼接随 pause/resume 删除)
224
+ ctx.ui.notify(`Workflow ${runId}: aborted`, "info");
225
+ } catch (err) {
226
+ ctx.ui.notify(`Failed to abort workflow ${runId}: ${toErrorMessage(err)}`, "warning");
227
+ }
228
+ }
229
+
230
+ /**
231
+ * resume 执行 + 结果通知(TUI verb / RPC verb 两通道共享——同语义同文案,防
232
+ * 双处文案漂移)。显式 model 先做目录预检(assertResumeModelInCatalog),成功/
233
+ * 失败均 notify(不向上抛)。
234
+ */
235
+ async function resumeWithNotify(
236
+ runId: string,
237
+ model: string | undefined,
238
+ deps: LauncherDeps,
239
+ ctx: ExtensionCommandContext,
240
+ ): Promise<void> {
241
+ try {
242
+ if (model !== undefined) assertResumeModelInCatalog(model);
243
+ await resumeRun(runId, deps, ...resumeModelArgs(model));
244
+ ctx.ui.notify(resumeStartedText(runId, model), "info");
245
+ } catch (err) {
246
+ ctx.ui.notify(`Failed to resume workflow ${runId}: ${toErrorMessage(err)}`, "warning");
247
+ }
248
+ }
249
+
250
+ /** 可选 model → resumeRun 变参(无覆盖 = 空变参,不造覆盖记账)。 */
251
+ function resumeModelArgs(model: string | undefined): [] | [{ model: string }] {
252
+ return model !== undefined ? [{ model }] : [];
253
+ }
254
+
255
+ /** resume 启动通知文案(带显式 model 时回显模型,用户可确认覆盖生效)。 */
256
+ function resumeStartedText(runId: string, model: string | undefined): string {
257
+ return `Workflow ${runId}: resuming${model !== undefined ? ` with model ${model}` : ""} — completed calls replay at zero token cost, unfinished calls re-dispatched`;
258
+ }
259
+
260
+ /**
261
+ * TUI 模式命令分派:resume verb → 断点续跑后 notify(不打开面板);带参 → 按
262
+ * runId/前缀匹配打开;无参 → 列表选择。(abort 动词在 TUI 侧无既有通道,不在此
263
+ * 顺手扩——resume 是本批接入面)
264
+ */
265
+ async function handleTuiCommand(
266
+ args: string,
267
+ ctx: ExtensionCommandContext,
268
+ getRuns: () => Map<string, WorkflowRun>,
269
+ deps: LauncherDeps,
270
+ ): Promise<void> {
271
+ const trimmedArgs = args.trim();
272
+ const [verb, ...rest] = trimmedArgs.split(/\s+/).filter(Boolean);
273
+ if (verb === "resume") {
274
+ await resumeTuiVerb(rest, ctx, deps);
275
+ return;
276
+ }
277
+ // 直接按 runId / 前缀匹配打开
278
+ if (trimmedArgs) {
279
+ await openByRunId(trimmedArgs, getRuns, ctx, deps);
280
+ return;
281
+ }
282
+ // 无参——列表选择
283
+ await openFromList(getRuns, ctx, deps);
284
+ }
285
+
286
+ /**
287
+ * TUI resume verb:缺 runId 给 Usage 提示。可选第二参数 = 显式模型 canonical ref
288
+ * ('provider/modelId[:thinkingLevel]')——落统一覆盖记账(F1-26 后续项:与
289
+ * setModel 补切同通道,resume 合一步表达)。执行与通知复用 RPC 同款 resumeWithNotify。
290
+ */
291
+ async function resumeTuiVerb(
292
+ rest: string[],
293
+ ctx: ExtensionCommandContext,
294
+ deps: LauncherDeps,
295
+ ): Promise<void> {
296
+ const runId = rest[0];
297
+ if (!runId) {
298
+ ctx.ui.notify("Usage: /workflows resume <runId> [model]", "warning");
299
+ return;
300
+ }
301
+ await resumeWithNotify(runId, rest[1], deps, ctx);
302
+ }
303
+
244
304
  /**
245
305
  * 命令通道显式 model 的目录预检(D8 拒单语义,与 tool 通道 run/resume action 同款
246
306
  * ——isModelRef 语法闸在 core resumeRun,此处补目录存在性:语法合法但目录查无
@@ -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,同源同处生成。