@zhushanwen/pi-subagent-workflow 9.4.1 → 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.
@@ -1,25 +1,28 @@
1
1
  /**
2
- * tool 提示词 / schema 契约合一(2026-09-27 测试审计组 6:三源并一文件)。
2
+ * tool 提示词 / schema 契约合一(2026-09-27 测试审计组 6:三源并一文件;
3
+ * tool-declaration-slimming D3/U-B1 重写:断言对象随 subagent→三工具、workflow→两工具
4
+ * 拆分重组,锚词清单逐条重新落位)。
3
5
  *
4
6
  * 断言对象分两类:
5
7
  * 1. 源码文本断言(读 .ts 文本非 import——避免 pi-ai/typebox/pi-tui 等值导入的
6
8
  * 重 mock 链):
7
- * - subagent tool description 行为约束器契约(原 subagent-tool-prompt.test.ts)。
9
+ * - subagent_start/manage/message 三工具声明契约(锚词清单 D2 契约拆分后重排)。
8
10
  * agent(LLM)决策时唯一能看到的 tool 元信息就是 description;本组断言锁
9
11
  * 调用信号(何时委派)、能力边界(cannot)、反模式密度、注入防御等约束措辞,
10
12
  * 防后续重构把约束删掉或弱化。
11
- * - workflow tool promptGuidelines 契约(原 workflow-tool-prompt.test.ts)。
13
+ * - workflow_run/workflow_control 两工具 promptGuidelines 契约。
12
14
  * 若 promptGuidelines 不提及内置 workflow 与交叉引用,LLM 无法知道有现成
13
15
  * 编排工具可用,会倾向自己 generate 脚本或瞎猜 name。
14
16
  * - runtime handler 错误文案 Correct 正例:subagent 侧锚 core 源码(D6② 下沉
15
17
  * 后文案权威源在 core subagent-actions-core);workflow 侧原 grep 条已删,
16
18
  * 行为版在 tool-workflow.test.ts 平铺检测 throw 条。
17
19
  * 2. schema 数据断言(JSON 往返剥离 typebox 类型包装,只看数据形态):
18
- * collect / conversation 退役契约(原 subagent-schema-collect.test.ts)。
19
- * required 断言条已删——本包 vitest 把 typebox alias 到 mocks/typebox.ts
20
- * (丢 options),required 恒缺省致断言提前 return 零执行;真实 typebox 下的
21
- * required/enum/pattern 锁由 structured-output cross-package-contract.test.ts
22
- * 承担。
20
+ * collect / conversation 退役契约 + 拆分必填性结构锚。
21
+ * required 的真实 typebox 锁由 structured-output cross-package-contract.test.ts
22
+ * 承担(本包 vitest 把 typebox alias 到 mocks/typebox.ts,丢 options)。
23
+ *
24
+ * 旧名残留负锚(U-B1 验收条款③的机器化):全部声明面源文件的模型可见文案零
25
+ * 旧工具名指引(`subagent` / `workflow` backtick 指引、action:"start" 旧调用形态)。
23
26
  *
24
27
  * 框架:vitest(禁 node:test)。
25
28
  */
@@ -30,7 +33,11 @@ import { createRequire } from "node:module";
30
33
 
31
34
  import { describe, expect, it } from "vitest";
32
35
 
33
- import { SubagentParams } from "../subagent-tool-schema.ts";
36
+ import {
37
+ SubagentManageParams,
38
+ SubagentMessageParams,
39
+ SubagentStartParams,
40
+ } from "../subagent-tool-schema.ts";
34
41
 
35
42
  const __dirname = dirname(fileURLToPath(import.meta.url));
36
43
 
@@ -42,20 +49,56 @@ const TOOL_WORKFLOW_SCRIPT_SRC = readFileSync(
42
49
  join(__dirname, "../tool-workflow-script.ts"),
43
50
  "utf-8",
44
51
  );
52
+ const TOOL_SUBAGENTS_SRC = readFileSync(join(__dirname, "../tool-subagents.ts"), "utf-8");
53
+ // 跨工具通用纪律的收敛点(tool-declaration-slimming D1 单源化终态的注入段侧锚源)。
54
+ const SUBAGENT_LIST_INJECTOR_SRC = readFileSync(
55
+ join(__dirname, "../../../injectors/subagent-list-injector.ts"),
56
+ "utf-8",
57
+ );
58
+ const WORKFLOW_LIST_INJECTOR_SRC = readFileSync(
59
+ join(__dirname, "../../../injectors/workflow-list-injector.ts"),
60
+ "utf-8",
61
+ );
62
+
63
+ /** 按工具名分割注册段:registerTool({... name: "<tool>" ...}) 的文本切片映射。 */
64
+ function toolSections(src: string): Record<string, string> {
65
+ const map: Record<string, string> = {};
66
+ for (const part of src.split("pi.registerTool({").slice(1)) {
67
+ const m = part.match(/name: "([^"]+)"/);
68
+ if (m) map[m[1]] = part;
69
+ }
70
+ return map;
71
+ }
72
+
73
+ const SUBAGENT_SECTIONS = toolSections(SUBAGENT_TOOL_SRC);
74
+ const WORKFLOW_SECTIONS = toolSections(TOOL_WORKFLOW_SRC);
45
75
 
46
- /** 提取 description: `...` 模板字符串的原始内容。 */
76
+ function section(sections: Record<string, string>, name: string): string {
77
+ const s = sections[name];
78
+ if (s === undefined) throw new Error(`tool section not found: ${name}`);
79
+ return s;
80
+ }
81
+
82
+ const START_SECTION = section(SUBAGENT_SECTIONS, "subagent_start");
83
+ const MANAGE_SECTION = section(SUBAGENT_SECTIONS, "subagent_manage");
84
+ const MESSAGE_SECTION = section(SUBAGENT_SECTIONS, "subagent_message");
85
+ const RUN_SECTION = section(WORKFLOW_SECTIONS, "workflow_run");
86
+ const CONTROL_SECTION = section(WORKFLOW_SECTIONS, "workflow_control");
87
+
88
+ /** 提取 section 内 description: `...` 模板字符串的原始内容。 */
47
89
  function extractDescription(src: string): string {
48
90
  const m = src.match(/description:\s*`([\s\S]*?)`,/);
49
91
  if (!m) throw new Error("description template literal not found");
50
92
  return m[1];
51
93
  }
