@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,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 };
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Resource List Injector 工厂(dual-track convergence D7-②)
3
+ *
4
+ * subagent / workflow 两个资源清单 injector 的同构骨架单实现(原两文件逐字同构的
5
+ * 缓存对 / 唯一写点 / 发现函数 / 三 handler 收敛于此,改 fallback 策略只改一处):
6
+ * - 缓存对:entries + 渲染快照。per-process = per-session——xyz-agent session-pool
7
+ * 模型下每 pi 子进程 = 一 session = 独立扩展实例,闭包级缓存天然 per-session 隔离
8
+ * (split mode 多 session 各自独立进程)。渲染快照与数据缓存同步更新:before_agent_start
9
+ * 每个 turn 都要注入,format(escapeXml 多趟正则 × 全部字段)在数据不变时输出完全
10
+ * 相同——渲染一次随缓存复用,turn 热路径零重复计算。
11
+ * - 唯一写点 setCache:数据与渲染缓存同步更新(null 清空两者)。
12
+ * - 三 handler 自管缓存生命周期(不耦合 index.ts session 逻辑):
13
+ * session_start(含 reload)发现+覆盖缓存(刷新节奏对齐 pi skill,fail-safe 异常
14
+ * 不阻断、缓存保持 null);before_agent_start 读缓存渲染注入、miss(session_start
15
+ * 未触发/缓存被清)则 fallback 重新发现+赋值,空列表/空注入不返回 systemPrompt,
16
+ * 任何异常被吞掉(记日志)不阻断 agent turn;session_shutdown 清缓存。
17
+ * pi 支持 async handler,同一 event 多 handler 链式(前者返回的 systemPrompt 作
18
+ * 后者输入)。
19
+ *
20
+ * 两实例的真差异经 config 参数化承载:kind(发现种类 + discoveryRoots 宿主槽位)/
21
+ * parse(单文件内容 → entry;workflow 侧 description 截断内聚于其 parse)/ format
22
+ * (entries → XML 注入段;guide 文案差异内聚于各自 format)/ includeTmp(workflow 侧
23
+ * 覆盖 .pi/workflows/.tmp/ generate 产物)/ onParseNull(agent 侧「有 frontmatter 但
24
+ * 解析失败」warn 判据)/ logTag(错误日志前缀,保持既有可检索性);assemble(agent
25
+ * 侧真差异:装配循环覆写——U11 下沉 core discoverAgents 后工厂不重复装配语义,
26
+ * parse/onParseNull 不参与)。
27
+ *
28
+ * model-list-injector 不参与本工厂:数据源是 ModelRegistry 内存快照(真差异,无文件
29
+ * 发现与缓存生命周期),见该文件头注释。
30
+ */
31
+
32
+ import type {
33
+ BeforeAgentStartEvent,
34
+ BeforeAgentStartEventResult,
35
+ ExtensionAPI,
36
+ ExtensionContext,
37
+ SessionShutdownEvent,
38
+ SessionStartEvent,
39
+ } from "@earendil-works/pi-coding-agent";
40
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
41
+
42
+ import { getHostServices } from "@zhushanwen/subagent-core";
43
+
44
+ import {
45
+ discoverResources,
46
+ findWorkspaceRoot,
47
+ getCachedParsed,
48
+ sortByCodepoint,
49
+ } from "@zhushanwen/subagent-core";
50
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
51
+
52
+ const logger = getLogger("injector");
53
+
54
+ /** 清单条目最小契约:name(去重键 + 码点序排序键)+ path(<location> 注入,发现层填充)。 */
55
+ export interface ResourceListEntry {
56
+ name: string;
57
+ path: string;
58
+ }
59
+
60
+ /** 工厂参数(真差异承载面,见文件头)。 */
61
+ export interface ResourceListInjectorConfig<TEntry extends ResourceListEntry> {
62
+ /** 发现种类(discoverResources kind + discoveryRoots 宿主槽位)。 */
63
+ kind: "agents" | "workflows";
64
+ /** 错误/发现失败日志前缀(如 "[subagent-list-injector]")。 */
65
+ logTag: string;
66
+ /**
67
+ * 装配循环覆写:提供时工厂 discover 整体委托它(发现→解析→去重→排序 +
68
+ * warn/error 口径全归被委托方),工厂不再跑内置装配循环。agent 侧真差异:
69
+ * U11(sink 设计)装配算法单源 core discoverAgents(第三宿主免复刻 G3/S5),
70
+ * 壳侧收缩为「宿主注入根现取 + 委托」。此时 parse/onParseNull 不参与工厂装配。
71
+ */
72
+ assemble?: (workspaceRoot: string) => Promise<TEntry[]>;
73
+ /** 单文件内容 → entry;null = 跳过该文件(无有效 frontmatter/meta)。assemble 覆写时不需要。 */
74
+ parse?(content: string): TEntry | null;
75
+ /** entry 列表 → 注入段;空列表返回空串(不注入)。 */
76
+ format(entries: TEntry[]): string;
77
+ /** workflow 侧真差异:包含 .pi/workflows/.tmp/(workflow-script generate 产物)。 */
78
+ includeTmp?: boolean;
79
+ /** parse 返 null 的旁路处理(agent 侧真差异:仅「有 frontmatter 但解析失败」warn)。 */
80
+ onParseNull?(filePath: string): void;
81
+ }
82
+
83
+ /** 工厂产物:setup 注册三 handler;discover 为发现函数(薄模块再导出)。 */
84
+ export interface ResourceListInjector<TEntry extends ResourceListEntry> {
85
+ setup(pi: ExtensionAPI): void;
86
+ discover(workspaceRoot: string): Promise<TEntry[]>;
87
+ }
88
+
89
+ /** 码点序排序契约见 core sortByCodepoint(KV-cache 契约:禁 localeCompare——宿主 locale 差异会破坏跨环境字节一致)。 */
90
+
91
+ /** kind 单数形态(错误日志文案用,保持与合并前逐字一致)。 */
92
+ function singularKind(kind: "agents" | "workflows"): string {
93
+ return kind === "agents" ? "agent" : "workflow";
94
+ }
95
+
96
+ export function createResourceListInjector<TEntry extends ResourceListEntry>(
97
+ config: ResourceListInjectorConfig<TEntry>,
98
+ ): ResourceListInjector<TEntry> {
99
+ let entriesCache: TEntry[] | null = null;
100
+ let injectionCache: string | null = null;
101
+
102
+ /** 缓存唯一写点:数据与渲染缓存同步更新(null 清空两者)。 */
103
+ function setCache(entries: TEntry[] | null): void {
104
+ entriesCache = entries;
105
+ injectionCache = entries !== null ? config.format(entries) : null;
106
+ }
107
+
108
+ /**
109
+ * 用统一资源发现(ADR-031)发现所有可用条目。永不抛错——发现本身 fail-safe,
110
+ * 单个文件读失败仅记日志。
111
+ *
112
+ * assemble 覆写时整体委托(agent 侧:core discoverAgents 单源装配,U11);
113
+ * 否则跑内置装配循环——
114
+ * discoverResources 返回按文件名 stem 去重、优先级合并后的 DiscoveredResource[]
115
+ * (project > user > builtin,返回顺序低→高优先级——Map 后写覆盖依赖此序,不可在
116
+ * 发现层重排)。此处逐个 parse 提取条目(经 getCachedParsed mtime 级缓存),再按
117
+ * name 去重(高优先级靠后,Map.set 后者覆盖前者,故最终保留最高优先级同名条目)。
118
+ *
119
+ * 输出按 name 码点序排序(KV-cache 契约):注入段进每 turn system prompt,顺序必须
120
+ * 与文件系统枚举序(readdir 无契约)解耦——目录内容不变时,session_start / fallback /
121
+ * resume 任意重建的渲染结果逐字节一致;仅条目增减时文本才变化。
122
+ */
123
+ async function discover(workspaceRoot: string): Promise<TEntry[]> {
124
+ if (config.assemble) {
125
+ return config.assemble(workspaceRoot);
126
+ }
127
+ if (!config.parse) {
128
+ throw new Error(
129
+ `${config.logTag} invalid injector config: parse is required unless assemble is provided`,
130
+ );
131
+ }
132
+ const resources = await discoverResources({
133
+ kind: config.kind,
134
+ workspaceRoot,
135
+ // 宿主注入根现取(pi 壳 discoveryRoots 每次现取,实例隔离);agentDir 形参
136
+ // 已删——其唯一用途就是喂 ScanConfig(u0-data-discovery 偏差 #7)
137
+ hostRoots: getHostServices().discoveryRoots?.()?.[config.kind] ?? [],
138
+ ...(config.includeTmp ? { includeTmp: true } : {}),
139
+ });
140
+
141
+ const map = new Map<string, TEntry>();
142
+ for (const resource of resources) {
143
+ if (!resource.available) continue;
144
+ try {
145
+ const entry = getCachedParsed(resource.path, config.parse);
146
+ if (entry) {
147
+ map.set(entry.name, { ...entry, path: resource.path });
148
+ } else {
149
+ config.onParseNull?.(resource.path);
150
+ }
151
+ } catch (err) {
152
+ // 单个文件读失败不阻断整条清单注入
153
+ logger.error(
154
+ `${config.logTag} skip unreadable ${singularKind(config.kind)} file ${resource.path}`,
155
+ { reason: toErrorMessage(err) },
156
+ );
157
+ }
158
+ }
159
+ return sortByCodepoint([...map.values()], (entry) => entry.name);
160
+ }
161
+
162
+ function setup(pi: ExtensionAPI): void {
163
+ pi.on(
164
+ "session_start",
165
+ async (_event: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
166
+ try {
167
+ setCache(await discover(findWorkspaceRoot(ctx.cwd)));
168
+ } catch (err) {
169
+ // fail-safe:发现异常不阻断 session,缓存保持 null(before_agent_start 会 fallback)
170
+ logger.error(`${config.logTag} session_start discover failed`, {
171
+ reason: toErrorMessage(err),
172
+ });
173
+ }
174
+ },
175
+ );
176
+
177
+ pi.on(
178
+ "before_agent_start",
179
+ async (
180
+ event: BeforeAgentStartEvent,
181
+ ctx: ExtensionContext,
182
+ ): Promise<BeforeAgentStartEventResult | void> => {
183
+ try {
184
+ // 读缓存;miss(session_start 未触发/缓存被清)则 fallback 重新发现+赋值
185
+ if (entriesCache === null) {
186
+ setCache(await discover(findWorkspaceRoot(ctx.cwd)));
187
+ }
188
+ // injectionCache 与 entriesCache 不变量同步(setCache 保证),直接复用
189
+ const injection = injectionCache;
190
+ if (!injection) return;
191
+ return { systemPrompt: event.systemPrompt + injection };
192
+ } catch (err) {
193
+ logger.error(`${config.logTag} before_agent_start failed`, {
194
+ reason: toErrorMessage(err),
195
+ });
196
+ }
197
+ },
198
+ );
199
+
200
+ pi.on(
201
+ "session_shutdown",
202
+ (_event: SessionShutdownEvent, _ctx: ExtensionContext): void => {
203
+ setCache(null);
204
+ },
205
+ );
206
+ }
207
+
208
+ return { setup, discover };
209
+ }
@@ -1,81 +1,50 @@
1
1
  /**
2
- * Subagent List Injector(迁移自 unified-hooks + P3/P4 改造)
2
+ * Subagent List Injector(迁移自 unified-hooks + P3/P4 改造;C5① 渲染改接 core)
3
3
  *
4
4
  * 发现所有可用 subagent(builtin + user + project 各源)并通过 before_agent_start
5
5
  * 每 turn 注入 `<available_subagents>` 段(name + description),让模型能选对 agent
6
6
  * 名字而非臆造。注入格式对齐 pi 内置 skill 注入(XML 标签)。
7
7
  *
8
8
  * 改造点:
9
- * - P4:发现路径由自实现扫 4 目录改为同包 ADR-031 统一发现 discoverResources
10
- * (覆盖 7 源:user-pi/user-agents/npm/npm-dev/project-pi/project-agents + manifest
11
- * 模式 + 优先级合并)。discoverResources 只返回 DiscoveredResource(path/source/
12
- * available),不含 name/description——保留 parseAgentFrontmatter 解析每个 .md 的
13
- * frontmatter 提取 name+description。
14
- * - P3:formatAgentList 开头补正向触发引导(何时该 delegate),保留原有「ONLY use
15
- * agent names from this list」名字约束。
9
+ * - D7-②(dual-track convergence):同构骨架(缓存对/唯一写点/三 handler)收敛到
10
+ * 同目录 resource-list-injector.ts 工厂,本文件只保留 agent 侧真差异。
11
+ * - P3:注入段引导语含正向触发引导(何时该 delegate)+ 名字约束(文案见
12
+ * SUBAGENT_LIST_GUIDE)。
13
+ * - C5①(convergence D-3):formatAgentList/sortByCodepoint 下沉 core,本文件改调
14
+ * core barrel(渲染骨架与条目模板逐字节同 pi 旧本地实现——CA2 快照验收前提);
15
+ * guide 文案是 pi 宿主注入(core 不内嵌平台文案)。
16
+ * - U11(sink 设计):装配循环(发现→解析→去重→排序 + warn/error 口径)整体改
17
+ * 消费 core discoverAgents(execution/agents-assembly,U2/A6)——经工厂 assemble
18
+ * 覆写槽委托,壳侧收缩为「宿主注入根现取 + 委托」,语义等值口径见
19
+ * agents-assembly.ts 头注(⛔1 探针对照注入 XML diff 为空验收)。
16
20
  *
17
- * 归位原因:injector 是 subagent-workflow 的内聚功能(让 LLM 知道有哪些 agent 可用),
18
- * 与同包 resource-discovery 同包后可直接 import,消除跨包依赖。
21
+ * 归位原因:injector 是 subagent-workflow 的内聚功能壳——事件接线与数据获取留
22
+ * 在插件层(before_agent_start / modelRegistry),解析/渲染算法消费 core。
19
23
  */
