@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.
Files changed (49) hide show
  1. package/README.md +5 -11
  2. package/package.json +7 -10
  3. package/skills/workflow-script-format/SKILL.md +32 -13
  4. package/src/host/__tests__/pi-host.test.ts +23 -2
  5. package/src/host/pi-host.ts +57 -3
  6. package/src/index.ts +56 -110
  7. package/src/injectors/__tests__/engine-awareness.test.ts +2 -2
  8. package/src/injectors/__tests__/engine-section-stability.test.ts +6 -4
  9. package/src/injectors/__tests__/model-list-injector.test.ts +18 -21
  10. package/src/injectors/__tests__/subagent-list-injector.test.ts +54 -14
  11. package/src/injectors/__tests__/workflow-list-injector.test.ts +26 -12
  12. package/src/injectors/engine-awareness.ts +0 -4
  13. package/src/injectors/model-list-injector.ts +15 -58
  14. package/src/injectors/subagent-list-injector.ts +55 -113
  15. package/src/injectors/workflow-list-injector.ts +33 -66
  16. package/src/interface/__tests__/detectors.test.ts +45 -34
  17. package/src/interface/__tests__/subagent-tool-prompt.test.ts +6 -3
  18. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +216 -0
  19. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +17 -6
  20. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +37 -1
  21. package/src/interface/bg-notify-render.ts +3 -13
  22. package/src/interface/command-actions.ts +5 -15
  23. package/src/interface/commands.ts +1 -1
  24. package/src/interface/format.ts +15 -4
  25. package/src/interface/gui-mappers.ts +18 -22
  26. package/src/interface/helpers.ts +8 -129
  27. package/src/interface/list-component.ts +1 -1
  28. package/src/interface/list-shared.ts +1 -1
  29. package/src/interface/list-view.ts +1 -1
  30. package/src/interface/subagent-actions.ts +51 -676
  31. package/src/interface/subagent-tool-schema.ts +1 -4
  32. package/src/interface/subagent-tool.ts +1 -1
  33. package/src/interface/subagents.ts +1 -1
  34. package/src/interface/tool-render.ts +3 -13
  35. package/src/interface/tool-workflow-script.ts +33 -75
  36. package/src/interface/tool-workflow.ts +51 -88
  37. package/src/interface/views/WorkflowsView.ts +3 -11
  38. package/src/interface/views/format.ts +19 -58
  39. package/src/jsonl-run-store.ts +134 -222
  40. package/agents/analyst.md +0 -61
  41. package/agents/coder.md +0 -70
  42. package/agents/debugger.md +0 -67
  43. package/agents/doc-reviewer.md +0 -50
  44. package/agents/explorer.md +0 -64
  45. package/agents/general-purpose.md +0 -32
  46. package/agents/orchestrator.md +0 -63
  47. package/agents/planner.md +0 -54
  48. package/agents/researcher.md +0 -65
  49. 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
- import type { ModelEntry } from "../model-list-injector";
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("空列表返回空串(不注入)", async () => {
34
- const { formatModelList } = await import("../model-list-injector");
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", async () => {
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 段", async () => {
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 前提)", async () => {
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)", async () => {
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 特殊字符时转义(防注入段结构破坏)", async () => {
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&lt;&amp;&gt;&quot;&apos;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 { formatAgentList, parseAgentFrontmatter } from "../subagent-list-injector";
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("&lt;task&gt;");
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("保留原 'ONLY use agent names from this list' 名字约束", () => {
119
- const out = formatAgentList([{ name: "worker", description: "d", path: "/agents/worker.md" }]);
120
- expect(out).toContain("ONLY use agent names from this list");
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&amp;b&lt;c&gt;</name>");
148
157
  expect(out).toContain("&quot;q&quot;");
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, formatAgentList } = await import("../subagent-list-injector");
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(formatAgentList(first));
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&amp;b</name>");
152
164
  expect(out).toContain("&lt;x&gt;");
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, formatWorkflowList } = await import("../workflow-list-injector");
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(formatWorkflowList(first));
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
- import { escapeXml, renderXmlSection } from "@zhushanwen/subagent-core/shared/xml-injection.ts";
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
- /** 注入段的最小模型投影(从 Model<Api> 收窄,测试无需构造完整 Model) */
43
- export interface ModelEntry {
44
- provider: string;
45
- id: string;
46
- name: string;
47
- reasoning: boolean;
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 };