@zhushanwen/pi-subagent-workflow 8.7.0 → 8.8.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.
Files changed (56) hide show
  1. package/README.md +5 -11
  2. package/package.json +8 -11
  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 +61 -6
  6. package/src/index.ts +155 -528
  7. package/src/injectors/__tests__/engine-awareness.test.ts +5 -3
  8. package/src/injectors/__tests__/engine-section-stability.test.ts +48 -33
  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 +80 -8
  13. package/src/injectors/model-list-injector.ts +15 -58
  14. package/src/injectors/resource-list-injector.ts +209 -0
  15. package/src/injectors/subagent-list-injector.ts +53 -224
  16. package/src/injectors/workflow-list-injector.ts +36 -196
  17. package/src/interface/__tests__/detectors.test.ts +46 -35
  18. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +42 -26
  19. package/src/interface/__tests__/subagent-tool-prompt.test.ts +14 -3
  20. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +216 -0
  21. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +17 -6
  22. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +38 -2
  23. package/src/interface/bg-notify-render.ts +7 -17
  24. package/src/interface/command-actions.ts +5 -15
  25. package/src/interface/commands.ts +116 -76
  26. package/src/interface/format.ts +354 -245
  27. package/src/interface/gui-mappers.ts +18 -22
  28. package/src/interface/helpers.ts +9 -130
  29. package/src/interface/list-component.ts +164 -117
  30. package/src/interface/list-shared.ts +3 -3
  31. package/src/interface/list-view.ts +130 -73
  32. package/src/interface/subagent-actions.ts +50 -675
  33. package/src/interface/subagent-tool-schema.ts +7 -9
  34. package/src/interface/subagent-tool.ts +5 -4
  35. package/src/interface/subagents.ts +9 -8
  36. package/src/interface/tool-render.ts +5 -15
  37. package/src/interface/tool-workflow-script.ts +34 -75
  38. package/src/interface/tool-workflow.ts +62 -96
  39. package/src/interface/tui-kit.ts +305 -0
  40. package/src/interface/views/WorkflowsView.ts +270 -207
  41. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
  42. package/src/interface/views/__tests__/detail-content-session-file.test.ts +7 -4
  43. package/src/interface/views/detail-content.ts +16 -14
  44. package/src/interface/views/view-constants.ts +19 -0
  45. package/src/jsonl-run-store.ts +144 -228
  46. package/agents/analyst.md +0 -61
  47. package/agents/coder.md +0 -70
  48. package/agents/debugger.md +0 -67
  49. package/agents/doc-reviewer.md +0 -50
  50. package/agents/explorer.md +0 -64
  51. package/agents/general-purpose.md +0 -32
  52. package/agents/orchestrator.md +0 -63
  53. package/agents/planner.md +0 -54
  54. package/agents/researcher.md +0 -65
  55. package/agents/reviewer.md +0 -74
  56. package/src/interface/views/format.ts +0 -320
@@ -1,7 +1,7 @@
1
1
  // engine-awareness 单测([engine-awareness U3])
2
2
  //
3
3
  // 覆盖(设计 docs/design/subagent-engine-awareness-injection.md 验收挂钩 D1/D1b/D2/D3/D5):
4
- // 1. normalizeEngineId:缺省/空白归一到 'pi'(单一权威源 registry.ts,经本模块再导出)
4
+ // 1. normalizeEngineId:缺省/空白归一到 'pi'(单一权威源 registry.ts,直连导入)
5
5
  // 2. buildEngineChangeNotice:§3.1 文案骨架、pi/非 pi 指路段分界、不含任何模型清单(D4)
6
6
  // 3. runEngineAwarenessTurn 编排:
7
7
  // - 变更触发 apply + 通知(D2 顺序硬约束:提交缓存先于通知先于记账)
@@ -18,12 +18,14 @@ import { describe, expect, it } from "vitest";
18
18
  import {
19
19
  buildEngineChangeNotice,
20
20
  ENGINE_CHANGE_CUSTOM_TYPE,
21
- normalizeEngineId,
22
21
  runEngineAwarenessTurn,
23
22
  type EngineAwarenessDeps,
24
23
  type EngineAwarenessOutcome,
25
24
  } from "../engine-awareness";
