@sema-agent/client-core 0.37.0 → 0.38.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/CHANGELOG.md +135 -0
- package/README.md +2 -2
- package/dist/adapt/arms.js +40 -0
- package/dist/adapt.d.ts +1 -1
- package/dist/adapt.js +2 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +94 -0
- package/dist/engineCapsCache.d.ts +33 -1
- package/dist/engineCapsCache.js +26 -4
- package/dist/engineErrorCodes.d.ts +20 -0
- package/dist/engineErrorCodes.js +44 -0
- package/dist/fleet/fleetProjection.d.ts +34 -0
- package/dist/fleet/fleetProjection.js +40 -0
- package/dist/hitl/hitlBridge.js +76 -17
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/model/modelSupplyRules.d.ts +102 -0
- package/dist/model/modelSupplyRules.js +149 -0
- package/dist/model/providerPresets.js +68 -12
- package/dist/seam.d.ts +48 -1
- package/dist/seam.js +7 -0
- package/dist/subagent/engineSubagentResume.d.ts +18 -0
- package/dist/subagent/engineSubagentResume.js +7 -0
- package/docs/INTEGRATION-CLIENTS.md +66 -16
- package/package.json +3 -3
|
@@ -218,6 +218,34 @@ export function wireStoppedBy(r) {
|
|
|
218
218
|
const v = r.stoppedBy;
|
|
219
219
|
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
220
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* #261 §2① `cycleSeq`(server ≥7.25.0 / core 5.36.0 #258;SDK **7.2.0** 才把它声明进
|
|
223
|
+
* `FleetTaskRow` ⇒ 本包 0.38.0 提货补投)—— 这一行的**代际号**,与 `FleetBgNotification.seq` /
|
|
224
|
+
* `task_progress.seq` **同域同轴**(fresh spawn 就是 cycle 1,每次 SendMessage 复活翻 +1)。
|
|
225
|
+
*
|
|
226
|
+
* 🔴 **缺席 = 「无此概念」,不是「第一代」**(SDK 头注逐字):没有 `a*` registry 行的 run ——
|
|
227
|
+
* 同步委派子代 / workflow `wa*` agent / **顶层 run 行** —— 根本没有代际。把缺席读成 1 的消费端
|
|
228
|
+
* 会把「首帧迟到」误判成「复活」。所以这里对**非正整数**一律整键缺席(0 / 负数 / 非有限数 /
|
|
229
|
+
* 非整数都不是合法代际号),绝不 `?? 0`、绝不 `?? 1`。
|
|
230
|
+
*/
|
|
231
|
+
export function wireCycleSeq(r) {
|
|
232
|
+
const v = r.cycleSeq;
|
|
233
|
+
return typeof v === 'number' && Number.isInteger(v) && v > 0 ? v : undefined;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* #261 §2② `retiredBy`(server ≥7.25.0;SDK 7.2.0 声明)—— 这一帧的终态**不是发布方亲报**,
|
|
237
|
+
* 而是对账腿从 durable run 行**投影**出来的(发布方死了,行本会永久僵在活跃集里当幽灵)。
|
|
238
|
+
*
|
|
239
|
+
* 🔴 **发布方亲报的终态帧恒不带此键** ⇒ 两种终态在 wire 上可判:要区分「引擎说它完了」与
|
|
240
|
+
* 「我们从库里读出来它完了」时,这是**唯一**的判据([ghost-rows-need-upstream-liveness] 的
|
|
241
|
+
* wire 侧对位物 —— 此前本层把这一位整个丢掉,两种终态在视图上同形)。
|
|
242
|
+
* service 侧今天是单词闭集(`"reconcile"`),**读侧开集**:未知词原样透传(将来的第二个投影者
|
|
243
|
+
* 会是一个新词而不是改义),消费端 branch 已知值 + 通渲兜底,**绝不**按成员判死。
|
|
244
|
+
*/
|
|
245
|
+
export function wireRetiredBy(r) {
|
|
246
|
+
const v = r.retiredBy;
|
|
247
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
248
|
+
}
|
|
221
249
|
/** 终态四键之 `resumable`(仅 bg 子代有源;非 boolean ⇒ 缺席,**不当 false**)。 */
|
|
222
250
|
export function wireResumable(r) {
|
|
223
251
|
return typeof r.resumable === 'boolean' ? r.resumable : undefined;
|
|
@@ -257,6 +285,8 @@ export function projectTasks(allRows, opts) {
|
|
|
257
285
|
const usage = wireRowUsage(r);
|
|
258
286
|
const resumable = wireResumable(r);
|
|
259
287
|
const editedFiles = wireEditedFiles(r);
|
|
288
|
+
const cycleSeq = wireCycleSeq(r);
|
|
289
|
+
const retiredBy = wireRetiredBy(r);
|
|
260
290
|
const task = {
|
|
261
291
|
id: r.id,
|
|
262
292
|
name: deriveAgentLabel(r.agentType, r.agentName, r.name, r.id),
|
|
@@ -286,6 +316,11 @@ export function projectTasks(allRows, opts) {
|
|
|
286
316
|
...(usage !== undefined ? { usage } : {}),
|
|
287
317
|
...(resumable !== undefined ? { resumable } : {}),
|
|
288
318
|
...(editedFiles !== undefined ? { editedFiles } : {}),
|
|
319
|
+
// #261 §2 两位:代际号与「非亲报终态」判别位。两者都是**在场才落键** —— cycleSeq 缺席是
|
|
320
|
+
// 「这条行没有代际概念」、retiredBy 缺席是「发布方亲报」,任何一个补默认值都会把一个诚实
|
|
321
|
+
// 缺席翻译成一句假话。
|
|
322
|
+
...(cycleSeq !== undefined ? { cycleSeq } : {}),
|
|
323
|
+
...(retiredBy !== undefined ? { retiredBy } : {}),
|
|
289
324
|
};
|
|
290
325
|
return task;
|
|
291
326
|
});
|
|
@@ -341,6 +376,8 @@ const FLEET_TASK_VIEW_KEY_TUPLE = [
|
|
|
341
376
|
'usage',
|
|
342
377
|
'resumable',
|
|
343
378
|
'editedFiles',
|
|
379
|
+
'cycleSeq',
|
|
380
|
+
'retiredBy',
|
|
344
381
|
];
|
|
345
382
|
/** `FleetTaskView` 的键名清单(运行期物;编译期与 `keyof FleetTaskView` 双向等值)。 */
|
|
346
383
|
export const FLEET_TASK_VIEW_KEYS = FLEET_TASK_VIEW_KEY_TUPLE;
|
|
@@ -382,6 +419,9 @@ const FLEET_TASK_ROW_WIRE_KEY_TUPLE = [
|
|
|
382
419
|
'usage',
|
|
383
420
|
'resumable',
|
|
384
421
|
'editedFiles',
|
|
422
|
+
// SDK 7.2.0 新声明的两位(#261 §2,server ≥7.25.0 早已在 wire 上 —— 这是**类型跟车**不是新能力)。
|
|
423
|
+
'cycleSeq',
|
|
424
|
+
'retiredBy',
|
|
385
425
|
];
|
|
386
426
|
/** SDK `FleetTaskRow` 的**入口**键名清单(编译期与 `keyof FleetTaskRow` 双向等值 ⇒ SDK 加一个
|
|
387
427
|
* wire 键,本元组不跟就编译红;跟了之后覆盖账那条腿再逼你表态「投不投」)。 */
|
package/dist/hitl/hitlBridge.js
CHANGED
|
@@ -61,12 +61,15 @@ export class HitlSafetyError extends Error {
|
|
|
61
61
|
// timeoutMs 是 client 构造级旋钮,本包对已构造的注入 client 不可配)。于是一次网络瞬断/超时 =
|
|
62
62
|
// decide 单发即死 → 上层拼 `failed` → parkResolver 合成 `hitl_unanswered` 终帧 → ask 死局。
|
|
63
63
|
//
|
|
64
|
-
// 🔴
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
64
|
+
// 🔴 **口径换代(#318 件②,2026-08-21):超时从「常态」变「真异常」** —— [4664] 的长调用口径作废。
|
|
65
|
+
// server #316([4687] 点名 BREAKING,随 7.37.0 发车)把任务级 decide 的 200 体从**终局形**改成
|
|
66
|
+
// **受理回执** `{taskId, sessionId, status:"resuming", bindingEnforced:true}`:受理点设在 core
|
|
67
|
+
// `resumeStream` 解析之后,一切会变成拒绝的判定(lease 429 / markResuming CAS 409 / core pre-CAS
|
|
68
|
+
// 守卫 / 绑定不符 409 / 卡不在 404)**仍同步发生**,受理后只剩模型段异步跑。实测受理即回 ≈23ms。
|
|
69
|
+
// ⇒ 对 ≥7.37 的部署,一次 60s 超时**不再是**「server 还在跑 resume」的常态,而是**真异常**
|
|
70
|
+
// (网络路径断在半途 / server 病态卡死 / 见下两条残余形)。口径随之收窄:
|
|
71
|
+
// · **超时类**(TimeoutError = SDK per-attempt 帽掐断)⇒ 仍带退避重试(重试环结构不动),但总窗
|
|
72
|
+
// 从 10 分钟收到 {@link DECIDE_TIMEOUT_RETRY_TOTAL_BUDGET_MS}(十秒级,见该常量的取值推导);
|
|
70
73
|
// · **网络断类**(ECONNREFUSED / fetch failed 等:连语义答复都没拿到,引擎多半真死)⇒ 重试
|
|
71
74
|
// **恰一次**,让真死尽快显形;
|
|
72
75
|
// · 语义答复类(带 HTTP status 的 4xx/5xx —— 引擎收到并回答了)⇒ 零重试,原样上抛;
|
|
@@ -81,17 +84,69 @@ export class HitlSafetyError extends Error {
|
|
|
81
84
|
// (typed 判别位)—— 消费方(toolApprovalWire / parkResolver)据此走**重呈臂**而不是把 turn 判死。
|
|
82
85
|
// 引擎真死时失败也会尽快显形:重呈臂的下一步(approvals.list / runs.events)对死引擎当场失败,
|
|
83
86
|
// 走既有诚实红。
|
|
87
|
+
// 🔴 **两条残余的同步形如实登记**(收窄不是「长调用消失了」,[4687] 逐字):终局形仍存在于
|
|
88
|
+
// ① **pre-7.37 的 server**(所有 decide 都是终局形 = 真长调用);② **≥7.37 但没有 durable run 行
|
|
89
|
+
// 可跟的部署**(那里提前受理 = 把结果扔掉,所以腿如实保持同步)。本包是三端共用件,面向的是
|
|
90
|
+
// 任意部署 —— 所以重试环**保留**、总窗**不设 0**。这两形上窗尽的代价是可接受的:耗尽走的是
|
|
91
|
+
// {@link DecideTransportRetryExhaustedError} → **重呈臂**(re-attach ⇒ durable 流重放 park ⇒
|
|
92
|
+
// 同一张卡重交用户),而不是把 turn 判死;若第一发其实已送达,重呈的下一步会撞
|
|
93
|
+
// `isAlreadyResolvedGateReason` 的已解决判据被救回。
|
|
84
94
|
// 🔴 请托半场(候黑板):SDK decide 若开 per-call timeoutMs(或对 HITL 面单列长缺省),本层的
|
|
85
|
-
//
|
|
95
|
+
// 超时类重试环可整段收敛成一发长等待。
|
|
86
96
|
/** 瞬断重试的起始退避(×2 递增,封顶 {@link DECIDE_RETRY_BACKOFF_MAX_MS};别把 decide 打成连发)。 */
|
|
87
97
|
const DECIDE_TRANSPORT_RETRY_BACKOFF_MS = 750;
|
|
88
98
|
const DECIDE_RETRY_BACKOFF_MAX_MS = 5_000;
|
|
89
|
-
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
|
|
99
|
+
/**
|
|
100
|
+
* SDK 的 **per-attempt 超时帽**(`AbortSignal.timeout(timeoutMs)`)的**缺省值**。
|
|
101
|
+
*
|
|
102
|
+
* 🔴 **它是「观察到的缺省」,不是本包能保证的量**(codex 对抗复审 [medium] 采纳,2026-08-21)。
|
|
103
|
+
* per-call opts 只有 `signal`(与之合流取先,只能收短不能放长),但 `timeoutMs` 是 **client
|
|
104
|
+
* 构造级**旋钮,而本桥吃的是**宿主注入的** client(`HitlClientLike` 根本不暴露它)⇒ 一个 web/桌面
|
|
105
|
+
* 宿主完全可以用 30s 或 120s 的 client 构造本桥。所以任何「总窗 ÷ 帽 = 发数」的推导都只在缺省值
|
|
106
|
+
* 上成立,**不能当成本共用件的性质**。
|
|
107
|
+
* ⇒ 本常量只用来推导下面那个**墙钟上界**;「至少重试一次」这条**性质**改由
|
|
108
|
+
* {@link DECIDE_TIMEOUT_MIN_ATTEMPTS} 用**发数**保证,与宿主的 timeoutMs 无关。
|
|
109
|
+
*/
|
|
110
|
+
const DECIDE_ATTEMPT_TIMEOUT_CAP_MS = 60_000;
|
|
111
|
+
/**
|
|
112
|
+
* 超时类**最少发数**(首发 + 至少一次重试)—— 与墙钟窗**两个独立的界**,不是第二层节奏。
|
|
113
|
+
*
|
|
114
|
+
* 🔴 为什么必须有它([4687] 登记的两条残余同步形是承重理由):终局形仍存在于 pre-7.37 的 server
|
|
115
|
+
* 与「≥7.37 但没有 durable run 行可跟」的部署 —— 那两形上一次超时**极可能是真的还在跑**,
|
|
116
|
+
* 至少给一次重试是这条腿唯一的补救。若只用墙钟窗判,宿主拿 120s 的 client 构造本桥时首发超时
|
|
117
|
+
* 那一刻 elapsed 已经 ≥ 窗 ⇒ **一次重试都没有**,而这件事在代码里毫无痕迹(注释还写着「恰一次重试」)。
|
|
118
|
+
* 🔴 这**不是** [4675] 说的「第二层节奏叠乘」:没有新增任何定时器/退避层,退避仍是同一条
|
|
119
|
+
* `backoff` 链;这只是同一个循环上的第二个**终止条件**(发数尽 ∧ 窗尽,两者都满足才停)。
|
|
120
|
+
*/
|
|
121
|
+
const DECIDE_TIMEOUT_MIN_ATTEMPTS = 2;
|
|
122
|
+
/**
|
|
123
|
+
* 超时类重试的总窗(#318 件② 收窄:`10 * 60_000` → 本值)。
|
|
124
|
+
*
|
|
125
|
+
* ── 取值推导(锚在 {@link DECIDE_ATTEMPT_TIMEOUT_CAP_MS} 上,不是拍脑袋的整数)────────────────
|
|
126
|
+
* 窗的语义是**不再起新发**(见下),而每一发超时类失败**本身**就要吃满一个 per-attempt 帽 ⇒
|
|
127
|
+
* 实际发数由「总窗 ÷ 帽」决定,且量化得很粗:
|
|
128
|
+
* · 窗 ≤ 1 帽 ⇒ 窗判本身当场耗尽;
|
|
129
|
+
* · 1 帽 < 窗 ≤ 2 帽 ⇒ 窗判允许恰一次重试(共 2 发);
|
|
130
|
+
* · > 2 帽 ⇒ 3 发起步,一路回到分钟级。
|
|
131
|
+
* 受理形下超时是**真异常**(不是「还在跑」),所以取**恰一次重试**那一档:一次重试足够吃掉单次
|
|
132
|
+
* 网络抖动,再多就是对着一个病态 server 空等。取 90s = 1.5 帽,**刻意落在区间中部**而不是边界
|
|
133
|
+
* (120s 恰等于 2 帽 + 退避,会让发数悬在退避时序的一根头发上)。
|
|
134
|
+
* ⇒ 在缺省帽上可预算的最坏墙钟 ≈ 60s(首发)+ 0.75s(退避)+ 60s(重发)≈ 121s,而不是旧口径的 10 分钟。
|
|
135
|
+
*
|
|
136
|
+
* 🔴 **本窗只是墙钟上界,不承诺发数**(codex [medium] 采纳):宿主可以用非缺省 `timeoutMs` 构造
|
|
137
|
+
* client(见 {@link DECIDE_ATTEMPT_TIMEOUT_CAP_MS}),那时「窗 ÷ 帽」得出的发数与这里写的不同。
|
|
138
|
+
* 与宿主无关的那条性质(**至少重试一次**)由 {@link DECIDE_TIMEOUT_MIN_ATTEMPTS} 单独保证 ——
|
|
139
|
+
* 两个界合取:**发数达标 ∧ 窗尽** 才停。所以在 120s client 上是「2 发、~240s」,在 30s client 上
|
|
140
|
+
* 是「3 发、~92s」,在缺省 60s 上是「2 发、~121s」—— 三者都有界,且都拿得到那一次重试。
|
|
141
|
+
*
|
|
142
|
+
* 🔴 窗的语义是**不再起新发**,刻意不掐在飞那一发(codex 复审议题,驳回后成文):给一发可能已被
|
|
143
|
+
* server 受理的 decide 塞截止 signal 换不来任何安全 —— server 侧照跑,客户端只多制造一个「送达
|
|
144
|
+
* 未知」。
|
|
145
|
+
* 🔴 **写成「帽 × 系数」而不是裸 90_000**:上面那段推导只有在两者绑在一起时才会随 SDK 改帽自动
|
|
146
|
+
* 跟手;写裸整数的话,SDK 哪天把帽改成 30s,注释里的「恰一次重试」当天变成假话而代码全绿。
|
|
147
|
+
*/
|
|
148
|
+
const DECIDE_TIMEOUT_RETRY_BUDGET_ATTEMPT_MULTIPLE = 1.5;
|
|
149
|
+
const DECIDE_TIMEOUT_RETRY_TOTAL_BUDGET_MS = DECIDE_ATTEMPT_TIMEOUT_CAP_MS * DECIDE_TIMEOUT_RETRY_BUDGET_ATTEMPT_MULTIPLE;
|
|
95
150
|
let decideTimeoutRetryBudgetOverrideMs;
|
|
96
151
|
/** 测试钩:把超时类重试总窗调小(传 undefined 还原缺省)。 */
|
|
97
152
|
export function __setDecideTimeoutRetryBudgetForTests(ms) {
|
|
@@ -457,10 +512,14 @@ export class HitlBridge {
|
|
|
457
512
|
if (!isTransientDecideTransportFailure(e) || callerAborted())
|
|
458
513
|
throw e;
|
|
459
514
|
if (isDecideAttemptTimeout(e)) {
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
|
|
515
|
+
// #318 件② 后口径:受理形(server ≥7.37)下 decide 受理即回,一次 per-attempt 帽掐断
|
|
516
|
+
// **是真异常**,不再是「server 还在跑 resume」的常态 —— 所以总窗只留恰一次重试的量
|
|
517
|
+
// (推导见 DECIDE_TIMEOUT_RETRY_TOTAL_BUDGET_MS)。重发仍然安全:重复 decide 由 server CAS
|
|
518
|
+
// 保证不双跑,首发其实送达时下一发只会撞 4xx(conflict/not-found ⇒ 上抛,已解决判据接手)。
|
|
519
|
+
// 🔴 **两个界合取**(codex [medium] 采纳):发数没达标就一定再发一次(与宿主的
|
|
520
|
+
// per-attempt timeoutMs 无关),达标之后才由墙钟窗决定还发不发。少了前半句,
|
|
521
|
+
// 120s client 的宿主一次重试都拿不到;少了后半句,30s client 会一路重试到分钟级。
|
|
522
|
+
if (attempts >= DECIDE_TIMEOUT_MIN_ATTEMPTS && Date.now() - startedAt >= decideTimeoutRetryBudgetMs()) {
|
|
464
523
|
throw new DecideTransportRetryExhaustedError(attempts, String(e));
|
|
465
524
|
}
|
|
466
525
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -246,6 +246,7 @@ export * from './model/catalogLoader.js';
|
|
|
246
246
|
export * from './model/providerAuth.js';
|
|
247
247
|
export * from './model/providerCatalog.js';
|
|
248
248
|
export * from './model/tierVocabulary.js';
|
|
249
|
+
export * from './model/modelSupplyRules.js';
|
|
249
250
|
export * from './websearch/searchProviderPresets.js';
|
|
250
251
|
export * from './env/localeGeo.js';
|
|
251
252
|
export * from './env/localeTag.js';
|
package/dist/index.js
CHANGED
|
@@ -428,6 +428,10 @@ export * from './model/providerCatalog.js';
|
|
|
428
428
|
// 🔴 `model/tierVocabulary.ts` = 档位五档词表 + CC 别名 + isTier 校验 + resolveTierBinding
|
|
429
429
|
// fail-open 降档派生。此前跨两仓三份;settings 读写那半场仍归宿主(cli tierCore 只留存储)。
|
|
430
430
|
export * from './model/tierVocabulary.js';
|
|
431
|
+
// 🔴 `model/modelSupplyRules.ts` = **Model Hub 供给面的三端公共判定**(#318 件③ 上收,cli [4752]
|
|
432
|
+
// 预告的三纯函数):vision 生效值+来源三态 / 删除断链核(拒删)/ 删除降级后果(照删但必说)。
|
|
433
|
+
// 零 IO —— 读盘那半场留各端(壳 modelChannels 读完再调这里);`doc === null` 的两义由调用方分流。
|
|
434
|
+
export * from './model/modelSupplyRules.js';
|
|
431
435
|
// ── 搜索 provider 目录 + 地域预选(2026-07-31)──────────────────────────────────────────────────
|
|
432
436
|
// `websearch/searchProviderPresets` = model 目录的**同形不同表**姊妹件:数据在 json、类型与查询
|
|
433
437
|
// 在 ts。它编译出来的不是模型目录而是**引擎部署 env**(`WEB_SEARCH_*`)。
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** vision 生效值的**出处**(UI 那一行「当前生效值与来源」的唯一数据源)。 */
|
|
2
|
+
export type VisionSource =
|
|
3
|
+
/** 这条 entry 自己显式表态。 */
|
|
4
|
+
'entry'
|
|
5
|
+
/** entry 不表态,取 `inferFamily` 的家族缺省。 */
|
|
6
|
+
| 'family'
|
|
7
|
+
/** 两边都不表态 —— 生效值**未知**,绝不折算成 false。 */
|
|
8
|
+
| 'unknown';
|
|
9
|
+
/**
|
|
10
|
+
* {@link resolveEntryVision} 的结果(具名:本仓 typeshape 门对导出面内联匿名形有棘轮)。
|
|
11
|
+
*
|
|
12
|
+
* 🔴 `value: undefined` 与 `value: false` 是**两件事**:前者「没人说过这个模型能不能看图」,
|
|
13
|
+
* 后者「确认它不能看图」。把前者渲成后者会永久封死一个真能力。
|
|
14
|
+
*/
|
|
15
|
+
export interface EntryVisionResolution {
|
|
16
|
+
value: boolean | undefined;
|
|
17
|
+
source: VisionSource;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 解析一条 entry 的 vision **生效值 + 来源**。纯函数(只吃 modelId/vision 两位,零 IO)。
|
|
21
|
+
*
|
|
22
|
+
* 🔴 [honest-absence]:家族与 entry 都不表态时给 `undefined`(未知),**不**给 `false`。
|
|
23
|
+
* ⚠️ 「未知」只是**客户端这一层**诚实:引擎侧 registry-core `ModelEntry.vision` 是
|
|
24
|
+
* `z.boolean().default(false)`,键缺席在那边就等于 false。**呈现层必须把这个后果一起说出来**,
|
|
25
|
+
* 别让用户以为「未知」= 引擎会自己去问供应商。
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveEntryVision(entry: {
|
|
28
|
+
modelId: string;
|
|
29
|
+
vision?: boolean;
|
|
30
|
+
}): EntryVisionResolution;
|
|
31
|
+
/** 一处挡住删除的引用。`where` 是坐标(用户照着去改),`fix` 是这一处该怎么解开。 */
|
|
32
|
+
export type PoolDeleteBlocker = {
|
|
33
|
+
kind: 'default' | 'roles' | 'roster' | 'unreadable';
|
|
34
|
+
where: string;
|
|
35
|
+
fix: string;
|
|
36
|
+
};
|
|
37
|
+
/** 一条「照删,但你该知道」的后果。删除**不**被阻断,确认屏必须把它渲出来。 */
|
|
38
|
+
export type PoolDeleteWarning = {
|
|
39
|
+
kind: 'allowlist-empties' | 'tier-binding';
|
|
40
|
+
/** 人话:删了会发生什么(不是坐标,是后果)。 */
|
|
41
|
+
consequence: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* 档位组的**归一形**(settings `semaTierGroups`)。两处删除判定共用,单独具名以免两边各写一份内联形。
|
|
45
|
+
*/
|
|
46
|
+
export interface TierGroupsView {
|
|
47
|
+
active: string;
|
|
48
|
+
groups: Record<string, {
|
|
49
|
+
tiers: Record<string, string | undefined>;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
/** {@link computeDeleteWarnings} 的入参(具名:导出面禁内联匿名形,B4 棘轮)。 */
|
|
53
|
+
export interface DeleteWarningsInput {
|
|
54
|
+
id: string;
|
|
55
|
+
/**
|
|
56
|
+
* 已读到的 models.json 文档切片。🔴 两位是 `unknown` 而不是收窄形 —— 它们是**盘上 JSON 的原样**,
|
|
57
|
+
* 属主是引擎的 models 域 schema(开集、可演进)。在这里手抄一份结构就是手抄一份会漂的上游形;
|
|
58
|
+
* 本函数对它们的处理本来就是「只判是不是数组/对象,然后逐项过滤」,窄读没有消费者。
|
|
59
|
+
* `null` = 调用方没读到文档(两义分流在调用方,见 {@link computeDeleteBlockers} 头注)。
|
|
60
|
+
*/
|
|
61
|
+
doc: {
|
|
62
|
+
atModelAllowlist?: unknown;
|
|
63
|
+
tierGroups?: unknown;
|
|
64
|
+
} | null;
|
|
65
|
+
/** settings `semaTierGroups` 归一形(缺席 ⇒ 回落 `doc.tierGroups`,与写路径的取值序同源)。 */
|
|
66
|
+
tierGroups: TierGroupsView | null;
|
|
67
|
+
/** 删除**之后**仍在池里的 catalog name 全集(判「有效名单会不会变空」要的就是它)。 */
|
|
68
|
+
survivingIds: readonly string[];
|
|
69
|
+
}
|
|
70
|
+
/** {@link computeDeleteBlockers} 的入参(具名:同上)。 */
|
|
71
|
+
export interface DeleteBlockersInput {
|
|
72
|
+
id: string;
|
|
73
|
+
/** 已读到的 models.json 文档切片;两位 `unknown` 的理由同 {@link DeleteWarningsInput.doc}。 */
|
|
74
|
+
doc: {
|
|
75
|
+
default?: unknown;
|
|
76
|
+
roles?: unknown;
|
|
77
|
+
} | null;
|
|
78
|
+
/** roster 归一形(激活组名 + 全部组的 slot 表;**分阶段组也在里面**,它们是真引用)。 */
|
|
79
|
+
rosters: {
|
|
80
|
+
active: string;
|
|
81
|
+
rosters: Record<string, {
|
|
82
|
+
slots: Record<string, string>;
|
|
83
|
+
}>;
|
|
84
|
+
} | null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 纯判定:删这条 entry 会带来哪些**语义变化**(不阻断)。
|
|
88
|
+
*
|
|
89
|
+
* 两格,都是「链不断但意思变了」:
|
|
90
|
+
* ① `atModelAllowlist` 的**有效名单**会被删空 ⇒ 按 registry-core 语义**全部启用模型都可 @**
|
|
91
|
+
* —— 一次静默的权限放宽,必须说;名单里还有别人 ⇒ 只是缩小名单,不用说。
|
|
92
|
+
* ② `semaTierGroups` 里有绑定指着它 ⇒ 删完那条绑定悬空 ⇒ 下次按档位解析会落到**另一个模型**上
|
|
93
|
+
* (而不是报错),用户会以为「我明明配了 pro 档」。
|
|
94
|
+
*/
|
|
95
|
+
export declare function computeDeleteWarnings(input: DeleteWarningsInput): PoolDeleteWarning[];
|
|
96
|
+
/**
|
|
97
|
+
* 纯判定:这条 entry 现在被哪些**断链类**引用挡着。空数组 = 可以删。
|
|
98
|
+
*
|
|
99
|
+
* 🔴 `doc === null` 的两义**在调用方分流**:这里只认「已读到的文档」;读不出来时调用方自己产
|
|
100
|
+
* `unreadable` 阻断 —— 那一刻我们并不知道谁指着它,fail-closed 拒删比「猜没人指着」安全。
|
|
101
|
+
*/
|
|
102
|
+
export declare function computeDeleteBlockers(input: DeleteBlockersInput): PoolDeleteBlocker[];
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* modelSupplyRules.ts — **Model Hub 供给面的三端公共判定**(#318 件③ 上收,cli [4752] 预告的
|
|
3
|
+
* 「resolveEntryVision / computeDeleteBlockers / computeDeleteWarnings 三纯函数」)。
|
|
4
|
+
*
|
|
5
|
+
* ── 为什么在这里 ────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
* 三端(TUI / web / desktop)的 Model Hub 要回答同样的三个问题:
|
|
7
|
+
* ① 这条模型档的 `vision` **生效值**是什么、**出处**是哪(自己表态 / 家族缺省 / 没人说过);
|
|
8
|
+
* ② 删这条档会不会**断链**(该拒删,并指路);
|
|
9
|
+
* ③ 删这条档会不会**悄悄改语义**(该照删,但确认屏必须逐条说出后果)。
|
|
10
|
+
* 判定放在库里、装配与呈现留端 —— 三端各写一遍的话,三个 Hub 会对「同一条档能不能删」给出三个
|
|
11
|
+
* 答案,而其中两个是在用户按下 y 之后才被发现的。
|
|
12
|
+
*
|
|
13
|
+
* ── 🔴 零 IO(本模块的立身之本)──────────────────────────────────────────────────────────────
|
|
14
|
+
* 三个函数**只吃已读好的数据**,不碰文件系统、不认识 `models.json` 在哪、不持锁。读盘那半场留在
|
|
15
|
+
* 各端(壳 = `modelChannels.poolDeleteBlockers` / `poolDeleteWarnings`,它们读完再调这里)。
|
|
16
|
+
* 分层的判据在 cli 侧成文:`doc === null` 的两义**在调用方分流** —— 本模块只认「已读到的文档」,
|
|
17
|
+
* 读不出来时由调用方自己产 `unreadable` 阻断(那一刻并不知道谁指着它,fail-closed 拒删比
|
|
18
|
+
* 「猜没人指着」安全)。
|
|
19
|
+
*
|
|
20
|
+
* ── ⚠️ 上收差分(逐条,行为零改动)────────────────────────────────────────────────────────
|
|
21
|
+
* · `resolveEntryVision` 的返回型从**内联匿名对象**改为具名 {@link EntryVisionResolution}
|
|
22
|
+
* —— 本仓 typeshape 门对导出面的内联匿名形有棘轮(B4)。**结构逐字相同**,壳侧剪切时零适配。
|
|
23
|
+
* · `inferFamily` 的来源从壳内 re-export 改为同包 `./providerPresets.js` 直取(壳那份本来就是
|
|
24
|
+
* 本包的 re-export)。⚠️ 与 #318 件③ 的**分层遮蔽修同批**:修前 `inferFamily(deepseek-*)?.vision`
|
|
25
|
+
* 结构性恒 `undefined`,所以 `source:'family'` 这一档对 deepseek 族**从来没走到过**;修后才真的
|
|
26
|
+
* 走得到。上收与修同批落地是刻意的 —— 只上收不修,等于把一条死分支原样搬进三端。
|
|
27
|
+
* · `isPlainObject` 在壳里是文件级私有,这里同形自持(不新增公面导出)。
|
|
28
|
+
*/
|
|
29
|
+
import { inferFamily } from './providerPresets.js';
|
|
30
|
+
/** 局部判据:普通对象(非 null、非数组)。与壳侧 `modelChannels.isPlainObject` 同形。 */
|
|
31
|
+
function isPlainObject(v) {
|
|
32
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 解析一条 entry 的 vision **生效值 + 来源**。纯函数(只吃 modelId/vision 两位,零 IO)。
|
|
36
|
+
*
|
|
37
|
+
* 🔴 [honest-absence]:家族与 entry 都不表态时给 `undefined`(未知),**不**给 `false`。
|
|
38
|
+
* ⚠️ 「未知」只是**客户端这一层**诚实:引擎侧 registry-core `ModelEntry.vision` 是
|
|
39
|
+
* `z.boolean().default(false)`,键缺席在那边就等于 false。**呈现层必须把这个后果一起说出来**,
|
|
40
|
+
* 别让用户以为「未知」= 引擎会自己去问供应商。
|
|
41
|
+
*/
|
|
42
|
+
export function resolveEntryVision(entry) {
|
|
43
|
+
if (typeof entry.vision === 'boolean')
|
|
44
|
+
return { value: entry.vision, source: 'entry' };
|
|
45
|
+
const fam = inferFamily(entry.modelId)?.vision;
|
|
46
|
+
if (typeof fam === 'boolean')
|
|
47
|
+
return { value: fam, source: 'family' };
|
|
48
|
+
return { value: undefined, source: 'unknown' };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 纯判定:删这条 entry 会带来哪些**语义变化**(不阻断)。
|
|
52
|
+
*
|
|
53
|
+
* 两格,都是「链不断但意思变了」:
|
|
54
|
+
* ① `atModelAllowlist` 的**有效名单**会被删空 ⇒ 按 registry-core 语义**全部启用模型都可 @**
|
|
55
|
+
* —— 一次静默的权限放宽,必须说;名单里还有别人 ⇒ 只是缩小名单,不用说。
|
|
56
|
+
* ② `semaTierGroups` 里有绑定指着它 ⇒ 删完那条绑定悬空 ⇒ 下次按档位解析会落到**另一个模型**上
|
|
57
|
+
* (而不是报错),用户会以为「我明明配了 pro 档」。
|
|
58
|
+
*/
|
|
59
|
+
export function computeDeleteWarnings(input) {
|
|
60
|
+
const { id, doc, tierGroups, survivingIds } = input;
|
|
61
|
+
const out = [];
|
|
62
|
+
const allow = doc?.atModelAllowlist;
|
|
63
|
+
if (Array.isArray(allow)) {
|
|
64
|
+
// 🔴 判据锚在**有效名单**上,不锚「字面上是不是只有它一个」:落盘闸口会把**所有悬空名**一起
|
|
65
|
+
// 滤掉,所以 `['victim','already-dangling']` 这种名单删掉 victim 之后有效名单同样变空 = 同样
|
|
66
|
+
// 放宽到「全部启用模型都可 @」,而 `every(n => n === id)` 那条旧判据对它一声不吭 = 承诺给用户
|
|
67
|
+
// 的权限提示上的**假阴**。
|
|
68
|
+
const declared = allow.filter((x) => typeof x === 'string');
|
|
69
|
+
const effectiveBefore = declared.filter(n => n === id || survivingIds.includes(n));
|
|
70
|
+
const effectiveAfter = declared.filter(n => n !== id && survivingIds.includes(n));
|
|
71
|
+
if (effectiveBefore.length > 0 && effectiveAfter.length === 0) {
|
|
72
|
+
out.push({
|
|
73
|
+
kind: 'allowlist-empties',
|
|
74
|
+
consequence: 'it is the last usable model on your @-mention allowlist — deleting it empties the list, which means EVERY enabled model becomes @-mentionable',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// 档位绑定的取值序与写路径同源:settings 现值优先,settings 没有档位组时回落 models.json 文件里
|
|
79
|
+
// 那份投影(只扫 settings 会漏掉「settings 无档位组、models.json 自带 tierGroups」那一格 ——
|
|
80
|
+
// 那份绑定同样会在写路径被 byId 静默滤掉)。
|
|
81
|
+
const groups = tierGroups
|
|
82
|
+
? Object.entries(tierGroups.groups).map(([g, def]) => [g, def?.tiers ?? {}])
|
|
83
|
+
: Array.isArray(doc?.tierGroups)
|
|
84
|
+
? doc.tierGroups.flatMap(g => isPlainObject(g) && typeof g.name === 'string' && isPlainObject(g.tiers)
|
|
85
|
+
? [[g.name, g.tiers]]
|
|
86
|
+
: [])
|
|
87
|
+
: [];
|
|
88
|
+
for (const [group, tiers] of groups) {
|
|
89
|
+
for (const [tier, ref] of Object.entries(tiers)) {
|
|
90
|
+
if (ref !== id)
|
|
91
|
+
continue;
|
|
92
|
+
out.push({
|
|
93
|
+
kind: 'tier-binding',
|
|
94
|
+
consequence: `tier group "${group}" binds the ${tier} tier to it — that binding goes dangling and the ${tier} tier will resolve to a different model`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 纯判定:这条 entry 现在被哪些**断链类**引用挡着。空数组 = 可以删。
|
|
102
|
+
*
|
|
103
|
+
* 🔴 `doc === null` 的两义**在调用方分流**:这里只认「已读到的文档」;读不出来时调用方自己产
|
|
104
|
+
* `unreadable` 阻断 —— 那一刻我们并不知道谁指着它,fail-closed 拒删比「猜没人指着」安全。
|
|
105
|
+
*/
|
|
106
|
+
export function computeDeleteBlockers(input) {
|
|
107
|
+
const { id, doc, rosters } = input;
|
|
108
|
+
const out = [];
|
|
109
|
+
if (doc && typeof doc.default === 'string' && doc.default === id) {
|
|
110
|
+
out.push({
|
|
111
|
+
kind: 'default',
|
|
112
|
+
where: 'models.json `default`',
|
|
113
|
+
fix: 'point another model at the default first (Model Hub: select it and press u), then delete this one',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (doc && isPlainObject(doc.roles)) {
|
|
117
|
+
for (const [role, target] of Object.entries(doc.roles)) {
|
|
118
|
+
// `{ select }` 形是**按 tier/needs 现选**,不是 catalog 引用 —— 删模型影响不到它,不拦。
|
|
119
|
+
if (!isPlainObject(target) || typeof target.model !== 'string')
|
|
120
|
+
continue;
|
|
121
|
+
if (target.model !== id)
|
|
122
|
+
continue;
|
|
123
|
+
out.push({
|
|
124
|
+
kind: 'roles',
|
|
125
|
+
where: `models.json roles.${role}`,
|
|
126
|
+
fix: `reassign the "${role}" role to another model first, then delete this one`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (rosters) {
|
|
131
|
+
for (const [name, def] of Object.entries(rosters.rosters)) {
|
|
132
|
+
if (!def || typeof def !== 'object' || !def.slots)
|
|
133
|
+
continue;
|
|
134
|
+
for (const [slot, ref] of Object.entries(def.slots)) {
|
|
135
|
+
if (ref !== id)
|
|
136
|
+
continue;
|
|
137
|
+
// 分阶段(未激活)组同样是真引用:它一被 switchRoster 激活就要按 slot 解析池,指着已删的
|
|
138
|
+
// entry = 激活当场断链。只看 active.slots 的实现会把分阶段组整个漏掉。
|
|
139
|
+
const isActive = name === rosters.active;
|
|
140
|
+
out.push({
|
|
141
|
+
kind: 'roster',
|
|
142
|
+
where: `roster "${name}" slot ${slot}${isActive ? ' (active)' : ' (staged)'}`,
|
|
143
|
+
fix: `point that slot at another model first (/config → Models → rosters), then delete this one`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
@@ -64,6 +64,47 @@ function presetIndex() {
|
|
|
64
64
|
presetIndexCache = { exact, stems };
|
|
65
65
|
return presetIndexCache;
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* family 表(第④层)的**唯一**匹配器 —— anchored 优先、再 unanchored 兜底。
|
|
69
|
+
*
|
|
70
|
+
* 提出来的理由不是整洁,是**单源**:`vision` 位只住在这张表上,而第①/②/③ 层命中时也要能读到它
|
|
71
|
+
* (见 {@link visionFromFamilyTable})。若两处各写一遍两趟正则,「这个 id 属哪个家族」在同一个函数
|
|
72
|
+
* 里就会有两个答案。
|
|
73
|
+
*/
|
|
74
|
+
function familyFromTable(id) {
|
|
75
|
+
for (const fam of MODEL_FAMILIES) {
|
|
76
|
+
if (fam.match && new RegExp(fam.match, 'i').test(id))
|
|
77
|
+
return fam;
|
|
78
|
+
}
|
|
79
|
+
for (const fam of MODEL_FAMILIES) {
|
|
80
|
+
if (fam.match && new RegExp(fam.match.replace(/\^/g, ''), 'i').test(id))
|
|
81
|
+
return fam;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 🔴 **#318 件③(cli [4752] 自领缺口1)—— `vision` 的分层遮蔽修**。
|
|
87
|
+
*
|
|
88
|
+
* 病(修前的真行为,不是假设):`vision` 位只写在 `modelFamilies.json` 上,而那张表只有**第④层**
|
|
89
|
+
* 读。可是任何真实的 `deepseek-*` id 在**第①层**(preset 大表精确)或**第③层**(家族主干包含)
|
|
90
|
+
* 就已经命中并 return 了,而这两层的构造器 `hitOf` **不带 vision 位** ⇒ 第④层对它们**结构性不可达**
|
|
91
|
+
* ⇒ `deepseek4` 行的 `vision: false` **永远读不出来**,壳的 `MODEL_VISION=false` stamp 恒不发生
|
|
92
|
+
* (下游两处注释自述的行为是死的)。
|
|
93
|
+
*
|
|
94
|
+
* 修的形:把 `vision` 从「第④层的一个字段」提成**与 ctx/maxTokens 证据层正交的一次独立查表**。
|
|
95
|
+
* 判据分工从此是两条轴,各自单源:
|
|
96
|
+
* · **容量轴**(contextWindow / maxTokens / perModelCap)= 四层证据,强者先赢 —— 一字不动;
|
|
97
|
+
* · **vision 轴** = 恒查 family 表(本函数),与哪一层命中**无关**。
|
|
98
|
+
* 为什么正交是对的:preset 大表(第①层)根本没有 vision 列,主干匹配(第③层)拿的是**别的模型**
|
|
99
|
+
* 的行 —— 两者都不是「这个 id 能不能看图」的证据。唯一有这个事实的地方就是 family 表。
|
|
100
|
+
*
|
|
101
|
+
* 🔴 **诚实缺席**:表上没标(键缺席)⇒ 返回 `undefined` ⇒ 调用方**不 stamp 这个键**。
|
|
102
|
+
* 「没人说过这个模型能不能看图」与「确认它不能看图」是两件事,后者会永久封死一个真能力。
|
|
103
|
+
*/
|
|
104
|
+
function visionFromFamilyTable(id) {
|
|
105
|
+
const fam = familyFromTable(id);
|
|
106
|
+
return typeof fam?.vision === 'boolean' ? fam.vision : undefined;
|
|
107
|
+
}
|
|
67
108
|
function normalizeId(raw) {
|
|
68
109
|
let id = raw.trim().toLowerCase();
|
|
69
110
|
const slash = id.lastIndexOf('/');
|
|
@@ -98,9 +139,23 @@ export function inferFamily(modelId) {
|
|
|
98
139
|
};
|
|
99
140
|
}
|
|
100
141
|
const id = normalizeId(raw);
|
|
142
|
+
// vision 轴:与容量证据层**正交**的一次独立查表(#318 件③ 分层遮蔽修,推导见 visionFromFamilyTable)。
|
|
143
|
+
// 恒按**完整归一 id** 查(不按主干):family 的 match 是锚在 id 上的前缀正则,拿主干去查会把
|
|
144
|
+
// `deepseek-chat` 的证据换成 `deepseek` 的证据 —— 同族时同解,不同族时是另一个答案。
|
|
145
|
+
const vision = visionFromFamilyTable(id);
|
|
101
146
|
// ① preset 大表精确(含剥变体尾巴后再试)——精确命中才带 perModelCap(F2 裁决封顶① 证据位;
|
|
102
147
|
// ③ 层主干匹配拿的是家族参考行,不是该模型的确证 cap)
|
|
103
|
-
const hitOf = (m, exact = false) => ({
|
|
148
|
+
const hitOf = (m, exact = false) => ({
|
|
149
|
+
id: m.famId,
|
|
150
|
+
name: m.famId,
|
|
151
|
+
match: '',
|
|
152
|
+
contextWindow: m.contextWindow,
|
|
153
|
+
maxTokens: m.maxTokens,
|
|
154
|
+
// 🔴 诚实缺席:表上没标就**不 stamp 键**(`exactOptionalPropertyTypes` 下 `vision: undefined`
|
|
155
|
+
// 与键缺席不是一回事,消费方读的正是「键在不在」这个三态)。
|
|
156
|
+
...(vision !== undefined ? { vision } : {}),
|
|
157
|
+
...(exact ? { perModelCap: m.maxTokens } : {}),
|
|
158
|
+
});
|
|
104
159
|
const exact = idx.exact.get(id);
|
|
105
160
|
if (exact)
|
|
106
161
|
return hitOf(exact, true);
|
|
@@ -116,7 +171,16 @@ export function inferFamily(modelId) {
|
|
|
116
171
|
const n = Number(inline[1]);
|
|
117
172
|
if ((inline[2] === 'k' && n >= 8) || inline[2] === 'm') {
|
|
118
173
|
const ctx = inline[2] === 'm' ? n * 1000000 : n * 1024;
|
|
119
|
-
|
|
174
|
+
// vision 轴同样正交透出(`deepseek-v3-128k` 这类 id 在这一层就 return,不透 = 同一个遮蔽病
|
|
175
|
+
// 换个层复发)。
|
|
176
|
+
return {
|
|
177
|
+
id: 'inline-cap',
|
|
178
|
+
name: 'capacity in id',
|
|
179
|
+
match: '',
|
|
180
|
+
contextWindow: ctx,
|
|
181
|
+
maxTokens: Math.min(ctx, 65536),
|
|
182
|
+
...(vision !== undefined ? { vision } : {}),
|
|
183
|
+
};
|
|
120
184
|
}
|
|
121
185
|
}
|
|
122
186
|
// ③ 家族主干:候选主干=preset model id 剥变体尾;主干出现在待匹配 id 中,取最长主干
|
|
@@ -135,16 +199,8 @@ export function inferFamily(modelId) {
|
|
|
135
199
|
}
|
|
136
200
|
if (best)
|
|
137
201
|
return hitOf(best);
|
|
138
|
-
// ④ family 表前缀(anchored → unanchored)
|
|
139
|
-
|
|
140
|
-
if (fam.match && new RegExp(fam.match, 'i').test(id))
|
|
141
|
-
return fam;
|
|
142
|
-
}
|
|
143
|
-
for (const fam of MODEL_FAMILIES) {
|
|
144
|
-
if (fam.match && new RegExp(fam.match.replace(/\^/g, ''), 'i').test(id))
|
|
145
|
-
return fam;
|
|
146
|
-
}
|
|
147
|
-
return undefined;
|
|
202
|
+
// ④ family 表前缀(anchored → unanchored)—— 与 vision 轴共用同一个匹配器(单源,见 familyFromTable)
|
|
203
|
+
return familyFromTable(id);
|
|
148
204
|
}
|
|
149
205
|
/** Format token counts like "1m" / "384k". */
|
|
150
206
|
export function fmtTokens(n) {
|
package/dist/seam.d.ts
CHANGED
|
@@ -389,7 +389,54 @@ export type ChromeEvent = {
|
|
|
389
389
|
* `actor.hostAsserted` 是消费端唯一能判「这个署名可信吗」的位:渲署名而不渲这个位 = 把一个
|
|
390
390
|
* 未经验证的名字渲成可信的(core 自己的 `[from …]` 渲染就是靠它决定加不加 `(unverified)`)。
|
|
391
391
|
*/
|
|
392
|
-
| HumanInputChromeEvent;
|
|
392
|
+
| HumanInputChromeEvent | EngineNoticeChromeEvent;
|
|
393
|
+
/**
|
|
394
|
+
* {@link ChromeEvent} 的 `engine_notice` 臂(#310 / #318 件①,server ≥7.36;契约 = server
|
|
395
|
+
* `ASSISTANT-WIRE-CONTRACT` 附录 D)——「**引擎想让这条会话的人知道一件事**」。
|
|
396
|
+
*
|
|
397
|
+
* 为什么**不走 `attachment` 臂**(与 `human_input` 同因):attachment 三条是要在转录流里渲一行的
|
|
398
|
+
* 东西,而通告是**会话级披露**、且 live/durable 两腿都会到(重放会再看到)⇒ 它需要一个带幂等键、
|
|
399
|
+
* 带结构化事实的独立臂,塞进 attachment 会逼消费端从一行文案里反解 code。
|
|
400
|
+
*
|
|
401
|
+
* 🔴 **宿主消费义务**(全部可选、fail-soft;本臂存在的第一价值 = 帧不再落 `unknown_arm` 被丢弃):
|
|
402
|
+
* ① **按 `code` + `detail` 渲染,`message` 只作 fallback**。core 明写
|
|
403
|
+
* `memory.session_polluted` 的 message 随 `memoryProvenance` 模式变文 ⇒ **按 message 文本匹配必碎**。
|
|
404
|
+
* ② **`code` 是开集,认不得也不许丢**:认得的码渲专用呈现,认不得的码用 `message` 兜底展示。
|
|
405
|
+
* 库侧**一个码都不硬编**,所以「认不认得」这张表由渲染面自己持有 —— 但它只能决定**怎么渲**,
|
|
406
|
+
* 不能决定**渲不渲**。
|
|
407
|
+
* ③ **幂等消费**:durable 腿重放会再送同一条(与 `workspace_changed` 同纪律),按 `eventId`
|
|
408
|
+
* (在场时)或 `code+ts` 去重,别按到达次数计数。
|
|
409
|
+
* ④ 🔴 `memory.harvest_quarantined` 的 `moved` 与 `escalated` **不可相减**(就地墓碑同时计入两者,
|
|
410
|
+
* core 顶注)—— 两个数各自读、并列呈现,任何减法都会得出一个不存在的量。
|
|
411
|
+
* ⑤ 🔴 **缺席不可反推**:server 对非白名单码 / 缺 `sessionId` 的通告**如实不投**(宁缺席不串台),
|
|
412
|
+
* 全族那一份只在 server 的运维日志里。所以「没收到通告」**不等于**「没发生」,渲染面不许把
|
|
413
|
+
* 本臂的缺席说成「一切正常」。
|
|
414
|
+
* 缺席(宿主不接本臂)= 引擎的这类披露在该宿主上看不见,**不是**报错。
|
|
415
|
+
*/
|
|
416
|
+
export interface EngineNoticeChromeEvent {
|
|
417
|
+
kind: 'engine_notice';
|
|
418
|
+
laneProof: LaneProof;
|
|
419
|
+
/** 稳定机器码,点分命名空间(`memory.session_polluted` …)。**开集**——见臂注 ②。 */
|
|
420
|
+
code: string;
|
|
421
|
+
/** core 铸的人话行。🔴 **仅 fallback 展示,不是匹配键**;wire 上非串时本位是空串。 */
|
|
422
|
+
message: string;
|
|
423
|
+
/** 机器可读事实(逐码不同,开集)。server 已脱敏 + 尺寸 bound,**仍按外部串处理**。 */
|
|
424
|
+
detail: Record<string, unknown>;
|
|
425
|
+
/** 归属会话(server 从 `detail` 提升为顶层)。畸形/缺席 ⇒ 本键不在场。 */
|
|
426
|
+
sessionId?: string;
|
|
427
|
+
/** **server 观察时刻**(ms epoch),**不是**引擎铸造时刻 —— `EngineNotice` 自身不带时间戳。 */
|
|
428
|
+
ts?: number;
|
|
429
|
+
/** durable 重放的稳定身份键 —— **core 铸的事件身份**(uuidv7 形)。wire 今天未必带。 */
|
|
430
|
+
eventId?: string;
|
|
431
|
+
/**
|
|
432
|
+
* durable 重放的**第二层**身份 —— SDK 从 SSE `id:` 字段 stamp 上来的 per-task 序号
|
|
433
|
+
* (= `task_event.seq`)。对「同一条账本行」稳定,重放会带同一个值。
|
|
434
|
+
* 🔴 与 {@link EngineNoticeChromeEvent.eventId} 是**两个命名空间**(全局身份 vs per-task 序号),
|
|
435
|
+
* 绝不互相顶替。消费端幂等序:`eventId` > `eventSeq` > 两者都缺才退 `code+ts`
|
|
436
|
+
* (最后那一档是有损的:`ts` 是 server 观察时刻(ms),同毫秒同码的两条会被折成一条)。
|
|
437
|
+
*/
|
|
438
|
+
eventSeq?: string;
|
|
439
|
+
}
|
|
393
440
|
/**
|
|
394
441
|
* `human_input` 臂的署名(core `ActorAssertion` 的投影;server 已 redact 后上帧)。
|
|
395
442
|
* 🔴 `hostAsserted` 是消费端**唯一**能判「这个署名可信吗」的位 —— 渲 `id` 而不渲它,
|