@zhushanwen/pi-subagent-workflow 8.4.0 → 8.6.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 (171) hide show
  1. package/package.json +22 -7
  2. package/relay/relay.mjs +390 -0
  3. package/skills/subagent-ext-config/SKILL.md +80 -0
  4. package/src/execution/__tests__/agent-registry.test.ts +110 -0
  5. package/src/execution/__tests__/bg-notify-render.test.ts +73 -0
  6. package/src/execution/__tests__/chat-engine-routing.test.ts +601 -0
  7. package/src/execution/__tests__/delivery-methods.test.ts +38 -1
  8. package/src/execution/__tests__/execute-options-mapper.test.ts +11 -0
  9. package/src/execution/__tests__/execution-record.test.ts +237 -1
  10. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  11. package/src/execution/__tests__/format-schema-instruction.test.ts +63 -32
  12. package/src/execution/__tests__/helpers/spawn-mock.ts +4 -0
  13. package/src/execution/__tests__/index-session-start.test.ts +86 -7
  14. package/src/execution/__tests__/lifecycle-manager.test.ts +46 -0
  15. package/src/execution/__tests__/list-fields.test.ts +45 -14
  16. package/src/execution/__tests__/model-resolver.test.ts +57 -5
  17. package/src/execution/__tests__/notifier-flush.test.ts +64 -26
  18. package/src/execution/__tests__/notify-ledger.test.ts +826 -0
  19. package/src/execution/__tests__/output-collector.test.ts +299 -2
  20. package/src/execution/__tests__/pi-invocation.test.ts +62 -1
  21. package/src/execution/__tests__/relay-agent.test.ts +448 -0
  22. package/src/execution/__tests__/relay-env.test.ts +42 -0
  23. package/src/execution/__tests__/rpc-mode.test.ts +1 -1
  24. package/src/execution/__tests__/run-spawn-edges.test.ts +44 -1
  25. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  26. package/src/execution/__tests__/session-runner-schema-env.test.ts +39 -0
  27. package/src/execution/__tests__/spawn-args.test.ts +37 -26
  28. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  29. package/src/execution/__tests__/startup-config-declaration.test.ts +35 -0
  30. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  31. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  32. package/src/execution/__tests__/subprocess-agent-runner.test.ts +147 -6
  33. package/src/execution/__tests__/timeout-integration.test.ts +220 -2
  34. package/src/execution/__tests__/tool-action.test.ts +92 -1
  35. package/src/execution/agent-registry.ts +16 -0
  36. package/src/execution/argv-mirror.ts +5 -1
  37. package/src/execution/concurrency-pool.ts +1 -1
  38. package/src/execution/config.ts +25 -2
  39. package/src/execution/engine/__tests__/common/data-dir.test.ts +53 -0
  40. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  41. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  42. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  43. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  44. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  45. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  46. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  47. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  48. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  49. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  50. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  51. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  52. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  53. package/src/execution/engine/__tests__/conformance/contract.relay.test.ts +342 -0
  54. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  55. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  56. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  57. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  58. package/src/execution/engine/__tests__/engines-declaration.test.ts +36 -0
  59. package/src/execution/engine/__tests__/model-prompt.test.ts +85 -0
  60. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  61. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  62. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  63. package/src/execution/engine/common/data-dir.ts +62 -0
  64. package/src/execution/engine/common/errors.ts +183 -0
  65. package/src/execution/engine/common/event-journal.ts +254 -0
  66. package/src/execution/engine/common/journal-replay.ts +62 -0
  67. package/src/execution/engine/common/kill-chain.ts +221 -0
  68. package/src/execution/engine/common/nesting-guard.ts +50 -0
  69. package/src/execution/engine/common/persona-router.ts +108 -0
  70. package/src/execution/engine/common/pool-manager.ts +226 -0
  71. package/src/execution/engine/common/schema-emulation.ts +189 -0
  72. package/src/execution/engine/common/session-view-projection.ts +51 -0
  73. package/src/execution/engine/engine-discovery.ts +65 -0
  74. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  75. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  76. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  77. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  78. package/src/execution/engine/engines/pi/reader.ts +48 -0
  79. package/src/execution/engine/engines/pi/registration.ts +35 -0
  80. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  81. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  82. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  83. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  84. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  85. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  86. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +64 -0
  87. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  88. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
  89. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  90. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  91. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  92. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  93. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  94. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  95. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  96. package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
  97. package/src/execution/engine/host-task-spec.ts +47 -0
  98. package/src/execution/engine/model-prompt.ts +59 -0
  99. package/src/execution/engine/paths.ts +42 -0
  100. package/src/execution/engine/port.ts +153 -0
  101. package/src/execution/engine/registry.ts +123 -0
  102. package/src/execution/engine/routing.ts +218 -0
  103. package/src/execution/engine/types.ts +309 -0
  104. package/src/execution/execute-options-mapper.ts +13 -8
  105. package/src/execution/execution-record.ts +66 -1
  106. package/src/execution/lifecycle-manager.ts +23 -1
  107. package/src/execution/model-config-service.ts +16 -1
  108. package/src/execution/model-resolver.ts +37 -59
  109. package/src/execution/notifier.ts +105 -35
  110. package/src/execution/notify-ledger.ts +580 -0
  111. package/src/execution/output-collector.ts +143 -3
  112. package/src/execution/pi-invocation.ts +32 -2
  113. package/src/execution/record-entry.ts +14 -0
  114. package/src/execution/record-store.ts +34 -0
  115. package/src/execution/relay-env.ts +37 -0
  116. package/src/execution/session-runner.ts +328 -71
  117. package/src/execution/stream-sink.ts +26 -0
  118. package/src/execution/subagent-service.ts +273 -13
  119. package/src/execution/subprocess-agent-runner.ts +210 -14
  120. package/src/execution/types.ts +124 -5
  121. package/src/execution/ui-request-queue.ts +14 -4
  122. package/src/index.ts +99 -1
  123. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +157 -0
  124. package/src/interface/__tests__/subagent-tool-prompt.test.ts +12 -0
  125. package/src/interface/bg-notify-render.ts +33 -12
  126. package/src/interface/helpers.ts +2 -2
  127. package/src/interface/subagent-actions.ts +29 -9
  128. package/src/interface/subagent-tool-schema.ts +156 -0
  129. package/src/interface/subagent-tool.ts +56 -119
  130. package/src/interface/subagents.ts +2 -2
  131. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +19 -3
  132. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +0 -6
  133. package/src/orchestration/__tests__/agent-call-stream.test.ts +0 -5
  134. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +89 -4
  135. package/src/orchestration/__tests__/execute-agent-call.test.ts +137 -0
  136. package/src/orchestration/__tests__/jsonl-run-store-corrupt-entry.test.ts +150 -0
  137. package/src/orchestration/__tests__/jsonl-run-store-retention.test.ts +202 -0
  138. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +326 -3
  139. package/src/orchestration/__tests__/lifecycle.test.ts +41 -7
  140. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  141. package/src/orchestration/__tests__/review-fix-loop-e2e.test.ts +57 -3
  142. package/src/orchestration/__tests__/skill-discovery.test.ts +44 -0
  143. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  144. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +43 -0
  145. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +22 -3
  146. package/src/orchestration/agent-opts-resolver.ts +104 -23
  147. package/src/orchestration/error-recovery.ts +189 -33
  148. package/src/orchestration/execute-agent-call.ts +39 -0
  149. package/src/orchestration/jsonl-run-store.ts +121 -7
  150. package/src/orchestration/launcher.ts +60 -15
  151. package/src/orchestration/lifecycle.ts +10 -7
  152. package/src/orchestration/models/__tests__/budget.test.ts +1 -61
  153. package/src/orchestration/models/budget.ts +5 -35
  154. package/src/orchestration/models/run-runtime.ts +24 -9
  155. package/src/orchestration/models/types.ts +16 -0
  156. package/src/orchestration/script-lint.ts +1 -1
  157. package/src/orchestration/skill-discovery.ts +31 -8
  158. package/src/orchestration/worker-script-builder.ts +19 -3
  159. package/src/shared/__tests__/model-ref.test.ts +306 -0
  160. package/src/shared/__tests__/schema-jsonify.test.ts +1 -1
  161. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  162. package/src/shared/meta-parser.ts +5 -1
  163. package/src/shared/model-ref.ts +286 -0
  164. package/src/shared/resource-meta.ts +5 -0
  165. package/src/shared/schema-env.ts +44 -0
  166. package/src/shared/schema-jsonify.ts +6 -4
  167. package/src/shared/timer-delay.ts +54 -0
  168. package/workflows/review-fix-loop-utils.cjs +9 -7
  169. package/workflows/review-fix-loop.js +20 -12
  170. package/src/orchestration/__tests__/concurrency-gate.test.ts +0 -125
  171. package/src/orchestration/concurrency-gate.ts +0 -69
