@zhushanwen/subagent-engine-sdk 0.2.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 (139) hide show
  1. package/dist/best-effort.cjs +88 -0
  2. package/dist/best-effort.d.cts +14 -0
  3. package/dist/best-effort.d.ts +14 -0
  4. package/dist/best-effort.js +8 -0
  5. package/dist/chunk-2DIMPZCQ.js +0 -0
  6. package/dist/chunk-365AUV6N.js +67 -0
  7. package/dist/chunk-3K2P2CM2.js +127 -0
  8. package/dist/chunk-75QEMUGV.js +58 -0
  9. package/dist/chunk-7I4XGL5J.js +95 -0
  10. package/dist/chunk-A75XDJIC.js +227 -0
  11. package/dist/chunk-DSQ7JQKM.js +39 -0
  12. package/dist/chunk-EJMF63R5.js +19 -0
  13. package/dist/chunk-GT6YLLN4.js +46 -0
  14. package/dist/chunk-HYES77BR.js +109 -0
  15. package/dist/chunk-JSBRDJBE.js +30 -0
  16. package/dist/chunk-LEOBWKRM.js +128 -0
  17. package/dist/chunk-N3RL6OVM.js +38 -0
  18. package/dist/chunk-OPMY4G4M.js +27 -0
  19. package/dist/chunk-PPEPBVCC.js +120 -0
  20. package/dist/chunk-PYO3YR7W.js +16 -0
  21. package/dist/chunk-RDH3ZOV6.js +46 -0
  22. package/dist/chunk-RULLX6C6.js +11 -0
  23. package/dist/chunk-X24SFZYW.js +6646 -0
  24. package/dist/chunk-YFSN3D5N.js +216 -0
  25. package/dist/chunk-ZOFFJNJD.js +136 -0
  26. package/dist/chunk-ZXEAW25V.js +132 -0
  27. package/dist/cli-entry.cjs +119 -0
  28. package/dist/cli-entry.d.cts +24 -0
  29. package/dist/cli-entry.d.ts +24 -0
  30. package/dist/cli-entry.js +8 -0
  31. package/dist/contract-types-sSlgppBC.d.cts +352 -0
  32. package/dist/contract-types-sSlgppBC.d.ts +352 -0
  33. package/dist/data-dir.cjs +117 -0
  34. package/dist/data-dir.d.cts +20 -0
  35. package/dist/data-dir.d.ts +20 -0
  36. package/dist/data-dir.js +12 -0
  37. package/dist/env.cjs +205 -0
  38. package/dist/env.d.cts +73 -0
  39. package/dist/env.d.ts +73 -0
  40. package/dist/env.js +16 -0
  41. package/dist/error-codes-DHco5-i_.d.cts +118 -0
  42. package/dist/error-codes-Dhss2Kmk.d.ts +118 -0
  43. package/dist/error-message.cjs +40 -0
  44. package/dist/error-message.d.cts +3 -0
  45. package/dist/error-message.d.ts +3 -0
  46. package/dist/error-message.js +7 -0
  47. package/dist/index.cjs +8375 -0
  48. package/dist/index.d.cts +23 -0
  49. package/dist/index.d.ts +23 -0
  50. package/dist/index.js +268 -0
  51. package/dist/journal-io.cjs +70 -0
  52. package/dist/journal-io.d.cts +12 -0
  53. package/dist/journal-io.d.ts +12 -0
  54. package/dist/journal-io.js +7 -0
  55. package/dist/journal-replay.cjs +256 -0
  56. package/dist/journal-replay.d.cts +45 -0
  57. package/dist/journal-replay.d.ts +45 -0
  58. package/dist/journal-replay.js +17 -0
  59. package/dist/kill-chain.cjs +221 -0
  60. package/dist/kill-chain.d.cts +74 -0
  61. package/dist/kill-chain.d.ts +74 -0
  62. package/dist/kill-chain.js +23 -0
  63. package/dist/logger.cjs +84 -0
  64. package/dist/logger.d.cts +24 -0
  65. package/dist/logger.d.ts +24 -0
  66. package/dist/logger.js +11 -0
  67. package/dist/logs/stderr-rotation.cjs +151 -0
  68. package/dist/logs/stderr-rotation.d.cts +37 -0
  69. package/dist/logs/stderr-rotation.d.ts +37 -0
  70. package/dist/logs/stderr-rotation.js +23 -0
  71. package/dist/nesting-guard.cjs +95 -0
  72. package/dist/nesting-guard.d.cts +65 -0
  73. package/dist/nesting-guard.d.ts +65 -0
  74. package/dist/nesting-guard.js +15 -0
  75. package/dist/node-executor.cjs +180 -0
  76. package/dist/node-executor.d.cts +63 -0
  77. package/dist/node-executor.d.ts +63 -0
  78. package/dist/node-executor.js +16 -0
  79. package/dist/paths.cjs +55 -0
  80. package/dist/paths.d.cts +8 -0
  81. package/dist/paths.d.ts +8 -0
  82. package/dist/paths.js +15 -0
  83. package/dist/port-contract.cjs +35 -0
  84. package/dist/port-contract.d.cts +88 -0
  85. package/dist/port-contract.d.ts +88 -0
  86. package/dist/port-contract.js +7 -0
  87. package/dist/protocol/index.cjs +400 -0
  88. package/dist/protocol/index.d.cts +685 -0
  89. package/dist/protocol/index.d.ts +685 -0
  90. package/dist/protocol/index.js +89 -0
  91. package/dist/relay-env.cjs +71 -0
  92. package/dist/relay-env.d.cts +37 -0
  93. package/dist/relay-env.d.ts +37 -0
  94. package/dist/relay-env.js +23 -0
  95. package/dist/schema-emulation.cjs +6680 -0
  96. package/dist/schema-emulation.d.cts +39 -0
  97. package/dist/schema-emulation.d.ts +39 -0
  98. package/dist/schema-emulation.js +11 -0
  99. package/dist/spawn.cjs +200 -0
  100. package/dist/spawn.d.cts +80 -0
  101. package/dist/spawn.d.ts +80 -0
  102. package/dist/spawn.js +16 -0
  103. package/dist/ui-channels.cjs +120 -0
  104. package/dist/ui-channels.d.cts +60 -0
  105. package/dist/ui-channels.d.ts +60 -0
  106. package/dist/ui-channels.js +9 -0
  107. package/dist/ui-types.cjs +18 -0
  108. package/dist/ui-types.d.cts +62 -0
  109. package/dist/ui-types.d.ts +62 -0
  110. package/dist/ui-types.js +1 -0
  111. package/package.json +58 -0
  112. package/src/best-effort.ts +37 -0
  113. package/src/cli-entry.ts +77 -0
  114. package/src/data-dir.ts +88 -0
  115. package/src/env.ts +265 -0
  116. package/src/error-message.ts +22 -0
  117. package/src/index.ts +63 -0
  118. package/src/journal-io.ts +82 -0
  119. package/src/journal-replay.ts +432 -0
  120. package/src/kill-chain.ts +265 -0
  121. package/src/logger.ts +105 -0
  122. package/src/logs/stderr-rotation.ts +166 -0
  123. package/src/nesting-guard.ts +140 -0
  124. package/src/node-executor.ts +272 -0
  125. package/src/paths.ts +48 -0
  126. package/src/port-contract.ts +117 -0
  127. package/src/protocol/contract-types.ts +378 -0
  128. package/src/protocol/engine-protocol.ts +81 -0
  129. package/src/protocol/error-codes.ts +179 -0
  130. package/src/protocol/frames.ts +145 -0
  131. package/src/protocol/index.ts +12 -0
  132. package/src/protocol/methods.ts +229 -0
  133. package/src/protocol/reverse-channels.ts +274 -0
  134. package/src/protocol/schema.ts +154 -0
  135. package/src/relay-env.ts +60 -0
  136. package/src/schema-emulation.ts +192 -0
  137. package/src/spawn.ts +246 -0
  138. package/src/ui-channels.ts +219 -0
  139. package/src/ui-types.ts +84 -0