52
94
 
53
- const DESCRIPTION = extractDescription(SUBAGENT_TOOL_SRC);
95
+ const START_DESCRIPTION = extractDescription(START_SECTION);
54
96
 
55
97
  /** 截取 promptGuidelines 数组文本——防 KNOWN_ARG_KEYS 等注释/代码中的裸词污染断言。
56
98
  * 数到数组闭合(跳过字符串字面量内的括号,exec-review F9:'],' 序列静默截断)。 */
57
99
  function promptGuidelinesText(src: string): string {
58
100
  const start = src.indexOf("promptGuidelines: [");
101
+ if (start < 0) return "";
59
102
  let depth = 0;
60
103
  let inStr = false;
61
104
  for (let i = start; i < src.length; i++) {
@@ -83,144 +126,251 @@ function isRecord(v: unknown): v is Record<string, unknown> {
83
126
  }
84
127
 
85
128
  /** schema 的可检视图:经 JSON 往返剥离 typebox 类型包装,只看数据形态。 */
86
- function schemaData(): Record<string, unknown> {
87
- const raw: unknown = JSON.parse(JSON.stringify(SubagentParams));
88
- if (!isRecord(raw)) throw new Error("SubagentParams is not an object schema");
129
+ function schemaData(schema: unknown, label: string): Record<string, unknown> {
130
+ const raw: unknown = JSON.parse(JSON.stringify(schema));
131
+ if (!isRecord(raw)) throw new Error(`${label} is not an object schema`);
89
132
  return raw;
90
133
  }
91
134
 
92
- function properties(): Record<string, unknown> {
93
- const props = schemaData().properties;
94
- if (!isRecord(props)) throw new Error("SubagentParams has no properties");
135
+ function startProperties(): Record<string, unknown> {
136
+ const props = schemaData(SubagentStartParams, "SubagentStartParams").properties;
137
+ if (!isRecord(props)) throw new Error("SubagentStartParams has no properties");
95
138
  return props;
96
139
  }
97
140
 
98
141
  // ══════════════════════════════════════════════════════════════
99
- // subagent tool description(原 subagent-tool-prompt.test.ts)
142
+ // 旧名残留负锚(U-B1 验收条款③):全部声明面源文件零旧工具名指引
100
143
  // ══════════════════════════════════════════════════════════════
101
144
 
102
- describe("subagent tool description — 行为约束器(非功能说明书)", () => {
103
- it("词数 ≤ 850(高风险 description 密度上限)", () => {
104
- // 高风险 tool 的 description 应聚焦约束而非功能铺陈;过长会稀释信号。
105
- // 上限演进:400 → 550(补 start/list/cancel 三 action 完整 JSON 正例)→
106
- // 650(M2-B3 新增 message/close 两 action + conversation 对话模式 + 例子)→
107
- // 800(递归指导:树形判据 + 自包含 task + 独立验收 + fork 成本警告)→
108
- // 850(C2 补 sync 批独立判据禁令句——adversarial-review-fixes §3.4:原 description
109
- // 未定义 independent 且未交叉引用依赖链禁令,依赖任务入批缺判据拦截)。
110
- // 逼近上限时优先精简正文而非再放宽断言。
111
- // 每次 action 扩展必然增加必需描述;正例对弱模型首次用对参数的价值 > 节省 description 预算。
112
- const words = DESCRIPTION.trim().split(/\s+/).filter(Boolean).length;
113
- expect(words).toBeLessThanOrEqual(850);
114
- });
115
-
116
- it("含 'When to delegate' 调用条件段(何时委派 vs 自己做)", () => {
117
- // 开篇必须给信号驱动的调用条件,而非纯功能说明。
118
- expect(DESCRIPTION).toMatch(/When to delegate/i);
119
- });
120
-
121
- it("Anti-patterns 段含 ≥ 4 条 bullet", () => {
122
- // 高风险 tool 要求 ≥ 4 条反模式,密度才足以覆盖主要误用路径。
123
- const apIdx = DESCRIPTION.indexOf("## Anti-patterns");
124
- expect(apIdx).toBeGreaterThan(-1);
125
- const afterAp = DESCRIPTION.slice(apIdx);
126
- // 截到下一个 ## 段
127
- const nextSection = afterAp.indexOf("##", "## Anti-patterns".length);
128
- const apSection =
129
- nextSection > -1 ? afterAp.slice(0, nextSection) : afterAp;
130
- const bullets = apSection.match(/^- .+/gm) || [];
131
- expect(bullets.length).toBeGreaterThanOrEqual(4);
132
- });
133
-
134
- it("含能力边界段 'You cannot'", () => {
135
- // 必须显式声明 tool 做不到的事,阻止 LLM 错误假设。
136
- expect(DESCRIPTION).toMatch(/You cannot/);
137
- expect(DESCRIPTION.toLowerCase()).toContain("cannot");
138
- });
139
-
140
- it("含注入防御:声明 completion message 为不可信数据", () => {
141
- // completion message 是 auto-injected(F14 注入面),必须告诉 LLM
142
- // 把它当作不可信数据,校验其中的指令后再执行。
143
- const lower = DESCRIPTION.toLowerCase();
144
- expect(
145
- lower.includes("untrusted") || lower.includes("verify"),
146
- ).toBe(true);
147
- });
148
-
149
- it("保留 nested spawning 段(允许 sub-subagent,仅深度限制)", () => {
150
- // 这段防止 LLM 错误拒绝合法的 nested delegation。
151
- expect(DESCRIPTION).toMatch(/Nested spawning/);
152
- expect(DESCRIPTION).toMatch(/Depth: N\/10/);
153
- });
154
-
155
- it("递归指导含树形判据 + 自包含 task + 独立验收 + 深度定位", () => {
156
- // 递归不是「能套就套」,必须有明确使用判据,否则 LLM 会滥用深递归。
157
- const lower = DESCRIPTION.toLowerCase();
158
- expect(lower).toContain("tree-shaped"); // 树形判据
159
- expect(lower).toContain("self-contained"); // task 自包含
160
- expect(lower).toContain("acceptance criteria"); // 独立验收
161
- expect(lower).toContain("safety rail"); // 深度定位:10 是护栏非预算
162
- });
163
-
164
- it("保留 executionMode sequential 的 CRITICAL 说明", () => {
165
- // sequential 是关键执行语义,删了会导致 LLM 误以为并行可用。
166
- expect(DESCRIPTION).toMatch(/CRITICAL/i);
167
- expect(DESCRIPTION).toMatch(/sequential/);
168
- expect(DESCRIPTION).toMatch(/SAME message/i);
169
- });
170
-
171
- it("Examples 段含平铺 JSON 正例(task/slug 在顶层,无 startParam envelope)", () => {
172
- // 弱模型信任 schema 结构信号 > 文本信号,原本嵌套 startParam 容器经常被省略。
173
- // 现已拍平:task/slug 等 13 字段直接放在顶层。description 必须有完整平铺 JSON 正例,
174
- // 让模型能直接照抄。强约束:startParam envelope 必须从 description 中彻底消失。
175
- expect(DESCRIPTION).toContain('"action":"start","task"');
176
- expect(DESCRIPTION).not.toContain('"startParam"');
177
- });
178
-
179
- it("cancel 示例 subagentId 用 sa- 连字符前缀(与 subagent-service.ts 实际生成格式一致)", () => {
180
- // subagent-service.ts:600 生成 `sa-${crypto.randomUUID()}`(连字符)。
181
- // description 示例必须与实际生成格式一致——弱模型会照抄示例,前缀错(如 sa_ 下划线)
182
- // 会导致 subagentId 永远匹配不到真实 record。
183
- expect(DESCRIPTION).toContain('"subagentId":"sa-');
184
- expect(DESCRIPTION).not.toContain('"sa_');
185
- });
186
-
187
- it("Examples 示例 agent 值必须是绝对路径 .md 形态(显式 ref 硬守卫拒绝裸名)", () => {
188
- // 显式 agent ref 走硬守卫:getRequiredAgentConfig → loadByPath(ref, true),
189
- // 裸名(normalizeRef 非绝对路径 → null)同步 throw `Invalid agent ref` +
190
- // <available_subagents> 恢复指引(explicit-agent-ref-guard.test.ts 锁定拒绝路径)。
191
- // 示例若教裸名(历史漂移:"agent":"coder"),弱模型照抄即必败调用——浪费一轮
192
- // 并系统性教唆反模式。与 subagentId 格式测试同风格:示例必须与实际契约一致。
193
- // ① 零裸名:任何 "agent":"<非/>" 形态都禁止
194
- expect(DESCRIPTION).not.toMatch(/"agent":"(?!\/)[^"]*"/);
195
- // ② 正例在位且为绝对路径 .md 形态(<available_subagents> 注入的 <location> 形态)
196
- expect(DESCRIPTION).toMatch(/"agent":"\/[^"]*\.md"/);
197
- });
198
-
199
- it("agent 字段 description 不写死枚举,指向 <available_subagents>(通用化防漂移)", () => {
200
- // 原实现把 9 个内置 agent 名写死在 schema field description(防漏),
201
- // 但枚举与包内 agents/*.md 存在漂移风险(新增/删除 agent 要手改两处)。
202
- // 现改为指向每 turn 动态注入的 <available_subagents> 列表——防漏职责由注入段承担,
203
- // 工具描述只保留通用指引(2026-08 通用化重构)。
145
+ describe("旧名残留负锚(U-B1):声明面源文件零旧工具名指引", () => {
146
+ // 负锚形态界定:backtick 精确包裹旧名(`subagent` / `workflow`)= 模型可见的
147
+ // 工具名指引;代码标识符(registerSubagentTool)、两键控集合保留的旧名、历史注释
148
+ // 不在声明面(验收条款③除外项)。旧调用形态 action:"start" 同属拆分前结构指引。
149
+ const DECLARATION_SOURCES: Array<[string, string]> = [
150
+ ["subagent-tool.ts", SUBAGENT_TOOL_SRC],
151
+ ["tool-workflow.ts", TOOL_WORKFLOW_SRC],
152
+ ["tool-subagents.ts", TOOL_SUBAGENTS_SRC],
153
+ ["tool-workflow-script.ts", TOOL_WORKFLOW_SCRIPT_SRC],
154
+ ["subagent-list-injector.ts", SUBAGENT_LIST_INJECTOR_SRC],
155
+ ["workflow-list-injector.ts", WORKFLOW_LIST_INJECTOR_SRC],
156
+ ];
157
+
158
+ for (const [file, src] of DECLARATION_SOURCES) {
159
+ it(`${file}: 零 backtick 旧名指引与旧调用形态`, () => {
160
+ expect(src, `${file} 含 backtick 旧名 \`subagent\``).not.toContain("`subagent`");
161
+ expect(src, `${file} 含 backtick 旧名 \`workflow\``).not.toContain("`workflow`");
162
+ });
163
+ }
164
+
165
+ it("subagent-tool.ts: 零拆分前 action:\"start\" 旧调用形态(start 工具无 action 字段)", () => {
166
+ expect(SUBAGENT_TOOL_SRC).not.toContain('action:"start"');
167
+ expect(SUBAGENT_TOOL_SRC).not.toContain('\\"action\\":\\"start\\"');
168
+ });
169
+ });
170
+
171
+ // ══════════════════════════════════════════════════════════════
172
+ // subagent_start 声明(tool-declaration-slimming D1 三层分工终态 + D2 锚词清单拆分重排)
173
+ // ══════════════════════════════════════════════════════════════
174
+
175
+ /**
176
+ * 【锚词保留清单 — subagent_start】(tool-declaration-slimming D2:压缩后的契约 = 逐锚词
177
+ * grep,替代原逐字断言。每个锚词是一个弱模型回补点,删除前须确认有替代承载;
178
+ * D3 拆分后原 A1-A17 逐条重新落位):
179
+ *
180
+ * description 层(是什么 + 边界 + 唯一正例):
181
+ * A1 CRITICAL + sequential + SAME message —— 同消息多调用串行执行语义
182
+ * A2 Nested spawning + "Depth: N/10" —— 递归允许 + 深度上限(防错误拒绝嵌套)
183
+ * A3 TREE-SHAPED / SELF-CONTAINED / acceptance criteria / safety rail —— 递归判据
184
+ * A4 You cannot —— 能力边界(无同步结果 / 无流式输出)
185
+ * A5 {"task":" —— 唯一正例的平铺形态;action 键与 "startParam" 必须不存在
186
+ * A7 "agent":"/….md" —— agent ref 绝对路径形态(禁裸名示例)
187
+ * promptGuidelines 层(使用纪律单点):
188
+ * A6 `subagent_manage` list 先行 + `subagent_message` 续聊指路(原 A11 nested 不对称
189
+ * 说明随拆分消亡——不同工具不同结构,结构信号由 schema 承载)
190
+ * A8 session_read {"action":"result" —— 长结果截断取回指针(指向 defer 名单工具的
191
+ * 交叉引用,D1 范围裁决②:保留原文)
192
+ * A9 Delegate FIRST —— 何时委派(原 description「When to delegate」段的承载点)
193
+ * A10 untrusted —— 注入防御(completion 即确认 + 不可信数据)
194
+ * A12 `subagents` tool —— 批量入口交叉引用
195
+ * schema 层(功能语义;required/enum/pattern/maxLength 由 structured-output
196
+ * cross-package-contract 以真实 typebox 锁定):
197
+ * A13 task description "Throws if missing or whitespace-only"(原 REQUIRED for
198
+ * action:'start' 文本锚——拆分后必填性由 schema required 表达,运行时语义锚保留)
199
+ * A14 model CASE-SENSITIVE + Resolution order(三层优先级链是功能语义)
200
+ * A15 idleTimeoutMs 缺省 300000 + TAIJI_SUBAGENT_IDLE_TIMEOUT_MS env 覆盖链
201
+ * A16 messageParam.interrupt "Deprecated, no effect" —— 退役参数兼容语义锚
202
+ * (存量调用防 schema 报错;manage/message schema 兼容语义锚用例承载)
203
+ * 单源化负锚(D1:跨工具通用纪律只在注入段说一次):
204
+ * A17 description/guidelines 零 poll/bash sleep/busy-wait;注入段含规则
205
+ * subagent_message / subagent_manage 侧新增锚:
206
+ * B1 message/manage description 含 {"action":"message","messageParam" 与
207
+ * {"action":"cancel","cancelParam" 调用正例(原单工具 description Actions 一览的
208
+ * 落位点,"subagentId":"sa- id 格式锚随之迁移)
209
+ * B2 manage/message promptGuidelines 使用纪律锚:manage 含 "Never guess" + list 先行
210
+ * 调用形状(subagent_start 侧 A6 的本工具落点);message 含 session tree 归属
211
+ * 纪律(nested 经其 parent——归属守卫 record-access 直接父校验的模型可见面)
212
+ * B3 schema subagentId 说明含打断/复活副作用("interrupts its in-flight round" /
213
+ * "starts a new round")——必填字段说明是模型填参必读面,打断语义不依赖
214
+ * deprecated interrupt 字段(interrupt 只声明 no-op 本身)
215
+ */
216
+ describe("subagent_start description — 锚词保留清单(D2/D3 契约)", () => {
217
+ it("体量上界:description ≤ 2400 字符(D2 目标区间 subagent 组 ≤8500 的分维护栏)", () => {
218
+ // description 只承载「是什么 + 边界 + 唯一正例」;使用纪律在
219
+ // promptGuidelines(见 A6/A8-A12),参数功能语义在 schema(A13-A15)。
220
+ // 逼近上界时先砍正文冗余,不得砍锚词(A1-A7)。
221
+ expect(START_DESCRIPTION.length).toBeLessThanOrEqual(2400);
222
+ });
223
+
224
+ it("执行语义锚(A1)+ 递归锚(A2/A3)+ 能力边界锚(A4)", () => {
225
+ expect(START_DESCRIPTION).toMatch(/CRITICAL/i);
226
+ expect(START_DESCRIPTION).toMatch(/sequential/);
227
+ expect(START_DESCRIPTION).toMatch(/SAME message/i);
228
+ expect(START_DESCRIPTION).toMatch(/Nested spawning/);
229
+ expect(START_DESCRIPTION).toMatch(/Depth: N\/10/);
230
+ const lower = START_DESCRIPTION.toLowerCase();
231
+ expect(lower).toContain("tree-shaped");
232
+ expect(lower).toContain("self-contained");
233
+ expect(lower).toContain("acceptance criteria");
234
+ expect(lower).toContain("safety rail");
235
+ expect(START_DESCRIPTION).toMatch(/You cannot/);
236
+ expect(START_DESCRIPTION.toLowerCase()).toContain("cannot");
237
+ });
238
+
239
+ it("正例锚(A5/A7):平铺形态唯一正例 + agent 绝对路径,反例形态零残留", () => {
240
+ expect(START_DESCRIPTION).toContain('{"task":"');
241
+ // 拆分后 start 无 action 键:正例与正文中不得复活旧调用结构
242
+ expect(START_DESCRIPTION).not.toContain('"action"');
243
+ expect(START_DESCRIPTION).not.toContain('"startParam"');
244
+ // agent ref 硬守卫拒绝裸名——示例必须教绝对路径 .md 形态(<available_subagents>
245
+ // 注入的 <location> 形态)。
246
+ expect(START_DESCRIPTION).not.toMatch(/"agent":"(?!\/)[^"]*"/);
247
+ expect(START_DESCRIPTION).toMatch(/"agent":"\/[^"]*\.md"/);
248
+ });
249
+
250
+ it("promptGuidelines 承载使用纪律(A6/A8-A12):何时委派 / list 先行 / 注入防御 / 截断指针 / 批量入口", () => {
251
+ const guidelines = promptGuidelinesText(START_SECTION);
252
+ expect(guidelines).toContain("Delegate FIRST");
253
+ expect(guidelines).toContain("untrusted");
254
+ expect(guidelines).toContain("`subagent_manage`");
255
+ // guidelines 源码内 JSON 是转义形态(\"action\")
256
+ expect(guidelines).toContain('{\\"action\\":\\"list\\"}');
257
+ expect(guidelines).toContain("Never guess an id");
258
+ // A8:defer 名单工具交叉引用保留原文(D1 范围裁决②;源码内是转义形态)
259
+ expect(START_SECTION).toContain('session_read {\\"action\\":\\"result\\"');
260
+ expect(guidelines).toContain("`subagents` tool");
261
+ });
262
+
263
+ it("agent 字段通用化(防漂移):description 不写死 agent 名,指向 <available_subagents>", () => {
264
+ // agent 枚举随 agents/*.md 动态变化,防漏职责由每 turn 注入的 <available_subagents>
265
+ // 列表承担;工具声明只保留通用指引。
204
266
  expect(SUBAGENT_TOOL_SRC).toContain("available_subagents");
205
- // 通用化约束:不写死任何具体 agent 名(名字随 agents/*.md 动态变化)。
206
- // 断言目标收窄到 DESCRIPTION 串(原打 SUBAGENT_TOOL_SRC 全文——文件任意位置
207
- // 出现 worker 等词即误红,如 import 路径 / 注释 / 标识符)。
208
- expect(DESCRIPTION).not.toMatch(/orchestrator|code-reviewer|context-builder|worker/);
209
- });
210
-
211
- it("Anti-patterns 段明确 list/cancel 仍 nested(防过度泛化 flatten)", () => {
212
- // PR 只拍平 start,listParam/cancelParam 仍 nested。description 必须明确这一不对称性,
213
- // 否则弱模型学了「subagent tool 现在平铺」会过度泛化发 {"action":"list","includeFinished":true}。
214
- const apIdx = DESCRIPTION.indexOf("## Anti-patterns");
215
- expect(apIdx).toBeGreaterThan(-1);
216
- const afterAp = DESCRIPTION.slice(apIdx);
217
- const nextSection = afterAp.indexOf("##", "## Anti-patterns".length);
218
- const apSection = nextSection > -1 ? afterAp.slice(0, nextSection) : afterAp;
219
- expect(apSection).toMatch(/list.*nested.*listParam|listParam.*nested/i);
267
+ expect(START_DESCRIPTION).not.toMatch(/orchestrator|code-reviewer|context-builder|worker/);
268
+ });
269
+
270
+ it("schema 功能语义锚(A13-A15)", () => {
271
+ // 拒绝条件清单不进 schema 说明(core handler 运行时错误已带恢复动作,D1);
272
+ // 必填性 / 优先级链 / 缺省值是 schema 侧唯一文本锚。A13 随拆分改形态:
273
+ // required 结构锚由 cross-package-contract 锁,此处锁运行时语义句。
274
+ const schemaSrc = readFileSync(join(__dirname, "../subagent-tool-schema.ts"), "utf-8");
275
+ expect(schemaSrc).toContain("Throws if missing or whitespace-only");
276
+ expect(schemaSrc).toContain("CASE-SENSITIVE");
277
+ expect(schemaSrc).toContain("Resolution order");
278
+ expect(schemaSrc).toContain("TAIJI_SUBAGENT_IDLE_TIMEOUT_MS");
279
+ });
280
+
281
+ it("勿等待纪律单源化(A17):description/guidelines 零 poll 措辞,规则只在注入段说一次(D1)", () => {
282
+ // 「结果自动通知送达、勿 bash sleep/busy-wait/poll 等待」是 subagent 组/
283
+ // subagents/workflow 组共享的调度纪律,收敛到 <available_subagents> 注入段说一次。
284
+ // completion 即确认的 subagent 特有语义由 A10 锚(untrusted)承接。
285
+ expect(START_DESCRIPTION).not.toMatch(/bash sleep|busy-wait|bash-sleeping|polling/i);
286
+ expect(SUBAGENT_LIST_INJECTOR_SRC).toContain("busy-wait, or poll");
287
+ expect(SUBAGENT_LIST_INJECTOR_SRC).toContain("SUBAGENT_DISPATCH_SHARED_RULES");
288
+ });
289
+
290
+ it("注入段工具名清单随拆分更新(D1 收敛点覆盖新名全集)", () => {
291
+ expect(SUBAGENT_LIST_INJECTOR_SRC).toContain(
292
+ "(subagent_start, subagent_manage, subagent_message, subagents, workflow_run, workflow_control tools)",
293
+ );
294
+ });
295
+
296
+ it("空态也承载调度纪律:工厂 emptyGuide 接 SUBAGENT_DISPATCH_SHARED_RULES(零 agent 环境纪律不消失)", () => {
297
+ // formatAgentList 空列表返回空串、空态段由工厂 formatEmptyResourceList 接管——
298
+ // 工厂配置不传 emptyGuide 时 SUBAGENT_DISPATCH_SHARED_RULES 在零 agent 环境
299
+ // 零承载(D1 单源化被击穿:勿 poll 等待/预算纪律随清单一起消失)。
300
+ // 锚生产 config 的接线形态;空态段行为级断言在 injectors 域
301
+ // workflow-list-injector.test.ts(agent 空态注入含纪律关键词)。
302
+ expect(SUBAGENT_LIST_INJECTOR_SRC).toContain("emptyGuide: SUBAGENT_DISPATCH_SHARED_RULES");
303
+ });
304
+ });
305
+
306
+ // ══════════════════════════════════════════════════════════════
307
+ // subagent_manage / subagent_message 声明(B1 锚:call shapes + sa- id 格式锚落位)
308
+ // ══════════════════════════════════════════════════════════════
309
+
310
+ describe("subagent_manage / subagent_message description — call shapes 与 id 格式锚(B1)", () => {
311
+ it("manage description:三动作 call shapes 正例(list/cancel/close)+ sa- id 格式锚", () => {
312
+ const description = extractDescription(MANAGE_SECTION);
313
+ expect(description).toContain('{"action":"list"');
314
+ expect(description).toContain('{"action":"cancel","cancelParam":{"subagentId":"sa-');
315
+ expect(description).toContain('{"action":"close","closeParam":{"subagentId":"sa-');
316
+ // A6 id 格式锚落位:sa-<uuid> 连字符(弱模型照抄示例,sa_ 下划线永远匹配不到真实 record)
317
+ expect(description).toContain('"subagentId":"sa-');
318
+ expect(description).not.toContain('"sa_');
319
+ });
320
+
321
+ it("message description:两动作 call shapes 正例(message/fork-from)", () => {
322
+ const description = extractDescription(MESSAGE_SECTION);
323
+ expect(description).toContain('{"action":"message","messageParam":{"subagentId":"sa-');
324
+ expect(description).toContain('{"action":"fork-from","forkFromParam":{"sourceSubagentId":"sa-');
325
+ expect(description).not.toContain('"sa_');
326
+ });
327
+
328
+ it("manage/message schema 兼容语义锚(A16):interrupt deprecated no-op 保留", () => {
329
+ const schemaSrc = readFileSync(join(__dirname, "../subagent-tool-schema.ts"), "utf-8");
330
+ expect(schemaSrc).toContain("Deprecated, no effect");
331
+ });
332
+
333
+ it("manage/message promptGuidelines 使用纪律锚(B2):list 先行 + session tree 归属", () => {
334
+ const manageGuidelines = promptGuidelinesText(MANAGE_SECTION);
335
+ expect(manageGuidelines).toContain("Never guess");
336
+ // guidelines 源码内 JSON 是转义形态(\"action\")
337
+ expect(manageGuidelines).toContain('{\\"action\\":\\"list\\"}');
338
+ expect(manageGuidelines).toContain("compaction can swallow ids");
339
+ const messageGuidelines = promptGuidelinesText(MESSAGE_SECTION);
340
+ expect(messageGuidelines).toContain("Only message subagents you started in this session tree");
341
+ expect(messageGuidelines).toContain("through its parent");
342
+ });
343
+
344
+ it("message 打断/复活语义锚(B3):subagentId 必填字段说明承载副作用,interrupt 只声明 no-op", () => {
345
+ const schemaSrc = readFileSync(join(__dirname, "../subagent-tool-schema.ts"), "utf-8");
346
+ // 只切 messageParam 块(cancel/close 处有同名 subagentId 声明,不在断言面)
347
+ const start = schemaSrc.indexOf("messageParam:");
348
+ const end = schemaSrc.indexOf("forkFromParam:");
349
+ if (start < 0 || end < 0 || end <= start) throw new Error("messageParam block not found");
350
+ const block = schemaSrc.slice(start, end);
351
+ expect(block).toContain("interrupts its in-flight round");
352
+ expect(block).toContain("starts a new round");
353
+ expect(block).toContain("revives on the same id");
354
+ // 单源负锚:语义句不残留在 deprecated interrupt 字段说明里(interrupt 是块内
355
+ // 最后一个声明,切到块尾即其声明全体)
356
+ const interruptDecl = block.slice(block.indexOf("interrupt:"));
357
+ expect(interruptDecl).toContain("Deprecated, no effect");
358
+ expect(interruptDecl).not.toContain("interrupts its in-flight round");
359
+ });
360
+
361
+ it("manage/message description 零 poll 措辞(A17 单源化)", () => {
362
+ const manageDescription = extractDescription(MANAGE_SECTION);
363
+ const messageDescription = extractDescription(MESSAGE_SECTION);
364
+ expect(manageDescription).not.toMatch(/bash sleep|busy-wait|polling/i);
365
+ expect(messageDescription).not.toMatch(/bash sleep|busy-wait|polling/i);
220
366
  });
221
367
  });
222
368
 
223
- describe("subagent tool runtime handler — 错误文案含纠正正例", () => {
369
+ // ══════════════════════════════════════════════════════════════
370
+ // subagent start 运行时守卫(原「handler 错误文案 Correct 正例」组)
371
+ // ══════════════════════════════════════════════════════════════
372
+
373
+ describe("subagent runtime handler — 错误文案含纠正正例", () => {
224
374
  // 读源码文本断言 startHandler throw 含 Correct 正例,
225
375
  // 让弱模型撞错后第二次能直接照抄正确形态。
226
376
  // 拍平后:startParam envelope 删除,平铺 task/slug 是合法形态;
@@ -253,14 +403,14 @@ describe("subagent tool runtime handler — 错误文案含纠正正例", () =>
253
403
  });
254
404
 
255
405
  // ══════════════════════════════════════════════════════════════
256
- // workflow tool promptGuidelines(原 workflow-tool-prompt.test.ts)
406
+ // workflow_run / workflow_control promptGuidelines(原 workflow-tool-prompt.test.ts)
257
407
  // ══════════════════════════════════════════════════════════════
258
408
 
259
- describe("U1: workflow tool prompt mentions built-in workflows", () => {
409
+ describe("workflow_run promptGuidelines 契约(U1/TC4 拆分重排)", () => {
260
410
  it("TC4a: promptGuidelines 不含具体内置 args 枚举(m4 瘦身——参数知识在 read location)", () => {
261
411
  // m4:BUILT-IN 枚举删除,发现职责转移给 <available_workflows> 注入段 + read location。
262
412
  // 截取 promptGuidelines 段断言("batch1..batchN" 等在 KNOWN_ARG_KEYS 注释中出现)。
263
- const guidelines = promptGuidelinesText(TOOL_WORKFLOW_SRC);
413
+ const guidelines = promptGuidelinesText(RUN_SECTION);
264
414
  expect(guidelines).not.toContain("chain (sequential");
265
415
  expect(guidelines).not.toContain("args: task");
266
416
  expect(guidelines).not.toContain("batch1..batchN");
@@ -268,37 +418,46 @@ describe("U1: workflow tool prompt mentions built-in workflows", () => {
268
418
  });
269
419
 
270
420
  it("TC4b: promptGuidelines 引导 read location 获取参数细节(info 已砍,ADR-0003 D5)", () => {
271
- const guidelines = promptGuidelinesText(TOOL_WORKFLOW_SRC);
421
+ const guidelines = promptGuidelinesText(RUN_SECTION);
272
422
  expect(guidelines).toContain("read the <location>");
273
423
  expect(guidelines).toContain("script file");
274
424
  // info action 已砍,引导语不再含 workflow info
275
425
  expect(guidelines).not.toContain("workflow info");
276
426
  });
277
427
 
278
- it("tool-workflow.ts promptGuidelines 含 workflow-script list 交叉引用", () => {
428
+ it("workflow_run promptGuidelines 含 workflow-script 交叉引用(发现路径反向锚)", () => {
279
429
  // LLM 需要知道"先 list 再 run"的发现路径——两个 tool 之间必须有交叉引用。
280
- // 文本可能跨字符串拼接行,分别断言两个关键词都存在。
281
- expect(TOOL_WORKFLOW_SRC).toContain("workflow-script");
282
- expect(TOOL_WORKFLOW_SRC).toMatch(/action.*list/i);
283
- });
284
-
285
- it("tool-workflow.ts promptGuidelines 含 run action 的正例", () => {
286
- // 给出 run 调用的 JSON 示例,LLM 才知道参数格式(action/name/args 嵌套)。
287
- expect(TOOL_WORKFLOW_SRC).toContain('{"action":"run","name":"');
430
+ // 「action:list 先行」的教学落点在 workflow-script 侧 guidelines(拆分后 run 侧
431
+ // 无 list 语义);run 侧锁 generate 禁令交叉引用。
432
+ expect(RUN_SECTION).toContain("workflow-script");
288
433
  });
289
434
 
290
- it("promptGuidelines 含 JSON 调用正例(run/status/lifecycle)", () => {
291
- // 弱模型信任 schema 结构信号 > 文本信号,容易把 args 子字段平铺到顶层。
292
- // promptGuidelines 必须有完整 JSON 调用正例,让模型能直接照抄 {"action":"run",...} 嵌套结构。
293
- expect(TOOL_WORKFLOW_SRC).toContain('{"action":"run"');
294
- expect(TOOL_WORKFLOW_SRC).toContain("Call shapes (JSON)");
435
+ it("workflow_run promptGuidelines 含 run 正例(无 action 键的拆分形态)", () => {
436
+ // 给出 run 调用的 JSON 示例,LLM 才知道参数格式(name/args 嵌套)。
437
+ // guidelines 源码内 JSON 是转义形态(\"name\")。
438
+ expect(promptGuidelinesText(RUN_SECTION)).toContain('{\\"name\\":\\"<script>\\"');
439
+ // 拆分后 run 无 action 字段:正例与正文零 action:"run" 旧结构
440
+ expect(RUN_SECTION).not.toContain('action:"run"');
441
+ expect(RUN_SECTION).not.toContain('\\"action\\":\\"run\\"');
295
442
  });
296
443
 
297
444
  it("promptGuidelines 含参数结构反例(args 平铺到顶层)", () => {
298
445
  // 显式说明 args 子字段不能平铺到顶层,必须嵌在 args 里。
299
- expect(TOOL_WORKFLOW_SRC).toContain("args");
300
- expect(TOOL_WORKFLOW_SRC).toContain("Anti-patterns");
301
- expect(TOOL_WORKFLOW_SRC).toContain("top level");
446
+ expect(promptGuidelinesText(RUN_SECTION)).toContain("args");
447
+ expect(promptGuidelinesText(RUN_SECTION)).toContain("Anti-patterns");
448
+ expect(promptGuidelinesText(RUN_SECTION)).toContain("top level");
449
+ });
450
+
451
+ it("workflow_control promptGuidelines 含 status/abort/resume call shapes 与 resume 语义", () => {
452
+ const guidelines = promptGuidelinesText(CONTROL_SECTION);
453
+ // guidelines 源码内 JSON 是转义形态(\"action\")
454
+ expect(guidelines).toContain('{\\"action\\":\\"status\\"}');
455
+ expect(guidelines).toContain('{\\"action\\":\\"abort\\",\\"runId\\":\\"<id>\\"}');
456
+ expect(guidelines).toContain('{\\"action\\":\\"resume\\",\\"runId\\":\\"<id>\\"');
457
+ expect(guidelines).toContain("ONLY for interrupted runs");
458
+ expect(guidelines).toContain("zero token cost");
459
+ // 跨工具指路:新 run 用 workflow_run
460
+ expect(guidelines).toContain("`workflow_run`");
302
461
  });
303
462
 
304
463
  it("KNOWN_ARG_KEYS 退役守卫(参数知识源 = schema 动态集 argKeysFromMeta,静态枚举不得复活)", () => {
@@ -310,12 +469,12 @@ describe("U1: workflow tool prompt mentions built-in workflows", () => {
310
469
  expect(TOOL_WORKFLOW_SRC).not.toMatch(/const\s+KNOWN_ARG_KEYS/);
311
470
  });
312
471
 
313
- it("tool-workflow-script.ts list action 的 promptGuidelines 含 workflow run 交叉引用", () => {
314
- // 反向交叉引用:list 的指引里要提到用 workflow tool 的 run action 启动脚本。
315
- expect(TOOL_WORKFLOW_SCRIPT_SRC).toMatch(/workflow.*tool.*run|run.*workflow.*tool/i);
472
+ it("tool-workflow-script.ts list action 的 promptGuidelines 含 workflow_run 交叉引用", () => {
473
+ // 反向交叉引用:list 的指引里要提到用 workflow_run 启动脚本。
474
+ expect(TOOL_WORKFLOW_SCRIPT_SRC).toContain("`workflow_run`");
316
475
  });
317
476
 
318
- it("tool-workflow.ts promptGuidelines 强化 anti-generate(直接 run,不要 generate)", () => {
477
+ it("workflow_run promptGuidelines 强化 anti-generate(直接 run,不要 generate)", () => {
319
478
  // session 证据:弱模型看到 workflow list 后倾向 workflow-script generate 而非直接 run。
320
479
  // 提示词必须显式禁止对内置编排使用 generate。
321
480
  expect(TOOL_WORKFLOW_SRC).toContain("NEVER use workflow-script action:generate");
@@ -326,14 +485,27 @@ describe("U1: workflow tool prompt mentions built-in workflows", () => {
326
485
  expect(TOOL_WORKFLOW_SCRIPT_SRC).toContain("NEVER generate");
327
486
  });
328
487
 
329
- it("promptGuidelines + parameter description 标注 budget 默认不限制(B/C:除非用户要求否则别设)", () => {
330
- // budget 是 run 级参数,运行时 maxTokens===undefined → 不限制(budget.ts isExceeded 守卫)。
331
- // 但 call shape 示例展示了 tokens/time 字段,LLM 会误以为每次都该填。
332
- // promptGuidelines 必须明确 "Do NOT set ... unless user explicitly requests",
333
- // parameter description 同步标 "omit = unlimited",双重约束压过示例的反引导。
334
- const guidelines = promptGuidelinesText(TOOL_WORKFLOW_SRC);
335
- expect(guidelines).toContain("Do NOT set tokens/time unless the user explicitly requests");
336
- expect(TOOL_WORKFLOW_SRC).toContain("omit = unlimited (default)");
488
+ it("跨工具预算/模型/勿等待纪律单源化:工具文案零残留,规则只在注入段说一次(D1)", () => {
489
+ // tool-declaration-slimming D1 信息单源化:tokens/time「仅用户明确要求才设」、
490
+ // model/thinkingLevel「缺省继承主 agent」与「勿 poll/bash sleep 等待」原在本工具
491
+ // promptGuidelines 与 schema 说明重复陈述,现收敛到 <available_subagents> 注入段
492
+ // (subagent-list-injector.ts SUBAGENT_DISPATCH_SHARED_RULES)说一次——工具声明
493
+ // 文案零残留 + 注入段含规则。call shape 示例里的 tokens/time 字段展示保留(参数
494
+ // 存在性由 schema 承载,何时设由注入段纪律约束)。
495
+ const runGuidelines = promptGuidelinesText(RUN_SECTION);
496
+ const controlGuidelines = promptGuidelinesText(CONTROL_SECTION);
497
+ expect(runGuidelines).not.toContain("Do NOT set tokens/time unless the user explicitly requests");
498
+ expect(TOOL_WORKFLOW_SRC).not.toContain("omit = unlimited (default)");
499
+ expect(TOOL_WORKFLOW_SRC).not.toContain("Omit to inherit the main agent's model");
500
+ expect(runGuidelines).not.toContain("bash sleep or poll status");
501
+ expect(controlGuidelines).not.toContain("bash sleep or poll status");
502
+ expect(SUBAGENT_LIST_INJECTOR_SRC).toContain("busy-wait, or poll");
503
+ expect(SUBAGENT_LIST_INJECTOR_SRC).toContain("explicitly requests a limit");
504
+ expect(SUBAGENT_LIST_INJECTOR_SRC).toContain("inherit the main agent's model");
505
+ });
506
+
507
+ it("workflow 注入段指路 workflow_run(D3 新名)", () => {
508
+ expect(WORKFLOW_LIST_INJECTOR_SRC).toContain("run directly via the workflow_run tool");
337
509
  });
338
510
  });
339
511
 
@@ -343,19 +515,31 @@ describe("U1: workflow tool prompt mentions built-in workflows", () => {
343
515
 
344
516
  describe("subagent start schema: collect param retired (批量入口 = subagents tool)", () => {
345
517
  it("collect 字段已删(schema 层不可再自报:批量编排唯一入口是 `subagents` tool)", () => {
346
- expect(Object.keys(properties())).not.toContain("collect");
347
- // 数据面全量断言(描述文本里的 collect 措辞随之删除):序列化后零残留。
348
- expect(JSON.stringify(schemaData())).not.toContain("collect");
518
+ const start = schemaData(SubagentStartParams, "SubagentStartParams");
519
+ const manage = schemaData(SubagentManageParams, "SubagentManageParams");
520
+ const message = schemaData(SubagentMessageParams, "SubagentMessageParams");
521
+ expect(Object.keys(start.properties as Record<string, unknown>)).not.toContain("collect");
522
+ // 数据面全量断言(描述文本里的 collect 措辞随之删除):三个拆分 schema 序列化后零残留。
523
+ expect(JSON.stringify(start)).not.toContain("collect");
524
+ expect(JSON.stringify(manage)).not.toContain("collect");
525
+ expect(JSON.stringify(message)).not.toContain("collect");
349
526
  });
350
527
 
351
- it("sits flattened at top level beside task/slug/engine (拍平契约不变)", () => {
352
- const props = Object.keys(properties());
528
+ it("sits flattened at top level beside task/slug/engine (拍平契约不变,D3 必填性真实化)", () => {
529
+ const props = Object.keys(startProperties());
353
530
  for (const sibling of ["task", "slug", "engine"]) {
354
531
  expect(props).toContain(sibling);
355
532
  }
533
+ // 拆分结构负锚:start schema 无 action 字段(action 只在 manage/message)
534
+ expect(props).not.toContain("action");
535
+ expect(props).not.toContain("listParam");
536
+ expect(props).not.toContain("cancelParam");
537
+ expect(props).not.toContain("messageParam");
538
+ expect(props).not.toContain("closeParam");
539
+ expect(props).not.toContain("forkFromParam");
356
540
  });
357
541
 
358
542
  it("conversation param is deleted (modeless 波5:模式消亡,schema 层不可再自报)", () => {
359
- expect(Object.keys(properties())).not.toContain("conversation");
543
+ expect(Object.keys(startProperties())).not.toContain("conversation");
360
544
  });
361
545
  });