@lqc123qwe/car-runtime 1.13.0 → 1.14.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/README.md +3 -2
- package/dist/SHA256SUMS +1 -1
- package/dist/car-runtime-1.14.0.tgz +0 -0
- package/dist/car-runtime-1.14.0.tgz.sig.bundle +1 -0
- package/dist/cli.js +48 -16
- package/dist/load/config.d.ts +45 -0
- package/dist/load/config.js +84 -1
- package/dist/load/pool.d.ts +97 -0
- package/dist/load/pool.js +261 -0
- package/dist/loop/stop.js +9 -1
- package/dist/release-audit.jsonl +4 -4
- package/dist/runtime-core/chatStep.js +2 -0
- package/dist/runtime-core/credentials.d.ts +2 -0
- package/dist/runtime-core/credentials.js +2 -1
- package/dist/runtime-core/llm.d.ts +6 -0
- package/dist/runtime-core/llm.js +54 -3
- package/dist/runtime-core/types.d.ts +15 -0
- package/dist/sbom.json +22 -12
- package/dist/seam.d.ts +7 -0
- package/dist/seam.js +8 -0
- package/dist/session/compact.d.ts +57 -0
- package/dist/session/compact.js +128 -0
- package/dist/session/log.d.ts +5 -1
- package/dist/session/log.js +22 -2
- package/package.json +1 -1
- package/src/cli.ts +48 -17
- package/src/load/config.ts +135 -1
- package/src/load/pool.ts +339 -0
- package/src/loop/stop.ts +8 -2
- package/src/runtime-core/chatStep.ts +2 -0
- package/src/runtime-core/credentials.ts +4 -1
- package/src/runtime-core/llm.ts +59 -3
- package/src/runtime-core/types.ts +11 -0
- package/src/seam.ts +8 -0
- package/src/session/compact.ts +168 -0
- package/src/session/log.ts +19 -3
- package/dist/car-runtime-1.13.0.tgz +0 -0
- package/dist/car-runtime-1.13.0.tgz.sig.bundle +0 -1
package/src/load/pool.ts
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1.14-S4 · K-POOL-1 模型池网关(D-42:登记 + health 探测 + fallback 链 + retry 策略 + routing/decide 落链)
|
|
3
|
+
*
|
|
4
|
+
* 口径:
|
|
5
|
+
* - 候选序 = priority 升序(缺省 0)→ weight 降序(缺省 0;D-42:weight 仅作排序 tie-break,
|
|
6
|
+
* 不做负载均衡调度)→ 登记序(稳定排序);
|
|
7
|
+
* - health 预检(doctor 面同款口径——值不打印):GET {baseUrl}/models(openai)/
|
|
8
|
+
* {baseUrl}/v1/models(anthropic);**任意 HTTP 状态码 = 传输可达(健康)**——404/401 ≠ 不健康
|
|
9
|
+
* (真实连通性以发消息为准,doctor 先例);网络层失败/超时 = 不健康(按序跳过,fail-visible
|
|
10
|
+
* 落 routing/decide attempts);结果 TTL 缓存(防死主端点每请求烧重试预算);probeImpl 测试注入缝;
|
|
11
|
+
* - fallback 链 = **首块前失败才可降级**(已消费 chunk 不可重放——llm.ts §3.5.4 同红线):
|
|
12
|
+
* 成员请求抛错(网络/429/5xx 重试耗尽/凭据缺失)或首 chunk 即 error chunk(4xx 业务错,零内容
|
|
13
|
+
* 消费)→ 按序降级下一候选;流中途失败 = error chunk 原样收口不降级;
|
|
14
|
+
* - retry 策略 = 成员级重试(§3.5.4 基线,装配层透传)+ 池级 maxAttempts 候选请求尝试上限
|
|
15
|
+
* (缺省 = 端点数;耗尽 = providerUnreachable B080001——runTurn 以 error 收口);
|
|
16
|
+
* - **routing/decide 审计事件**:每次决策一条(payload.type='decide':selected/reason/retries/
|
|
17
|
+
* attempts)——onDecide 回调由宿主接线 SessionLog.append(事件链只追加红线;禁静默换模型);
|
|
18
|
+
* retries = 发起请求且失败的候选数(健康跳过不计,attempts 全量在册);
|
|
19
|
+
* - 成员请求 req.model/maxTokens 按成员改写(成员适配器消费自身 model——usage 按成员模型落账);
|
|
20
|
+
* - 无池配置 = 行为与 1.13 逐字节一致(兼容验收线——池路由零介入);
|
|
21
|
+
* - 明文凭据红线:成员凭据仅收 keyEnv env 变量名引用(config schema 明文键显式拒绝,CR-05)。
|
|
22
|
+
*/
|
|
23
|
+
import { request as httpRequest } from 'node:http'
|
|
24
|
+
import { request as httpsRequest } from 'node:https'
|
|
25
|
+
import type { ModelPoolConfig } from './config.ts'
|
|
26
|
+
import { createAnthropicAdapter, createOpenAICompatAdapter } from '../runtime-core/llm.ts'
|
|
27
|
+
import { CredentialService } from '../runtime-core/credentials.ts'
|
|
28
|
+
import { providerUnreachable } from '../runtime-core/errors.ts'
|
|
29
|
+
import type { LlmAdapter, LlmChunk, LlmRequest } from '../runtime-core/types.ts'
|
|
30
|
+
|
|
31
|
+
/** 池成员(解析后形态——id 缺省已派生;候选序已定) */
|
|
32
|
+
export interface PoolMember {
|
|
33
|
+
id: string
|
|
34
|
+
baseUrl: string
|
|
35
|
+
model: string
|
|
36
|
+
adapterId: 'openai-compat' | 'anthropic'
|
|
37
|
+
keyEnv?: string
|
|
38
|
+
maxTokens?: number
|
|
39
|
+
priority?: number
|
|
40
|
+
weight?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 候选序:priority 升序(缺省 0)→ weight 降序(缺省 0,D-42 tie-break)→ 登记序(稳定) */
|
|
44
|
+
export function orderPoolMembers(members: PoolMember[]): PoolMember[] {
|
|
45
|
+
return members
|
|
46
|
+
.map((m, i) => ({ m, i }))
|
|
47
|
+
.sort((a, b) => (a.m.priority ?? 0) - (b.m.priority ?? 0) || (b.m.weight ?? 0) - (a.m.weight ?? 0) || a.i - b.i)
|
|
48
|
+
.map(x => x.m)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** config modelPool → 池成员(id 缺省 ep-1..N 派生;重复 id 显式拒绝;候选序就位) */
|
|
52
|
+
export function resolveModelPoolMembers(pool: ModelPoolConfig): { members: PoolMember[]; error?: string } {
|
|
53
|
+
const seen = new Set<string>()
|
|
54
|
+
const members: PoolMember[] = []
|
|
55
|
+
for (const [i, ep] of pool.endpoints.entries()) {
|
|
56
|
+
const id = ep.id ?? `ep-${i + 1}`
|
|
57
|
+
if (seen.has(id)) {
|
|
58
|
+
return { members: [], error: `CAR-E-CONFIG: 模型池端点 id "${id}" 重复(routing/decide 落链定位歧义禁入)` }
|
|
59
|
+
}
|
|
60
|
+
seen.add(id)
|
|
61
|
+
members.push({
|
|
62
|
+
id,
|
|
63
|
+
baseUrl: ep.baseUrl,
|
|
64
|
+
model: ep.model,
|
|
65
|
+
adapterId: ep.adapterId ?? 'openai-compat',
|
|
66
|
+
...(ep.keyEnv !== undefined ? { keyEnv: ep.keyEnv } : {}),
|
|
67
|
+
...(ep.maxTokens !== undefined ? { maxTokens: ep.maxTokens } : {}),
|
|
68
|
+
...(ep.priority !== undefined ? { priority: ep.priority } : {}),
|
|
69
|
+
...(ep.weight !== undefined ? { weight: ep.weight } : {}),
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
return { members: orderPoolMembers(members) }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ==================== health 预检(doctor 面同款口径) ====================
|
|
76
|
+
|
|
77
|
+
export interface PoolMemberHealth {
|
|
78
|
+
/** 传输可达(任意 HTTP 状态码)= true;网络层失败/超时 = false */
|
|
79
|
+
ok: boolean
|
|
80
|
+
status?: number
|
|
81
|
+
latencyMs: number
|
|
82
|
+
/** 不健康原因类别(值不打印纪律——只出类别) */
|
|
83
|
+
errorCategory?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** node:http(s) GET 状态码(1.8-BUG-1 出站通道口径——全局 fetch/undici win32 退出断言预防) */
|
|
87
|
+
function httpGetStatus(url: string, signal: AbortSignal): Promise<number> {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
try {
|
|
90
|
+
const u = new URL(url)
|
|
91
|
+
const mod = u.protocol === 'https:' ? httpsRequest : httpRequest
|
|
92
|
+
const req = mod(u, { method: 'GET', headers: { accept: 'application/json' } }, res => {
|
|
93
|
+
res.resume() // 排干 body——连接收口
|
|
94
|
+
resolve(res.statusCode ?? 0)
|
|
95
|
+
})
|
|
96
|
+
const onAbort = () => { try { req.destroy(new Error('aborted')) } catch { /* 已完成 */ } }
|
|
97
|
+
if (signal.aborted) onAbort()
|
|
98
|
+
else signal.addEventListener('abort', onAbort, { once: true })
|
|
99
|
+
req.on('error', reject)
|
|
100
|
+
req.end()
|
|
101
|
+
} catch (e) { reject(e) }
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 单端点 health 探测(doctor/池预检复用;值不打印——只出状态/延迟/类别) */
|
|
106
|
+
export async function probePoolMember(
|
|
107
|
+
member: Pick<PoolMember, 'baseUrl' | 'adapterId'>,
|
|
108
|
+
opts: { timeoutMs?: number } = {},
|
|
109
|
+
): Promise<PoolMemberHealth> {
|
|
110
|
+
const base = member.baseUrl.replace(/\/+$/, '')
|
|
111
|
+
const url = member.adapterId === 'anthropic' ? `${base}/v1/models` : `${base}/models`
|
|
112
|
+
const t0 = Date.now()
|
|
113
|
+
const controller = new AbortController()
|
|
114
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 4000)
|
|
115
|
+
try {
|
|
116
|
+
const status = await httpGetStatus(url, controller.signal)
|
|
117
|
+
return { ok: true, status, latencyMs: Date.now() - t0 }
|
|
118
|
+
} catch (e) {
|
|
119
|
+
const aborted = (e as Error).name === 'AbortError' || String((e as Error).message).includes('aborted')
|
|
120
|
+
return { ok: false, latencyMs: Date.now() - t0, errorCategory: aborted ? 'timeout' : 'network-error' }
|
|
121
|
+
} finally {
|
|
122
|
+
clearTimeout(timer)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ==================== 路由引擎(fallback 链 + retry 上限 + routing/decide) ====================
|
|
127
|
+
|
|
128
|
+
/** 逐候选结果(attempts 全量在册——健康跳过与请求失败都显性化) */
|
|
129
|
+
export interface RoutingAttempt {
|
|
130
|
+
id: string
|
|
131
|
+
outcome: 'selected' | 'failed' | 'skipped-unhealthy'
|
|
132
|
+
/** 失败/跳过类别(错误码/名称,值不打印) */
|
|
133
|
+
detail?: string
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 路由决策(routing/decide 事件 payload 源——每次请求一条) */
|
|
137
|
+
export interface RoutingDecision {
|
|
138
|
+
/** 选中端点 id(耗尽 = null) */
|
|
139
|
+
selected: string | null
|
|
140
|
+
/** 首位候选选中 = primary;降级选中 = fallback;全部不可用 = exhausted */
|
|
141
|
+
reason: 'primary' | 'fallback' | 'exhausted'
|
|
142
|
+
/** 降级重试次数 = 发起请求且失败的候选数(健康跳过不计——attempts 全量在册) */
|
|
143
|
+
retries: number
|
|
144
|
+
attempts: RoutingAttempt[]
|
|
145
|
+
turnId?: string
|
|
146
|
+
sessionId?: string
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface PoolAdapterOptions {
|
|
150
|
+
id?: string
|
|
151
|
+
/** 成员与适配器(装配层产出的候选序) */
|
|
152
|
+
members: Array<{ member: PoolMember; adapter: LlmAdapter }>
|
|
153
|
+
/** health 预检(enabled 缺省 true;probeImpl 测试注入缝——缺省真实 GET 探测) */
|
|
154
|
+
probe?: { enabled?: boolean; timeoutMs?: number; ttlMs?: number; probeImpl?: typeof probePoolMember }
|
|
155
|
+
/** 候选请求尝试上限(缺省 = 成员数) */
|
|
156
|
+
maxAttempts?: number
|
|
157
|
+
/** routing/decide 审计回调(宿主接线 SessionLog.append——禁静默换模型红线) */
|
|
158
|
+
onDecide?: (d: RoutingDecision) => void
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const errorCategory = (e: unknown): string => {
|
|
162
|
+
const err = e as { slug?: string; name?: string }
|
|
163
|
+
return err.slug ?? err.name ?? 'unknown'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function createModelPoolAdapter(opts: PoolAdapterOptions): LlmAdapter & { id: string; members: PoolMember[] } {
|
|
167
|
+
const ordered = opts.members
|
|
168
|
+
const maxAttempts = opts.maxAttempts ?? ordered.length
|
|
169
|
+
const probeEnabled = opts.probe?.enabled !== false
|
|
170
|
+
const probeFn = opts.probe?.probeImpl ?? probePoolMember
|
|
171
|
+
const probeTimeoutMs = opts.probe?.timeoutMs ?? 4000
|
|
172
|
+
const ttlMs = opts.probe?.ttlMs ?? 30_000
|
|
173
|
+
const healthCache = new Map<string, { ok: boolean; at: number; category?: string }>()
|
|
174
|
+
const id = opts.id ?? 'model-pool'
|
|
175
|
+
|
|
176
|
+
const healthOf = async (m: PoolMember): Promise<{ ok: boolean; category?: string }> => {
|
|
177
|
+
const hit = healthCache.get(m.id)
|
|
178
|
+
if (hit && Date.now() - hit.at < ttlMs) {
|
|
179
|
+
return { ok: hit.ok, ...(hit.category !== undefined ? { category: hit.category } : {}) }
|
|
180
|
+
}
|
|
181
|
+
const h = await probeFn(m, { timeoutMs: probeTimeoutMs })
|
|
182
|
+
const entry = { ok: h.ok, at: Date.now(), ...(h.errorCategory !== undefined ? { category: h.errorCategory } : {}) }
|
|
183
|
+
healthCache.set(m.id, entry)
|
|
184
|
+
return { ok: entry.ok, ...(entry.category !== undefined ? { category: entry.category } : {}) }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const withMeta = (d: Omit<RoutingDecision, 'turnId' | 'sessionId'>, req: LlmRequest): RoutingDecision => ({
|
|
188
|
+
...d,
|
|
189
|
+
...(req.metadata?.turnId !== undefined ? { turnId: req.metadata.turnId } : {}),
|
|
190
|
+
...(req.metadata?.sessionId !== undefined ? { sessionId: req.metadata.sessionId } : {}),
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
id,
|
|
195
|
+
members: ordered.map(x => x.member),
|
|
196
|
+
chat(req: LlmRequest): AsyncIterable<LlmChunk> {
|
|
197
|
+
return (async function* () {
|
|
198
|
+
const attempts: RoutingAttempt[] = []
|
|
199
|
+
let failedCount = 0
|
|
200
|
+
let requestAttempts = 0
|
|
201
|
+
for (const [pos, entry] of ordered.entries()) {
|
|
202
|
+
if (requestAttempts >= maxAttempts) break
|
|
203
|
+
if (probeEnabled) {
|
|
204
|
+
const h = await healthOf(entry.member)
|
|
205
|
+
if (!h.ok) {
|
|
206
|
+
attempts.push({ id: entry.member.id, outcome: 'skipped-unhealthy', ...(h.category !== undefined ? { detail: h.category } : {}) })
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
requestAttempts++
|
|
211
|
+
// 成员请求:req.model/maxTokens 按成员改写(usage 按成员模型落账)
|
|
212
|
+
let it: AsyncIterator<LlmChunk>
|
|
213
|
+
try {
|
|
214
|
+
it = entry.adapter.chat({
|
|
215
|
+
...req,
|
|
216
|
+
model: entry.member.model,
|
|
217
|
+
...(entry.member.maxTokens != null ? { maxTokens: entry.member.maxTokens } : {}),
|
|
218
|
+
})[Symbol.asyncIterator]()
|
|
219
|
+
} catch (e) {
|
|
220
|
+
// 适配器同步抛错(契约违规/装配错误)= 候选不可用,降级下一候选
|
|
221
|
+
attempts.push({ id: entry.member.id, outcome: 'failed', detail: errorCategory(e) })
|
|
222
|
+
failedCount++
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
let first: IteratorResult<LlmChunk>
|
|
227
|
+
try {
|
|
228
|
+
first = await it.next()
|
|
229
|
+
} catch (e) {
|
|
230
|
+
// 首块前失败(网络/429/5xx 重试耗尽/凭据缺失)→ 降级下一候选
|
|
231
|
+
attempts.push({ id: entry.member.id, outcome: 'failed', detail: errorCategory(e) })
|
|
232
|
+
failedCount++
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
if (first.done || !first.value) {
|
|
236
|
+
attempts.push({ id: entry.member.id, outcome: 'failed', detail: 'empty-stream' })
|
|
237
|
+
failedCount++
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
const fc = first.value
|
|
241
|
+
// 首 chunk 即 error chunk 且零内容消费(4xx 业务错)→ 候选不可用,降级(零 chunk 已出——可重放安全)
|
|
242
|
+
const errorOnly = fc.finishReason === 'error' && fc.delta === undefined && fc.toolCallDelta === undefined
|
|
243
|
+
if (errorOnly) {
|
|
244
|
+
attempts.push({ id: entry.member.id, outcome: 'failed', detail: fc.error ? 'http-4xx' : 'error-chunk' })
|
|
245
|
+
failedCount++
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
// 选中:routing/decide 落链(禁静默换模型——选中端点/原因/重试次数显性化)
|
|
249
|
+
attempts.push({ id: entry.member.id, outcome: 'selected' })
|
|
250
|
+
opts.onDecide?.(withMeta({
|
|
251
|
+
selected: entry.member.id,
|
|
252
|
+
reason: pos === 0 ? 'primary' : 'fallback',
|
|
253
|
+
retries: failedCount,
|
|
254
|
+
attempts,
|
|
255
|
+
}, req))
|
|
256
|
+
yield fc
|
|
257
|
+
while (true) {
|
|
258
|
+
const r = await it.next()
|
|
259
|
+
if (r.done) break
|
|
260
|
+
yield r.value // 流中途失败 = error chunk 原样收口(已消费 chunk 不可重放——不降级)
|
|
261
|
+
}
|
|
262
|
+
return
|
|
263
|
+
} finally {
|
|
264
|
+
// 成员迭代器收口(首 chunk 后未耗尽路径——4xx 降级/取消/异常;自然结束路径 no-op)
|
|
265
|
+
await it.return?.(undefined)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// 耗尽:决策落链 + B080001(runTurn error 收口——BD-04)
|
|
269
|
+
opts.onDecide?.(withMeta({ selected: null, reason: 'exhausted', retries: failedCount, attempts }, req))
|
|
270
|
+
const summary = attempts.map(a => `${a.id}:${a.outcome}${a.detail ? `(${a.detail})` : ''}`).join(', ') || '无候选'
|
|
271
|
+
throw providerUnreachable(`模型池全部候选不可用(${summary})`)
|
|
272
|
+
})()
|
|
273
|
+
},
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ==================== 池装配层(config → 成员适配器 → 路由适配器) ====================
|
|
278
|
+
|
|
279
|
+
export interface ModelPoolAssemblyOptions {
|
|
280
|
+
pool: ModelPoolConfig
|
|
281
|
+
/** 共享凭据服务(成员级 keyEnv 经 credentialEnvVars 精确候选) */
|
|
282
|
+
credentials?: CredentialService
|
|
283
|
+
/** 测试注入(成员适配器 fetch/退避——缺省真实通道) */
|
|
284
|
+
fetchImpl?: typeof fetch
|
|
285
|
+
timeoutMs?: number
|
|
286
|
+
firstByteMs?: number
|
|
287
|
+
retries?: number
|
|
288
|
+
backoffMs?: number[]
|
|
289
|
+
sleep?: (ms: number) => Promise<void>
|
|
290
|
+
/** routing/decide 审计回调(透传路由引擎) */
|
|
291
|
+
onDecide?: (d: RoutingDecision) => void
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** 池装配:config 解析(候选序/重复拒绝)→ 成员适配器(内置两族 + keyEnv)→ 路由适配器 */
|
|
295
|
+
export function assembleModelPool(opts: ModelPoolAssemblyOptions): {
|
|
296
|
+
adapter: LlmAdapter & { id: string; members: PoolMember[] }
|
|
297
|
+
members: PoolMember[]
|
|
298
|
+
} {
|
|
299
|
+
const { members, error } = resolveModelPoolMembers(opts.pool)
|
|
300
|
+
if (error) throw new Error(error)
|
|
301
|
+
const entries = members.map(m => ({
|
|
302
|
+
member: m,
|
|
303
|
+
adapter: (m.adapterId === 'anthropic'
|
|
304
|
+
? createAnthropicAdapter({
|
|
305
|
+
baseUrl: m.baseUrl,
|
|
306
|
+
credentials: opts.credentials,
|
|
307
|
+
...(m.keyEnv !== undefined ? { credentialEnvVars: [m.keyEnv] } : {}),
|
|
308
|
+
...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
|
|
309
|
+
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
310
|
+
...(opts.firstByteMs !== undefined ? { firstByteMs: opts.firstByteMs } : {}),
|
|
311
|
+
...(opts.retries !== undefined ? { retries: opts.retries } : {}),
|
|
312
|
+
...(opts.backoffMs !== undefined ? { backoffMs: opts.backoffMs } : {}),
|
|
313
|
+
...(opts.sleep ? { sleep: opts.sleep } : {}),
|
|
314
|
+
})
|
|
315
|
+
: createOpenAICompatAdapter({
|
|
316
|
+
baseUrl: m.baseUrl,
|
|
317
|
+
credentials: opts.credentials,
|
|
318
|
+
provider: 'openai-compat',
|
|
319
|
+
...(m.keyEnv !== undefined ? { credentialEnvVars: [m.keyEnv] } : {}),
|
|
320
|
+
...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
|
|
321
|
+
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
322
|
+
...(opts.firstByteMs !== undefined ? { firstByteMs: opts.firstByteMs } : {}),
|
|
323
|
+
...(opts.retries !== undefined ? { retries: opts.retries } : {}),
|
|
324
|
+
...(opts.backoffMs !== undefined ? { backoffMs: opts.backoffMs } : {}),
|
|
325
|
+
...(opts.sleep ? { sleep: opts.sleep } : {}),
|
|
326
|
+
})),
|
|
327
|
+
}))
|
|
328
|
+
const adapter = createModelPoolAdapter({
|
|
329
|
+
members: entries,
|
|
330
|
+
probe: {
|
|
331
|
+
...(opts.pool.probe?.enabled !== undefined ? { enabled: opts.pool.probe.enabled } : {}),
|
|
332
|
+
...(opts.pool.probe?.timeoutMs !== undefined ? { timeoutMs: opts.pool.probe.timeoutMs } : {}),
|
|
333
|
+
...(opts.pool.probe?.ttlMs !== undefined ? { ttlMs: opts.pool.probe.ttlMs } : {}),
|
|
334
|
+
},
|
|
335
|
+
...(opts.pool.maxAttempts !== undefined ? { maxAttempts: opts.pool.maxAttempts } : {}),
|
|
336
|
+
...(opts.onDecide ? { onDecide: opts.onDecide } : {}),
|
|
337
|
+
})
|
|
338
|
+
return { adapter, members }
|
|
339
|
+
}
|
package/src/loop/stop.ts
CHANGED
|
@@ -72,12 +72,13 @@ export class TurnAborted extends Error {
|
|
|
72
72
|
* 在 JSONL,聚合由事实行求和而非运行时状态;字段非数值容错跳过不吞计数)。无 usage 事件
|
|
73
73
|
* 返回 undefined → turnEnd meta 不带 usage 键(缺省=未采集,旧格式向后兼容)。
|
|
74
74
|
*/
|
|
75
|
-
function turnUsageFrom(log: SessionLog, turnId: string): { input: number; output: number; cacheRead: number; cacheCreation: number; requests: number } | undefined {
|
|
75
|
+
function turnUsageFrom(log: SessionLog, turnId: string): { input: number; output: number; cacheRead: number; cacheCreation: number; requests: number; segments?: { reasoningTokens: number } } | undefined {
|
|
76
76
|
let input = 0
|
|
77
77
|
let output = 0
|
|
78
78
|
let cacheRead = 0
|
|
79
79
|
let cacheCreation = 0
|
|
80
80
|
let requests = 0
|
|
81
|
+
let reasoning: number | undefined
|
|
81
82
|
for (const e of log.events) {
|
|
82
83
|
if (e.kind !== 'usage' || e.turnId !== turnId) continue
|
|
83
84
|
const p = (e.payload ?? {}) as Record<string, unknown>
|
|
@@ -85,9 +86,14 @@ function turnUsageFrom(log: SessionLog, turnId: string): { input: number; output
|
|
|
85
86
|
if (typeof p.output === 'number') output += p.output
|
|
86
87
|
if (typeof p.cacheRead === 'number') cacheRead += p.cacheRead
|
|
87
88
|
if (typeof p.cacheCreation === 'number') cacheCreation += p.cacheCreation
|
|
89
|
+
// 1.14-S3(K-MP-2b / D-41):分段聚合——任一请求携带 segments.reasoningTokens 即求和;
|
|
90
|
+
// 全 turn 无分段事实 = meta.usage 不带 segments 键(禁估算补造)
|
|
91
|
+
const seg = (p.segments ?? null) as { reasoningTokens?: unknown } | null
|
|
92
|
+
if (seg && typeof seg.reasoningTokens === 'number') reasoning = (reasoning ?? 0) + seg.reasoningTokens
|
|
88
93
|
requests++
|
|
89
94
|
}
|
|
90
|
-
|
|
95
|
+
if (!requests) return undefined
|
|
96
|
+
return { input, output, cacheRead, cacheCreation, requests, ...(reasoning !== undefined ? { segments: { reasoningTokens: reasoning } } : {}) }
|
|
91
97
|
}
|
|
92
98
|
|
|
93
99
|
export async function runTurn(opts: {
|
|
@@ -117,6 +117,8 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
|
|
|
117
117
|
output: usage.outputTokens,
|
|
118
118
|
cacheRead: usage.cacheReadTokens,
|
|
119
119
|
cacheCreation: usage.cacheCreationTokens,
|
|
120
|
+
// 1.14-S3(K-MP-2b / D-41):分段事实随事件落链——无分段 = 不带键(禁估算)
|
|
121
|
+
...(usage.segments ? { segments: usage.segments } : {}),
|
|
120
122
|
providerRaw: usage.providerRaw,
|
|
121
123
|
})
|
|
122
124
|
}
|
|
@@ -28,6 +28,8 @@ export interface CredentialResolveOptions {
|
|
|
28
28
|
keychainReader?: (service: string) => string | null
|
|
29
29
|
/** reveal 审计回调(值不入参——审计全量留痕但零明文) */
|
|
30
30
|
audit?: (event: { action: 'resolve' | 'reveal'; provider: string; source: CredentialSource; origin: string }) => void
|
|
31
|
+
/** 1.14-S4(K-POOL-1):显式 env 变量候选(modelPool keyEnv 精确引用——替代 provider 缺省表;keychain 链保持在前) */
|
|
32
|
+
envVarCandidates?: string[]
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/** provider → 候选 env 变量(按序检索;CAR_LLM_API_KEY 为通用兜底) */
|
|
@@ -107,7 +109,8 @@ export class CredentialService {
|
|
|
107
109
|
|
|
108
110
|
const allowEnv = opts.allowEnvFallback === true || env.CAR_ALLOW_ENV_CREDENTIALS === '1'
|
|
109
111
|
if (allowEnv) {
|
|
110
|
-
|
|
112
|
+
// 1.14-S4(K-POOL-1):显式候选(keyEnv 精确引用)压过 provider 缺省表——用户点名变量即显式意见
|
|
113
|
+
const candidates = opts.envVarCandidates?.length ? opts.envVarCandidates : providerEnvVars(provider)
|
|
111
114
|
for (const name of candidates) {
|
|
112
115
|
searched.push(`env:${name}`)
|
|
113
116
|
if (env[name]) {
|
package/src/runtime-core/llm.ts
CHANGED
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
* 解析「先摘 evt.usage 再判 choice」(空 choices 尾包不再被吞);anthropic message_start(input 侧)
|
|
26
26
|
* + message_delta(output 侧)两包合并(message_delta 缺席 = output 不完整,整条不落);
|
|
27
27
|
* 归一口径 §1.1(openai 减法 / anthropic 透传 + cache_creation),畸形/缺失整条不落(禁补造)。
|
|
28
|
+
* - 1.14-S1(K-MP-1 / D-39):思考参数透传——LlmRequest.reasoning.effort 按家族映射:
|
|
29
|
+
* openai-compat = `reasoning_effort`;anthropic = `thinking:{type:'enabled',budget_tokens}`
|
|
30
|
+
* (ANTHROPIC_THINKING_BUDGET 表);**off / 缺席 = 字节面不下发**;未知 effort 值显式报错
|
|
31
|
+
* (CAR-E-LLM-REASONING,fail-visible 禁静默忽略——宿主越型传入防线);anthropic 侧
|
|
32
|
+
* budget ≥ max_tokens 违反 API 约束 → 显式报错提示调高(禁静默改写 max_tokens)。
|
|
28
33
|
*/
|
|
29
34
|
import { CarM8Error, providerUnreachable } from './errors.ts'
|
|
30
35
|
import { CredentialService } from './credentials.ts'
|
|
@@ -72,6 +77,22 @@ export class AdapterRegistry {
|
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
// ==================== 1.14-S1(K-MP-1 / D-39)思考参数映射 ====================
|
|
81
|
+
|
|
82
|
+
/** 归一思考档位:缺席/off → undefined(不下发);low/medium/high 原样;未知值 fail-visible(宿主越型传入防线) */
|
|
83
|
+
export function normalizeReasoningEffort(effort: unknown): 'off' | 'low' | 'medium' | 'high' | undefined {
|
|
84
|
+
if (effort === undefined || effort === null) return undefined
|
|
85
|
+
if (effort === 'off' || effort === 'low' || effort === 'medium' || effort === 'high') return effort
|
|
86
|
+
throw new Error(`CAR-E-LLM-REASONING: 未知思考档位 "${String(effort)}"(合法:off/low/medium/high;off/缺席 = 不下发)`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** anthropic effort → thinking.budget_tokens 映射(D-39 登记表);off/缺席不下发 */
|
|
90
|
+
const ANTHROPIC_THINKING_BUDGET: Record<'low' | 'medium' | 'high', number> = {
|
|
91
|
+
low: 2048,
|
|
92
|
+
medium: 8192,
|
|
93
|
+
high: 16384,
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
// ==================== finishReason 不可变守卫(SDK 边界) ====================
|
|
76
97
|
|
|
77
98
|
/** 首个 finishReason 定格;适配器后续给出不同值 = 契约违规显式抛错(不改写不吞没) */
|
|
@@ -264,6 +285,8 @@ export interface OpenAICompatOptions {
|
|
|
264
285
|
/** 凭据服务与 provider 名(Bearer token 来源) */
|
|
265
286
|
credentials?: CredentialService
|
|
266
287
|
provider?: string
|
|
288
|
+
/** 1.14-S4(K-POOL-1):成员级凭据 env 变量名引用(modelPool keyEnv——resolve 精确候选;keychain 链保持在前) */
|
|
289
|
+
credentialEnvVars?: string[]
|
|
267
290
|
fetchImpl?: typeof fetch
|
|
268
291
|
/** §3.5.4 基线:总 120s / 首字节 30s / 2 次退避 1s-2s(测试可注入缩短) */
|
|
269
292
|
timeoutMs?: number
|
|
@@ -288,6 +311,9 @@ class Retryable extends Error {}
|
|
|
288
311
|
* 1.11-S1(D-33 / 计量设计 §1.1):openai usage 归一——prompt_tokens 含 cached,inputTokens
|
|
289
312
|
* 必须做减法(prompt − prompt_tokens_details.cached_tokens);cache_creation 恒 0(无对应字段,
|
|
290
313
|
* 登记不估算)。usage 对象缺席/畸形(两侧主字段任一非数值)= 整条不落(缺失口径,禁补造)。
|
|
314
|
+
* 1.14-S3(K-MP-2b / D-41):分段计量——completion_tokens_details.reasoning_tokens(思考输出
|
|
315
|
+
* 细分,provider 真实事实)数值才采(segments.reasoningTokens);details/字段缺席或非数值 =
|
|
316
|
+
* segments 不带键(禁估算补造)。
|
|
291
317
|
*/
|
|
292
318
|
function normalizeOpenAIUsage(u: unknown, model: string, adapterId: string): LlmUsage | undefined {
|
|
293
319
|
if (!u || typeof u !== 'object') return undefined
|
|
@@ -295,6 +321,8 @@ function normalizeOpenAIUsage(u: unknown, model: string, adapterId: string): Llm
|
|
|
295
321
|
if (typeof r.prompt_tokens !== 'number' || typeof r.completion_tokens !== 'number') return undefined
|
|
296
322
|
const details = (r.prompt_tokens_details ?? null) as Record<string, unknown> | null
|
|
297
323
|
const cached = details && typeof details.cached_tokens === 'number' ? details.cached_tokens : 0
|
|
324
|
+
const compDetails = (r.completion_tokens_details ?? null) as Record<string, unknown> | null
|
|
325
|
+
const reasoning = compDetails && typeof compDetails.reasoning_tokens === 'number' ? compDetails.reasoning_tokens : undefined
|
|
298
326
|
return {
|
|
299
327
|
inputTokens: r.prompt_tokens - cached,
|
|
300
328
|
outputTokens: r.completion_tokens,
|
|
@@ -303,6 +331,7 @@ function normalizeOpenAIUsage(u: unknown, model: string, adapterId: string): Llm
|
|
|
303
331
|
model,
|
|
304
332
|
adapterId,
|
|
305
333
|
providerRaw: u,
|
|
334
|
+
...(reasoning !== undefined ? { segments: { reasoningTokens: reasoning } } : {}),
|
|
306
335
|
}
|
|
307
336
|
}
|
|
308
337
|
|
|
@@ -394,6 +423,10 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
|
|
|
394
423
|
const firstByteMs = opts.firstByteMs ?? 30_000
|
|
395
424
|
const retries = opts.retries ?? 2
|
|
396
425
|
const backoffMs = opts.backoffMs ?? [1_000, 2_000]
|
|
426
|
+
// 1.14-S4(K-POOL-1):成员级 keyEnv 精确候选——keyEnv 引用即显式意见(env 通道开启;候选=仅该变量,keychain 链保持在前)
|
|
427
|
+
const credResolveOpts = opts.credentialEnvVars?.length
|
|
428
|
+
? { envVarCandidates: opts.credentialEnvVars, allowEnvFallback: true }
|
|
429
|
+
: {}
|
|
397
430
|
|
|
398
431
|
const toOpenAI = (req: LlmRequest, apiKey: string) => ({
|
|
399
432
|
// 1.4-BUG-1:method: 'POST' 缺失——M8 最小实现潜伏缺陷(注入式 fetch 的单测忽略 method,
|
|
@@ -406,6 +439,11 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
|
|
|
406
439
|
body: JSON.stringify({
|
|
407
440
|
model: req.model,
|
|
408
441
|
stream: true,
|
|
442
|
+
// 1.14-S1(K-MP-1 / D-39):思考参数——effort 非 off/缺席才下发 reasoning_effort(off = 字节面零痕迹)
|
|
443
|
+
...(normalizeReasoningEffort(req.reasoning?.effort) !== undefined &&
|
|
444
|
+
normalizeReasoningEffort(req.reasoning?.effort) !== 'off'
|
|
445
|
+
? { reasoning_effort: normalizeReasoningEffort(req.reasoning?.effort) }
|
|
446
|
+
: {}),
|
|
409
447
|
// 1.11-S1(D-33 / 计量设计 §9-2 推荐A):usage 采集默认开启;第三方网关不兼容时的
|
|
410
448
|
// 回退方案(baseUrl 白名单)登记可回退项——不支持网关走缺失口径(禁估算补造)
|
|
411
449
|
stream_options: { include_usage: true },
|
|
@@ -439,7 +477,7 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
|
|
|
439
477
|
buildRequest: async () => {
|
|
440
478
|
let apiKey = ''
|
|
441
479
|
if (opts.credentials) {
|
|
442
|
-
const ref = opts.credentials.resolve(opts.provider ?? id,
|
|
480
|
+
const ref = opts.credentials.resolve(opts.provider ?? id, credResolveOpts)
|
|
443
481
|
apiKey = opts.credentials.reveal(ref, {})
|
|
444
482
|
}
|
|
445
483
|
return toOpenAI(req, apiKey)
|
|
@@ -470,6 +508,8 @@ export interface AnthropicAdapterOptions {
|
|
|
470
508
|
sleep?: (ms: number) => Promise<void>
|
|
471
509
|
/** D-20:Anthropic API max_tokens 必填——req.maxTokens 缺席时取此缺省(provider 差异登记,缺省 4096) */
|
|
472
510
|
defaultMaxTokens?: number
|
|
511
|
+
/** 1.14-S4(K-POOL-1):成员级凭据 env 变量名引用(modelPool keyEnv——resolve 精确候选;keychain 链保持在前) */
|
|
512
|
+
credentialEnvVars?: string[]
|
|
473
513
|
}
|
|
474
514
|
|
|
475
515
|
/** stop_reason → CAR finishReason(未映射 fail-visible——openai FINISH_MAP 同纪律) */
|
|
@@ -495,6 +535,8 @@ function mergeAnthropicUsage(
|
|
|
495
535
|
): LlmUsage | undefined {
|
|
496
536
|
if (!start || !delta) return undefined
|
|
497
537
|
if (typeof start.input_tokens !== 'number' || typeof delta.output_tokens !== 'number') return undefined
|
|
538
|
+
// 1.14-S3(K-MP-2b / D-41):anthropic 无输出细分事实(thinking 计入 output_tokens)→
|
|
539
|
+
// segments 不带键(禁估算补造——桌面色条退化单色,登记口径)
|
|
498
540
|
return {
|
|
499
541
|
inputTokens: start.input_tokens,
|
|
500
542
|
outputTokens: delta.output_tokens,
|
|
@@ -545,6 +587,15 @@ function toAnthropic(req: LlmRequest, apiKey: string) {
|
|
|
545
587
|
}
|
|
546
588
|
}
|
|
547
589
|
flushToolResults()
|
|
590
|
+
// 1.14-S1(K-MP-1 / D-39):思考参数——effort → thinking.budget_tokens(ANTHROPIC_THINKING_BUDGET 表);
|
|
591
|
+
// off/缺席不下发;budget ≥ max_tokens 违反 Messages API 约束(max_tokens 必须大于 budget_tokens)→
|
|
592
|
+
// 显式报错提示调高(fail-visible,禁静默改写 max_tokens)
|
|
593
|
+
const reasoningEffort = normalizeReasoningEffort(req.reasoning?.effort)
|
|
594
|
+
const thinkingBudget = reasoningEffort && reasoningEffort !== 'off' ? ANTHROPIC_THINKING_BUDGET[reasoningEffort] : undefined
|
|
595
|
+
const maxTokens = req.maxTokens ?? 4096
|
|
596
|
+
if (thinkingBudget !== undefined && thinkingBudget >= maxTokens) {
|
|
597
|
+
throw new Error(`CAR-E-LLM-REASONING: thinking budget_tokens(${thinkingBudget}) 必须小于 max_tokens(${maxTokens})——请调高 maxTokens(禁静默改写请求参数)`)
|
|
598
|
+
}
|
|
548
599
|
return {
|
|
549
600
|
method: 'POST' as const,
|
|
550
601
|
headers: {
|
|
@@ -555,7 +606,8 @@ function toAnthropic(req: LlmRequest, apiKey: string) {
|
|
|
555
606
|
body: JSON.stringify({
|
|
556
607
|
model: req.model,
|
|
557
608
|
stream: true,
|
|
558
|
-
max_tokens:
|
|
609
|
+
max_tokens: maxTokens,
|
|
610
|
+
...(thinkingBudget !== undefined ? { thinking: { type: 'enabled', budget_tokens: thinkingBudget } } : {}),
|
|
559
611
|
...(system.length ? { system: system.join('\n\n') } : {}),
|
|
560
612
|
messages: turns,
|
|
561
613
|
...(req.tools.length
|
|
@@ -639,6 +691,10 @@ export function createAnthropicAdapter(opts: AnthropicAdapterOptions): LlmAdapte
|
|
|
639
691
|
const firstByteMs = opts.firstByteMs ?? 30_000
|
|
640
692
|
const retries = opts.retries ?? 2
|
|
641
693
|
const backoffMs = opts.backoffMs ?? [1_000, 2_000]
|
|
694
|
+
// 1.14-S4(K-POOL-1):成员级 keyEnv 精确候选(同 openai-compat 口径)
|
|
695
|
+
const credResolveOpts = opts.credentialEnvVars?.length
|
|
696
|
+
? { envVarCandidates: opts.credentialEnvVars, allowEnvFallback: true }
|
|
697
|
+
: {}
|
|
642
698
|
|
|
643
699
|
return {
|
|
644
700
|
id,
|
|
@@ -650,7 +706,7 @@ export function createAnthropicAdapter(opts: AnthropicAdapterOptions): LlmAdapte
|
|
|
650
706
|
buildRequest: async () => {
|
|
651
707
|
let apiKey = ''
|
|
652
708
|
if (opts.credentials) {
|
|
653
|
-
const ref = opts.credentials.resolve(opts.provider ?? id,
|
|
709
|
+
const ref = opts.credentials.resolve(opts.provider ?? id, credResolveOpts)
|
|
654
710
|
apiKey = opts.credentials.reveal(ref, {})
|
|
655
711
|
}
|
|
656
712
|
return toAnthropic(req, apiKey)
|
|
@@ -25,6 +25,10 @@ export interface LlmRequest {
|
|
|
25
25
|
/** 1.4-S4(D-19):turn 取消信号(runTurn 的 {aborted} 形态透传)——适配器在 attempt 前 /
|
|
26
26
|
* chunk 间隙检查;外部取消 ≠ 失败(不 retry、不产 error chunk,静默收口由 chatStep 抛 TurnAborted) */
|
|
27
27
|
signal?: { aborted: boolean }
|
|
28
|
+
/** 1.14-S1(K-MP-1 / D-39):思考参数——适配器按家族映射(openai-compat = reasoning_effort;
|
|
29
|
+
* anthropic = thinking.budget_tokens);**off / 缺席 = 不下发该字段**(字节面零痕迹,非「effort:none」);
|
|
30
|
+
* 模型思考档位(模型级思考如 R1)为宿主侧模型元数据事实源(桌面 models:meta),内核不做档位判断 */
|
|
31
|
+
reasoning?: { effort: 'off' | 'low' | 'medium' | 'high' }
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/** 工具调用流式增量(跨 chunk 聚合出完整 ToolCall) */
|
|
@@ -53,6 +57,13 @@ export interface LlmUsage {
|
|
|
53
57
|
adapterId: string
|
|
54
58
|
/** provider 原始 usage 对象(anthropic 为 messageStart/messageDelta 两原始片段) */
|
|
55
59
|
providerRaw: unknown
|
|
60
|
+
/**
|
|
61
|
+
* 1.14-S3(K-MP-2b / D-41):请求分段计量——仅采 provider 真实返回的分段事实(归一化后),
|
|
62
|
+
* **无分段事实 = 不带键(禁静态估算,桌面色条退化单色不造假)**。v1 分段 = reasoningTokens
|
|
63
|
+
* (openai completion_tokens_details.reasoning_tokens——K-MP-1 思考参数的计量配套,thinking
|
|
64
|
+
* 输出细分;anthropic thinking 计入 output_tokens 无细分事实 → 缺省不带键,登记)。
|
|
65
|
+
*/
|
|
66
|
+
segments?: { reasoningTokens?: number }
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
/** 流式块(finishReason 一经产生不可被任何中间层改写) */
|
package/src/seam.ts
CHANGED
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
* 只读载入面;载入/校验单一权威在内核,桌面禁自实现哈希重算)+ K-S3-3 exportForensicsBundle/verifyBundle 出面
|
|
18
18
|
* (取证包导出 + 读回重验;断链即中止语义随本面)。恢复面 K-S3-2 零登记确认:SessionLog.fromEvents 自 M5 DEC-4
|
|
19
19
|
* 起已随本面可达(fork.ts 生产调用点),S2 D-S2-6 前提修正登记(S3 执行期发现)。
|
|
20
|
+
* 加法登记:1.14.0 —— PluginManager 装载报告面(K-S4-1 / D-43):loadPlugins 出面(六阶段流水线
|
|
21
|
+
* discover/verify/parse/validate/topo/register——LoadReportVO 结构化 VO,verify/topo 真值取代 v0.1 桌面
|
|
22
|
+
* mountPlugin 组合呈现的 SKIPPED 标注)+ ReloadManager 出面(热重载 epoch 击穿 + invalidate 语义随本面)+
|
|
23
|
+
* verifyPluginFile 出面(签名校验挂载点——宿主自有装载路径可复用同一签名门;minisign 轨唯一实装,sigstore
|
|
24
|
+
* 主轨维持 verifier 骨架期预留——D-43 裁决:登记 strict 期限不引入 sigstore 依赖,engines 硬不兼容见评估报告)。
|
|
20
25
|
*/
|
|
21
26
|
export { SessionLog, loadSessionLog, deriveSessionId, type SessionEvent, type EventKind, type Actor } from './session/log.ts'
|
|
22
27
|
export { SessionFileStore } from './session/store.ts'
|
|
@@ -42,3 +47,6 @@ export type { McpClientSpanHandle, PtcRelayHandle } from './runtime-core/trace.t
|
|
|
42
47
|
// —— 1.13.0:桌面 S3 治理面(文件安全/审计/取证)——
|
|
43
48
|
export { redact, type RedactionResult } from './runtime-core/redaction.ts'
|
|
44
49
|
export { exportForensicsBundle, verifyBundle, type ForensicsBundle, type ExportMeta } from './session/export.ts'
|
|
50
|
+
// —— 1.14.0(K-S4-1 / D-43):PluginManager 装载报告面 + 签名校验挂载点 ——
|
|
51
|
+
export { loadPlugins, ReloadManager, type LoadReportVO, type LoadPluginsResult, type LoadPluginsOptions, type LoadStage, type StageStatus, type PeerConflictChain } from './load/report.ts'
|
|
52
|
+
export { verifyPluginFile, type SignatureGateOptions, type SigGateResult } from './load/sigGate.ts'
|