@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 +19 -5
- package/package.json +8 -8
- package/src/index.ts +9 -2
- package/src/injectors/__tests__/workflow-list-injector.test.ts +11 -4
- package/src/injectors/resource-list-injector.ts +17 -7
- package/src/injectors/subagent-list-injector.ts +27 -2
- package/src/injectors/workflow-list-injector.ts +1 -1
- package/src/interface/gui/subagent-actions.ts +4 -2
- package/src/interface/tool/__tests__/capture-tool.ts +34 -13
- package/src/interface/tool/__tests__/subagent-tool-actions.test.ts +105 -41
- package/src/interface/tool/__tests__/subagent-tool-path-guard.test.ts +16 -12
- package/src/interface/tool/__tests__/tool-prompt-contract.test.ts +359 -175
- package/src/interface/tool/__tests__/tool-subagents.test.ts +28 -13
- package/src/interface/tool/__tests__/tool-workflow.test.ts +80 -59
- package/src/interface/tool/subagent-tool-schema.ts +79 -70
- package/src/interface/tool/subagent-tool.ts +205 -136
- package/src/interface/tool/tool-render.ts +7 -5
- package/src/interface/tool/tool-result.ts +3 -2
- package/src/interface/tool/tool-subagents.ts +39 -40
- package/src/interface/tool/tool-workflow-script.ts +7 -5
- package/src/interface/tool/tool-workflow.ts +164 -116
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
|
-
## 工具面(
|
|
5
|
+
## 工具面(7 tools)
|
|
6
6
|
|
|
7
7
|
| 工具 | action 集合 | 关键参数 |
|
|
8
8
|
|------|------------|---------|
|
|
9
|
-
| `
|
|
10
|
-
| `
|
|
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,
|
|
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` 暂停态——执行被打断但可续跑)。`
|
|
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.
|
|
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.
|
|
66
|
-
"@zhushanwen/subagent-core": "1.5.
|
|
67
|
-
"@zhushanwen/pi-subagent-cli": "0.7.
|
|
68
|
-
"@zhushanwen/zcode-subagent-cli": "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.
|
|
76
|
-
"@zhushanwen/pi-structured-output": "6.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
|
-
* 注册项:
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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)/
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
10
|
-
//
|
|
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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
76
|
+
return byName[names[0]] as T;
|
|
56
77
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* subagent
|
|
3
|
-
* 互补,覆盖
|
|
2
|
+
* subagent 工具组非 start 通道 + renderCall 预解析测试(与 path-guard 的 start 守卫面
|
|
3
|
+
* 互补,覆盖 subagent_manage / subagent_message 的 action 路由全分支、subagent_start
|
|
4
|
+
* 的顶层透传与 renderCall 的 model 预解析降级链)。
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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("
|
|
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 =
|
|
118
|
+
const tool = captureToolByName("subagent_start"); // 槽未注入
|
|
103
119
|
await expect(
|
|
104
|
-
tool.execute("c1", {
|
|
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
|
-
|
|
125
|
+
describe("subagent_manage execute:三 action 通道接线", () => {
|
|
126
|
+
/** 注入最小 fake service 并捕获 manage 工具(三通道共用基座)。 */
|
|
127
|
+
function withService(): SubagentToolView {
|
|
109
128
|
setSubagentService({ execute: vi.fn() } as never);
|
|
110
|
-
|
|
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("
|
|
113
|
-
).rejects.toThrow(
|
|
135
|
+
tool.execute("c0", { action: "list" }, undefined, undefined, undefined),
|
|
136
|
+
).rejects.toThrow(/subagents runtime not initialized/);
|
|
114
137
|
});
|
|
115
|
-
});
|
|
116
138
|
|
|
117
|
-
|
|
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
|
-
|
|
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("
|
|
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 =
|
|
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("
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
+
});
|