26
- import type { GlobalConfigReadResult } from "@zhushanwen/subagent-core/execution/config.ts";
25
+ // dev 1b05be6f9 删壳 engine-awareness 的 normalizeEngineId 再导出面后,测试直连
26
+ // core barrel(与生产 engine-awareness.ts 的 import 形态一致)
27
+ import { normalizeEngineId } from "@zhushanwen/subagent-core";
28
+ import type { GlobalConfigReadResult } from "@zhushanwen/subagent-core";
27
29
 
28
30
  // ── 测试数据 ────────────────────────────────────────────
29
31
 
@@ -9,15 +9,19 @@
9
9
  // 1. 段渲染确定性:pi / zcode / ghost 三形态多次独立调用逐字节相等(toBe 级);
10
10
  // 2. 引擎切换只变尾部:provider models 段前缀不变,变化只发生在 engine 段位置(尾部);
11
11
  // 3. 段序守护:engine 段恒位于 provider models 段之后(恒链尾)。双保险实现:
12
- // a) handler 链模拟——真实导入 setupModelListInjector + 复刻 index.ts engine
13
- // handler 的渲染拼装(检测编排部分归 engine-awareness.test.ts),模拟 pi 的
12
+ // a) handler 链模拟——真实导入 setupModelListInjector + 复刻 engine handler
13
+ // (D7-④ 起为 engine-awareness.ts 的 setupEngineAwarenessInjector)的渲染
14
+ // 拼装(检测编排部分归 engine-awareness.test.ts),模拟 pi 的
14
15
  // before_agent_start 链式叠加语义;
15
- // b) index.ts 源码级结构断言——锚定注册序与拼装形态。
16
+ // b) 源码级结构断言——index.ts 锚定注册序(四个 setup 调用先后),engine-awareness.ts
17
+ // 锚定拼装形态。
16
18
  //
17
- // 源码级断言的脆弱性权衡(刻意为之):engine handler 是 index.ts 内联闭包(无法
18
- // 导入),而 before_agent_start 多 handler 的段序由注册序唯一决定,「注册序」这一
19
- // 事实只存在于 index.ts 源码中——源码结构断言是唯一能锚定它的方式。重构挪动注册
20
- // 位置导致本断言失败属预期行为(提醒同步更新链模拟与守护锚点),不是误报。
19
+ // 源码级断言的脆弱性权衡(刻意为之):engine handler 的生产实现装配依赖
20
+ // ModelConfigService 单例与 sessionState 存取器(getModelConfigService() 在测试环境
21
+ // 恒 null,导入后 handler 直接早退),链模拟仍以复刻形态锚定拼装保真;而
22
+ // before_agent_start 多 handler 的段序由注册序唯一决定,「注册序」这一事实只存在于
23
+ // index.ts 源码中——源码结构断言是唯一能锚定它的方式。重构挪动注册位置导致本断言
24
+ // 失败属预期行为(提醒同步更新链模拟与守护锚点),不是误报。
21
25
 
22
26
  import { readFileSync } from "node:fs";
23
27
  import { fileURLToPath } from "node:url";
@@ -26,13 +30,15 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
26
30
 
27
31
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
28
32
 
29
- import { formatModelList, setupModelListInjector, type ModelEntry } from "../model-list-injector.ts";
30
- import type { EnginePort } from "@zhushanwen/subagent-core/execution/engine/port.ts";
33
+ // C5①:formatModelList/ModelEntry 下沉 core barrel(setupModelListInjector 留 injector)
34
+ import { formatModelList, type ModelEntry } from "@zhushanwen/subagent-core";
35
+ import { MODEL_LIST_GUIDE, setupModelListInjector } from "../model-list-injector.ts";
36
+ import type { EnginePort } from "@zhushanwen/subagent-core";
31
37
  import { clearEngines, registerEngine } from "@zhushanwen/subagent-core/execution/engine/registry.ts";
32
38
  import {
33
39
  buildEngineModelsPromptAppend,
34
40
  buildSubagentEngineSection,
35
- } from "@zhushanwen/subagent-core/execution/engine/model-prompt.ts";
41
+ } from "@zhushanwen/subagent-core";
36
42
 
37
43
  // ── 测试数据 ────────────────────────────────────────────
38
44
 