20
24
 
21
25
 
22
26
 
23
- import type {
24
- BeforeAgentStartEvent,
25
- BeforeAgentStartEventResult,
26
- ExtensionAPI,
27
- ExtensionContext,
28
- SessionShutdownEvent,
29
- SessionStartEvent,
30
- } from "@earendil-works/pi-coding-agent";
31
- import { getLogger } from "@zhushanwen/pi-extension-logger";
32
-
33
- import { getHostServices } from "@zhushanwen/subagent-core/core/host-services.ts";
34
-
35
27
  import {
36
- discoverResources,
37
- findWorkspaceRoot,
38
- getCachedFileContent,
39
- getCachedParsed,
40
- } from "@zhushanwen/subagent-core/shared/resource-discovery.ts";
41
- import { parseResourceMeta } from "@zhushanwen/subagent-core/shared/meta-parser.ts";
42
- import { escapeXml, renderXmlSection } from "@zhushanwen/subagent-core/shared/xml-injection.ts";
28
+ type AgentEntry,
29
+ discoverAgents,
30
+ formatAgentList,
31
+ getHostServices,
32
+ parseResourceMeta,
33
+ } from "@zhushanwen/subagent-core";
43
34
 
44
- const logger = getLogger("injector");
35
+ import { createResourceListInjector } from "./resource-list-injector.ts";
45
36
 
