@sema-agent/client-core 0.65.1 → 0.66.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 +61 -0
- package/README.md +3 -1
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +172 -1
- package/dist/adapter/downstream/terminalToSdkResult.js +239 -7
- package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +20 -1
- package/dist/adapter/downstream/turnUsageToModelUsage.js +7 -2
- package/dist/adapter/runStream.js +38 -4
- package/dist/seam.d.ts +29 -0
- package/dist/seam.js +9 -0
- package/docs/INTEGRATION-CLIENTS.md +279 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -49,6 +49,67 @@
|
|
|
49
49
|
> 挡住 ⇒ 本批把它机械化——④a0 对 `pending` 行**要求段头已是日期形**(`(未发布)` 直接红),阶段一
|
|
50
50
|
> commit 漏转在发布前就红,不再靠人记。
|
|
51
51
|
|
|
52
|
+
## 0.66.0(2026-09-11)
|
|
53
|
+
|
|
54
|
+
> **终局真值三件**(cli 台账 L-192①② / B-068 · DEBTS L-198):三件都是同一条病形 ——
|
|
55
|
+
> 上游把事实摆在 wire 上,而包边界用一个**字面量**把它答成了常数。处置也只有一条:CC 形上必填的位
|
|
56
|
+
> **值不动**(旧消费者逐字不变),「不知道」交给同行的 `_sema_` 判别位;CC 形上没有的事实另开超集座位,
|
|
57
|
+
> **绝不改 CC 同名键的语义**。接入文档 §31(速览 / 逐件键表与缺席语义 / 黑盒判据 G31-1..G31-16 /
|
|
58
|
+
> 三端待办 / 本批的门)。判据编号 G31-1..G31-18。
|
|
59
|
+
|
|
60
|
+
- **① L-192① 终帧拒绝清单不再硬编 `[]`**(`adapter/downstream/terminalToSdkResult.ts`,成功臂与错误
|
|
61
|
+
信封**两处**):按 `TaskStats.humanReview.gates[]` 里 `decision === "deny"` 的行**逐条**铸记录,
|
|
62
|
+
顺序 = wire 顺序、条数 = 被拒次数,键按 wire 能兑现的铸(`tool_name` ⇐ `toolName`;入参摘要
|
|
63
|
+
`_sema_tool_arg` ⇐ `toolArg`,引擎侧已脱敏截短)。🔴 **两条清单**:CC 的 `SDKPermissionDenial`
|
|
64
|
+
三键**全是必填**,而 core 的 gate 账本给不出 `tool_use_id` / `tool_input`(逐字:耐久 resume 的 gate
|
|
65
|
+
只带 `toolName`)—— 补零补空是**编造**,把半条记录塞进 CC 数组则**破坏元素契约**(严格消费方
|
|
66
|
+
`safeParse` 会把**整条 result 帧**判非法)⇒ CC 数组只收三键齐全的记录(今天恒空,上游补齐后
|
|
67
|
+
**自动**开始填,包侧一行不用改),wire 上真有的每一条走超集载体 `_sema_permission_denials`。
|
|
68
|
+
顶层判别位 `_sema_permission_denials_absent: true` = 「CC 那条清单不可声称完整」,四条路径:
|
|
69
|
+
账本读不出(老引擎 / 409 拒绝信封 / `failed` 事件帧)/ 有读不出的行 / 有**认不出的判词**
|
|
70
|
+
(两张判词表之外的词、以及缺判词的耐久 wake 行)/ 有记录只在超集载体上。⇒「零拒绝」与
|
|
71
|
+
「清单不完整」从此可分。门 `run-permission-denial-projection-test.mjs`(新,68 checks,含**缺席证据**
|
|
72
|
+
与**自动升级腿**的正控:上游哪天把两格串上账本当场红/当场填)。
|
|
73
|
+
- **② B-068 / L-198 终局成本对账**(同文件 + `adapter/runStream.ts` + `seam.ts`):`TaskStats.costBreakdown`
|
|
74
|
+
与 `nested` 两段投上终帧超集键 `_sema_cost_breakdown` / `_sema_nested_usage`,**micro-USD 原值不折 USD**,
|
|
75
|
+
逐键窄读、坏键剥掉不连坐、整段读不出不铸空对象、上游新类目原样过境;nested 的 `costMicroUsd` 缺席 =
|
|
76
|
+
委派花费**没定价** ⇒ 键缺席,绝不铸 0。新增 chrome 臂 **`run_cost_reconciled`**(`required: false`):
|
|
77
|
+
终局那一拍交出 own / nested / compaction 三段 + `reconciledMicroUsd = own + nested`,任一段不知道
|
|
78
|
+
(或和本身非有限)则只铸 `costAbsent` / `nestedCostAbsent` 判别位而**不铸**对账值;`stats` 读不出的
|
|
79
|
+
终帧(409 / park 体 / `failed` 事件帧 / 畸形载体)一条都不发,非成功终局照发。🔴 「委派过」的判据是
|
|
80
|
+
`stats.nested` **这个载体在不在**,不是它里面有没有读得出的数 —— 否则 `nested: {}` 会被当成「没委派」
|
|
81
|
+
而把 own 铸成一个**确定的总额**。🔴 **CC 同名键 `total_cost_usd` 语义一字未改**(仍是 own 花费,
|
|
82
|
+
nested 不折进去);对账值由消费方按超集键自己加。🔴 子流 `turn_end` 仍不发 `last_turn_usage`(既有断闸
|
|
83
|
+
一字未动)—— 子代花费只经终局 `nested` 到账,端**不要**把流中增量与终局总账相加。
|
|
84
|
+
新增公面导出 **`readRunCostFacts`**(终帧两个超集键与 chrome 臂**共用**它 ⇒ 两面不会各算各的)。
|
|
85
|
+
门 `run-cost-reconcile-projection-test.mjs`(新,76 checks)。
|
|
86
|
+
- **③ L-192② 终帧扁平 `usage` 三格**(同文件 + `adapter/downstream/turnUsageToModelUsage.ts`):
|
|
87
|
+
`webSearchRequests` 从**字面量 0** 改按 CC 的名字开集宽读,读不出时值仍 0(CC 形必填 number)+ 同行
|
|
88
|
+
判别位 `_sema_web_search_requests_absent` —— **两个 mint 点**(终帧扁平 usage 与 CC `ModelUsage` 镜像)
|
|
89
|
+
同扫,同形不留第二处。新增 cache-INCLUSIVE 总量座位 `usage._sema_total_input_tokens`
|
|
90
|
+
(⇐ `TaskStats.totalInputTokens`);终帧此前**没有任何载体**能说出「这条 run 摆了多少上下文」。
|
|
91
|
+
同批按**同一条理由**给**终局 per-model 行**补三座位(`_sema_total_input_tokens` /
|
|
92
|
+
`_sema_usage_basis` 口径标记 / `_sema_cache_write_tokens_long`)—— 一个全局总量答不了「哪个模型摆了
|
|
93
|
+
多少」,而那张分表同样没有逐字通道;加挂只发生在两条**终局**腿上,per-turn 与终局共用的那个 mint 点
|
|
94
|
+
一个字不动(门里有反向钉)。扁平 usage 同批补 `_sema_cache_write_tokens_long`:🔴 **只另给不相加** ——
|
|
95
|
+
core 对 `cacheWriteTokens` 与 `cacheWriteTokensLong` 的说法互相矛盾(前者自述是协议侧那个**含 1h 子项**
|
|
96
|
+
的量,而总量式又把两格并列相加),相加与不加各有一种错法,**证据不足不猜**,CC 那一格逐字不动。
|
|
97
|
+
🔴 **`inputTokens` 逐字不动**,仍是 cache-**MISS** 分量:台账原句要求改读 `totalInputTokens`,核合同后
|
|
98
|
+
**不采** —— core RB-457-a 把 `promptTokens` 翻成 MISS 分量而 CC/Anthropic 的 `input_tokens` 本义正是 MISS,
|
|
99
|
+
灌总量会与同帧两个 cache 格**双算**(core 逐字:98% 命中率被渲成 49.5%),那是改 CC 同名键语义不是超集。
|
|
100
|
+
理由与「本批刻意不投的位」(`contextWindow`/`maxOutputTokens`、`cacheHitRate`/`toolCalls`/`mechanisms` 等)
|
|
101
|
+
逐条登记在 §31d。门 `run-cost-absence-projection-test.mjs` 扩 E/F/G/H 四段(144 checks)。
|
|
102
|
+
- **④ 异源对抗复审(查漏轨)采纳四件**:①**[high]** 终局 chrome 回调的**异步**拒绝此前会外溢成
|
|
103
|
+
`unhandledRejection`(端口契约是 `void | Promise<void>`,而 `try/catch` 只接得住同步抛)——
|
|
104
|
+
把未处理拒绝当致命的宿主会整只退出;修 = 新增共用发射口 `emitChromeFireAndForget`(同步抛在
|
|
105
|
+
`try` 里吞、异步拒绝挂 `.catch`),**同形族扫**把既有的 `last_turn_usage` 那条腿一并收编,语义
|
|
106
|
+
逐字不变(不 await、不改时序);②nested 存在性口径(见上);③CC 元素契约(见 ①);
|
|
107
|
+
④两处同形漏键(长 TTL 分量 / 终局 per-model 三座位)。
|
|
108
|
+
- **消费方影响**:只读既有键的端**零改**(CC 必填位的值在同样输入下一字不变);要新事实的端按 §31e
|
|
109
|
+
三端待办接。超集键 10 行登记进 `docs/type-superset.json`(常驻门双向对账);`unknown` 出境棘轮
|
|
110
|
+
320→321(逐条记账在 `run-client-core-typeshape-test.mjs`:`SemaPermissionDenial.tool_input` 是
|
|
111
|
+
CC 契约**本身**的形 `Record<string, unknown>`)。
|
|
112
|
+
|
|
52
113
|
## 0.65.1(2026-09-11)
|
|
53
114
|
|
|
54
115
|
test [6961] 对 0.65.0 十件五路交叉验证全 PASS,对抗复审轨另抓两处带坐标的实现缺口(cli 亲核源码成立,两处都是 patch):
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
35
35
|
|
|
36
36
|
## Scope
|
|
37
37
|
|
|
38
|
-
**Version:** 0.
|
|
38
|
+
**Version:** 0.66.0
|
|
39
39
|
|
|
40
40
|
- **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
|
|
41
41
|
B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
|
|
@@ -295,6 +295,8 @@ public-surface guard checks that last one).
|
|
|
295
295
|
| `scripts/run-classifier-status-test.mjs` | What state the auto-mode classifier is in **on this session** — the question a doctor line, a model settings page and a permission card’s status row all ask, and a different question from the one the approval card asks (*why am I being asked right now*), so the sentences are pinned mutually distinct from that face’s as well as from each other. The session-level half of this reading — a breaker record the engine used to keep — was **retired upstream**, and the guard now holds that retirement from **both** sides: the engine's own declarations must really no longer carry it (a fact coming back would mean the removal here was the wrong disposition, and that deserves a conversation rather than silence), and this package must carry no alias, no state word and no leftover narrowing for it — a reading kept alive for something nobody emits any more is a promise the interface cannot keep, and it left the doctor line advertising a state it can never reach. What remains is ordered by the quantity that actually decides whether the classifier is running: the fact from **this round** first, then whether this leg is armed — a decider is minted per run, so a later leg can be armed again. Not armed, and a section that never arrived, both answer **undefined** rather than *available*; that arming question has its own field and answering it twice grows a second ledger. Arming and availability are also **two words, not one**: the engine says a decider was minted *for this leg*, which is an assembly-time fact, while whether that decider answers any given round is a **per-call** one — so an armed leg reads `armed` and only a positive per-call fact (an ask whose origin is the classifier's own denial-bound fallback, which by construction stands *after* the classifier ran) reads `available`. Every other ask origin is refused as evidence and for a stated reason rather than out of caution: several are ones the classifier is structurally forbidden to answer, and for the rest a surviving ask is precisely the case where it did **not** resolve one — so reading availability off them would be a guess. The projection is a **whitelist**, so an older engine still sending the retired member loses it at the boundary while the two live facts beside it ride through untouched. Rendering never throws and never impersonates: a state word this client does not know — including the retired one, which a restored view can still carry — reaches an honest fallback that names it verbatim, carries no invented explanation of a mechanism that no longer exists, and is proven distinct from all three real sentences; prototype keys reach that same fallback rather than a function body, checked against a real out-of-table word so the comparison cannot hold vacuously |
|
|
296
296
|
| `scripts/run-compaction-boundary-projection-test.mjs` | The compaction divider and the one frame that makes its anchor resolvable. The trigger word is passed through as an **open set** instead of being folded to two: the engine deliberately stopped flattening its third value (a compaction that was not optional — a prompt-too-long recovery or trim pressure) and carries what the hook layer saw, so folding it again at the package boundary re-introduces exactly what upstream had just removed, while a consumer branching on *is it manual* keeps its behaviour byte for byte. Only an unreadable word (absent, empty, non-string) falls back — that is *could not read it*, not *read it and did not recognise it*. Two superset keys ride the metadata and neither fabricates: the preserved-segment anchor is minted only when its id really reads out, because half an anchor sends the host looking up an empty string in its map, and the clamp ratio is a **disclosure** whose real zero is a fact rather than an absence. The clamp ratio also carries a registered exit condition — the service really sends it while the SDK arm has no seat for it yet, so the read is defensive and this guard reds the day that seat appears, forcing a re-check instead of leaving a cast to rot. The committed-message frame moves out of *deliberately not projected*: that classification was true about transcript rows and false about **positioning**, since the engine states that consumers build their own id-to-message map from this frame to place the divider — projecting the anchor without it hands the host something it cannot resolve. It becomes a neutral internal arm and an optional chrome ledger event, never a transcript row (the frame carries no body, so minting one would put words in the engine's mouth), with both required ids narrowed and a malformed frame recorded rather than half-minted |
|
|
297
297
|
| `scripts/run-cost-absence-projection-test.mjs` | Telling **declared free** apart from **never priced**, in both directions, because the package was getting each one wrong in the opposite way. The engine separates them on the wire — an absent cost means some spend had no price table, an explicit zero means the model declared itself free — and the result projector used to require a *positive* number, so a genuinely free run could not say so; while the per-model mirror folded absence to zero, so an unpriced run told a billing consumer it cost nothing. The total is now reported as the engine stated it, with absence and non-finite values alone reading as unknown, and a negative passed through rather than corrected, since a refund is a legal figure and the package is not a second accountant. The per-model figure keeps the CC shape intact — that field is a required number and *unknown* is simply not expressible in it — so the value stays zero and a **companion superset bit** carries the distinction, which means the two are read together and a reader that only ever looked at the number is unchanged; the bit is minted only in the absent case and never as `false`, since a key present with a false value reads as a third state. The same mint point serves both the wire's per-model split and the synthesised current-model row, so neither can drift. Alongside it the cache-write figure stops being a hardcoded zero and reads the field the wire has always carried, in both the flat usage and the synthesised row, and all four flat token slots move from a null-coalesce to a finite-number guard — the stats object has an open index signature and the wire is JSON, so a string or an infinity would otherwise land in a slot the types promise is a number, compiling green and surfacing only when something sums it |
|
|
298
|
+
| `scripts/run-permission-denial-projection-test.mjs` | The terminal result's **permission-denial list** being the wire's real one rather than a hardcoded empty array. The session vocabulary carries a list of tool calls that were denied; the projector used to mint `[]` in both the success arm and the error envelope, which folded two different statements into one — *nothing was denied on this run* and *this frame carries no such ledger at all* (an older engine, a rejection envelope, a failure event that arrives without stats) looked identical. Each denied gate on the wire's human-review ledger now becomes one record, in wire order, carrying the keys the wire can actually honour: the tool name when it reported one, and a superset field with the engine's own short, redacted one-line summary of the call's input. **Two lists, deliberately.** The reference shape requires three fields on every element — tool name, call id, and the full input object — and the wire's ledger carries only the first. Filling the other two with an empty string and an empty object would be invention; putting a half-filled element into the reference array would break the element contract, and a strict consumer validating the stream drops the *whole* result message rather than one field. So the reference array admits only fully-formed records — empty today, and filling itself the day the wire grows the two missing fields, with no code change — while every record the wire really has rides a superset carrier beside it. A contract check pins today's absence, so that day turns this guard red on purpose. The companion bit means *this reference list cannot be claimed complete*: no ledger, an unreadable row, an unrecognised decision word (a rejected plan is not a denied tool call, and a row with no decision at all is not a judgement), or a record that could not be fully formed. Only its absence lets a reader say *zero denials*; it is never minted as `false`. Rows that cannot be read drop themselves rather than the whole ledger, and both arms go through one mint point so they cannot drift |
|
|
299
|
+
| `scripts/run-cost-reconcile-projection-test.mjs` | The **end-of-run cost reconciliation** reaching consumers at all. The engine splits a run's spend on the wire — the task's own cost, which deliberately excludes delegated sub-agents, the delegated total itself, and the within-task compaction subtotal that sits inside the own figure — and states two reconciliation identities for them. The package used to project none of it, so a cost view could only ever see one number and under-reported both delegated and compaction spend. Both structures are now projected onto the result as superset fields in the wire's integer micro-currency unit, read key by key, with unreadable keys dropped individually, an entirely unreadable structure omitted rather than emitted empty, and unknown categories passed through since the vocabulary belongs upstream. The delegated cost stays **absent when it was never priced**, never a fabricated zero. The same reader also feeds a terminal chrome arm carrying the three parts plus the reconciled total, so the two faces can never compute different answers; the reconciled total is minted only when both sides are known, and otherwise a discriminator bit says which side is unknown. **The reference field for total cost keeps its meaning** — it remains the task's own spend and the delegated total is not folded into it — because that is a shape the wider ecosystem reads; the reconciled figure is offered beside it, not in place of it. A frame that carries no stats emits no arm at all, and the existing rule that in-stream per-turn usage is not published for sub-flows is pinned unchanged, since delegated spend arrives once, at the end |
|
|
298
300
|
| `scripts/run-task-progress-terminal-projection-test.mjs` | The one tick that says a delegated child **finished**. The engine fires exactly one final beat carrying a terminal face, and says in the same breath why it exists — so a consumer sees the row finish instead of watching it vanish after the last running beat — but the package's projection whitelist had no seat for that field and its adapter still carried the older premise in a comment, so the terminal beat arrived byte-identical to another running one: the panel row stayed up waiting for a defensive sweep (which only ever settles rows bound to a card still open this turn) or for a separate notification frame. The status now rides through as an **open set** with the vocabulary left upstream, while the question *which words are terminal* is answered by a closed pair on the adapter side — an unrecognised new word takes the running path, because guessing it terminal ends a row that is still working whereas one extra running beat merely renders late. A terminal beat settles the row directly under the lane proof its binding gives it (not the main lane a notification would use, and not by card id, since the engine is naming a child rather than closing a card), freezes the inline group-row twin in the same beat so a later sweep cannot reset the real tool count, clears the session-resident ledger, and fires the stop hook only for a child whose start really fired. It does not mark the row live or emit a second progress beat, and it shares the settled-row ledger with the other two settle legs so a replay or a double-delivery cannot produce a second end. Three things are pinned **unchanged**: a running beat, an absent status (older engines never send the field, and reading absence as terminal would make every child row disappear on its first beat), and the workflow lane gate, which still runs before any of this |
|
|
299
301
|
| `scripts/run-assistant-arm-identity-test.mjs` | The identity keys on an assistant row, and an explicit account of the two that are **deliberately not** there. What the renderer received was a bare role-and-content object, so a dozen consumer sites downstream were each estimating what the message envelope should have told them. The id is taken from the engine's own event id rather than minted locally, because it has to be **the same value** on the live leg and on a durable replay — a freshly minted one would make a replayed message look new to a host's dedup and to rewind — and when the wire carries none the key is simply absent rather than filled with a random stand-in wearing an identity it does not have; it is also kept distinct from the envelope's own local render key, which is a different identity. The model name comes from what the host pinned when it opened the stream (the request was the host's to build) and is never guessed, since a wrong model name is worse than none once a billing or capability face looks it up. Usage and stop reason are **not** minted on this arm, and the reason is frame order rather than effort: content arms arrive before the turn's closing frame, so at the moment the arm is emitted the engine has not yet said what the round cost — anything put there would be an estimate, which is the very thing this work exists to remove — and synthesising a follow-up assistant update when the real figure lands is also refused, because that shape does not exist upstream and would place a message in the transcript the engine never sent. Their real values leave through the turn's own neutral arm as two superset keys, the usage one reusing the **same single mint point** the footer rollup already folds so the two faces cannot diverge, and the stop reason passed through verbatim as an open set — the machine signal for *was this turn cut short*, previously blind on both the stream and the trace. The existing behaviours beside them are pinned too: no arm at all when usage is wholly absent, and the sub-flow cut-out that keeps a child's turn from driving the leader's face |
|
|
300
302
|
|
|
@@ -75,8 +75,179 @@
|
|
|
75
75
|
* 部署级「这台 worker 到底配没配价表」仍可另问 `Capabilities.pricingConfigured`(contract 02
|
|
76
76
|
* §2.10 VERIFY)。本投影器只做单位换算(/1e6)并原样过境。
|
|
77
77
|
*/
|
|
78
|
-
import type { AgentEvent } from '@sema-agent/sdk';
|
|
78
|
+
import type { AgentEvent, TaskStats } from '@sema-agent/sdk';
|
|
79
79
|
import { type SDKMessage, type EmitContext } from '../types.js';
|
|
80
|
+
import { type SemaModelUsage } from './turnUsageToModelUsage.js';
|
|
81
|
+
/**
|
|
82
|
+
* CC 的 `NonNullableUsage` 占位形(coreSchemas 里是 `z.unknown()`,这里给它一个名字)
|
|
83
|
+
* + 0.66.0 / D-2 的两位 sema 超集(两位都**只在该说话时在场**,绝不铸 `false` / 假 0)。
|
|
84
|
+
*/
|
|
85
|
+
export interface SemaFlatUsage {
|
|
86
|
+
readonly inputTokens: number;
|
|
87
|
+
readonly outputTokens: number;
|
|
88
|
+
readonly cacheReadInputTokens: number;
|
|
89
|
+
readonly cacheCreationInputTokens: number;
|
|
90
|
+
readonly webSearchRequests: number;
|
|
91
|
+
/**
|
|
92
|
+
* D-2 / L-192② —— 在场且为 `true` ⇒ 同行的 `webSearchRequests: 0` 是「**wire 上没有这本账**」,
|
|
93
|
+
* 不是「这条 run 一次网搜都没做」。core 的 `TaskStats` 面上**没有**这一格(网搜只在散文里出现),
|
|
94
|
+
* 而 CC 的这一格型面是必填 `number` ⇒ 「不知道」只能以判别位在场(与 `_sema_cost_absent`
|
|
95
|
+
* 同一条两键合读纪律)。上游哪天真发这一格,值照读、本位退场(门里有正反两控)。
|
|
96
|
+
*/
|
|
97
|
+
readonly _sema_web_search_requests_absent?: true;
|
|
98
|
+
/**
|
|
99
|
+
* D-2 / L-192② —— `TaskStats.totalInputTokens`:**cache-INCLUSIVE** 的输入总量
|
|
100
|
+
* (core 逐字「the quantity cost is computed from … 'how much context did this task present'」)。
|
|
101
|
+
*
|
|
102
|
+
* 🔴 **为什么另开一格而不是灌进 `inputTokens`**:core RB-457-a(3.0.0 BREAKING)把
|
|
103
|
+
* `promptTokens` 的语义翻成 cache **MISS** 分量,而 CC / Anthropic 的 `input_tokens` 本义
|
|
104
|
+
* 正是 MISS —— 同帧已经另有 `cacheReadInputTokens` / `cacheCreationInputTokens` 两格,把总量
|
|
105
|
+
* 灌进 `inputTokens` 就是 core 逐字点名的那笔双算(「a 98% hit rate surfaced as 49.5%」)。
|
|
106
|
+
* 🔴 **为什么这一位必须长在扁平 usage 上**(与 [2295]「CC 形状不承载非 CC 语义」不矛盾,
|
|
107
|
+
* 差别与 `_sema_cost_absent` 那条同源):per-turn 那一面有**逐字通道**
|
|
108
|
+
* (`EngineTurnUsage` / chrome `last_turn_usage.engineUsage`),镜像不必长第二个座位;而
|
|
109
|
+
* **终帧这一面没有任何别的载体** —— 不给座位,这条 run 到底摆了多少上下文在终帧上就问不出来。
|
|
110
|
+
* 缺席 = wire 没报(老引擎 / 网关没报 usage),**绝不**拿 `promptTokens` 冒充总量。
|
|
111
|
+
*/
|
|
112
|
+
readonly _sema_total_input_tokens?: number;
|
|
113
|
+
/**
|
|
114
|
+
* D-2 族扫(0.66.0;异源对抗复审 [medium])—— `TaskStats.cacheWriteTokensLong`:**1 小时 TTL**
|
|
115
|
+
* 那一档的缓存写入分量。缺席 = wire 没报;显式 `0` 是**读数**(core 逐字「0 unless 1h caching
|
|
116
|
+
* is in use」),不是缺席。
|
|
117
|
+
* 🔴 **刻意不求和进 `cacheCreationInputTokens`**:core 对这两格的说法互相矛盾 ——
|
|
118
|
+
* `cacheWriteTokens` 自述是「Anthropic `cache_creation_input_tokens`」(协议侧那个量本就含 1h
|
|
119
|
+
* 子项),而 `totalInputTokens` 的求和式又把两格**并列相加**(那要求它们互不重叠)。相加与不加
|
|
120
|
+
* 各有一种错法,证据不足**不猜**:CC 那一格逐字保持 `cacheWriteTokens`(既有值一字不动),长 TTL
|
|
121
|
+
* 分量原样另给,对账由消费方按两个数自己做。上游澄清后再定(登记见 INTEGRATION §31d)。
|
|
122
|
+
*/
|
|
123
|
+
readonly _sema_cache_write_tokens_long?: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* D-1 / L-192①(0.66.0)—— 一条被拒记录的**全可选**形:CC `SDKPermissionDenial`
|
|
127
|
+
* (agent-types `permissions.d.ts`,真形 `{tool_name: string; tool_use_id: string;
|
|
128
|
+
* tool_input: Record<string, unknown>}` **三键必填**)的三键 + 一位 sema 超集,**每一键都按
|
|
129
|
+
* wire 能不能兑现决定在不在**。
|
|
130
|
+
*
|
|
131
|
+
* 🔴 **今天三键里只兑现得出一键**:这本账在 wire 上是 `TaskStats.humanReview.gates[]`,而 core
|
|
132
|
+
* 的 gate 记录只有五格(`kind` / `waitMs` / `decision?` / `toolName?` / `toolArg?`,
|
|
133
|
+
* `task-result.d.ts` 真字节)—— **没有** `toolCallId`,**没有**被拒时的入参对象(core 逐字:
|
|
134
|
+
* 「a durable-resume gate carries `toolName` only (its input is not threaded onto the persisted
|
|
135
|
+
* gate — a documented follow-on)」;sdk `types.d.ts` 的同一格也逐字记着「`tool_input`/
|
|
136
|
+
* `toolInput` **不在** gate ledger 上」)。⇒ 那两键**缺席**,绝不铸 `""` / `{}`:一个空对象在
|
|
137
|
+
* CC 形上读起来是「这次调用的入参是空的」,那是编的。
|
|
138
|
+
* 🔴 **两键仍然声明在这里**(不是假 affordance):mint 点对它们是**开集宽读** —— 上游哪天把
|
|
139
|
+
* 两格串上 gate 账本,这一形与 CC 那条清单**自动**开始带值(见 {@link permissionDenialParts}
|
|
140
|
+
* 的自动升级腿),门里同时钉着今天的缺席证据与那一天的正控。消费端读它们**必须按可选位读**。
|
|
141
|
+
* 🔴 `_sema_tool_arg` = core 已经**脱敏并截短**的一行入参摘要(`primaryActivityArg` 同一道口),
|
|
142
|
+
* 它是 CC「denied: Bash(rm …)」那行显示唯一拿得到的材料。UNTRUSTED-for-display:只渲染,
|
|
143
|
+
* 绝不回喂模型、绝不当鉴权判据。
|
|
144
|
+
*/
|
|
145
|
+
export interface SemaPermissionDenial {
|
|
146
|
+
/** 被拒的工具名(⇐ `gates[].toolName`);wire 没报 ⇒ 键缺席,绝不编一个名字。 */
|
|
147
|
+
readonly tool_name?: string;
|
|
148
|
+
/** 被拒的**那一次调用**(⇐ `gates[].toolCallId`,今天 wire 上没有 ⇒ 恒缺席,见下方 mint 点头注)。 */
|
|
149
|
+
readonly tool_use_id?: string;
|
|
150
|
+
/** 被拒调用的**完整入参**(⇐ `gates[].toolInput`,今天 wire 上没有 ⇒ 恒缺席;非对象一律不铸)。 */
|
|
151
|
+
readonly tool_input?: Record<string, unknown>;
|
|
152
|
+
/** 被拒调用的一行入参摘要(⇐ `gates[].toolArg`,core 侧已脱敏截短);缺席 = 这条腿没串入参。 */
|
|
153
|
+
readonly _sema_tool_arg?: string;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* D-3 / B-068 · L-198(0.66.0)—— core `TaskStats.costBreakdown`(`task-result.d.ts` 的
|
|
157
|
+
* finance taxonomy)的**窄读投影**,单位 = **micro-USD 原值**(键名即单位,包不折 USD:
|
|
158
|
+
* 折一次就多一次浮点漂移,而这一面正是账单面)。
|
|
159
|
+
*
|
|
160
|
+
* 每一格都是**可选**的:读不出的键不铸(绝不补 0 —— core 明说 unpriced 时整段与 `costMicroUsd`
|
|
161
|
+
* 一起省略,而一个补出来的 0 在账单面上就是一句「这一段没花钱」的假话)。**开集**:core 往这段
|
|
162
|
+
* 里加新类目时原样过境(sdk 的型面是 `costBreakdown?: unknown`,词表属主在 core)。
|
|
163
|
+
*/
|
|
164
|
+
export interface SemaCostBreakdown {
|
|
165
|
+
/** 根 agent 的 LLM 花费 = `costMicroUsd − compactionMicroUsd`(**不减 nested**)。 */
|
|
166
|
+
readonly llmRootMicroUsd?: number;
|
|
167
|
+
/** 委派子代的 LLM 花费 = `nested?.costMicroUsd ?? 0`;它**在 `costMicroUsd` 之外**。 */
|
|
168
|
+
readonly nestedSubagentMicroUsd?: number;
|
|
169
|
+
readonly memoryConsolidationMicroUsd?: number;
|
|
170
|
+
readonly suggestionsMicroUsd?: number;
|
|
171
|
+
/** 任务内压缩的 LLM 花费 —— 它**在 `costMicroUsd` 里面**(所以从 root 里减掉)。 */
|
|
172
|
+
readonly compactionMicroUsd?: number;
|
|
173
|
+
/** 开集:core 新加的类目原样过境(消费方 switch 必须带 default)。 */
|
|
174
|
+
readonly [k: string]: number | undefined;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* D-3 —— core `NestedUsage`(`tool-spec.d.ts`)的窄读投影:这条 run **委派出去**的那本账。
|
|
178
|
+
* 🔴 `costMicroUsd` **缺席 = 委派花费没定价**(core 逐字 `ABSENT when the delegated spend was
|
|
179
|
+
* unpriced (RB-368) — never a fabricated 0`)⇒ 键缺席,绝不铸 0。
|
|
180
|
+
*/
|
|
181
|
+
export interface SemaNestedUsage {
|
|
182
|
+
readonly tokens?: number;
|
|
183
|
+
readonly turns?: number;
|
|
184
|
+
readonly tasks?: number;
|
|
185
|
+
readonly costMicroUsd?: number;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* D-3 —— 终局**对账三段 + 两个判别位**。这正是 chrome 臂 `run_cost_reconciled` 的载荷本体:
|
|
189
|
+
* 两面共用**同一个**读器,所以「终帧超集键」与「chrome 对账臂」永远不会各算各的
|
|
190
|
+
* ([paired-mechanisms-must-share-premise])。
|
|
191
|
+
*
|
|
192
|
+
* core 的两条对账式(`task-result.d.ts` 逐字):
|
|
193
|
+
* · `llmRootMicroUsd + compactionMicroUsd === costMicroUsd`(压缩在 own 里面);
|
|
194
|
+
* · fully-reconciled spend = `costMicroUsd + nested.costMicroUsd`(子代在 own **外面**)。
|
|
195
|
+
* 🔴 本包**不当第二个会计**:三段照实过境,不改数、不补差;`reconciledMicroUsd` 只在**两段都
|
|
196
|
+
* 读得出**时才铸 —— 少了任何一边,总额就是不知道,而「不知道」只能以判别位在场。
|
|
197
|
+
*/
|
|
198
|
+
export interface RunCostReconcile {
|
|
199
|
+
/** 本任务 own 花费(⇐ `costMicroUsd`),**不含**子代。缺席 ⇒ 没定价,见 `costAbsent`。 */
|
|
200
|
+
readonly ownMicroUsd?: number;
|
|
201
|
+
/** 委派子代花费(⇐ `nested.costMicroUsd`)。缺席 ⇒ 没委派、或委派花费没定价(见判别位)。 */
|
|
202
|
+
readonly nestedMicroUsd?: number;
|
|
203
|
+
/** 任务内压缩花费(⇐ `costBreakdown.compactionMicroUsd`);它已含在 `ownMicroUsd` 里。 */
|
|
204
|
+
readonly compactionMicroUsd?: number;
|
|
205
|
+
/** 根 agent 花费(⇐ `costBreakdown.llmRootMicroUsd`);`own − compaction`。 */
|
|
206
|
+
readonly llmRootMicroUsd?: number;
|
|
207
|
+
/** `own + nested` —— core 逐字的 fully-reconciled spend;**任一段不知道就不铸**。 */
|
|
208
|
+
readonly reconciledMicroUsd?: number;
|
|
209
|
+
/** `true` ⇒ own 花费**没定价**(不是 0)。绝不铸 `false`。 */
|
|
210
|
+
readonly costAbsent?: true;
|
|
211
|
+
/** `true` ⇒ 委派过,但那本账**没定价**(不是 0)。绝不铸 `false`。 */
|
|
212
|
+
readonly nestedCostAbsent?: true;
|
|
213
|
+
}
|
|
214
|
+
/** {@link readRunCostFacts} 的产物:两面(终帧超集键 / chrome 对账臂)各取所需。 */
|
|
215
|
+
export interface RunCostFacts {
|
|
216
|
+
/** 终帧 `_sema_cost_breakdown` 的值;一个键都读不出 ⇒ `undefined`(不铸空对象)。 */
|
|
217
|
+
readonly breakdown?: SemaCostBreakdown;
|
|
218
|
+
/** 终帧 `_sema_nested_usage` 的值;同上。 */
|
|
219
|
+
readonly nested?: SemaNestedUsage;
|
|
220
|
+
/** chrome 臂 `run_cost_reconciled` 的载荷本体。 */
|
|
221
|
+
readonly reconcile: RunCostReconcile;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* D-3 / B-068 · L-198 —— 终局成本事实的**唯一读器**(终帧超集键与 chrome 对账臂共用)。
|
|
225
|
+
*
|
|
226
|
+
* 🔴 `stats` 不是可读对象(409 拒绝信封 / park 体 / `failed` 事件帧)⇒ 返 `undefined` =
|
|
227
|
+
* **这条帧没有账**,调用方据此「不说话」(不发臂、不铸键),而不是发一条全缺席的空账。
|
|
228
|
+
*/
|
|
229
|
+
export declare function readRunCostFacts(stats: TaskStats | undefined): RunCostFacts | undefined;
|
|
230
|
+
/**
|
|
231
|
+
* D-2 族扫(0.66.0;异源对抗复审 [medium])—— **终局** per-model 行的 sema 超集位。
|
|
232
|
+
*
|
|
233
|
+
* 🔴 为什么只长在终局这一面:per-turn 那一面有**逐字通道**(`EngineTurnUsage` /
|
|
234
|
+
* chrome `last_turn_usage.engineUsage`,整对象原形过境),镜像不必长第二个座位([2295] 裁 ②);
|
|
235
|
+
* 而**终局的 per-model 分表没有任何逐字通道** —— 不给座位,多模型部署下「哪个模型摆了多少上下文 /
|
|
236
|
+
* 那一行的分量口径可不可信」在终帧上就问不出来。故本形只由 `mapModelUsage` / `modelUsageFor`
|
|
237
|
+
* (两条终局腿)加挂,`toCcModelUsage` 那个共用 mint 点一个字不动。
|
|
238
|
+
*/
|
|
239
|
+
export interface SemaTerminalModelUsage extends SemaModelUsage {
|
|
240
|
+
/** ⇐ 行上的 `totalInputTokens`(cache-INCLUSIVE 总量);缺席 = 这一行没报。 */
|
|
241
|
+
readonly _sema_total_input_tokens?: number;
|
|
242
|
+
/**
|
|
243
|
+
* ⇐ 行上的 `usageBasis` —— **口径版本标记**(sdk 逐字:`"uncached-components-v1"` = 三个输入
|
|
244
|
+
* 分量键互不重叠;缺席 = 口径不可保证,可能是存量行、也可能是跨阶段混窗聚合)。**开集串**:
|
|
245
|
+
* 原样过境,消费方 `switch` 必须带 `default`;🔴 **缺席不许倒推口径**(sdk 明令)。
|
|
246
|
+
*/
|
|
247
|
+
readonly _sema_usage_basis?: string;
|
|
248
|
+
/** ⇐ 行上的 `cacheWriteTokensLong`;语义与不求和的理由见 `SemaFlatUsage` 的同名位。 */
|
|
249
|
+
readonly _sema_cache_write_tokens_long?: number;
|
|
250
|
+
}
|
|
80
251
|
/** `done` → SDKResultSuccess (contract 02 §2.10 / 08 CS-10). */
|
|
81
252
|
export declare function doneToSdkResult(ev: Extract<AgentEvent, {
|
|
82
253
|
type: 'done';
|
|
@@ -20,6 +20,16 @@ function finiteOr0(v) {
|
|
|
20
20
|
* 另一种形状,就是本函数 `stats === undefined` 的退化输出 —— 现在它也走这里(单一构造点)。
|
|
21
21
|
*/
|
|
22
22
|
function flattenUsage(stats) {
|
|
23
|
+
// D-2 / L-192②:`webSearchRequests` 修前是**字面量 0**。`TaskStats` 是开集索引 ⇒ 按 CC 的名字
|
|
24
|
+
// 宽读一次:读得出就是真账(显式 0 = 做了零次,一句正面事实),读不出才是「没有这本账」。
|
|
25
|
+
const webSearch = stats?.webSearchRequests;
|
|
26
|
+
const webSearchKnown = typeof webSearch === 'number' && Number.isFinite(webSearch);
|
|
27
|
+
const totalInput = stats?.totalInputTokens;
|
|
28
|
+
const totalInputKnown = typeof totalInput === 'number' && Number.isFinite(totalInput);
|
|
29
|
+
// D-2 族扫(异源对抗复审 [medium]):1 小时 TTL 的缓存写入(core `cacheWriteTokensLong`)此前
|
|
30
|
+
// 在终局的**两处**投影上都读不到 —— 见 {@link SemaFlatUsage._sema_cache_write_tokens_long}。
|
|
31
|
+
const longWrite = stats?.cacheWriteTokensLong;
|
|
32
|
+
const longWriteKnown = typeof longWrite === 'number' && Number.isFinite(longWrite);
|
|
23
33
|
return {
|
|
24
34
|
// 🔴 B-073 ③ 族扫(0.65.0):四格一律走 {@link finiteOr0},不再用 `?? 0`。
|
|
25
35
|
// `??` 只挡 `null`/`undefined` —— 而 `TaskStats` 带 `[key: string]: unknown` 开集索引,
|
|
@@ -36,7 +46,200 @@ function flattenUsage(stats) {
|
|
|
36
46
|
// (token 计数没报就是没写过),缺席与真 0 在 wire 上本来就同义 —— 成本那一格的缺席是 core
|
|
37
47
|
// 刻意造出来的第三档,故单有判别位(见 `toCcModelUsage`)。
|
|
38
48
|
cacheCreationInputTokens: finiteOr0(stats?.cacheWriteTokens),
|
|
39
|
-
|
|
49
|
+
// D-2:值仍是 number(CC 形不破、只读这一格的旧消费者行为逐字不变);「wire 上没有这本账」
|
|
50
|
+
// 这句话交给同行的判别位,见 {@link SemaFlatUsage._sema_web_search_requests_absent}。
|
|
51
|
+
webSearchRequests: webSearchKnown ? webSearch : 0,
|
|
52
|
+
...(webSearchKnown ? {} : { _sema_web_search_requests_absent: true }),
|
|
53
|
+
// D-2:cache-INCLUSIVE 总量的超集座位(缺席 ⇒ 键不铸,绝不拿 MISS 分量冒充总量)。
|
|
54
|
+
...(totalInputKnown ? { _sema_total_input_tokens: totalInput } : {}),
|
|
55
|
+
// D-2 族扫:长 TTL 缓存写入分量原样过境(**不求和**,理由见该位注释)。
|
|
56
|
+
...(longWriteKnown ? { _sema_cache_write_tokens_long: longWrite } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** 非空串窄化(空串按缺席归一 —— 空串骑上一个键就是「这一格有值」的冒充)。 */
|
|
60
|
+
function nonEmptyStr(v) {
|
|
61
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 「这条 gate 是**被拒**的」判词表。
|
|
65
|
+
* 🔴 **识别表,不是合法值表**(同 `engineErrorCodes` 的开集纪律):core 今天在两处铸这一格 ——
|
|
66
|
+
* 同步 ask 腿写 `resolved.action === 'allow' ? 'allow' : 'deny'`,耐久 resume 腿原样透传
|
|
67
|
+
* `ResumeOutcome.decision`。后者的 `plan_review` 臂词表是 `approve` / `edit` / `reject` ——
|
|
68
|
+
* 「人否了一份**计划**」不是「一次**工具调用**被拒」,CC 的 `permission_denials` 逐字是
|
|
69
|
+
* "One auto-denied **tool call**"。⇒ 认不出的判词一律**不铸** denial:少报一条(消费方还能从
|
|
70
|
+
* `humanReview` 看见有人工门)远好过把一次计划否决冒充成一次工具拒绝。
|
|
71
|
+
*/
|
|
72
|
+
const GATE_DENIED_DECISIONS = new Set(['deny']);
|
|
73
|
+
/**
|
|
74
|
+
* **认得出、但不是工具拒绝**的判词。与上面那张表合起来才是「这一行我判得出」的全集 ——
|
|
75
|
+
* 落在两张表**之外**的词(上游新词 / 大小写变体 / 空串 / 整个缺席)一律**不猜**:既不铸 denial,
|
|
76
|
+
* 也**不许**让这份清单继续声称完整(异源对抗复审 [medium]:修前这些行被静默跳过,`[]` 于是被
|
|
77
|
+
* 当成「零拒绝」这句正面事实)。今天的成员:同步 ask 腿的 `allow`,计划复核腿的
|
|
78
|
+
* `approve`/`edit`/`reject`(那是「人否了一份**计划**」,不是 CC 说的 one auto-denied tool call)。
|
|
79
|
+
*/
|
|
80
|
+
const GATE_NON_DENIAL_DECISIONS = new Set(['allow', 'approve', 'edit', 'reject']);
|
|
81
|
+
/**
|
|
82
|
+
* D-1 / L-192① —— 终帧拒绝清单的**唯一 mint 点**(成功臂与错误信封共用)。
|
|
83
|
+
*
|
|
84
|
+
* 修前两处各写一个字面量 `[]`,于是**两句话被折成一句**:「这条 run 一次都没被拒」与「这条帧
|
|
85
|
+
* 根本没有这本账」(老引擎 / 409 拒绝信封 / `failed` 事件帧都不带 stats)在 CC 形上都是 `[]`。
|
|
86
|
+
*
|
|
87
|
+
* 🔴 **为什么是两条清单而不是把 wire 的记录塞进 CC 数组**(异源复审 [medium] 逼出的口径订正):
|
|
88
|
+
* CC 的 `SDKPermissionDenial` 三键 `tool_name` / `tool_use_id` / `tool_input` **全是必填**
|
|
89
|
+
* (agent-types `permissions.d.ts` 与壳侧 zod `SDKPermissionDenialSchema` 皆然),而 wire 的 gate
|
|
90
|
+
* 账本只有五格、给不出后两键。两条路都不能走:**补零补空 = 编造**;**塞半条记录 = 破坏元素契约**
|
|
91
|
+
* ——严格消费方 `safeParse` 会把**整条 result 帧**判非法(丢一格 vs 丢整帧,后者严重得多)。
|
|
92
|
+
* ⇒ CC 数组只收**三键齐全**的记录(今天恒空),wire 上真有的每一条走 sema 载体
|
|
93
|
+
* `_sema_permission_denials`,键按能兑现的铸。**自动升级腿**:上游哪天把 `toolCallId` /
|
|
94
|
+
* `toolInput` 串上 gate 账本,CC 数组自己就开始填,本函数一行不用改(门里有那一条的正控)。
|
|
95
|
+
*
|
|
96
|
+
* 🔴 判别位 `_sema_permission_denials_absent` = **「CC 那条清单不可声称完整」**,四条路径:
|
|
97
|
+
* ① 账本整个读不出(`humanReview` 缺席 / 坏形 / `gates` 不是数组 / 无 stats);
|
|
98
|
+
* ② 有读不出的行(非对象);
|
|
99
|
+
* ③ 有**认不出的判词**(两张判词表之外的词 / 判词缺席)—— 不猜它是不是拒绝;
|
|
100
|
+
* ④ 有被拒记录**没能铸成 CC 形**(今天只要有一条拒绝就必然命中这条)。
|
|
101
|
+
* 读法因此是两键合读:`[]` + 判别位缺席 = **零拒绝**(正面事实);`[]` + 判别位 `true` = 别渲肯定句。
|
|
102
|
+
* 判别位**只在该说话时铸,绝不铸 `false`**(本包 additive 一贯纪律)。
|
|
103
|
+
*
|
|
104
|
+
* 逐行独立:坏行只丢自己,绝不丢整本账(但会让清单失去「完整」资格,见 ②③)。
|
|
105
|
+
*/
|
|
106
|
+
function permissionDenialParts(stats) {
|
|
107
|
+
const hr = stats?.humanReview;
|
|
108
|
+
const gates = hr !== null && typeof hr === 'object' ? hr.gates : undefined;
|
|
109
|
+
// 账本整个读不出(缺席 / 坏形 / 无 stats)—— 诚实缺席,不猜。
|
|
110
|
+
if (!Array.isArray(gates))
|
|
111
|
+
return { permission_denials: [], _sema_permission_denials_absent: true };
|
|
112
|
+
const denials = [];
|
|
113
|
+
/** 这份清单还能不能声称「完整」(见头注 ②③④)。 */
|
|
114
|
+
let complete = true;
|
|
115
|
+
for (const g of gates) {
|
|
116
|
+
if (g === null || typeof g !== 'object') {
|
|
117
|
+
complete = false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const row = g;
|
|
121
|
+
const decision = row.decision;
|
|
122
|
+
if (typeof decision !== 'string' || decision.length === 0) {
|
|
123
|
+
// 判词缺席(耐久 wake 腿真会这样)⇒ 这一行是不是拒绝**判不出**,不猜、也不再声称完整。
|
|
124
|
+
complete = false;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!GATE_DENIED_DECISIONS.has(decision)) {
|
|
128
|
+
// 认得出的非拒绝判词照旧跳过(不影响完整性);认不出的词一律降级为「清单不完整」。
|
|
129
|
+
if (!GATE_NON_DENIAL_DECISIONS.has(decision))
|
|
130
|
+
complete = false;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const toolName = nonEmptyStr(row.toolName);
|
|
134
|
+
const toolArg = nonEmptyStr(row.toolArg);
|
|
135
|
+
// 🔴 开集宽读两键:今天 core 的 gate 账本上没有它们(门里锚着这条缺席证据),按 core 自己的
|
|
136
|
+
// 命名习惯(`toolName` / `toolArg` 同族)预读 `toolCallId` / `toolInput`;上游若用别的名字
|
|
137
|
+
// 补上,缺席证据当天红 ⇒ 那一批改读真名。绝不自铸值。
|
|
138
|
+
const toolUseId = nonEmptyStr(row.toolCallId);
|
|
139
|
+
const rawInput = row.toolInput;
|
|
140
|
+
const toolInput = rawInput !== null && typeof rawInput === 'object' && !Array.isArray(rawInput)
|
|
141
|
+
? rawInput
|
|
142
|
+
: undefined;
|
|
143
|
+
// 键都读不出时仍然 push 一条空记录:**条数**(「这条 run 被拒了几次」)是这本账最要紧的
|
|
144
|
+
// 事实,丢掉它等于把一次真实发生的拒绝抹成没发生;键按能兑现的铸,不编。
|
|
145
|
+
denials.push({
|
|
146
|
+
...(toolName !== undefined ? { tool_name: toolName } : {}),
|
|
147
|
+
...(toolUseId !== undefined ? { tool_use_id: toolUseId } : {}),
|
|
148
|
+
...(toolInput !== undefined ? { tool_input: toolInput } : {}),
|
|
149
|
+
...(toolArg !== undefined ? { _sema_tool_arg: toolArg } : {}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/** CC 元素契约:三键齐全才进 CC 数组(类型窄化由这只谓词负责)。 */
|
|
153
|
+
const ccList = denials.filter((d) => d.tool_name !== undefined && d.tool_use_id !== undefined && d.tool_input !== undefined);
|
|
154
|
+
const claimable = complete && ccList.length === denials.length;
|
|
155
|
+
return {
|
|
156
|
+
permission_denials: ccList,
|
|
157
|
+
...(denials.length > 0 ? { _sema_permission_denials: denials } : {}),
|
|
158
|
+
...(claimable ? {} : { _sema_permission_denials_absent: true }),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** 有限数窄化(非数 / 非有限 ⇒ 缺席;`0` 是事实不是缺席)。 */
|
|
162
|
+
function finiteOrAbsent(v) {
|
|
163
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* D-3 / B-068 · L-198 —— 终局成本事实的**唯一读器**(终帧超集键与 chrome 对账臂共用)。
|
|
167
|
+
*
|
|
168
|
+
* 🔴 `stats` 不是可读对象(409 拒绝信封 / park 体 / `failed` 事件帧)⇒ 返 `undefined` =
|
|
169
|
+
* **这条帧没有账**,调用方据此「不说话」(不发臂、不铸键),而不是发一条全缺席的空账。
|
|
170
|
+
*/
|
|
171
|
+
export function readRunCostFacts(stats) {
|
|
172
|
+
// 数组也不是「一份账」:`typeof [] === 'object'` 会把一条畸形载体放进来,然后它的每一格都读不出
|
|
173
|
+
// ⇒ 发出一条「own 没定价」的臂,而真相是**这条帧根本没有账**(两句话又折成一句)。
|
|
174
|
+
if (stats === null || typeof stats !== 'object' || Array.isArray(stats))
|
|
175
|
+
return undefined;
|
|
176
|
+
const rawBreakdown = stats.costBreakdown;
|
|
177
|
+
let breakdown;
|
|
178
|
+
if (rawBreakdown !== null && typeof rawBreakdown === 'object' && !Array.isArray(rawBreakdown)) {
|
|
179
|
+
const out = {};
|
|
180
|
+
// 逐键独立:坏值只丢自己(整段不连坐)。开集 —— 键名不套闭表,词表属主是 core。
|
|
181
|
+
for (const [k, v] of Object.entries(rawBreakdown)) {
|
|
182
|
+
const n = finiteOrAbsent(v);
|
|
183
|
+
if (n !== undefined)
|
|
184
|
+
out[k] = n;
|
|
185
|
+
}
|
|
186
|
+
if (Object.keys(out).length > 0)
|
|
187
|
+
breakdown = out;
|
|
188
|
+
}
|
|
189
|
+
const rawNested = stats.nested;
|
|
190
|
+
// 🔴 「**委派过**」这件事本身是 load-bearing 的(它决定总额算不算得出来),所以它的判据是
|
|
191
|
+
// **原始载体**在不在,而**不是**过滤产物空不空(异源对抗复审 [medium] 的实抓:
|
|
192
|
+
// `{costMicroUsd:100, nested:{}}` / `nested:{tokens:'bad'}` 修前被当成「没委派」⇒ 铸出一个
|
|
193
|
+
// 确定的总额 100,而真相是「委派过、那本账读不出来」)。
|
|
194
|
+
const nestedRan = rawNested !== null && typeof rawNested === 'object' && !Array.isArray(rawNested);
|
|
195
|
+
let nested;
|
|
196
|
+
if (nestedRan) {
|
|
197
|
+
const n = rawNested;
|
|
198
|
+
const picked = {};
|
|
199
|
+
for (const k of ['tokens', 'turns', 'tasks', 'costMicroUsd']) {
|
|
200
|
+
const v = finiteOrAbsent(n[k]);
|
|
201
|
+
if (v !== undefined)
|
|
202
|
+
picked[k] = v;
|
|
203
|
+
}
|
|
204
|
+
// 一个数都读不出时仍然铸**空对象**:键在场 = 委派过(与 `costMicroUsd` 缺席 = 那本账不知道
|
|
205
|
+
// 是两键合读的两半);整键不铸会把「委派过」这句事实一起抹掉。
|
|
206
|
+
nested = picked;
|
|
207
|
+
}
|
|
208
|
+
const ownMicroUsd = finiteOrAbsent(stats.costMicroUsd);
|
|
209
|
+
const nestedMicroUsd = nested?.costMicroUsd;
|
|
210
|
+
// 「委派过但那本账没定价/读不出」与「压根没委派」是两句话:前者判别位立起来,后者什么都不说。
|
|
211
|
+
const nestedUnknown = nestedRan && nestedMicroUsd === undefined;
|
|
212
|
+
// 求和自身也必须是有限数:两个合法但极端的值相加可以溢出成 Infinity,而 Infinity 会被下游
|
|
213
|
+
// 当成真数字摊进总计(与 `microUsdToUsd` 同一条规约)⇒ 宁可不铸。
|
|
214
|
+
const sum = ownMicroUsd !== undefined && !nestedUnknown ? ownMicroUsd + (nestedMicroUsd ?? 0) : undefined;
|
|
215
|
+
const reconcile = {
|
|
216
|
+
...(ownMicroUsd !== undefined ? { ownMicroUsd } : { costAbsent: true }),
|
|
217
|
+
...(nestedMicroUsd !== undefined ? { nestedMicroUsd } : {}),
|
|
218
|
+
...(nestedUnknown ? { nestedCostAbsent: true } : {}),
|
|
219
|
+
...(breakdown?.compactionMicroUsd !== undefined ? { compactionMicroUsd: breakdown.compactionMicroUsd } : {}),
|
|
220
|
+
...(breakdown?.llmRootMicroUsd !== undefined ? { llmRootMicroUsd: breakdown.llmRootMicroUsd } : {}),
|
|
221
|
+
// 对账值:own 读得出、子代那一边不是「跑了却不知道多少钱」、且和本身有限时才算得出来。
|
|
222
|
+
...(sum !== undefined && Number.isFinite(sum) ? { reconciledMicroUsd: sum } : {}),
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
...(breakdown !== undefined ? { breakdown } : {}),
|
|
226
|
+
...(nested !== undefined ? { nested } : {}),
|
|
227
|
+
reconcile,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* D-3 —— 终帧上的两个成本超集键(成功臂与错误信封共用的**唯一** mint 点)。
|
|
232
|
+
* 🔴 CC 同名键 `total_cost_usd` **语义零改**:它仍然只是 own 花费,`nested` **不折进去**
|
|
233
|
+
* (超集键纪律:CC 形上已有的位不许塞我们自己的含义)。「fully-reconciled spend」由消费方按
|
|
234
|
+
* 这两个超集键自己加 —— 包给的是**可对账的事实**,不是一个改了口径的数。
|
|
235
|
+
*/
|
|
236
|
+
function costFactParts(stats) {
|
|
237
|
+
const facts = readRunCostFacts(stats);
|
|
238
|
+
if (facts === undefined)
|
|
239
|
+
return {};
|
|
240
|
+
return {
|
|
241
|
+
...(facts.breakdown !== undefined ? { _sema_cost_breakdown: facts.breakdown } : {}),
|
|
242
|
+
...(facts.nested !== undefined ? { _sema_nested_usage: facts.nested } : {}),
|
|
40
243
|
};
|
|
41
244
|
}
|
|
42
245
|
/** P1-5 — real elapsed ms since the stream opened (runStream stamps startedAtMs); 0 only pre-stamp. */
|
|
@@ -112,13 +315,15 @@ function mapModelUsage(stats) {
|
|
|
112
315
|
if (u === undefined || u === null || typeof u !== 'object')
|
|
113
316
|
continue;
|
|
114
317
|
const m = u;
|
|
115
|
-
out[modelId] = toCcModelUsage({
|
|
318
|
+
out[modelId] = withTerminalUsageFacts(toCcModelUsage({
|
|
116
319
|
inputTokens: numOrAbsent(m.inputTokens),
|
|
117
320
|
outputTokens: numOrAbsent(m.outputTokens),
|
|
118
321
|
cacheReadTokens: numOrAbsent(m.cacheReadTokens),
|
|
119
322
|
cacheWriteTokens: numOrAbsent(m.cacheWriteTokens),
|
|
120
323
|
costMicroUsd: numOrAbsent(m.costMicroUsd),
|
|
121
|
-
|
|
324
|
+
// D-2(0.66.0):per-model 行今天也不带这一格 ⇒ 宽读一次,读不出由 mint 点立判别位。
|
|
325
|
+
webSearchRequests: numOrAbsent(m.webSearchRequests),
|
|
326
|
+
}), m);
|
|
122
327
|
}
|
|
123
328
|
return out;
|
|
124
329
|
}
|
|
@@ -139,16 +344,37 @@ function modelUsageFor(stats, model) {
|
|
|
139
344
|
// stats?.costUsd === 'number'` 的旧写法在 SDK 1.x 下照样编译得过,只是恒假 ⇒ 合成行的
|
|
140
345
|
// costUSD 静默恒 0(比报错更隐蔽)。tsc 只红了 6 处直传,这处靠人核 + pure 门的行为钉。
|
|
141
346
|
return {
|
|
142
|
-
[model]: toCcModelUsage({
|
|
347
|
+
[model]: withTerminalUsageFacts(toCcModelUsage({
|
|
143
348
|
inputTokens: stats?.promptTokens,
|
|
144
349
|
outputTokens: stats?.outputTokens,
|
|
145
350
|
cacheReadTokens: stats?.cachedTokens,
|
|
146
351
|
// B-073 ③ 族扫:合成行是 `flattenUsage` 的**同形第二处**,同批一并改读真值。
|
|
147
352
|
cacheWriteTokens: stats?.cacheWriteTokens,
|
|
148
353
|
costMicroUsd: stats?.costMicroUsd,
|
|
149
|
-
|
|
354
|
+
// D-2(0.66.0):与扁平 usage 同一格同一读法(开集宽读,读不出由 mint 点立判别位)。
|
|
355
|
+
webSearchRequests: stats?.webSearchRequests,
|
|
356
|
+
}), stats),
|
|
150
357
|
};
|
|
151
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* 给一条已铸好的 CC `ModelUsage` 行挂上终局那三个座位(读不出的一律不铸)。
|
|
361
|
+
* `raw` = 这一行的 wire 原形(分表的那一行,或扁平 `TaskStats` —— 两处键名相同)。
|
|
362
|
+
*/
|
|
363
|
+
function withTerminalUsageFacts(row, raw) {
|
|
364
|
+
const total = finiteOr0Absent(raw?.totalInputTokens);
|
|
365
|
+
const long = finiteOr0Absent(raw?.cacheWriteTokensLong);
|
|
366
|
+
const basis = nonEmptyStr(raw?.usageBasis);
|
|
367
|
+
return {
|
|
368
|
+
...row,
|
|
369
|
+
...(total !== undefined ? { _sema_total_input_tokens: total } : {}),
|
|
370
|
+
...(basis !== undefined ? { _sema_usage_basis: basis } : {}),
|
|
371
|
+
...(long !== undefined ? { _sema_cache_write_tokens_long: long } : {}),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/** 有限数窄化(非数 / 非有限 ⇒ 缺席;`0` 是读数不是缺席)。 */
|
|
375
|
+
function finiteOr0Absent(v) {
|
|
376
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
377
|
+
}
|
|
152
378
|
/**
|
|
153
379
|
* TaskResult 五键最小消费批([1543] SDK-M,2026-07-23)之一 —— `degraded` = 模型降级链实录
|
|
154
380
|
* (from/to/reason/atTurn;多模型网关部署的关键可观测面)。
|
|
@@ -264,7 +490,10 @@ function errorResult(ctx, parts) {
|
|
|
264
490
|
total_cost_usd: costOrNull(parts.stats), // P1-5: null when unknown, never a fake 0
|
|
265
491
|
usage: flattenUsage(parts.stats),
|
|
266
492
|
modelUsage: modelUsageFor(parts.stats, parts.model),
|
|
267
|
-
|
|
493
|
+
// D-1 / L-192①:两句话不再折成一句 —— 清单 + 「有没有这本账」的判别位,见 permissionDenialParts。
|
|
494
|
+
...permissionDenialParts(parts.stats),
|
|
495
|
+
// D-3 / B-068:失败/到限/park 的 run 一样花过钱,账不因结局不好就不报。
|
|
496
|
+
...costFactParts(parts.stats),
|
|
268
497
|
errors: [...parts.errors],
|
|
269
498
|
...(parts.errorCode !== undefined && parts.errorCode.length > 0 ? { errorCode: parts.errorCode } : {}),
|
|
270
499
|
...(parts.degraded !== undefined ? { degraded: parts.degraded } : {}),
|
|
@@ -454,7 +683,10 @@ export function doneToSdkResult(ev, ctx) {
|
|
|
454
683
|
usage: flattenUsage(stats),
|
|
455
684
|
// MF-25/P1-5 — the per-model usage split (wire `stats.modelUsage`, or the synthesized current-model row).
|
|
456
685
|
modelUsage: modelUsageFor(stats, r.model),
|
|
457
|
-
|
|
686
|
+
// D-1 / L-192①:同形第二处 —— 与错误信封共用**同一个** mint 点(修前两处各一个字面量 [])。
|
|
687
|
+
...permissionDenialParts(stats),
|
|
688
|
+
// D-3 / B-068:成本明细与子代那本账(micro-USD 原值);`total_cost_usd` 语义一字不动。
|
|
689
|
+
...costFactParts(stats),
|
|
458
690
|
// MF-25 — the effective served model id (`done.result.model`, e.g. "deepseek-v4-pro"). The CC
|
|
459
691
|
// SDKResultSuccess schema has no `model` field, so this rides as an additive seam field a cost/overview
|
|
460
692
|
// consumer reads (it is ALSO surfaced as the `modelUsage` key). Omitted when the wire didn't carry it.
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* `maxOutputTokens` — the missing `contextWindow` denominator is mock-fill from
|
|
17
17
|
* a static per-model table (contract 02 §2.7 VERIFY). Defaulted to 0 to satisfy
|
|
18
18
|
* the CC `ModelUsage` shape without inventing a number.
|
|
19
|
+
* 🔴 0.66.0 / D-2:这三格里的 `webSearchRequests` 现在**开集宽读**(读得出就是真账),读不出时
|
|
20
|
+
* 值仍 0 但同行立 `_sema_web_search_requests_absent` —— 「这一格是 0」与「这一格没有账」在
|
|
21
|
+
* CC 形上此前不可分。另两格维持 0 且**不立判别位**,理由见 {@link SemaModelUsage} 的该位注释。
|
|
19
22
|
*
|
|
20
23
|
* core 3.0.0 语义翻转注(2026-08-02,[2318] 提货):wire 的 `inputTokens` 从「含 cache 总量」
|
|
21
24
|
* 翻为「归一化未命中分量(cache-MISS)」。本映射**一个字节没动**,但语义上恰好从错变对 ——
|
|
@@ -49,6 +52,12 @@ export interface EngineUsageLike {
|
|
|
49
52
|
readonly cacheReadTokens?: number | undefined;
|
|
50
53
|
readonly cacheWriteTokens?: number | undefined;
|
|
51
54
|
readonly costMicroUsd?: number | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* D-2 / L-192②(0.66.0):CC 同名格的宽读位。今天 wire 上**没有**任何一条腿发它
|
|
57
|
+
* (core 的 `TaskStats` / per-model 行都没有这一格)⇒ 恒缺席、镜像上的 `webSearchRequests`
|
|
58
|
+
* 恒 0 + 判别位;开集读是为了上游真发那天**照读**而不是继续硬编(退出条件在门里)。
|
|
59
|
+
*/
|
|
60
|
+
readonly webSearchRequests?: number | undefined;
|
|
52
61
|
}
|
|
53
62
|
/**
|
|
54
63
|
* B-073 ②(0.65.0)—— CC `ModelUsage` 的 sema 超集形:多一个**成本缺席判别位**。
|
|
@@ -70,6 +79,15 @@ export interface EngineUsageLike {
|
|
|
70
79
|
export type SemaModelUsage = ModelUsage & {
|
|
71
80
|
/** 在场且为 `true` ⇒ 这一行的 `costUSD: 0` 是「**没定价**」,不是「免费」。 */
|
|
72
81
|
readonly _sema_cost_absent?: true;
|
|
82
|
+
/**
|
|
83
|
+
* D-2 / L-192②(0.66.0)—— 在场且为 `true` ⇒ 这一行的 `webSearchRequests: 0` 是
|
|
84
|
+
* 「**wire 上没有这本账**」,不是「这一轮一次网搜都没做」。与 `_sema_cost_absent` 同一条
|
|
85
|
+
* 两键合读纪律;同形第二处在终帧的扁平 usage(`SemaFlatUsage`),两处一个规矩。
|
|
86
|
+
* 🔴 `contextWindow` / `maxOutputTokens` 那两格**刻意不给判别位**:它们是**模型的静态元数据**
|
|
87
|
+
* 而不是这一轮的账,权威来源是模型目录(`/v1/models[].contextWindow`),不是 usage 面;
|
|
88
|
+
* 给它们立位会把「去目录里取」误导成「等 usage 面补上」(登记见 INTEGRATION §31d)。
|
|
89
|
+
*/
|
|
90
|
+
readonly _sema_web_search_requests_absent?: true;
|
|
73
91
|
};
|
|
74
92
|
/**
|
|
75
93
|
* 🔴 **CC `ModelUsage` 八键形状的唯一 mint 点**(REF-CC-062 / xlate-08,E1 单源构造)。
|
|
@@ -85,7 +103,8 @@ export type SemaModelUsage = ModelUsage & {
|
|
|
85
103
|
* cacheReadTokens → cacheReadInputTokens
|
|
86
104
|
* cacheWriteTokens → cacheCreationInputTokens
|
|
87
105
|
* costMicroUsd → costUSD(/1e6;micro-USD 是 wire 单位,字段名即单位)
|
|
88
|
-
*
|
|
106
|
+
* webSearchRequests→ webSearchRequests(0.66.0 / D-2 起开集宽读;读不出 ⇒ 0 + 判别位)
|
|
107
|
+
* DROPPED(wire 无此数,填 0 而不是编一个):contextWindow / maxOutputTokens。
|
|
89
108
|
*
|
|
90
109
|
* 收编时的**一处语义统一**(REF-CC-062,记在这里以免被读成无意的漂移):cost 的换算此前两份
|
|
91
110
|
* 写法不同 —— 本文件是裸 `/1e6`(非有限值原样传播出 Infinity/NaN),`terminalToSdkResult` 是
|
|
@@ -16,7 +16,8 @@ function finiteOrZero(v) {
|
|
|
16
16
|
* cacheReadTokens → cacheReadInputTokens
|
|
17
17
|
* cacheWriteTokens → cacheCreationInputTokens
|
|
18
18
|
* costMicroUsd → costUSD(/1e6;micro-USD 是 wire 单位,字段名即单位)
|
|
19
|
-
*
|
|
19
|
+
* webSearchRequests→ webSearchRequests(0.66.0 / D-2 起开集宽读;读不出 ⇒ 0 + 判别位)
|
|
20
|
+
* DROPPED(wire 无此数,填 0 而不是编一个):contextWindow / maxOutputTokens。
|
|
20
21
|
*
|
|
21
22
|
* 收编时的**一处语义统一**(REF-CC-062,记在这里以免被读成无意的漂移):cost 的换算此前两份
|
|
22
23
|
* 写法不同 —— 本文件是裸 `/1e6`(非有限值原样传播出 Infinity/NaN),`terminalToSdkResult` 是
|
|
@@ -28,17 +29,21 @@ export function toCcModelUsage(raw) {
|
|
|
28
29
|
// 没报就是没花),而成本的缺席是 core **刻意**造出来的一档语义(`ABSENT when any spend was
|
|
29
30
|
// unpriced … rather than a fabricated 0`)。故这里单独判一次,而不是继续走 `finiteOrZero`。
|
|
30
31
|
const costPriced = typeof raw.costMicroUsd === 'number' && Number.isFinite(raw.costMicroUsd);
|
|
32
|
+
// D-2 / L-192②(0.66.0):`webSearchRequests` 与成本那一格同族 —— 修前是**字面量 0**,
|
|
33
|
+
// 把「wire 上没有这本账」渲成「做了零次」。值仍留 0(CC 形必填 number),判别位说真话。
|
|
34
|
+
const webSearchKnown = typeof raw.webSearchRequests === 'number' && Number.isFinite(raw.webSearchRequests);
|
|
31
35
|
return {
|
|
32
36
|
inputTokens: finiteOrZero(raw.inputTokens),
|
|
33
37
|
outputTokens: finiteOrZero(raw.outputTokens),
|
|
34
38
|
cacheReadInputTokens: finiteOrZero(raw.cacheReadTokens),
|
|
35
39
|
cacheCreationInputTokens: finiteOrZero(raw.cacheWriteTokens),
|
|
36
|
-
webSearchRequests:
|
|
40
|
+
webSearchRequests: webSearchKnown ? raw.webSearchRequests : 0,
|
|
37
41
|
// CC 形不破:值仍是 number。「不知道」由同行的判别位说,见 {@link SemaModelUsage}。
|
|
38
42
|
costUSD: costPriced ? raw.costMicroUsd / 1_000_000 : 0,
|
|
39
43
|
contextWindow: 0, // dropped on the wire → mock-fill (static per-model table)
|
|
40
44
|
maxOutputTokens: 0, // dropped on the wire → mock-fill
|
|
41
45
|
...(costPriced ? {} : { _sema_cost_absent: true }),
|
|
46
|
+
...(webSearchKnown ? {} : { _sema_web_search_requests_absent: true }),
|
|
42
47
|
};
|
|
43
48
|
}
|
|
44
49
|
export function turnUsageToModelUsage(usage) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { eventSeq, } from './types.js';
|
|
2
2
|
import { eventToSdkMessage, turnEndUsage } from './downstream/eventToSdkMessage.js';
|
|
3
|
-
|
|
3
|
+
// D-3(0.66.0):终局对账臂与终帧超集键共用**同一个**成本读器(readRunCostFacts)。
|
|
4
|
+
import { readRunCostFacts, terminalToSdkResult } from './downstream/terminalToSdkResult.js';
|
|
4
5
|
import { coerceOutput, publishSubagentContentEvent } from '../subagentContentStore.js';
|
|
5
6
|
/**
|
|
6
7
|
* 409 session-busy 拒绝的 **canonical errorCode**([2377]C-1,server main `049ff2c`,随 5.0.0 发)。
|
|
@@ -12,6 +13,28 @@ import { ACTIVE_RUN_BUSY_ERROR_CODE, OUTPUT_INVALID, isLimitsExceededCode } from
|
|
|
12
13
|
import { isReviewPark, readRunTerminal, runTerminalCode } from '../runTerminal.js';
|
|
13
14
|
/** 本文件发的 chrome 事件全在 leader lane(子代内容在上面就被 divert 走了)。 */
|
|
14
15
|
const MAIN = { lane: 'main' };
|
|
16
|
+
/**
|
|
17
|
+
* **fire-and-forget 的 chrome 发射口**(0.66.0 / 异源对抗复审 [high] 逼出的同形族扫)。
|
|
18
|
+
*
|
|
19
|
+
* 病形:`try { void ctx.emitChrome(...) } catch {}` 只接得住**同步**抛出 —— 而端口的契约是
|
|
20
|
+
* `void | Promise<void>`,一个返回被拒 Promise 的 sink 会在 try/catch **之外**变成
|
|
21
|
+
* `unhandledRejection`;把未处理拒绝当致命错误的宿主(`--unhandled-rejections=strict`、
|
|
22
|
+
* Electron 主进程的全局钩子)会**整只退出**。fail-soft 的承诺在那一刻反而成了最响的失败。
|
|
23
|
+
*
|
|
24
|
+
* ⇒ 两条腿一起接:同步抛在 `try` 里吞,异步拒绝挂 `.catch`。语义与此前逐字一致
|
|
25
|
+
* (不 await、不改时序、sink 失败不影响流);`await` 的那一处(plan_review park)**不走本口**,
|
|
26
|
+
* 它的时序是刻意的(卡片先于完成行上屏)。
|
|
27
|
+
*/
|
|
28
|
+
function emitChromeFireAndForget(ctx, event) {
|
|
29
|
+
try {
|
|
30
|
+
const settled = ctx.emitChrome?.(event);
|
|
31
|
+
// thenable 才挂手柄:`void` 返回的同步 sink 上没有 `.catch` 可挂。
|
|
32
|
+
if (settled !== undefined && settled !== null && typeof settled.catch === 'function') {
|
|
33
|
+
void settled.catch(() => { });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch { /* fail-soft — sink 抛错不影响流(原语义) */ }
|
|
37
|
+
}
|
|
15
38
|
/**
|
|
16
39
|
* `pendingGate` 的**单向**同一性判断(#285 件2,0.36.0):「我现在看到的这道门,能不能**证明**它
|
|
17
40
|
* 不是刚才那一道?」
|
|
@@ -377,11 +400,11 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
377
400
|
// ——壳内向的动态边。改成 chrome 事件(宿主消费义务见 seam.ts `last_turn_usage` 臂);
|
|
378
401
|
// 与原式同样**不 await**(fire-and-forget),sink 抛错不影响流(fail-soft 原语义)。
|
|
379
402
|
if (!isSubFlow && ctx.emitChrome) {
|
|
380
|
-
|
|
403
|
+
{
|
|
381
404
|
// engineUsage = [2295] 裁 ② 逐字原形(additive 键,契约见 seam.ts 本臂 doc):
|
|
382
405
|
// 总量/命中率消费面吃它,镜像 usage 保纯。eopt:additive 键按「诚实缺席」= 键不在
|
|
383
406
|
// (与 turn_end.usage 本身「整体 optional,缺席≠空对象」同一语义,不是「键在值 undefined」)。
|
|
384
|
-
|
|
407
|
+
emitChromeFireAndForget(ctx, {
|
|
385
408
|
kind: 'last_turn_usage',
|
|
386
409
|
laneProof: MAIN,
|
|
387
410
|
usage,
|
|
@@ -394,7 +417,6 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
394
417
|
...(stopWord !== undefined ? { stopReason: stopWord } : {}),
|
|
395
418
|
});
|
|
396
419
|
}
|
|
397
|
-
catch { /* fail-soft — statusline 退回 null,原语义 */ }
|
|
398
420
|
}
|
|
399
421
|
}
|
|
400
422
|
const outputTokens = ev.usage?.outputTokens;
|
|
@@ -554,6 +576,18 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
554
576
|
}
|
|
555
577
|
catch { /* fail-soft — the terminal frame below still renders */ }
|
|
556
578
|
}
|
|
579
|
+
// D-3 / B-068 · L-198(0.66.0):**终局对账那一拍**。与终帧超集键
|
|
580
|
+
// (`_sema_cost_breakdown` / `_sema_nested_usage`)同一个读器 `readRunCostFacts` ⇒ 两面同源。
|
|
581
|
+
// 🔴 没有账就不说话:`stats` 读不出(409 拒绝信封 / park 体 / `failed` 事件帧)⇒ 不发。
|
|
582
|
+
// 🔴 子流断闸(§E2)一字不动:子代花费只经本臂的 `nestedMicroUsd`(终局 stats.nested)到账。
|
|
583
|
+
// fail-soft 同 plan_review_park:sink 抛错不影响终帧照常投影。
|
|
584
|
+
if (ev.type === 'done' && ctx.emitChrome) {
|
|
585
|
+
const doneStats = ev.result?.stats;
|
|
586
|
+
const costFacts = readRunCostFacts(doneStats);
|
|
587
|
+
if (costFacts !== undefined) {
|
|
588
|
+
emitChromeFireAndForget(ctx, { kind: 'run_cost_reconciled', laneProof: MAIN, ...costFacts.reconcile });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
557
591
|
yield terminalToSdkResult(ev, ctx);
|
|
558
592
|
return;
|
|
559
593
|
}
|
package/dist/seam.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { ModelUsage, SDKMessage } from '@sema-agent/agent-types';
|
|
10
10
|
import type { EngineTurnUsage } from './adapter/downstream/turnUsageToModelUsage.js';
|
|
11
|
+
import type { RunCostReconcile } from './adapter/downstream/terminalToSdkResult.js';
|
|
11
12
|
import type { WiringManifestAutoMode, WiringManifestModelGate, WiringManifestMcpEntry } from './adapter/downstream/eventToSdkMessage.js';
|
|
12
13
|
/**
|
|
13
14
|
* `AbortSignal` 的结构型(B3 扩容,SEAM-GAP-4)。
|
|
@@ -405,6 +406,34 @@ export type ChromeEvent = {
|
|
|
405
406
|
laneProof: LaneProof;
|
|
406
407
|
result: unknown;
|
|
407
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* D-3 / B-068 · L-198(0.66.0;core ≥7.12.0 `TaskStats.costBreakdown` / `nested`)——
|
|
411
|
+
* **终局那一拍的成本对账**。载荷本体 = {@link RunCostReconcile}(与终帧超集键
|
|
412
|
+
* `_sema_cost_breakdown` / `_sema_nested_usage` **同一个读器**产出,两面不会各算各的)。
|
|
413
|
+
*
|
|
414
|
+
* 🔴 **为什么另起一条臂,而不是扩 `last_turn_usage` 的终局那一拍**:那条臂的宿主义务逐字是
|
|
415
|
+
* 「落**最近一次 turn** 的真 usage(statusline 回落源)」—— 它回答的是「这一轮」的问题,而
|
|
416
|
+
* 本臂回答的是「**这条 run 一共**花了多少、其中子代与压缩各多少」。把两个量塞进一条臂,宿主
|
|
417
|
+
* 就得在同一个载荷上分辨「这是 turn 还是 run」,那正是[same-name-different-meaning]那一族病;
|
|
418
|
+
* 而且 `last_turn_usage` 是 `required: true` 的既有义务面,往上加语义会改已接宿主的读法。
|
|
419
|
+
*
|
|
420
|
+
* 🔴 **子流断闸不动**:`isSubFlow` 的 `turn_end` 仍然**不发** `last_turn_usage`(§E2 判据),
|
|
421
|
+
* 子代花费**只**经本臂的 `nestedMicroUsd`(= 终局 `stats.nested`)到账 —— 流中增量与终局总账
|
|
422
|
+
* 两条腿分工不变,壳把两者相加就是重复计账。
|
|
423
|
+
*
|
|
424
|
+
* 发臂条件 = **这条终帧带得出 stats**;`stats` 缺席(409 拒绝信封 / park 体 / `failed` 事件帧)
|
|
425
|
+
* ⇒ 一条都不发(没有账就不说话,不发一条全缺席的空账)。非成功终局**照发** —— 花掉的钱与
|
|
426
|
+
* 结局好不好无关。
|
|
427
|
+
*
|
|
428
|
+
* 宿主消费义务(`required: false`):终局按本臂对账(`/cost` 面把 own / nested / compaction
|
|
429
|
+
* 三段各渲一行)。不接 = 必须改从终帧那两个超集键自己对账,否则子代与压缩两笔账永远不进成本面
|
|
430
|
+
* (B-068 原病)。🔴 判别位在场时**别渲 0**:`costAbsent` / `nestedCostAbsent` 说的是
|
|
431
|
+
* 「这一段没定价」,渲成 `$0.00` 就是替引擎编了一笔它没报的账。
|
|
432
|
+
*/
|
|
433
|
+
| ({
|
|
434
|
+
kind: 'run_cost_reconciled';
|
|
435
|
+
laneProof: LaneProof;
|
|
436
|
+
} & RunCostReconcile)
|
|
408
437
|
/**
|
|
409
438
|
* FIX⑦(2026-08-07,[3017]/[3020];core 5.14.0 design/171 新 `TaskEvent` 臂,server 7.4.0
|
|
410
439
|
* 起真上 SSE)——「**谁把什么喂进了这条 run**」的生命周期账本帧:objective / 实时 steer /
|
package/dist/seam.js
CHANGED
|
@@ -35,6 +35,15 @@ const CHROME_ARM_TABLE = {
|
|
|
35
35
|
prompt_suggestions: { required: false, duty: '推 composer 上方 chips(绝不回喂模型)' },
|
|
36
36
|
last_turn_usage: { required: true, duty: '落最近一次 turn 真 usage(statusline 回落源)' },
|
|
37
37
|
plan_review_park: { required: true, duty: '弹 plan 审批卡(fail-soft,不挡后续终态帧)' },
|
|
38
|
+
// D-3 / B-068:终局对账。`required:false` —— 不接不等于哑掉(终帧的两个超集键载着同一份事实),
|
|
39
|
+
// 但**不接就必须自己按那两个键对账**,否则 /cost 恒低报子代与压缩两笔账(B-068 原病)。
|
|
40
|
+
run_cost_reconciled: {
|
|
41
|
+
required: false,
|
|
42
|
+
duty: '可选:终局成本对账(own / nested / compaction 三段 + reconciled = own + nested)。' +
|
|
43
|
+
'不接 ⇒ 必须改从终帧 `_sema_cost_breakdown` / `_sema_nested_usage` 自己对账,否则子代与压缩两笔账不进成本面;' +
|
|
44
|
+
'🔴 `costAbsent` / `nestedCostAbsent` 在场时渲「未定价」而不是 $0.00;' +
|
|
45
|
+
'🔴 流中的 `last_turn_usage` 增量与本臂的终局总账**不相加**(子代花费只经本臂到账,相加 = 重复计账)',
|
|
46
|
+
},
|
|
38
47
|
// FIX⑦:账本帧,**不带正文** ⇒ 义务是可选的(记进审计/时间线即可);🔴 硬约束是**不许**据此
|
|
39
48
|
// 铸转录行,渲署名时必须带上 `actor.hostAsserted` 的可信度标注。
|
|
40
49
|
// B-072 ④:定位账本帧,**不带正文** ⇒ 义务可选(落一张 entryId→已渲消息的映射表);
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
|
|
20
20
|
| 项 | 值 | 真源 |
|
|
21
21
|
|---|---|---|
|
|
22
|
-
| 本包 | `@sema-agent/client-core` **0.
|
|
22
|
+
| 本包 | `@sema-agent/client-core` **0.66.0**(本批发布版 = minor:内容批 3c0ac0e,终局真值三件 L-192① / B-068·L-198 / L-192②,全部 additive,详见 §31a–§31f;0.65.x 见 §30;`CHANGELOG.md` `## 0.66.0(2026-09-11)`;bump 与冻结账两阶段由发包批做) | `package.json` `version` |
|
|
23
23
|
| peer:wire 契约 | `@sema-agent/sdk` **>=8.4.0**(value-level,非 type-only;**0.60.0 抬版**,四条硬理由见 §24a 与 `scripts/run-sdk-floor-test.mjs` 的 `FLOOR` 注;上一次是 0.59.0 的 `>=8.3.0`)。🔴 支持窗同批收到 **engine ≥7.64.0**:sdk 8.4.0 与 7.63.0 及以前的 wire **不同窗** | `package.json` `peerDependencies` |
|
|
24
24
|
| peer:会话词汇表 | `@sema-agent/agent-types` **>=0.2.0**(type-only,零运行时) | 同上 |
|
|
25
25
|
| runtime dep | `diff` ^9.0.0(**唯一**一条;portability 门按**等值**钉死) | `package.json` `dependencies` |
|
|
26
|
-
| 公开导出面 | **
|
|
26
|
+
| 公开导出面 | **929** 个运行期符号(+ 44 个测试钩;= 工作树当下的值 = 工作树里另有 **0.66.0 未发的一件 additive**`readRunCostFacts`(终局成本事实的唯一读器:`costBreakdown` / `nested` 窄读 + 对账三段与两个判别位,终帧超集键与 chrome 对账臂共用它;详见 §31c);已发的 **0.65.x 十一件 additive、两件删除**:additive = `DECIDE_WORKFLOW_HOST_UNKNOWN`/`DECIDE_WORKFLOW_HOST_NOT_PARKED`/`DECIDE_WORKFLOW_REMEMBER_UNSUPPORTED`/`DECIDE_WORKFLOW_LANE_CODES`/`readDecideReceipt`/`decideAcceptedNotResolved`/`decideRefusalFromError`(B-070 `/decide` 回执读面与三码人话,详见 §30h)/ `TERMINAL_NOT_SUCCESS_STATUSES`/`isTerminalNotSuccess`/`TERMINAL_STATUSES`/`isTerminalStatus`(L-215② 终局词汇单铸谓词,详见 §30j);🔴 **删除两件** `AUTO_MODE_BREAKER_CAUSES`/`classifierBreakerOf`(熔断族随 core 7.12.0 的 Removed(BREAKING) 整只退役,clean-cut 不留别名,详见 §30k②;同批删的还有**型** `ClassifierBreakerView` 与 `ClassifierStatusView.breaker` 一位)。`0.64.2` 是 **919**,那批是一件 additive、零删除:`readDenialLimitFallback`(限额回落卡的窄读器上公面,壳侧同判据副本退役,详见 §29③);`0.64.0` 是 **918**,那批是件①+件② 十三件 additive、零删除:`CLASSIFIER_STATUS_STATES`/`classifierBreakerOf`/`classifierStatusOf`/`classifierStatusDetail`(分类器状态面,详见 §28②)/ `THINKING_FORMATS`/`THINKING_FORMAT_WIRE_TWINS`/`THINKING_DISABLE_PROBE_ORDER`/`MODEL_PROBE_VERDICTS`/`PROBE_PROMPT`/`PROBE_MAX_TOKENS`/`probeRequestBody`/`probeModelCapability`/`applyProbeToEntry`(模型思考能力探测,详见 §28①);`0.63.0` 是 **905**,那批是八件 additive、零删除:`projectServerGateKnobs`/`postureKnobSourceOf`/`postureSourceIsOperatorPinned`/`postureKnobDetail`/`POSTURE_SOURCE_WORDS`(S-178 三根 posture 旋钮读数,详见 §27b)/ `PERSISTED_RULE_BEHAVIORS`/`PERSISTED_RULE_BEHAVIOR_UNKNOWN`/`persistedRuleBehaviorOf`/`persistedRuleBehaviorLabel`/`revokeTargetFromPersistedRule`(三态规则身份,§27c)/ `engineIdentityOf`/`engineIdentityVerdict`/`engineIdentityChanged`/`engineIdentityChangedBy`/`ENGINE_IDENTITY_ANCHORS`(S-179 代际锚,§27d)/ `GATE_DENIED_BY_WORDS`/`gateDeniedByDetail`/`ASK_ORIGIN_WORDS`/`askOriginDetail`(门词汇 +2,§27e)/ `PERMISSION_RULE_ISSUE_CODES`/`RETIRED_PERMISSION_RULE_ISSUE_CODES`/`permissionRuleIssueDetail`(规则 lint 码表,§27f)/ `projectToolRoster`/`toolRosterNames`/`toolShimFromRoster`/`applyToolRosterDelta`(L-161 工具名册,§27g)/ `ENGINE_NOTICE_CODES`/`ENGINE_NOTICE_AUDIENCE`/`noticeAudienceOf`/`engineNoticeInCatalog`/`MCP_INJECTION_DROP_REASONS`/`readMcpInjectionDrop`(L-167 通告码册,§27h)/ `AUTO_MODE_UNAVAILABLE_CAUSES`/`AUTO_MODE_BREAKER_CAUSES`/`classifierUnavailableOf`/`classifierUnavailableDetail`(件⑧ 分类器不可用事实,§27h2);`0.62.0` 是 **869**,那批是五件 additive、**零删除**:`capForDisplay`(呈前消毒 + 封长的共用铸点,详见 §26⑦)/ `SEAT_SEND_MESSAGE_KEY_ORIGINS`(座位载荷逐键出身表,详见 §26③)/ `configureSubagentContentStore`+`subagentContentStoreConfig`+`SUBAGENT_CONTENT_STORE_DEFAULTS`(子代内容账本字节预算,测试钩 `__resetSubagentContentStoreConfigForTests` +1,详见 §26②);`0.61.0` 是 **864**,那批是三件 additive、**零删除**:`projectReadFacePosture`/`readFacePostureDetail`/`readFaceDisagreement`(S-167 READ 容纳面 operator 读器,详见 §25);`0.60.0` 是 **861**,那批是二十五件 additive、**零删除**:`readRunTerminal`/`runTerminalCode`/`runTerminalGateKind`/`runTerminalGateToolName`/`isReviewPark`/`REVIEW_PARK_GATE_KINDS` 六件终局因由读器 + `gateOutcomeOf`/`isDeniedGate`/`gateDeniedBy`/`gateApprover`/`isApprovalWindowExpiredGate`/`isDenialLimitAutoDeniedGate`/`isParkSlaExpiredGate`/`isHumanSettledGate` 八件门记录读器 + `projectWriteProtectionCapability`/`noteEngineCapsForWriteProtection`/`observedWriteProtection`/`forgetWriteProtectionReading`/`writeProtectionDoctorDetail`/`projectWriteProtectionPosture`/`writeProtectionPostureDetail` 七件写保护读面(测试钩 `__resetWriteProtectionReadingsForTests` +1)+ `readWireErrorCode` + 真机边界①的两件 `isGateParkedToolEnd`/`GATE_PARKED_ERROR_CODE` + 异源复审逼出的 `engineCapsGeneration`;`0.59.0` 是 **836**,那批是八件 additive:`RULE_OFFERS_ABSENCE_REASONS`/`DENIAL_LIMIT_KINDS` 两张闭词表再导出 + `engineCapValue` 能力位四态通用读口 + `projectSqlEngineCapability`/`observedSqlEngine`/`noteEngineCapsForSqlEngine`/`forgetSqlEngineReading`/`sqlEngineDoctorDetail` 五件 SQL 姿态读面(测试钩 `__resetSqlEngineReadingsForTests` +1);`0.58.0` 是 **828**,那批是三件 additive:`RULE_OFFER_MATCHES`/`RULE_OFFER_BATCH_MEMBER_KINDS`/`RULE_OFFER_UNCOVERED_REASONS` 三张闭词表再导出 + `RESUME_REFUSAL_CODES`/`RESUME_PLACEMENT_MISMATCH`/`resumeRefusalFromError`/`resumeRefusalContent` 四件文案铸点;`0.57.0` 是 **821**;`0.56.0`/`0.55.0` 是 **815**;已发 `0.54.0`(design/385 十件 additive 含 `AUTHORITY_ENVELOPE_TAGS`);`0.52.0` 是 **805**(L-69⑨ 两件);`0.51.0` 是 **803**;`0.50.0` 是 **800**(S-81 五件);`0.49.0` 是 **795**;`0.48.0` 是 **794**;npm `0.47.0` 是 **790**,`0.46.0` 是 **787**,`0.44.0` 是 **783**,`0.43.1`/`0.43.0` 是 **776**,`0.42.0` 是 **771**,`0.41.0` 是 **767**,`0.39.0` 是 **766**,`0.38.0` 是 **764**;`0.37.0` 是 **753**,见 `CHANGELOG.md`) | `scripts/public-export-baseline.json` 的 `count` / `testHookCount` —— **别手抄进别处,以该文件为准** |
|
|
27
27
|
| 常驻门 | 以 `scripts/gates-manifest.json` 的 `suites` 长度为准(**本档不抄这个数**) | `scripts/gates-manifest.json`;`npm test` 的名单等值门与它逐名对账 |
|
|
28
28
|
| 沿革档 | 0.29.0 起建 `CHANGELOG.md`;更早批次记账在 `src/index.ts` 文件头 + `docs/REFACTOR-LEDGER.md` | — |
|
|
29
29
|
|
|
@@ -111,7 +111,7 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
|
|
|
111
111
|
|
|
112
112
|
## §2 公共导出面地图(按域)
|
|
113
113
|
|
|
114
|
-
> 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**
|
|
114
|
+
> 全集真源 = `scripts/public-export-baseline.json` 的 `names`(**929** 项)。
|
|
115
115
|
> 本节**不逐名抄**,只给「域 → 承重导出 → 用途 → 实现锚」。承重导出 = 一个端为了让这个域干活
|
|
116
116
|
> **必须**直接调到的那几个符号;其余是它们的类型、变体与辅助位。
|
|
117
117
|
> 单一入口:`import { … } from '@sema-agent/client-core'`(`exports` 只有 `.` 一个;
|
|
@@ -135,16 +135,16 @@ SDK 核对「本包 import 的每个值级符号仍然导出」「`TaskStats.cos
|
|
|
135
135
|
`WorkflowsGateUnknownDenial` 四形**不在**基线里,`src/selfOrchestrationDenial.ts` 对基线贡献
|
|
136
136
|
**4** 项运行期导出(三个函数 + `SELF_ORCHESTRATION_RETRY_WITHOUT`)。
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
929 项的内部构成(帮助端估读表大小):**283** 项是 `SCREAMING_SNAKE` 常量数据表/词汇表
|
|
139
139
|
(矩阵、键集、env 名、锚串)而非可调用物;**5** 项是 PascalCase 运行期值
|
|
140
140
|
(`ControlRouter` / `ControlSafetyError` / `HitlBridge` / `HitlSafetyError` / `DecideTransportRetryExhaustedError`);
|
|
141
141
|
**41** 项是 `*For(sessionKey, …)` 的 per-session 变体(§6;其中 `engineNamespaceKeyFor` 是命名巧合 —— 参数是 baseUrl 不是 sessionKey,见域 14)。
|
|
142
142
|
|
|
143
|
-
### 2b. 域图(16 域,逐域计数之和 =
|
|
143
|
+
### 2b. 域图(16 域,逐域计数之和 = 929)
|
|
144
144
|
|
|
145
145
|
| # | 域 | 名数 | 承重导出 | 用途 | 实现锚 |
|
|
146
146
|
|---|---|---|---|---|---|
|
|
147
|
-
| 1 | **适配内核(下行主链)** |
|
|
147
|
+
| 1 | **适配内核(下行主链)** | 44 | `adapt` · `createWireToCcAdapter` · `runStream` · `eventToSdkMessage` · `terminalToSdkResult` · `readRunCostFacts`(0.66.0:终局成本事实的**唯一读器** —— `TaskStats.costBreakdown` / `nested` 逐键窄读 + 对账三段(own/nested/compaction)与两个「没定价」判别位;终帧的两个超集键与 chrome 臂 `run_cost_reconciled` **共用它**,两面不会各算各的,详见 §31c)· `TERMINAL_NOT_SUCCESS_STATUSES`/`isTerminalNotSuccess`/`TERMINAL_STATUSES`/`isTerminalStatus`(L-215② 终局词汇的**单铸谓词** —— `blocked` 是 agent 自报终态,`suspended`/`needs_review` 刻意不在表里,详见 §30j) · `turnUsageToModelUsage` · `isRunStreamActive` · `ADAPTER_DIVERGENCES` · `readRunTerminal` / `runTerminalCode` / `runTerminalGateKind` / `runTerminalGateToolName` / `isReviewPark` / `REVIEW_PARK_GATE_KINDS`(0.60.0 / engine ≥7.64.0:一次 run **怎么结束的**唯一读器 —— 终局从八个平面键换成一条带标因由 `terminal`,四臂 + `unknown` 防御臂 + `plane` 两代字节标;🔴 不认识的终态词绝不折成成功、本包不提供「因由→五词 status」的兼容投影,详见 §24b)| 引擎 SSE `AgentEvent` → 端要渲的**双面输出**:transcript(`SDKMessage`)+ chrome(瞬态 `ChromeEvent`)。**本包存在的理由** | `src/adapt.ts`、`src/adapt/{arms,wireShapes,panelTasks}.ts`(经 `adapt.ts` 再导出)、`src/adapter/runStream.ts`、`src/adapter/downstream/*`、`src/adapter/types.ts`、`src/runTerminal.ts` |
|
|
148
148
|
| 2 | **seam 公共契约** | 2(其余为 type-only) | `CHROME_ARMS` · `deriveTranscriptId` | 公共词汇 + **id 确定性不变量**(同一条流重放 ⇒ 同一串 id)。`CHROME_ARMS` = 端「我要消费哪些 chrome 臂」的对照清单 | `src/seam.ts` |
|
|
149
149
|
| 3 | **HITL 决断卡链**(§4/§5 主战场) | 167 | `makeHitlCanUseTool` · `HitlBridge` · `findPendingForTask` · `readDecideReceipt`/`decideAcceptedNotResolved`/`decideRefusalFromError`(B-070:`/decide` 200 = **投递受理**不是「门已解决」;`executionOutcome` 缺席 = 未知,详见 §30h) · `HitlSafetyError` · `bridgeAskUserQuestionGates` · `surfaceToolApprovalFrameAndRespond` / `surfaceFsApprovalAndDecide` · `readToolApprovalRespondAck` · `installApprovalCardPort(For)` · `installHitlHostSurface(For)` · `armPlanReviewApproval` · `reopenPlanReviewCard` · `decidePlanReview` · `startApprovalsFeed` · `pendingRowIsOwnedByThisSession` · `approvalCallKey`/`liveFrameCallKey`/`planReviewQuestionId` · `registerArmedGateFor`/`wasGateArmedFor`/`clearArmedGateFor` · `waitForGateArmed(For)`/`onGateArmed(For)`/`gateArmedWaitMs`(#244 F1 呈现回执事件源) · `planReviewArmedKey(For)`/`notePlanReviewAnswered(For)`/`notePlanReviewAnsweredIfDecisive(For)`(A-024.4 plan 呈现分代) · `toolEndOutputText` · `isAskTool` · `waitForParkRowBirth` · `classifyAskParkRows` / `askParkRowArm` / `classifyAskParkChainFailure` · `readDecisionNoteAudit` / `decisionNoteAuditLine` · `resumeRunningOptions` / `resumeChoiceFromLabels` · `persistedRulesLaneAvailable`/`persistedRulesGovernanceAvailable` · `classifyRulesFailure` · `listAllPersistedRules` · `classifySkippedReason` · `readRulePersistOutcome`(#244 F2:persist-ack 读口与 `readToolApprovalRespondAck` 合成一处) · `parseLocalAllowRule`(durable 腿本地落规则窄化骨架,谓词经 `LocalAllowRuleDeps` 注入) · `readToolApprovalRespondRefusal`(#225 件5,0.42.0:respond 抛错的结构化原文读口 —— 三位各自防御读、各自缺席不铸、**三位皆缺席时整只返 `undefined`**;原样交还零加工,UNTRUSTED-for-display)· `installEditedRuleTextPrechecker` / `hasEditedRuleTextPrechecker` / `precheckEditedRuleText`([5076] 转出口,0.42.0:core 5.57.0 `precheckEditedRuleText` 的**端口注入形** —— 类型面 + 注入口 + 诚实缺席读口。🔴 **不是** value 级 re-export,理由见 §7 缺口 **P-34**;未装 ⇒ 返 `undefined`,绝不编一个 `{ok:true}`)· `surfaceRuleArmNotSent` / `RULE_NOT_SENT_WARN_TEXT` + `surfaceRuleArmRejected` / `RULE_NOT_SENT_REJECTED_WARN_TEXT`(#334,0.43.0:人在卡上按下的「不再询问」被整条丢弃时的诚实告知——**两条刻意分开**:前者=**引擎能力位未确认**(换台引擎/等探测就好),后者=**这次选择没过包内表核/互斥核**(表外文本/坏下标/两臂同场,换引擎也不会变) —— 编辑臂 `respondFreeFormRules` 与批臂 `respondBatchRuleOffers` 两条同形存量共用一条,与 `surfaceRememberNotApplied` 同族纪律:决断照送、只是规则没存,静默丢掉用户明确意图 = 让人以为功能坏了)· `projectCrashConverged`(L-38,0.49.0:`/v1/approvals` additive 键 `crashConverged` 的分桶投影 —— 崩溃收敛的孤儿审批读面,**缺席 ≠ 空数组**、分桶恰一个合取、坏行丢弃并计数,详见 §12;同批把 `ApprovalsResourceLike.list()` 的返回位 additive 放宽成 `ApprovalsListEnvelope`,老形 `{pending}` 仍可赋值)· `readRuleOffers` / `readRuleOfferSupply`(L-103/[C228],0.57.0 additive 导出:「不再询问」候选两代 wire 键的窄读器**公面化** —— 此前只经卡端口出包,不走卡端口架构的宿主只能自己重铸一把,而它承载的是兑付安全判据(原始下标不前移/逐条丢坏/闭集 kind/两代取舍序),详见 §20)· `RULE_OFFERS_ABSENCE_REASONS` / `DENIAL_LIMIT_KINDS`(0.59.0 / sdk 8.3.0 提货:`ruleOffersAbsence` 三词与 `denialLimitFallback.limit` 二词的**运行期**闭词表再导出 —— 与 0.58.0 那三张同一条纪律:端拿它做判定别再手抄字面量。⚠️ **本包自己不拿这两张表做窄读判定**(两处都按开集透传,词表属主是 core、server 已按闭集拒过),再导出只是给端的 `switch` 一份可数的表,端仍必须带 `default` 臂,详见 §23a)· `DecideTransportRetryExhaustedError`(Inkglow-1085 P0a:decide 出站瞬断重试耗尽的 typed 判别 —— HitlBridge 内建单次退避重试,耗尽走重呈臂不判死 turn;端一般只消费行为,不需要 instanceof) · `gateOutcomeOf` / `isDeniedGate` / `gateDeniedBy` / `gateApprover` / `isApprovalWindowExpiredGate` / `isDenialLimitAutoDeniedGate` / `isParkSlaExpiredGate` / `isHumanSettledGate`(0.60.0 / engine ≥7.64.0:`tool_end.gate` 整条门记录的窄读器 + 判定谓词 —— 取代退役的四个正交词 `settledBy`/`resolution`/`autoDenied`/`approver`。🔴 **判定归包**:「是不是审批窗自己走完的拒绝」旧形上是端各自拼的两键合取,而三条窗词现在分得开、合并读回去等于把刚修好的判别力扔掉;⚠️ 公面型一律带 `View` 后缀 —— 本文件的 `GateOutcome`(决断腿结局)与 sdk 的 `GateOutcome`(引擎门记录)**同名不同义**,详见 §24c)· `isGateParkedToolEnd`(「这次调用没在这里结算」的**第二形**谓词:gate 缺席 + `errorCode:"gate.parked"`;端判这件事必须与 `isApprovalWindowExpiredGate` **两形合读**) · `readWireErrorCode`(B-037 候包件②:decide/取件失败上引擎 wire 机器码的单源读口,结构视图读不 `instanceof`;与本包铸的 `code` 是两个命名空间,详见 §24g)| suspended→decide→resume 环。🔴 **D-1 两元组 verbatim 回显**是字节级断言的安全不变量,端**不许重实现它的任何一段**。🔴 键空间边界(web [C1] d3 拦截):`gateIdentity` 四常量两函数只覆盖 HITL questionId/callKey 空间;seat 的 `TOOL_PERMISSION_REQUEST_ID_DOMAINS`(`plan:` 等)是另一键空间,**两者绝不合并**(合并=座位校验器静默拒全部 plan-review 卡) | `src/hitl/hitlBridge.ts`、`toolApprovalWire.ts`、`askGateWire.ts`、`planReviewWire.ts`、`hitlHostSurface.ts`、`gateIdentity.ts`、`armedGateRegistry.ts`、`parkOwnership.ts`、`parkResolver.ts`、`approvalsFeed.ts`、`frameRouter.ts`(**只挑名导出** `toolEndOutputText`/`ENGINE_ABORT_TOOL_RESULT`/`isAskTool`/`HITL_REJECT_MESSAGE`/`HITL_INTERRUPT_MESSAGE_FOR_TOOL_USE`)、`parkRowBirthWait.ts`、`approvalDecisionNoteAudit.ts`、`askParkRowRouting.ts`、`resumeRunningCard.ts`(#265 上收的判定层)、`persistedRulesWire.ts`、`localAllowRule.ts`(#244 F2 规则侧)、`crashConverged.ts`(L-38 崩溃收敛读面)、`src/gateOutcome.ts` |
|
|
150
150
|
| 4 | **子代 wire + 面板侧信道台账** | 87 | `tailEngineSubagent` · `installSubagentActivitySink` · `installSubagentTailMetaSink`(#280 件2:tail meta 帧发布口,`contentFrames` 判别位载体)· `stopEngineTask` + `classifyTaskStopConflict` · `fetchEngineSubagentReport` · `steerEngineSubagent`(0.32.0 未发布 #280 件A:additive 第三参 `childTaskId` —— 端有行上下文时**应当**传,传了就走「台账优先 / 缺席即诚实缺席 + `noteBgOwnerAbsence` 留痕」的 Q3 口径,与 tail·taskOutput·subagentOutput 三腿同姿势、与孪生 resume 腿共用同一个 `resolveOwnerRunId` 判据;**不传**则逐字维持旧行为=回落在飞 run)· `resumeSettledSubagent` + `resolveSubagentResumeContext` + `resolveOwnerRunId` + `classifySubagentResumeFailure` + `subagentResumeAvailable`(#242 批 2 A-028.7:resume 判定半场上收,与 steer 孪生同居;取址三态 = 台账有行用行值 / 指名了行但台账缺席则**诚实缺席绝不回落在飞 run** / 没指名行才回落。出路文案归端)· `recordSubagentOwnerFromProgress` + `getBgParentRunOwner`(A-028.6:「子代 → 宿主 run」**单表**,宿主 run 必须由持 stream-local 值的调用方显式传入,包内绝不从 `activeEngineRunId()` 推断)· `noteBgOwnerAbsence`(#242 批 3 [4000] Q3=B:tail/taskOutput·taskStop/subagentOutput 三腿台账缺席即诚实缺席**绝不回落在飞 run**,缺席 warn 留痕每 (腿,taskId) 一条)· `clearBgTerminalFacts`(#242 批 3 扫码修:复活=新周期,旧周期终态事实作废——fleetLedger 复活两腿按尾段清账,factsAccepted 方向核不再拿上周期终态当先例)· `auditRetainWithoutWake`([4000] Q5:引擎宣示 `subagentResume` + 本端在付 `retainSubagentSessions` + 端未实现 `wakeSubagent` ⇒ 响亮一条;`CLIENT_VERBS.wakeSubagent` 维持 fail-soft)· `subscribeSubagentContent` · `subscribeEngineAgentPanel` · `publishQuestionFrame` / `respondToQuestion` · `configureSubagentContentStore` / `subagentContentStoreConfig` / `SUBAGENT_CONTENT_STORE_DEFAULTS`(0.62.0:子代内容账本的**字节预算** —— 此前只有条数帽而单条无上限,一条 base64 工具结果就能让一个子代槽吃到十几 MB;越限从最早的内容开始丢并留一条可渲染的 `truncated` 记录,总帽淘汰整条最久未用的账本。配置面 fail-loud,详见 §26②)| 驱动与观测委派子代;经 module 级台账喂活体 agent/task 面板。全部**能力位 gate**(§5b) | `src/subagent/*.ts`、`src/subagentContentStore.ts`、`src/engineAgentPanelStore.ts`、`src/engineInlineTaskStats.ts`、`src/engineToolLabelStore.ts`、`src/liveQuestionStore.ts` |
|
|
@@ -5985,3 +5985,276 @@ server 7.70.0([6927])读面同批收窄。
|
|
|
5985
5985
|
(源码扫描零铸点)⇒ 加词对本包**零落点**。归属登记:**候 sdk 镜像**(D1 是它的到货哨兵);
|
|
5986
5986
|
哪天 sdk 镜像了,D1 红 ⇒ 那一批同时铸措辞表并把 D2 退役。
|
|
5987
5987
|
|
|
5988
|
+
---
|
|
5989
|
+
|
|
5990
|
+
## §31 🆕 0.66.0 内容批(**终局真值三件**:L-192① `permission_denials` + B-068 / L-198 成本对账 + L-192② 终帧 usage 三格)
|
|
5991
|
+
|
|
5992
|
+
### 31a. 本节速览
|
|
5993
|
+
|
|
5994
|
+
| 件 | 一句话 | 端要做什么 |
|
|
5995
|
+
|---|---|---|
|
|
5996
|
+
| ① L-192① | 终帧拒绝清单不再硬编 `[]`:被拒行逐条进 **sema 载体** `_sema_permission_denials`,CC 数组只收**三键齐全**的记录(今天恒空,上游补齐即自动填)+ 「这条清单不可声称完整」判别位 | 渲拒绝清单的端**改读 sema 载体**(壳里本地铸 `[]` 的两处退役);CC 那条 `[]` 必须与判别位**合读** |
|
|
5997
|
+
| ② B-068 / L-198 | `costBreakdown` / `nested` 两段投上终帧超集键 + 新增终局 chrome 臂 `run_cost_reconciled`;`total_cost_usd` **语义零改** | 成本面接**终局对账**(own / nested / compaction 三段);不接 ⇒ 子代与压缩两笔账永远不进账 |
|
|
5998
|
+
| ③ L-192② | 终帧扁平 `usage`:`webSearchRequests` 从**字面量 0** 改开集宽读 + 缺席判别位;新增 cache-INCLUSIVE 总量座位 `_sema_total_input_tokens`、长 TTL 缓存写入座位 `_sema_cache_write_tokens_long`;终局 per-model 行同批补三座位(总量 / 口径标记 / 长 TTL);🔴 `inputTokens` **仍是 cache-MISS 分量**(不灌总量) | 只读现有四格的端**零改**;要「这条 run/这个模型摆了多少上下文」的端改读新座位 |
|
|
5999
|
+
|
|
6000
|
+
**共同形**:三件都是「上游把事实摆在 wire 上,而**包边界用一个字面量把它答成常数**」——
|
|
6001
|
+
`permission_denials: []` / `webSearchRequests: 0` / 整段 `costBreakdown` 不投,是 0.65.0 那一批
|
|
6002
|
+
(`toolFaces` / `model` / `cacheWriteTokens`)的同族第 N 例。**共同处置**也只有一条:
|
|
6003
|
+
CC 形上必填的位**值不动**(旧消费者逐字不变),把「不知道」交给同行的 `_sema_` 判别位;
|
|
6004
|
+
CC 形上没有的事实**另开超集座位**,绝不改 CC 同名键的语义。
|
|
6005
|
+
|
|
6006
|
+
**出处**:cli 台账 `L-192`(①②)/ `B-068` 与 `DEBTS-cli L-198`(②)。合同真源 = 实装 devDep
|
|
6007
|
+
`@sema-agent/core` 7.12.0 的 `dist/core/task-result.d.ts`(`humanReview` 的 gate 账本 /
|
|
6008
|
+
`costBreakdown` 的对账两式 / `promptTokens` 与 `totalInputTokens` 的 RB-457-a 语义翻转)、
|
|
6009
|
+
`dist/core/tool-spec.d.ts`(`NestedUsage`)、`@sema-agent/sdk` 8.8.0 的 `dist/types.d.ts`
|
|
6010
|
+
(`TaskStats.costBreakdown?: unknown` 开集透传 / `humanReview.gates` 的 live 钉死键面)与
|
|
6011
|
+
`@sema-agent/agent-types` 0.2.0 的 `permissions.d.ts`(CC `SDKPermissionDenial` 三键真形)。
|
|
6012
|
+
|
|
6013
|
+
---
|
|
6014
|
+
|
|
6015
|
+
### 31b. ① L-192① 终帧 `permission_denials`(core ≥7.12.0 `TaskStats.humanReview`)
|
|
6016
|
+
|
|
6017
|
+
**病**:CC 的 `SDKResultSuccess` / `SDKResultError` 上有一格 `permission_denials`
|
|
6018
|
+
(「这条 run 里有哪些工具调用被拒了」),而包边界在**两处**(成功臂 + 错误信封)硬编 `[]`。
|
|
6019
|
+
代价不是少一格数据,是**两句话被折成一句**:「这条 run 一次都没被拒」与「这条帧根本没有这本账」
|
|
6020
|
+
(老引擎 / 409 拒绝信封 / `failed` 事件帧都不带 `stats`)在 CC 形上都是 `[]`。
|
|
6021
|
+
|
|
6022
|
+
**键名与载体**
|
|
6023
|
+
|
|
6024
|
+
| 键 | 载体 | 来源 | 缺席语义 |
|
|
6025
|
+
|---|---|---|---|
|
|
6026
|
+
| `permission_denials[]` | CC result 帧(成功臂 + 错误信封) | 上面那本账里**三键齐全**的记录 | 今天恒 `[]`(wire 给不出后两键);`[]` 本身**不是**缺席(见判别位) |
|
|
6027
|
+
| `_sema_permission_denials[]` | 同上(超集键) | `TaskStats.humanReview.gates[]` 里 `decision === "deny"` 的行,**逐条一记录、顺序 = wire 顺序** | 整键不铸 = 没读出任何被拒记录 |
|
|
6028
|
+
| `…[].tool_name` | 两条清单共用的记录形 | `gates[].toolName` | 键不铸 = 这条腿没报工具名(**绝不编一个**) |
|
|
6029
|
+
| `…[].tool_use_id` / `…[].tool_input` | 同上 | 开集预读 `gates[].toolCallId` / `toolInput`(**今天 wire 上没有**) | 键不铸;**绝不**铸 `""` / `{}` |
|
|
6030
|
+
| `…[]._sema_tool_arg` | 同上(超集键) | `gates[].toolArg` —— 引擎侧已脱敏截短的**一行入参摘要** | 键不铸 = 这条腿没串入参(同步 ask 腿有、耐久 resume 腿没有) |
|
|
6031
|
+
| `_sema_permission_denials_absent` | CC result 帧顶层(超集键) | 见下面四条路径 | **只在该说话时**为 `true`;绝不铸 `false` |
|
|
6032
|
+
|
|
6033
|
+
**🔴 为什么是两条清单**:CC 的 `SDKPermissionDenial` 三键 `tool_name` / `tool_use_id` / `tool_input`
|
|
6034
|
+
**全是必填**(`agent-types` 与本包对照的会话词汇 zod 皆然),而 wire 上这本账的真形是 core 的 gate
|
|
6035
|
+
记录 —— 只有 `kind` / `waitMs` / `decision?` / `toolName?` / `toolArg?` **五格**,既没有 `toolCallId`、
|
|
6036
|
+
也没有被拒时的入参对象(core 逐字:「a durable-resume gate carries `toolName` only (its input is not
|
|
6037
|
+
threaded onto the persisted gate — a documented follow-on)」;sdk 的同一格亦逐字记着 `tool_input` /
|
|
6038
|
+
`toolInput` **不在** gate ledger 上)。两条路都不能走:**补零补空 = 编造**;**把半条记录塞进 CC 数组
|
|
6039
|
+
= 破坏元素契约** —— 严格消费方 `safeParse` 会把**整条 result 帧**判非法(丢一格 vs 丢整帧,后者严重
|
|
6040
|
+
得多)。⇒ CC 数组只收三键齐全的记录,wire 上真有的每一条走 sema 载体。
|
|
6041
|
+
**自动升级腿**:上游哪天把两格串上账本,CC 数组**自己**就开始填(包侧一行不用改),判别位随之退场;
|
|
6042
|
+
常驻门里有那一条的正控 + 今天的**缺席证据**(锚 core 的 `gates` 声明块),上游一动当天红。
|
|
6043
|
+
|
|
6044
|
+
**🔴 两键合读**(判别位 = 「CC 那条清单**不可声称完整**」,四条路径:①账本整个读不出 ②有读不出的行
|
|
6045
|
+
③有**认不出的判词**(两张判词表之外的词 / 判词缺席)④有被拒记录没能铸成 CC 形)
|
|
6046
|
+
|
|
6047
|
+
| `permission_denials` | `_sema_permission_denials_absent` | 含义 | 端该渲什么 |
|
|
6048
|
+
|---|---|---|---|
|
|
6049
|
+
| `[]` | 缺席 | 账读得出且逐行判得出,**零拒绝** | 「本次无拒绝」(一句正面事实) |
|
|
6050
|
+
| `[]` | `true` | 清单不完整(没有账 / 有读不出的行 / 有认不出的判词 / 有记录只在 sema 载体上) | 不渲肯定句;改读 `_sema_permission_denials` |
|
|
6051
|
+
| 非空 | 缺席 | 有 N 次拒绝且每条都齐全 | 逐条渲 |
|
|
6052
|
+
|
|
6053
|
+
**🔴 开集判词**:只有 `deny` 被读成「工具调用被拒」;`allow` / `approve` / `edit` / `reject` 是
|
|
6054
|
+
**认得出的非拒绝**(计划复核的否决是「人否了一份**计划**」,不是 CC 说的 "One auto-denied **tool
|
|
6055
|
+
call**")—— 这两张表之外的词、以及**缺判词的行**(耐久 wake 腿真会这样)一律**不猜**:不铸 denial,
|
|
6056
|
+
但让这份清单失去「完整」资格(判别位立起来)。坏行(非对象)**只丢自己**,不丢整本账;而键都读不出
|
|
6057
|
+
的被拒行**仍然占一条** ——「被拒了几次」是这本账最要紧的事实。
|
|
6058
|
+
|
|
6059
|
+
**黑盒判据(test 视角可执行)**
|
|
6060
|
+
|
|
6061
|
+
- **G31-1**:一条 run 里让审批人**拒**掉一次工具调用(如 `Bash`),`-p --output-format json` 的
|
|
6062
|
+
`_sema_permission_denials` **恰有一条**,`tool_name === "Bash"`,且该条**没有** `tool_use_id` /
|
|
6063
|
+
`tool_input` 两键(修前这条记录整个不存在);同帧 `permission_denials === []` 且
|
|
6064
|
+
`_sema_permission_denials_absent === true`。
|
|
6065
|
+
- **G31-2**:同一条 run,若拒的是**同步 ask** 腿,该条带 `_sema_tool_arg`(非空串、与引擎账本一致);
|
|
6066
|
+
若拒的是**耐久 resume** 腿,该键不出现。
|
|
6067
|
+
- **G31-3**:一条**全程无人工门**的 run:`permission_denials === []`、**没有**
|
|
6068
|
+
`_sema_permission_denials` 键、**没有** `_sema_permission_denials_absent` 键。
|
|
6069
|
+
- **G31-4**:一条被 409 拒绝(或以 `failed` 事件终止)的 run:`permission_denials === []` 且
|
|
6070
|
+
`_sema_permission_denials_absent === true`。
|
|
6071
|
+
- **G31-5**(负控):任一情形下 `_sema_permission_denials_absent` **都不会是 `false`**;
|
|
6072
|
+
任一情形下 `permission_denials` 里的元素**要么三键齐全、要么这个数组是空的**(半条记录永不进 CC 面)。
|
|
6073
|
+
|
|
6074
|
+
---
|
|
6075
|
+
|
|
6076
|
+
### 31c. ② B-068 / L-198 终局成本对账(core ≥7.12.0 `costBreakdown` / `nested`)
|
|
6077
|
+
|
|
6078
|
+
**病**:core 把一条 run 的花费**拆开**摆在 wire 上并给了两条对账式(`task-result.d.ts` 逐字):
|
|
6079
|
+
|
|
6080
|
+
- `llmRootMicroUsd + compactionMicroUsd === costMicroUsd` —— **压缩花费在 own 里面**;
|
|
6081
|
+
- fully-reconciled spend = `costMicroUsd + nested.costMicroUsd` —— **子代花费在 own 外面**
|
|
6082
|
+
(core 逐字:`costMicroUsd` deliberately EXCLUDES nested)。
|
|
6083
|
+
|
|
6084
|
+
而包边界**一格都不投**:`costBreakdown` 与 `nested` 整段被剥,终帧只剩一个 `total_cost_usd`,
|
|
6085
|
+
于是成本面永远看不见子代与压缩两笔账(`/cost` 与 `-p` 低报)。
|
|
6086
|
+
|
|
6087
|
+
**键名与载体**
|
|
6088
|
+
|
|
6089
|
+
| 键 | 载体 | 来源 | 缺席语义 |
|
|
6090
|
+
|---|---|---|---|
|
|
6091
|
+
| `_sema_cost_breakdown` | CC result 帧顶层(超集键) | `TaskStats.costBreakdown`,**micro-USD 原值**(键名即单位,包不折 USD) | 整键不铸 = 这条 run 没有明细(core 在 unpriced 时与 `costMicroUsd` 一起省略);单键不铸 = 那一类读不出 |
|
|
6092
|
+
| `_sema_nested_usage` | 同上 | `TaskStats.nested`(`tokens`/`turns`/`tasks`/`costMicroUsd`) | 整键不铸 = **没委派过**(判据是**原始载体**在不在,不是过滤产物空不空);键在场而其 `costMicroUsd` 不铸 = 委派过但那本账**没定价/读不出**(core 逐字 `never a fabricated 0`)—— 键在场但值为空对象是合法的一句话 |
|
|
6093
|
+
| chrome 臂 `run_cost_reconciled` | `ChromeEvent`(新臂,`required: false`) | 与上面两键**同一个读器**(`readRunCostFacts`) | 整条不发 = 这条终帧没有 `stats`(409 / park 体 / `failed` 事件帧) |
|
|
6094
|
+
| `total_cost_usd` | CC result 帧(**CC 同名键**) | `TaskStats.costMicroUsd / 1e6` | **语义一字未改**(见下) |
|
|
6095
|
+
|
|
6096
|
+
**臂载荷**:`{ ownMicroUsd?, nestedMicroUsd?, compactionMicroUsd?, llmRootMicroUsd?,
|
|
6097
|
+
reconciledMicroUsd?, costAbsent?, nestedCostAbsent? }`。
|
|
6098
|
+
`reconciledMicroUsd = own + nested` **只在两边都知道、且和本身是有限数时才铸**;own 没定价 ⇒
|
|
6099
|
+
`costAbsent: true`,委派过但那本账没定价/读不出 ⇒ `nestedCostAbsent: true`,两种情形下对账值
|
|
6100
|
+
**都不铸**(不知道就是不知道)。🔴 「委派过」这件事的判据是 `stats.nested` **这个载体在不在**,
|
|
6101
|
+
不是它里面有没有读得出的数 —— 否则一条 `nested: {}` 会被当成「没委派」,于是 own 被铸成一个
|
|
6102
|
+
**确定的总额**,而真相是「委派过、花了多少不知道」。
|
|
6103
|
+
|
|
6104
|
+
**🔴 `total_cost_usd` 为什么不把 nested 折进去**:它是 **CC 同名键**,生态里读它的人默认它是
|
|
6105
|
+
CC 那个语义;把子代花费折进去 = 在一个已有的位上塞我们自己的口径(超集键纪律明令禁止),
|
|
6106
|
+
而且会让「own 花费」这个量在整条链上再也说不清。⇒ 包给的是**可对账的事实**,不是一个改了口径的
|
|
6107
|
+
数:要 fully-reconciled spend 的端**自己加**(`own + nested`),而且加之前要先看两个判别位。
|
|
6108
|
+
|
|
6109
|
+
**🔴 为什么另起一条 chrome 臂而不是扩 `last_turn_usage`**:那条臂的宿主义务逐字是「落**最近一次
|
|
6110
|
+
turn** 的真 usage」——它回答「这一轮」,本臂回答「**这条 run 一共**」。两个量同臂,宿主就得在同一
|
|
6111
|
+
载荷上分辨「这是 turn 还是 run」(同名不同义那一族病);且那条臂是 `required: true` 的既有义务面,
|
|
6112
|
+
往上加语义会改已接宿主的读法。
|
|
6113
|
+
|
|
6114
|
+
**🔴 子流断闸不动**:`isSubFlow` 的 `turn_end` 仍然**不发** `last_turn_usage`(§E2 判据一字未动),
|
|
6115
|
+
子代花费**只**经终局 `nested` 到账。⇒ 端**绝不能**把流中增量与本臂的终局总账相加 —— 那是重复计账。
|
|
6116
|
+
|
|
6117
|
+
**端怎么接**
|
|
6118
|
+
|
|
6119
|
+
```ts
|
|
6120
|
+
// 终局那一拍(chrome 臂);不接这条臂的端改从终帧两个超集键读同一份事实
|
|
6121
|
+
case 'run_cost_reconciled': {
|
|
6122
|
+
if (ev.costAbsent) showOwn('未定价') // 🔴 别渲 $0.00
|
|
6123
|
+
else showOwn(ev.ownMicroUsd! / 1e6)
|
|
6124
|
+
if (ev.nestedCostAbsent) showNested('未定价') // 委派过,但那本账没有价
|
|
6125
|
+
else if (ev.nestedMicroUsd !== undefined) showNested(ev.nestedMicroUsd / 1e6)
|
|
6126
|
+
if (ev.compactionMicroUsd !== undefined) showCompaction(ev.compactionMicroUsd / 1e6)
|
|
6127
|
+
if (ev.reconciledMicroUsd !== undefined) showTotal(ev.reconciledMicroUsd / 1e6)
|
|
6128
|
+
break
|
|
6129
|
+
}
|
|
6130
|
+
```
|
|
6131
|
+
|
|
6132
|
+
**黑盒判据**
|
|
6133
|
+
|
|
6134
|
+
- **G31-6**:一条**委派过子代**且**发生过压缩**的 run:终帧 `_sema_cost_breakdown.compactionMicroUsd`
|
|
6135
|
+
与 `_sema_nested_usage.costMicroUsd` 都是有限数,且
|
|
6136
|
+
`llmRootMicroUsd + compactionMicroUsd === ` 该 run 的 `costMicroUsd`(core 的第一条对账式,
|
|
6137
|
+
由 wire 值自身成立 —— 包不改数)。
|
|
6138
|
+
- **G31-7**:同一条 run 的 `total_cost_usd` **等于 own**(`costMicroUsd / 1e6`),**不等于**
|
|
6139
|
+
`own + nested`(修前后都不变 —— 这是一条防回归的反钉)。
|
|
6140
|
+
- **G31-8**:同一条 run **恰有一条** `run_cost_reconciled` chrome 事件,
|
|
6141
|
+
`reconciledMicroUsd === ownMicroUsd + nestedMicroUsd`。
|
|
6142
|
+
- **G31-9**:一台**没有价表**的 worker 上跑同一条 run:臂上 `costAbsent === true`,
|
|
6143
|
+
**没有** `ownMicroUsd`、**没有** `reconciledMicroUsd`;终帧上 `_sema_cost_breakdown` 键**不出现**
|
|
6144
|
+
(core 在 unpriced 时整段省略)。
|
|
6145
|
+
- **G31-10**:一条**没委派**的 run:臂上没有 `nestedMicroUsd` 且 `reconciledMicroUsd === ownMicroUsd`;
|
|
6146
|
+
终帧上 `_sema_nested_usage` 键不出现。
|
|
6147
|
+
- **G31-10b**:一条**委派过但子代账读不出**的 run(`nested` 在场、其 `costMicroUsd` 缺席):
|
|
6148
|
+
臂上 `nestedCostAbsent === true` 且**没有** `reconciledMicroUsd`(修前会铸出一个只含 own 的
|
|
6149
|
+
「确定总额」);终帧上 `_sema_nested_usage` 键**在场**(委派这件事不丢)。
|
|
6150
|
+
- **G31-11**:一条被 409 拒绝的 turn:`run_cost_reconciled` 事件 **0 条**(没有账就不说话);
|
|
6151
|
+
`stats` 是畸形载体(数组/标量)时同样 0 条。
|
|
6152
|
+
- **G31-12**(负控):一条**子流** `turn_end` 不产生 `last_turn_usage` chrome 事件(既有断闸未被本批改动)。
|
|
6153
|
+
|
|
6154
|
+
---
|
|
6155
|
+
|
|
6156
|
+
### 31d. ③ L-192② 终帧扁平 `usage` 三格(core ≥7.12.0 usage 语义)
|
|
6157
|
+
|
|
6158
|
+
**键名与载体**
|
|
6159
|
+
|
|
6160
|
+
| 键 | 载体 | 来源 | 缺席语义 |
|
|
6161
|
+
|---|---|---|---|
|
|
6162
|
+
| `usage.webSearchRequests` | CC result 帧的扁平 usage | 按 CC 的名字**开集宽读**(`TaskStats` 是开集索引) | 读不出 ⇒ 值仍 `0`(CC 形必填 number)**+ 判别位** |
|
|
6163
|
+
| `usage._sema_web_search_requests_absent` | 同上(超集键) | 上一格读不读得出 | 只在读不出时 `true`;绝不铸 `false` |
|
|
6164
|
+
| `usage._sema_total_input_tokens` | 同上(超集键) | `TaskStats.totalInputTokens`(**cache-INCLUSIVE** 总量) | 键不铸 = wire 没报;显式 `0` 是事实 |
|
|
6165
|
+
| `usage._sema_cache_write_tokens_long` | 同上(超集键) | `TaskStats.cacheWriteTokensLong`(**1 小时 TTL** 那一档的缓存写入) | 键不铸 = wire 没报;显式 `0` 是读数(core 逐字「0 unless 1h caching is in use」) |
|
|
6166
|
+
| `ModelUsage._sema_web_search_requests_absent` | 每一行 CC `ModelUsage` 镜像(超集键) | 同上(同形第二处,两个 mint 点一个规矩) | 同上 |
|
|
6167
|
+
| **终局** per-model 行的 `_sema_total_input_tokens` / `_sema_usage_basis` / `_sema_cache_write_tokens_long` | 终帧 `modelUsage[*]`(超集键;**只有终局这一面**) | 行上的 `totalInputTokens` / `usageBasis` / `cacheWriteTokensLong` | 逐键不铸 = 这一行没报;`usageBasis` 是**开集串**,🔴 **缺席不许倒推口径** |
|
|
6168
|
+
|
|
6169
|
+
**🔴 `inputTokens` 为什么**不**改读 `totalInputTokens`**(本批唯一一处「台账写法 vs 合同」的分歧,
|
|
6170
|
+
处置写明在此):台账原句是「input 用旧键 `promptTokens` 未读 `totalInputTokens`」。核合同后**不采**
|
|
6171
|
+
那个写法 —— core 的 RB-457-a(3.0.0 BREAKING)把 `promptTokens` 的语义**翻**成了 cache-**MISS**
|
|
6172
|
+
分量,而 CC / Anthropic 的 `input_tokens` 本义正是 MISS;同一个 `usage` 对象上已经另有
|
|
6173
|
+
`cacheReadInputTokens` 与 `cacheCreationInputTokens` 两格,把 cache-INCLUSIVE 总量灌进 `inputTokens`
|
|
6174
|
+
就是 core 逐字点名的那笔**双算**(逐字:「a consumer computing `cachedTokens / (promptTokens +
|
|
6175
|
+
cachedTokens)` double-counted the cache subset and reported `h/(1+h)` (a 98% hit rate surfaced as
|
|
6176
|
+
49.5%)」)。那等于**改 CC 同名键的语义**,不是超集。⇒ `inputTokens` 逐字不动(MISS 分量),
|
|
6177
|
+
总量**另开超集座位**。这也与本包既有纪律一致:CC `ModelUsage` 镜像上同样没有总量座位。
|
|
6178
|
+
|
|
6179
|
+
**🔴 那为什么终帧要开座位、而 per-turn 那一面不开**:per-turn 有**逐字通道**
|
|
6180
|
+
(`EngineTurnUsage` / chrome `last_turn_usage.engineUsage`,整对象原形过境),要总量的消费面去那儿取;
|
|
6181
|
+
而**终帧这一面没有任何别的载体** —— 不开座位,「这条 run 一共摆了多少上下文」在终帧上就问不出来。
|
|
6182
|
+
**同一条理由**把三个座位一并给了**终局的 per-model 行**(一个全局总量答不了「哪个模型摆了多少」,
|
|
6183
|
+
而那张分表同样没有逐字通道):加挂只发生在两条**终局**腿上,per-turn 与终局共用的那个 mint 点
|
|
6184
|
+
(`toCcModelUsage`)一个字不动 —— 门里有一条反向钉盯着 per-turn 镜像不长这些座位。
|
|
6185
|
+
|
|
6186
|
+
**🔴 长 TTL 缓存写入为什么只「另给」不「相加」**:core 对两格的说法互相矛盾 ——
|
|
6187
|
+
`cacheWriteTokens` 自述是「Anthropic `cache_creation_input_tokens`」(协议侧那个量本就含 1h 子项),
|
|
6188
|
+
而 `totalInputTokens` 的求和式又把 `cacheWriteTokens` 与 `cacheWriteTokensLong` **并列相加**
|
|
6189
|
+
(那要求二者互不重叠)。相加与不加**各有一种错法**,证据不足**不猜**:CC 的
|
|
6190
|
+
`cacheCreationInputTokens` 逐字保持 `cacheWriteTokens`(既有值一字不动),长 TTL 分量原样另给,
|
|
6191
|
+
对账由消费方按两个数自己做;候上游澄清后再定。
|
|
6192
|
+
|
|
6193
|
+
**本批刻意不投的位(登记在案,不是漏)**
|
|
6194
|
+
|
|
6195
|
+
| 位 | 为什么不投 | 端要它时去哪儿取 |
|
|
6196
|
+
|---|---|---|
|
|
6197
|
+
| `ModelUsage.contextWindow` / `maxOutputTokens` | 它们是**模型的静态元数据**,不是这一轮的账;给它们立「缺席判别位」会把「去目录里取」误导成「等 usage 面补上」 | 模型目录(`/v1/models[].contextWindow`),本包 `resolveModelCatalog` 一侧 |
|
|
6198
|
+
| `TaskStats.cacheHitRate` / `toolCalls` / `mechanisms` / `memory` / `suggestions` | 都是**可观测面**而非 CC 形上的位,今天无端消费;投影它们等于先造一堆没人读的超集键 | 需要时按 `L-211` 一并立项(该票本就是「终局回执未消费族」) |
|
|
6199
|
+
| `humanReview.count` / `totalWaitMs` | 人工复核**耗时轴**(core 明记它**永不**折进成本/预算),与本批的拒绝清单是两件事;要它的是运维面不是会话面 | 同上,候 `L-211` |
|
|
6200
|
+
|
|
6201
|
+
**黑盒判据**
|
|
6202
|
+
|
|
6203
|
+
- **G31-13**:任一条 run 的终帧:`usage.webSearchRequests === 0` 且
|
|
6204
|
+
`usage._sema_web_search_requests_absent === true`(今天 wire 上没有这本账);
|
|
6205
|
+
每一行 `modelUsage[*]` 上同样带这一位。
|
|
6206
|
+
- **G31-14**:一条 prompt 命中过缓存的 run:`usage.inputTokens` **小于**
|
|
6207
|
+
`usage._sema_total_input_tokens`(前者是 MISS 分量、后者是含 cache 的总量),且
|
|
6208
|
+
`usage.inputTokens` 与该 run 的 `promptTokens` **逐字相等**。
|
|
6209
|
+
- **G31-15**:一台不报 usage 的网关上的 run:`usage._sema_total_input_tokens` 键**不出现**
|
|
6210
|
+
(不拿 `promptTokens` 冒充总量),四个既有 token 格仍是 `0`。
|
|
6211
|
+
- **G31-16**(负控):三个超集位**都不会**出现 `false` / `null` 形态。
|
|
6212
|
+
- **G31-17**:一条在**多模型**部署上跑完的 run:终帧 `modelUsage[<每个模型>]` 各自带
|
|
6213
|
+
`_sema_total_input_tokens`(该模型那一行的 cache-INCLUSIVE 总量);行上报了口径标记时
|
|
6214
|
+
`_sema_usage_basis` 逐字过境(修前两键都被剥)。
|
|
6215
|
+
- **G31-18**:一条用了 **1 小时 TTL 缓存**的 run:`usage._sema_cache_write_tokens_long > 0`,
|
|
6216
|
+
而 `usage.cacheCreationInputTokens` **逐字等于** wire 的 `cacheWriteTokens`(包不替引擎求和)。
|
|
6217
|
+
|
|
6218
|
+
---
|
|
6219
|
+
|
|
6220
|
+
### 31e. 消费方待办(三端逐条)
|
|
6221
|
+
|
|
6222
|
+
**cli(壳)**
|
|
6223
|
+
|
|
6224
|
+
1. 壳仓的成本账本(`cost-tracker.ts`)的终局对账:接 chrome 臂 `run_cost_reconciled`(或读终帧两个超集键),
|
|
6225
|
+
`/cost` 面把 **own / nested / compaction** 三段各渲一行;🔴 两个判别位在场时渲「未定价」而**不是**
|
|
6226
|
+
`$0.00`;🔴 **不要**把流中 `last_turn_usage` 的增量与本臂的终局总账相加(子代花费只经终局到账)。
|
|
6227
|
+
2. 壳仓里**本地铸** `permission_denials: []` 的两处(`sema/bridgeMessaging.ts` 与
|
|
6228
|
+
`sema/bootSignalTail.ts`):退役,改读投影;🔴 那两处自己合成的终帧同样只该在**账真的读得出且
|
|
6229
|
+
逐行判得出**时给 `[]`,否则要带上判别位 —— 本地铸一个裸 `[]` 就是把「没有账」说成「零拒绝」。
|
|
6230
|
+
3. 要「这条 run 摆了多少上下文」的读点(`/context` 与终帧 usage 面):改读
|
|
6231
|
+
`usage._sema_total_input_tokens`;🔴 **不要**把 `usage.inputTokens` 当总量(它是 MISS 分量)。
|
|
6232
|
+
4. 网搜计数面(若有):`webSearchRequests === 0` 从此要与判别位合读,别渲成「本次未使用网搜」。
|
|
6233
|
+
5. 拒绝清单的读点改吃 `_sema_permission_denials`(CC 的 `permission_denials` 今天恒 `[]`,
|
|
6234
|
+
要等上游把 `toolCallId`/`toolInput` 串上 gate 账本才会自动填);渲之前先看判别位。
|
|
6235
|
+
6. 缓存成本面若按 `cacheCreationInputTokens` 算钱:1 小时 TTL 那一档在
|
|
6236
|
+
`_sema_cache_write_tokens_long` 上单列,**别自己相加**(两格关系上游尚未澄清,见 §31d)。
|
|
6237
|
+
|
|
6238
|
+
**desktop**
|
|
6239
|
+
|
|
6240
|
+
1. 走包管线的成本面同 cli 第 1 条:`run_cost_reconciled` 是 `required: false`,**不接不会哑**,
|
|
6241
|
+
但不接就必须自己按终帧两个超集键对账,否则子代与压缩两笔账永远不进面板。
|
|
6242
|
+
2. 自查点:有没有自己铸过 `permission_denials`(座位 IPC 侧的结果帧转投)——有则改读投影。
|
|
6243
|
+
|
|
6244
|
+
**web**
|
|
6245
|
+
|
|
6246
|
+
1. 同上第 1 条;另:web 的账单/用量面若按 `usage.inputTokens` 求和估算上下文占用,按 §31d 改读
|
|
6247
|
+
`_sema_total_input_tokens`(否则命中率高的会话会被低估)。
|
|
6248
|
+
2. 判别位一律按「不渲肯定句」处理:本批四个判别位(`_sema_permission_denials_absent` /
|
|
6249
|
+
`_sema_web_search_requests_absent` / 臂上的 `costAbsent`·`nestedCostAbsent`)都**只在该说话时在场**,
|
|
6250
|
+
从不铸 `false`。
|
|
6251
|
+
|
|
6252
|
+
---
|
|
6253
|
+
|
|
6254
|
+
### 31f. 本批的门(新增两道 + 扩一道)
|
|
6255
|
+
|
|
6256
|
+
| 门 | 守什么 |
|
|
6257
|
+
|---|---|
|
|
6258
|
+
| `scripts/run-permission-denial-projection-test.mjs` | 🆕(L-192①):deny 行逐条铸(条数/顺序/`tool_name`)· `tool_use_id` 与 `tool_input` **键缺席**且缺席证据锚在 core 的 gate 声明块上(上游补上即红)· `_sema_tool_arg` 承载已脱敏摘要 · 「零拒绝」与「没有账」两键合读且判别位绝不铸 `false` · 成功臂与错误信封**单一 mint 点** · 非 deny 判词不猜 · 坏行逐行独立 |
|
|
6259
|
+
| `scripts/run-cost-reconcile-projection-test.mjs` | 🆕(B-068 / L-198):两段逐键窄读(micro-USD 原值、坏键剥掉不连坐、整段读不出不铸空对象、开集新键过境)· nested 花费未定价**键缺席绝不铸 0** · 🔴 `total_cost_usd` 语义零改的反钉 · chrome 臂三段与对账值 · `stats` 缺席一条不发 · 非成功终局照发 · §E2 子流断闸反钉 |
|
|
6260
|
+
| `scripts/run-cost-absence-projection-test.mjs` | 🔁(L-192②):新增 E/F/G/H 四段 —— `webSearchRequests` 开集宽读 + 判别位(两个 mint 点同扫)· `_sema_total_input_tokens` 有座位而 `inputTokens` **仍是 MISS 分量**(灌总量即红)· core 逐字(MISS / cache-INCLUSIVE / 双算警告)· 长 TTL 分量另给不相加 · 终局 per-model 三座位 + **per-turn 镜像不长座位**的反向钉 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/client-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.0",
|
|
4
4
|
"description": "Client-side session runtime shared by every sema human client (TUI / web / desktop): sema wire frames (AgentEvent) -> CC session vocabulary (SDKMessage) with dual-plane output (transcript/chrome), deterministic transcript ids, lane discipline as a type, and the notification/dedup ledgers. Every CC-skin shape is collected here so the wire itself stays neutral. Renamed from @sema-agent/wire-cc-adapter (0.1.x).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|