@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
|
@@ -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
|
-
* -
|
|
9
|
+
* - subagent_start/manage/message 三工具声明契约(锚词清单 D2 契约拆分后重排)。
|
|
8
10
|
* agent(LLM)决策时唯一能看到的 tool 元信息就是 description;本组断言锁
|
|
9
11
|
* 调用信号(何时委派)、能力边界(cannot)、反模式密度、注入防御等约束措辞,
|
|
10
12
|
* 防后续重构把约束删掉或弱化。
|
|
11
|
-
* -
|
|
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
|
|
19
|
-
* required
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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(
|
|
88
|
-
if (!isRecord(raw)) throw new Error(
|
|
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
|
|
93
|
-
const props = schemaData().properties;
|
|
94
|
-
if (!isRecord(props)) throw new Error("
|
|
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
|
-
//
|
|
142
|
+
// 旧名残留负锚(U-B1 验收条款③):全部声明面源文件零旧工具名指引
|
|
100
143
|
// ══════════════════════════════════════════════════════════════
|
|
101
144
|
|
|
102
|
-
describe("
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
expect(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
expect(
|
|
184
|
-
expect(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
expect(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
406
|
+
// workflow_run / workflow_control promptGuidelines(原 workflow-tool-prompt.test.ts)
|
|
257
407
|
// ══════════════════════════════════════════════════════════════
|
|
258
408
|
|
|
259
|
-
describe("
|
|
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(
|
|
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(
|
|
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("
|
|
428
|
+
it("workflow_run promptGuidelines 含 workflow-script 交叉引用(发现路径反向锚)", () => {
|
|
279
429
|
// LLM 需要知道"先 list 再 run"的发现路径——两个 tool 之间必须有交叉引用。
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
expect(
|
|
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 含
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
expect(
|
|
294
|
-
|
|
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(
|
|
300
|
-
expect(
|
|
301
|
-
expect(
|
|
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 含
|
|
314
|
-
// 反向交叉引用:list 的指引里要提到用
|
|
315
|
-
expect(TOOL_WORKFLOW_SCRIPT_SRC).
|
|
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("
|
|
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("
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
// promptGuidelines
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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(
|
|
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(
|
|
543
|
+
expect(Object.keys(startProperties())).not.toContain("conversation");
|
|
360
544
|
});
|
|
361
545
|
});
|