46
37
  /**
47
- * Session 级 agent 列表缓存(per-process = per-session)。
38
+ * pi 版注入引导文案(C5① guide 参数化——core 渲染函数不内嵌平台文案,宿主注入)。
48
39
  *
49
- * xyz-agent session-pool 模型下每 pi 子进程 = 一 session = 独立扩展实例,模块级缓存
50
- * 天然 per-session 隔离(split mode 多 session 各自独立进程)。session_start(含
51
- * reload)触发发现覆盖缓存(刷新节奏对齐 pi skill);session_shutdown 清空;
52
- * before_agent_start 读缓存,miss(session_start 未触发/缓存被清)则 fallback
53
- * 重新发现——保证鲁棒。
54
- */
55
- let agentCache: AgentEntry[] | null = null;
56
-
57
- /**
58
- * 注入块渲染缓存(与 agentCache 同步更新):before_agent_start 每个 turn 都要注入,
59
- * formatAgentList(escapeXml 多趟正则 × 全部字段)在数据不变时输出完全相同——渲染
60
- * 一次随缓存复用,turn 热路径零重复计算。
40
+ * 2026-08 C5 重写依据(pi 现参数面,src/interface/subagent-tool-schema.ts):
41
+ * 旧句「pass systemPrompt alongside the agent name to create a dynamic agent」
42
+ * 指向的 systemPrompt 参数已不在 subagent tool schema——现 agent 参数 = .md 绝对
43
+ * 路径(<location>),缺省落 general-purpose(继承主 agent 模型与项目上下文),
44
+ * 动态指引经 task 文本 / appendSystemPrompt 参数承载。
61
45
  */