@@ -0,0 +1,432 @@
1
+ // src/journal-replay.ts
2
+ //
3
+ // journal → SessionView 的纯投影(引擎侧原语,自 core
4
+ // execution/engine/common/journal-replay.ts + execution-record.ts reducer 核心 +
5
+ // common/session-view-projection.ts 迁入 @zhushanwen/subagent-engine-sdk)。
6
+ // 迁移处置(impl-plan §2.1 journal-replay 行):**纯投影部分(entry → record 的
7
+ // reducer)下沉 SDK,journal I/O 与②级降级链留 core**——
8
+ // - SDK 版 = eventsToSessionView(事件流 → SessionView,live/replay 共用 reducer
9
+ // 语义)+ reducer 本体(updateFromEvent 及其私有 handler)+ Turn → ReplayedTurn
10
+ // 投影 / usage 聚合(原 core session-view-projection.ts 同名函数逐字等价,
11
+ // R6 收口后本模块为唯一实现,双活副本已删);
12
+ // - 留 core = replayJournal(journal 文件 I/O)+ replayJournalToSessionView(read
13
+ // 第②级降级编排)——core 侧引用切换已完成(core 依赖 SDK 方向合法)。
14
+ //
15
+ // record 形态:SDK 侧 reducer 操作 ReplayRecordView(turns/turnCount/totalTokens/
16
+ // lastError 四字段视图)——core ExecutionRecord 留 core(类型闭包表裁决),其结构
17
+ // 满足本视图(W2 双向可赋值断言验证)。重放场景不需要 identity 字段(agent/model/
18
+ // task 等只服务投影与持久化,reducer 不触碰),不搬 createRecord 全量 identity。
19
+ //
20
+ // 设计权威源:docs/architecture/subagent-engine-abstraction.md D6 + §3.3.6「重放等价性」
21
+ // ——journal 重放与 live 通路共用同一 reducer(updateFromEvent 范式),不引入第二套
22
+ // 解析器;conformance C5 断言重放 turns 与 live 一致。
23
+ //
24
+ // CJS 多 entry 内联副本的实例分裂影响 = runningToolIndex WeakMap(按 record 实例
25
+ // 隔离)各自为政,无跨实例语义。
26
+
27
+ import type {
28
+ AgentEvent,
29
+ AgentUsage,
30
+ AgentUsageTotal,
31
+ InternalToolCall,
32
+ ReplayedTurn,
33
+ SessionView,
34
+ ToolCall,
35
+ Turn,
36
+ EngineHandleData,
37
+ } from "./protocol/contract-types.ts";
38
+
39
+ /**
40
+ * reducer 的 record 视图(core ExecutionRecord 的结构子集:reducer 只触碰这四个字段)。
41
+ * core ExecutionRecord 满足本视图(W2 断言挂靠点);SDK 内自持 createReplayRecord 产出。
42
+ */
43
+ export interface ReplayRecordView {
44
+ turns: Turn[];
45
+ turnCount: number;
46
+ totalTokens: number;
47
+ lastError: string | undefined;
48
+ }
49
+
50
+ // ============================================================
51
+ // usage 累积(core execution-record.ts 私有 helper 逐字等价)
52
+ // ============================================================
53
+
54
+ /** usage 单字段求和(undefined 视为 0——与旧 `(a ?? 0) + (b ?? 0)` 内联式逐字等价)。 */
55
+ function sumUsageField(a: number | undefined, b: number | undefined): number {
56
+ return (a ?? 0) + (b ?? 0);
57
+ }
58
+
59
+ /** prev 为空时 next 的规范化拷贝(cost 保留原值,可能 undefined——与旧首条分支逐字等价)。 */
60
+ function usageFromNext(next: AgentUsage): AgentUsage {
61
+ return {
62
+ input: next.input ?? 0,
63
+ output: next.output ?? 0,
64
+ cacheRead: next.cacheRead ?? 0,
65
+ cacheWrite: next.cacheWrite ?? 0,
66
+ cost: next.cost,
67
+ };
68
+ }
69
+
70
+ /**
71
+ * 累加两个 AgentUsage(field-wise)。prev 为空时返回 next 的拷贝。
72
+ * 供 message_end 把 usage 增量并入 turn.usageDelta。
73
+ */
74
+ function addUsage(prev: AgentUsage | undefined, next: AgentUsage): AgentUsage {
75
+ if (prev === undefined) return usageFromNext(next);
76
+ return {
77
+ input: sumUsageField(prev.input, next.input),
78
+ output: sumUsageField(prev.output, next.output),
79
+ cacheRead: sumUsageField(prev.cacheRead, next.cacheRead),
80
+ cacheWrite: sumUsageField(prev.cacheWrite, next.cacheWrite),
81
+ cost: sumUsageField(prev.cost, next.cost),
82
+ };
83
+ }
84
+
85
+ // ============================================================
86
+ // 创建(重放场景的 record 构造)
87
+ // ============================================================
88
+
89
+ /** 创建一个空 turn(text/thinking 空,无 toolCalls,未闭合)。 */
90
+ function emptyTurn(): Turn {
91
+ return { text: "", thinking: "", toolCalls: [], usageDelta: undefined, closed: false };
92
+ }
93
+
94
+ /** 重放场景的 record 构造(对齐 core eventsToSessionView 内 createRecord 后的 reducer 视图)。 */
95
+ export function createReplayRecord(): ReplayRecordView {
96
+ return {
97
+ // turns[] 初始化为 [空 turn]——第一个 turn 从创建即存在,
98
+ // updateFromEvent 直接往 turns[last] 累积,无需「无 turn」分支判断。
99
+ turns: [emptyTurn()],
100
+ turnCount: 0,
101
+ totalTokens: 0,
102
+ lastError: undefined,
103
+ };
104
+ }
105
+
106
+ // ============================================================
107
+ // 事件更新(唯一更新点;core updateFromEvent 私有结构逐字等价)
108
+ // ============================================================
109
+
110
+ /**
111
+ * 取当前正在进行(未 closed)的 turn;若全部 closed 则开新 turn。
112
+ * 保证调用后返回的 turn 一定 closed===false,可安全累积内容。
113
+ */
114
+ function currentTurn(record: ReplayRecordView): Turn {
115
+ const last = record.turns[record.turns.length - 1];
116
+ if (last !== undefined && !last.closed) return last;
117
+ const fresh = emptyTurn();
118
+ record.turns.push(fresh);
119
+ return fresh;
120
+ }
121
+
122
+ /**
123
+ * 在 record.turns[] 范围内倒序找最后一个同名且仍 running 的 toolCall。
124
+ *
125
+ * 扫描所有 turn(非仅当前 turn)——SDK 在 turn_end 后仍可能补发滞后的 tool_end,
126
+ * 仅扫当前 turn 会漏配对、误 push 幽灵 ToolCall。跨 turn 扫描兜底滞后事件。
127
+ *
128
+ * 返回 [turn, index];未找到返回 undefined。
129
+ */
130
+ function findRunningToolCall(
131
+ record: ReplayRecordView,
132
+ toolName: string,
133
+ ): readonly [Turn, number] | undefined {
134
+ for (let t = record.turns.length - 1; t >= 0; t--) {
135
+ const turn = record.turns[t];
136
+ if (turn === undefined) continue;
137
+ for (let i = turn.toolCalls.length - 1; i >= 0; i--) {
138
+ const tc = turn.toolCalls[i];
139
+ if (tc?._status === "running" && tc.toolName === toolName) {
140
+ return [turn, i] as const;
141
+ }
142
+ }
143
+ }
144
+ return undefined;
145
+ }
146
+
147
+ /**
148
+ * [perf] running toolCall 倒序索引:tool_start push 位置入索引,tool_end 弹尾定位
149
+ *(尾部 = 最后 push 的同名项,与 findRunningToolCall 倒序全扫的语义等价),把每次
150
+ * tool_end 的 O(所有 turns × toolCalls) 扫描降为 O(1)。WeakMap 按 record 实例隔离
151
+ *(createRecord 新实例从空索引开始,不影响旧实例)。
152
+ * 索引 miss(重建 record 的历史 running toolCall / 外部注入工具无 tool_start)
153
+ * 回退 findRunningToolCall 全扫兜底——正确性不依赖索引完整性。
154
+ */
155
+ const runningToolIndex = new WeakMap<ReplayRecordView, Map<string, Array<{ turn: Turn; idx: number }>>>();
156
+
157
+ function indexToolStart(record: ReplayRecordView, turn: Turn, toolName: string): void {
158
+ let byName = runningToolIndex.get(record);
159
+ if (byName === undefined) {
160
+ byName = new Map();
161
+ runningToolIndex.set(record, byName);
162
+ }
163
+ const arr = byName.get(toolName);
164
+ if (arr === undefined) {
165
+ byName.set(toolName, [{ turn, idx: turn.toolCalls.length - 1 }]);
166
+ } else {
167
+ arr.push({ turn, idx: turn.toolCalls.length - 1 });
168
+ }
169
+ }
170
+
171
+ // ── 各事件处理器(updateFromEvent 按 case 分发,每个处理器单一职责)──
172
+
173
+ /** text_delta:流式累积进当前 turn 的 text(完整内容,非切片)。 */
174
+ function applyTextDelta(
175
+ record: ReplayRecordView,
176
+ event: Extract<AgentEvent, { type: "text_delta" }>,
177
+ ): void {
178
+ currentTurn(record).text += event.delta;
179
+ }
180
+
181
+ /** thinking_delta:流式累积进当前 turn 的 thinking(完整内容)。 */
182
+ function applyThinkingDelta(
183
+ record: ReplayRecordView,
184
+ event: Extract<AgentEvent, { type: "thinking_delta" }>,
185
+ ): void {
186
+ currentTurn(record).thinking += event.delta;
187
+ }
188
+
189
+ /** tool_start:push 一个 running 的 InternalToolCall(带 startedTs)+ 弹尾索引入册。 */
190
+ function applyToolStart(
191
+ record: ReplayRecordView,
192
+ event: Extract<AgentEvent, { type: "tool_start" }>,
193
+ ): void {
194
+ const tc: InternalToolCall = {
195
+ toolName: event.toolName,
196
+ args: event.args,
197
+ result: undefined,
198
+ isError: false,
199
+ _status: "running",
200
+ startedTs: Date.now(),
201
+ };
202
+ const turn = currentTurn(record);
203
+ turn.toolCalls.push(tc);
204
+ indexToolStart(record, turn, event.toolName);
205
+ }
206
+
207
+ /**
208
+ * tool_end 定位:索引弹尾 O(1) 命中 running 同名 toolCall;索引 miss(无记录 / 槽位
209
+ * 已非 running)回退 findRunningToolCall 跨 turn 倒序全扫兜底。
210
+ * 返回 [turn, index];两路都 miss 返回 undefined。
211
+ */
212
+ function matchRunningToolCall(
213
+ record: ReplayRecordView,
214
+ toolName: string,
215
+ ): readonly [Turn, number] | undefined {
216
+ const byName = runningToolIndex.get(record);
217
+ const arr = byName?.get(toolName);
218
+ if (arr !== undefined && arr.length > 0) {
219
+ const item = arr[arr.length - 1];
220
+ arr.pop();
221
+ const tc = item.turn.toolCalls[item.idx];
222
+ if (tc !== undefined && tc._status === "running") {
223
+ return [item.turn, item.idx] as const;
224
+ }
225
+ }
226
+ // 兜底:重建 record 的历史 running toolCall(索引未覆盖)、索引项被外部路径
227
+ // 置非 running 等场景——保持与旧实现一致的跨 turn 倒序全扫。
228
+ return findRunningToolCall(record, toolName);
229
+ }
230
+
231
+ /**
232
+ * tool_end:命中则回填 result/isError/_status;未命中(SDK 发了 tool_end 但无对应
233
+ * tool_start,如外部注入的工具)直接 push 一个已完成的 InternalToolCall,避免数据丢失。
234
+ */
235
+ function applyToolEnd(
236
+ record: ReplayRecordView,
237
+ event: Extract<AgentEvent, { type: "tool_end" }>,
238
+ ): void {
239
+ const matched = matchRunningToolCall(record, event.toolName);
240
+ if (matched !== undefined) {
241
+ const [turn, i] = matched;
242
+ const tc = turn.toolCalls[i]!;
243
+ tc.args = event.args ?? tc.args;
244
+ tc.result = event.result;
245
+ tc.isError = event.isError ?? false;
246
+ tc._status = event.isError ? "failed" : "done";
247
+ return;
248
+ }
249
+ currentTurn(record).toolCalls.push({
250
+ toolName: event.toolName,
251
+ args: event.args,
252
+ result: event.result,
253
+ isError: event.isError ?? false,
254
+ _status: event.isError ? "failed" : "done",
255
+ startedTs: Date.now(),
256
+ });
257
+ }
258
+
259
+ /**
260
+ * turn_end:闭合当前 turn,记 closedTs(真实墙钟),turnCount++,清 lastError。
261
+ * 正常闭合清 lastError:瞬态 error 恢复后不应误判 success=false
262
+ * (若 turn_end 后 message_end 报 error,会在 message_end 处理器重新写回)。
263
+ */
264
+ function applyTurnEnd(record: ReplayRecordView): void {
265
+ const turn = currentTurn(record);
266
+ turn.closed = true;
267
+ turn.closedTs = Date.now();
268
+ record.turnCount += 1;
269
+ record.lastError = undefined;
270
+ }
271
+
272
+ /**
273
+ * message_end:usage 增量存进末 turn.usageDelta(直接写末 turn,不开新 turn);
274
+ * totalTokens 累加;error(stopReason=error)记进 lastError。
275
+ *
276
+ * usageDelta 按 message_end **累加**(非覆盖)——同一 turn 内若多次 message_end
277
+ * 到达(或 turn_end 后的滞后 message_end 落到 currentTurn 开的新 turn),
278
+ * 累加保证不丢 usage。getTotalUsage 扁平求和所有 turn,归属 turn 的精确性
279
+ * 不影响最终 total(无消费方读单 turn usage)。
280
+ */
281
+ function applyMessageEnd(
282
+ record: ReplayRecordView,
283
+ event: Extract<AgentEvent, { type: "message_end" }>,
284
+ ): void {
285
+ if (event.usage) {
286
+ const turn = currentTurn(record);
287
+ turn.usageDelta = addUsage(turn.usageDelta, event.usage);
288
+ // totalTokens 累加四项之和(保留旧语义,投影直接读)
289
+ record.totalTokens +=
290
+ (event.usage.input ?? 0) + (event.usage.output ?? 0) +
291
+ (event.usage.cacheRead ?? 0) + (event.usage.cacheWrite ?? 0);
292
+ }
293
+ if (event.error) {
294
+ record.lastError = event.error;
295
+ }
296
+ }
297
+
298
+ /** error:存 record.lastError(getEventLog 派生 error 条目用)。 */
299
+ function applyErrorEvent(
300
+ record: ReplayRecordView,
301
+ event: Extract<AgentEvent, { type: "error" }>,
302
+ ): void {
303
+ record.lastError = event.message;
304
+ }
305
+
306
+ /**
307
+ * 从 AgentEvent 更新 record。所有数据收口进 record.turns[]。
308
+ * - text/thinking:流式累积进 currentTurn()(完整内容,非切片)
309
+ * - tool_start/end:push 进 currentTurn().toolCalls(含完整 result)
310
+ * tool_end 跨 turn 扫描找 running 同名 toolCall(兜底滞后事件)
311
+ * - turn_end:闭合当前 turn,记 closedTs(真实墙钟,供 getEventLog);
312
+ * 正常闭合清 lastError(瞬态 error 恢复后不应误判 success=false)
313
+ * - message_end:usage 增量存进末 turn.usageDelta(直接写末 turn,不开新 turn);
314
+ * totalTokens 累加
315
+ * - error:存 record.lastError(getEventLog 派生 error 条目用)
316
+ *
317
+ * 唯一写点——replay 与 live 共用(重放等价性的实现体)。
318
+ *
319
+ * 穷尽性:switch 覆盖 AgentEvent 全部 variant;default 的 `never` 断言保证
320
+ * 新增 variant 时编译期报错(而非静默 no-op)。
321
+ */
322
+ export function updateFromEvent(record: ReplayRecordView, event: AgentEvent): void {
323
+ switch (event.type) {
324
+ // ── text / thinking:流式累积进当前 turn ──
325
+ case "text_delta":
326
+ return applyTextDelta(record, event);
327
+ case "thinking_delta":
328
+ return applyThinkingDelta(record, event);
329
+
330
+ // ── tool_start/end:push 进 currentTurn().toolCalls(含完整 result)──
331
+ case "tool_start":
332
+ return applyToolStart(record, event);
333
+ case "tool_end":
334
+ return applyToolEnd(record, event);
335
+
336
+ // ── turn_end:闭合当前 turn ──
337
+ case "turn_end":
338
+ return applyTurnEnd(record);
339
+
340
+ // ── message_end:usage 增量累加 + totalTokens 累加 ──
341
+ case "message_end":
342
+ return applyMessageEnd(record, event);
343
+
344
+ // ── error:存 record.lastError ──
345
+ case "error":
346
+ return applyErrorEvent(record, event);
347
+
348
+ // ── compaction:不产生数据(不变)──
349
+ case "compaction":
350
+ return;
351
+
352
+ default: {
353
+ // 穷尽性检查:新增 AgentEvent variant 时编译期报错
354
+ const _exhaustive: never = event;
355
+ return _exhaustive;
356
+ }
357
+ }
358
+ }
359
+
360
+ // ============================================================
361
+ // Turn → ReplayedTurn 投影 + usage 聚合
362
+ // (原 core session-view-projection.ts 逐字等价,R6 收口后本模块为唯一实现:
363
+ // 投影语义唯一——strip 内部态 + closed 恒 true + usageDelta 聚合,实现也唯一)
364
+ // ============================================================
365
+
366
+ /** InternalToolCall → ToolCall(导出纯净形状,不泄漏 running/done/failed 内部状态机)。 */
367
+ function toExportedToolCall(tc: InternalToolCall): ToolCall {
368
+ return {
369
+ toolName: tc.toolName,
370
+ ...(tc.args !== undefined ? { args: tc.args } : {}),
371
+ ...(tc.result !== undefined ? { result: tc.result } : {}),
372
+ ...(tc.isError !== undefined ? { isError: tc.isError } : {}),
373
+ };
374
+ }
375
+
376
+ /** Turn → ReplayedTurn:剥离内部态(closed 恒 true——重放物无进行时语义,§3.3.6)。 */
377
+ export function toReplayedTurn(turn: Turn): ReplayedTurn {
378
+ return {
379
+ text: turn.text,
380
+ thinking: turn.thinking,
381
+ toolCalls: turn.toolCalls.map(toExportedToolCall),
382
+ closed: true,
383
+ };
384
+ }
385
+
386
+ /** 各 turn usageDelta 聚合为 AgentUsageTotal(无任何 usage 数据时 undefined)。 */
387
+ export function aggregateUsage(turns: readonly Turn[]): AgentUsageTotal | undefined {
388
+ let acc: AgentUsageTotal | undefined;
389
+ for (const turn of turns) {
390
+ const d = turn.usageDelta;
391
+ if (!d) continue;
392
+ if (!acc) acc = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, total: 0 };
393
+ acc.input += d.input;
394
+ acc.output += d.output;
395
+ acc.cacheRead += d.cacheRead;
396
+ acc.cacheWrite += d.cacheWrite;
397
+ acc.cost += d.cost ?? 0;
398
+ }
399
+ if (acc) acc.total = acc.input + acc.output + acc.cacheRead + acc.cacheWrite;
400
+ return acc;
401
+ }
402
+
403
+ // ============================================================
404
+ // 事件流 → SessionView(纯投影出口)
405
+ // ============================================================
406
+
407
+ /**
408
+ * 事件流 → SessionView(live reducer 累积 turns——重放等价性的实现体)。
409
+ * journal I/O(replayJournal)不在此层——read 第②级降级编排(core 版
410
+ * replayJournalToSessionView)留 core,引擎侧按需自读 journal 后调本函数。
411
+ */
412
+ export function eventsToSessionView(
413
+ events: readonly AgentEvent[],
414
+ engineId: string,
415
+ sessionId?: string,
416
+ ): SessionView {
417
+ const record = createReplayRecord();
418
+ for (const ev of events) updateFromEvent(record, ev);
419
+ return {
420
+ engineId,
421
+ ...(sessionId !== undefined ? { sessionId } : {}),
422
+ turns: record.turns.map(toReplayedTurn),
423
+ usage: aggregateUsage(record.turns),
424
+ source: "journal",
425
+ };
426
+ }
427
+
428
+ /** handle.sessionRef 的 sessionId 提取(引擎自定义键,运行时 guard)。 */
429
+ export function sessionIdFromHandle(handle: EngineHandleData): string | undefined {
430
+ const v = handle.sessionRef["sessionId"];
431
+ return typeof v === "string" ? v : undefined;
432
+ }
@@ -0,0 +1,265 @@
1
+ // src/kill-chain.ts
2
+ //
3
+ // 超时杀链与 abort 两级中断(引擎侧原语,自 core execution/engine/common/kill-chain.ts
4
+ // 迁入 @zhushanwen/subagent-engine-sdk,实现体逐字等价)。迁移处置(impl-plan §2.1
5
+ // kill-chain 行,逐项落地):
6
+ // - 契约类型随 SDK 下沉:AgentCallOpts(引擎面子集)/ AgentOutcome 改自
7
+ // src/protocol/contract-types.ts(结构等价,零 core import);
8
+ // - DEFAULT_ENGINE_ID 改参数注入:synthesizeTimeoutOutcome 的 engineId 必填,
9
+ // 不再携带 core registry 缺省值(调用方显式传自己的引擎 id);
10
+ // - logger 走 SDK facade(src/logger.ts);
11
+ // - engineTimeoutDetail / STDOUT_TAIL_ECHO_CHARS 自持(src/protocol/error-codes.ts,
12
+ // 文案与 core errors.ts 逐字等价);toErrorMessage 改用 SDK 单源
13
+ // src/error-message.ts(round1-reuse R11 收编原内联副本)。
14
+ //
15
+ // 设计权威源:docs/architecture/subagent-engine-abstraction.md D1(abort 分级:引擎原生
16
+ // 中断 → 公共杀链兜底;CLI-only 引擎直接杀链,杀死后宿主合成终态)+ §3.3.3
17
+ // engine_timeout / engine_run_failed 行 + 附录 A「CLI 超时」行。
18
+
19
+ import { getLogger } from "./logger.ts";
20
+ import { toErrorMessage } from "./error-message.ts";
21
+ import { engineTimeoutDetail, STDOUT_TAIL_ECHO_CHARS } from "./protocol/error-codes.ts";
22
+ import type { AgentCallOpts, AgentOutcome } from "./protocol/contract-types.ts";
23
+
24
+ const logger = getLogger("subagents");
25
+
26
+ /** STDOUT_TAIL_ECHO_CHARS re-export(core 版同款导出面,调用方免跨模块 import)。 */
27
+ export { STDOUT_TAIL_ECHO_CHARS };
28
+
29
+ // ============================================================
30
+ // 杀链(SIGTERM → grace → SIGKILL)
31
+ // ============================================================
32
+
33
+ /**
34
+ * 可杀子进程的结构形状(Node ChildProcess 的结构子集)。
35
+ * 用结构接口而非 ChildProcess 类型:测试可注入 fake(ChildProcess 全字段构造过重),
36
+ * 且未来 driver host 的常驻进程句柄只要满足此形状即可复用杀链。
37
+ */
38
+ export interface KillableChild {
39
+ /** 非 null = 进程已退出(自然退出码)。 */
40
+ readonly exitCode: number | null;
41
+ /** 非 null = 进程被信号杀死。exitCode/signalCode 任一非 null 即已退出。 */
42
+ readonly signalCode: string | null;
43
+ /** 发信号。返回 false = 进程已不存在(kill no-op)。 */
44
+ kill(signal?: NodeJS.Signals | number): boolean;
45
+ once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
46
+ }
47
+
48
+ // 毫秒→秒换算(SIGKILL 升级 warn 日志的秒数显示)。文件内私有定义:工程内 MS_PER_SECOND
49
+ // 惯例是各使用文件私有常量(execution-record / lifecycle-manager / session-file-gc /
50
+ // session-runner 四处先例),无共享导出源可 import,保持同惯例不另立导出点。
51
+ const MS_PER_SECOND = 1_000;
52
+
53
+ /** SIGTERM 优雅窗口默认值(ms)。实测校准点:设计 §5 待验证检查点⑤(zcode 对 SIGTERM 的响应时序)。 */
54
+ export const DEFAULT_KILL_GRACE_MS = 5_000;
55
+
56
+ /** SIGKILL 发出后的收尸等待上限(ms)——SIGKILL 后进程必死(D 状态罕见),有界等待防挂死。 */
57
+ const SIGKILL_REAP_TIMEOUT_MS = 10_000;
58
+
59
+ /**
60
+ * 宿主超时 abort 的 signal.reason 标记(对齐点④)。mergeTimeoutSignal(SAR 侧超时
61
+ * 合并链)产出;引擎 abort 合成终态时判别「超时 vs 用户 cancel」——超时统一走
62
+ * synthesizeTimeoutOutcome(engine_timeout),cancel 维持中止标记(engine_run_failed)。
63
+ */
64
+ export const HOST_TIMEOUT_ABORT_REASON = "agent-call-timeout";
65
+
66
+ /** killChain 的参数形状。 */
67
+ export interface KillChainOptions {
68
+ /**
69
+ * SIGTERM 优雅窗口(ms)。grace 窗口按引擎参数化(D3-① 杀链合一):pi 传 30s
70
+ * ([race-F4] 现状值)、zcode 传 5s(ZCODE_KILL_GRACE_MS)——两引擎现状逐字节保持。
71
+ */
72
+ graceMs: number;
73
+ /**
74
+ * [D3-①] 内部 timer unref。pi 路径现状 = 升级 timer unref(不阻止主进程退出,dispose
75
+ * killAll 兜底);zcode 现状 = ref'd(timer 挂起时进程等待收尸)——各引擎按现状传。
76
+ */
77
+ unrefTimers?: boolean;
78
+ /**
79
+ * SIGKILL 升级时的 warn 留痕载体(如 `child sa-xxx (source: spawn watchdog)`)。
80
+ * 不传则升级静默(zcode 现状)——pi 侧现状有 warn,由调用方组装完整语境。
81
+ */
82
+ escalationNote?: string;
83
+ }
84
+
85
+ /**
86
+ * 杀链:SIGTERM → 等待 graceMs → 仍存活则 SIGKILL。
87
+ *
88
+ * @returns 'terminated' = SIGTERM 优雅退出(或进程已自行退出);'killed' = 走了 SIGKILL。
89
+ */
90
+ export async function killChain(
91
+ child: KillableChild,
92
+ opts: KillChainOptions,
93
+ ): Promise<"terminated" | "killed"> {
94
+ // 已退出(自然/已被杀)→ 无需发信号,按优雅终止口径返回
95
+ if (child.exitCode !== null || child.signalCode !== null) return "terminated";
96
+
97
+ const exited = waitForExit(child);
98
+ safeKill(child, "SIGTERM");
99
+
100
+ const graceful = await raceTimeout(exited, opts.graceMs, opts.unrefTimers === true);
101
+ if (graceful === "settled") return "terminated";
102
+ // grace 超时后进程可能恰好在检查前一刻退出——复核退出态,避免误杀已死进程
103
+ if (child.exitCode !== null || child.signalCode !== null) return "terminated";
104
+
105
+ if (opts.escalationNote !== undefined) {
106
+ logger.warn(
107
+ `[kill-chain] ${opts.escalationNote} still alive ${opts.graceMs / MS_PER_SECOND}s after SIGTERM, escalating to SIGKILL`,
108
+ );
109
+ }
110
+ safeKill(child, "SIGKILL");
111
+ // 有界收尸:无论等到与否都返回 'killed'(信号已发出,返回值表达「走了 SIGKILL」)
112
+ await raceTimeout(exited, SIGKILL_REAP_TIMEOUT_MS, opts.unrefTimers === true);
113
+ return "killed";
114
+ }
115
+
116
+ /**
117
+ * 发信号兜底包裹:进程恰在退出态检查与 kill 之间自退时,ChildProcess.kill 可能抛
118
+ * (zsub 实测经验)——幂等吞掉并 debug 留痕,不阻断杀链语义(对已退进程信号本就是
119
+ * no-op)。收口自 zcode launcher 的内联实现(对齐点②:单一权威)。
120
+ */
121
+ function safeKill(child: KillableChild, signal: NodeJS.Signals): void {
122
+ try {
123
+ child.kill(signal);
124
+ } catch (err) {
125
+ logger.debug(
126
+ `[kill-chain] ${signal} on exited process: ${toErrorMessage(err)}`,
127
+ );
128
+ }
129
+ }
130
+
131
+ // ============================================================
132
+ // 超时终态合成
133
+ // ============================================================
134
+
135
+ /**
136
+ * 合成 engine_timeout 终态(宿主超时杀链走完后的 AgentOutcome)。
137
+ * 含 stdout 尾部 2000 字 + 恢复指引(§3.3.3 第 6 行);exitCode: null = 被信号杀死
138
+ * (AgentOutcome 的杀链判据,§3.3.5)。
139
+ *
140
+ * engineId 参数注入(原 core 版缺省 DEFAULT_ENGINE_ID——SDK 不感知宿主路由缺省值,
141
+ * 调用方必须显式传实际引擎 id)。
142
+ */
143
+ export function synthesizeTimeoutOutcome(
144
+ task: AgentCallOpts,
145
+ stdoutTail: string,
146
+ engineId: string,
147
+ ): AgentOutcome {
148
+ return {
149
+ content: "",
150
+ // slug 进错误信息:单看 outcome(record 之外)也能定位是哪个任务超时
151
+ error: `engine_timeout: [slug=${task.description ?? "unknown"}] ${engineTimeoutDetail(stdoutTail)}`,
152
+ // 被信号杀死:退出码语义为 null(§3.3.5 AgentOutcome.exitCode 注释)
153
+ exitCode: null,
154
+ engineId,
155
+ };
156
+ }
157
+
158
+ // ============================================================
159
+ // abort 两级编排
160
+ // ============================================================
161
+
162
+ /** 原生中断后的宽限窗口默认值(ms):中断指令送达 → 引擎自行收尾的等待上限。 */
163
+ export const DEFAULT_NATIVE_INTERRUPT_GRACE_MS = 3_000;
164
+
165
+ /**
166
+ * abort 两级编排 helper(D1 abort 分级的公共实现):
167
+ * signal abort 时 ①先调引擎原生中断(tryNativeInterrupt,若有)并给宽限窗口,
168
+ * 窗口内未停(或无原生中断——CLI-only 引擎)则 ②走杀链兜底。
169
+ *
170
+ * 进程自然退出时以 'terminated' settle(挂在 exit 事件上,杀链对已退进程是 no-op),
171
+ * 调用方可安全 await;signal 永不 abort 且进程已退时也会 settle,不悬挂。
172
+ */
173
+ export function abortWithFallback(
174
+ child: KillableChild,
175
+ signal: AbortSignal,
176
+ tryNativeInterrupt?: () => Promise<void>,
177
+ opts?: { graceMs?: number; nativeGraceMs?: number },
178
+ ): Promise<"terminated" | "killed"> {
179
+ const graceMs = opts?.graceMs ?? DEFAULT_KILL_GRACE_MS;
180
+ const nativeGraceMs = opts?.nativeGraceMs ?? DEFAULT_NATIVE_INTERRUPT_GRACE_MS;
181
+
182
+ return new Promise<"terminated" | "killed">((resolve) => {
183
+ let settled = false;
184
+ // 三个 handler 互相引用(finish 移除 onAbort / onAbort 触发 runChain / runChain
185
+ // 兜底 finish),用函数声明 + 提升消解声明环,避免 use-before-define。
186
+ function finish(result: "terminated" | "killed"): void {
187
+ if (settled) return;
188
+ settled = true;
189
+ signal.removeEventListener("abort", onAbort);
190
+ resolve(result);
191
+ }
192
+
193
+ function onExit(): void {
194
+ finish("terminated");
195
+ }
196
+
197
+ function runChain(): void {
198
+ void (async () => {
199
+ if (tryNativeInterrupt) {
200
+ try {
201
+ await tryNativeInterrupt();
202
+ } catch (err) {
203
+ // 原生中断失败(协议错/管道断)→ 直接落杀链,中断失败不阻断兜底。
204
+ // debug 级留诊断线索:这不是错误终态,只是该引擎优雅中断不可用
205
+ logger.debug(
206
+ `[kill-chain] native interrupt failed, falling back to kill chain: ${
207
+ toErrorMessage(err)
208
+ }`,
209
+ );
210
+ }
211
+ const stopped = await raceTimeout(waitForExit(child), nativeGraceMs);
212
+ if (stopped === "settled" || settled) return; // 原生中断生效,进程已停
213
+ }
214
+ finish(await killChain(child, { graceMs }));
215
+ })();
216
+ }
217
+
218
+ function onAbort(): void {
219
+ runChain();
220
+ }
221
+
222
+ // 进程自然退出(含原生中断生效)→ 优雅终止口径。
223
+ // once listener 不显式移除:触发时 finish 已幂等,单 child 单 listener 无堆积。
224
+ child.once("exit", onExit);
225
+ if (child.exitCode !== null || child.signalCode !== null) {
226
+ finish("terminated");
227
+ return;
228
+ }
229
+
230
+ if (signal.aborted) runChain();
231
+ else signal.addEventListener("abort", onAbort, { once: true });
232
+ });
233
+ }
234
+
235
+ // ── 内部工具 ────────────────────────────────────────────────────
236
+
237
+ /** 等 child 退出(已退出立即 settle;否则挂一次性 exit listener)。 */
238
+ function waitForExit(child: KillableChild): Promise<void> {
239
+ return new Promise<void>((resolve) => {
240
+ if (child.exitCode !== null || child.signalCode !== null) {
241
+ resolve();
242
+ return;
243
+ }
244
+ child.once("exit", () => resolve());
245
+ });
246
+ }
247
+
248
+ /** promise vs 超时:超时先到返回 'timeout'(promise 继续但被放弃等待)。unref 见 KillChainOptions。 */
249
+ function raceTimeout(p: Promise<void>, ms: number, unref = false): Promise<"settled" | "timeout"> {
250
+ return new Promise<"settled" | "timeout">((resolve) => {
251
+ const timer = setTimeout(() => resolve("timeout"), ms);
252
+ if (unref) timer.unref();
253
+ p.then(
254
+ () => {
255
+ clearTimeout(timer);
256
+ resolve("settled");
257
+ },
258
+ () => {
259
+ // 被 await 的 exit promise 不会 reject;防御分支保持语义完整(超时口径胜出前出错按 settled 处理)
260
+ clearTimeout(timer);
261
+ resolve("settled");
262
+ },
263
+ );
264
+ });
265
+ }