@@ -66,8 +72,9 @@ const ZCODE_MODELS: Array<{ id: string; name?: string }> = [
66
72
  const BASE = "You are a coding agent.\n\n# System\nCore prompt body.";
67
73
 
68
74
  /**
69
- * engine handler 的追加段拼装(与 index.ts engine handler 同款:状态段 + 清单段、
70
- * 空段剔除、\n\n 连接)。复刻保真由下方「段序守护」describe 的源码锚点断言保证。
75
+ * engine handler 的追加段拼装(与 engine-awareness.ts setupEngineAwarenessInjector
76
+ * 内 handler 同款:状态段 + 清单段、空段剔除、\n\n 连接)。复刻保真由下方「段序守护」
77
+ * describe 的源码锚点断言保证。
71
78
  */
72
79
  function composeEngineAppend(defaultEngine: string | undefined): string {
73
80
  return [buildSubagentEngineSection(defaultEngine), buildEngineModelsPromptAppend(defaultEngine)]
@@ -80,7 +87,7 @@ const APPEND_SEPARATOR = "\n\n";
80
87
 
81
88
  /** 合成一个 turn 的 system prompt 尾部:BASE + provider models 段 + engine 追加段。 */
82
89
  function composeTurn(defaultEngine: string | undefined): string {
83
- const providerSection = formatModelList(PROVIDER_ENTRIES);
90
+ const providerSection = formatModelList(PROVIDER_ENTRIES, { guide: MODEL_LIST_GUIDE });
84
91
  const afterProvider = providerSection === "" ? BASE : BASE + providerSection;
85
92
  const append = composeEngineAppend(defaultEngine);
86
93
  return append === "" ? afterProvider : `${afterProvider}${APPEND_SEPARATOR}${append}`;
@@ -190,7 +197,7 @@ describe("段渲染确定性(D7:同输入恒同输出)", () => {
190
197
  * - 切换后的引擎追加段恰居 prompt 尾部。
191
198
  */
192
199
  function expectOnlyTailDiffers(from: string | undefined, to: string | undefined): void {
193
- const stableHead = BASE + formatModelList(PROVIDER_ENTRIES);
200
+ const stableHead = BASE + formatModelList(PROVIDER_ENTRIES, { guide: MODEL_LIST_GUIDE });
194
201
  const before = composeTurn(from);
195
202
  const after = composeTurn(to);
196
203
  const beforeAppend = composeEngineAppend(from);
@@ -260,7 +267,8 @@ describe("段序守护:engine 段恒链尾(D7)", () => {
260
267
  const { handlers, api } = capturePi();
261
268
  // 真实导入 model list injector(provider models 段的生产渲染路径)
262
269
  setupModelListInjector(api);
263
- // 复刻 index.ts engine handler 的渲染拼装部分(检测编排部分归 engine-awareness.test.ts)
270
+ // 复刻 engine handler(engine-awareness.ts setupEngineAwarenessInjector)的渲染
271
+ // 拼装部分(检测编排部分归 engine-awareness.test.ts)
264
272
  const defaultEngine = "zcode";
265
273
  handlers.push((event) => {
266
274
  const append = composeEngineAppend(defaultEngine);
@@ -279,7 +287,7 @@ describe("段序守护:engine 段恒链尾(D7)", () => {
279
287
  // 恒链尾:engine 追加段是 prompt 的最后内容
280
288
  expect(prompt.endsWith(`${APPEND_SEPARATOR}${composeEngineAppend(defaultEngine)}`)).toBe(true);
281
289
  // model list handler 注入形态锚定(BASE + injection 无分隔,生产 handler 同款)
282
- expect(prompt.startsWith(BASE + formatModelList(PROVIDER_ENTRIES))).toBe(true);
290
+ expect(prompt.startsWith(BASE + formatModelList(PROVIDER_ENTRIES, { guide: MODEL_LIST_GUIDE }))).toBe(true);
283
291
  });
284
292
 
285
293
  it("对照:注册序颠倒(engine 先、model list 后)时段序断言必失败——守护有判别力", async () => {
@@ -300,8 +308,9 @@ describe("段序守护:engine 段恒链尾(D7)", () => {
300
308
  expect(statusIdx).toBeLessThan(providerIdx);
301
309
  });
302
310
 
303
- it("index.ts 源码级守护:engine handler 注册序在 model list injector 之后(段序由注册序唯一决定)", () => {
311
+ it("源码级守护:engine handler 注册序在 model list injector 之后(段序由注册序唯一决定)", () => {
304
312
  const src = readFileSync(fileURLToPath(new URL("../../index.ts", import.meta.url)), "utf8");
313
+ const eaSrc = readFileSync(fileURLToPath(new URL("../engine-awareness.ts", import.meta.url)), "utf8");
305
314
  const lineOf = (needle: string): number => {
306
315
  const idx = src.indexOf(needle);
307
316
  if (idx < 0) throw new Error(`index.ts 中未找到守护锚点:${needle}`);
@@ -309,33 +318,39 @@ describe("段序守护:engine 段恒链尾(D7)", () => {
309
318
  };
310
319
 
311
320
  // 注册序:subagents → workflows → provider models → engine handler(链尾,D7)。
312
- // engine handler 是唯一的直接 pi.on("before_agent_start")(其余三段经 setup* 间接注册),
313
- // 恰出现一次的断言让「链尾」语义精确成立;未来若新增直接注册需人工复核段序。
314
- expect(src.split('pi.on("before_agent_start"')).toHaveLength(2);
321
+ // D7-④:index.ts 不再有直接 pi.on("before_agent_start") 注册——四段注入全部经
322
+ // setup* 函数(engine 段经 setupEngineAwarenessInjector)。「零直接注册」断言让
323
+ // 「链尾」语义精确成立;未来若新增直接注册需人工复核段序。
324
+ expect(src.split('pi.on("before_agent_start"')).toHaveLength(1);
315
325
  const lineSubagents = lineOf("setupSubagentListInjector(pi);");
316
326
  const lineWorkflows = lineOf("setupWorkflowListInjector(pi);");
317
327
  const lineModels = lineOf("setupModelListInjector(pi);");
318
- const lineEngineHandler = lineOf('pi.on("before_agent_start"');
328
+ const lineEngineSetup = lineOf("setupEngineAwarenessInjector(pi,");
319
329
  expect(lineSubagents).toBeLessThan(lineWorkflows);
320
330
  expect(lineWorkflows).toBeLessThan(lineModels);
321
- expect(lineModels).toBeLessThan(lineEngineHandler);
331
+ expect(lineModels).toBeLessThan(lineEngineSetup);
332
+
333
+ // engine handler 的唯一注册点在 engine-awareness.ts(setupEngineAwarenessInjector
334
+ // 内恰一处 pi.on("before_agent_start"))。
335
+ expect(eaSrc.split('pi.on("before_agent_start"')).toHaveLength(2);
322
336
 
323
337
  // engine handler 内渲染调用序:状态段在清单段之前(append 内段序:状态段 → 清单段)。
324
- // 两个调用在 index.ts 中位于同一行(数组字面量),行号无法区分先后——用字符偏移断言
325
- const offsetStatusCall = src.indexOf("buildSubagentEngineSection(defaultEngine)");
326
- const offsetModelsCall = src.indexOf("buildEngineModelsPromptAppend(defaultEngine)");
338
+ // 两个调用在 engine-awareness.ts 中位于同一行(数组字面量),行号无法区分先后——
339
+ // 用字符偏移断言
340
+ const offsetStatusCall = eaSrc.indexOf("buildSubagentEngineSection(defaultEngine)");
341
+ const offsetModelsCall = eaSrc.indexOf("buildEngineModelsPromptAppend(defaultEngine)");
327
342
  if (offsetStatusCall < 0 || offsetModelsCall < 0) {
328
- throw new Error("index.ts 中未找到 engine 段渲染调用锚点");
343
+ throw new Error("engine-awareness.ts 中未找到 engine 段渲染调用锚点");
329
344
  }
330
- expect(offsetStatusCall).toBeGreaterThan(src.indexOf('pi.on("before_agent_start"'));
345
+ expect(offsetStatusCall).toBeGreaterThan(eaSrc.indexOf('pi.on("before_agent_start"'));
331
346
  expect(offsetStatusCall).toBeLessThan(offsetModelsCall);
332
347
 
333
348
  // 链模拟复刻保真锚点:append 拼装形态(空段剔除 + \n\n 连接 + 尾部追加模板)。
334
- // 这些锚点保证 composeEngineAppend 与 index.ts 生产拼装的一致性——生产形态
335
- // 变化会先在这里红,提醒同步更新本文件全部复刻点。
336
- expect(src).toContain('[buildSubagentEngineSection(defaultEngine), buildEngineModelsPromptAppend(defaultEngine)]');
337
- expect(src).toContain('.filter((part) => part !== "")');
338
- expect(src).toContain('.join("\\n\\n")');
339
- expect(src).toContain("${event.systemPrompt}\\n\\n${append}");
349
+ // 这些锚点保证 composeEngineAppend 与生产拼装(engine-awareness.ts)的一致性——
350
+ // 生产形态变化会先在这里红,提醒同步更新本文件全部复刻点。
351
+ expect(eaSrc).toContain('[buildSubagentEngineSection(defaultEngine), buildEngineModelsPromptAppend(defaultEngine)]');
352
+ expect(eaSrc).toContain('.filter((part) => part !== "")');
353
+ expect(eaSrc).toContain('.join("\\n\\n")');
354
+ expect(eaSrc).toContain("${event.systemPrompt}\\n\\n${append}");
340
355
  });
341
356
  });
@@ -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
  });
@@ -10,23 +10,30 @@
10
10
  // 只改注入不改路由会出现「prompt 说引擎 B、实际派发跑引擎 A」,权威信息源说谎)
11
11
  // → sendMessage 对话流通知(D3/D4,短文案不含任何模型清单)→ 更新 lastEngine。
12
12
  //
13
- // 本模块是无状态纯编排(依赖注入式,可测);状态(lastEngine)与宿主能力
14
- // (readConfig / applyRead / sendMessage)由 index.ts engine handler 装配注入。
13
+ // 本模块的编排核心(runEngineAwarenessTurn)是无状态纯编排(依赖注入式,可测);
14
+ // 生产装配在下方 setupEngineAwarenessInjector(D7-④:接线从 index.ts 内联第 4 处
15
+ // 收编至此,与另三个 injector 的 setup* 同形)——lastEngine 的 per-session 存取经
16
+ // 参数注入(index.ts sessionState 装配),其余宿主能力(readConfig / applyRead /
17
+ // sendMessage)在装配函数内组装。
15
18
 
19
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
20
+ import type {
21
+ BeforeAgentStartEvent,
22
+ ExtensionAPI,
23
+ ExtensionContext,
24
+ } from "@earendil-works/pi-coding-agent";
16
25
  import { getLogger } from "@zhushanwen/pi-extension-logger";
17
26
 
18
- import type { GlobalConfigReadResult } from "@zhushanwen/subagent-core/execution/config.ts";
19
- import { DEFAULT_ENGINE_ID, normalizeEngineId } from "@zhushanwen/subagent-core/execution/engine/registry.ts";
27
+ import { readGlobalConfig, type GlobalConfigReadResult } from "@zhushanwen/subagent-core";
28
+ import { DEFAULT_ENGINE_ID, normalizeEngineId } from "@zhushanwen/subagent-core";
29
+ import { buildEngineModelsPromptAppend, buildSubagentEngineSection } from "@zhushanwen/subagent-core";
30
+ import { getModelConfigService } from "@zhushanwen/subagent-core";
20
31
 
21
32
  const logger = getLogger("subagents");
22
33
 
23
34
  /** 引擎变更通知 customType(D3;renderer/测试按此识别引擎变更消息)。 */
24
35
  export const ENGINE_CHANGE_CUSTOM_TYPE = "subagent-engine-changed";
25
36
 
26
- // normalizeEngineId 单一权威源在 registry.ts,此处再导出保持既有导入面(index.ts
27
- // 与测试经本模块导入)不断裂。
28
- export { normalizeEngineId };
29
-
30
37
  /**
31
38
  * 引擎变更通知文案(设计 §3.1 英文基准,三行结构逐字骨架;D4:不含任何模型清单,
32
39
  * 清单只活在 system prompt 现值一份)。指路段按目标引擎参数化,与
@@ -121,3 +128,68 @@ export function runEngineAwarenessTurn(deps: EngineAwarenessDeps): EngineAwarene
121
128
  );
122
129
  return { outcome: "changed", from: last, to: target };
123
130
  }
131
+
132
+ // ── 生产装配(D7-④:接线从 index.ts 内联收编为统一 setup 函数)─────────────
133
+ //
134
+ // 注册链尾 before_agent_start handler:per-turn 检测编排 + 恒在状态段/引擎清单段
135
+ // 渲染。原 index.ts 内联第 4 处接线([engine-awareness U3] 标记区域)收编于此——
136
+ // index.ts 的注入链序由四个 setup 调用(subagents → workflows → provider models →
137
+ // engine)的先后表达,不再靠内联闭包 + 注释维护。
138
+ //
139
+ // 编排依据(自 index.ts 随迁的设计注释):
140
+ // - sendMessage 不设 triggerTurn(D3)——切换是用户主动行为,无需唤醒 AI 立即行动;
141
+ // P1 探针已证此形态消息进入本 turn LLM 上下文(证据:真机 pi rpc payload dump +
142
+ // 0.84.4 dist sendMessage→_appendCustomMessage→agent.state.messages.push→
143
+ // createContextSnapshot 调用链)。
144
+ // - 段序:状态段在前(文案声明 "listed ... below"),清单段在后;provider models 段
145
+ // 由更早注册的 handler 注入、位于上方——本 handler 恒链尾注册(D7:段内容变化只断
146
+ // system prompt 尾部 cache 前缀)。apply 后 getGlobalConfig() 即新值——通知、状态段、
147
+ // 路由三处同 turn 对齐(G2)。
148
+ // - fail-safe:任何异常不注入不阻塞 agent loop;service 未装配(null)或
149
+ // systemPrompt 非 string 同样静默跳过。
150
+
151
+ /** per-session lastEngine 存取(生产装配由 index.ts sessionState 提供)。 */
152
+ export interface EngineAwarenessSessionAccessors {
153
+ /** per-session lastEngine 读取(undefined = 未基线化,D1b)。 */
154
+ getLastEngine(sessionId: string): string | undefined;
155
+ /** per-session lastEngine 写入(仅合法引擎 id,不写 undefined)。 */
156
+ setLastEngine(sessionId: string, engine: string): void;
157
+ }
158
+
159
+ /**
160
+ * 注册引擎感知 before_agent_start handler(链尾)。
161
+ *
162
+ * 每 turn:① runEngineAwarenessTurn 检测编排(§2.3 数据流 ①② 步);② 恒在状态段
163
+ * <current_subagent_engine>(D6)+ 引擎清单段 <available_<engine>_models> 渲染追加
164
+ * (③ 渲染归本函数)。渲染拼装 = 状态段 + 清单段、空段剔除、\n\n 连接、尾部追加
165
+ * (engine-section-stability.test.ts 的链模拟复刻此形态,源码锚点断言保真)。
166
+ */
167
+ export function setupEngineAwarenessInjector(
168
+ pi: ExtensionAPI,
169
+ session: EngineAwarenessSessionAccessors,
170
+ ): void {
171
+ pi.on("before_agent_start", (event: BeforeAgentStartEvent, ctx: ExtensionContext) => {
172
+ try {
173
+ const service = getModelConfigService();
174
+ if (service === null || typeof event.systemPrompt !== "string") return undefined;
175
+ const sid = ctx.sessionManager.getSessionId();
176
+ runEngineAwarenessTurn({
177
+ readConfig: () => readGlobalConfig(getAgentDir()),
178
+ applyRead: (read) => service.applyGlobalConfig(read),
179
+ sendMessage: (message) => {
180
+ // D3:不设 triggerTurn——切换是用户主动行为,无需唤醒 AI 立即行动
181
+ pi.sendMessage(message, {});
182
+ },
183
+ getLastEngine: () => session.getLastEngine(sid),
184
+ setLastEngine: (engine) => session.setLastEngine(sid, engine),
185
+ });
186
+ const defaultEngine = service.getGlobalConfig().defaultEngine;
187
+ const append = [buildSubagentEngineSection(defaultEngine), buildEngineModelsPromptAppend(defaultEngine)]
188
+ .filter((part) => part !== "")
189
+ .join("\n\n");
190
+ return { systemPrompt: `${event.systemPrompt}\n\n${append}` };
191
+ } catch {
192
+ return undefined;
193
+ }
194
+ });
195
+ }