@zhushanwen/subagent-engine-sdk 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/{chunk-7I4XGL5J.js → chunk-BBWUC2EE.js} +2 -2
  2. package/dist/{chunk-3K2P2CM2.js → chunk-CNQTJJF5.js} +1 -1
  3. package/dist/{chunk-DSQ7JQKM.js → chunk-GSW3DMN6.js} +5 -5
  4. package/dist/{chunk-BBVZK5DL.js → chunk-M3SZTDNQ.js} +20 -20
  5. package/dist/{chunk-HYES77BR.js → chunk-MSAJXIP4.js} +2 -2
  6. package/dist/{chunk-FJT23P7T.js → chunk-QOEXD2FT.js} +1 -1
  7. package/dist/{chunk-LKPVN3N6.js → chunk-W3BKI2BP.js} +3 -3
  8. package/dist/{chunk-WS4WAZVR.js → chunk-WMDMXP2F.js} +2 -2
  9. package/dist/{chunk-GT6YLLN4.js → chunk-X24IVQKJ.js} +5 -5
  10. package/dist/{chunk-365AUV6N.js → chunk-YE472JOO.js} +2 -2
  11. package/dist/{contract-types-C71Sqq8d.d.cts → contract-types-Dsvy79r3.d.cts} +9 -10
  12. package/dist/{contract-types-C71Sqq8d.d.ts → contract-types-Dsvy79r3.d.ts} +9 -10
  13. package/dist/data-dir.cjs +6 -6
  14. package/dist/data-dir.d.cts +4 -4
  15. package/dist/data-dir.d.ts +4 -4
  16. package/dist/data-dir.js +3 -3
  17. package/dist/env.cjs +20 -20
  18. package/dist/env.d.cts +12 -5
  19. package/dist/env.d.ts +12 -5
  20. package/dist/env.js +1 -1
  21. package/dist/{error-codes-D5YlWaBx.d.ts → error-codes-DMGeUUUm.d.ts} +12 -12
  22. package/dist/{error-codes-CiOO3PMg.d.cts → error-codes-pT7RYTqQ.d.cts} +12 -12
  23. package/dist/index.cjs +42 -42
  24. package/dist/index.d.cts +3 -3
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.js +12 -12
  27. package/dist/journal-io.d.cts +1 -1
  28. package/dist/journal-io.d.ts +1 -1
  29. package/dist/journal-replay.d.cts +1 -1
  30. package/dist/journal-replay.d.ts +1 -1
  31. package/dist/kill-chain.d.cts +2 -2
  32. package/dist/kill-chain.d.ts +2 -2
  33. package/dist/kill-chain.js +2 -2
  34. package/dist/logs/stderr-rotation.cjs +2 -2
  35. package/dist/logs/stderr-rotation.d.cts +1 -1
  36. package/dist/logs/stderr-rotation.d.ts +1 -1
  37. package/dist/logs/stderr-rotation.js +1 -1
  38. package/dist/nesting-guard.cjs +2 -2
  39. package/dist/nesting-guard.d.cts +2 -2
  40. package/dist/nesting-guard.d.ts +2 -2
  41. package/dist/nesting-guard.js +1 -1
  42. package/dist/node-executor.cjs +2 -2
  43. package/dist/node-executor.d.cts +5 -5
  44. package/dist/node-executor.d.ts +5 -5
  45. package/dist/node-executor.js +2 -2
  46. package/dist/port-contract.d.cts +1 -1
  47. package/dist/port-contract.d.ts +1 -1
  48. package/dist/protocol/index.cjs +2 -2
  49. package/dist/protocol/index.d.cts +5 -5
  50. package/dist/protocol/index.d.ts +5 -5
  51. package/dist/protocol/index.js +1 -1
  52. package/dist/relay-env.cjs +5 -5
  53. package/dist/relay-env.d.cts +5 -5
  54. package/dist/relay-env.d.ts +5 -5
  55. package/dist/relay-env.js +1 -1
  56. package/dist/spawn.cjs +1 -1
  57. package/dist/spawn.d.cts +2 -2
  58. package/dist/spawn.d.ts +2 -2
  59. package/dist/spawn.js +1 -1
  60. package/dist/ui-channels.cjs +2 -2
  61. package/dist/ui-channels.js +1 -1
  62. package/package.json +2 -4
  63. package/src/best-effort.ts +0 -37
  64. package/src/cli-entry.ts +0 -82
  65. package/src/data-dir.ts +0 -89
  66. package/src/env.ts +0 -265
  67. package/src/error-message.ts +0 -22
  68. package/src/index.ts +0 -63
  69. package/src/journal-io.ts +0 -82
  70. package/src/journal-replay.ts +0 -436
  71. package/src/kill-chain.ts +0 -265
  72. package/src/logger.ts +0 -105
  73. package/src/logs/stderr-rotation.ts +0 -166
  74. package/src/nesting-guard.ts +0 -140
  75. package/src/node-executor.ts +0 -272
  76. package/src/paths.ts +0 -48
  77. package/src/port-contract.ts +0 -150
  78. package/src/protocol/contract-types.ts +0 -379
  79. package/src/protocol/engine-protocol.ts +0 -91
  80. package/src/protocol/error-codes.ts +0 -180
  81. package/src/protocol/frames.ts +0 -143
  82. package/src/protocol/index.ts +0 -12
  83. package/src/protocol/methods.ts +0 -237
  84. package/src/protocol/reverse-channels.ts +0 -123
  85. package/src/protocol/schema.ts +0 -185
  86. package/src/relay-env.ts +0 -60
  87. package/src/schema-emulation.ts +0 -192
  88. package/src/spawn.ts +0 -246
  89. package/src/ui-channels.ts +0 -219
  90. package/src/ui-types.ts +0 -101