62
- let agentInjectionCache: string | null = null;
63
-
64
- /** agentCache 唯一写点:数据与渲染缓存同步更新(null 清空两者)。 */
65
- function setAgentCache(entries: AgentEntry[] | null): void {
66
- agentCache = entries;
67
- agentInjectionCache = entries !== null ? formatAgentList(entries) : null;
68
- }
69
-
70
- /** 从 .md frontmatter 提取的最小 agent 信息(m5:+ when/examples 路由样本;S1:+ path) */
71
- export interface AgentEntry {
72
- name: string;
73
- description: string;
74
- when?: string;
75
- examples?: Array<{ match: string; action: string; positive: boolean }>;
76
- /** agentRef:agent .md 文件的绝对路径(注入段 <location>,模型直接引用) */
77
- path: string;
78
- }
46
+ export const SUBAGENT_LIST_GUIDE =
47
+ "The following subagents are available. PRIORITY: when a task involves reading 3+ files, writing 100+ lines, parallel research, or specialized review, delegate to a matching subagent FIRST instead of doing it yourself — this keeps your context focused on orchestration. Do NOT call list to discover available subagents; use list only for running state. When using the subagent tool, ONLY use agents from this list — pass the <location> path (absolute .md path) as the agent param. If no agent matches your task, omit agent (a general-purpose agent is used) and put all role-specific instructions in the task text.";
79
48
 
