@zhushanwen/pi-subagent-workflow 8.8.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 (35) hide show
  1. package/package.json +8 -8
  2. package/src/host/pi-host.ts +5 -4
  3. package/src/index.ts +145 -464
  4. package/src/injectors/__tests__/engine-awareness.test.ts +4 -2
  5. package/src/injectors/__tests__/engine-section-stability.test.ts +42 -29
  6. package/src/injectors/engine-awareness.ts +80 -4
  7. package/src/injectors/resource-list-injector.ts +209 -0
  8. package/src/injectors/subagent-list-injector.ts +30 -143
  9. package/src/injectors/workflow-list-injector.ts +23 -150
  10. package/src/interface/__tests__/detectors.test.ts +1 -1
  11. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +42 -26
  12. package/src/interface/__tests__/subagent-tool-prompt.test.ts +10 -2
  13. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +1 -1
  14. package/src/interface/bg-notify-render.ts +4 -4
  15. package/src/interface/commands.ts +115 -75
  16. package/src/interface/format.ts +339 -241
  17. package/src/interface/helpers.ts +2 -2
  18. package/src/interface/list-component.ts +163 -116
  19. package/src/interface/list-shared.ts +2 -2
  20. package/src/interface/list-view.ts +129 -72
  21. package/src/interface/subagent-actions.ts +5 -5
  22. package/src/interface/subagent-tool-schema.ts +6 -5
  23. package/src/interface/subagent-tool.ts +4 -3
  24. package/src/interface/subagents.ts +8 -7
  25. package/src/interface/tool-render.ts +2 -2
  26. package/src/interface/tool-workflow-script.ts +15 -14
  27. package/src/interface/tool-workflow.ts +14 -11
  28. package/src/interface/tui-kit.ts +305 -0
  29. package/src/interface/views/WorkflowsView.ts +267 -196
  30. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
  31. package/src/interface/views/__tests__/detail-content-session-file.test.ts +7 -4
  32. package/src/interface/views/detail-content.ts +16 -14
  33. package/src/interface/views/view-constants.ts +19 -0
  34. package/src/jsonl-run-store.ts +11 -7
  35. package/src/interface/views/format.ts +0 -281
@@ -22,8 +22,10 @@ import {
22
22
  type EngineAwarenessDeps,
23
23
  type EngineAwarenessOutcome,
24
24
  } from "../engine-awareness";
25
- import { normalizeEngineId } from "@zhushanwen/subagent-core/execution/engine/registry.ts";
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";
@@ -29,12 +33,12 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
29
33
  // C5①:formatModelList/ModelEntry 下沉 core barrel(setupModelListInjector 留 injector)
30
34
  import { formatModelList, type ModelEntry } from "@zhushanwen/subagent-core";
31
35
  import { MODEL_LIST_GUIDE, setupModelListInjector } from "../model-list-injector.ts";
32
- import type { EnginePort } from "@zhushanwen/subagent-core/execution/engine/port.ts";
36
+ import type { EnginePort } from "@zhushanwen/subagent-core";
33
37
  import { clearEngines, registerEngine } from "@zhushanwen/subagent-core/execution/engine/registry.ts";
34
38
  import {
35
39
  buildEngineModelsPromptAppend,
36
40
  buildSubagentEngineSection,
37
- } from "@zhushanwen/subagent-core/execution/engine/model-prompt.ts";
41
+ } from "@zhushanwen/subagent-core";
38
42
 
39
43
  // ── 测试数据 ────────────────────────────────────────────
40
44
 
