@zhushanwen/pi-subagent-workflow 8.7.0 → 8.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -11
- package/package.json +7 -10
- package/skills/workflow-script-format/SKILL.md +32 -13
- package/src/host/__tests__/pi-host.test.ts +23 -2
- package/src/host/pi-host.ts +57 -3
- package/src/index.ts +56 -110
- package/src/injectors/__tests__/engine-awareness.test.ts +2 -2
- package/src/injectors/__tests__/engine-section-stability.test.ts +6 -4
- package/src/injectors/__tests__/model-list-injector.test.ts +18 -21
- package/src/injectors/__tests__/subagent-list-injector.test.ts +54 -14
- package/src/injectors/__tests__/workflow-list-injector.test.ts +26 -12
- package/src/injectors/engine-awareness.ts +0 -4
- package/src/injectors/model-list-injector.ts +15 -58
- package/src/injectors/subagent-list-injector.ts +55 -113
- package/src/injectors/workflow-list-injector.ts +33 -66
- package/src/interface/__tests__/detectors.test.ts +45 -34
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +6 -3
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +216 -0
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +17 -6
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +37 -1
- package/src/interface/bg-notify-render.ts +3 -13
- package/src/interface/command-actions.ts +5 -15
- package/src/interface/commands.ts +1 -1
- package/src/interface/format.ts +15 -4
- package/src/interface/gui-mappers.ts +18 -22
- package/src/interface/helpers.ts +8 -129
- package/src/interface/list-component.ts +1 -1
- package/src/interface/list-shared.ts +1 -1
- package/src/interface/list-view.ts +1 -1
- package/src/interface/subagent-actions.ts +51 -676
- package/src/interface/subagent-tool-schema.ts +1 -4
- package/src/interface/subagent-tool.ts +1 -1
- package/src/interface/subagents.ts +1 -1
- package/src/interface/tool-render.ts +3 -13
- package/src/interface/tool-workflow-script.ts +33 -75
- package/src/interface/tool-workflow.ts +51 -88
- package/src/interface/views/WorkflowsView.ts +3 -11
- package/src/interface/views/format.ts +19 -58
- package/src/jsonl-run-store.ts +134 -222
- package/agents/analyst.md +0 -61
- package/agents/coder.md +0 -70
- package/agents/debugger.md +0 -67
- package/agents/doc-reviewer.md +0 -50
- package/agents/explorer.md +0 -64
- package/agents/general-purpose.md +0 -32
- package/agents/orchestrator.md +0 -63
- package/agents/planner.md +0 -54
- package/agents/researcher.md +0 -65
- package/agents/reviewer.md +0 -74
|
@@ -11,7 +11,10 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
11
11
|
|
|
12
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// C5①:formatModelList + ModelEntry 口径下沉 core barrel——测试改从 barrel 取实现,
|
|
15
|
+
// guide 用 pi 宿主注入文案
|
|
16
|
+
import { formatModelList, type ModelEntry } from "@zhushanwen/subagent-core";
|
|
17
|
+
import { MODEL_LIST_GUIDE } from "../model-list-injector";
|
|
15
18
|
|
|
16
19
|
// ── 测试数据工厂 ────────────────────────────────────────
|
|
17
20
|
|
|
@@ -30,65 +33,59 @@ function entry(overrides: Partial<ModelEntry> = {}): ModelEntry {
|
|
|
30
33
|
// ── 纯函数:formatModelList ─────────────────────────────
|
|
31
34
|
|
|
32
35
|
describe("formatModelList", () => {
|
|
33
|
-
it("空列表返回空串(不注入)",
|
|
34
|
-
|
|
35
|
-
expect(formatModelList([])).toBe("");
|
|
36
|
+
it("空列表返回空串(不注入)", () => {
|
|
37
|
+
expect(formatModelList([], { guide: MODEL_LIST_GUIDE })).toBe("");
|
|
36
38
|
});
|
|
37
39
|
|
|
38
|
-
it("渲染 provider/modelId + name + caps + contextWindow",
|
|
39
|
-
const { formatModelList } = await import("../model-list-injector");
|
|
40
|
+
it("渲染 provider/modelId + name + caps + contextWindow", () => {
|
|
40
41
|
const out = formatModelList([
|
|
41
42
|
entry({ provider: "p1", id: "m1", name: "Model One" }),
|
|
42
|
-
]);
|
|
43
|
+
], { guide: MODEL_LIST_GUIDE });
|
|
43
44
|
expect(out).toContain("<available_provider_models>");
|
|
44
45
|
expect(out).toContain("<model><id>p1/m1</id><name>Model One</name><caps>reasoning</caps><contextWindow>200000</contextWindow></model>");
|
|
45
46
|
expect(out).toContain("</available_provider_models>");
|
|
46
47
|
});
|
|
47
48
|
|
|
48
|
-
it("input 含 image 时 caps 加 vision;无能力时省略 caps 段",
|
|
49
|
-
const { formatModelList } = await import("../model-list-injector");
|
|
49
|
+
it("input 含 image 时 caps 加 vision;无能力时省略 caps 段", () => {
|
|
50
50
|
const out = formatModelList([
|
|
51
51
|
entry({ id: "vision-m", input: ["text", "image"] }),
|
|
52
52
|
entry({ id: "plain-m", reasoning: false }),
|
|
53
|
-
]);
|
|
53
|
+
], { guide: MODEL_LIST_GUIDE });
|
|
54
54
|
expect(out).toContain("<id>zai-coding-cn/vision-m</id>");
|
|
55
55
|
expect(out).toContain("<caps>reasoning,vision</caps>");
|
|
56
56
|
// plain-m:reasoning=false 且无 image → 无 caps 段
|
|
57
57
|
expect(out).toMatch(/<id>zai-coding-cn\/plain-m<\/id><name>[^<]*<\/name><contextWindow>/);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
it("按 (provider, id) 排序——输入乱序输出仍字节稳定(KV cache 前提)",
|
|
61
|
-
const { formatModelList } = await import("../model-list-injector");
|
|
60
|
+
it("按 (provider, id) 排序——输入乱序输出仍字节稳定(KV cache 前提)", () => {
|
|
62
61
|
const models = [
|
|
63
62
|
entry({ provider: "b-prov", id: "z-model" }),
|
|
64
63
|
entry({ provider: "a-prov", id: "y-model" }),
|
|
65
64
|
entry({ provider: "a-prov", id: "x-model" }),
|
|
66
65
|
];
|
|
67
|
-
const out1 = formatModelList([...models]);
|
|
68
|
-
const out2 = formatModelList([...models].reverse());
|
|
66
|
+
const out1 = formatModelList([...models], { guide: MODEL_LIST_GUIDE });
|
|
67
|
+
const out2 = formatModelList([...models].reverse(), { guide: MODEL_LIST_GUIDE });
|
|
69
68
|
expect(out1).toBe(out2);
|
|
70
69
|
// 排序断言:a-prov/x-model 在最前,b-prov 在最后
|
|
71
70
|
const ids = [...out1.matchAll(/<id>([^<]+)<\/id>/g)].map((m) => m[1]);
|
|
72
71
|
expect(ids).toEqual(["a-prov/x-model", "a-prov/y-model", "b-prov/z-model"]);
|
|
73
72
|
});
|
|
74
73
|
|
|
75
|
-
it("排序为码点序而非 locale 序(跨环境字节一致契约,禁 localeCompare)",
|
|
76
|
-
const { formatModelList } = await import("../model-list-injector");
|
|
74
|
+
it("排序为码点序而非 locale 序(跨环境字节一致契约,禁 localeCompare)", () => {
|
|
77
75
|
// 判别样本:码点序 "B"(0x42) < "a"(0x61),而多数 locale 的 localeCompare
|
|
78
76
|
// 会把 "a-model" 排在 "B-model" 前——本断言在 localeCompare 实现下必挂
|
|
79
77
|
const out = formatModelList([
|
|
80
78
|
entry({ provider: "p", id: "a-model" }),
|
|
81
79
|
entry({ provider: "p", id: "B-model" }),
|
|
82
|
-
]);
|
|
80
|
+
], { guide: MODEL_LIST_GUIDE });
|
|
83
81
|
const ids = [...out.matchAll(/<id>([^<]+)<\/id>/g)].map((m) => m[1]);
|
|
84
82
|
expect(ids).toEqual(["p/B-model", "p/a-model"]);
|
|
85
83
|
});
|
|
86
84
|
|
|
87
|
-
it("name 含 XML 特殊字符时转义(防注入段结构破坏)",
|
|
88
|
-
const { formatModelList } = await import("../model-list-injector");
|
|
85
|
+
it("name 含 XML 特殊字符时转义(防注入段结构破坏)", () => {
|
|
89
86
|
const out = formatModelList([
|
|
90
87
|
entry({ provider: "p", id: "m", name: `A<&>"'B` }),
|
|
91
|
-
]);
|
|
88
|
+
], { guide: MODEL_LIST_GUIDE });
|
|
92
89
|
expect(out).toContain("<name>A<&>"'B</name>");
|
|
93
90
|
});
|
|
94
91
|
});
|
|
@@ -99,7 +96,7 @@ describe("setupModelListInjector", () => {
|
|
|
99
96
|
type Handler = (event: unknown, ctx: unknown) => Promise<unknown>;
|
|
100
97
|
|
|
101
98
|
async function setupWithRegistry(models: ModelEntry[], fail = false): Promise<Handler> {
|
|
102
|
-
const mod = await import("../model-list-injector");
|
|
99
|
+
const mod = await import("../model-list-injector"); // handler 注册面(含 MODEL_LIST_GUIDE 注入)
|
|
103
100
|
const handlers: Record<string, Handler> = {};
|
|
104
101
|
const pi = {
|
|
105
102
|
on: (name: string, fn: Handler) => {
|
|
@@ -12,7 +12,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
12
12
|
|
|
13
13
|
import type { DiscoveredResource } from "@zhushanwen/subagent-core/shared/resource-discovery.ts";
|
|
14
14
|
// 共享 mock 基建(vi.mock 工厂 / mock pi / mock ctx):helpers/injector-test-mocks.ts
|
|
15
|
+
// ——必须先于 barrel import 初始化:barrel 的 re-export 链会触发深路径 mock 工厂,
|
|
16
|
+
// 工厂闭包引用本 helpers 的绑定(helpers 文件头「惰性求值」约束)
|
|
15
17
|
import { createDiscoveryModuleMock, createLoggerModuleMock, createMockCtx, createMockPi, type CapturedHandlers } from "./helpers/injector-test-mocks.ts";
|
|
18
|
+
// C5①:formatAgentList 下沉 core barrel——测试改从 barrel 取实现,guide 用 pi 宿主注入文案
|
|
19
|
+
import { formatAgentList } from "@zhushanwen/subagent-core";
|
|
16
20
|
|
|
17
21
|
// ── 稳定 spy:vi.hoisted 保证 resetModules 后引用不变(vi.mock 工厂闭包捕获同一 fn,
|
|
18
22
|
// 故 fresh import 的注入器拿到的 discoverResources === spies.discoverResources,跨
|
|
@@ -26,7 +30,7 @@ vi.mock("@zhushanwen/subagent-core/shared/resource-discovery.ts", () => createDi
|
|
|
26
30
|
vi.mock("@zhushanwen/pi-extension-logger", () => createLoggerModuleMock());
|
|
27
31
|
|
|
28
32
|
// ── 纯函数测试:静态 import(模块级缓存状态不影响纯函数;与下方缓存 describe 隔离) ──
|
|
29
|
-
import {
|
|
33
|
+
import { parseAgentFrontmatter, SUBAGENT_LIST_GUIDE } from "../subagent-list-injector";
|
|
30
34
|
|
|
31
35
|
describe("parseAgentFrontmatter", () => {
|
|
32
36
|
it("解析双引号包裹的 name + description", () => {
|
|
@@ -70,7 +74,7 @@ description: '代码审查'
|
|
|
70
74
|
|
|
71
75
|
describe("formatAgentList", () => {
|
|
72
76
|
it("空列表返回空串(不注入)", () => {
|
|
73
|
-
expect(formatAgentList([])).toBe("");
|
|
77
|
+
expect(formatAgentList([], { guide: SUBAGENT_LIST_GUIDE })).toBe("");
|
|
74
78
|
});
|
|
75
79
|
|
|
76
80
|
it("TC1: when + examples 注入(原样渲染 + escapeXml + 缺省兼容)", () => {
|
|
@@ -86,7 +90,7 @@ describe("formatAgentList", () => {
|
|
|
86
90
|
],
|
|
87
91
|
},
|
|
88
92
|
{ name: "legacy", description: "未迁移 agent", path: "/agents/legacy.md" },
|
|
89
|
-
]);
|
|
93
|
+
], { guide: SUBAGENT_LIST_GUIDE });
|
|
90
94
|
expect(out).toContain("<when>用户要求 review 代码</when>");
|
|
91
95
|
// 正反原样渲染——negative action 含原因文本(评审 M5:渲染器不硬编码)
|
|
92
96
|
expect(out).toContain('"帮我 review 这段代码" → 调用 reviewer 对抗式审查');
|
|
@@ -99,29 +103,34 @@ describe("formatAgentList", () => {
|
|
|
99
103
|
path: "/agents/x.md",
|
|
100
104
|
examples: [{ match: "处理 <task> 的 diff", action: "调用 x", positive: true }],
|
|
101
105
|
},
|
|
102
|
-
]);
|
|
106
|
+
], { guide: SUBAGENT_LIST_GUIDE });
|
|
103
107
|
expect(xmlOut).toContain("<task>");
|
|
104
108
|
// 缺省兼容:无 when/examples 的 agent 不渲染该段
|
|
105
|
-
const legacyOut = formatAgentList([{ name: "legacy", description: "未迁移 agent", path: "/agents/legacy.md" }]);
|
|
109
|
+
const legacyOut = formatAgentList([{ name: "legacy", description: "未迁移 agent", path: "/agents/legacy.md" }], { guide: SUBAGENT_LIST_GUIDE });
|
|
106
110
|
expect(legacyOut).not.toContain("<when>");
|
|
107
111
|
expect(legacyOut).not.toContain("<examples>");
|
|
108
112
|
});
|
|
109
113
|
|
|
110
114
|
it("包含 P3 正向触发引导语(何时该 delegate)", () => {
|
|
111
|
-
const out = formatAgentList([{ name: "worker", description: "d", path: "/agents/worker.md" }]);
|
|
115
|
+
const out = formatAgentList([{ name: "worker", description: "d", path: "/agents/worker.md" }], { guide: SUBAGENT_LIST_GUIDE });
|
|
112
116
|
expect(out).toContain("PRIORITY");
|
|
113
117
|
expect(out).toContain("3+ files");
|
|
114
118
|
expect(out).toContain("delegate");
|
|
115
119
|
expect(out).toContain("FIRST");
|
|
116
120
|
});
|
|
117
121
|
|
|
118
|
-
it("
|
|
119
|
-
const out = formatAgentList([{ name: "worker", description: "d", path: "/agents/worker.md" }]);
|
|
120
|
-
expect(out).toContain("ONLY use
|
|
122
|
+
it("C5① guide 更新:agent 引用改为 <location> 路径形态,动态 agent 指引改为缺省 general-purpose(systemPrompt 参数已不在 schema)", () => {
|
|
123
|
+
const out = formatAgentList([{ name: "worker", description: "d", path: "/agents/worker.md" }], { guide: SUBAGENT_LIST_GUIDE });
|
|
124
|
+
expect(out).toContain("ONLY use agents from this list");
|
|
125
|
+
expect(out).toContain("pass the <location> path (absolute .md path) as the agent param");
|
|
126
|
+
expect(out).toContain("omit agent (a general-purpose agent is used)");
|
|
127
|
+
// 旧句已过期(subagent tool schema 无 systemPrompt 参数,见 subagent-tool-schema.ts)
|
|
128
|
+
expect(out).not.toContain("systemPrompt");
|
|
129
|
+
expect(out).not.toContain("ONLY use agent names from this list");
|
|
121
130
|
});
|
|
122
131
|
|
|
123
132
|
it("包含 'Do NOT call list to discover' 引导语", () => {
|
|
124
|
-
const out = formatAgentList([{ name: "worker", description: "d", path: "/agents/worker.md" }]);
|
|
133
|
+
const out = formatAgentList([{ name: "worker", description: "d", path: "/agents/worker.md" }], { guide: SUBAGENT_LIST_GUIDE });
|
|
125
134
|
expect(out).toContain("Do NOT call list to discover");
|
|
126
135
|
expect(out).toContain("use list only for running state");
|
|
127
136
|
});
|
|
@@ -130,7 +139,7 @@ describe("formatAgentList", () => {
|
|
|
130
139
|
const out = formatAgentList([
|
|
131
140
|
{ name: "worker", description: "does work", path: "/agents/worker.md" },
|
|
132
141
|
{ name: "reviewer", description: "reviews code", path: "/agents/reviewer.md" },
|
|
133
|
-
]);
|
|
142
|
+
], { guide: SUBAGENT_LIST_GUIDE });
|
|
134
143
|
expect(out).toContain("<available_subagents>");
|
|
135
144
|
expect(out).toContain("</available_subagents>");
|
|
136
145
|
expect(out).toContain("<name>worker</name>");
|
|
@@ -143,10 +152,39 @@ describe("formatAgentList", () => {
|
|
|
143
152
|
});
|
|
144
153
|
|
|
145
154
|
it("转义 XML 特殊字符", () => {
|
|
146
|
-
const out = formatAgentList([{ name: "a&b<c>", description: "\"q\"", path: "/agents/a&b<c>.md" }]);
|
|
155
|
+
const out = formatAgentList([{ name: "a&b<c>", description: "\"q\"", path: "/agents/a&b<c>.md" }], { guide: SUBAGENT_LIST_GUIDE });
|
|
147
156
|
expect(out).toContain("<name>a&b<c></name>");
|
|
148
157
|
expect(out).toContain(""q"");
|
|
149
158
|
});
|
|
159
|
+
|
|
160
|
+
it("CA2 快照等价锚定:骨架 + 条目段逐字节等于 pi 旧本地实现模板(除 location 前缀外零变化)", () => {
|
|
161
|
+
// 条目模板逐字复制自 C5① 前的本地 formatAgentList 实现(escapeXml 同函数),
|
|
162
|
+
// 锚定 core 下沉未改变渲染字节(红线 8:等价豁免仅 location 前缀 + guide 段)
|
|
163
|
+
const agents = [
|
|
164
|
+
{
|
|
165
|
+
name: "reviewer",
|
|
166
|
+
description: "代码审查",
|
|
167
|
+
path: "/OLD-PREFIX/agents/reviewer.md",
|
|
168
|
+
when: "用户要求 review",
|
|
169
|
+
},
|
|
170
|
+
{ name: "worker", description: "does work", path: "/OLD-PREFIX/agents/worker.md" },
|
|
171
|
+
];
|
|
172
|
+
const expectedItems = [
|
|
173
|
+
' <agent><name>reviewer</name><description>代码审查</description><when>用户要求 review</when><location>/OLD-PREFIX/agents/reviewer.md</location></agent>',
|
|
174
|
+
' <agent><name>worker</name><description>does work</description><location>/OLD-PREFIX/agents/worker.md</location></agent>',
|
|
175
|
+
];
|
|
176
|
+
const out = formatAgentList(agents, { guide: SUBAGENT_LIST_GUIDE });
|
|
177
|
+
expect(out).toBe(
|
|
178
|
+
`\n\n<available_subagents>\n${SUBAGENT_LIST_GUIDE}\n${expectedItems.join("\n")}\n</available_subagents>`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// location 前缀替换(资产来源迁移:pi-sw 安装目录 → core 包)后其余字节零变化
|
|
182
|
+
const migrated = formatAgentList(
|
|
183
|
+
agents.map((a) => ({ ...a, path: a.path.replace("/OLD-PREFIX/", "/NEW-CORE-PKG/") })),
|
|
184
|
+
{ guide: SUBAGENT_LIST_GUIDE },
|
|
185
|
+
);
|
|
186
|
+
expect(migrated).toBe(out.replaceAll("/OLD-PREFIX/", "/NEW-CORE-PKG/"));
|
|
187
|
+
});
|
|
150
188
|
});
|
|
151
189
|
|
|
152
190
|
// ──────────────────────────────────────────────────────────────
|
|
@@ -327,10 +365,12 @@ describe("discoverAllAgents 顺序契约(KV-cache)", () => {
|
|
|
327
365
|
]);
|
|
328
366
|
spies.getCachedFileContent.mockImplementation((p: string) => byPath[p] ?? null);
|
|
329
367
|
|
|
330
|
-
const { discoverAllAgents
|
|
368
|
+
const { discoverAllAgents } = await import("../subagent-list-injector");
|
|
331
369
|
const first = await discoverAllAgents("/ws");
|
|
332
370
|
const second = await discoverAllAgents("/ws");
|
|
333
371
|
expect(second).toEqual(first);
|
|
334
|
-
expect(formatAgentList(second)).toBe(
|
|
372
|
+
expect(formatAgentList(second, { guide: SUBAGENT_LIST_GUIDE })).toBe(
|
|
373
|
+
formatAgentList(first, { guide: SUBAGENT_LIST_GUIDE }),
|
|
374
|
+
);
|
|
335
375
|
});
|
|
336
376
|
});
|
|
@@ -12,7 +12,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
12
12
|
|
|
13
13
|
import type { DiscoveredResource } from "@zhushanwen/subagent-core/shared/resource-discovery.ts";
|
|
14
14
|
// 共享 mock 基建(vi.mock 工厂 / mock pi / mock ctx):helpers/injector-test-mocks.ts
|
|
15
|
+
// ——必须先于 barrel import 初始化:barrel 的 re-export 链会触发深路径 mock 工厂,
|
|
16
|
+
// 工厂闭包引用本 helpers 的绑定(helpers 文件头「惰性求值」约束)
|
|
15
17
|
import { createDiscoveryModuleMock, createLoggerModuleMock, createMockCtx, createMockPi, type CapturedHandlers } from "./helpers/injector-test-mocks.ts";
|
|
18
|
+
// C5①:formatWorkflowList/summarizeDescription 下沉 core barrel——测试改从 barrel 取实现
|
|
19
|
+
import { formatWorkflowList, summarizeDescription } from "@zhushanwen/subagent-core";
|
|
16
20
|
|
|
17
21
|
// ── 稳定 spy(vi.hoisted 保证 resetModules 后引用不变,见 subagent 测试同款注释) ──
|
|
18
22
|
const spies = vi.hoisted(() => ({ discoverResources: vi.fn(), getCachedFileContent: vi.fn() }));
|
|
@@ -23,11 +27,7 @@ vi.mock("@zhushanwen/subagent-core/shared/resource-discovery.ts", () => createDi
|
|
|
23
27
|
vi.mock("@zhushanwen/pi-extension-logger", () => createLoggerModuleMock());
|
|
24
28
|
|
|
25
29
|
// ── 纯函数测试:静态 import(模块级缓存状态不影响纯函数) ──
|
|
26
|
-
import {
|
|
27
|
-
formatWorkflowList,
|
|
28
|
-
parseWorkflowMeta,
|
|
29
|
-
summarizeDescription,
|
|
30
|
-
} from "../workflow-list-injector";
|
|
30
|
+
import { parseWorkflowMeta, WORKFLOW_LIST_GUIDE } from "../workflow-list-injector";
|
|
31
31
|
|
|
32
32
|
describe("summarizeDescription", () => {
|
|
33
33
|
it("短描述原样返回", () => {
|
|
@@ -113,14 +113,14 @@ phases: [Review, Fix]
|
|
|
113
113
|
|
|
114
114
|
describe("formatWorkflowList", () => {
|
|
115
115
|
it("空列表返回空串(不注入)", () => {
|
|
116
|
-
expect(formatWorkflowList([])).toBe("");
|
|
116
|
+
expect(formatWorkflowList([], { guide: WORKFLOW_LIST_GUIDE })).toBe("");
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
it("用 <available_workflows> 标签包裹并列出每个 workflow", () => {
|
|
120
120
|
const out = formatWorkflowList([
|
|
121
121
|
{ name: "chain", description: "三步链", path: "/workflows/chain.js" },
|
|
122
122
|
{ name: "parallel", description: "并行分析", path: "/workflows/parallel.js" },
|
|
123
|
-
]);
|
|
123
|
+
], { guide: WORKFLOW_LIST_GUIDE });
|
|
124
124
|
expect(out).toContain("<available_workflows>");
|
|
125
125
|
expect(out).toContain("</available_workflows>");
|
|
126
126
|
expect(out).toContain("<name>chain</name>");
|
|
@@ -131,14 +131,26 @@ describe("formatWorkflowList", () => {
|
|
|
131
131
|
expect(out).toContain("<location>/workflows/parallel.js</location>");
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
it("CA2 快照等价锚定:骨架 + 条目段逐字节等于 pi 旧本地实现模板(workflow 段 guide 未变)", () => {
|
|
135
|
+
// 条目模板逐字复制自 C5① 前的本地 formatWorkflowList 实现——锚定 core 下沉
|
|
136
|
+
// 未改变渲染字节(workflow 段无 location 变化面,全段逐字节等价)
|
|
137
|
+
const out = formatWorkflowList(
|
|
138
|
+
[{ name: "chain", description: "三步链", path: "/workflows/chain.js" }],
|
|
139
|
+
{ guide: WORKFLOW_LIST_GUIDE },
|
|
140
|
+
);
|
|
141
|
+
expect(out).toBe(
|
|
142
|
+
`\n\n<available_workflows>\n${WORKFLOW_LIST_GUIDE}\n <workflow><name>chain</name><description>三步链</description><location>/workflows/chain.js</location></workflow>\n</available_workflows>`,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
134
146
|
it("包含 'Do NOT call list to discover available workflows' 引导语", () => {
|
|
135
|
-
const out = formatWorkflowList([{ name: "chain", description: "d", path: "/workflows/chain.js" }]);
|
|
147
|
+
const out = formatWorkflowList([{ name: "chain", description: "d", path: "/workflows/chain.js" }], { guide: WORKFLOW_LIST_GUIDE });
|
|
136
148
|
expect(out).toContain("Do NOT call list to discover available workflows");
|
|
137
149
|
expect(out).toContain("use list only for running state");
|
|
138
150
|
});
|
|
139
151
|
|
|
140
152
|
it("引导语通用化:不写死内置 workflow 名,含 info 回收指引", () => {
|
|
141
|
-
const out = formatWorkflowList([{ name: "chain", description: "d", path: "/workflows/chain.js" }]);
|
|
153
|
+
const out = formatWorkflowList([{ name: "chain", description: "d", path: "/workflows/chain.js" }], { guide: WORKFLOW_LIST_GUIDE });
|
|
142
154
|
expect(out).toContain("All listed workflows run directly via action:run");
|
|
143
155
|
expect(out).toContain("read the <location> script file");
|
|
144
156
|
// 通用化约束:引导语不点名具体 workflow(名字由 @pi-meta 动态注入,
|
|
@@ -147,7 +159,7 @@ describe("formatWorkflowList", () => {
|
|
|
147
159
|
});
|
|
148
160
|
|
|
149
161
|
it("转义 XML 特殊字符", () => {
|
|
150
|
-
const out = formatWorkflowList([{ name: "a&b", description: "<x>", path: "/workflows/a&b.js" }]);
|
|
162
|
+
const out = formatWorkflowList([{ name: "a&b", description: "<x>", path: "/workflows/a&b.js" }], { guide: WORKFLOW_LIST_GUIDE });
|
|
151
163
|
expect(out).toContain("<name>a&b</name>");
|
|
152
164
|
expect(out).toContain("<x>");
|
|
153
165
|
});
|
|
@@ -313,10 +325,12 @@ describe("discoverAllWorkflows 顺序契约(KV-cache)", () => {
|
|
|
313
325
|
]);
|
|
314
326
|
spies.getCachedFileContent.mockImplementation((p: string) => byPath[p] ?? null);
|
|
315
327
|
|
|
316
|
-
const { discoverAllWorkflows
|
|
328
|
+
const { discoverAllWorkflows } = await import("../workflow-list-injector");
|
|
317
329
|
const first = await discoverAllWorkflows("/ws");
|
|
318
330
|
const second = await discoverAllWorkflows("/ws");
|
|
319
331
|
expect(second).toEqual(first);
|
|
320
|
-
expect(formatWorkflowList(second)).toBe(
|
|
332
|
+
expect(formatWorkflowList(second, { guide: WORKFLOW_LIST_GUIDE })).toBe(
|
|
333
|
+
formatWorkflowList(first, { guide: WORKFLOW_LIST_GUIDE }),
|
|
334
|
+
);
|
|
321
335
|
});
|
|
322
336
|
});
|
|
@@ -23,10 +23,6 @@ const logger = getLogger("subagents");
|
|
|
23
23
|
/** 引擎变更通知 customType(D3;renderer/测试按此识别引擎变更消息)。 */
|
|
24
24
|
export const ENGINE_CHANGE_CUSTOM_TYPE = "subagent-engine-changed";
|
|
25
25
|
|
|
26
|
-
// normalizeEngineId 单一权威源在 registry.ts,此处再导出保持既有导入面(index.ts
|
|
27
|
-
// 与测试经本模块导入)不断裂。
|
|
28
|
-
export { normalizeEngineId };
|
|
29
|
-
|
|
30
26
|
/**
|
|
31
27
|
* 引擎变更通知文案(设计 §3.1 英文基准,三行结构逐字骨架;D4:不含任何模型清单,
|
|
32
28
|
* 清单只活在 system prompt 现值一份)。指路段按目标引擎参数化,与
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Model List Injector
|
|
2
|
+
* Model List Injector(C5① 渲染改接 core)
|
|
3
3
|
*
|
|
4
4
|
* 通过 before_agent_start 每 turn 注入 `<available_provider_models>` 段,列出
|
|
5
5
|
* 当前 auth 可用的模型(provider/modelId + 能力 + contextWindow),与
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* injector 的码点序契约对齐)。数据真实变化(用户中途配置了新 provider)
|
|
20
20
|
* 时下一 turn 自然反映。
|
|
21
21
|
*
|
|
22
|
+
* C5①(convergence D-3):formatModelList + ModelEntry 口径下沉 core 并改调
|
|
23
|
+
* barrel——pi 投影全字段必给(provider/reasoning:boolean/input[]/contextWindow),
|
|
24
|
+
* core 的 (provider,id) 码点序 + provider/id 拼接在 pi 投影下与本地旧实现逐字节
|
|
25
|
+
* 一致(CA2 快照验收前提);guide 文案是 pi 宿主注入(core 不内嵌平台文案)。
|
|
26
|
+
*
|
|
22
27
|
* 立场:本注入段只服务「派发时选模型」,明确告知模型不要在会话中切换主模型
|
|
23
28
|
* (KV cache 不友好);用户明确要求换模型时走 pi 原生 /model 命令(人手动触发)。
|
|
24
29
|
*/
|
|
@@ -35,19 +40,17 @@ import type {
|
|
|
35
40
|
} from "@earendil-works/pi-coding-agent";
|
|
36
41
|
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
// C5①/C5⑦:渲染统一走 core barrel(formatModelList + ModelEntry 类型为 barrel 导出面)
|
|
44
|
+
import { formatModelList, type ModelEntry } from "@zhushanwen/subagent-core";
|
|
39
45
|
|
|
40
46
|
const logger = getLogger("injector");
|
|
41
47
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
input: string[];
|
|
49
|
-
contextWindow: number;
|
|
50
|
-
}
|
|
48
|
+
/**
|
|
49
|
+
* pi 版注入引导文案(C5① guide 参数化——core 渲染函数不内嵌平台文案,宿主注入;
|
|
50
|
+
* 文案与改造前本地实现逐字一致——models 段 guide 无过期问题)。
|
|
51
|
+
*/
|
|
52
|
+
export const MODEL_LIST_GUIDE =
|
|
53
|
+
"The following models are available (auth-configured). Use these ids when delegating via the subagent/workflow `model` param (\"provider/modelId\" format) to match the task (e.g. vision models for screenshots, strong reasoners for architecture). Do NOT switch the main conversation model mid-session — per-call model override on delegates only (switching the main model is cache-hostile); use the /model command only when the user explicitly asks to change it.";
|
|
51
54
|
|
|
52
55
|
/** Model<Api> → ModelEntry 投影(只留注入段消费的字段;模块内唯一消费方 setupModelListInjector) */
|
|
53
56
|
function toModelEntry(model: Model<Api>): ModelEntry {
|
|
@@ -61,53 +64,6 @@ function toModelEntry(model: Model<Api>): ModelEntry {
|
|
|
61
64
|
};
|
|
62
65
|
}
|
|
63
66
|
|
|
64
|
-
/** 码点序比较(显式契约,禁 localeCompare——宿主 locale 差异会破坏跨环境字节一致)。 */
|
|
65
|
-
function compareByCodepoint(a: string, b: string): number {
|
|
66
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** 能力标记:reasoning → "reasoning",input 含 image → "vision"(空则省略 caps 段) */
|
|
70
|
-
function formatCaps(entry: ModelEntry): string {
|
|
71
|
-
const caps: string[] = [];
|
|
72
|
-
if (entry.reasoning) caps.push("reasoning");
|
|
73
|
-
if (entry.input.includes("image")) caps.push("vision");
|
|
74
|
-
return caps.join(",");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 将模型列表格式化为 XML 注入段。
|
|
79
|
-
*
|
|
80
|
-
* 输入按 (provider, id) 码点序排序——registry 返回顺序不作保证,排序后同一
|
|
81
|
-
* 数据集输出字节稳定。码点序是显式契约(禁 localeCompare——宿主 locale 差异
|
|
82
|
-
* 会破坏跨环境字节一致,见 subagent-list-injector.ts sortByCodepoint 注释),
|
|
83
|
-
* 保证注入段进每 turn system prompt 时跨环境逐字节可复现(cache-probe 前缀
|
|
84
|
-
* 指纹归因 / 换机器 resume 场景依赖此性质)。空列表返回空串(不注入)。
|
|
85
|
-
*/
|
|
86
|
-
export function formatModelList(models: ModelEntry[]): string {
|
|
87
|
-
if (models.length === 0) return "";
|
|
88
|
-
|
|
89
|
-
const sorted = [...models].sort((a, b) =>
|
|
90
|
-
a.provider === b.provider
|
|
91
|
-
? compareByCodepoint(a.id, b.id)
|
|
92
|
-
: compareByCodepoint(a.provider, b.provider),
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const items = sorted.map((m) => {
|
|
96
|
-
const caps = formatCaps(m);
|
|
97
|
-
return (
|
|
98
|
-
` <model><id>${escapeXml(`${m.provider}/${m.id}`)}</id>`
|
|
99
|
-
+ `<name>${escapeXml(m.name)}</name>`
|
|
100
|
-
+ (caps ? `<caps>${caps}</caps>` : "")
|
|
101
|
-
+ `<contextWindow>${m.contextWindow}</contextWindow></model>`
|
|
102
|
-
);
|
|
103
|
-
});
|
|
104
|
-
return renderXmlSection({
|
|
105
|
-
tag: "available_provider_models",
|
|
106
|
-
guide: "The following models are available (auth-configured). Use these ids when delegating via the subagent/workflow `model` param (\"provider/modelId\" format) to match the task (e.g. vision models for screenshots, strong reasoners for architecture). Do NOT switch the main conversation model mid-session — per-call model override on delegates only (switching the main model is cache-hostile); use the /model command only when the user explicitly asks to change it.",
|
|
107
|
-
items,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
67
|
/**
|
|
112
68
|
* 注册 before_agent_start handler,注入 `<available_provider_models>` 段。
|
|
113
69
|
*
|
|
@@ -125,6 +81,7 @@ export function setupModelListInjector(pi: ExtensionAPI): void {
|
|
|
125
81
|
try {
|
|
126
82
|
const injection = formatModelList(
|
|
127
83
|
ctx.modelRegistry.getAvailable().map(toModelEntry),
|
|
84
|
+
{ guide: MODEL_LIST_GUIDE },
|
|
128
85
|
);
|
|
129
86
|
if (!injection) return;
|
|
130
87
|
return { systemPrompt: event.systemPrompt + injection };
|