80
49
  /**
81
50
  * 解析 markdown 文件的 YAML frontmatter(name + description)。
@@ -83,6 +52,9 @@ export interface AgentEntry {
83
52
  * m2 收敛:删本地单行 key:value parser,改调 shared/meta-parser.ts parseResourceMeta
84
53
  * (IF1 统一 parser,支持 block scalar)。无有效 frontmatter 或缺 name/description 返 null。
85
54
  * 投影 {name, description} 注入用(examples 注入留 m5)。
55
+ *
56
+ * U11 后装配循环(含本函数的装配内消费)单源 core discoverAgents;保留导出作为
57
+ * 严格注入投影语义锚(parseResourceMeta 消费面)。
86
58
  */
87
59
  export function parseAgentFrontmatter(content: string): AgentEntry | null {
88
60
  const meta = parseResourceMeta(content, "agent");
@@ -92,172 +64,29 @@ export function parseAgentFrontmatter(content: string): AgentEntry | null {
92
64
  description: meta.description,
93
65
  when: meta.when,
94
66
  examples: meta.examples,
95
- path: "", // 由 discoverAllAgents 从 DiscoveredResource.path 填充
67
+ path: "", // 装配时由 core discoverAgents 从 DiscoveredResource.path 填充
96
68
  };
97
69
  }
98
70
 
99
71
  /**
100
- * 用统一资源发现(ADR-031)发现所有可用 agent。
101
- *
102
- * discoverResources 返回按文件名 stem 去重、优先级合并后的 DiscoveredResource[]
103
- * (project > user > builtin,返回顺序低→高优先级——Map 后写覆盖依赖此序,不可在
104
- * 发现层重排)。此处逐个解析 frontmatter 提取 name+description(经 getCachedParsed
105
- * mtime 级缓存),再按 agent name 去重(高优先级靠后,Map.set 后者覆盖前者,故最终
106
- * 保留最高优先级同名 agent)。
107
- *
108
- * 输出按 name 码点序排序(KV-cache 契约):注入段进每 turn system prompt,顺序必须
109
- * 与文件系统枚举序(readdir 无契约)解耦——目录内容不变时,session_start / fallback /
110
- * resume 任意重建的渲染结果逐字节一致;仅条目增减时文本才变化。
111
- *
112
- * 永不抛错——发现本身 fail-safe,单个文件读失败仅记日志。
72
+ * agent 清单实例:缓存生命周期 / 三 handler 经工厂骨架;装配循环经 assemble 覆写
73
+ * 委托 core discoverAgents(U11 单源,parse/onParseNull 不参与工厂装配——warn 口径
74
+ * 内聚 core:仅「有 frontmatter 但严格校验未通过」才 warn,README 等无 frontmatter
75
+ * 的 .md 静默跳过)。
113
76
  */