@@ -68,8 +72,9 @@ const ZCODE_MODELS: Array<{ id: string; name?: string }> = [
68
72
  const BASE = "You are a coding agent.\n\n# System\nCore prompt body.";
69
73
 
70
74
  /**
71
- * engine handler 的追加段拼装(与 index.ts engine handler 同款:状态段 + 清单段、
72
- * 空段剔除、\n\n 连接)。复刻保真由下方「段序守护」describe 的源码锚点断言保证。
75
+ * engine handler 的追加段拼装(与 engine-awareness.ts setupEngineAwarenessInjector
76
+ * 内 handler 同款:状态段 + 清单段、空段剔除、\n\n 连接)。复刻保真由下方「段序守护」
77
+ * describe 的源码锚点断言保证。
73
78
  */
74
79
  function composeEngineAppend(defaultEngine: string | undefined): string {
75
80
  return [buildSubagentEngineSection(defaultEngine), buildEngineModelsPromptAppend(defaultEngine)]
@@ -262,7 +267,8 @@ describe("段序守护:engine 段恒链尾(D7)", () => {
262
267
  const { handlers, api } = capturePi();
263
268
  // 真实导入 model list injector(provider models 段的生产渲染路径)
264
269
  setupModelListInjector(api);
265
- // 复刻 index.ts engine handler 的渲染拼装部分(检测编排部分归 engine-awareness.test.ts)
270
+ // 复刻 engine handler(engine-awareness.ts setupEngineAwarenessInjector)的渲染
271
+ // 拼装部分(检测编排部分归 engine-awareness.test.ts)
266
272
  const defaultEngine = "zcode";
267
273
  handlers.push((event) => {
268
274
  const append = composeEngineAppend(defaultEngine);
@@ -302,8 +308,9 @@ describe("段序守护:engine 段恒链尾(D7)", () => {
302
308
  expect(statusIdx).toBeLessThan(providerIdx);
303
309
  });
304
310
 
305
- it("index.ts 源码级守护:engine handler 注册序在 model list injector 之后(段序由注册序唯一决定)", () => {
311
+ it("源码级守护:engine handler 注册序在 model list injector 之后(段序由注册序唯一决定)", () => {
306
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");
307
314
  const lineOf = (needle: string): number => {
308
315
  const idx = src.indexOf(needle);
309
316
  if (idx < 0) throw new Error(`index.ts 中未找到守护锚点:${needle}`);
@@ -311,33 +318,39 @@ describe("段序守护:engine 段恒链尾(D7)", () => {
311
318
  };
312
319
 
313
320
  // 注册序:subagents → workflows → provider models → engine handler(链尾,D7)。
314
- // engine handler 是唯一的直接 pi.on("before_agent_start")(其余三段经 setup* 间接注册),
315
- // 恰出现一次的断言让「链尾」语义精确成立;未来若新增直接注册需人工复核段序。
316
- 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);
317
325
  const lineSubagents = lineOf("setupSubagentListInjector(pi);");
318
326
  const lineWorkflows = lineOf("setupWorkflowListInjector(pi);");
319
327
  const lineModels = lineOf("setupModelListInjector(pi);");
320
- const lineEngineHandler = lineOf('pi.on("before_agent_start"');
328
+ const lineEngineSetup = lineOf("setupEngineAwarenessInjector(pi,");
321
329
  expect(lineSubagents).toBeLessThan(lineWorkflows);
322
330
  expect(lineWorkflows).toBeLessThan(lineModels);
323
- 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);
324
336
 
325
337
  // engine handler 内渲染调用序:状态段在清单段之前(append 内段序:状态段 → 清单段)。
326
- // 两个调用在 index.ts 中位于同一行(数组字面量),行号无法区分先后——用字符偏移断言
327
- const offsetStatusCall = src.indexOf("buildSubagentEngineSection(defaultEngine)");
328
- 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)");
329
342
  if (offsetStatusCall < 0 || offsetModelsCall < 0) {
330
- throw new Error("index.ts 中未找到 engine 段渲染调用锚点");
343
+ throw new Error("engine-awareness.ts 中未找到 engine 段渲染调用锚点");
331
344
  }
332
- expect(offsetStatusCall).toBeGreaterThan(src.indexOf('pi.on("before_agent_start"'));
345
+ expect(offsetStatusCall).toBeGreaterThan(eaSrc.indexOf('pi.on("before_agent_start"'));
333
346
  expect(offsetStatusCall).toBeLessThan(offsetModelsCall);
334
347
 
335
348
  // 链模拟复刻保真锚点:append 拼装形态(空段剔除 + \n\n 连接 + 尾部追加模板)。
336
- // 这些锚点保证 composeEngineAppend 与 index.ts 生产拼装的一致性——生产形态
337
- // 变化会先在这里红,提醒同步更新本文件全部复刻点。
338
- expect(src).toContain('[buildSubagentEngineSection(defaultEngine), buildEngineModelsPromptAppend(defaultEngine)]');
339
- expect(src).toContain('.filter((part) => part !== "")');
340
- expect(src).toContain('.join("\\n\\n")');
341
- 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}");
342
355
  });
343
356
  });
@@ -10,13 +10,24 @@
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
 
@@ -117,3 +128,68 @@ export function runEngineAwarenessTurn(deps: EngineAwarenessDeps): EngineAwarene
117
128
  );
118
129
  return { outcome: "changed", from: last, to: target };
119
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
+ }
@@ -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
+ }