@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
@@ -8,7 +8,9 @@
8
8
  // 接线层级:
9
9
  // [跨模块 port] implements AgentRunner(orchestration/models/ports.ts)
10
10
  // [模块内直调] mapToExecuteOptions + mergeTimeoutSignal(execute-options-mapper)
11
- // [模块内直调] this.subagentService.executeAndAwait
11
+ // [P4 路由层] engine/routing.ts routeEngine(三层优先级 + probe + fallback 三守卫,D9)
12
+ // [引擎层] EnginePort.run(pi = 本地 DI 绑定实例;非 pi = registry.getEngine 动态获取)
13
+ // [模块内直调] this.subagentService.executeAndAwait(PiEngine 内部委托目标)
12
14
  //
13
15
  // 设计基线:
14
16
  // D-A2(映射归 adapter)/ D-A8(onEvent 桥接)/ D-A9(timeoutMs 合并 signal)/
@@ -17,8 +19,20 @@
17
19
  import type { AgentRunner } from "../orchestration/models/ports.ts";
18
20
  import type { AgentCallOpts, AgentResult } from "../orchestration/models/types.ts";
19
21
  import type { AgentEvent } from "../shared/agent-event.ts";
22
+ import { getEngineDataDir } from "./engine/common/data-dir.ts";
23
+ import { JournalWriter } from "./engine/common/event-journal.ts";
24
+ import { createPiEngine, PI_POOL_KEY } from "./engine/engines/pi/registration.ts";
25
+ import { executeOptionsToTaskSpec } from "./engine/engines/pi/task-spec-mapper.ts";
26
+ import { resolveJournalPath } from "./engine/paths.ts";
27
+ import type { EnginePort, RunContext } from "./engine/port.ts";
28
+ import { routeEngine, resolveEngineRouting, type EngineRouteResult, type EngineRoutingInput } from "./engine/routing.ts";
29
+ import { getEngine, hasEngine, listEngines } from "./engine/registry.ts";
30
+ import type { AgentOutcome, EngineHandle } from "./engine/types.ts";
20
31
  import { mapToExecuteOptions, mergeTimeoutSignal } from "./execute-options-mapper.ts";
32
+ import { getModelConfigService } from "./model-config-service.ts";
21
33
  import type { ModelInfo } from "./model-resolver.ts";
34
+ import { modelRefFromVerified } from "../shared/model-ref";
35
+ import { registerSpawnedChildForRecord } from "./session-runner.ts";
22
36
  import type { SubagentStream } from "./stream-sink.ts";
23
37
  import type { SubagentService } from "./subagent-service.ts";
24
38
  import type { ExecuteOptions } from "./types.ts";