114
- export async function discoverAllAgents(
115
- workspaceRoot: string,
116
- ): Promise<AgentEntry[]> {
117
- const resources = await discoverResources({
118
- kind: "agents",
119
- workspaceRoot,
120
- // 宿主注入根现取(pi 壳 discoveryRoots 每次现取 getAgentDir,实例隔离);
121
- // agentDir 形参已删——其唯一用途就是喂 ScanConfig(u0-data-discovery 偏差 #7)
122
- hostRoots: getHostServices().discoveryRoots?.()?.agents ?? [],
123
- });
124
-
125
- const agentMap = new Map<string, AgentEntry>();
126
- for (const resource of resources) {
127
- if (!resource.available) continue;
128
- try {
129
- const agent = getCachedParsed(resource.path, parseAgentFrontmatter);
130
- if (agent) {
131
- agentMap.set(agent.name, { ...agent, path: resource.path });
132
- } else if (startsWithFrontmatter(resource.path)) {
133
- // m5(评审 M3/F2 + minor-5):仅「有 frontmatter 但解析失败」才 warn
134
- // (缺 name/description/examples 单条非法致整体 reject)——README 等
135
- // 无 frontmatter 的 .md 不刷 warn(每 turn 扫描)。
136
- logger.warn(
137
- `[subagent-list-injector] ${resource.path}: agent frontmatter 解析失败(IF1 校验不通过)——agent 未注入`,
138
- );
139
- }
140
- } catch (err) {
141
- // 单个文件读失败不阻断整条 agent 列表注入
142
- logger.error(
143
- `[subagent-list-injector] skip unreadable agent file ${resource.path}`,
144
- { reason: err instanceof Error ? err.message : String(err) },
145
- );
146
- }
147
- }
148
- return sortByCodepoint([...agentMap.values()], (a) => a.name);
149
- }
150
-
151
- /** 码点序排序(显式契约,禁 localeCompare——宿主 locale 差异会破坏跨环境字节一致)。 */
152
- function sortByCodepoint<T>(items: T[], key: (item: T) => string): T[] {
153
- return items.sort((a, b) => {
154
- const ka = key(a);
155
- const kb = key(b);
156
- return ka < kb ? -1 : ka > kb ? 1 : 0;
157
- });
158
- }
159
-
160
- /** 内容以 frontmatter 分隔符开头(解析失败才值得 warn 的判据)。 */
161
- function startsWithFrontmatter(filePath: string): boolean {
162
- return (getCachedFileContent(filePath) ?? "").trimStart().startsWith("---");
163
- }
164
-
165
- /**
166
- * 将 agent 列表格式化为 XML 注入段。
167
- *
168
- * P3:引导语开头补正向触发条件(何时该 delegate),再保留原「ONLY use agent names
169
- * from this list」名字约束。空列表返回空串(不注入)。
170
- */
171
- export function formatAgentList(agents: AgentEntry[]): string {
172
- if (agents.length === 0) return "";
173
-
174
- const items = agents.map((agent) => {
175
- let block = ` <agent><name>${escapeXml(agent.name)}</name><description>${escapeXml(agent.description)}</description>`;
176
- // m5:路由样本(when + examples 正反原样渲染——negative 的 action 由作者写
177
- // 「不调用(原因)」,渲染器不硬编码;全部内容 escapeXml 防 XML 注入段破坏)
178
- if (agent.when) {
179
- block += `<when>${escapeXml(agent.when)}</when>`;
180
- }
181
- if (agent.examples && agent.examples.length > 0) {
182
- // 两极性原样渲染——negative 的 action 由作者写「不调用(原因)」,
183
- // 渲染器不硬编码后缀(exec-review major-1:曾追加「(不调用)」致双后缀)
184
- const exampleLines = agent.examples.map(
185
- (e) => ` - "${escapeXml(e.match)}" → ${escapeXml(e.action)}`,
186
- );
187
- block += `\n <examples>\n${exampleLines.join("\n")}\n </examples>`;
188
- }
189
- block += `<location>${escapeXml(agent.path)}</location></agent>`;
190
- return block;
191
- });
192
- return renderXmlSection({
193
- tag: "available_subagents",
194
- guide: "The following subagents are available. PRIORITY: when a task involves reading 3+ files, writing 100+ lines, parallel research, or specialized review, delegate to a matching subagent FIRST instead of doing it yourself — this keeps your context focused on orchestration. Do NOT call list to discover available subagents; use list only for running state. When using the subagent tool, ONLY use agent names from this list. If no agent matches your task, pass systemPrompt alongside the agent name to create a dynamic agent.",
195
- items,
196
- });
197
- }
198
-
199
- /**
200
- * 注册 session 生命周期 handler,注入 `<available_subagents>` 段。
201
- *
202
- * 三 handler 自管缓存生命周期(不耦合 index.ts session 逻辑):
203
- * - session_start:发现+缓存赋值(fail-safe,异常不阻断,缓存保持 null)
204
- * - before_agent_start:读缓存渲染注入;miss 则 fallback 发现+赋值;空列表/空注入不返回
205
- * systemPrompt;任何异常被吞掉(记日志),不阻断 agent turn
206
- * - session_shutdown:清缓存
207
- *
208
- * pi 支持 async handler,且同一 event 多 handler 链式(前者返回的 systemPrompt 作
209
- * 后者输入)。
210
- */
211
- export function setupSubagentListInjector(pi: ExtensionAPI): void {
212
- pi.on(
213
- "session_start",
214
- async (_event: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
215
- try {
216
- setAgentCache(
217
- await discoverAllAgents(
218
- findWorkspaceRoot(ctx.cwd),
219
- ),
220
- );
221
- } catch (err) {
222
- // fail-safe:发现异常不阻断 session,缓存保持 null(before_agent_start 会 fallback)
223
- logger.error("[subagent-list-injector] session_start discover failed", {
224
- reason: err instanceof Error ? err.message : String(err),
225
- });
226
- }
227
- },
228
- );
229
-
230
- pi.on(
231
- "before_agent_start",
232
- async (
233
- event: BeforeAgentStartEvent,
234
- ctx: ExtensionContext,
235
- ): Promise<BeforeAgentStartEventResult | void> => {
236
- try {
237
- // 读缓存;miss(session_start 未触发/缓存被清)则 fallback 重新发现+赋值
238
- if (agentCache === null) {
239
- setAgentCache(
240
- await discoverAllAgents(
241
- findWorkspaceRoot(ctx.cwd),
242
- ),
243
- );
244
- }
245
- // agentInjectionCache 与 agentCache 不变量同步(setAgentCache 保证),直接复用
246
- const injection = agentInjectionCache;
247
- if (!injection) return;
248
- return { systemPrompt: event.systemPrompt + injection };
249
- } catch (err) {
250
- logger.error("[subagent-list-injector] before_agent_start failed", {
251
- reason: err instanceof Error ? err.message : String(err),
252
- });
253
- }
254
- },
255
- );
256
-
257
- pi.on(
258
- "session_shutdown",
259
- (_event: SessionShutdownEvent, _ctx: ExtensionContext): void => {
260
- setAgentCache(null);
261
- },
262
- );
263
- }
77
+ const injector = createResourceListInjector<AgentEntry>({
78
+ kind: "agents",
79
+ logTag: "[subagent-list-injector]",
80
+ format: (agents) => formatAgentList(agents, { guide: SUBAGENT_LIST_GUIDE }),
81
+ assemble: (workspaceRoot) =>
82
+ discoverAgents(
83
+ workspaceRoot,
84
+ getHostServices().discoveryRoots?.()?.agents ?? [],
85
+ ),
86
+ });
87
+
88
+ /** 发现所有可用 agent(装配骨架见工厂;装配算法单源 core discoverAgents,U11)。 */
89
+ export const discoverAllAgents = injector.discover;
90
+
91
+ /** 注册 session 生命周期 handler,注入 `<available_subagents>` 段(三 handler 语义见工厂)。 */
92
+ export const setupSubagentListInjector = injector.setup;