@sema-agent/client-core 0.67.1 → 0.68.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 +211 -0
- package/README.md +67 -3
- package/dist/adapt/arms.js +27 -2
- package/dist/adapt/turnFlags.d.ts +14 -0
- package/dist/adapt/turnFlags.js +4 -1
- package/dist/adapter/activeRunSelfHeal.d.ts +53 -6
- package/dist/adapter/activeRunSelfHeal.js +79 -8
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +18 -1
- package/dist/adapter/downstream/eventToSdkMessage.js +33 -2
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +75 -6
- package/dist/adapter/downstream/terminalToSdkResult.js +144 -44
- package/dist/adapter/runStream.d.ts +22 -2
- package/dist/adapter/runStream.js +190 -52
- package/dist/adapter/types.d.ts +4 -28
- package/dist/autoModeUnavailable.d.ts +17 -9
- package/dist/autoModeUnavailable.js +26 -8
- package/dist/classifierStatus.d.ts +32 -4
- package/dist/classifierStatus.js +5 -3
- package/dist/engineErrorCodes.d.ts +52 -0
- package/dist/engineErrorCodes.js +117 -0
- package/dist/engineNoticeCodes.d.ts +95 -1
- package/dist/engineNoticeCodes.js +124 -1
- package/dist/gateVocabulary.d.ts +18 -7
- package/dist/gateVocabulary.js +21 -8
- package/dist/hitl/parkResolver.d.ts +0 -14
- package/dist/hitl/parkResolver.js +22 -9
- package/dist/hitl/toolApprovalWire.d.ts +2 -1
- package/dist/hitl/toolApprovalWire.js +1 -0
- package/dist/ownKey.d.ts +34 -0
- package/dist/ownKey.js +36 -0
- package/dist/retryStatus.d.ts +13 -2
- package/dist/retryStatus.js +4 -1
- package/dist/runTerminal.d.ts +87 -14
- package/dist/runTerminal.js +89 -15
- package/dist/toolResult.js +8 -0
- package/dist/workflowClient.d.ts +22 -0
- package/dist/workflowClient.js +37 -0
- package/docs/INTEGRATION-CLIENTS.md +469 -10
- package/package.json +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { eventSeq, } from './types.js';
|
|
2
2
|
import { eventToSdkMessage, turnEndUsage } from './downstream/eventToSdkMessage.js';
|
|
3
|
+
import { turnUsageToModelUsage } from './downstream/turnUsageToModelUsage.js';
|
|
3
4
|
// D-3(0.66.0):终局对账臂与终帧超集键共用**同一个**成本读器(readRunCostFacts)。
|
|
4
5
|
import { readRunCostFacts, terminalToSdkResult } from './downstream/terminalToSdkResult.js';
|
|
5
6
|
import { coerceOutput, publishSubagentContentEvent } from '../subagentContentStore.js';
|
|
@@ -244,6 +245,26 @@ function sanitizeFrameType(type) {
|
|
|
244
245
|
* 两处 fail-soft 同款);而②的让位前提是「sink 真接住了」,没接住就得把 console 那腿还回来
|
|
245
246
|
* —— 否则一个坏 sink 会让丢帧比装它之前更隐蔽(装了个东西反而更瞎,是最坏的一种)。
|
|
246
247
|
*/
|
|
248
|
+
/**
|
|
249
|
+
* 判词 → 这一行说给人听的那句话。**开集**(`why` 是投影器给的开集判词)⇒ 表外判词走缺省句,
|
|
250
|
+
* 绝不因为多了一个判词就不说话。
|
|
251
|
+
*
|
|
252
|
+
* 🔴 为什么要分句而不是一句通用的:缺省那句逐字说「this build's projector has no arm for it」——
|
|
253
|
+
* 对 `unknown_arm` / `malformed` 是真话,对 **0.68.0 的契约违约判词是假话**(本 build 有臂,
|
|
254
|
+
* 是上游那一帧违了自己声明的契约)。一句说错方向的诊断会把读它的人指去升级客户端,而该做的是
|
|
255
|
+
* 去看引擎那一侧。
|
|
256
|
+
*/
|
|
257
|
+
const DROPPED_WHY_SENTENCE = Object.freeze({
|
|
258
|
+
turn_end_usage_absent: "the engine declares `turn_end.usage` as always present (core >= 7.17.0), and this frame has none. " +
|
|
259
|
+
'The turn is counted as UNKNOWN spend (the run total is reported as a lower bound), and the frame itself renders NOWHERE.',
|
|
260
|
+
});
|
|
261
|
+
const DROPPED_WHY_SENTENCE_DEFAULT = "this build's projector has no arm for it (engine newer than the client, or a malformed frame). It renders NOWHERE.";
|
|
262
|
+
/** 🔴 **按自有属性查表**(本仓对措辞表的既定纪律):`Object.freeze` 不移除原型,裸下标会让一个
|
|
263
|
+
* 叫 `constructor` / `toString` 的判词命中 `Object.prototype` 上的**函数**并被拼进日志行。 */
|
|
264
|
+
function droppedWhySentence(why) {
|
|
265
|
+
const row = Object.hasOwn(DROPPED_WHY_SENTENCE, why) ? DROPPED_WHY_SENTENCE[why] : undefined;
|
|
266
|
+
return typeof row === 'string' ? row : DROPPED_WHY_SENTENCE_DEFAULT;
|
|
267
|
+
}
|
|
247
268
|
function reportDroppedFrame(why, type, ctx) {
|
|
248
269
|
if (ctx.onDroppedFrame) {
|
|
249
270
|
try {
|
|
@@ -267,8 +288,7 @@ function reportDroppedFrame(why, type, ctx) {
|
|
|
267
288
|
// eslint-disable-next-line no-console
|
|
268
289
|
console.error(
|
|
269
290
|
// ADAPTER-F5 ②:type 是 wire 值,进日志行前必须 sanitize(裸值能伪造额外的整行)。
|
|
270
|
-
`[client-core] dropped an engine frame (${why}): type="${sanitizeFrameType(type)}" —
|
|
271
|
-
'for it (engine newer than the client, or a malformed frame). It renders NOWHERE.');
|
|
291
|
+
`[client-core] dropped an engine frame (${why}): type="${sanitizeFrameType(type)}" — ${droppedWhySentence(why)}`);
|
|
272
292
|
}
|
|
273
293
|
/** 测试钩:清空「已上报过的臂」去重表(去重是**跨调用**状态,不清就只有第一条用例看得见)。 */
|
|
274
294
|
export function _resetDroppedFrameReportForTest() {
|
|
@@ -319,6 +339,17 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
319
339
|
// 开流时挂等于让后开的那条把先开的那条的表顶掉,先开的终帧于是报出别人的账。
|
|
320
340
|
// 挂在终帧那一拍 + 与 `terminalToSdkResult(...)` 在**同一个同步步**里,那个窗按构造不存在。
|
|
321
341
|
const nestedUsageByTask = new Map();
|
|
342
|
+
// ── 🔴 0.67.2 / 车 I 件 I-1(异源对抗复审 [medium])—— usage 缺口观测位是 **per-stream** ──
|
|
343
|
+
// 修前它写在 `ctx.usageMissingObserved` 上:`EmitContext` 是**调用方的对象**、可以被复用给多条流
|
|
344
|
+
// (`startedAtMs` 本来就是这么用的;`run-subagent-usage-projection-test.mjs` 的 J3 段明确支持这一形),
|
|
345
|
+
// 而那一位**只置 true、永不清** ⇒
|
|
346
|
+
// · 顺序复用:上一条流里的一轮缺口,让**下一条 usage 完整的流**的终帧铸出 `_sema_usage_lower_bound`、
|
|
347
|
+
// 同一拍的 `run_cost_reconciled` 也被标成下界 —— 宿主据此把一条账数得全的 run 渲成「≥」并持久化;
|
|
348
|
+
// · 并发复用:两条流互相串这一位。
|
|
349
|
+
// 「别人那条流有缺口」不是「这条流的数字是下界」的证据。⇒ 观测位落在**本函数的局部量**上,终局
|
|
350
|
+
// 把**本流快照**同时交给两个投影口(终帧与对账臂),两面读同一份、且谁都读不到别人那份。
|
|
351
|
+
// 🔴 **不是**靠「终帧那一拍把 ctx 上那一位清掉」修的:并发的两条流会互相覆盖那次清除。
|
|
352
|
+
let usageMissingObserved = false;
|
|
322
353
|
for await (const ev of events) {
|
|
323
354
|
// event-id idempotency — drop a re-seen durable seq (contract 02 §1.1).
|
|
324
355
|
const seq = eventSeq(ev);
|
|
@@ -395,10 +426,53 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
395
426
|
// (而 `usage` 那几格恰好是 `flattenUsage(undefined)` 的全零)——「不知道」渲成了精确零。
|
|
396
427
|
// ⇒ 流内观测到就记下来,终帧那一拍与 stats 的读数**取并**(见 costFactParts)。
|
|
397
428
|
if (usageMissing)
|
|
398
|
-
|
|
429
|
+
usageMissingObserved = true;
|
|
399
430
|
const stopReasonRaw = ev.stopReason;
|
|
400
431
|
const stopWord = typeof stopReasonRaw === 'string' && stopReasonRaw.length > 0 ? stopReasonRaw : undefined;
|
|
401
|
-
|
|
432
|
+
// 🔴 `engineUsage` = 引擎**逐字原形**([2295] 裁 ②);`usage` = 它的 CC 镜像
|
|
433
|
+
// (`turnUsageToModelUsage` 是唯一铸口,footer 折叠与本臂用的是**同一只产物**,不另铸第二份)。
|
|
434
|
+
// 两者同拍取、同拍过下面那道违约闸 —— 闸后**都恒在场**,下游一处条件判都不再需要。
|
|
435
|
+
// 🔴 0.68.0:除它们之外还要**第三只读数**,因为本层要答的是**两个不同的问题**:
|
|
436
|
+
// · `engineUsage` —— 「这一帧有没有 usage 这个格子、而且它**成形**」(违约闸的判据);
|
|
437
|
+
// · `measured` —— 公面读器 `turnEndUsage` 的答案:`undefined` = **这一轮的账不知道**
|
|
438
|
+
// (整格缺席 / 占位六零)。它与 `usage`(逐字镜像)刻意分开 —— 把两件事合成一个读数,
|
|
439
|
+
// 一条**合法**的占位帧就会在违约闸上被误判成违约并整帧丢掉。
|
|
440
|
+
// 🔴 **成形判据不是 `!== undefined`**(异源对抗复审轮四实抓):wire 是 JSON,SSE 解析**原样透传**
|
|
441
|
+
// ⇒ `usage: null` / `usage: 7` 这类形真到得了这里。`null` 会让下面的映射在读 `costMicroUsd`
|
|
442
|
+
// 时抛 `TypeError` —— 那不是「一帧读不懂」,那是**整条流当场断掉**(后面的 `done` 一并丢);
|
|
443
|
+
// 标量则更坏:它会被映射成一份**看起来已测量**的全零账。⇒ 一律先判「非 null 的非数组对象」,
|
|
444
|
+
// 坏形与缺席走**同一条**违约路(响亮 + 不投影 + 立下界位)。
|
|
445
|
+
const rawUsage = ev.usage;
|
|
446
|
+
const engineUsage = typeof rawUsage === 'object' && rawUsage !== null && !Array.isArray(rawUsage)
|
|
447
|
+
? rawUsage
|
|
448
|
+
: undefined;
|
|
449
|
+
const usage = engineUsage !== undefined ? turnUsageToModelUsage(engineUsage) : undefined;
|
|
450
|
+
// 「这一轮的账知不知道」的**唯一判据**(与公面同一只;本层不另写一份「六格全零」的判断)。
|
|
451
|
+
const measured = turnEndUsage(ev);
|
|
452
|
+
// ── 🔴 0.68.0 BREAKING(core 7.17.0 #711)—— `turn_end.usage` **恒在场** ────────────────
|
|
453
|
+
// core 的铸点自 7.17.0 起是无条件的:`const usage = rs.turn.turnUsage ?? {六个 0}` 后
|
|
454
|
+
// `queue.push({type:"turn_end", usage, ...(turnUsageUnknown ? {usageMissing:true} : {}), …})`
|
|
455
|
+
// (`run-harness-handlers.js` `onTurnEnd`)⇒ 「没有 usage 的 turn_end」**不再是一条合法形**:
|
|
456
|
+
// 那一轮真没量出账时,core 发的是**六个 0 + `usageMissing:true`**,而不是不发 usage。
|
|
457
|
+
// ⇒ 0.65.1 / B-088 那条「usage 缺席也要照发」的臂(以及它逼出来的三处
|
|
458
|
+
// `usage !== undefined ? … : …` 条件)在本版**整条删掉**:它守的那个输入形已经不存在,
|
|
459
|
+
// 留着它等于给一个契约违约的帧准备一条静默通道。
|
|
460
|
+
//
|
|
461
|
+
// 🔴 **缺席 = 契约违约 ⇒ 响亮**(§32 的「违约无断言」纪律,本批的反钉格):
|
|
462
|
+
// ① 走宿主的丢帧留痕口({@link EmitContext.onDroppedFrame},判词开集 ⇒ 宿主不必改型),
|
|
463
|
+
// 缺 sink 时落 console —— 两条腿都说得出「哪一帧、为什么」;
|
|
464
|
+
// ② **这一轮的账确实不知道** ⇒ 同时立下界位,终帧那些数字按「≥」交付。不立的话,
|
|
465
|
+
// 一条丢了账的 run 会在终帧上被渲成一笔精确的账 —— 正是本仓反复在修的那条病;
|
|
466
|
+
// ③ 整帧**不投影**:没有 usage 就没有任何数字可交,折 0 就是把「不知道」写成已知账。
|
|
467
|
+
// 丢掉的只有同帧可能带的 `stopReason` —— 一条违约帧上的附带位不值得为它保留一条
|
|
468
|
+
// 「半读」臂(那条臂就是 ① 要消灭的静默通道)。
|
|
469
|
+
if (engineUsage === undefined || usage === undefined) {
|
|
470
|
+
usageMissingObserved = true;
|
|
471
|
+
// 🔴 违约帧同样让 footer 出口说实话:这一轮的账不知道,上面那两份读数属于更早的一轮。
|
|
472
|
+
handle.latestUsageMissing = true;
|
|
473
|
+
reportDroppedFrame('turn_end_usage_absent', ev.type, ctx);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
402
476
|
// §E2 identity (service 1.78) — a SUB-FLOW's turn_end (orchestration/subagent round, carries
|
|
403
477
|
// the identity envelope) must NOT drive the leader's C1a `end` reconcile: its outputTokens are
|
|
404
478
|
// the child's, and reconciling the leader's responseLength against them is the token-jump bug
|
|
@@ -420,10 +494,28 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
420
494
|
// `parent` / `sourceTaskId`),两层问的不是同一个问题:信封在不在 vs 这一行归到谁名下。
|
|
421
495
|
const isSubFlow = ev.parentToolCallId !== undefined ||
|
|
422
496
|
ev.sourceTaskId !== undefined;
|
|
423
|
-
if (usage) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
497
|
+
// 🔴 0.68.0:此处修前是 `if (usage) {` —— 那条 `usage` 在不在的判据随 core 7.17.0 的
|
|
498
|
+
// 「恒在场」一起退役(缺席在上面的违约闸里已经整帧收口)。块保留是为了**不动缩进**,
|
|
499
|
+
// 读法上它已经是无条件的一段。
|
|
500
|
+
{
|
|
501
|
+
// 🔴 0.68.0(异源对抗复审 [medium] 实抓,轮四再订正一次)—— 这个公开出口上**没有**判别位
|
|
502
|
+
// 可以让 footer 分辨「占位」与「真零」,所以它要分两件事各自决定:
|
|
503
|
+
// ① **读数**:只有**占位**(`measured === undefined` 且这一帧成形)才不覆盖 —— 保留上一次
|
|
504
|
+
// 真读数,与 #711 之前的行为逐字相同(那时这种轮根本不带 usage)⇒ 没跟车的消费者零回归。
|
|
505
|
+
// 🔴 **缺账 ≠ 占位**:`usageMissing` 可以与**真数字同帧**(core 在同一轮里攒到过数字而
|
|
506
|
+
// 另一次调用报了缺账)—— 那些数字是真的量到过(是下界),blanket 跳过会把它们丢掉
|
|
507
|
+
// (轮四实测:5 → 42+缺账位,main 的 handle 到 42,blanket 写法停在 5)。
|
|
508
|
+
// ② **判别位**:只要这一轮报了缺账就立(never false;测到账的那一轮删键)。
|
|
509
|
+
// 两件事分开之后,「读数是最新的真值」与「最新那一轮的账不全」可以同时为真。
|
|
510
|
+
if (measured !== undefined) {
|
|
511
|
+
handle.latestUsage = usage;
|
|
512
|
+
// [2295] 裁 ② 逐字通道:与镜像同拍存一份引擎原形(六键含 totalInputTokens)。
|
|
513
|
+
handle.latestEngineUsage = engineUsage;
|
|
514
|
+
}
|
|
515
|
+
if (usageMissing)
|
|
516
|
+
handle.latestUsageMissing = true;
|
|
517
|
+
else
|
|
518
|
+
delete handle.latestUsageMissing;
|
|
427
519
|
// plugins 专项 G1(2026-07-21):同一折叠点多发一份给 lastTurnUsageStore——StatusLine
|
|
428
520
|
// 的 statusline 命令 stdin(context_window.current_usage)在消息面无 usage(seam 合成
|
|
429
521
|
// 消息不带)时回落到这里,claude-hud 类插件的 Context 条才有真值。sub-flow 的 turn_end
|
|
@@ -441,7 +533,9 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
441
533
|
kind: 'last_turn_usage',
|
|
442
534
|
laneProof: MAIN,
|
|
443
535
|
usage,
|
|
444
|
-
|
|
536
|
+
// 🔴 0.68.0:`engineUsage` 的条件 spread 退役 —— `turn_end.usage` 恒在场
|
|
537
|
+
// (缺席已在违约闸里整帧收口),这里再判一次就是给一个不可能的形留座位。
|
|
538
|
+
engineUsage,
|
|
445
539
|
// L-215③:chrome 腿同批带这两位(message 腿的对偶在 `turn_usage` 臂的 `_sema_` 键上)。
|
|
446
540
|
// 🔴 `usageMissing` 在这条腿上**不能**靠「不发 usage」表达 —— 本臂的 `usage` 是必填位
|
|
447
541
|
// (宿主义务是「落最近一次 turn 真 usage」),所以它只能以判别位在场:
|
|
@@ -476,16 +570,29 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
476
570
|
// ⚠️ 回落**有损**:同父调用多子任务会并成一行 —— 那时终帧那张表的 `partial` 判别位会因
|
|
477
571
|
// 行数对不上 `nested.tasks` 而立起来(诚实缺席优先于假装分得开)。
|
|
478
572
|
const taskId = sourceTaskId ?? parent;
|
|
573
|
+
// ── 🔴 0.67.2 / 车 I 件 I-2(异源对抗复审 [medium])—— **累加表的键按出身隔离** ──
|
|
574
|
+
// 病:`taskId` 有**两个命名空间**(真身份 `sourceTaskId` / 回落 `parentToolCallId`),而 core 的
|
|
575
|
+
// 合同**没有**保证两者互斥 —— 一只子任务的 id 与另一只子任务的父调用 id 完全可以撞字面。修前
|
|
576
|
+
// 直接拿裸 `taskId` 当累加键,于是撞字面的两行**在累加那一层就已经并掉**,而出身位
|
|
577
|
+
// (`keyFromParentFallback`)是**按行**记的 ⇒ 并掉之后那一行只剩一种出身,「出身混合」这道闸
|
|
578
|
+
// (`nestedUsageByTaskParts` 的 `mixedKeyOrigin`)当场读不出混合,于是被绕过。
|
|
579
|
+
// 三帧反例(异源复审逐字复现):`(sourceTaskId,parentToolCallId,inputTokens)` = ('p','a',10) / (缺席,'p',20) /
|
|
580
|
+
// (缺席,'a',30),终局 `nested={tasks:2,turns:3}` ⇒ 修前输出 `{p:30, a:30}`、行数与轮数两条对账
|
|
581
|
+
// **同时成立** ⇒ `partial` 不铸,而真相是父调用 `a` 那只花了 40、父调用 `p` 那只花了 20。
|
|
582
|
+
// ⇒ 累加键前缀化(`s:` = 真身份 / `p:` = 回落),**逐事件**把出身记进键本身;交付面的键仍是
|
|
583
|
+
// 裸 id(端零改),同字面的跨空间碰撞由 `nestedUsageByTaskParts` 判出来并如实标记。
|
|
584
|
+
// 🔴 前缀只活在**本层的累加表**里:它不是身份的一部分,wire 上自带 `s:`/`p:` 前缀的 id 因此
|
|
585
|
+
// 不会与别人串(两个空间的键各带自己的前缀,`s:` + `"p:x"` ≠ `p:` + `"s:x"`)。
|
|
479
586
|
// 🔴 0.67.1 / B-090:入表条件从「行键 **且** 父调用 id 都读得出」放宽到「**行键**读得出」。
|
|
480
587
|
// 修前那个 `&& parent !== undefined` 把「只带 `sourceTaskId`」的子代整条挡在表外 ——
|
|
481
588
|
// 而 `parent` 在这里的唯一用处是**铸 chrome 增量臂的车道证明**,不是行的身份。
|
|
482
589
|
// 行键读不出(两键都缺 / 都是空串)仍然整条不入表:编一个 `"unknown"` 行会把几只子代
|
|
483
590
|
// 的账混成一只(C3 那一格守的就是这条)。
|
|
484
591
|
if (taskId !== undefined) {
|
|
485
|
-
// 🔴
|
|
486
|
-
// `{type:'turn_end', usageMissing:true}`(无 usage
|
|
487
|
-
//
|
|
488
|
-
//
|
|
592
|
+
// 🔴 0.68.0 BREAKING:发臂条件的「三者任一在场」整条退役 —— 它是 0.65.1 / B-088 为
|
|
593
|
+
// 「裸 `{type:'turn_end', usageMissing:true}`(无 usage)」那一形写的,而 core 7.17.0
|
|
594
|
+
// 起那一形不再存在(`usage` 恒在场,缺席在上面的违约闸里整帧收口)⇒ 走到这里就**恒有
|
|
595
|
+
// 话可说**,条件判只剩车道证明那一条(`parent`)与宿主有没有装 sink。
|
|
489
596
|
// 🔴 **`parent` 缺席时本臂不发,而行照进表**(0.67.1 定谳,理由如实写在这里):
|
|
490
597
|
// chrome 信封的车道证明 `LaneProof` 的子流臂是 `{lane:'subagent', parentToolCallId: string}`
|
|
491
598
|
// (`seam.ts`),座位门 `seatContract.checkLaneProof` 对它是**硬要求**(缺伴随位的信封
|
|
@@ -496,73 +603,95 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
496
603
|
// 按 `parentToolCallId: string` 读),不是一个 patch 能做的事。
|
|
497
604
|
// ⇒ 增量腿在这一形上静默,**收口快照(终帧 `_sema_nested_usage_by_task`)照带这一行**
|
|
498
605
|
// —— 账不丢,少的只是这一形的实时增量;两者本来就是「同一份账的两个时刻」。
|
|
499
|
-
if (parent !== undefined &&
|
|
606
|
+
if (parent !== undefined && ctx.emitChrome) {
|
|
500
607
|
emitChromeFireAndForget(ctx, {
|
|
501
608
|
kind: 'subagent_turn_usage',
|
|
502
609
|
laneProof: { lane: 'subagent', parentToolCallId: parent },
|
|
503
610
|
taskId,
|
|
504
611
|
...(sourceTaskId !== undefined ? { sourceTaskId } : {}),
|
|
505
612
|
parentToolCallId: parent,
|
|
506
|
-
|
|
507
|
-
|
|
613
|
+
usage,
|
|
614
|
+
engineUsage,
|
|
508
615
|
...(usageMissing ? { usageMissing: true } : {}),
|
|
509
616
|
...(stopWord !== undefined ? { stopReason: stopWord } : {}),
|
|
510
617
|
});
|
|
511
618
|
}
|
|
512
|
-
|
|
619
|
+
// 累加键 = 出身前缀 + 裸 id(见上面那段 🔴)。`sourceTaskId` 缺席时 `taskId === parent`
|
|
620
|
+
// (它就是 `sourceTaskId ?? parent`),所以这里不必再写一次回落判据。
|
|
621
|
+
const rowKey = sourceTaskId !== undefined ? `s:${sourceTaskId}` : `p:${taskId}`;
|
|
622
|
+
const row = nestedUsageByTask.get(rowKey) ?? { taskId, turns: 0, inputTokens: 0, outputTokens: 0 };
|
|
513
623
|
// 🔴 0.67.1(异源复审 [medium] 实抓):记下**这一行的键是回落来的**(真身份缺席)。
|
|
514
624
|
// B-090 放宽入表条件之后行键可以有两种出身,而混合出身时「拆一行 + 并一行」的计数
|
|
515
625
|
// 误差方向相反、可以恰好抵消 ⇒ 终帧那张表的 `partial` 判据要看得见出身
|
|
516
626
|
// (理由与反例逐字在 `MutableSubagentUsageRow.keyFromParentFallback` 的头注里)。
|
|
517
|
-
//
|
|
518
|
-
//
|
|
627
|
+
// 🔴 0.67.2 / 件 I-2:出身按行均匀这件事现在由**键空间**保证(累加键带 `s:`/`p:` 前缀),
|
|
628
|
+
// 不再依赖「`taskId = sourceTaskId ?? parent` 所以真身份在场的轮不会落到回落行上」这条
|
|
629
|
+
// 推理 —— 那条推理在两个空间**撞字面**时不成立(见上面 `rowKey` 的头注)。
|
|
519
630
|
if (sourceTaskId === undefined)
|
|
520
631
|
row.keyFromParentFallback = true;
|
|
521
632
|
row.turns += 1;
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
633
|
+
// 🔴 0.68.0:两处 `usage?.x ?? 0` 的 `?.`/`?? 0` 退役 —— `usage` 恒在场(违约闸在上面),
|
|
634
|
+
// 留着「缺席折 0」的写法等于在代码里为一个不可能的形保留一条把「不知道」写成 0 的路。
|
|
635
|
+
row.inputTokens += usage.inputTokens;
|
|
636
|
+
row.outputTokens += usage.outputTokens;
|
|
637
|
+
// 🔴 `cacheReadTokens` 读的是**引擎原形** `engineUsage`,不是 CC 镜像:镜像的
|
|
525
638
|
// `cacheReadInputTokens` 是**必填** number,缺席在那儿已经被折成 0
|
|
526
639
|
// (`toCcModelUsage` 的 `finiteOrZero`)⇒ 从镜像读就再也分不出「没报」与「零命中」。
|
|
527
640
|
// ⇒ 一轮都没报过 ⇒ 键**不铸**;报过之后再加 0 的那些轮是真的零命中。
|
|
528
|
-
|
|
529
|
-
|
|
641
|
+
// 🔴 **0.68.0 跟车修(#711 的同形后果,族扫捞出;异源对抗复审轮一订正过一次)**:
|
|
642
|
+
// `usageMissing` 的那一轮,core 的 `usage` 是 `rs.turn.turnUsage ?? {六个 0}` ——
|
|
643
|
+
// ⚠️ **`usageMissing` 并不保证那六格是零**:core 在同一轮里可能已经攒到过真数字
|
|
644
|
+
// (`turnUsage` 有值)而**另一次**模型调用报了缺账,于是 `turnUsageMissing` 与真数字
|
|
645
|
+
// **同帧并存**(`run-harness-handlers.js:79-101` 与 `:289-293` 真字节)。
|
|
646
|
+
// ⇒ 判据只能锚在**能证明是真读数的那一半**:占位恒为 `0`,所以一个**非零有限数**
|
|
647
|
+
// 必定是真的量到过 ⇒ 照累加;而 `0` 在这一形上**分不出**占位与「零命中」⇒ 不铸
|
|
648
|
+
// (本行的全部意义就是把「没报」与「零命中」分开,那一格会被 #711 悄悄抹平)。
|
|
649
|
+
// 🔴 轮一的写法是「`usageMissing` 的轮整条跳过」,那会把**真的非零 cache 读数丢掉**
|
|
650
|
+
// (两帧 10 / 100 且第二帧带缺账位 ⇒ 修前 110、轮一写法 10,而 chrome 增量腿仍交
|
|
651
|
+
// 10 与 100 ⇒ 实时面与终局分表对不上)。收窄成「只屏蔽零」两面就一致了。
|
|
652
|
+
const cacheRead = engineUsage.cacheReadTokens;
|
|
653
|
+
if (typeof cacheRead === 'number' && Number.isFinite(cacheRead) && (!usageMissing || cacheRead !== 0)) {
|
|
530
654
|
row.cacheReadTokens = (row.cacheReadTokens ?? 0) + cacheRead;
|
|
531
655
|
}
|
|
532
656
|
if (usageMissing)
|
|
533
657
|
row.usageMissing = true;
|
|
534
|
-
nestedUsageByTask.set(
|
|
658
|
+
nestedUsageByTask.set(rowKey, row);
|
|
535
659
|
}
|
|
536
660
|
}
|
|
537
|
-
const outputTokens =
|
|
538
|
-
// 🔴
|
|
539
|
-
// core
|
|
540
|
-
//
|
|
541
|
-
// ⚠️ 旧消费者零影响:`outputTokens`
|
|
542
|
-
// 本来就以 `typeof m.outputTokens === 'number'` 开门 ⇒
|
|
543
|
-
|
|
544
|
-
// `usageMissing` 判别位**本身就是话**——core 该轮零 usage 帧时发裸 `{type:'turn_end',
|
|
545
|
-
// usageMissing:true}`(无 usage、无 stopReason),旧条件让它整条静默 ⇒ 最诚实的那一帧
|
|
546
|
-
// 反而丢了 `_sema_usage_missing`(G30-23)。三者任一在场即发;三者皆缺席仍不发(F4)。
|
|
547
|
-
if ((typeof outputTokens === 'number' || stopWord !== undefined || usageMissing) && !isSubFlow) {
|
|
661
|
+
const outputTokens = engineUsage.outputTokens;
|
|
662
|
+
// 🔴 0.68.0 BREAKING:发臂条件的「三者任一在场」整条退役(0.65.1 / B-088 那条判据的
|
|
663
|
+
// 输入形随 core 7.17.0 消失 —— 见本块顶部的违约闸)。走到这里 `usage` 恒在场 ⇒
|
|
664
|
+
// **恒有话可说**:要么是镜像,要么是 `usageMissing` 判别位,两者必有其一。
|
|
665
|
+
// ⚠️ 旧消费者零影响:`outputTokens` 读不出时仍然**整键不铸**,而 adapt 的 `turnUsageArm`
|
|
666
|
+
// 本来就以 `typeof m.outputTokens === 'number'` 开门 ⇒ 那种帧对它照旧是 no-op。
|
|
667
|
+
if (!isSubFlow) {
|
|
548
668
|
// ── L-215③(0.65.0):assistant 行那两个**算不出来**的键的真值出口 ─────────────────
|
|
549
669
|
// `eventToSdkMessage` 的 `assistantArm` 刻意**不**在内容臂上铸 `usage` / `stop_reason`
|
|
550
670
|
// (帧序:内容臂先到、`turn_end` 后到 ⇒ 臂发出时引擎还没说这一轮花了多少;在那里铸只能
|
|
551
671
|
// 是估算,而估算正是本件要根治的病)。真值只能在**这里**给 —— 这条臂本来就是 turn 收尾
|
|
552
672
|
// 那一拍的中性出口。两个都是 `_sema_` 超集键,CC 同名键语义零改:
|
|
553
673
|
// · `_sema_last_assistant_usage` —— 这一轮的 CC `ModelUsage` 镜像(与 footer 折叠用的
|
|
554
|
-
// 是**同一只** `
|
|
674
|
+
// 是**同一只** `turnUsageToModelUsage()` 产物,不另铸第二份 ⇒ 两面永远不会各漂各的);
|
|
555
675
|
// · `_sema_stop_reason` —— `turn_end.stopReason` **原词透传**(core 归一化后的五词
|
|
556
676
|
// `stop`/`length`/`toolUse`/`error`/`aborted`,sdk 型面是开放 string ⇒ 按开集读;
|
|
557
677
|
// 「这一轮是不是被 max_tokens 截了」就靠它,此前 stream 与 trace 两面互盲)。
|
|
558
|
-
// 缺席一律不铸(旧引擎不发 `stopReason`;`usage`
|
|
678
|
+
// 缺席一律不铸(旧引擎不发 `stopReason`;`usage` 整体缺席的帧在违约闸那一拍就收口了)。
|
|
559
679
|
yield {
|
|
560
680
|
type: 'turn_usage',
|
|
561
|
-
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
|
|
565
|
-
|
|
681
|
+
// 🔴 `usageMissing` 在场 ⇒ **不铸镜像**(0.65.x 起的既有规矩:全零的「不知道」绝不冒充
|
|
682
|
+
// 一笔已知的零账),改铸判别位。
|
|
683
|
+
// 🔴 `outputTokens` 的判据随 #711 多一条(与上面子代腿的 `cacheReadTokens` **同一条**):
|
|
684
|
+
// 缺账轮的 `usage` 是 `turnUsage ?? {六个 0}`,而 `usageMissing` **不保证**那六格是零
|
|
685
|
+
// (同一轮里另一次调用报了缺账时,真数字与判别位同帧并存)⇒ 占位恒为 `0`,所以
|
|
686
|
+
// **非零有限数必定是真读数**(照铸),而 `0` 在这一形上分不出占位与真零 ⇒ 不铸 ——
|
|
687
|
+
// 把占位 `0` 交出去,以 `typeof === 'number'` 开门的既有消费者(adapt 的 `turnUsageArm`
|
|
688
|
+
// → spinner 的 responseLength 对账)会拿它当一次真的「这一轮吐了 0 个 token」。
|
|
689
|
+
...(usageMissing ? { _sema_usage_missing: true } : { _sema_last_assistant_usage: usage }),
|
|
690
|
+
// `outputTokens` 仍按**值**判:`usage` 恒在场不等于它里面每一格都是有限数,而 wire 是
|
|
691
|
+
// JSON —— 坏值折 0 就是把「读不出」写成一笔零账。读不出 ⇒ 整键不铸。
|
|
692
|
+
...(typeof outputTokens === 'number' && Number.isFinite(outputTokens) && (!usageMissing || outputTokens !== 0)
|
|
693
|
+
? { outputTokens }
|
|
694
|
+
: {}),
|
|
566
695
|
...(stopWord !== undefined ? { _sema_stop_reason: stopWord } : {}),
|
|
567
696
|
};
|
|
568
697
|
}
|
|
@@ -701,19 +830,28 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
701
830
|
// fail-soft 同 plan_review_park:sink 抛错不影响终帧照常投影。
|
|
702
831
|
if (ev.type === 'done' && ctx.emitChrome) {
|
|
703
832
|
const doneStats = ev.result?.stats;
|
|
704
|
-
// 🔴 0.67.1 / B-091:**第二参必须传** —— 下界位的取并(`stats.usageMissing` ∪ 流内观测
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
|
|
833
|
+
// 🔴 0.67.1 / B-091:**第二参必须传** —— 下界位的取并(`stats.usageMissing` ∪ 流内观测)
|
|
834
|
+
// 已经下沉进读器;不传就是把「臂绕开取并」那条不对称原样种回去(终帧铸了
|
|
835
|
+
// `_sema_usage_lower_bound`、同一拍的臂上却没有 `usageLowerBound`)。
|
|
836
|
+
// 🔴 0.67.2 / 件 I-1:传的是**本流快照**(局部量按值包一层),不再是共享 ctx —— 与下面那行
|
|
837
|
+
// `terminalToSdkResult(..., observed)` 是**同一份**读数。
|
|
838
|
+
const costFacts = readRunCostFacts(doneStats, { usageMissingObserved });
|
|
708
839
|
if (costFacts !== undefined) {
|
|
709
840
|
emitChromeFireAndForget(ctx, { kind: 'run_cost_reconciled', laneProof: MAIN, ...costFacts.reconcile });
|
|
710
841
|
}
|
|
711
842
|
}
|
|
712
|
-
// L-228
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
ctx
|
|
716
|
-
|
|
843
|
+
// L-228 / 🔴 0.67.2 件 I-2b 订正:分表**不再挂到 `ctx` 上**,而是与观测位一起随第三参按值交给
|
|
844
|
+
// 终帧投影器。此前那句「挂表与铸终帧在同一个同步步里,复用 ctx 的并发流不会串账」只覆盖了
|
|
845
|
+
// **两边都经 runStream** 的路径 —— 而这三只终帧投影器是**公面导出**,端完全可以「A 走
|
|
846
|
+
// runStream、B 直调终帧投影」共用一个 ctx,那时 B 的终帧带出的是 A 的分表(B 的 `nested`
|
|
847
|
+
// 计数恰好对得上时连 `partial` 都不铸)。与件 I-1 逐字同形,所以同批一起摘掉。
|
|
848
|
+
// 🔴 0.67.2 / 件 I-1 订正:这里此前还写着「`usageMissingObserved` 是 per-ctx 的单调布尔,
|
|
849
|
+
// 复用 ctx 的两条流里只要有一条观测到缺口,两条都该按下界读 —— 取并是安全的那一侧」。
|
|
850
|
+
// **那句话是错的**:下界位问的是「**这一条 run** 的账数全了没有」,别的流的缺口对它一个
|
|
851
|
+
// 字节的证据都不是;按那句话办,一条账数得全的 run 会被渲成「≥」并被宿主持久化成不完整状态
|
|
852
|
+
// (失效方向在这里**不是**安全的那一侧,它是在断言一件没发生的事)。
|
|
853
|
+
// ⇒ 观测位改为 per-stream 局部量,终帧按值收(第三参),与上面对账臂读的是同一份。
|
|
854
|
+
yield terminalToSdkResult(ev, ctx, { usageMissingObserved, nestedUsageByTask });
|
|
717
855
|
return;
|
|
718
856
|
}
|
|
719
857
|
// Every other arm → typed three-state projection(REF-CC-058)。
|
package/dist/adapter/types.d.ts
CHANGED
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
import type { AgentEvent } from '@sema-agent/sdk';
|
|
37
37
|
import type { ModelUsage, SDKMessage } from '@sema-agent/agent-types';
|
|
38
38
|
import type { ChromeEvent } from '../seam.js';
|
|
39
|
-
import type { MutableSubagentUsageRow } from './downstream/terminalToSdkResult.js';
|
|
40
39
|
export type { ModelUsage, SDKMessage };
|
|
41
40
|
export type StampedAgentEvent = AgentEvent & {
|
|
42
41
|
id?: string;
|
|
@@ -48,7 +47,10 @@ export declare function uuid(): string;
|
|
|
48
47
|
* 一条被丢弃的引擎帧的**结构化留痕**([C77]①,`EmitContext.onDroppedFrame` 的载荷)。
|
|
49
48
|
* 两个位都取自 `EventProjection` 的 `dropped` 臂原值,包侧不做任何加工:
|
|
50
49
|
* · `type` = 引擎那一帧的 `type`(未知臂/畸形帧的臂名);
|
|
51
|
-
* · `why` = 投影器给的判词(今天是 `unknown_arm` / `malformed`)。
|
|
50
|
+
* · `why` = 投影器给的判词(今天是 `unknown_arm` / `malformed` / `turn_end_usage_absent`)。
|
|
51
|
+
* 🔴 第三个词是 **0.68.0 新加**(core 7.17.0 #711 之后 `turn_end.usage` 恒在场 ⇒ 缺席是
|
|
52
|
+
* **契约违约**而不是一条合法形):它是本包第一处「不是渲不出来,而是上游违约」的判词,
|
|
53
|
+
* 正因为 `why` 是开集,宿主不必改型就能收到它。
|
|
52
54
|
* 🔴 **不是**开集枚举:`why` 故意留成 string —— 投影器长出新判词时宿主不该编译不过,
|
|
53
55
|
* 它本来就是「说给人看的一句判词」,不是控制流上的判别位。
|
|
54
56
|
*/
|
|
@@ -101,32 +103,6 @@ export interface EmitContext {
|
|
|
101
103
|
* · sink 抛错**绝不影响流**,并且痕迹落回 console —— 让位的前提是它真接住了。
|
|
102
104
|
*/
|
|
103
105
|
onDroppedFrame?(info: DroppedFrameInfo): void;
|
|
104
|
-
/**
|
|
105
|
-
* L-228(0.67.0)—— **这条流上看见的 per-subagent turn 用量分表**,键 = 子任务 id
|
|
106
|
-
* (wire 缺席时回落 `parentToolCallId`)。终帧的两个超集键
|
|
107
|
-
* `_sema_nested_usage_by_task` / `_sema_nested_usage_by_task_partial` 由它铸出
|
|
108
|
-
* (唯一 mint 点 = `terminalToSdkResult.ts` 的 `nestedUsageByTaskParts`)。
|
|
109
|
-
*
|
|
110
|
-
* 🔴 **由 `runStream` 在流内写,宿主不要自己填** —— 与同接口的 `startedAtMs` 同一类
|
|
111
|
-
* (「请求是宿主构造的,这一格是驱动自己攒的」)。宿主塞一份进来 = 把一份**不是这条流看见的**
|
|
112
|
-
* 账当成这条流的,而下游那个 `partial` 判别位恰恰是靠「这条流看见了多少」才成立的。
|
|
113
|
-
* 🔴 **缺席 / 空表 ⇒ 终帧两个键都不铸**:空表会被读成「一个子代都没委派」,而真相可能是
|
|
114
|
-
* 「委派了但这条流没看见任何一轮」。
|
|
115
|
-
*/
|
|
116
|
-
nestedUsageByTask?: ReadonlyMap<string, MutableSubagentUsageRow>;
|
|
117
|
-
/**
|
|
118
|
-
* 0.67.0 —— **这条流上观测到过「某一轮没有 usage」**(core 的 `turn_end.usageMissing`)。
|
|
119
|
-
* 终帧的 `_sema_usage_lower_bound` 与 chrome 对账臂的 `usageLowerBound` 与 `stats.usageMissing`
|
|
120
|
-
* **取并**读它。
|
|
121
|
-
*
|
|
122
|
-
* 🔴 **为什么非有它不可**:`stats.usageMissing` 只在带得出 `TaskResult` 的终帧上有,而
|
|
123
|
-
* `failed` 事件帧 / 409 拒绝信封 / park 体**根本没有 stats** ⇒ 只读 stats 的话,一条**已经
|
|
124
|
-
* 观测到缺口**的 run 会在终帧上落成「判别位缺席」,而按新合同那读作「每一轮都报了 usage」——
|
|
125
|
-
* 偏偏那种终帧的 `usage` 是 `flattenUsage(undefined)` 的**全零**:「不知道」被渲成了精确零。
|
|
126
|
-
* 🔴 **由 `runStream` 在流内写,宿主不要自己填**(同 `nestedUsageByTask` / `startedAtMs`)。
|
|
127
|
-
* 🔴 **只置 `true`,从不置回 false**:一轮不知道,整条流的数字就是下界,后面的轮补不回来。
|
|
128
|
-
*/
|
|
129
|
-
usageMissingObserved?: true;
|
|
130
106
|
}
|
|
131
107
|
/** Stamp `uuid` + `session_id` onto a freshly-built arm body. */
|
|
132
108
|
export declare function stamp<T extends {
|
|
@@ -61,7 +61,9 @@
|
|
|
61
61
|
* 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
|
|
62
62
|
* 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
|
|
63
63
|
*/
|
|
64
|
-
export declare const CLASSIFIER_DENY_CAUSES: readonly
|
|
64
|
+
export declare const CLASSIFIER_DENY_CAUSES: readonly ["unavailable", "parse_error"];
|
|
65
|
+
/** {@link CLASSIFIER_DENY_CAUSES} 的成员型(端的型面改派生,不再手抄两词)。 */
|
|
66
|
+
export type ClassifierDenyCause = (typeof CLASSIFIER_DENY_CAUSES)[number];
|
|
65
67
|
/**
|
|
66
68
|
* 这个词是不是 {@link CLASSIFIER_DENY_CAUSES} 的成员(core `isClassifierDenyCause` 的镜像)。
|
|
67
69
|
*
|
|
@@ -71,7 +73,7 @@ export declare const CLASSIFIER_DENY_CAUSES: readonly string[];
|
|
|
71
73
|
* 所以在这一格上按闭集判不会「把一个合法的新词吞成缺席」——真读到表外词只说明那条记录本不该长
|
|
72
74
|
* 这样,而把它渲成一句成因就是替引擎编事实。(`deniedBy` 那张表在 `gateVocabulary.ts` 上同一条。)
|
|
73
75
|
*/
|
|
74
|
-
export declare function isClassifierDenyCause(v: unknown):
|
|
76
|
+
export declare function isClassifierDenyCause(v: unknown): v is ClassifierDenyCause;
|
|
75
77
|
/**
|
|
76
78
|
* 一条**门记录**(`tool_end.gate` / `PermissionDeniedPayload.gate` / 耐久行的 resolved outcome,
|
|
77
79
|
* 或本包 `gateOutcomeOf` 投出的 {@link import('./gateOutcome.js').GateOutcomeView})→
|
|
@@ -89,7 +91,7 @@ export declare function isClassifierDenyCause(v: unknown): boolean;
|
|
|
89
91
|
* 🔴 **缺席不是断言**:缺席同时覆盖「分类器自己裁决 block 了」「这次不是分类器轮」「本部署没接分类器」
|
|
90
92
|
* 三形,端**禁**读成「分类器好着呢」。
|
|
91
93
|
*/
|
|
92
|
-
export declare function classifierDenyCauseOf(gate: unknown):
|
|
94
|
+
export declare function classifierDenyCauseOf(gate: unknown): ClassifierDenyCause | undefined;
|
|
93
95
|
/**
|
|
94
96
|
* 一轮分类**为什么**跑不了(core `AUTO_MODE_UNAVAILABLE_CAUSES`;逐词逐序镜像,core 7.12.0 起两词)。
|
|
95
97
|
* · `error` —— 模型那条腿抛了/被拒(分类时的路由失败也读在这里:派生路由的前置在任何 decide
|
|
@@ -109,10 +111,16 @@ export declare const AUTO_MODE_UNAVAILABLE_CAUSES: readonly string[];
|
|
|
109
111
|
*
|
|
110
112
|
* 🔴 **按自有属性查表**(与本包其余措辞铸点同一条纪律):`Object.freeze` 不移除原型,裸下标会让
|
|
111
113
|
* 一个来自 wire 的 `constructor` / `toString` 命中 `Object.prototype` 上的**函数**并被当成一句话。
|
|
112
|
-
* 🔴
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
114
|
+
* ── 🔴 0.68.0 / L-245:入参从 `unknown` 收窄成**闭集成员型** ─────────────────────────────────
|
|
115
|
+
* 修前这里留着一句「its reported cause X **is a word newer than this client**」的兜底,而它
|
|
116
|
+
* **结构上走不到**:唯一到达本铸点的路是 {@link classifierDenyCauseOf},那一层已经按闭集把表外词
|
|
117
|
+
* 判成了缺席(理由见该函数:出闭集的 cause 在 core 那边是**记录缺陷**,server 整条不上帧 ⇒ 一个
|
|
118
|
+
* 表外词根本到不了消费端)。一条走不到的兜底有两重坏处:① 它假装这一面是开集,于是没人给这张表
|
|
119
|
+
* 配编译期围栏,core 加词那天这里一声不响;② 它说的那句话是**假的** —— 真有一个「比这一端新」的词
|
|
120
|
+
* 时,它压根不会到这里。
|
|
121
|
+
* ⇒ 入参收窄(表外词现在是**编译期**错误)+ 表型 `Record<ClassifierDenyCause, string>`(加词当天
|
|
122
|
+
* 缺键红)。剩下的运行期兜底只服务一种情形:调用方 cast 绕过型面、或从旧持久态恢复出一个非成员值
|
|
123
|
+
* —— 那时它说的是「这个词不在本端的闭集里」(一句真话),而**不再**冒充「上游比我新」。
|
|
124
|
+
* 🔴 渲染路径**不许抛**(本仓已定谳的病形),所以 never 分支照样交一句话,不 throw。
|
|
117
125
|
*/
|
|
118
|
-
export declare function classifierDenyCauseDetail(cause:
|
|
126
|
+
export declare function classifierDenyCauseDetail(cause: ClassifierDenyCause): string;
|
|
@@ -61,6 +61,9 @@
|
|
|
61
61
|
* 🔴 形制:`Object.freeze` 的数组,**不是**只在类型面只读的 `readonly T[]` —— 后者一行 `.splice()`
|
|
62
62
|
* 就能改,而公面消费者拿到的正是这个实例(本仓已定谳的病形,同 `RESUME_RETRY_LATER_CODES`)。
|
|
63
63
|
*/
|
|
64
|
+
// 🔴 0.68.0 / L-245:形制从 `readonly string[]` 改成**字面元组**(`as const`)—— 型面交得出成员
|
|
65
|
+
// 字面量,端的型面才能**派生**而不是再抄一遍两个词(与 `runTerminal.ts` 的两表分源同一条medicine)。
|
|
66
|
+
// 冻结的理由一字未改(公面消费者拿到的正是这个实例)。
|
|
64
67
|
export const CLASSIFIER_DENY_CAUSES = Object.freeze(['unavailable', 'parse_error']);
|
|
65
68
|
/** {@link CLASSIFIER_DENY_CAUSES} 的运行期成员判据(判据用它,别在端上再抄一张表)。 */
|
|
66
69
|
const DENY_CAUSE_SET = new Set(CLASSIFIER_DENY_CAUSES);
|
|
@@ -103,6 +106,8 @@ export function classifierDenyCauseOf(gate) {
|
|
|
103
106
|
const dd = d;
|
|
104
107
|
if (dd.kind !== 'denied')
|
|
105
108
|
return undefined;
|
|
109
|
+
// 🔴 0.68.0 / L-245:出参改**闭集成员型**(谓词已是型守卫)—— 读器闭集进、闭集出,措辞铸点
|
|
110
|
+
// 那一层的入参因此也能收窄,那条「结构不可达的兜底」于是变成编译期就闭的事(见下面那段)。
|
|
106
111
|
return isClassifierDenyCause(dd.cause) ? dd.cause : undefined;
|
|
107
112
|
}
|
|
108
113
|
/**
|
|
@@ -142,6 +147,11 @@ export const AUTO_MODE_UNAVAILABLE_CAUSES = Object.freeze(['error', 'timeout']);
|
|
|
142
147
|
* 重试解决不了,要去看那一轮的裁决散文 / 调分类器。
|
|
143
148
|
* 🔴 两句**不许合并**:合并等于把「稍后重试」与「别重试」渲成同一句。
|
|
144
149
|
*/
|
|
150
|
+
// 🔴 0.68.0 / L-245:表型从 `Record<string, string>` 改成 **`Record<ClassifierDenyCause, string>`**。
|
|
151
|
+
// 这一改就是本件真正的「never 分支断言」:core 哪天加第三个词,元组长一员 ⇒ 成员型多一员 ⇒
|
|
152
|
+
// **这张表少一个键** ⇒ **编译期当场红**,逼人同批补那一句话。修前是 `Record<string,…>`,加词那天
|
|
153
|
+
// 这里一声不响,靠的是运行期那句「a word newer than this client」的兜底 —— 而那句兜底**结构上
|
|
154
|
+
// 走不到**(读器在上一层就按闭集把表外词判成缺席),所以它既拦不住漂移、也从来没说过话。
|
|
145
155
|
const DENY_CAUSE_SENTENCES = Object.freeze({
|
|
146
156
|
unavailable: 'denied because the auto-mode classifier could not run this round (the call may be retried later)',
|
|
147
157
|
parse_error: 'denied because the auto-mode classifier answered outside its contract (its reply could not be parsed, so the call was blocked)',
|
|
@@ -151,18 +161,26 @@ const DENY_CAUSE_SENTENCES = Object.freeze({
|
|
|
151
161
|
*
|
|
152
162
|
* 🔴 **按自有属性查表**(与本包其余措辞铸点同一条纪律):`Object.freeze` 不移除原型,裸下标会让
|
|
153
163
|
* 一个来自 wire 的 `constructor` / `toString` 命中 `Object.prototype` 上的**函数**并被当成一句话。
|
|
154
|
-
* 🔴
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
164
|
+
* ── 🔴 0.68.0 / L-245:入参从 `unknown` 收窄成**闭集成员型** ─────────────────────────────────
|
|
165
|
+
* 修前这里留着一句「its reported cause X **is a word newer than this client**」的兜底,而它
|
|
166
|
+
* **结构上走不到**:唯一到达本铸点的路是 {@link classifierDenyCauseOf},那一层已经按闭集把表外词
|
|
167
|
+
* 判成了缺席(理由见该函数:出闭集的 cause 在 core 那边是**记录缺陷**,server 整条不上帧 ⇒ 一个
|
|
168
|
+
* 表外词根本到不了消费端)。一条走不到的兜底有两重坏处:① 它假装这一面是开集,于是没人给这张表
|
|
169
|
+
* 配编译期围栏,core 加词那天这里一声不响;② 它说的那句话是**假的** —— 真有一个「比这一端新」的词
|
|
170
|
+
* 时,它压根不会到这里。
|
|
171
|
+
* ⇒ 入参收窄(表外词现在是**编译期**错误)+ 表型 `Record<ClassifierDenyCause, string>`(加词当天
|
|
172
|
+
* 缺键红)。剩下的运行期兜底只服务一种情形:调用方 cast 绕过型面、或从旧持久态恢复出一个非成员值
|
|
173
|
+
* —— 那时它说的是「这个词不在本端的闭集里」(一句真话),而**不再**冒充「上游比我新」。
|
|
174
|
+
* 🔴 渲染路径**不许抛**(本仓已定谳的病形),所以 never 分支照样交一句话,不 throw。
|
|
159
175
|
*/
|
|
160
176
|
export function classifierDenyCauseDetail(cause) {
|
|
161
|
-
|
|
177
|
+
// 🔴 **按自有属性查表**(与本包其余措辞铸点同一条纪律):`Object.freeze` 不移除原型,一个被
|
|
178
|
+
// cast 进来的 `constructor` / `toString` 会命中 `Object.prototype` 上的**函数**并被当成一句话。
|
|
179
|
+
const known = Object.hasOwn(DENY_CAUSE_SENTENCES, cause)
|
|
162
180
|
? DENY_CAUSE_SENTENCES[cause]
|
|
163
181
|
: undefined;
|
|
164
|
-
if (known
|
|
182
|
+
if (typeof known === 'string')
|
|
165
183
|
return known;
|
|
166
184
|
const word = typeof cause === 'string' && cause.length > 0 ? cause : '(none)';
|
|
167
|
-
return `denied by the auto-mode classifier lane; its reported cause ${word} is
|
|
185
|
+
return `denied by the auto-mode classifier lane; its reported cause ${word} is not one of the causes this client's closed set knows`;
|
|
168
186
|
}
|
|
@@ -20,6 +20,32 @@
|
|
|
20
20
|
* `AUTO_MODE_UNAVAILABLE_CAUSES` 的理由)。
|
|
21
21
|
*/
|
|
22
22
|
export declare const CLASSIFIER_STATUS_STATES: readonly string[];
|
|
23
|
+
/**
|
|
24
|
+
* 「本轮那一次观测」的**窄型面**(0.68.0 / L-245 B6)。
|
|
25
|
+
*
|
|
26
|
+
* ── 为什么这个型要有名字 ────────────────────────────────────────────────────────────────────
|
|
27
|
+
* 修前第二参是裸 `unknown`,而端手里常常只有**两格裸串**(`disposition.kind` / `disposition.cause`)——
|
|
28
|
+
* 于是壳把它们**铸回一个假门记录** `{gate:{disposition:{kind,cause}}}` 再喂进来(cli
|
|
29
|
+
* `classifierRoundObservation.ts` 的那一处)。那是一条「为了过读器而伪造上游形状」的路:伪造出来的
|
|
30
|
+
* 那层 `gate` 在 wire 上根本不存在,读者会以为端手里有一整只门记录,而下一次读器换键路时端还得
|
|
31
|
+
* 跟着改自己的伪造件。
|
|
32
|
+
* ⇒ 给它一个名字,并把**三条合法入形**写进型面:
|
|
33
|
+
* · `{ disposition }` —— 端手里只有处置那一格时**直接给这一格**(不必再铸一层 `gate`);
|
|
34
|
+
* · `{ gate }` —— 整只 `tool_end` 帧 / 一条耐久 park 行(键路 `gate.disposition`);
|
|
35
|
+
* · `{ origin }` —— 「分类器真的跑过」那条**肯定事实**的载体。
|
|
36
|
+
* 🔴 索引签名是**故意**的:整只 wire 帧(键远不止这三个)照样喂得进来,端零改造;而 `unknown`
|
|
37
|
+
* 类型的变量从此喂不进来 —— 那正是「端先自己判一下手里是什么」的那一步,也是伪造件消失的地方。
|
|
38
|
+
*/
|
|
39
|
+
export interface ClassifierRoundObservation {
|
|
40
|
+
/** 门记录上的处置(`{kind:'denied', cause}`)。端手里只有这一格时直接给它。 */
|
|
41
|
+
readonly disposition?: unknown;
|
|
42
|
+
/** 整只帧 / 耐久行上的门记录(键路 `gate.disposition`)。 */
|
|
43
|
+
readonly gate?: unknown;
|
|
44
|
+
/** 这只 ask 的出身词(`askOriginOf` 读它;`CLASSIFIER_RAN_ORIGINS` 的成员 = 分类器真跑过)。 */
|
|
45
|
+
readonly origin?: unknown;
|
|
46
|
+
/** 整只 wire 帧上的其余键(读器一个都不读;留索引签名是为了端零改造)。 */
|
|
47
|
+
readonly [extra: string]: unknown;
|
|
48
|
+
}
|
|
23
49
|
/** 一次状态读数。 */
|
|
24
50
|
export interface ClassifierStatusView {
|
|
25
51
|
/** {@link CLASSIFIER_STATUS_STATES} 之一。 */
|
|
@@ -31,9 +57,11 @@ export interface ClassifierStatusView {
|
|
|
31
57
|
* 「这个会话上,auto 分类器现在是什么状态」——三态,或 `undefined`(**说不出来**)。
|
|
32
58
|
*
|
|
33
59
|
* @param autoMode `wiring_manifest` 的 `autoMode` 段(投影后的或原始的都吃;本函数自己窄读)
|
|
34
|
-
* @param ask 可选:**本轮那一次观测**
|
|
35
|
-
* 载荷 / 一只带 `gate` 的 `tool_end`
|
|
36
|
-
*
|
|
60
|
+
* @param ask 可选:**本轮那一次观测**({@link ClassifierRoundObservation};0.68.0 / L-245 起
|
|
61
|
+
* 有名字)—— 一只 ask / 一条 durable park 行的 `tool_approval` 载荷 / 一只带 `gate` 的 `tool_end`
|
|
62
|
+
* 帧 / 一条门记录本体 / **只有 `{disposition}` 那一格**。键路同形,同一把读器吃(0.67.0:否定事实
|
|
63
|
+
* 的载体从 ask 上的 `classifierUnavailable` 改成门记录上的 `disposition.cause`)。
|
|
64
|
+
* 🔴 端**不必**为了过这只读器去铸一个假门记录:手里只有两格裸串时,直接交 `{disposition:{kind,cause}}`。
|
|
37
65
|
*
|
|
38
66
|
* 优先序(承重,理由见模块顶注):**本轮事实(否定 + 肯定)> 这条腿的 `armed`**——先看观测座上的
|
|
39
67
|
* 本轮不可用事实,再看那条「分类器真的跑过」的肯定事实,最后才看这条腿武没武装。
|
|
@@ -45,7 +73,7 @@ export interface ClassifierStatusView {
|
|
|
45
73
|
* 两种情形都**绝不**折成 `available`(那是把「没报」渲成「一切正常」)。
|
|
46
74
|
* ⚠️ 「没武装」本身仍是一条要渲的事实 —— 但它的出处是 `autoMode.reason`,不是本读器。
|
|
47
75
|
*/
|
|
48
|
-
export declare function classifierStatusOf(autoMode: unknown, ask?:
|
|
76
|
+
export declare function classifierStatusOf(autoMode: unknown, ask?: ClassifierRoundObservation): ClassifierStatusView | undefined;
|
|
49
77
|
/**
|
|
50
78
|
* 一次状态读数 → 一句人话。**唯一措辞铸点**(三端共用;端零自拼)。
|
|
51
79
|
*
|