@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.
- package/dist/best-effort.cjs +88 -0
- package/dist/best-effort.d.cts +14 -0
- package/dist/best-effort.d.ts +14 -0
- package/dist/best-effort.js +8 -0
- package/dist/chunk-2DIMPZCQ.js +0 -0
- package/dist/chunk-365AUV6N.js +67 -0
- package/dist/chunk-3K2P2CM2.js +127 -0
- package/dist/chunk-75QEMUGV.js +58 -0
- package/dist/chunk-7I4XGL5J.js +95 -0
- package/dist/chunk-A75XDJIC.js +227 -0
- package/dist/chunk-DSQ7JQKM.js +39 -0
- package/dist/chunk-EJMF63R5.js +19 -0
- package/dist/chunk-GT6YLLN4.js +46 -0
- package/dist/chunk-HYES77BR.js +109 -0
- package/dist/chunk-JSBRDJBE.js +30 -0
- package/dist/chunk-LEOBWKRM.js +128 -0
- package/dist/chunk-N3RL6OVM.js +38 -0
- package/dist/chunk-OPMY4G4M.js +27 -0
- package/dist/chunk-PPEPBVCC.js +120 -0
- package/dist/chunk-PYO3YR7W.js +16 -0
- package/dist/chunk-RDH3ZOV6.js +46 -0
- package/dist/chunk-RULLX6C6.js +11 -0
- package/dist/chunk-X24SFZYW.js +6646 -0
- package/dist/chunk-YFSN3D5N.js +216 -0
- package/dist/chunk-ZOFFJNJD.js +136 -0
- package/dist/chunk-ZXEAW25V.js +132 -0
- package/dist/cli-entry.cjs +119 -0
- package/dist/cli-entry.d.cts +24 -0
- package/dist/cli-entry.d.ts +24 -0
- package/dist/cli-entry.js +8 -0
- package/dist/contract-types-sSlgppBC.d.cts +352 -0
- package/dist/contract-types-sSlgppBC.d.ts +352 -0
- package/dist/data-dir.cjs +117 -0
- package/dist/data-dir.d.cts +20 -0
- package/dist/data-dir.d.ts +20 -0
- package/dist/data-dir.js +12 -0
- package/dist/env.cjs +205 -0
- package/dist/env.d.cts +73 -0
- package/dist/env.d.ts +73 -0
- package/dist/env.js +16 -0
- package/dist/error-codes-DHco5-i_.d.cts +118 -0
- package/dist/error-codes-Dhss2Kmk.d.ts +118 -0
- package/dist/error-message.cjs +40 -0
- package/dist/error-message.d.cts +3 -0
- package/dist/error-message.d.ts +3 -0
- package/dist/error-message.js +7 -0
- package/dist/index.cjs +8375 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +268 -0
- package/dist/journal-io.cjs +70 -0
- package/dist/journal-io.d.cts +12 -0
- package/dist/journal-io.d.ts +12 -0
- package/dist/journal-io.js +7 -0
- package/dist/journal-replay.cjs +256 -0
- package/dist/journal-replay.d.cts +45 -0
- package/dist/journal-replay.d.ts +45 -0
- package/dist/journal-replay.js +17 -0
- package/dist/kill-chain.cjs +221 -0
- package/dist/kill-chain.d.cts +74 -0
- package/dist/kill-chain.d.ts +74 -0
- package/dist/kill-chain.js +23 -0
- package/dist/logger.cjs +84 -0
- package/dist/logger.d.cts +24 -0
- package/dist/logger.d.ts +24 -0
- package/dist/logger.js +11 -0
- package/dist/logs/stderr-rotation.cjs +151 -0
- package/dist/logs/stderr-rotation.d.cts +37 -0
- package/dist/logs/stderr-rotation.d.ts +37 -0
- package/dist/logs/stderr-rotation.js +23 -0
- package/dist/nesting-guard.cjs +95 -0
- package/dist/nesting-guard.d.cts +65 -0
- package/dist/nesting-guard.d.ts +65 -0
- package/dist/nesting-guard.js +15 -0
- package/dist/node-executor.cjs +180 -0
- package/dist/node-executor.d.cts +63 -0
- package/dist/node-executor.d.ts +63 -0
- package/dist/node-executor.js +16 -0
- package/dist/paths.cjs +55 -0
- package/dist/paths.d.cts +8 -0
- package/dist/paths.d.ts +8 -0
- package/dist/paths.js +15 -0
- package/dist/port-contract.cjs +35 -0
- package/dist/port-contract.d.cts +88 -0
- package/dist/port-contract.d.ts +88 -0
- package/dist/port-contract.js +7 -0
- package/dist/protocol/index.cjs +400 -0
- package/dist/protocol/index.d.cts +685 -0
- package/dist/protocol/index.d.ts +685 -0
- package/dist/protocol/index.js +89 -0
- package/dist/relay-env.cjs +71 -0
- package/dist/relay-env.d.cts +37 -0
- package/dist/relay-env.d.ts +37 -0
- package/dist/relay-env.js +23 -0
- package/dist/schema-emulation.cjs +6680 -0
- package/dist/schema-emulation.d.cts +39 -0
- package/dist/schema-emulation.d.ts +39 -0
- package/dist/schema-emulation.js +11 -0
- package/dist/spawn.cjs +200 -0
- package/dist/spawn.d.cts +80 -0
- package/dist/spawn.d.ts +80 -0
- package/dist/spawn.js +16 -0
- package/dist/ui-channels.cjs +120 -0
- package/dist/ui-channels.d.cts +60 -0
- package/dist/ui-channels.d.ts +60 -0
- package/dist/ui-channels.js +9 -0
- package/dist/ui-types.cjs +18 -0
- package/dist/ui-types.d.cts +62 -0
- package/dist/ui-types.d.ts +62 -0
- package/dist/ui-types.js +1 -0
- package/package.json +58 -0
- package/src/best-effort.ts +37 -0
- package/src/cli-entry.ts +77 -0
- package/src/data-dir.ts +88 -0
- package/src/env.ts +265 -0
- package/src/error-message.ts +22 -0
- package/src/index.ts +63 -0
- package/src/journal-io.ts +82 -0
- package/src/journal-replay.ts +432 -0
- package/src/kill-chain.ts +265 -0
- package/src/logger.ts +105 -0
- package/src/logs/stderr-rotation.ts +166 -0
- package/src/nesting-guard.ts +140 -0
- package/src/node-executor.ts +272 -0
- package/src/paths.ts +48 -0
- package/src/port-contract.ts +117 -0
- package/src/protocol/contract-types.ts +378 -0
- package/src/protocol/engine-protocol.ts +81 -0
- package/src/protocol/error-codes.ts +179 -0
- package/src/protocol/frames.ts +145 -0
- package/src/protocol/index.ts +12 -0
- package/src/protocol/methods.ts +229 -0
- package/src/protocol/reverse-channels.ts +274 -0
- package/src/protocol/schema.ts +154 -0
- package/src/relay-env.ts +60 -0
- package/src/schema-emulation.ts +192 -0
- package/src/spawn.ts +246 -0
- package/src/ui-channels.ts +219 -0
- package/src/ui-types.ts +84 -0
package/src/spawn.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// src/spawn.ts
|
|
2
|
+
//
|
|
3
|
+
// 引擎子进程 spawn 原语 + 引擎侧自灭(W12,impl-plan §2.12 / §2.2 R9-4② / §7.2 R9-2)。
|
|
4
|
+
//
|
|
5
|
+
// spawnEngineChild 是全部引擎 CLI 任务子进程的唯一 spawn 入口(W10 静态断言目标符号):
|
|
6
|
+
// - 硬编码 POSIX/Windows detached:false(+ windowsHide:true),不暴露 detached 选项
|
|
7
|
+
// ——引擎一代子进程不脱离进程组,宿主杀组即连带收割;
|
|
8
|
+
// - 不把引擎自身的 stdin fd 传给后代:子进程 stdin 恒为自有 pipe('pipe'),
|
|
9
|
+
// 禁止 'inherit'/fd 形态——否则宿主死后代仍持写端、EOF 永不到达,自灭主判据失效
|
|
10
|
+
// (R9-4②)。
|
|
11
|
+
//
|
|
12
|
+
// armEngineSelfDestruct 是引擎 CLI 的宿主死亡自灭守卫:主判据 stdio EOF,
|
|
13
|
+
// 辅助判据 in-flight 反向请求计时超时(已 ack 的人机交互 / 两阶段长运行请求除外)。
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import type { ChildProcess } from "node:child_process";
|
|
17
|
+
import { getLogger } from "./logger.ts";
|
|
18
|
+
import type { buildEngineChildEnv } from "./env.ts";
|
|
19
|
+
|
|
20
|
+
const logger = getLogger("engine-sdk/spawn");
|
|
21
|
+
|
|
22
|
+
/** spawnEngineChild 的 env 必须来自 buildEngineChildEnv(三层契约终态)。 */
|
|
23
|
+
export type EngineChildEnv = ReturnType<typeof buildEngineChildEnv>;
|
|
24
|
+
|
|
25
|
+
/** 引擎子进程 spawn 选项(刻意不暴露 detached / stdio / stdin——形态硬编码,见文件头)。 */
|
|
26
|
+
export interface SpawnEngineChildOptions {
|
|
27
|
+
command: string;
|
|
28
|
+
args: readonly string[];
|
|
29
|
+
/** 须由 buildEngineChildEnv 构建(类型即契约:三层 env 终态) */
|
|
30
|
+
env: EngineChildEnv;
|
|
31
|
+
cwd?: string;
|
|
32
|
+
/** 子进程 stdout/stderr 形态;缺省 'pipe'(协议帧走 stdout,stderr 走日志/轮转) */
|
|
33
|
+
stdout?: "pipe" | "ignore" | "inherit";
|
|
34
|
+
stderr?: "pipe" | "ignore" | "inherit";
|
|
35
|
+
/** AbortSignal 触发即 SIGTERM 杀子(缺省无) */
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 这些键出现即拒绝:spawn 形态是本原语的契约面,不允许调用方绕过硬编码值。 */
|
|
40
|
+
const FORBIDDEN_OPTION_KEYS = ["detached", "stdio", "stdin", "windowsHide"] as const;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 引擎任务子进程唯一 spawn 入口。
|
|
44
|
+
*
|
|
45
|
+
* 形态硬编码:detached:false、windowsHide:true、子进程 stdin 恒 'pipe'(自有 pipe,
|
|
46
|
+
* 绝不继承引擎自身 stdin fd)。传入被禁止的形态键(detached/stdio/stdin/windowsHide)
|
|
47
|
+
* 直接抛错——契约破坏应在配置期出声,不静默降级。
|
|
48
|
+
*/
|
|
49
|
+
export function spawnEngineChild(opts: SpawnEngineChildOptions): ChildProcess {
|
|
50
|
+
for (const key of FORBIDDEN_OPTION_KEYS) {
|
|
51
|
+
if (key in opts) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`spawnEngineChild: option "${key}" is hardcoded by the engine spawn contract `
|
|
54
|
+
+ `(detached:false / windowsHide:true / child stdin is always its own pipe); `
|
|
55
|
+
+ `remove it from the call site`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const child = spawn(opts.command, [...opts.args], {
|
|
61
|
+
cwd: opts.cwd,
|
|
62
|
+
env: opts.env,
|
|
63
|
+
detached: false,
|
|
64
|
+
windowsHide: true,
|
|
65
|
+
stdio: ["pipe", opts.stdout ?? "pipe", opts.stderr ?? "pipe"],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (opts.signal !== undefined) {
|
|
69
|
+
opts.signal.addEventListener(
|
|
70
|
+
"abort",
|
|
71
|
+
() => {
|
|
72
|
+
child.kill("SIGTERM");
|
|
73
|
+
},
|
|
74
|
+
{ once: true },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return child;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
// 引擎侧自灭(宿主死亡检测)
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/** in-flight 未 ack 反向请求的计时超时缺省值(辅助判据,30s)。 */
|
|
85
|
+
export const DEFAULT_ENGINE_HOST_REQUEST_TIMEOUT_MS = 30_000;
|
|
86
|
+
|
|
87
|
+
/** 计时超时覆盖 env 名(引擎 CLI 启动时读;非法值 warn + 回落缺省)。 */
|
|
88
|
+
export const ENGINE_HOST_REQUEST_TIMEOUT_ENV = "XYZ_ENGINE_HOST_REQUEST_TIMEOUT_MS";
|
|
89
|
+
|
|
90
|
+
/** 计时巡检周期(ms)。仅为辅助判据的采样粒度,不构成行为契约。 */
|
|
91
|
+
const SWEEP_INTERVAL_MS = 1_000;
|
|
92
|
+
|
|
93
|
+
/** 反向请求计时面操作接口(消费方 = 引擎 CLI 的反向通道分发层)。 */
|
|
94
|
+
export interface ReverseRequestClock {
|
|
95
|
+
/** 反向请求已发出(未 ack)——进入计时面。 */
|
|
96
|
+
started(id: string): void;
|
|
97
|
+
/**
|
|
98
|
+
* 收到 ack(两阶段第一阶段:已 ack 的 host/askUser、HostBridge.executeAndAwait
|
|
99
|
+
* 的 ack)——移出计时面但不终结(R9-2:已 ack 的等待不计入 in-flight 超时,
|
|
100
|
+
* ADR-0047 静默 ≠ 卡死)。
|
|
101
|
+
*/
|
|
102
|
+
acked(id: string): void;
|
|
103
|
+
/** 请求终结(应答完成/失败/取消)——彻底移出。 */
|
|
104
|
+
settled(id: string): void;
|
|
105
|
+
/** 卸载守卫(引擎正常 shutdown 路径;自灭后幂等)。 */
|
|
106
|
+
dispose(): void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface EngineSelfDestructOptions {
|
|
110
|
+
/** 主判据数据源:宿主→引擎的协议 stdin(EOF = 宿主死亡 → 立即自灭)。 */
|
|
111
|
+
stdin: NodeJS.ReadableStream;
|
|
112
|
+
/** 辅助判据超时(缺省 30s;显式传值优先于 env XYZ_ENGINE_HOST_REQUEST_TIMEOUT_MS)。 */
|
|
113
|
+
hostRequestTimeoutMs?: number;
|
|
114
|
+
/** 计时超时的 env 覆盖源(缺省读 process.env;测试注入用)。 */
|
|
115
|
+
timeoutEnv?: Record<string, string | undefined>;
|
|
116
|
+
/** 自灭执行器(缺省 = 杀自己进程组再杀自己;测试注入用)。 */
|
|
117
|
+
killSelf?: () => void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 解析计时超时:显式 opt > env > 缺省;env 非法(非正整数)warn + 回落缺省。 */
|
|
121
|
+
function resolveHostRequestTimeoutMs(opts: EngineSelfDestructOptions): number {
|
|
122
|
+
if (opts.hostRequestTimeoutMs !== undefined) return opts.hostRequestTimeoutMs;
|
|
123
|
+
const raw = (opts.timeoutEnv ?? process.env)[ENGINE_HOST_REQUEST_TIMEOUT_ENV];
|
|
124
|
+
if (raw === undefined || raw === "") return DEFAULT_ENGINE_HOST_REQUEST_TIMEOUT_MS;
|
|
125
|
+
const parsed = Number(raw);
|
|
126
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
127
|
+
logger.warn(
|
|
128
|
+
`invalid ${ENGINE_HOST_REQUEST_TIMEOUT_ENV}=${JSON.stringify(raw)}, falling back to `
|
|
129
|
+
+ `${DEFAULT_ENGINE_HOST_REQUEST_TIMEOUT_MS}ms`,
|
|
130
|
+
);
|
|
131
|
+
return DEFAULT_ENGINE_HOST_REQUEST_TIMEOUT_MS;
|
|
132
|
+
}
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 缺省自灭:先杀自己进程组(-pid,收割引擎 spawn 的任务子进程),失败回落杀自己。 */
|
|
137
|
+
function killOwnProcessGroup(): void {
|
|
138
|
+
const pid = process.pid;
|
|
139
|
+
try {
|
|
140
|
+
process.kill(-pid, "SIGKILL");
|
|
141
|
+
} catch (err) {
|
|
142
|
+
// 进程组杀失败(非组长 / Windows 形态差异)——回落单进程杀,任务子进程由
|
|
143
|
+
// 各自的 stdio EOF / 宿主回收层兜底。
|
|
144
|
+
logger.warn(`process-group kill failed (${String(err)}), falling back to self kill`);
|
|
145
|
+
process.kill(pid, "SIGKILL");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 武装引擎侧自灭守卫。返回 ReverseRequestClock 给反向通道分发层登记请求生命周期。
|
|
151
|
+
*
|
|
152
|
+
* 判据:
|
|
153
|
+
* - 主判据 stdio EOF:stdin 'end'(数据面读尽)或 'close'(fd 关闭)→ 立即自灭
|
|
154
|
+
* (宿主死后无人再写协议帧,引擎继续跑只烧 token);
|
|
155
|
+
* - 辅助判据 in-flight 反向请求计时:未 ack 的反向请求超过 hostRequestTimeoutMs
|
|
156
|
+
* 仍无 ack → 宿主大概率已死(EOF 未达或正在关闭)→ 自灭。**排除面**:已 ack 的
|
|
157
|
+
* host/askUser 与两阶段 ack 的长运行 HostBridge.executeAndAwait(ack 后等待任意
|
|
158
|
+
* 时长合法,漏此项即「>30s pi 任务被自灭」turnTimeoutMs 同型事故)。
|
|
159
|
+
*
|
|
160
|
+
* 自灭执行 = 引擎自杀并杀自己进程组(spawnEngineChild 硬编码 detached:false,
|
|
161
|
+
* 任务子进程同组连带收割)。
|
|
162
|
+
*/
|
|
163
|
+
export function armEngineSelfDestruct(opts: EngineSelfDestructOptions): ReverseRequestClock {
|
|
164
|
+
const timeoutMs = resolveHostRequestTimeoutMs(opts);
|
|
165
|
+
const killSelf = opts.killSelf ?? killOwnProcessGroup;
|
|
166
|
+
|
|
167
|
+
// pending = 已发出未 ack(计时面);settled/acked 请求不在面内。
|
|
168
|
+
const pending = new Map<string, number>();
|
|
169
|
+
let disposed = false;
|
|
170
|
+
|
|
171
|
+
const destroy = (reason: string): void => {
|
|
172
|
+
if (disposed) return;
|
|
173
|
+
disposed = true;
|
|
174
|
+
clearInterval(timer);
|
|
175
|
+
opts.stdin.removeAllListeners?.("end");
|
|
176
|
+
opts.stdin.removeAllListeners?.("close");
|
|
177
|
+
logger.warn(`engine self-destruct: ${reason}`);
|
|
178
|
+
killSelf();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// 主判据:stdio EOF('end' 与 'close' 双挂——pipe 半关闭与 fd 销毁都算宿主死亡)
|
|
182
|
+
opts.stdin.once("end", () => destroy("host stdin EOF (end)"));
|
|
183
|
+
opts.stdin.once("close", () => destroy("host stdin closed"));
|
|
184
|
+
|
|
185
|
+
// 辅助判据:未 ack 反向请求计时巡检(timer 声明于 destroy 之后——destroy 仅经
|
|
186
|
+
// 事件/回调异步触发,调用时已初始化)
|
|
187
|
+
const timer = setInterval(() => {
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
for (const [id, startedAt] of pending) {
|
|
190
|
+
if (now - startedAt > timeoutMs) {
|
|
191
|
+
destroy(`unacked reverse request ${id} exceeded ${timeoutMs}ms without host ack`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}, SWEEP_INTERVAL_MS);
|
|
196
|
+
timer.unref?.();
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
started(id) {
|
|
200
|
+
if (!disposed) pending.set(id, Date.now());
|
|
201
|
+
},
|
|
202
|
+
acked(id) {
|
|
203
|
+
pending.delete(id);
|
|
204
|
+
},
|
|
205
|
+
settled(id) {
|
|
206
|
+
pending.delete(id);
|
|
207
|
+
},
|
|
208
|
+
dispose() {
|
|
209
|
+
if (disposed) return;
|
|
210
|
+
disposed = true;
|
|
211
|
+
clearInterval(timer);
|
|
212
|
+
opts.stdin.removeAllListeners?.("end");
|
|
213
|
+
opts.stdin.removeAllListeners?.("close");
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
219
|
+
// NDJSON 行泵(子进程 stdout 拆行)
|
|
220
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 流式 NDJSON 行泵:utf8 解码 + 缓冲累积 + "\n" 拆行循环(S4 簇 5b 收编:pi
|
|
224
|
+
* spawn-run-pump 与 zcode connection 的逐字同构段单源化,实现以 pi 版为基线)。
|
|
225
|
+
* 末行无换行符时滞留缓冲(NDJSON 语义:行以 \n 定界);不监听 end/close/error——
|
|
226
|
+
* 生命周期接线(close 收尾/error 兜底)由调用方自行挂接。stream 为 null/undefined
|
|
227
|
+
* (无 stdio)时无操作,与原调用点 `child.stdout?.` 可选链语义等价。
|
|
228
|
+
*/
|
|
229
|
+
export function pumpNdjsonLines(
|
|
230
|
+
stream: NodeJS.ReadableStream | null | undefined,
|
|
231
|
+
onLine: (line: string) => void,
|
|
232
|
+
): void {
|
|
233
|
+
if (stream === null || stream === undefined) return;
|
|
234
|
+
stream.setEncoding("utf8");
|
|
235
|
+
let buffer = "";
|
|
236
|
+
stream.on("data", (chunk: string) => {
|
|
237
|
+
buffer += chunk;
|
|
238
|
+
let nl = buffer.indexOf("\n");
|
|
239
|
+
while (nl >= 0) {
|
|
240
|
+
const line = buffer.slice(0, nl);
|
|
241
|
+
buffer = buffer.slice(nl + 1);
|
|
242
|
+
onLine(line);
|
|
243
|
+
nl = buffer.indexOf("\n");
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// src/ui-channels.ts
|
|
2
|
+
//
|
|
3
|
+
// UI channel 提取(marker 解析)+ channel 注册表(引擎侧原语,自 core
|
|
4
|
+
// execution/ui-channels.ts 移入 @zhushanwen/subagent-engine-sdk,实现体逐字等价;
|
|
5
|
+
// round1-reuse R1:core execution 副本与 pi 引擎 CLI 副本自本模块 re-export 收编,
|
|
6
|
+
// 消逐字等价双副本——core → SDK、pi → SDK 均正向合法)。
|
|
7
|
+
//
|
|
8
|
+
// channel 是扩展协议自定义的业务路由标识,由 NUL 前缀 marker 标记。
|
|
9
|
+
// 已知 marker(来自 @xyz-agent/extension-protocol):
|
|
10
|
+
// - ASK_USER_MARKER = "\0XYZ_ASK_USER" 走 select method,出现在 title
|
|
11
|
+
// (options[0] = JSON payload {questions, allowCancel})
|
|
12
|
+
// - GUI_WIDGET_MARKER = "\0XYZ_GUI_WIDGET:" 走 setWidget method,出现在 widgetLines[0]
|
|
13
|
+
// (同行 marker 后紧跟 JSON payload {component})
|
|
14
|
+
//
|
|
15
|
+
// channel 提取位置随 method 变(.fix-plans/00-master-summary.md §一冲突 2):
|
|
16
|
+
// - select → 从 title 解析 NUL 前缀(payload 从 options[0] 取)
|
|
17
|
+
// - setWidget → 从 widgetLines[0] 解析 NUL 前缀(payload 从同行 marker 后取)
|
|
18
|
+
// - 其他 method → 无 channel(返回 {})
|
|
19
|
+
//
|
|
20
|
+
// channel 名规范化:去 "XYZ_" 命名空间前缀,去尾部 ":",小写化。
|
|
21
|
+
// XYZ_ASK_USER → ask_user
|
|
22
|
+
// XYZ_GUI_WIDGET → gui_widget
|
|
23
|
+
//
|
|
24
|
+
// 本模块是协议层工具(method/marker 都是协议概念),不感知业务。
|
|
25
|
+
|
|
26
|
+
/** NUL 前缀字符。Pi extension-protocol 用 NUL(\0)标记控制行,
|
|
27
|
+
* 避免与用户可见文本冲突。 */
|
|
28
|
+
const NUL = "\0";
|
|
29
|
+
|
|
30
|
+
/** channel 提取结果。channel 无 NUL 前缀、字段缺失、JSON parse 失败时
|
|
31
|
+
* channel 与 channelPayload 均为 undefined(返回 {})。 */
|
|
32
|
+
export interface ParsedChannel {
|
|
33
|
+
/** 规范化后的 channel 名(如 "ask_user"、"gui_widget")。
|
|
34
|
+
* 无 marker 或解析失败时为 undefined。 */
|
|
35
|
+
channel?: string;
|
|
36
|
+
/** marker 标记的结构化 payload(已 JSON.parse)。
|
|
37
|
+
* ask_user: {questions, allowCancel};gui_widget: {component}。
|
|
38
|
+
* payload 来源缺失或 JSON parse 失败时为 undefined(channel 仍可解析)。 */
|
|
39
|
+
channelPayload?: unknown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** parseChannel 入参的最小形状。
|
|
43
|
+
* method 是判别字段;按 method 不同,对应字段(select 的 title/options、
|
|
44
|
+
* setWidget 的 widgetLines)可选出现。其他 method 的字段统称 [key:string]。 */
|
|
45
|
+
export interface ExtensionUiRequestLike {
|
|
46
|
+
method: string;
|
|
47
|
+
/** select method:title 字段(可能含 ASK_USER_MARKER NUL 前缀)。 */
|
|
48
|
+
title?: string;
|
|
49
|
+
/** select method:options 数组(options[0] 可能是 channel payload 的 JSON)。 */
|
|
50
|
+
options?: string[];
|
|
51
|
+
/** setWidget method:widgetKey 字段。 */
|
|
52
|
+
widgetKey?: string;
|
|
53
|
+
/** setWidget method:widgetLines 数组(widgetLines[0] 可能含 GUI_WIDGET_MARKER)。 */
|
|
54
|
+
widgetLines?: string[] | undefined;
|
|
55
|
+
/** 其他 method 的任意字段(容错:允许测试和未来扩展传入额外字段)。 */
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** channel handler 签名:接收 UiRequest,返回 UiResponse。
|
|
60
|
+
* 具体类型定义在 session-runner.ts(W2 工作),此处用最小形状避免循环依赖。
|
|
61
|
+
* handler 实现方按 channel 注册,由 session-runner 按 req.channel 分派。 */
|
|
62
|
+
export type ChannelHandler = (req: unknown) => Promise<unknown>;
|
|
63
|
+
|
|
64
|
+
/** channel 注册表接口。职责单一:只管业务路由,不管排队、不管透传判定。
|
|
65
|
+
* - register(channel, handler):注册 channel 对应的 handler(同名覆盖)
|
|
66
|
+
* - resolve(channel):取 channel 对应的 handler,未注册返回 undefined
|
|
67
|
+
* - list():列举所有已注册 channel 名 */
|
|
68
|
+
export interface UiChannelRegistry {
|
|
69
|
+
register(channel: string, handler: ChannelHandler): void;
|
|
70
|
+
resolve(channel: string): ChannelHandler | undefined;
|
|
71
|
+
list(): string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 规范化 channel 名。
|
|
75
|
+
* 输入是 NUL 前缀后的字面量(如 "XYZ_ASK_USER"、"XYZ_GUI_WIDGET:")。
|
|
76
|
+
* 规则:
|
|
77
|
+
* 1. 去 "XYZ_" 命名空间前缀(协议命名空间标识,非业务语义)
|
|
78
|
+
* 2. 去尾部 ":"(GUI_WIDGET 等"行内 payload"型 marker 的分隔符)
|
|
79
|
+
* 3. 小写化(XYZ_ASK_USER → ask_user)
|
|
80
|
+
*
|
|
81
|
+
* 例:
|
|
82
|
+
* "XYZ_ASK_USER" → "ask_user"
|
|
83
|
+
* "XYZ_GUI_WIDGET:" → "gui_widget"
|
|
84
|
+
* "FOO_BAR" → "foo_bar"(无 XYZ_ 前缀也容忍,去前缀仅当字面量以 XYZ_ 开头) */
|
|
85
|
+
function normalizeChannelName(markerLiteral: string): string {
|
|
86
|
+
let name = markerLiteral;
|
|
87
|
+
// 去 "XYZ_" 命名空间前缀(仅当以此开头)
|
|
88
|
+
if (name.startsWith("XYZ_")) {
|
|
89
|
+
name = name.slice("XYZ_".length);
|
|
90
|
+
}
|
|
91
|
+
// 去尾部 ":"(行内 payload 型 marker 的分隔符)
|
|
92
|
+
if (name.endsWith(":")) {
|
|
93
|
+
name = name.slice(0, -1);
|
|
94
|
+
}
|
|
95
|
+
return name.toLowerCase();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 从 marker 字面量字符串解析 channel 名。
|
|
99
|
+
* 输入 str 形如 "\0XYZ_ASK_USER"(marker 占满整个字段,payload 在别处)。
|
|
100
|
+
* 无 NUL 前缀返回 undefined。 */
|
|
101
|
+
function parseMarkerFromField(str: string): string | undefined {
|
|
102
|
+
if (!str.startsWith(NUL)) return undefined;
|
|
103
|
+
// 去掉 NUL 前缀,取剩余字面量作为 marker literal
|
|
104
|
+
const literal = str.slice(NUL.length);
|
|
105
|
+
if (literal === "") return undefined;
|
|
106
|
+
return normalizeChannelName(literal);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 从 marker + 行内 payload 字符串解析 channel 名 + payload。
|
|
110
|
+
* 输入 str 形如 "\0XYZ_GUI_WIDGET:{...json...}"(marker 与 payload 在同一行)。
|
|
111
|
+
* 无 NUL 前缀返回 undefined。
|
|
112
|
+
* payload 解析失败时不抛(返回 channel 名,payload 由调用方处理)。 */
|
|
113
|
+
function parseInlineMarkerFromField(str: string): { channel: string } | undefined {
|
|
114
|
+
if (!str.startsWith(NUL)) return undefined;
|
|
115
|
+
const rest = str.slice(NUL.length);
|
|
116
|
+
// marker literal 与 payload 的分界:第一个 ":" 或行尾
|
|
117
|
+
// GUI_WIDGET_MARKER 格式为 "\0XYZ_GUI_WIDGET:" + json,分界是 ":"
|
|
118
|
+
const colonIdx = rest.indexOf(":");
|
|
119
|
+
let literal: string;
|
|
120
|
+
if (colonIdx >= 0) {
|
|
121
|
+
literal = rest.slice(0, colonIdx + 1); // 含 ":",normalizeChannelName 会去尾部 ":"
|
|
122
|
+
} else {
|
|
123
|
+
literal = rest;
|
|
124
|
+
}
|
|
125
|
+
if (literal === "") return undefined;
|
|
126
|
+
return { channel: normalizeChannelName(literal) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 从 select.title 解析 channel(payload 从 options[0] 取)。
|
|
130
|
+
* - title 无 NUL 前缀 → undefined
|
|
131
|
+
* - title 含 marker → channel 名;payload 从 options[0] JSON.parse(失败/缺失 → undefined) */
|
|
132
|
+
function parseFromMarkerString(
|
|
133
|
+
title: string | undefined,
|
|
134
|
+
options: string[] | undefined,
|
|
135
|
+
): ParsedChannel {
|
|
136
|
+
if (title === undefined) return {};
|
|
137
|
+
const channel = parseMarkerFromField(title);
|
|
138
|
+
if (channel === undefined) return {};
|
|
139
|
+
// payload 从 options[0] 取(ask_user 协议:title 是 marker,options[0] 是 JSON payload)
|
|
140
|
+
let payload: unknown;
|
|
141
|
+
if (options !== undefined && options.length > 0) {
|
|
142
|
+
try {
|
|
143
|
+
payload = JSON.parse(options[0]);
|
|
144
|
+
} catch {
|
|
145
|
+
payload = undefined; // JSON parse 失败:不抛,channel 仍解析
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { channel, channelPayload: payload };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 从 setWidget.widgetLines[0] 解析 channel(payload 从同行 marker 后取)。
|
|
152
|
+
* - widgetLines 缺失/空数组/首行无 NUL 前缀 → undefined
|
|
153
|
+
* - 首行含 marker → channel 名;payload 从 marker 后的 JSON 取(失败 → undefined) */
|
|
154
|
+
function parseFromMarkerArray(
|
|
155
|
+
widgetLines: string[] | undefined,
|
|
156
|
+
): ParsedChannel {
|
|
157
|
+
if (widgetLines === undefined || widgetLines.length === 0) return {};
|
|
158
|
+
const firstLine = widgetLines[0];
|
|
159
|
+
if (typeof firstLine !== "string") return {};
|
|
160
|
+
const parsed = parseInlineMarkerFromField(firstLine);
|
|
161
|
+
if (parsed === undefined) return {};
|
|
162
|
+
// payload 从 marker 后的 JSON 取(GUI_WIDGET 协议:marker 与 payload 同行)
|
|
163
|
+
let payload: unknown;
|
|
164
|
+
const rest = firstLine.slice(NUL.length);
|
|
165
|
+
const colonIdx = rest.indexOf(":");
|
|
166
|
+
if (colonIdx >= 0) {
|
|
167
|
+
const jsonStr = rest.slice(colonIdx + 1);
|
|
168
|
+
if (jsonStr !== "") {
|
|
169
|
+
try {
|
|
170
|
+
payload = JSON.parse(jsonStr);
|
|
171
|
+
} catch {
|
|
172
|
+
payload = undefined; // JSON parse 失败:不抛,channel 仍解析
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { channel: parsed.channel, channelPayload: payload };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** 按 method 分派解析 channel。
|
|
180
|
+
*
|
|
181
|
+
* - select → 从 title 解析 NUL 前缀(payload 从 options[0])
|
|
182
|
+
* - setWidget → 从 widgetLines[0] 解析 NUL 前缀(payload 从同行 marker 后)
|
|
183
|
+
* - 其他 method → {}(无 channel 提取位置)
|
|
184
|
+
*
|
|
185
|
+
* 边界(均不抛错):
|
|
186
|
+
* - title/widgetLines 字段缺失 → {}
|
|
187
|
+
* - 无 NUL 前缀 → {}
|
|
188
|
+
* - JSON parse 失败 → channel 仍解析,channelPayload 为 undefined
|
|
189
|
+
*
|
|
190
|
+
* @param req ExtensionUiRequestLike(method + 对应字段)
|
|
191
|
+
* @returns ParsedChannel(channel/channelPayload 可选) */
|
|
192
|
+
export function parseChannel(req: ExtensionUiRequestLike): ParsedChannel {
|
|
193
|
+
switch (req.method) {
|
|
194
|
+
case "select":
|
|
195
|
+
return parseFromMarkerString(req.title, req.options);
|
|
196
|
+
case "setWidget":
|
|
197
|
+
return parseFromMarkerArray(req.widgetLines);
|
|
198
|
+
default:
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 创建 channel 注册表实例。
|
|
204
|
+
* 进程级单例(通常由 SubagentService 持有一个实例,跨所有子进程共享)。
|
|
205
|
+
* register 同名 channel 会覆盖旧 handler。 */
|
|
206
|
+
export function createUiChannelRegistry(): UiChannelRegistry {
|
|
207
|
+
const handlers = new Map<string, ChannelHandler>();
|
|
208
|
+
return {
|
|
209
|
+
register(channel: string, handler: ChannelHandler): void {
|
|
210
|
+
handlers.set(channel, handler);
|
|
211
|
+
},
|
|
212
|
+
resolve(channel: string): ChannelHandler | undefined {
|
|
213
|
+
return handlers.get(channel);
|
|
214
|
+
},
|
|
215
|
+
list(): string[] {
|
|
216
|
+
return Array.from(handlers.keys());
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
package/src/ui-types.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/ui-types.ts
|
|
2
|
+
//
|
|
3
|
+
// UI 请求/响应契约类型(自 core execution/dialog-queue.ts :94/:113/:150/:159 迁入
|
|
4
|
+
// @zhushanwen/subagent-engine-sdk,结构等价)。迁移处置(impl-plan §2.1 类型闭包表
|
|
5
|
+
// R3 S-A / R4-S③):**UiMethod / UiRequest / UiResponse / UiRequestHandler 入 SDK
|
|
6
|
+
// 类型闭包**——该模块是三类型的规范来源(dialog-queue.ts:87 注释自证);`UiMethod`
|
|
7
|
+
// 是 `UiRequest.method` 的依赖类型(:94,R4-S③ 补录,包外零消费方)。队列实现本体
|
|
8
|
+
// 留 core;core 侧改再导出 SDK 类型(`export type { ... } from
|
|
9
|
+
// "@zhushanwen/subagent-engine-sdk"`,§2.7 MF-X2 既定决策,引用切换归 W2/W7);
|
|
10
|
+
// 双向可赋值断言挂 core typecheck 断言族。
|
|
11
|
+
//
|
|
12
|
+
// 协议消费面:反向通道 host/askUser 的载荷携带 UiRequest(reverse-channels.ts),
|
|
13
|
+
// 两阶段结果 = UiResponse(ack 先行、结果异步到达,R9-2)。
|
|
14
|
+
|
|
15
|
+
/** Pi extension_ui_request 的方法枚举(dialog + fire-and-forget 两类)。
|
|
16
|
+
* dialog 类:select/confirm/input/editor(占输入焦点,等响应)。
|
|
17
|
+
* fire-and-forget 类:notify/setStatus/setWidget/setTitle/set_editor_text(纯展示/写入)。
|
|
18
|
+
* (string & {}) 兜底:Pi 未来新增 method 或未知 method 走字符串字面量类型。 */
|
|
19
|
+
export type UiMethod =
|
|
20
|
+
| "select"
|
|
21
|
+
| "confirm"
|
|
22
|
+
| "input"
|
|
23
|
+
| "editor"
|
|
24
|
+
| "notify"
|
|
25
|
+
| "setStatus"
|
|
26
|
+
| "setWidget"
|
|
27
|
+
| "setTitle"
|
|
28
|
+
| "set_editor_text"
|
|
29
|
+
| (string & {});
|
|
30
|
+
|
|
31
|
+
/** UI 请求(session-runner 构造后传给 handler)。
|
|
32
|
+
*
|
|
33
|
+
* method 是判别字段,决定排队策略(dialog 排队)和业务路由(channel 分发)。
|
|
34
|
+
* method 特定字段按 method 可选出现(与 ExtensionUiRequest 1:1,由 session-runner 从
|
|
35
|
+
* ExtensionUiRequest 平铺构造)。channel/channelPayload 由 parseChannel 填充。
|
|
36
|
+
*
|
|
37
|
+
* 契约来源:.fix-plans/00-master-summary.md §二 2.2。 */
|
|
38
|
+
export interface UiRequest {
|
|
39
|
+
/** Pi rpc-types.ts 的 method(select/confirm/input/editor 为 dialog 类)。 */
|
|
40
|
+
method: UiMethod;
|
|
41
|
+
/** 请求 id(从 extension_ui_request envelope 顶层提取,用于 response 关联)。 */
|
|
42
|
+
id: string;
|
|
43
|
+
// method 特定字段(按 method 可选,与 ExtensionUiRequest 1:1)
|
|
44
|
+
title?: string;
|
|
45
|
+
options?: string[];
|
|
46
|
+
message?: string;
|
|
47
|
+
placeholder?: string;
|
|
48
|
+
prefill?: string;
|
|
49
|
+
notifyType?: string;
|
|
50
|
+
statusKey?: string;
|
|
51
|
+
statusText?: string | undefined;
|
|
52
|
+
widgetKey?: string;
|
|
53
|
+
widgetLines?: string[] | undefined;
|
|
54
|
+
widgetPlacement?: "aboveEditor" | "belowEditor";
|
|
55
|
+
text?: string;
|
|
56
|
+
timeout?: number;
|
|
57
|
+
/** channel 名(从 method 对应字段的 NUL 前缀解析)。
|
|
58
|
+
* select → 从 title 解析;setWidget → 从 widgetLines[0] 解析;其他 → undefined。
|
|
59
|
+
* 已知值:"ask_user"(select)、"gui_widget"(setWidget)。handler 按 channel 分发。 */
|
|
60
|
+
channel?: string;
|
|
61
|
+
/** channel 解析后的结构化 payload(已 JSON.parse)。
|
|
62
|
+
* ask_user: {questions, allowCancel};gui_widget: {component};无 channel: undefined。 */
|
|
63
|
+
channelPayload?: unknown;
|
|
64
|
+
/** 内部元数据字段:发起该 UI 请求的子进程 pid(由 session-runner.handleUiRequest 从
|
|
65
|
+
* child.pid 填入)。L2 队列据此关联 rejectChildDialogs(child close 时批量 reject)。
|
|
66
|
+
* 下划线前缀表示内部字段,非 Pi 协议字段,不参与 stdin 回写。 */
|
|
67
|
+
_childPid?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** UI 响应(handler 返回,session-runner 按 shape 回写 stdin)。
|
|
71
|
+
* - {value}: select/input/editor 的答案
|
|
72
|
+
* - {confirmed}: confirm 的答案
|
|
73
|
+
* - {cancelled}: 取消(child close / handler 抛错 / 用户取消)
|
|
74
|
+
* - {ack}: fire-and-forget(当前不透传到 TUI,留作协议完整) */
|
|
75
|
+
export type UiResponse =
|
|
76
|
+
| { value: string }
|
|
77
|
+
| { confirmed: boolean }
|
|
78
|
+
| { cancelled: true }
|
|
79
|
+
| { ack: true };
|
|
80
|
+
|
|
81
|
+
/** UI 请求 handler 签名(单函数,按 req.method 内部路由)。
|
|
82
|
+
* 实现方负责:channel 业务路由(ask_user → AskUserComponent)+ 默认转发(ctx.ui.*)。
|
|
83
|
+
* 抛错由调用方(DialogGlobalQueue / session-runner)兜底为 {cancelled:true}。 */
|
|
84
|
+
export type UiRequestHandler = (req: UiRequest) => Promise<UiResponse>;
|