@@ -51,14 +65,27 @@ export interface SubprocessAgentRunnerDeps {
51
65
  * - result 形状不变(workflow AgentResult: content/parsedOutput/usage/error/toolCalls)
52
66
  * - 不 reject——失败信息入 result.error(与 executeAgentCall 契约一致)
53
67
  * - timeoutMs 合并 signal(D-A9);onEvent 桥接 AgentEvent→workflow liveRecord(D-A8)
68
+ *
69
+ * [P1 引擎接线] 执行经 EnginePort。pi 引擎绑定本 SAR 的服务引用(per-session DI——
70
+ * 单测注入 mock 时全局单例不可见;生产环境两者是同一进程单例对象),行为零变化;
71
+ * 非 pi 引擎(P4 路由可达:frontmatter/调用参数/全局默认指定)经 registry.getEngine
72
+ * 动态获取——「引擎身份」的归属边界在注册表,SAR 不感知具体引擎实现。
73
+ *
74
+ * [P4 配置路由] 每次运行经 engine/routing.ts 解析三层优先级(调用参数 opts.engine >
75
+ * agent frontmatter engine > 全局默认 'pi')+ probe + fallback 三守卫(D9)。路由失败
76
+ * (engine_not_found / engine_probe_failed / model_not_available)不 reject,入
77
+ * result.error——与 executeAgentCall 契约一致。
54
78
  */
55
79
  export class SubprocessAgentRunner implements AgentRunner {
56
80
  private readonly subagentService: SubagentService;
57
81
  private ctxModel: ModelInfo | undefined;
82
+ /** pi 引擎(per-session DI 绑定,缺省执行路径——见类注释)。 */
83
+ private readonly piEngine: EnginePort;
58
84
 
59
85
  constructor(deps: SubprocessAgentRunnerDeps) {
60
86
  this.subagentService = deps.subagentService;
61
87
  this.ctxModel = deps.ctxModel;
88
+ this.piEngine = createPiEngine(() => this.subagentService);
62
89
  }
63
90
 
64
91
  /**
@@ -72,13 +99,16 @@ export class SubprocessAgentRunner implements AgentRunner {
72
99
  }
73
100
 
74
101
  /**
75
- * 执行单次 agent 调用:委托 SubagentService.executeAndAwait。
102
+ * 执行单次 agent 调用:路由(P4)→ EnginePort.run → PiEngine 委托
103
+ * SubagentService.executeAndAwait。
76
104
  *
77
105
  * 接线链路:
78
- * mergeTimeoutSignal → mapToExecuteOptions →
79
- * this.subagentService.executeAndAwait返回 AgentResult
106
+ * routeEngine(三层 + probe + 守卫)→ mergeTimeoutSignal → mapToExecuteOptions →
107
+ * AgentTaskSpec → engine.run(PiEngine:specExecuteOptions 还原 + engine 留痕)→
108
+ * executeAndAwait → AgentOutcome → AgentResult
80
109
  *
81
110
  * 错误处理:不 reject。
111
+ * - 路由失败(未注册 id / probe 失败 + 守卫或 strict)→ AgentResult.error(错误码前缀)
82
112
  * - executeAndAwait 内部失败 → 返回 AgentResult(success:false) → 已映射 error 字段
83
113
  * - executeAndAwait throw(嵌套超限 BC-12)→ catch → AgentResult.error
84
114
  * - spawn 级失败已由 runSpawn 内部收口为 failed AgentResult(不逃逸)
@@ -91,22 +121,118 @@ export class SubprocessAgentRunner implements AgentRunner {
91
121
  ): Promise<AgentResult> {
92
122
  const startedAt = Date.now();
93
123
 
124
+ // ── P4 路由:三层优先级 + probe + fallback 三守卫(D9①/D7)──
125
+ // 路由在最前——引擎身份决定 journal 路径与后续一切执行面;失败(未注册 id /
126
+ // probe 失败 + strict/守卫)按「不 reject」契约转 result.error。
127
+ // pi 快路径同步短路(不经 routeEngine 的 await):pi 恒免探、无 fallback 可言、
128
+ // engineFor('pi') 本地 DI 绑定恒可用——不引入微任务边界,缺省路径时序与 P1 接线
129
+ // 前完全一致(下游依赖「run 内首个 await 前已触达 executeAndAwait」的时序契约)。
130
+ let routingInput: EngineRoutingInput;
131
+ let route: EngineRouteResult;
132
+ try {
133
+ routingInput = this.buildRoutingInput(opts);
134
+ const routing = resolveEngineRouting(routingInput);
135
+ if (routing.engineId === "pi") {
136
+ route = {
137
+ engine: this.piEngine,
138
+ engineId: "pi",
139
+ requestedEngineId: "pi",
140
+ source: routing.source,
141
+ };
142
+ } else {
143
+ route = await routeEngine({
144
+ routing: routingInput,
145
+ taskModel: opts.model,
146
+ strict: getModelConfigService()?.getGlobalConfig().engineRouting?.strict === true,
147
+ probe: (engineId) => this.engineFor(engineId).probe(),
148
+ getEngineFn: (engineId) => this.engineFor(engineId),
149
+ // pi 经本地 DI 绑定恒可用(不依赖 registry 全局注册态——SAR 持有服务引用),
150
+ // has/list 注入同一口径,engine_not_found 文案不会把本地 pi 漏报成未注册
151
+ hasEngineFn: (engineId) => engineId === "pi" || hasEngine(engineId),
152
+ listEnginesFn: () => (hasEngine("pi") ? listEngines() : ["pi", ...listEngines()]),
153
+ });
154
+ }
155
+ } catch (err) {
156
+ // buildRoutingInput 的 agent 解析期校验(未注册 frontmatter engine)与路由失败
157
+ // (probe + strict/守卫)一并在此收口——「不 reject」契约
158
+ return errorResult(err, startedAt);
159
+ }
160
+
161
+ // ── P2 event journal 接线(设计 D6 第②级;对齐点③:路径权威 = 引擎池 key)──
162
+ // host 在 onEvent 回调内统一落盘(全引擎免费获得②级数据源)。初始 poolKey 用 pi
163
+ // 缺省占位(pi 恒 'shared');非池化稳定引擎(zcode)在 prepare 期经
164
+ // RunContext.onPoolResolved 声明实际池 key → writer.retarget——保证 journal 落盘
165
+ // 路径与 handle.poolKey 同源(handle.journalPath 由本方法 run 返回后回填)。
166
+ //
167
+ // taskId 为宿主侧任务标识(journal 文件名与池引用计数 key)——executeAndAwait
168
+ // 不外露内部 record id(取真实 id 需 hook record store 且改 createRecordForMode
169
+ // 签名,影响面大;P4 决策:保留占位,`sa-` 前缀与 record id 同构。影响面:record
170
+ // GC 时无法按 taskId 联动删 journal(journal 依赖 30 天 TTL 自然回收),read ②级
171
+ // 经 handle.journalPath 自描述定位不受影响)。
172
+ const taskId = `sa-${crypto.randomUUID()}`;
173
+ const journal = new JournalWriter({
174
+ path: resolveJournalPath(getEngineDataDir(), route.engineId, PI_POOL_KEY, taskId),
175
+ taskId,
176
+ engineId: route.engineId,
177
+ });
178
+ const retargetJournal = (poolKey: string): void => {
179
+ journal.retarget(resolveJournalPath(getEngineDataDir(), route.engineId, poolKey, taskId));
180
+ };
181
+ // 包装:先写 journal 再转发原 onEvent(原 onEvent 未传时也恒传包装版——
182
+ // 下游 onEvent 通道是事件生成后的纯转发,无行为分支,仅多一次入队)
183
+ const journalingOnEvent = (event: AgentEvent): void => {
184
+ journal.append(event);
185
+ onEvent?.(event);
186
+ };
187
+
94
188
  try {
95
- // ── D-A9: timeoutMs 合并 signal ──
189
+ // ── [U1 D2] RunContext.modelRef 接入:ctxModel 继承路径的孪生守卫 ──
190
+ // ctxModel 是运行时已验证的 ModelInfo(豁免 registry 存在性复查),但继承产出的
191
+ // canonical 串与显式入参走同一个 pi pattern 引擎,孪生守卫同等适用(modelRefFromVerified)。
192
+ // 守卫在 engine.run 之前同步拒绝:含孪生 registry 下不产生任何 record/spawn,
193
+ // 失败走下方 catch → errorResult(错误文案含恢复指引)。
194
+ // RunContext 类型本身定义在 engine/port.ts(跨模块 port,不在本单元领地),
195
+ // 故接入点为构造 runCtx 前的守卫调用;pi 链路下游 resolveModel 的 ctxModel 分支
196
+ // 有同一守卫(model-resolver.ts),两处共用同一入口函数。
197
+ if (this.ctxModel) {
198
+ const modelService = getModelConfigService();
199
+ if (modelService) modelRefFromVerified(this.ctxModel, modelService.getModelRegistry());
200
+ }
201
+
202
+ // ── D-A9: timeoutMs 合并 signal(超时 abort 带 HOST_TIMEOUT_ABORT_REASON 标记)──
96
203
  const mergedSignal = mergeTimeoutSignal(signal, opts.timeoutMs);
97
204
 
98
205
  // ── D-A2 + D-008: AgentCallOpts → ExecuteOptions 映射 ──
99
206
  const mappedOpts: ExecuteOptions = mapToExecuteOptions(opts, this.ctxModel);
100
207
 
101
- // ── D-A8: onEvent 桥接 ──
102
- // executeAndAwait 发强类型 AgentEvent(session-runner handleSdkEvent 出口)。
103
- // workflow 的 onEvent 闭包(error-recovery.ts dispatchAgentCall)类型已升级为
104
- // (event: AgentEvent) => updateFromEvent(liveRecord, event)(D-005)。
105
- // SAR 直接透传 onEvent——类型对齐后零桥接开销。
106
- const bridgedOnEvent = onEvent;
107
-
108
- // ── 核心委托 ──
109
- return await this.subagentService.executeAndAwait(mappedOpts, mergedSignal, bridgedOnEvent, stream);
208
+ // ── P1/P4 引擎接线:EnginePort.run ──
209
+ const runCtx: RunContext = {
210
+ taskId,
211
+ poolKey: PI_POOL_KEY,
212
+ signal: mergedSignal,
213
+ onEvent: journalingOnEvent,
214
+ ctxModel: this.ctxModel,
215
+ ...(route.engineFallback !== undefined ? { engineFallback: route.engineFallback } : {}),
216
+ onPoolResolved: retargetJournal,
217
+ // [U0 D10] 终止链路径①:引擎 spawn 的子进程注册进 session-runner 的
218
+ // spawnedChildren 记账(dispose killAll 收割兜底对 workflow 域引擎任务生效);
219
+ // taskId('sa-' 前缀)即记账 key,与 chat 域 kickOffEngineRun 的 record.id 同构
220
+ onChildSpawned: (child) => registerSpawnedChildForRecord(taskId, child),
221
+ ...(stream !== undefined ? { stream } : {}),
222
+ // 解耦形态(有 schemaEnv 无 schema)的兜底通道——耦合形态下引擎从 task.schema
223
+ // 派生等值,此值被忽略(见 RunContext.schemaEnv 注释)
224
+ ...(mappedOpts.schemaEnv !== undefined ? { schemaEnv: mappedOpts.schemaEnv } : {}),
225
+ };
226
+ const { handle, outcome } = await route.engine.run(
227
+ // 泛化为中立声明(PiEngine 内部再还原回 ExecuteOptions——往返保真,
228
+ // 由 engines/pi/__tests__/task-spec-mapper.test.ts 逐字段锁定)
229
+ executeOptionsToTaskSpec(mappedOpts),
230
+ runCtx,
231
+ );
232
+ // handle.journalPath 回填(§3.3.6:read ②级的自描述定位符——运行期落盘路径
233
+ // 权威在 writer,handle 记录最终路径供跨重启 read 消费)
234
+ backfillJournalPath(handle, journal.path);
235
+ return outcomeToRunnerResult(outcome);
110
236
  } catch (err) {
111
237
  // executeAndAwait throw(嵌套超限 ForkDepthExceededError,BC-12)或未预期异常 → 不 reject,入 error。
112
238
  const message = err instanceof Error ? err.message : String(err);
@@ -116,6 +242,76 @@ export class SubprocessAgentRunner implements AgentRunner {
116
242
  error: message,
117
243
  toolCalls: [],
118
244
  };
245
+ } finally {
246
+ // run 终态后 flush + fsync 一次(§3.3.6 写入纪律);写失败已由 writer 内部
247
+ // warn + failed 收口,close 不抛(journal 是②级尽力而为数据源)
248
+ await journal.close();
119
249
  }
120
250
  }
251
+
252
+ // ── 内部 ──
253
+
254
+ /**
255
+ * 引擎获取:pi 走 per-session DI 绑定(mock 语义 + 生产同单例,P1 行为零变化);
256
+ * 非 pi 经 registry.getEngine 动态获取(P4:引擎身份归属注册表,未注册 id 抛
257
+ * EngineNotFoundError——路由期已前置校验,这里是防御性兜底)。
258
+ */
259
+ private engineFor(engineId: string): EnginePort {
260
+ if (engineId === "pi") return this.piEngine;
261
+ return getEngine(engineId);
262
+ }
263
+
264
+ /**
265
+ * 三层路由输入装配(D9):调用参数(opts.engine,workflow step 级透传)> agent
266
+ * frontmatter(ModelConfigService.getAgentConfig——loadByPath mtime 缓存,幂等)>
267
+ * 全局默认(config.json defaultEngine)。单例未就绪(session_start 前/测试 mock)
268
+ * 时各层缺省——落内置 'pi'。
269
+ */
270
+ private buildRoutingInput(opts: AgentCallOpts): EngineRoutingInput {
271
+ const service = getModelConfigService();
272
+ const agentEngine =
273
+ opts.agent !== undefined && opts.agent !== "" ? service?.getAgentConfig(opts.agent)?.engine : undefined;
274
+ const globalDefault = service?.getGlobalConfig().defaultEngine;
275
+ return {
276
+ ...(opts.engine !== undefined && opts.engine !== "" ? { callEngine: opts.engine } : {}),
277
+ ...(agentEngine !== undefined && agentEngine !== "" ? { agentEngine } : {}),
278
+ ...(globalDefault !== undefined && globalDefault !== "" ? { globalDefaultEngine: globalDefault } : {}),
279
+ };
280
+ }
281
+ }
282
+
283
+ /** 路由期错误 → AgentResult.error(错误码前缀格式保留——engine_not_found 等)。 */
284
+ function errorResult(err: unknown, startedAt: number): AgentResult {
285
+ return {
286
+ content: "",
287
+ durationMs: Date.now() - startedAt,
288
+ error: err instanceof Error ? err.message : String(err),
289
+ toolCalls: [],
290
+ };
291
+ }
292
+
293
+ /** handle.journalPath 回填(一次写者:SAR 是 handle 的首个消费者)。 */
294
+ function backfillJournalPath(handle: EngineHandle, journalPath: string): void {
295
+ handle.data.journalPath = journalPath;
296
+ }
297
+
298
+ /**
299
+ * AgentOutcome → workflow AgentResult:剥离引擎层新增字段(engineId/engineFallback/
300
+ * exitCode——engineFallback 经 record(pi 路径)/outcome(zcode 路径)留痕,GUI 投影
301
+ * 通道在后续 wave 接线,workflow 引擎不消费)。其余字段由 PiEngine 从 executeAndAwait
302
+ * 的返回值逐字段映射而来,字段全集完整性由 pi-engine 单测锁定(缺字段会在该处转红,
303
+ * 不会静默丢失)。
304
+ */
305
+ function outcomeToRunnerResult(outcome: AgentOutcome): AgentResult {
306
+ return {
307
+ content: outcome.content,
308
+ parsedOutput: outcome.parsedOutput,
309
+ usage: outcome.usage,
310
+ durationMs: outcome.durationMs,
311
+ error: outcome.error,
312
+ sessionId: outcome.sessionId,
313
+ sessionFile: outcome.sessionFile,
314
+ worktreePath: outcome.worktreePath,
315
+ toolCalls: outcome.toolCalls,
316
+ };
121
317
  }
@@ -61,6 +61,37 @@ export type ExecutionStatus = "running" | "closed";
61
61
  */
62
62
  export type ClosedReason = 'parent-shutdown' | 'parent-fork' | 'parent-new' | 'user-close' | 'cancelled' | 'gc';
63
63
 
64
+ /** ClosedReason 全枚举值(运行时守卫用——防御性解析外部输入时校验成员资格)。 */
65
+ export const CLOSED_REASONS: readonly ClosedReason[] = [
66
+ 'parent-shutdown',
67
+ 'parent-fork',
68
+ 'parent-new',
69
+ 'user-close',
70
+ 'cancelled',
71
+ 'gc',
72
+ ];
73
+
74
+ /**
75
+ * 终态三态对外语义(U3 C-outcome 一等披露)。
76
+ *
77
+ * 由 completeRecord 唯一写入点按 deriveOutcome 一次计算(判定顺序:cancelled 优先
78
+ * → error 非空 → completed),消费方(project/list/notify 文案/渲染器)只读本字段,
79
+ * 不再各自手写成败推导 switch(三处同构 switch 已随 U3 收敛删除)。
80
+ *
81
+ * [D6 显式取舍] parent-shutdown/parent-fork/parent-new 合成关闭(subagent-service
82
+ * disposeAllRecords 合成 result 恒写 error:"closed due to ...")落 "failed"——语义为
83
+ * 「父进程关闭时子 agent 未完成即失败」,选定行为而非疏漏,勿当 bug 改回 cancelled。
84
+ */
85
+ export type ExecutionOutcome = "completed" | "failed" | "cancelled";
86
+
87
+ /**
88
+ * 对外投影的 outcome 联合:含历史 record(outcome 字段诞生前的存量数据)兼容态。
89
+ * 投影层(projectOutcome 唯一出口)对无 outcome 字段的 closed record 按
90
+ * deriveOutcome(closedReason, error) 兜底派生;"closed-legacy" 预留给连派生输入都
91
+ * 不足以判读的存量形态,消费方必须处理该成员(不得因未知值崩溃)。
92
+ */
93
+ export type ProjectedOutcome = ExecutionOutcome | "closed-legacy";
94
+
64
95
  /**
65
96
  * 对外四态(设计决策 10 细则 3):内部 ExecutionStatus(v4 B-1 两态)收敛为 agent
66
97
  * 可理解的状态语义。真实映射只有两条:
@@ -88,6 +119,15 @@ export type ExecutionMode = "background";
88
119
  *
89
120
  * 设计:AgentEvent 携带 updateFromEvent 收口进 record 所需的**全部数据**——
90
121
  * tool_end 带 result(供 turn.toolCalls 存完整 ToolCall),无需翻译层旁路累积。
122
+ *
123
+ * ACP 词汇对照(D11 注记级校准,零行为变更;新引擎实现者按本表对齐语义,
124
+ * 详见 docs/architecture/subagent-engine-gui-visibility.md §3.3 D11):
125
+ * text_delta / thinking_delta ↔ ACP content blocks(text / thinking)
126
+ * tool_start / tool_end ↔ ACP tool_call / tool_call_update
127
+ * turn_end / message_end ↔ ACP prompt turn 终态(stop_reason + usage)
128
+ * compaction ↔ ACP session/compaction
129
+ * 本协议以 pi 为语义锚点(D3)——命名不迁移,对照表仅保证未来 AcpEngine 适配器
130
+ * 与跨引擎 trace 映射的翻译成本最低。
91
131
  */
92
132
  export type AgentEvent =
93
133
  | { type: "tool_start"; toolName: string; args?: unknown }
@@ -384,6 +424,23 @@ export interface ExecutionRecord {
384
424
  * 向后兼容:旧 record 无此字段,按默认值处理。
385
425
  */
386
426
  readonly idleTimeoutMs?: number;
427
+ /**
428
+ * 实际执行引擎 id(P4 路由留痕,D9①)。创建时确定不可变;缺省(存量 record)
429
+ * = pi 投影(消费方零迁移)。持久化经 subagent-record entry。
430
+ */
431
+ readonly engine?: string;
432
+ /**
433
+ * 引擎 fallback 留痕(D9①:probe 失败路由回默认引擎)。GUI 警告条数据源;
434
+ * 缺省 = 无 fallback。持久化经 subagent-record entry。
435
+ */
436
+ readonly engineFallback?: { from: string; reason: string };
437
+ /**
438
+ * 引擎自描述定位符(U2:非 pi run resolve 后回填、终态迁移落 entry 前——run 前
439
+ * 缺省不可用)。sessionRef 整体透传(失败终态 sessionId 缺失时仍回填已有部分,
440
+ * 读侧①级降②级的防御形态);journalPath 为 retarget 后实际落盘路径。pi 分支不
441
+ * 回填(sessionFile 即定位符)。持久化经 subagent-record entry。
442
+ */
443
+ engineHandle?: { sessionRef: Record<string, string>; journalPath?: string; poolKey: string };
387
444
 
388
445
  // ── 状态(实时更新)──
389
446
  status: ExecutionStatus;
@@ -391,6 +448,12 @@ export interface ExecutionRecord {
391
448
  * 由 tryTransition(record, "closed", reason) 写入;投影层按需派生对外语义。
392
449
  * 向后兼容:旧 record 无此字段,按 gc 处理(通用完成/失败)。 */
393
450
  closedReason?: ClosedReason;
451
+ /**
452
+ * 终态三态对外语义(U3 C-outcome)。completeRecord 唯一写入点按 deriveOutcome
453
+ * 一次计算,消费方只读本字段不再自行推导。向后兼容:旧 record / 磁盘重建
454
+ * record 无此字段,投影层按 projectOutcome 兜底(closed-legacy 语义)。
455
+ */
456
+ outcome?: ExecutionOutcome;
394
457
  /** 完整执行内容,按 turn 组织。createRecord 初始化为 [空 turn]。 */
395
458
  turns: Turn[];
396
459
  /** turn 计数(= turns.filter(closed).length,冗余存储供投影直接读)。 */
@@ -494,6 +557,11 @@ export interface ExecutionRecord {
494
557
  */
495
558
  export interface SubagentToolDetails {
496
559
  status: ExecutionStatus;
560
+ /**
561
+ * 终态三态对外语义(U3 C-outcome,projectOutcome 唯一出口)。running → undefined;
562
+ * 历史数据无 outcome 字段时兜底派生(见 ProjectedOutcome)。
563
+ */
564
+ outcome?: ProjectedOutcome;
497
565
  mode: ExecutionMode;
498
566
  agent: string;
499
567
  model: string;
@@ -538,6 +606,10 @@ export interface ExecuteOptions {
538
606
  schema?: Record<string, unknown>;
539
607
  /** D-A6 bridge: workflow schemaEnv 经 ExecuteOptions 透传到 runSpawn childEnv。 */
540
608
  schemaEnv?: string;
609
+ /**
610
+ * Turn 上限 limiter。显式 0/负 = 显式不限:压过 SPAWN_WATCHDOG_ENV 兑底不挂
611
+ * watchdog(SP-6 参数 > env,U5);undefined 未传才由 env 兑底。
612
+ */
541
613
  maxTurns?: number;
542
614
  graceTurns?: number;
543
615
  /** sync 模式来自 Pi tool 框架;background 模式 hub 忽略,自建 controller。 */
@@ -561,8 +633,19 @@ export interface ExecuteOptions {
561
633
  /**
562
634
  * 空闲超时毫秒数(仅 conversation 模式有意义)。覆盖默认 5min idle timeout。
563
635
  * 优先级:参数 > env XYZ_SUBAGENT_IDLE_TIMEOUT_MS > 默认 300000ms。
636
+ * 显式传 0/负数 = 禁用 idle GC(不挂 timer;旧实现 0 会落成 setTimeout(0) 立即 kill)。
564
637
  */
565
638
  idleTimeoutMs?: number;
639
+ /**
640
+ * 实际执行引擎 id(P4 路由留痕):pi 引擎由 PiEngine.run 在还原 opts 时写入;
641
+ * 缺省(历史调用方不设)= pi 投影。createRecordForMode 读入 record identity。
642
+ */
643
+ engine?: string;
644
+ /**
645
+ * 引擎 fallback 留痕(D9①:probe 失败路由回默认引擎时由路由层写入)。
646
+ * from = 请求引擎 id,reason 恒 'engine_probe_failed'(GUI 警告条数据源)。
647
+ */
648
+ engineFallback?: { from: string; reason: string };
566
649
  // 注:fork 深度不从外部传入(曾暴露 parentForkDepth,改用 ALS 后 execute 内部从调用链派生,
567
650
  // 公开字段成为死字段误导调用方,已移除)。深度限制检查见 session-runner.ts 内部 RunOptions.parentForkDepth
568
651
  // (与历史残留的 types.ts RunOptions 同名不同 interface——后者已删除)。
@@ -607,10 +690,13 @@ export interface SubagentListItem {
607
690
  /** 可冷路径 resume(running 且无活进程句柄)。[v4 A-6] B-1「可续聊」对外表达,
608
691
  * agent 据 list 判断哪些 running subagent 实际可续聊(vs 正在忙)。 */
609
692
  resumable?: boolean;
610
- /** L2 关闭原因子枚举(仅 status="closed" 时有意义)。[v4 A-6] SP-4 级联关闭告知
611
- * 替代——砍 before_agent_start 注入通道后,被级联关闭的 record 经 list
612
- * (includeFinished:true)可查,closedReason 显示 'parent-fork'/'parent-new' 等。 */
613
- closedReason?: ClosedReason;
693
+ /**
694
+ * 终态三态对外语义(U3 C-outcome 一等披露,projectOutcome 唯一出口):
695
+ * completed / failed / cancelled,历史 record 无 outcome 字段时兜底派生,
696
+ * 不可判读的存量形态为 "closed-legacy"。GUI pane / agent 据此判读成败,
697
+ * 无需翻 error 字段原文(S5)。
698
+ */
699
+ outcome?: ProjectedOutcome;
614
700
  }
615
701
 
616
702
  /** background 启动的内层响应(挂在 SubagentToolResult.bgResponse)。 */
@@ -619,6 +705,19 @@ export interface BgResponse {
619
705
  mode: "background";
620
706
  /** 启动提示文案("detached, will notify on completion")。 */
621
707
  message: string;
708
+ /**
709
+ * 终态三态语义(U3 C-outcome 对外 JSON 契约完备位)。start 时点 record 尚未终态,
710
+ * 恒 undefined(JSON.stringify 落键省略);终态成败语义经 list items[].outcome
711
+ * 披露。旧字段 status/mode/message 原样保留(向后兼容)。
712
+ */
713
+ outcome?: ProjectedOutcome;
714
+ /**
715
+ * 通知投递契约回显位(U1 预置,U2 通知账本的契约声明)。恒值
716
+ * "ledger+at-least-once":主 agent 在当前 run 结束或有限延迟内收到完成通知,
717
+ * 送达保证为 at-least-once + notifyId 幂等可识别。字段与填充由 U1 负责,
718
+ * 值语义由 U2(execution/notify-ledger.ts)兑现。
719
+ */
720
+ notifyContract: "ledger+at-least-once";
622
721
  }
623
722
 
624
723
  /** list 的内层响应(挂在 SubagentToolResult.listResponse)。 */
@@ -661,7 +760,7 @@ export interface CloseResponse {
661
760
  * - close → closeResponse(subagentId 有值;sessionFile 无意义,可为 null)
662
761
  */
663
762
  export type SubagentToolResult =
664
- | { action: "start"; subagentId: string; sessionFile: string | null; slug: string; bgResponse: BgResponse; __gui__?: GuiRenderResult }
763
+ | { action: "start"; subagentId: string; sessionFile: string | null; slug: string; /** registry 全等回显(U1):放行即与 registry 条目全等,"provider/id" 形态。 */ model: string; bgResponse: BgResponse; __gui__?: GuiRenderResult }
665
764
  | { action: "list"; subagentId: null; sessionFile: null; listResponse: ListResponse; __gui__?: GuiRenderResult }
666
765
  | { action: "cancel"; subagentId: string; sessionFile: null; cancelResponse: CancelResponse; __gui__?: GuiRenderResult }
667
766
  | { action: "message"; subagentId: string; sessionFile: null; messageResponse: MessageResponse; __gui__?: GuiRenderResult }
@@ -731,6 +830,19 @@ export interface SubagentRecord {
731
830
  externalInstance?: AliveMarker;
732
831
  /** fork 模式下的 worktree handle。 */
733
832
  worktreeHandle?: WorktreeHandle;
833
+ /**
834
+ * 实际执行引擎 id(P4 路由留痕)。缺省 = pi 投影(存量 record 零迁移);
835
+ * GUI 警告条/引擎标记的数据源之一。
836
+ */
837
+ engine?: string;
838
+ /** 引擎 fallback 留痕(D9①:probe 失败路由回默认引擎)。GUI 警告条数据源。 */
839
+ engineFallback?: { from: string; reason: string };
840
+ /**
841
+ * 引擎自描述定位符(U1:EngineHandleData 的持久化消费面子集,引擎无关——
842
+ * sessionRef 整体透传不枚举内部键)。read 降级链①②级的数据源(runtime
843
+ * subagent-engine-history);缺省 = pi(走 JSONL 直读链)。
844
+ */
845
+ engineHandle?: { sessionRef: Record<string, string>; journalPath?: string; poolKey: string };
734
846
  }
735
847
 
736
848
  // ============================================================
@@ -747,6 +859,13 @@ export interface SubagentRecord {
747
859
  export interface SubagentsGlobalConfig {
748
860
  version: number;
749
861
  maxConcurrent: number;
862
+ /**
863
+ * 全局默认执行引擎(D9 三层优先级的最底层:调用参数 > agent frontmatter > 本值)。
864
+ * 缺省 'pi'(P4 路由层 DEFAULT_ENGINE_ID)。加载期只做类型校验,注册表校验归路由层。
865
+ */
866
+ defaultEngine?: string;
867
+ /** 引擎路由策略(D9①):strict=true 时一切 probe 失败直接报错(不 fallback)。 */
868
+ engineRouting?: { strict: boolean };
750
869
  }
751
870
 
752
871
  // ============================================================
@@ -55,10 +55,20 @@ export function createUiRequestQueue(
55
55
  if (processing || queue.length === 0 || closed) return;
56
56
  processing = true;
57
57
  const { id, request, signal } = queue.shift()!;
58
- handleUiRequest(child, id, request, ctx, signal).finally(() => {
59
- processing = false;
60
- processNext();
61
- });
58
+ // [F2] .catch .finally 之前:handleUiRequest 是 async 函数,任何同步异常(parseChannel
59
+ // 解析 throw / respond → writeStdinLine 的 EPIPE 同步 throw / catch 分支内 respond 再次
60
+ // throw)都会变成 rejection。旧链只有 .finally——rejection 穿透后无人接 →
61
+ // unhandledRejection(Node 15+ 默认 mode=throw)可崩父进程。记 error 后吞掉,
62
+ // .finally 照常释放 processing 推进队列(单个请求失败不阻塞后续 UI 请求)。
63
+ handleUiRequest(child, id, request, ctx, signal)
64
+ .catch((err: unknown) => {
65
+ const m = err instanceof Error ? err.message : String(err);
66
+ logger.error(`[subagents] ui request ${id} (${request.method}) failed unexpectedly: ${m}`);
67
+ })
68
+ .finally(() => {
69
+ processing = false;
70
+ processNext();
71
+ });
62
72
  }
63
73
 
64
74
  // [R3] 子进程退出时 abort 所有 pending handler,队列不再阻塞