package/src/kill-chain.ts DELETED
@@ -1,265 +0,0 @@
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
- }
package/src/logger.ts DELETED
@@ -1,105 +0,0 @@
1
- // src/logger.ts
2
- //
3
- // 日志 facade(引擎侧原语,自 core src/core/logger.ts 迁入 @zhushanwen/subagent-engine-sdk)。
4
- // 迁移处置(impl-plan §2.1:「日志 facade」直接搬;kill-chain 行「logger 走 SDK facade」):
5
- // core 版经 getHostServices()(configureCore 时序契约)动态解析宿主实现;SDK 进程
6
- // (引擎 CLI)无 configureCore 通道,同款「调用时动态解析」语义改为经可注入
7
- // LoggerSink——引擎 CLI 启动早期 configureLoggerSink(sink) 注入宿主形态 sink,
8
- // 未注入时落缺省 console 出口(对齐 core NULL_HOST.log 语义:warn/error 走 console、
9
- // debug no-op)。facade 代理而非绑死实现的时序契约(模块顶层 getLogger 先于配置、
10
- // 配置后透明切换)与 core 版逐字一致。
11
- //
12
- // CJS 多 entry 内联副本的实例分裂影响 = facadeCache 分裂(同 component 引用不同);
13
- // facade 无状态、sink 解析每次调用发生,无语义后果——与 core 版同结论。
14
-
15
- /**
16
- * SDK 侧 logger sink(引擎 CLI 注入宿主形态日志出口;core 版对应 HostServices.log)。
17
- */
18
- export interface LoggerSink {
19
- log(level: LogLevel, component: string, message: string, data?: unknown): void;
20
- }
21
-
22
- /** 日志级别。对齐 core logger 的 LogLevel(三值,无 info)。 */
23
- export type LogLevel = "debug" | "warn" | "error";
24
-
25
- /** logger 接口。与 core CoreLogger 结构兼容——迁移调用面(方法名/参数序)逐文件等价。 */
26
- export interface CoreLogger {
27
- debug(msg: string, data?: unknown): void;
28
- warn(msg: string, data?: unknown): void;
29
- error(msg: string, data?: unknown): void;
30
- }
31
-
32
- // sink 配置态:globalThis[Symbol.for] slot(core host-services 同款范式)——模块级
33
- // `let` 在 dist 双形态 / CJS 多 entry 内联副本下会被分裂,slot 形态跨副本一致。
34
- const SINK_SLOT_KEY = Symbol.for("@zhushanwen/subagent-engine-sdk.logger-sink");
35
-
36
- type SinkSlot = { current: LoggerSink | undefined };
37
-
38
- function getSinkSlot(): SinkSlot {
39
- let slot = Reflect.get(globalThis, SINK_SLOT_KEY) as SinkSlot | undefined;
40
- if (!slot) {
41
- slot = { current: undefined };
42
- Reflect.set(globalThis, SINK_SLOT_KEY, slot);
43
- }
44
- return slot;
45
- }
46
-
47
- /** 缺省 console 出口(未注入 sink 时的日志不丢)。格式对齐 core NULL_HOST.log。 */
48
- const CONSOLE_SINK: LoggerSink = {
49
- log(level, component, message, data) {
50
- const line = `[${component}] ${message}`;
51
- // data 作第二参数;缺省时必须省略——node console 会把显式 undefined 格式化成
52
- // " undefined" 尾巴污染每行输出。
53
- if (level === "error") {
54
- if (data === undefined) console.error(line);
55
- else console.error(line, data);
56
- return;
57
- }
58
- if (level === "warn") {
59
- if (data === undefined) console.warn(line);
60
- else console.warn(line, data);
61
- return;
62
- }
63
- // debug 缺省 no-op:对齐 core NULL_HOST 语义(未配置期多为模块加载窗口,刷屏无
64
- // 诊断价值);warn/error 不可静默。
65
- },
66
- };
67
-
68
- function currentSink(): LoggerSink {
69
- return getSinkSlot().current ?? CONSOLE_SINK;
70
- }
71
-
72
- /**
73
- * 注入日志 sink(引擎 CLI 启动早期调用;重复调用后者覆盖——测试切 sink 依赖此语义)。
74
- * 解析发生在每次 logger 方法调用时,模块顶层已缓存的 logger 透明切换,无加载顺序依赖。
75
- */
76
- export function configureLoggerSink(sink: LoggerSink): void {
77
- getSinkSlot().current = sink;
78
- }
79
-
80
- /** 测试隔离专用:清空 sink 配置态(生产禁用)。 */
81
- export function resetLoggerSinkForTests(): void {
82
- getSinkSlot().current = undefined;
83
- }
84
-
85
- // 与 core getLogger singleton 惯例对齐(同 component 同引用)。
86
- // facade 自身无状态(解析发生在方法内),缓存只为引用稳定,不影响透明切换。
87
- const facadeCache = new Map<string, CoreLogger>();
88
-
89
- export function getLogger(component: string): CoreLogger {
90
- const existing = facadeCache.get(component);
91
- if (existing) return existing;
92
- const facade: CoreLogger = {
93
- debug(msg, data) {
94
- currentSink().log("debug", component, msg, data);
95
- },
96
- warn(msg, data) {
97
- currentSink().log("warn", component, msg, data);
98
- },
99
- error(msg, data) {
100
- currentSink().log("error", component, msg, data);
101
- },
102
- };
103
- facadeCache.set(component, facade);
104
- return facade;
105
- }
@@ -1,166 +0,0 @@
1
- // src/logs/stderr-rotation.ts
2
- //
3
- // 引擎侧 stderr tee 的实例维度文件名 + 轮转 + 过期清理(单源实现,前缀参数化——
4
- // 两引擎包(pi-subagent-cli / zcode-subagent-cli)各自的 logs/stderr-rotation.ts
5
- // 是本模块的薄包装,只绑定各自前缀常量)。
6
- //
7
- // 清理三判据(三者同时成立才删,缺一不删):
8
- // 1. 同前缀(调用方绑定);2. pid 已死(process.kill(pid,0) 跨实例探测,存活实例
9
- // 的文件一律不删);3. mtime 过期(XYZ_LOG_KEEP_DAYS,缺省 7 天)。
10
- // 轮转参数读宿主同款 env:XYZ_LOG_MAX_BYTES(缺省 50MB)/ XYZ_LOG_KEEP_DAYS。
11
-
12
- import * as fs from "node:fs";
13
- import { dirname, join } from "node:path";
14
-
15
- /** 缺省轮转阈值(与宿主 logger 同源缺省:50MB)。 */
16
- const BYTES_PER_KB = 1024;
17
- const DEFAULT_MAX_FILE_MB = 50;
18
- const MS_PER_SECOND = 1000;
19
- const SECONDS_PER_MINUTE = 60;
20
- const MINUTES_PER_HOUR = 60;
21
- const HOURS_PER_DAY = 24;
22
-
23
- export const DEFAULT_STDERR_MAX_BYTES = DEFAULT_MAX_FILE_MB * BYTES_PER_KB * BYTES_PER_KB;
24
-
25
- const DAY_MS = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
26
-
27
- /** 缺省保留天数(与宿主 logger 同源缺省:7 天)。 */
28
- export const DEFAULT_STDERR_KEEP_DAYS = 7;
29
-
30
- /** 轮转参数(env 覆盖面 = 宿主 logger 同款 XYZ_LOG_* 两键)。 */
31
- export interface StderrRotationParams {
32
- maxBytes: number;
33
- keepDays: number;
34
- }
35
-
36
- function positiveIntEnv(env: NodeJS.ProcessEnv, key: string): number | undefined {
37
- const raw = env[key];
38
- if (raw === undefined || raw.trim() === "") return undefined;
39
- const n = Number(raw);
40
- return Number.isFinite(n) && n > 0 ? n : undefined;
41
- }
42
-
43
- /** 读 env 解析轮转参数(非法值回缺省——取证面配置不拖垮主通道)。 */
44
- export function stderrRotationParams(env: NodeJS.ProcessEnv): StderrRotationParams {
45
- return {
46
- maxBytes:
47
- positiveIntEnv(env, "XYZ_LOG_MAX_BYTES") ?? DEFAULT_STDERR_MAX_BYTES,
48
- keepDays:
49
- positiveIntEnv(env, "XYZ_LOG_KEEP_DAYS") ?? DEFAULT_STDERR_KEEP_DAYS,
50
- };
51
- }
52
-
53
- /** 实例维度 stderr tee 路径:<engineDataDir>/logs/<prefix><pid>.log。 */
54
- export function stderrLogPathFor(engineDataDir: string, pid: number, prefix: string): string {
55
- return join(engineDataDir, "logs", `${prefix}${pid}.log`);
56
- }
57
-
58
- /** 从文件名解析实例 pid(非本前缀形态返回 undefined——调用方跳过)。 */
59
- export function stderrLogPidOf(fileName: string, prefix: string): number | undefined {
60
- if (!fileName.startsWith(prefix)) return undefined;
61
- const rest = fileName.slice(prefix.length);
62
- // 兼容轮转副本形态 <pid>.log.<timestamp>
63
- const pidToken = rest.split(".", 1)[0] ?? "";
64
- if (!/^\d+$/.test(pidToken)) return undefined;
65
- return Number(pidToken);
66
- }
67
-
68
- /** pid 存活探测(跨实例:ESRCH = 死;EPERM = 存活但非属主,按存活保守处理)。 */
69
- export function isPidAlive(pid: number): boolean {
70
- try {
71
- process.kill(pid, 0);
72
- return true;
73
- } catch (err) {
74
- return (err as NodeJS.ErrnoException).code === "EPERM";
75
- }
76
- }
77
-
78
- /**
79
- * 尺寸轮转:当前文件超过 maxBytes 时 rename 为 `<path>.<时间戳>` 副本并返回
80
- * true(调用方懒重开新文件继续 append)。rename 失败返回 false(继续 append 原
81
- * 文件——轮转是取证面优化,不是主通道)。
82
- */
83
- export function rotateStderrLogIfNeeded(
84
- path: string,
85
- params: StderrRotationParams,
86
- ): boolean {
87
- let size: number;
88
- try {
89
- size = fs.statSync(path).size;
90
- } catch {
91
- return false;
92
- }
93
- if (size <= params.maxBytes) return false;
94
- const rotated = `${path}.${Date.now()}`;
95
- try {
96
- fs.renameSync(path, rotated);
97
- return true;
98
- } catch {
99
- return false;
100
- }
101
- }
102
-
103
- export interface CleanupResult {
104
- /** 已删除文件数。 */
105
- deleted: number;
106
- /** 命中前缀但跳过(pid 存活 / mtime 未过期 / 探测不确定)的文件数。 */
107
- skipped: number;
108
- }
109
-
110
- /**
111
- * 过期清理:扫描 logsDir 下同前缀文件,三判据同时成立才删。目录缺失/读失败返回
112
- * 零删除(取证面 best-effort)。
113
- */
114
- export function cleanupStaleStderrLogs(
115
- logsDir: string,
116
- env: NodeJS.ProcessEnv,
117
- prefix: string,
118
- now: number = Date.now(),
119
- ): CleanupResult {
120
- const params = stderrRotationParams(env);
121
- const expiryMs = params.keepDays * DAY_MS;
122
- let deleted = 0;
123
- let skipped = 0;
124
- let names: string[];
125
- try {
126
- names = fs.readdirSync(logsDir);
127
- } catch {
128
- return { deleted, skipped };
129
- }
130
- for (const name of names) {
131
- const pid = stderrLogPidOf(name, prefix);
132
- if (pid === undefined) continue;
133
- // 判据 2:pid 存活 → 该实例的文件(含轮转副本)一律不删
134
- if (isPidAlive(pid)) {
135
- skipped += 1;
136
- continue;
137
- }
138
- // 判据 3:mtime 未过期 → 保留
139
- const full = join(logsDir, name);
140
- try {
141
- if (fs.statSync(full).mtimeMs > now - expiryMs) {
142
- skipped += 1;
143
- continue;
144
- }
145
- } catch {
146
- continue;
147
- }
148
- try {
149
- fs.rmSync(full, { force: true });
150
- deleted += 1;
151
- } catch {
152
- skipped += 1;
153
- }
154
- }
155
- return { deleted, skipped };
156
- }
157
-
158
- /** 便捷入口:对 tee 路径所在 logs 目录做过期清理(流打开时机调用,每代一次)。 */
159
- export function cleanupSiblingStderrLogs(
160
- logPath: string,
161
- env: NodeJS.ProcessEnv,
162
- prefix: string,
163
- now?: number,
164
- ): CleanupResult {
165
- return cleanupStaleStderrLogs(dirname(logPath), env, prefix, now);
166
- }
@@ -1,140 +0,0 @@
1
- // src/nesting-guard.ts
2
- //
3
- // 嵌套防护(引擎侧原语,自 core execution/engine/common/nesting-guard.ts 迁入
4
- // @zhushanwen/subagent-engine-sdk,实现体逐字等价)。迁移处置:无 core 内部依赖 →
5
- // 直接搬(impl-plan §2.1 原语迁移处置表);唯二差异:
6
- // 1. nestedSpawnRejectedError 自 core common/errors.ts 内联进本模块(errors.ts 不在
7
- // 迁移清单,本模块只消费它这一个构造器;文案与恢复指引逐字保留);
8
- // 2. 头部注释的模块路径改为 SDK 落点。
9
- //
10
- // 设计权威源:docs/architecture/subagent-engine-abstraction.md D8(嵌套防护双层):
11
- // 统一 XYZ_AGENT_SUBAGENT=1 标记(所有引擎 spawn 都注入,引擎 adapter 检测到即拒绝
12
- // 递归派发)+ 剥离各引擎原生标记(CC 的 CLAUDECODE / zsub 的 ZSW_NESTED / pi 的
13
- // PI_SUBAGENT_*)防继承泄漏——子代理环境的旧标记会让孙代理误判自己已在嵌套层。
14
- //
15
- // 为什么 env 标记是唯一跨引擎可靠手段(被否方案见设计):「隔离目录里不装扩展」
16
- // 依赖配置洁癖,且 opencode/CC 会吃项目级配置;env 由宿主显式控制,随 spawn 必达。
17
- //
18
- // [D3-⑤ 嵌套防护合一] 进程内执行嵌套上下文(原 SubagentService.execCtxAls,pi 路径
19
- // 私有)并入本文件——「嵌套防护」的两层机制(跨进程 env 标记 / 进程内 ALS 深度计数)
20
- // 单点于公共层。设计权威源:docs/design/dual-track(已删 git 可追溯)§3.3 D3-⑤
21
- // + 双轨清单 #10。
22
-
23
- import { AsyncLocalStorage } from "node:async_hooks";
24
-
25
- /** 统一嵌套标记 env 名(D8)。值恒 '1'。 */
26
- export const NESTED_SPAWN_ENV = "XYZ_AGENT_SUBAGENT";
27
-
28
- /** 需剥离的引擎原生嵌套标记(精确名)。 */
29
- const NATIVE_NESTED_KEYS: readonly string[] = ["CLAUDECODE", "ZSW_NESTED"];
30
-
31
- /** 需剥离的引擎原生嵌套标记前缀(pi 家族)。 */
32
- const NATIVE_NESTED_PREFIXES: readonly string[] = ["PI_SUBAGENT_"];
33
-
34
- /** env 对象形状(NodeJS.ProcessEnv 的结构子集,测试可传普通对象)。 */
35
- export type SpawnEnv = Record<string, string | undefined>;
36
-
37
- /**
38
- * nested_spawn_rejected 的结构化错误(自 core errors.ts 内联;code/recovery 与 core
39
- * 词表逐字一致,EngineError 类形态对齐 SDK protocol/error-codes.ts 的 EngineSdkError)。
40
- */
41
- export class NestedSpawnRejectedError extends Error {
42
- readonly code = "nested_spawn_rejected";
43
- /** 恢复指引:指向具体下一步,非安慰性文案。 */
44
- readonly recovery: string;
45
-
46
- constructor() {
47
- super(
48
- "nested_spawn_rejected: this process is already a subagent (XYZ_AGENT_SUBAGENT=1)",
49
- );
50
- this.name = "NestedSpawnRejectedError";
51
- this.recovery =
52
- "Subagents must not spawn further subagents (unbounded recursion guard). " +
53
- "Do the work directly inside the current task instead of delegating.";
54
- }
55
- }
56
-
57
- /**
58
- * 构造子代理 spawn env:注入 XYZ_AGENT_SUBAGENT=1 + 剥离引擎原生嵌套标记。
59
- * 返回新对象,不改入参(spawn env 组装链中的多层 spread 安全)。
60
- */
61
- export function buildNestedSpawnEnv(baseEnv: SpawnEnv): SpawnEnv {
62
- const env: SpawnEnv = {};
63
- for (const [key, value] of Object.entries(baseEnv)) {
64
- if (NATIVE_NESTED_KEYS.includes(key)) continue;
65
- if (NATIVE_NESTED_PREFIXES.some((prefix) => key.startsWith(prefix))) continue;
66
- env[key] = value;
67
- }
68
- env[NESTED_SPAWN_ENV] = "1";
69
- return env;
70
- }
71
-
72
- /**
73
- * 嵌套 spawn 防护断言:检测到统一标记(本进程已是 subagent)抛
74
- * nested_spawn_rejected——文案说明防护规则、指向 task 内自行完成。
75
- * 调用点:subagent 工具入口(进程创建前拒绝,D11 处置三级)。
76
- */
77
- export function assertNotNestedSpawn(env: SpawnEnv): void {
78
- if (env[NESTED_SPAWN_ENV] === "1") {
79
- throw new NestedSpawnRejectedError();
80
- }
81
- }
82
-
83
- // ============================================================
84
- // [D3-⑤] 进程内执行嵌套上下文(原 SubagentService.execCtxAls 下沉)
85
- // ============================================================
86
-
87
- /** 执行嵌套状态:当前正在跑的 record 身份 + 递归深度(D-033 通用嵌套深度护栏的计数载体)。 */
88
- export interface ExecutionNestingState {
89
- recordId: string | undefined;
90
- depth: number;
91
- }
92
-
93
- /**
94
- * 进程内执行嵌套上下文([D3-⑤] 从 SubagentService 的 execCtxAls 私有字段并入公共层)。
95
- *
96
- * 机制(原样迁移,行为零变化):
97
- * - ALS 按异步调用链传递当前 record 身份:B run() 期间包 this 上下文,B 内创建 C 时
98
- * 读到 B → C.parentRecordId=B.id、C.depth=B.depth+1;主 session 链上无 store → 顶层。
99
- * - 进程级基线兜底 [ALS 断裂修复]:pi RPC mode 的 stdin JSONL 是事件回调式
100
- * (attachJsonlLineReader stream.on("data")),每个命令是独立异步链,enterWith 的
101
- * store 不会贯穿到后续 tool 调用事件(实测:递归第二层 parentRecordId/depth 丢失
102
- * 而 rootSessionId 正确)。基线 = 本进程自己的身份(initSession 从 env 读取):
103
- * 读 ALS store 失败时兜底,保证「本进程派发的 subagent 都是本进程记录的孩子」。
104
- *
105
- * 实例归属:per-Service(基线随宿主进程身份而异),Service 构造时创建并持有。
106
- * 深度上限判据(MAX_FORK_DEPTH)留在调用方——上限常量属 execution 层
107
- * (session-context-resolver),公共层只提供状态存取单点。
108
- */
109
- export class ExecutionNestingContext {
110
- private readonly als = new AsyncLocalStorage<ExecutionNestingState>();
111
- private baselineState: ExecutionNestingState | null = null;
112
-
113
- /** 建立进程级基线(initSession:有 env 自我标记 → env 身份;根进程 → null 顶层)。 */
114
- setBaseline(state: ExecutionNestingState | null): void {
115
- this.baselineState = state;
116
- }
117
-
118
- /** 读当前嵌套状态:ALS store 优先,断裂时基线兜底(顶层 = null)。 */
119
- current(): ExecutionNestingState | null {
120
- return this.als.getStore() ?? this.baselineState;
121
- }
122
-
123
- /**
124
- * 读进程级基线(与 current() 的差异:不看 ALS store——直接父归属校验等场景要的是
125
- * 「本进程自己的身份」而非「当前异步链正在跑的 record 身份」)。
126
- */
127
- baseline(): ExecutionNestingState | null {
128
- return this.baselineState;
129
- }
130
-
131
- /** 包裹执行(B run() 期间挂 B 身份——内层创建 C 时 current() 读到 B)。 */
132
- run<T>(state: ExecutionNestingState, fn: () => T): T {
133
- return this.als.run(state, fn);
134
- }
135
-
136
- /** 顶层 enterWith(initSession 建立基线身份后挂入当前异步链)。 */
137
- enterWith(state: ExecutionNestingState): void {
138
- this.als.enterWith(state);
139
- }
140
- }