@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
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// src/protocol/reverse-channels.ts
|
|
2
|
+
//
|
|
3
|
+
// 9 反向通道(引擎 → core,帧④,必须应答)载荷与超时二分。设计权威源:
|
|
4
|
+
// 设计 §3.3 方法集表 host/* 行 + impl-plan §2.1「8 反向通道」与「反向请求超时二分」
|
|
5
|
+
// + chat 域 v1.x 增量(docs/design/chat-domain-v1x-liveness-governance.md §3.2 D1-A:
|
|
6
|
+
// 第 9 通道 host/roundLifecycle 承载轮次终态事件与 record 回写)。
|
|
7
|
+
//
|
|
8
|
+
// 应答约定:数据面类回 {ok:true}(REVERSE_REQUEST_TIMEOUT_MS=10s 未答 = 引擎故障 →
|
|
9
|
+
// 杀进程 + 在途 run 失败);人机交互类走 ack 两阶段——先回 {ack:true},结果异步到达
|
|
10
|
+
// (R9-2:已 ack 的等待不计入任何 in-flight 超时;ADR-0047 静默 ≠ 卡死);
|
|
11
|
+
// 未实现的交互能力回 {unsupported:true}(引擎自行降级,不重试)。
|
|
12
|
+
|
|
13
|
+
import type { ReverseRequestTimeoutClass } from "./engine-protocol.ts";
|
|
14
|
+
import type { UiRequest, UiResponse } from "../ui-types.ts";
|
|
15
|
+
import type { AgentUsage, ResumeAnchor } from "./contract-types.ts";
|
|
16
|
+
import type { ProtocolError } from "./frames.ts";
|
|
17
|
+
|
|
18
|
+
/** 反向通道名联合(恰好 9 个;REVERSE_CHANNELS 常量数组与之同源互证)。 */
|
|
19
|
+
export type ReverseChannel =
|
|
20
|
+
| "host/log"
|
|
21
|
+
| "host/askUser"
|
|
22
|
+
| "host/permission"
|
|
23
|
+
| "host/streamDelta"
|
|
24
|
+
| "host/poolResolved"
|
|
25
|
+
| "host/handleReady"
|
|
26
|
+
| "host/childSpawned"
|
|
27
|
+
| "host/childStateChanged"
|
|
28
|
+
| "host/roundLifecycle";
|
|
29
|
+
|
|
30
|
+
export const REVERSE_CHANNELS = [
|
|
31
|
+
"host/log",
|
|
32
|
+
"host/askUser",
|
|
33
|
+
"host/permission",
|
|
34
|
+
"host/streamDelta",
|
|
35
|
+
"host/poolResolved",
|
|
36
|
+
"host/handleReady",
|
|
37
|
+
"host/childSpawned",
|
|
38
|
+
"host/childStateChanged",
|
|
39
|
+
"host/roundLifecycle",
|
|
40
|
+
] as const satisfies readonly ReverseChannel[];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 超时二分归属(10s 数据面 / 不设统一超时的人机交互面)。实现归 W2 EngineClient;
|
|
44
|
+
* 引擎侧自灭计时(W12)复用同表——已 ack 的 askUser 等待不计入 in-flight(R9-2)。
|
|
45
|
+
* host/roundLifecycle 属数据面(终态回执语义:宿主必须确认收到,10s 未答 = 引擎故障)。
|
|
46
|
+
*/
|
|
47
|
+
export const REVERSE_CHANNEL_TIMEOUT_CLASS: Record<ReverseChannel, ReverseRequestTimeoutClass> = {
|
|
48
|
+
"host/log": "data-plane",
|
|
49
|
+
"host/streamDelta": "data-plane",
|
|
50
|
+
"host/poolResolved": "data-plane",
|
|
51
|
+
"host/handleReady": "data-plane",
|
|
52
|
+
"host/childSpawned": "data-plane",
|
|
53
|
+
"host/childStateChanged": "data-plane",
|
|
54
|
+
"host/roundLifecycle": "data-plane",
|
|
55
|
+
"host/askUser": "interaction",
|
|
56
|
+
"host/permission": "interaction",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ============================================================
|
|
60
|
+
// 通道载荷(params)
|
|
61
|
+
// ============================================================
|
|
62
|
+
|
|
63
|
+
/** host/log:引擎日志落宿主日志(对齐 core HostServices.log 调用面)。 */
|
|
64
|
+
export interface HostLogParams {
|
|
65
|
+
level: "debug" | "warn" | "error";
|
|
66
|
+
component: string;
|
|
67
|
+
message: string;
|
|
68
|
+
data?: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** host/askUser:UI 请求经反向通道送达宿主(core 壳侧 uiRequestHandler 应答)。 */
|
|
72
|
+
export interface HostAskUserParams {
|
|
73
|
+
runId: string;
|
|
74
|
+
/** Pi extension_ui_request 平铺形态(类型 SSOT = SDK ui-types.ts,core 反向 re-export)。 */
|
|
75
|
+
request: UiRequest;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** host/askUser 的最终结果(ack 两阶段第二阶段,异步应答帧②的 result)。 */
|
|
79
|
+
export type HostAskUserResult = UiResponse;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* host/permission:权限询问(引擎请求宿主裁决工具执行)。
|
|
83
|
+
* v1 骨架字段(设计未钉死载荷细节,细化归各引擎提取设计;W2 实装 core 应答端时
|
|
84
|
+
* 若需扩展走 additive 演进)。
|
|
85
|
+
*/
|
|
86
|
+
export interface HostPermissionParams {
|
|
87
|
+
runId: string;
|
|
88
|
+
toolName: string;
|
|
89
|
+
args?: unknown;
|
|
90
|
+
/** 请求方向引擎给出的原因/说明(展示用)。 */
|
|
91
|
+
reason?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** host/permission 两阶段结果。 */
|
|
95
|
+
export type HostPermissionResult = { approved: boolean } | { unsupported: true };
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* host/streamDelta:UI 实时通道(双通道之一;与 event 通知并行的渲染加速面)。
|
|
99
|
+
*
|
|
100
|
+
* [v1.x 关联键扩展——D1-A 裁定,W1 落地不再临场选择]:
|
|
101
|
+
* - run 域轮(含 run 会话形态首轮):runId 关联(v1 现状不变,runId 由 core 在
|
|
102
|
+
* run 帧分配);
|
|
103
|
+
* - interact 发起的续聊轮:**recordId** 关联(续聊轮无独立 runId——InteractParams/
|
|
104
|
+
* InteractResult 均不含,recordId 经 handle.sessionRef 送达引擎)。
|
|
105
|
+
* 两键互斥(undefined 孪生位防双填),消费侧经 isHostStreamDeltaParams 收窄。
|
|
106
|
+
*/
|
|
107
|
+
export type HostStreamDeltaParams =
|
|
108
|
+
| { runId: string; recordId?: undefined; delta: string }
|
|
109
|
+
| { recordId: string; runId?: undefined; delta: string };
|
|
110
|
+
|
|
111
|
+
/** streamDelta 载荷结构判定(关联键互斥 + delta 形状;消费侧共用,防双侧各写一份)。 */
|
|
112
|
+
export function isHostStreamDeltaParams(value: unknown): value is HostStreamDeltaParams {
|
|
113
|
+
if (typeof value !== "object" || value === null) return false;
|
|
114
|
+
const v = value as Record<string, unknown>;
|
|
115
|
+
const hasRunId = typeof v.runId === "string";
|
|
116
|
+
const hasRecordId = typeof v.recordId === "string";
|
|
117
|
+
// 恰一键:无键 / 双键 / 键非 string 都拒
|
|
118
|
+
if (hasRunId === hasRecordId) return false;
|
|
119
|
+
return typeof v.delta === "string";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* host/poolResolved:journal 落盘路径单一权威(契约:必须在首个事件 emit 前调用——
|
|
124
|
+
* 否则 journal 归属错)。载荷 = core onPoolResolved(poolKey) 的 runId 关联形态。
|
|
125
|
+
*/
|
|
126
|
+
export interface HostPoolResolvedParams {
|
|
127
|
+
runId: string;
|
|
128
|
+
poolKey: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* host/handleReady:运行中句柄回填(core onHandleReady 语义:session/create 应答后、
|
|
133
|
+
* 早于 run resolve;AGENTS.md 关键规则 9「重开 session 仍可见」的前提)。
|
|
134
|
+
*/
|
|
135
|
+
export interface HostHandleReadyParams {
|
|
136
|
+
runId: string;
|
|
137
|
+
sessionRef: Record<string, string>;
|
|
138
|
+
poolKey: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* host/childSpawned:引擎内一次性子进程 pid 上报。
|
|
143
|
+
* 用途 = isResumable 镜像谓词 + 诊断留痕;**不供杀链/收割**(v6 已删按 pid 补杀,
|
|
144
|
+
* 收割只靠进程组);常驻进程不报(归 dispose)。
|
|
145
|
+
*/
|
|
146
|
+
export interface HostChildSpawnedParams {
|
|
147
|
+
pid: number;
|
|
148
|
+
recordId: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* host/childStateChanged:childSpawned 的状态面(core 侧镜像数据源,
|
|
153
|
+
* hasLiveProcessHandle/isResumable 同步读镜像,不跨进程查询)。
|
|
154
|
+
* **killed 必含**(判据 `child !== undefined && !child.killed`)——类型层 required。
|
|
155
|
+
*/
|
|
156
|
+
export interface HostChildStateChangedParams {
|
|
157
|
+
pid: number;
|
|
158
|
+
recordId: string;
|
|
159
|
+
state: "running" | "exited";
|
|
160
|
+
/** 必含:true = 已被杀/已终止(镜像置死判据)。 */
|
|
161
|
+
killed: boolean;
|
|
162
|
+
exitCode?: number;
|
|
163
|
+
signal?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ============================================================
|
|
167
|
+
// [v1.x] host/roundLifecycle:轮次生命周期载荷(chat 域 v1.x 唯一新通道)
|
|
168
|
+
// ============================================================
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 轮次关联键(D1-A 裁定的类型面):run 域轮 = runId(v1 现状);interact 续聊轮 =
|
|
172
|
+
* recordId(经 handle.sessionRef)。undefined 孪生位保证两键互斥。
|
|
173
|
+
*/
|
|
174
|
+
export interface RoundKeyedByRun {
|
|
175
|
+
runId: string;
|
|
176
|
+
recordId?: undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface RoundKeyedByRecord {
|
|
180
|
+
recordId: string;
|
|
181
|
+
runId?: undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 轮次终态相位(事件即 record 回写载体——chat 域 record 处置由相位一一映射,
|
|
186
|
+
* 设计 D2 裁决表 conversation 行:settled/idle 轮收口不终态、failed 标 failed):
|
|
187
|
+
* - settled:轮收敛(输出完整)。消费点 = settled-watchdog disarm + D3 abort
|
|
188
|
+
* 收敛判据(cancel 受理后等本事件,超 CANCEL_SETTLE_GRACE_MS 走杀链);
|
|
189
|
+
* - idle:轮收口 + 会话进 idle 稳态(core 侧 doFinalizeRoundToIdle + idle 定时器
|
|
190
|
+
* 锚点)。settled 与 idle 是两个锚点:watchdog 在 settled 即解除,idle 管置闲;
|
|
191
|
+
* - failed:轮异常终止(引擎自知失败,如 EPIPE 兜底耗尽)——error 如实上报,
|
|
192
|
+
* record 标 failed(与 run 域 AgentOutcome.error 的「失败收口」语义对齐)。
|
|
193
|
+
* usage 为本轮 message_end 增量(interact 续聊轮无 event 通知通道,用量经本帧回填)。
|
|
194
|
+
* 不设 seq:stdio NDJSON 单连接有序 + 数据面应答确认,无重排/重放面(与 event
|
|
195
|
+
* 通知的 seq 对照——后者镜像进程内事件流基线,本帧无基线可镜像)。
|
|
196
|
+
*/
|
|
197
|
+
export interface RoundSettledPhase {
|
|
198
|
+
phase: "settled";
|
|
199
|
+
usage?: AgentUsage;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface RoundIdlePhase {
|
|
203
|
+
phase: "idle";
|
|
204
|
+
usage?: AgentUsage;
|
|
205
|
+
/** 冷续锚点回填(session 滚动/compaction 后锚点可能变化,宿主按帧刷新)。 */
|
|
206
|
+
anchor?: ResumeAnchor;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface RoundFailedPhase {
|
|
210
|
+
phase: "failed";
|
|
211
|
+
/** 失败原因(结构对齐协议 error 帧——code/message/recovery 可操作闭环)。 */
|
|
212
|
+
error: ProtocolError;
|
|
213
|
+
anchor?: ResumeAnchor;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 相位联合(消费侧 switch(phase) 判别用)。 */
|
|
217
|
+
export type RoundLifecyclePhase = RoundSettledPhase | RoundIdlePhase | RoundFailedPhase;
|
|
218
|
+
|
|
219
|
+
/** host/roundLifecycle 载荷:关联键(run|record)× 相位(settled|idle|failed)。 */
|
|
220
|
+
export type HostRoundLifecycleParams =
|
|
221
|
+
| (RoundKeyedByRun & RoundSettledPhase)
|
|
222
|
+
| (RoundKeyedByRun & RoundIdlePhase)
|
|
223
|
+
| (RoundKeyedByRun & RoundFailedPhase)
|
|
224
|
+
| (RoundKeyedByRecord & RoundSettledPhase)
|
|
225
|
+
| (RoundKeyedByRecord & RoundIdlePhase)
|
|
226
|
+
| (RoundKeyedByRecord & RoundFailedPhase);
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* roundLifecycle 载荷结构判定:关联键互斥 + phase 词表 + 各相位专属形状
|
|
230
|
+
* (failed 必含 error.code/message)。引擎侧发帧前自检与 core 侧消费共用,
|
|
231
|
+
* 防两侧各写一份判别(与 isHostStreamDeltaParams 同理)。
|
|
232
|
+
*/
|
|
233
|
+
export function isHostRoundLifecycleParams(value: unknown): value is HostRoundLifecycleParams {
|
|
234
|
+
if (typeof value !== "object" || value === null) return false;
|
|
235
|
+
const v = value as Record<string, unknown>;
|
|
236
|
+
const hasRunId = typeof v.runId === "string";
|
|
237
|
+
const hasRecordId = typeof v.recordId === "string";
|
|
238
|
+
if (hasRunId === hasRecordId) return false;
|
|
239
|
+
if (v.phase === "settled" || v.phase === "idle") return true;
|
|
240
|
+
if (v.phase !== "failed") return false;
|
|
241
|
+
const err = v.error;
|
|
242
|
+
return (
|
|
243
|
+
typeof err === "object" &&
|
|
244
|
+
err !== null &&
|
|
245
|
+
typeof (err as Record<string, unknown>).code === "string" &&
|
|
246
|
+
typeof (err as Record<string, unknown>).message === "string"
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** 通道 → 载荷类型映射。 */
|
|
251
|
+
export interface ReverseChannelParamsMap {
|
|
252
|
+
"host/log": HostLogParams;
|
|
253
|
+
"host/askUser": HostAskUserParams;
|
|
254
|
+
"host/permission": HostPermissionParams;
|
|
255
|
+
"host/streamDelta": HostStreamDeltaParams;
|
|
256
|
+
"host/poolResolved": HostPoolResolvedParams;
|
|
257
|
+
"host/handleReady": HostHandleReadyParams;
|
|
258
|
+
"host/childSpawned": HostChildSpawnedParams;
|
|
259
|
+
"host/childStateChanged": HostChildStateChangedParams;
|
|
260
|
+
"host/roundLifecycle": HostRoundLifecycleParams;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 通道 → 异步/同步结果类型映射(ack 两阶段通道的第二阶段 result)。 */
|
|
264
|
+
export interface ReverseChannelResultMap {
|
|
265
|
+
"host/log": { ok: true };
|
|
266
|
+
"host/askUser": HostAskUserResult;
|
|
267
|
+
"host/permission": HostPermissionResult;
|
|
268
|
+
"host/streamDelta": { ok: true };
|
|
269
|
+
"host/poolResolved": { ok: true };
|
|
270
|
+
"host/handleReady": { ok: true };
|
|
271
|
+
"host/childSpawned": { ok: true };
|
|
272
|
+
"host/childStateChanged": { ok: true };
|
|
273
|
+
"host/roundLifecycle": { ok: true };
|
|
274
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// src/protocol/schema.ts
|
|
2
|
+
//
|
|
3
|
+
// 帧校验 JSON Schema(draft-07)。设计权威源:impl-plan §2.1 W1 职责「JSON Schema
|
|
4
|
+
// (帧校验用)」+ 设计 §3.10 不变量 5「凭据不过协议:协议里不出现 apiKey / token
|
|
5
|
+
// 字段(schema 层禁止 + 测试断言)」。
|
|
6
|
+
//
|
|
7
|
+
// schema 层禁止凭据的实现口径:所有对象帧 additionalProperties:false + 属性白名单,
|
|
8
|
+
// 凭据类键名(apiKey/token/credential 等)不在任何白名单内;测试侧另断言白名单全集
|
|
9
|
+
// 无凭据键(src/__tests__/protocol-frames.test.ts)。params/result 的深载荷结构由
|
|
10
|
+
// TS 类型承载(protocol/methods.ts / reverse-channels.ts / contract-types.ts),
|
|
11
|
+
// 帧级 schema 只定形状骨架(id/method/params/result/error 的存在性与类型)——
|
|
12
|
+
// 帧校验的目的是行解析器快速拒格式坏帧,不做深校验(深校验成本高于收益,坏载荷
|
|
13
|
+
// 由消费方结构化报错)。
|
|
14
|
+
|
|
15
|
+
import type { ReverseChannel } from "./reverse-channels.ts";
|
|
16
|
+
import { REVERSE_CHANNELS } from "./reverse-channels.ts";
|
|
17
|
+
import { PROTOCOL_METHODS } from "./methods.ts";
|
|
18
|
+
|
|
19
|
+
/** 任意 JSON 值(draft-07 空约束)。 */
|
|
20
|
+
const ANY_JSON = {} as const;
|
|
21
|
+
|
|
22
|
+
/** 协议错误对象 schema。 */
|
|
23
|
+
export const protocolErrorSchema = {
|
|
24
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
25
|
+
title: "EngineProtocolError",
|
|
26
|
+
type: "object",
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
required: ["code", "message", "recovery"],
|
|
29
|
+
properties: {
|
|
30
|
+
code: { type: "string", pattern: "^engine_" },
|
|
31
|
+
message: { type: "string" },
|
|
32
|
+
recovery: { type: "string" },
|
|
33
|
+
data: { type: "object" },
|
|
34
|
+
},
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
/** ① 请求帧(core → 引擎)。 */
|
|
38
|
+
export const requestFrameSchema = {
|
|
39
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
40
|
+
title: "EngineProtocolRequestFrame",
|
|
41
|
+
type: "object",
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
required: ["id", "method", "params"],
|
|
44
|
+
properties: {
|
|
45
|
+
id: { type: "number" },
|
|
46
|
+
method: { type: "string", enum: [...PROTOCOL_METHODS] },
|
|
47
|
+
params: ANY_JSON,
|
|
48
|
+
},
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
/** ② 应答帧(引擎 → core;result 与 error 互斥)。 */
|
|
52
|
+
export const successResponseFrameSchema = {
|
|
53
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
54
|
+
title: "EngineProtocolSuccessResponseFrame",
|
|
55
|
+
type: "object",
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
required: ["id", "result"],
|
|
58
|
+
properties: {
|
|
59
|
+
id: { type: ["number", "string"] },
|
|
60
|
+
result: ANY_JSON,
|
|
61
|
+
},
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
export const errorResponseFrameSchema = {
|
|
65
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
66
|
+
title: "EngineProtocolErrorResponseFrame",
|
|
67
|
+
type: "object",
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
required: ["id", "error"],
|
|
70
|
+
properties: {
|
|
71
|
+
id: { type: ["number", "string"] },
|
|
72
|
+
error: protocolErrorSchema,
|
|
73
|
+
},
|
|
74
|
+
} as const;
|
|
75
|
+
|
|
76
|
+
/** ③ 通知帧(引擎 → core,无 id;method 恒 "event")。 */
|
|
77
|
+
export const notificationFrameSchema = {
|
|
78
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
79
|
+
title: "EngineProtocolNotificationFrame",
|
|
80
|
+
type: "object",
|
|
81
|
+
additionalProperties: false,
|
|
82
|
+
required: ["method", "params"],
|
|
83
|
+
properties: {
|
|
84
|
+
method: { type: "string", const: "event" },
|
|
85
|
+
params: {
|
|
86
|
+
type: "object",
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
required: ["runId", "seq", "event"],
|
|
89
|
+
properties: {
|
|
90
|
+
runId: { type: "string" },
|
|
91
|
+
seq: { type: "number" },
|
|
92
|
+
event: {
|
|
93
|
+
type: "object",
|
|
94
|
+
required: ["type"],
|
|
95
|
+
properties: {
|
|
96
|
+
type: {
|
|
97
|
+
type: "string",
|
|
98
|
+
enum: [
|
|
99
|
+
"tool_start",
|
|
100
|
+
"tool_end",
|
|
101
|
+
"text_delta",
|
|
102
|
+
"thinking_delta",
|
|
103
|
+
"turn_end",
|
|
104
|
+
"message_end",
|
|
105
|
+
"compaction",
|
|
106
|
+
"error",
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
} as const;
|
|
115
|
+
|
|
116
|
+
/** ④ 反向请求帧(引擎 → core,必须应答;method 恒 host/*)。 */
|
|
117
|
+
export const reverseRequestFrameSchema = {
|
|
118
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
119
|
+
title: "EngineProtocolReverseRequestFrame",
|
|
120
|
+
type: "object",
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
required: ["id", "method", "params"],
|
|
123
|
+
properties: {
|
|
124
|
+
id: { type: "string" },
|
|
125
|
+
method: { type: "string", enum: [...REVERSE_CHANNELS] },
|
|
126
|
+
params: ANY_JSON,
|
|
127
|
+
},
|
|
128
|
+
} as const;
|
|
129
|
+
|
|
130
|
+
/** 帧型 → schema 索引(校验入口按帧型取用)。 */
|
|
131
|
+
export const ENGINE_PROTOCOL_SCHEMAS = {
|
|
132
|
+
request: requestFrameSchema,
|
|
133
|
+
successResponse: successResponseFrameSchema,
|
|
134
|
+
errorResponse: errorResponseFrameSchema,
|
|
135
|
+
notification: notificationFrameSchema,
|
|
136
|
+
reverseRequest: reverseRequestFrameSchema,
|
|
137
|
+
protocolError: protocolErrorSchema,
|
|
138
|
+
} as const;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 协议面凭据禁区键名(不变量 5 的测试断言依据):帧 schema 与深载荷 TS 类型面
|
|
142
|
+
* 不得出现。词表覆盖常见拼写(子串匹配,非精确枚举)。
|
|
143
|
+
*/
|
|
144
|
+
export const FORBIDDEN_CREDENTIAL_KEY_FRAGMENTS = [
|
|
145
|
+
"apikey",
|
|
146
|
+
"api_key",
|
|
147
|
+
"token",
|
|
148
|
+
"credential",
|
|
149
|
+
"password",
|
|
150
|
+
"secret",
|
|
151
|
+
] as const;
|
|
152
|
+
|
|
153
|
+
/** 反向通道名词表导出(schema enum 与 W2 路由同源)。 */
|
|
154
|
+
export const REVERSE_CHANNEL_NAMES: readonly ReverseChannel[] = REVERSE_CHANNELS;
|
package/src/relay-env.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/relay-env.ts
|
|
2
|
+
//
|
|
3
|
+
// subagent relay 通道 env 名与协议常量 SSOT(docs/architecture/subagent-realtime-channel.md
|
|
4
|
+
// §5.1/§5.2/§3.1)。跨进程契约单源:core(./relay-env 子入口)与 pi 引擎 CLI
|
|
5
|
+
// 的副本(round1-reuse R9)自本模块 re-export 收编——边界合法(引擎 CLI → SDK、
|
|
6
|
+
// core → SDK 均正向)。runtime 侧消费经 core 子入口间接取本单源。
|
|
7
|
+
//
|
|
8
|
+
// 为什么独立成模块:三方消费同一份常量——extension 侧(pi-invocation 激活判定 /
|
|
9
|
+
// buildChildEnv 归属写入)、runtime 侧(env 注入与镜像校验)、代理脚本 relay.mjs
|
|
10
|
+
// (零依赖脚本不能 import workspace 包,只能内嵌镜像,镜像一致性由 conformance
|
|
11
|
+
// relay 变体断言锁定)。禁各处手写字符串。
|
|
12
|
+
//
|
|
13
|
+
// env 语义:SOCKET/NODE/SCRIPT 三者同时非空 = relay 激活(全有或全无,无中间态);
|
|
14
|
+
// SESSION_ID/RECORD_ID 是 tee 帧归属键(缺失由代理握手前自检拒绝,退出码 13)。
|
|
15
|
+
|
|
16
|
+
export const RELAY_ENV_SOCKET = "XYZ_SUBAGENT_RELAY_SOCKET";
|
|
17
|
+
export const RELAY_ENV_NODE = "XYZ_SUBAGENT_RELAY_NODE";
|
|
18
|
+
export const RELAY_ENV_SCRIPT = "XYZ_SUBAGENT_RELAY_SCRIPT";
|
|
19
|
+
export const RELAY_ENV_SESSION_ID = "XYZ_SUBAGENT_RELAY_SESSION_ID";
|
|
20
|
+
export const RELAY_ENV_RECORD_ID = "XYZ_SUBAGENT_RELAY_RECORD_ID";
|
|
21
|
+
|
|
22
|
+
/** relay 协议版本(握手帧 v 字段;runtime 与代理同包分发,不匹配=安装损坏)。 */
|
|
23
|
+
export const RELAY_PROTOCOL_VERSION = 1;
|
|
24
|
+
|
|
25
|
+
/** 代理专用退出码(extension/引擎侧表现为「子进程非零退出」→ engine_run_failed 语义)。 */
|
|
26
|
+
export const RELAY_EXIT_CODES = {
|
|
27
|
+
/** 握手被拒:协议版本不匹配(安装损坏,重装应用)。 */
|
|
28
|
+
VERSION_MISMATCH: 10,
|
|
29
|
+
/** relay socket 不可达(runtime 未运行或已重启)。 */
|
|
30
|
+
SOCKET_UNREACHABLE: 11,
|
|
31
|
+
/** socket 中途断开(runtime 崩溃等)——代理生命线断即退。 */
|
|
32
|
+
SOCKET_CLOSED: 12,
|
|
33
|
+
/** 归属 env(SESSION_ID/RECORD_ID)缺失——防无归属帧污染广播。 */
|
|
34
|
+
MISSING_IDENTITY: 13,
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
/** 激活判定:三 env 同时非空才走 relay,任一缺失回落直连 spawn 真实引擎(TUI/独立引擎零回归)。 */
|
|
38
|
+
export function isRelayActive(env: NodeJS.ProcessEnv | Record<string, string | undefined>): boolean {
|
|
39
|
+
return Boolean(env[RELAY_ENV_SOCKET] && env[RELAY_ENV_NODE] && env[RELAY_ENV_SCRIPT]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* [W8 H12] 宿主 env → relay 连接三键的转发提取(原样转发语义的单一实现)。
|
|
44
|
+
*
|
|
45
|
+
* 三键同时非空(isRelayActive 同判)才返回——全有或全无,无中间态;身份键
|
|
46
|
+
* SESSION_ID/RECORD_ID 不在本提取面(它们是父身份归属键,引擎子进程 env 由
|
|
47
|
+
* SDK buildEngineChildEnv L1 deny 剥除、引擎按 run.params.ctx 重写,不靠 env 继承)。
|
|
48
|
+
*
|
|
49
|
+
* 消费方:D8 createZcodeEngine 薄壳(zsw 宿主链路的 relay 透传);宿主 env 形态
|
|
50
|
+
* 与 SDK EngineRelayEnv 结构等价(字段名 socket/node/script),TS 结构类型直接赋值。
|
|
51
|
+
*/
|
|
52
|
+
export function readRelayForwardEnv(
|
|
53
|
+
env: NodeJS.ProcessEnv | Record<string, string | undefined>,
|
|
54
|
+
): { socket: string; node: string; script: string } | undefined {
|
|
55
|
+
const socket = env[RELAY_ENV_SOCKET];
|
|
56
|
+
const node = env[RELAY_ENV_NODE];
|
|
57
|
+
const script = env[RELAY_ENV_SCRIPT];
|
|
58
|
+
if (!isRelayActive(env)) return undefined;
|
|
59
|
+
return { socket: socket as string, node: node as string, script: script as string };
|
|
60
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// src/schema-emulation.ts
|
|
2
|
+
//
|
|
3
|
+
// schema 仿真降级(引擎侧原语,自 core execution/engine/common/schema-emulation.ts
|
|
4
|
+
// 迁入 @zhushanwen/subagent-engine-sdk,实现体逐字等价)。迁移处置:无 core 内部
|
|
5
|
+
// 依赖 → 直接搬(impl-plan §2.1 原语迁移处置表)。设计权威源:
|
|
6
|
+
// docs/architecture/subagent-engine-abstraction.md D4(native/emulated 硬分流)+ §3.3.3
|
|
7
|
+
// schema_emulation_failed 行 + §5 P2 行。
|
|
8
|
+
//
|
|
9
|
+
// ─── D4 硬分流(违反 = 历史事故形态) ───────────────────────────────
|
|
10
|
+
// 本模块只服务 capabilities.schemaEnforcement === 'emulated' 的引擎
|
|
11
|
+
// (zcode / opencode / kimi-code)。capabilities.schemaEnforcement === 'native' 的引擎
|
|
12
|
+
// (pi 的 PI_WORKFLOW_SCHEMA env 注入链路、claude-code --json-schema、codex
|
|
13
|
+
// --output-schema)**禁止** import 本模块做二次校验或改写其结果——宿主侧再叠一层
|
|
14
|
+
// ajv 会制造第二校验权威,恰是 structured-output 方案 A [HISTORICAL]
|
|
15
|
+
// (2026-08-01「校验自报 schema 致修复静默丢失」事故)的形态。
|
|
16
|
+
// ajv 只允许出现在 emulated 路径(本模块),这是全仓唯一例外。
|
|
17
|
+
//
|
|
18
|
+
// 重试语义:本模块两个函数保持纯——校验失败/提取失败由调用方(宿主编排层)决定
|
|
19
|
+
// 重试,语义与 structured-output 的 workflow-hook 对齐(重试一次、强化 prompt;
|
|
20
|
+
// 仍失败报 schema_emulation_failed,错误含原始输出尾部)。
|
|
21
|
+
//
|
|
22
|
+
// 文案对齐:错误回显风格对齐 structured-output 的 execute.ts("Schema validation
|
|
23
|
+
// failed: ..."),但不 import 其内部模块(公共层自实现;structured-output 是
|
|
24
|
+
// peer 依赖且可选,import 会造成公共层对 sibling extension 的硬依赖)。
|
|
25
|
+
|
|
26
|
+
import Ajv, { type ValidateFunction } from "ajv";
|
|
27
|
+
|
|
28
|
+
/** 原始输出尾部回显长度(结果对象的 tail 字段,供错误展示与重试 prompt 回灌)。 */
|
|
29
|
+
export const SCHEMA_EMULATION_TAIL_CHARS = 500;
|
|
30
|
+
|
|
31
|
+
// ============================================================
|
|
32
|
+
// prompt 注入段
|
|
33
|
+
// ============================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 构造 prompt 注入段:schema 声明 + 输出格式约定(emulated 引擎的 launcher 把它拼进
|
|
37
|
+
* 最终 prompt,替代引擎不存在的 schema 强制通道)。
|
|
38
|
+
*
|
|
39
|
+
* 文案风格对齐 structured-output 的 promptGuidelines("Do not output JSON in text":emulated 引擎没有工具可调,JSON 必须出现在文本里)。
|
|
40
|
+
*/
|
|
41
|
+
export function buildSchemaEmulationSegment(schema: object): string {
|
|
42
|
+
const schemaJson = JSON.stringify(schema);
|
|
43
|
+
return [
|
|
44
|
+
"## Structured Output Requirement",
|
|
45
|
+
"Your final answer MUST contain exactly one JSON value conforming to this JSON Schema (draft-07):",
|
|
46
|
+
schemaJson,
|
|
47
|
+
"Output rules:",
|
|
48
|
+
"- Output ONLY the JSON value — no prose before or after it.",
|
|
49
|
+
"- If you wrap it in a markdown code fence, use a single ```json fence.",
|
|
50
|
+
"- Do not output multiple JSON values; the first complete JSON value is extracted.",
|
|
51
|
+
].join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================
|
|
55
|
+
// 三级容错提取 + ajv 校验
|
|
56
|
+
// ============================================================
|
|
57
|
+
|
|
58
|
+
/** 提取结果联合:ok=true 携带通过校验的 parsed;ok=false 携带 error 简述 + 原始输出尾部。 */
|
|
59
|
+
export type StructuredOutputResult =
|
|
60
|
+
| { ok: true; parsed: unknown }
|
|
61
|
+
| { ok: false; error: string; tail: string };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 从模型文本输出提取并校验结构化 JSON(三级容错 → ajv)。
|
|
65
|
+
*
|
|
66
|
+
* 三级容错(逐级降级,任一级提取成功即进 ajv):
|
|
67
|
+
* 1. 直接 JSON.parse(trim 后整体是合法 JSON);
|
|
68
|
+
* 2. 剥 markdown code fence(```json ... ``` / ``` ... ```)取围栏内内容;
|
|
69
|
+
* 3. 首尾括号扫描(首个 '{'/'[' 到末个 '}'/']' 的子串——容纳前后杂文本)。
|
|
70
|
+
*
|
|
71
|
+
* ajv 校验失败/三级提取失败返回 ok:false(不 throw)——重试与否是宿主编排层的
|
|
72
|
+
* 决策(见文件头重试语义),本函数每次调用独立无状态。
|
|
73
|
+
*/
|
|
74
|
+
export function extractAndValidateStructuredOutput(
|
|
75
|
+
text: string,
|
|
76
|
+
schema: object,
|
|
77
|
+
): { ok: true; parsed: unknown } | { ok: false; error: string; tail: string } {
|
|
78
|
+
return extractImpl(text, schema);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 实现体(签名拆分只为让导出签名与契约声明逐字一致,见 StructuredOutputResult)。 */
|
|
82
|
+
function extractImpl(text: string, schema: object): StructuredOutputResult {
|
|
83
|
+
const tail = text.length > SCHEMA_EMULATION_TAIL_CHARS
|
|
84
|
+
? text.slice(text.length - SCHEMA_EMULATION_TAIL_CHARS)
|
|
85
|
+
: text;
|
|
86
|
+
|
|
87
|
+
const extracted = extractJsonCandidate(text);
|
|
88
|
+
if (extracted === undefined) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
error:
|
|
92
|
+
"could not extract JSON from model output after 3-stage fallback " +
|
|
93
|
+
"(direct parse -> code-fence strip -> bracket scan)",
|
|
94
|
+
tail,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const validate = getOrCompileValidator(schema);
|
|
99
|
+
if (validate === undefined) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: "host-side JSON Schema compilation failed (invalid schema object)",
|
|
103
|
+
tail,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const valid = validate(extracted);
|
|
108
|
+
if (!valid) {
|
|
109
|
+
// 错误格式对齐 structured-output:"instancePath message" 逐条 join
|
|
110
|
+
const errors = validate.errors?.map((err) => `${err.instancePath} ${err.message}`).join("; ");
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
error: `Schema validation failed: ${errors ?? "(no detail)"}`,
|
|
114
|
+
tail,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return { ok: true, parsed: extracted };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── 内部:三级提取 ──────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/** 三级容错提取 JSON candidate;全部失败返回 undefined。 */
|
|
123
|
+
function extractJsonCandidate(text: string): unknown {
|
|
124
|
+
// 第 1 级:整体直接 parse(最常见——严格遵守输出约定的模型)
|
|
125
|
+
const direct = tryParse(text.trim());
|
|
126
|
+
if (direct.ok) return direct.value;
|
|
127
|
+
|
|
128
|
+
// 第 2 级:markdown code fence 剥离(模型爱用 ```json 包裹)
|
|
129
|
+
const fenced = extractFirstFencedBlock(text);
|
|
130
|
+
if (fenced !== undefined) {
|
|
131
|
+
const parsed = tryParse(fenced.trim());
|
|
132
|
+
if (parsed.ok) return parsed.value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 第 3 级:首尾括号扫描(前后杂文本:"Here is the result: {...} Hope this helps")
|
|
136
|
+
const scanned = extractByBracketScan(text);
|
|
137
|
+
if (scanned !== undefined) {
|
|
138
|
+
const parsed = tryParse(scanned.trim());
|
|
139
|
+
if (parsed.ok) return parsed.value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** JSON.parse 包装:失败返回 ok:false(malformed 不是异常态,是三级降级的输入)。 */
|
|
146
|
+
function tryParse(raw: string): { ok: true; value: unknown } | { ok: false } {
|
|
147
|
+
if (raw === "") return { ok: false };
|
|
148
|
+
try {
|
|
149
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
150
|
+
} catch {
|
|
151
|
+
return { ok: false };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 取第一个 markdown code fence 块内容(```json / ``` 均可);无 fence 返回 undefined。 */
|
|
156
|
+
function extractFirstFencedBlock(text: string): string | undefined {
|
|
157
|
+
const match = /```(?:json)?\s*([\s\S]*?)\s*```/.exec(text);
|
|
158
|
+
return match?.[1];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 首尾括号扫描:首个 '{'/'[' 到末个 '}'/']' 的子串;边界不合法返回 undefined。 */
|
|
162
|
+
function extractByBracketScan(text: string): string | undefined {
|
|
163
|
+
const objOpen = text.indexOf("{");
|
|
164
|
+
const arrOpen = text.indexOf("[");
|
|
165
|
+
const open = objOpen === -1 ? arrOpen : arrOpen === -1 ? objOpen : Math.min(objOpen, arrOpen);
|
|
166
|
+
const objClose = text.lastIndexOf("}");
|
|
167
|
+
const arrClose = text.lastIndexOf("]");
|
|
168
|
+
const close = Math.max(objClose, arrClose);
|
|
169
|
+
if (open === -1 || close === -1 || open >= close) return undefined;
|
|
170
|
+
return text.slice(open, close + 1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── 内部:ajv 编译缓存 ──────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
// WeakMap 缓存(schema 对象引用即 key,GC 友好)——模式对齐 structured-output 的
|
|
176
|
+
// ajv-validator.ts(自实现,不跨包 import;见文件头依赖边界说明)。
|
|
177
|
+
// CJS 多 entry 内联副本的实例分裂影响 = 缓存 miss 重新编译,无语义后果。
|
|
178
|
+
const ajvCache = new WeakMap<object, ValidateFunction>();
|
|
179
|
+
|
|
180
|
+
/** 编译(或取缓存)validator;schema 非法(ajv 抛错)返回 undefined 由调用方报错。 */
|
|
181
|
+
function getOrCompileValidator(schema: object): ValidateFunction | undefined {
|
|
182
|
+
const cached = ajvCache.get(schema);
|
|
183
|
+
if (cached) return cached;
|
|
184
|
+
try {
|
|
185
|
+
// strict:false 对齐 structured-output 的宽容度(非标 keyword 不炸编译)
|
|
186
|
+
const validate = new Ajv({ strict: false }).compile(schema);
|
|
187
|
+
ajvCache.set(schema, validate);
|
|
188
|
+
return validate;
|
|
189
|
+
} catch {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|