@sema-agent/client-core 0.65.0 → 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 +77 -0
- package/README.md +3 -1
- package/dist/adapt/panelTasks.d.ts +2 -2
- package/dist/adapt/panelTasks.js +20 -15
- 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 +43 -5
- package/dist/seam.d.ts +29 -0
- package/dist/seam.js +9 -0
- package/docs/INTEGRATION-CLIENTS.md +287 -7
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -403,7 +425,11 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
403
425
|
// 旧条件让它整条静默 ⇒ 「这一轮为什么停」这条机读位在最需要它的那一刻(出错/中止)不见了。
|
|
404
426
|
// ⚠️ 旧消费者零影响:`outputTokens` 读不出时**整键不铸**,而 adapt 的 `turnUsageArm`
|
|
405
427
|
// 本来就以 `typeof m.outputTokens === 'number'` 开门 ⇒ 这种帧对它是 no-op。
|
|
406
|
-
|
|
428
|
+
// 🔴 0.65.1 / B-088(test [6961] 对抗复审轨实抓,core [6962] 证实真铸形 run-harness-handlers.ts:491):
|
|
429
|
+
// `usageMissing` 判别位**本身就是话**——core 该轮零 usage 帧时发裸 `{type:'turn_end',
|
|
430
|
+
// usageMissing:true}`(无 usage、无 stopReason),旧条件让它整条静默 ⇒ 最诚实的那一帧
|
|
431
|
+
// 反而丢了 `_sema_usage_missing`(G30-23)。三者任一在场即发;三者皆缺席仍不发(F4)。
|
|
432
|
+
if ((typeof outputTokens === 'number' || stopWord !== undefined || usageMissing) && !isSubFlow) {
|
|
407
433
|
// ── L-215③(0.65.0):assistant 行那两个**算不出来**的键的真值出口 ─────────────────
|
|
408
434
|
// `eventToSdkMessage` 的 `assistantArm` 刻意**不**在内容臂上铸 `usage` / `stop_reason`
|
|
409
435
|
// (帧序:内容臂先到、`turn_end` 后到 ⇒ 臂发出时引擎还没说这一轮花了多少;在那里铸只能
|
|
@@ -550,6 +576,18 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
550
576
|
}
|
|
551
577
|
catch { /* fail-soft — the terminal frame below still renders */ }
|
|
552
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
|
+
}
|
|
553
591
|
yield terminalToSdkResult(ev, ctx);
|
|
554
592
|
return;
|
|
555
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→已渲消息的映射表);
|