@@ -0,0 +1,231 @@
1
+ // routing.test.ts —— P4 配置路由三层 + probe fallback 三守卫 + strict(验收 1/2 的
2
+ // 单测面)。fake probe/getEngine 注入(不依赖真机引擎与真实探针)。
3
+
4
+ import { describe, expect, it } from "vitest";
5
+
6
+ import type { EnginePort, RunContext } from "../port.ts";
7
+ import { DEFAULT_ENGINE_ID, EngineNotFoundError } from "../registry.ts";
8
+ import { EngineError } from "../common/errors.ts";
9
+ import type { AgentTaskSpec, ProbeReport, SessionView } from "../types.ts";
10
+ import { resolveEngineRouting, routeEngine, type EngineRouteOptions } from "../routing.ts";
11
+
12
+ /** 最小可运行假引擎(probe 结果可注入)。 */
13
+ function makeFakeEngine(id: string, probeOk: boolean): EnginePort {
14
+ return {
15
+ id,
16
+ capabilities: () => ({
17
+ schemaEnforcement: "emulated",
18
+ steer: "unsupported",
19
+ conversation: "unsupported",
20
+ personaInjection: "prompt",
21
+ eventGranularity: "coarse",
22
+ sandbox: "none",
23
+ sessionRead: "outcome-only",
24
+ resume: "cold",
25
+ interrupt: "kill-only",
26
+ permissionMode: "native",
27
+ }),
28
+ probe: () =>
29
+ Promise.resolve(
30
+ probeOk
31
+ ? { ok: true, engineVersion: "1.0.0", checks: [{ name: "stub", ok: true }] }
32
+ : {
33
+ ok: false,
34
+ engineVersion: "",
35
+ checks: [{ name: "binary", ok: false, detail: "missing" }],
36
+ error: { code: "engine_probe_failed", recovery: "reinstall the engine binary and retry the probe" },
37
+ } satisfies ProbeReport,
38
+ ),
39
+ run: (_t: AgentTaskSpec, _c: RunContext) => Promise.reject(new Error("fake: run not implemented")),
40
+ interact: () => Promise.resolve({ ok: false, code: "engine_capability_unsupported", message: "stub" }),
41
+ read: (_h: Parameters<EnginePort["read"]>[0]): Promise<SessionView> =>
42
+ Promise.resolve({ engineId: id, turns: [], source: "outcome-only" }),
43
+ };
44
+ }
45
+
46
+ /** routeEngine 的装配器:注册 {pi:ok, zcode:probeOk} 两引擎。 */
47
+ function makeRoute(overrides?: {
48
+ zcodeProbeOk?: boolean;
49
+ strict?: boolean;
50
+ routing?: Partial<EngineRouteOptions["routing"]>;
51
+ taskModel?: string;
52
+ /** 额外注册第三引擎(probe ok)——fallback 目标为「非 pi 的其他全局默认」用例。 */
53
+ globalEngine?: string;
54
+ }) {
55
+ const engines = new Map<string, EnginePort>();
56
+ engines.set(DEFAULT_ENGINE_ID, makeFakeEngine("pi", true));
57
+ engines.set("zcode", makeFakeEngine("zcode", overrides?.zcodeProbeOk ?? true));
58
+ if (overrides?.globalEngine !== undefined) {
59
+ engines.set(overrides.globalEngine, makeFakeEngine(overrides.globalEngine, true));
60
+ }
61
+ const probeCalls: string[] = [];
62
+ const opts: EngineRouteOptions = {
63
+ routing: overrides?.routing ?? {},
64
+ taskModel: overrides?.taskModel,
65
+ strict: overrides?.strict ?? false,
66
+ probe: (id) => {
67
+ probeCalls.push(id);
68
+ return engines.get(id)!.probe();
69
+ },
70
+ getEngineFn: (id) => {
71
+ const e = engines.get(id);
72
+ if (e === undefined) throw new EngineNotFoundError(id, [...engines.keys()]);
73
+ return e;
74
+ },
75
+ hasEngineFn: (id) => engines.has(id),
76
+ listEnginesFn: () => [...engines.keys()],
77
+ };
78
+ return { opts, probeCalls, engines };
79
+ }
80
+
81
+ // ── 三层优先级(验收 1)──
82
+
83
+ describe("resolveEngineRouting:三层优先级", () => {
84
+ it("无任何指定 → 全局缺省 'pi'", () => {
85
+ expect(resolveEngineRouting({})).toEqual({ engineId: "pi", source: "default" });
86
+ });
87
+
88
+ it("全局默认引擎指定(config defaultEngine=zcode)→ zcode", () => {
89
+ expect(resolveEngineRouting({ globalDefaultEngine: "zcode" })).toEqual({ engineId: "zcode", source: "default" });
90
+ });
91
+
92
+ it("frontmatter 指定 > 全局默认", () => {
93
+ expect(resolveEngineRouting({ agentEngine: "zcode", globalDefaultEngine: "pi" })).toEqual({
94
+ engineId: "zcode",
95
+ source: "frontmatter",
96
+ });
97
+ });
98
+
99
+ it("调用参数 > frontmatter > 全局默认(三层全设时调用参数胜)", () => {
100
+ expect(
101
+ resolveEngineRouting({ callEngine: "pi", agentEngine: "zcode", globalDefaultEngine: "zcode" }),
102
+ ).toEqual({ engineId: "pi", source: "call" });
103
+ });
104
+
105
+ it("空串视为未指定(AgentCallOpts.engine='' 落下一层)", () => {
106
+ expect(resolveEngineRouting({ callEngine: "", agentEngine: "zcode" })).toEqual({
107
+ engineId: "zcode",
108
+ source: "frontmatter",
109
+ });
110
+ });
111
+ });
112
+
113
+ describe("routeEngine:路由 + 探针 + 守卫编排(验收 1/2)", () => {
114
+ it("缺省 pi:免探(零行为变化口径)直接返回引擎", async () => {
115
+ const { opts, probeCalls, engines } = makeRoute();
116
+ const result = await routeEngine(opts);
117
+ expect(result.engine).toBe(engines.get("pi"));
118
+ expect(result.engineId).toBe("pi");
119
+ expect(result.engineFallback).toBeUndefined();
120
+ expect(probeCalls).toEqual([]); // pi 缺省路径不探(D7 轻量口径)
121
+ });
122
+
123
+ it("frontmatter 指定 zcode + probe ok:返回 zcode 引擎,无 fallback", async () => {
124
+ const { opts, probeCalls, engines } = makeRoute({ routing: { agentEngine: "zcode" } });
125
+ const result = await routeEngine(opts);
126
+ expect(result.engine).toBe(engines.get("zcode"));
127
+ expect(result.engineId).toBe("zcode");
128
+ expect(result.engineFallback).toBeUndefined();
129
+ expect(probeCalls).toEqual(["zcode"]);
130
+ });
131
+
132
+ it("调用参数指定 zcode:覆盖 frontmatter 的 pi(透传链 A7)", async () => {
133
+ const { opts } = makeRoute({ routing: { callEngine: "zcode", agentEngine: "pi" } });
134
+ const result = await routeEngine(opts);
135
+ expect(result.engineId).toBe("zcode");
136
+ expect(result.source).toBe("call");
137
+ });
138
+
139
+ it("未注册 id(调用参数层):engine_not_found,文案含注册清单(前置暴露)", async () => {
140
+ const { opts } = makeRoute({ routing: { callEngine: "nonexistent" } });
141
+ await expect(routeEngine(opts)).rejects.toThrowError(EngineNotFoundError);
142
+ await expect(routeEngine(opts)).rejects.toThrowError(/Registered engines: pi, zcode/);
143
+ });
144
+
145
+ // ── fallback 与三守卫(验收 2)──
146
+
147
+ it("frontmatter 指定 zcode + probe 失败 → 路由回 pi + engineFallback 留痕(A9①)", async () => {
148
+ const { opts, engines } = makeRoute({ zcodeProbeOk: false, routing: { agentEngine: "zcode" } });
149
+ const result = await routeEngine(opts);
150
+ expect(result.engine).toBe(engines.get("pi"));
151
+ expect(result.engineId).toBe("pi");
152
+ expect(result.requestedEngineId).toBe("zcode");
153
+ expect(result.engineFallback).toEqual({ from: "zcode", reason: "engine_probe_failed" });
154
+ });
155
+
156
+ it("守卫 a:调用参数显式指定 + probe 失败 → 不兜底,报 engine_probe_failed", async () => {
157
+ const { opts } = makeRoute({ zcodeProbeOk: false, routing: { callEngine: "zcode" } });
158
+ const err = await routeEngine(opts).catch((e: unknown) => e);
159
+ expect(err).toBeInstanceOf(EngineError);
160
+ expect((err as EngineError).code).toBe("engine_probe_failed");
161
+ expect((err as EngineError).message).toContain("调用参数显式指定");
162
+ expect((err as EngineError).recovery).toContain("retry the probe");
163
+ });
164
+
165
+ it("守卫 b(首期与 a 合流):AgentTaskSpec.requires 形状预留存在且不参与首期判定", async () => {
166
+ // 形状预留的类型面由 typecheck 守护;行为面:frontmatter 指定(非 call)+ 无 model
167
+ // → 仍走 fallback(守卫 b 独立生效留给 requires 下钻,见 types.ts 注释)
168
+ const { opts } = makeRoute({ zcodeProbeOk: false, routing: { agentEngine: "zcode" } });
169
+ const result = await routeEngine(opts);
170
+ expect(result.engineFallback).toBeDefined();
171
+ });
172
+
173
+ it("守卫 c:显式 model + probe 失败 → 不换引擎,报 model_not_available", async () => {
174
+ const { opts } = makeRoute({
175
+ zcodeProbeOk: false,
176
+ routing: { agentEngine: "zcode" },
177
+ taskModel: "builtin:bigmodel-coding-plan/GLM-5.3",
178
+ });
179
+ const err = await routeEngine(opts).catch((e: unknown) => e);
180
+ expect(err).toBeInstanceOf(EngineError);
181
+ expect((err as EngineError).code).toBe("model_not_available");
182
+ expect((err as EngineError).message).toContain("GLM-5.3");
183
+ });
184
+
185
+ it("守卫 c 不误伤:无显式 model 时 probe 失败正常 fallback", async () => {
186
+ const { opts } = makeRoute({ zcodeProbeOk: false, routing: { agentEngine: "zcode" } });
187
+ const result = await routeEngine(opts);
188
+ expect(result.engineId).toBe("pi");
189
+ });
190
+
191
+ it("strict=true:frontmatter 层 probe 失败也直接报 engine_probe_failed(A5)", async () => {
192
+ const { opts } = makeRoute({ zcodeProbeOk: false, strict: true, routing: { agentEngine: "zcode" } });
193
+ const err = await routeEngine(opts).catch((e: unknown) => e);
194
+ expect((err as EngineError).code).toBe("engine_probe_failed");
195
+ expect((err as EngineError).message).toContain("strict");
196
+ });
197
+
198
+ it("全局默认引擎自身 probe 失败(defaultEngine=zcode):fallback 回内置 pi", async () => {
199
+ const { opts } = makeRoute({ zcodeProbeOk: false, routing: { globalDefaultEngine: "zcode" } });
200
+ const result = await routeEngine(opts);
201
+ expect(result.engineId).toBe("pi");
202
+ expect(result.engineFallback).toEqual({ from: "zcode", reason: "engine_probe_failed" });
203
+ });
204
+
205
+ it("frontmatter zcode + config 默认同为 zcode + probe 失败:fallback 回 pi(不回退同一坏引擎)", async () => {
206
+ // review MF3 回归:全局默认 === 请求引擎时,fallback 目标不得 = 刚 probe 失败的
207
+ // 引擎(from==to 原地重试坏引擎 + 误导留痕)——回内置缺省 pi
208
+ const { opts, engines } = makeRoute({
209
+ zcodeProbeOk: false,
210
+ routing: { agentEngine: "zcode", globalDefaultEngine: "zcode" },
211
+ });
212
+ const result = await routeEngine(opts);
213
+ expect(result.engine).toBe(engines.get("pi"));
214
+ expect(result.engineId).toBe("pi");
215
+ expect(result.requestedEngineId).toBe("zcode");
216
+ expect(result.engineFallback).toEqual({ from: "zcode", reason: "engine_probe_failed" });
217
+ });
218
+
219
+ it("frontmatter zcode + config 默认为其他引擎:probe 失败回全局默认(既有行为不回归)", async () => {
220
+ // 全局默认 ≠ 请求引擎且非 pi:fallback 目标仍是全局默认(相等比较只拦截 from==to 形态)
221
+ const { opts, engines } = makeRoute({
222
+ zcodeProbeOk: false,
223
+ globalEngine: "claude",
224
+ routing: { agentEngine: "zcode", globalDefaultEngine: "claude" },
225
+ });
226
+ const result = await routeEngine(opts);
227
+ expect(result.engine).toBe(engines.get("claude"));
228
+ expect(result.engineId).toBe("claude");
229
+ expect(result.engineFallback).toEqual({ from: "zcode", reason: "engine_probe_failed" });
230
+ });
231
+ });
@@ -0,0 +1,62 @@
1
+ // src/execution/engine/common/data-dir.ts
2
+ //
3
+ // 引擎数据目录解析(P2)。设计权威源:
4
+ // docs/architecture/subagent-engine-abstraction.md D5/D6——engines 根锚定 getDataDir()
5
+ // 顶层(journal / 隔离池的父目录),extension 写侧与 runtime 校验侧同源推导。
6
+ //
7
+ // 通道调查结论(2026-08-25,P2 实测证据链):
8
+ // - runtime spawn pi 主会话时 RpcClient.buildSafeEnv 从 runtime process.env 继承
9
+ // XYZ_ 前缀白名单变量(rpc-client.ts:14-30),且显式设置 PI_CODING_AGENT_DIR;
10
+ // - dev 模式 Electron main 设置 XYZ_AGENT_DATA_DIR(apps/electron/main/main.ts:122),
11
+ // 经 process-control.ts:262 透传给 runtime → 透传链成立;
12
+ // - prod 默认路径(~/.xyz-agent 缺省)下 runtime 进程 env 中**没有**该变量
13
+ // (shared getDataDir 缺省时不写 env)→ 透传链断——已在 runtime 侧
14
+ // process-manager.ts spawn env 补注入 XYZ_AGENT_DATA_DIR 修复(跨包改动)。
15
+ //
16
+ // 回退语义:缺 env 时回退 piAgentDir(getAgentDir()——尊重 PI_CODING_AGENT_DIR,
17
+ // 独立 pi 用户直跑 extension 的场景)并 warn 一次——不留静默漂移(journal 落点变化
18
+ // 必须可观测)。
19
+
20
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
21
+
22
+ import { getLogger } from "@zhushanwen/pi-extension-logger";
23
+
24
+ const logger = getLogger("subagents");
25
+
26
+ /** xyz-agent 数据目录 env 名(与 packages/shared/src/paths.ts 的 SSOT 变量同名)。 */
27
+ export const XYZ_DATA_DIR_ENV = "XYZ_AGENT_DATA_DIR";
28
+
29
+ /** 测试隔离专用:重置 warn-once 标记(生产禁用)。 */
30
+ let warned = false;
31
+ export function resetDataDirWarnForTests(): void {
32
+ warned = false;
33
+ }
34
+
35
+ /**
36
+ * 解析引擎数据目录(journal 落盘 / 隔离池的根):
37
+ * 1. process.env.XYZ_AGENT_DATA_DIR(xyz-agent 宿主注入,权威通道);
38
+ * 2. 缺失 → piAgentDir(getAgentDir())+ warn 一次。
39
+ * 每次调用重新解析(env 读取是零成本操作,不缓存路径防测试/宿主切换时读到旧值)。
40
+ */
41
+ export function getEngineDataDir(
42
+ env: NodeJS.ProcessEnv = process.env,
43
+ warn: (msg: string) => void = defaultWarn,
44
+ ): string {
45
+ const fromEnv = env[XYZ_DATA_DIR_ENV]?.trim();
46
+ if (fromEnv !== undefined && fromEnv !== "") return fromEnv;
47
+
48
+ const fallback = getAgentDir();
49
+ if (!warned) {
50
+ warned = true;
51
+ warn(
52
+ `[engine-data-dir] ${XYZ_DATA_DIR_ENV} is not set; engine journal/pool fall back to the pi agent dir ` +
53
+ `(${fallback}). The xyz-agent host normally injects this env — if you are running inside xyz-agent, ` +
54
+ `check the runtime spawn env; standalone pi installs intentionally use the pi agent dir.`,
55
+ );
56
+ }
57
+ return fallback;
58
+ }
59
+
60
+ function defaultWarn(msg: string): void {
61
+ logger.warn(msg);
62
+ }
@@ -0,0 +1,183 @@
1
+ // src/execution/engine/common/errors.ts
2
+ //
3
+ // 引擎层错误 SSOT(P2 公共降级层)。设计权威源:
4
+ // docs/architecture/subagent-engine-abstraction.md §3.3.3 错误规格全表(11 条)。
5
+ //
6
+ // 为什么集中一处:错误文案契约(code + 恢复指引)被三层消费——公共降级层
7
+ // (prompt_too_large / nested_spawn_rejected / engine_timeout)、后续 wave 的路由与
8
+ // 探针(engine_not_found / engine_probe_failed / model_not_available)、引擎适配器
9
+ // (engine_run_failed / engine_session_not_resumable)。散落各处会漂移成同一错误
10
+ // 多种文案,GUI 无法按 code 分流。
11
+ //
12
+ // 与 registry.ts 的 EngineNotFoundError 的关系:那是 P1 随注册表落地的
13
+ // engine_not_found 错误类(getEngine 抛出),本文件不复制其类定义、只对齐 code
14
+ // 字面量——后续 wave 统一收敛时以本表为 SSOT。
15
+
16
+ // ============================================================
17
+ // 错误码全集(§3.3.3 表 code 列,顺序与设计文档一致)
18
+ // ============================================================
19
+
20
+ export const ENGINE_ERROR_CODES = [
21
+ "engine_not_found",
22
+ "engine_probe_failed",
23
+ "engine_credential_missing",
24
+ "nested_spawn_rejected",
25
+ "schema_emulation_failed",
26
+ "engine_timeout",
27
+ "engine_capability_unsupported",
28
+ "engine_session_not_resumable",
29
+ "model_not_available",
30
+ "prompt_too_large",
31
+ "engine_run_failed",
32
+ ] as const;
33
+
34
+ export type EngineErrorCode = (typeof ENGINE_ERROR_CODES)[number];
35
+
36
+ /** unknown → EngineErrorCode 收窄(外部输入携带错误码时的运行时 guard)。 */
37
+ export function isEngineErrorCode(value: unknown): value is EngineErrorCode {
38
+ return typeof value === "string" && (ENGINE_ERROR_CODES as readonly string[]).includes(value);
39
+ }
40
+
41
+ // ============================================================
42
+ // 错误类(code + 恢复指引的结构化载体)
43
+ // ============================================================
44
+
45
+ /**
46
+ * 引擎层结构化错误。message 恒为 `<code>: <detail>` 前缀格式(AgentOutcome.error
47
+ * 的错误码前缀约定,§3.3.5);recovery 指向恢复动作(全局规则:错误信息必须可操作)。
48
+ */
49
+ export class EngineError extends Error {
50
+ readonly code: EngineErrorCode;
51
+ /** 恢复指引:指向具体下一步(命令 / 配置路径 / 替代方案),非安慰性文案。 */
52
+ readonly recovery: string;
53
+
54
+ constructor(code: EngineErrorCode, detail: string, recovery: string) {
55
+ super(`${code}: ${detail}`);
56
+ this.name = "EngineError";
57
+ this.code = code;
58
+ this.recovery = recovery;
59
+ }
60
+
61
+ /** 结构化投影(InteractResult.code/message 与 GUI 警告条共用形态)。 */
62
+ toStructured(): { code: EngineErrorCode; message: string; recovery: string } {
63
+ return { code: this.code, message: this.message, recovery: this.recovery };
64
+ }
65
+ }
66
+
67
+ // ============================================================
68
+ // 默认恢复指引模板(§3.3.3 恢复指引列的静态部分)
69
+ // ============================================================
70
+
71
+ /**
72
+ * 11 条错误的默认恢复指引。Record<EngineErrorCode, string> 使 TS 强制全集覆盖——
73
+ * 新增错误码漏写模板会在此处编译失败,而不是运行时空指引。
74
+ * 含动态参数的恢复指引(版本命令 / 模型清单 / 字节数)用下方具名构造器,不走此表。
75
+ */
76
+ export const DEFAULT_RECOVERY_HINTS: Record<EngineErrorCode, string> = {
77
+ engine_not_found:
78
+ "Check the engine id in the agent .md frontmatter (engine: field) against the registered engine list, " +
79
+ "fix the typo, or install/register the engine first.",
80
+ engine_probe_failed:
81
+ "Confirm the engine version (e.g. `<engine> --version`), then re-run the engine probe. " +
82
+ "For contract drift, see docs/research/agent-engine-*.md for the expected output format.",
83
+ engine_credential_missing:
84
+ "Configure the engine credentials (see the engine credential section of docs/research/agent-engine-*.md), " +
85
+ "then retry — the preparer cannot synthesize credentials it has no source for.",
86
+ nested_spawn_rejected:
87
+ "Subagents must not spawn further subagents (unbounded recursion guard). " +
88
+ "Do the work directly inside the current task instead of delegating.",
89
+ schema_emulation_failed:
90
+ "The model output failed JSON extraction or schema validation. Retry once with a strengthened prompt " +
91
+ "(buildSchemaEmulationSegment output + the error tail); if it still fails, relax the schema or switch to a " +
92
+ "schema-native engine (engine: pi).",
93
+ engine_timeout:
94
+ "The engine was killed by the host timeout chain. Inspect the captured stdout tail, then re-run with a larger " +
95
+ "timeout, a narrower task, or `engine: pi`.",
96
+ engine_capability_unsupported:
97
+ "This engine declares the capability unsupported. Use a single-shot call instead of interactive steering, " +
98
+ "or dispatch with `engine: pi` which supports it.",
99
+ engine_session_not_resumable:
100
+ "Idle-process reuse does not survive a main-session reload. Use a cold resume path " +
101
+ "(engine --resume / --session with the recorded session reference), or start a new subagent.",
102
+ model_not_available:
103
+ "The requested model is not resolvable in this engine's provider system. Pick a model from the engine's " +
104
+ "available model list — the host never swaps engines implicitly on model mismatch.",
105
+ prompt_too_large:
106
+ "Shorten the task text, move the persona to the file channel, or use an engine with a stdin prompt channel.",
107
+ engine_run_failed:
108
+ "The engine process failed at runtime (parse failure / non-zero exit / contract drift). " +
109
+ "Check the captured stdout tail and exit code, confirm the engine version, re-run the probe, or retry with `engine: pi`.",
110
+ };
111
+
112
+ // ============================================================
113
+ // P2 消费的具名构造器(动态参数进 detail/recovery)
114
+ // ============================================================
115
+
116
+ /** 错误回显长度上限(截断长输出,避免错误消息爆炸——对齐 structured-output echo 上限量级)。 */
117
+ const DETAIL_ECHO_MAX_CHARS = 200;
118
+
119
+ /** truncate(text, max):尾部截断 + 省略号标记(模板共用)。 */
120
+ function truncate(text: string, max: number): string {
121
+ return text.length <= max ? text : `${text.slice(0, max)}...`;
122
+ }
123
+
124
+ /**
125
+ * prompt_too_large(persona-router 的 argv 预算拦截消费):仅 argv 投递的引擎在
126
+ * prepare 期报——禁止 spawn 后撞 E2BIG 才失败(§3.3.3 第 10 行)。
127
+ */
128
+ export function promptTooLargeError(actualBytes: number, limitBytes: number): EngineError {
129
+ return new EngineError(
130
+ "prompt_too_large",
131
+ `estimated argv size ${actualBytes} bytes exceeds the ${limitBytes}-byte budget`,
132
+ DEFAULT_RECOVERY_HINTS.prompt_too_large,
133
+ );
134
+ }
135
+
136
+ /** nested_spawn_rejected(nesting-guard 消费):文案说明防护规则 + 指向 task 内自行完成。 */
137
+ export function nestedSpawnRejectedError(): EngineError {
138
+ return new EngineError(
139
+ "nested_spawn_rejected",
140
+ "this process is already a subagent (XYZ_AGENT_SUBAGENT=1)",
141
+ DEFAULT_RECOVERY_HINTS.nested_spawn_rejected,
142
+ );
143
+ }
144
+
145
+ /** stdout 尾部回显上限(engine_timeout / engine_run_failed 的错误规格载体系数)。 */
146
+ export const STDOUT_TAIL_ECHO_CHARS = 2000;
147
+
148
+ /**
149
+ * engine_timeout 的 outcome.error 文案(kill-chain 的 synthesizeTimeoutOutcome 消费):
150
+ * 含 stdout 尾部 2000 字 + 「可用 engine: pi 重跑」建议(§3.3.3 第 6 行)。
151
+ */
152
+ export function engineTimeoutDetail(stdoutTail: string): string {
153
+ return (
154
+ `host timeout chain exhausted (SIGTERM -> grace -> SIGKILL). ` +
155
+ `Stdout tail (last ${STDOUT_TAIL_ECHO_CHARS} chars): ${truncate(stdoutTail, STDOUT_TAIL_ECHO_CHARS)}. ` +
156
+ `Recovery: ${DEFAULT_RECOVERY_HINTS.engine_timeout}`
157
+ );
158
+ }
159
+
160
+ /**
161
+ * engine_run_failed 的 outcome.error 文案:含 stdout 尾部 2000 字 + exit code +
162
+ * 恢复指引(版本确认 + 探针重跑 / engine: pi 重跑,§3.3.3 第 11 行)。
163
+ */
164
+ export function engineRunFailedDetail(reason: string, exitCode: number | null, stdoutTail: string): string {
165
+ const exit = exitCode === null ? "killed by signal" : `exit code ${exitCode}`;
166
+ return (
167
+ `engine failed at runtime: ${reason} (${exit}). ` +
168
+ `Stdout tail (last ${STDOUT_TAIL_ECHO_CHARS} chars): ${truncate(stdoutTail, STDOUT_TAIL_ECHO_CHARS)}. ` +
169
+ `Recovery: ${DEFAULT_RECOVERY_HINTS.engine_run_failed}`
170
+ );
171
+ }
172
+
173
+ /**
174
+ * schema_emulation_failed 的终报文案(宿主编排层「重试一次仍失败」后消费):
175
+ * 与 structured-output 的重试语义对齐(重试一次 → 报错含原始输出尾部)。
176
+ */
177
+ export function schemaEmulationFailedDetail(error: string, tail: string): string {
178
+ return (
179
+ `structured output emulation failed after tolerant extraction and one host-side retry: ${error}. ` +
180
+ `Raw output tail: ${truncate(tail, DETAIL_ECHO_MAX_CHARS)}. ` +
181
+ `Recovery: ${DEFAULT_RECOVERY_HINTS.schema_emulation_failed}`
182
+ );
183
+ }