@sema-agent/client-core 0.65.1 → 0.67.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 +139 -0
- package/README.md +5 -2
- package/dist/adapter/activeRunSelfHeal.js +3 -2
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +14 -0
- package/dist/adapter/downstream/eventToSdkMessage.js +13 -1
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +219 -1
- package/dist/adapter/downstream/terminalToSdkResult.js +317 -7
- package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +20 -1
- package/dist/adapter/downstream/turnUsageToModelUsage.js +7 -2
- package/dist/adapter/runStream.js +118 -4
- package/dist/adapter/types.d.ts +27 -0
- package/dist/autoModeUnavailable.d.ts +95 -70
- package/dist/autoModeUnavailable.js +130 -97
- package/dist/classifierStatus.d.ts +7 -5
- package/dist/classifierStatus.js +62 -20
- package/dist/engineNoticeCodes.d.ts +19 -2
- package/dist/engineNoticeCodes.js +25 -3
- package/dist/gateOutcome.d.ts +14 -0
- package/dist/gateOutcome.js +3 -1
- package/dist/gateVocabulary.d.ts +37 -0
- package/dist/gateVocabulary.js +69 -8
- package/dist/hitl/toolApprovalWire.d.ts +44 -43
- package/dist/hitl/toolApprovalWire.js +47 -59
- package/dist/seam.d.ts +89 -0
- package/dist/seam.js +22 -0
- package/docs/INTEGRATION-CLIENTS.md +776 -11
- package/package.json +2 -2
|
@@ -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,272 @@ 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
|
+
// 0.67.0(core 7.14.0):usage 下界位。**严格 true 才铸**(core 契约:`true` 或缺席,恒不写
|
|
216
|
+
// `false`/`null`;认宽了就会把一个 falsy 值渲成「数得不全」)。它与成本三段正交,所以读在这里、
|
|
217
|
+
// 与三段同一只读器出 —— 两面(终帧超集键 / chrome 对账臂)因此永远不会各算各的。
|
|
218
|
+
const usageLowerBound = stats.usageMissing === true;
|
|
219
|
+
const reconcile = {
|
|
220
|
+
...(ownMicroUsd !== undefined ? { ownMicroUsd } : { costAbsent: true }),
|
|
221
|
+
...(usageLowerBound ? { usageLowerBound: true } : {}),
|
|
222
|
+
...(nestedMicroUsd !== undefined ? { nestedMicroUsd } : {}),
|
|
223
|
+
...(nestedUnknown ? { nestedCostAbsent: true } : {}),
|
|
224
|
+
...(breakdown?.compactionMicroUsd !== undefined ? { compactionMicroUsd: breakdown.compactionMicroUsd } : {}),
|
|
225
|
+
...(breakdown?.llmRootMicroUsd !== undefined ? { llmRootMicroUsd: breakdown.llmRootMicroUsd } : {}),
|
|
226
|
+
// 对账值:own 读得出、子代那一边不是「跑了却不知道多少钱」、且和本身有限时才算得出来。
|
|
227
|
+
...(sum !== undefined && Number.isFinite(sum) ? { reconciledMicroUsd: sum } : {}),
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
...(breakdown !== undefined ? { breakdown } : {}),
|
|
231
|
+
...(nested !== undefined ? { nested } : {}),
|
|
232
|
+
reconcile,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* D-3 —— 终帧上的两个成本超集键(成功臂与错误信封共用的**唯一** mint 点)。
|
|
237
|
+
* 🔴 CC 同名键 `total_cost_usd` **语义零改**:它仍然只是 own 花费,`nested` **不折进去**
|
|
238
|
+
* (超集键纪律:CC 形上已有的位不许塞我们自己的含义)。「fully-reconciled spend」由消费方按
|
|
239
|
+
* 这两个超集键自己加 —— 包给的是**可对账的事实**,不是一个改了口径的数。
|
|
240
|
+
*/
|
|
241
|
+
function costFactParts(stats, ctx) {
|
|
242
|
+
const facts = readRunCostFacts(stats);
|
|
243
|
+
// 🔴 **下界位与成本三段分开算**(异源对抗复审 [medium] 实抓):`stats` 读不出(`failed` 事件帧 /
|
|
244
|
+
// 409 拒绝信封 / park 体)时**成本**那三段确实没有账、一条都不该说;但「这条流观测到过一轮
|
|
245
|
+
// 没有 usage」这件事**照旧成立**,而那种终帧的 `usage` 恰恰是 `flattenUsage(undefined)` 的
|
|
246
|
+
// 全零 —— 早退回空对象等于把「不知道」渲成精确零(正是本位要修的病的另一条路径)。
|
|
247
|
+
// ⇒ 两者**取并**:stats 说了算一半,流内观测算另一半。
|
|
248
|
+
const lowerBound = facts?.reconcile.usageLowerBound === true || ctx.usageMissingObserved === true;
|
|
249
|
+
const lowerBoundPart = lowerBound ? { _sema_usage_lower_bound: true } : {};
|
|
250
|
+
if (facts === undefined)
|
|
251
|
+
return lowerBoundPart;
|
|
252
|
+
return {
|
|
253
|
+
...lowerBoundPart,
|
|
254
|
+
...(facts.breakdown !== undefined ? { _sema_cost_breakdown: facts.breakdown } : {}),
|
|
255
|
+
...(facts.nested !== undefined ? { _sema_nested_usage: facts.nested } : {}),
|
|
256
|
+
// ── 0.67.0(core 7.14.0 `TaskResult.stats.usageMissing`)────────────────────────────────
|
|
257
|
+
// 🔴 CC 同名键**语义零改**:`usage` / `modelUsage` / `total_cost_usd` 的字节一个不动,本位是
|
|
258
|
+
// 一个 `_sema_` 超集**判别位**,说的是「上面那些数字是**下界**」。把它折进数字里(比如
|
|
259
|
+
// 把 tokens 抹成 null)会让每一个既有消费者当场坏掉,而它们今天读到的是一笔**看起来
|
|
260
|
+
// 已知**的账 —— 那正是本位要修的病。
|
|
261
|
+
// 🔴 **缺席不铸**(never false):缺席 ⇔ 每一轮都报了 usage。
|
|
262
|
+
// 🔴 与 §31d 的 `usage._sema_total_input_tokens` **同帧不同问**:那一位答「这一行报了多少总
|
|
263
|
+
// 输入」,本位答「这一整条 run 的账数全了没有」。
|
|
264
|
+
// (本位在上面与流内观测取并后已铸;这里不再重复。)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* L-228 —— 流内子代分表 → 终帧两个超集键的**唯一 mint 点**(成功臂与错误信封共用)。
|
|
269
|
+
*
|
|
270
|
+
* **不导出**:它的入参是一个只有 `runStream` 攒得出来的累加表,推上公面等于邀请宿主自己攒一份
|
|
271
|
+
* 「看见了多少」的账(而那份账的 `partial` 判据在宿主手上不成立)。包侧门走**端到端**素材:
|
|
272
|
+
* 喂子流 `turn_end` + `done` 给 `runStream`,断言终帧上那两个键 —— 那也是三端真正拿到它的路。
|
|
273
|
+
*
|
|
274
|
+
* ── 🔴 `partial` 怎么判(设计定谳,与派车单的「恒铸于重连车道」不同,理由写在这里)────────────
|
|
275
|
+
* 派车单给的形是「本条流不是从 run 起点观测 ⇒ 恒铸 partial」。而本层**没有**「从不从起点观测」
|
|
276
|
+
* 这个读数:`runStream` 手上只有**同一条流内**的事件序号去重表(`seen`),它答的是「这一帧重放过
|
|
277
|
+
* 没有」,答不了「这条流之前还有没有别的帧」;`--resume` / 重连开出来的流与首开的流在本层
|
|
278
|
+
* **逐位不可分**。按一个读不出来的量铸判别位 = 编一件事实。
|
|
279
|
+
* ⇒ 判据改锚在**真正决定结果的量**上([anchor-on-the-deciding-quantity]):拿引擎自己的**权威
|
|
280
|
+
* 合计** `stats.nested` 对账 —— 分表的 `turns` 之和等于 `nested.turns` **且**行数等于 `nested.tasks`
|
|
281
|
+
* 时,这张表**可证**覆盖了整条 run ⇒ 不铸 partial;任何一边对不上、或 `stats.nested` 根本读不出来
|
|
282
|
+
* ⇒ **铸 partial**。
|
|
283
|
+
* 🔴 失效方向是安全的那一侧:上游哪天让某类嵌套轮不上子流 `turn_end`(比如更深一层的编排),
|
|
284
|
+
* 本判据只会**多**铸 partial(把一张其实完整的表说成不完整),**永远不会**把一张残表说成完整。
|
|
285
|
+
* 🔴 **两个键不互证、也不相加**:`_sema_nested_usage`(合计,引擎报的)与本表(流内看见的)是
|
|
286
|
+
* 两份独立的账;`partial` 在场时两者**本来就该不等**,消费方不许拿其中一份去「修正」另一份。
|
|
287
|
+
*/
|
|
288
|
+
function nestedUsageByTaskParts(stats, rollup) {
|
|
289
|
+
// 🔴 一行都没有 ⇒ **什么都不说**:空表会被读成「这条 run 一个子代都没委派」,而真相可能是
|
|
290
|
+
// 「委派了,但这条流没看见任何一轮」(重连车道)。两句话不许折成一句。
|
|
291
|
+
if (rollup === undefined || rollup.size === 0)
|
|
292
|
+
return {};
|
|
293
|
+
const rows = {};
|
|
294
|
+
let turnsSeen = 0;
|
|
295
|
+
for (const [taskId, r] of rollup) {
|
|
296
|
+
turnsSeen += r.turns;
|
|
297
|
+
rows[taskId] = {
|
|
298
|
+
turns: r.turns,
|
|
299
|
+
inputTokens: r.inputTokens,
|
|
300
|
+
outputTokens: r.outputTokens,
|
|
301
|
+
...(r.cacheReadTokens !== undefined ? { cacheReadTokens: r.cacheReadTokens } : {}),
|
|
302
|
+
...(r.usageMissing === true ? { usageMissing: true } : {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const nested = stats !== null && typeof stats === 'object' && !Array.isArray(stats)
|
|
306
|
+
? stats.nested
|
|
307
|
+
: undefined;
|
|
308
|
+
const authTurns = finiteOrAbsent(nested?.turns);
|
|
309
|
+
const authTasks = finiteOrAbsent(nested?.tasks);
|
|
310
|
+
const complete = authTurns !== undefined && authTasks !== undefined &&
|
|
311
|
+
authTurns === turnsSeen && authTasks === Object.keys(rows).length;
|
|
312
|
+
return {
|
|
313
|
+
_sema_nested_usage_by_task: rows,
|
|
314
|
+
...(complete ? {} : { _sema_nested_usage_by_task_partial: true }),
|
|
40
315
|
};
|
|
41
316
|
}
|
|
42
317
|
/** P1-5 — real elapsed ms since the stream opened (runStream stamps startedAtMs); 0 only pre-stamp. */
|
|
@@ -112,13 +387,15 @@ function mapModelUsage(stats) {
|
|
|
112
387
|
if (u === undefined || u === null || typeof u !== 'object')
|
|
113
388
|
continue;
|
|
114
389
|
const m = u;
|
|
115
|
-
out[modelId] = toCcModelUsage({
|
|
390
|
+
out[modelId] = withTerminalUsageFacts(toCcModelUsage({
|
|
116
391
|
inputTokens: numOrAbsent(m.inputTokens),
|
|
117
392
|
outputTokens: numOrAbsent(m.outputTokens),
|
|
118
393
|
cacheReadTokens: numOrAbsent(m.cacheReadTokens),
|
|
119
394
|
cacheWriteTokens: numOrAbsent(m.cacheWriteTokens),
|
|
120
395
|
costMicroUsd: numOrAbsent(m.costMicroUsd),
|
|
121
|
-
|
|
396
|
+
// D-2(0.66.0):per-model 行今天也不带这一格 ⇒ 宽读一次,读不出由 mint 点立判别位。
|
|
397
|
+
webSearchRequests: numOrAbsent(m.webSearchRequests),
|
|
398
|
+
}), m);
|
|
122
399
|
}
|
|
123
400
|
return out;
|
|
124
401
|
}
|
|
@@ -139,16 +416,37 @@ function modelUsageFor(stats, model) {
|
|
|
139
416
|
// stats?.costUsd === 'number'` 的旧写法在 SDK 1.x 下照样编译得过,只是恒假 ⇒ 合成行的
|
|
140
417
|
// costUSD 静默恒 0(比报错更隐蔽)。tsc 只红了 6 处直传,这处靠人核 + pure 门的行为钉。
|
|
141
418
|
return {
|
|
142
|
-
[model]: toCcModelUsage({
|
|
419
|
+
[model]: withTerminalUsageFacts(toCcModelUsage({
|
|
143
420
|
inputTokens: stats?.promptTokens,
|
|
144
421
|
outputTokens: stats?.outputTokens,
|
|
145
422
|
cacheReadTokens: stats?.cachedTokens,
|
|
146
423
|
// B-073 ③ 族扫:合成行是 `flattenUsage` 的**同形第二处**,同批一并改读真值。
|
|
147
424
|
cacheWriteTokens: stats?.cacheWriteTokens,
|
|
148
425
|
costMicroUsd: stats?.costMicroUsd,
|
|
149
|
-
|
|
426
|
+
// D-2(0.66.0):与扁平 usage 同一格同一读法(开集宽读,读不出由 mint 点立判别位)。
|
|
427
|
+
webSearchRequests: stats?.webSearchRequests,
|
|
428
|
+
}), stats),
|
|
150
429
|
};
|
|
151
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* 给一条已铸好的 CC `ModelUsage` 行挂上终局那三个座位(读不出的一律不铸)。
|
|
433
|
+
* `raw` = 这一行的 wire 原形(分表的那一行,或扁平 `TaskStats` —— 两处键名相同)。
|
|
434
|
+
*/
|
|
435
|
+
function withTerminalUsageFacts(row, raw) {
|
|
436
|
+
const total = finiteOr0Absent(raw?.totalInputTokens);
|
|
437
|
+
const long = finiteOr0Absent(raw?.cacheWriteTokensLong);
|
|
438
|
+
const basis = nonEmptyStr(raw?.usageBasis);
|
|
439
|
+
return {
|
|
440
|
+
...row,
|
|
441
|
+
...(total !== undefined ? { _sema_total_input_tokens: total } : {}),
|
|
442
|
+
...(basis !== undefined ? { _sema_usage_basis: basis } : {}),
|
|
443
|
+
...(long !== undefined ? { _sema_cache_write_tokens_long: long } : {}),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/** 有限数窄化(非数 / 非有限 ⇒ 缺席;`0` 是读数不是缺席)。 */
|
|
447
|
+
function finiteOr0Absent(v) {
|
|
448
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
449
|
+
}
|
|
152
450
|
/**
|
|
153
451
|
* TaskResult 五键最小消费批([1543] SDK-M,2026-07-23)之一 —— `degraded` = 模型降级链实录
|
|
154
452
|
* (from/to/reason/atTurn;多模型网关部署的关键可观测面)。
|
|
@@ -264,7 +562,13 @@ function errorResult(ctx, parts) {
|
|
|
264
562
|
total_cost_usd: costOrNull(parts.stats), // P1-5: null when unknown, never a fake 0
|
|
265
563
|
usage: flattenUsage(parts.stats),
|
|
266
564
|
modelUsage: modelUsageFor(parts.stats, parts.model),
|
|
267
|
-
|
|
565
|
+
// D-1 / L-192①:两句话不再折成一句 —— 清单 + 「有没有这本账」的判别位,见 permissionDenialParts。
|
|
566
|
+
...permissionDenialParts(parts.stats),
|
|
567
|
+
// D-3 / B-068:失败/到限/park 的 run 一样花过钱,账不因结局不好就不报。
|
|
568
|
+
...costFactParts(parts.stats, ctx),
|
|
569
|
+
// L-228(0.67.0):流内 per-subagent 分表的收口快照(判据本体在 `nestedUsageByTaskParts`)。
|
|
570
|
+
// 🔴 与成本三段同理 —— 失败的 run 一样委派过,账不因结局不好就不报。
|
|
571
|
+
...nestedUsageByTaskParts(parts.stats, ctx.nestedUsageByTask),
|
|
268
572
|
errors: [...parts.errors],
|
|
269
573
|
...(parts.errorCode !== undefined && parts.errorCode.length > 0 ? { errorCode: parts.errorCode } : {}),
|
|
270
574
|
...(parts.degraded !== undefined ? { degraded: parts.degraded } : {}),
|
|
@@ -454,7 +758,13 @@ export function doneToSdkResult(ev, ctx) {
|
|
|
454
758
|
usage: flattenUsage(stats),
|
|
455
759
|
// MF-25/P1-5 — the per-model usage split (wire `stats.modelUsage`, or the synthesized current-model row).
|
|
456
760
|
modelUsage: modelUsageFor(stats, r.model),
|
|
457
|
-
|
|
761
|
+
// D-1 / L-192①:同形第二处 —— 与错误信封共用**同一个** mint 点(修前两处各一个字面量 [])。
|
|
762
|
+
...permissionDenialParts(stats),
|
|
763
|
+
// D-3 / B-068:成本明细与子代那本账(micro-USD 原值);`total_cost_usd` 语义一字不动。
|
|
764
|
+
...costFactParts(stats, ctx),
|
|
765
|
+
// L-228(0.67.0):流内 per-subagent 分表的收口快照;与 `_sema_nested_usage`(引擎报的合计)
|
|
766
|
+
// 是**两份独立的账**,不相加、不互证(见 `nestedUsageByTaskParts` 顶注)。
|
|
767
|
+
...nestedUsageByTaskParts(stats, ctx.nestedUsageByTask),
|
|
458
768
|
// MF-25 — the effective served model id (`done.result.model`, e.g. "deepseek-v4-pro"). The CC
|
|
459
769
|
// SDKResultSuccess schema has no `model` field, so this rides as an additive seam field a cost/overview
|
|
460
770
|
// 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
|
* 不是刚才那一道?」
|
|
@@ -287,6 +310,15 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
287
310
|
// projector can emit a REAL `duration_ms` (the wire carries no duration; the old hardcoded 0 was fake).
|
|
288
311
|
if (ctx.startedAtMs === undefined)
|
|
289
312
|
ctx.startedAtMs = Date.now();
|
|
313
|
+
// ── L-228(0.67.0):per-subagent turn usage 的**流内**累加表 ─────────────────────────────────
|
|
314
|
+
// 🔴 **每条流一张**(局部量,不是模块级):两条并发的流各自攒各自看见的账;做成模块级单例会让
|
|
315
|
+
// A 流的子代用量落进 B 流的终帧(本仓在册的「共享 store 跨流污染」病形)。
|
|
316
|
+
// 🔴 它**只在终帧那一拍**挂到 `ctx` 上(见下面 `done`/`failed` 分支),不是开流时就挂 ——
|
|
317
|
+
// 终帧的铸点在 `terminalToSdkResult`(单一 mint 点,见该文件顶注:给信封加一个位要改六处
|
|
318
|
+
// 正是它收编掉的病),而 `ctx` 是**调用方**的对象:宿主若把同一个 ctx 复用给两条并发的流,
|
|
319
|
+
// 开流时挂等于让后开的那条把先开的那条的表顶掉,先开的终帧于是报出别人的账。
|
|
320
|
+
// 挂在终帧那一拍 + 与 `terminalToSdkResult(...)` 在**同一个同步步**里,那个窗按构造不存在。
|
|
321
|
+
const nestedUsageByTask = new Map();
|
|
290
322
|
for await (const ev of events) {
|
|
291
323
|
// event-id idempotency — drop a re-seen durable seq (contract 02 §1.1).
|
|
292
324
|
const seq = eventSeq(ev);
|
|
@@ -356,6 +388,14 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
356
388
|
// `usage` **可以同帧**)。把它剥掉,本批新开的这条 usage 通道就会把「不知道」渲成一笔
|
|
357
389
|
// 全零的已知账 —— 与本批要根治的病(B-073 的成本 0/缺席)逐字同形,只是换了个量。
|
|
358
390
|
const usageMissing = ev.usageMissing === true;
|
|
391
|
+
// 🔴 L-215③ 的**终局对偶**(异源对抗复审 [medium] 实抓):这条流上**只要有一轮**报过
|
|
392
|
+
// 「这一轮没有 usage」,终帧上那些数字就是**下界**。`stats.usageMissing` 只在带得出
|
|
393
|
+
// `TaskResult` 的终帧上有,而 `failed` 事件帧 / 409 拒绝信封 / park 体**根本没有 stats**
|
|
394
|
+
// ⇒ 只读 stats 的话,一条已经观测到缺口的 run 会在终帧上被读成「每一轮都报了 usage」
|
|
395
|
+
// (而 `usage` 那几格恰好是 `flattenUsage(undefined)` 的全零)——「不知道」渲成了精确零。
|
|
396
|
+
// ⇒ 流内观测到就记下来,终帧那一拍与 stats 的读数**取并**(见 costFactParts)。
|
|
397
|
+
if (usageMissing)
|
|
398
|
+
ctx.usageMissingObserved = true;
|
|
359
399
|
const stopReasonRaw = ev.stopReason;
|
|
360
400
|
const stopWord = typeof stopReasonRaw === 'string' && stopReasonRaw.length > 0 ? stopReasonRaw : undefined;
|
|
361
401
|
const usage = turnEndUsage(ev);
|
|
@@ -377,11 +417,11 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
377
417
|
// ——壳内向的动态边。改成 chrome 事件(宿主消费义务见 seam.ts `last_turn_usage` 臂);
|
|
378
418
|
// 与原式同样**不 await**(fire-and-forget),sink 抛错不影响流(fail-soft 原语义)。
|
|
379
419
|
if (!isSubFlow && ctx.emitChrome) {
|
|
380
|
-
|
|
420
|
+
{
|
|
381
421
|
// engineUsage = [2295] 裁 ② 逐字原形(additive 键,契约见 seam.ts 本臂 doc):
|
|
382
422
|
// 总量/命中率消费面吃它,镜像 usage 保纯。eopt:additive 键按「诚实缺席」= 键不在
|
|
383
423
|
// (与 turn_end.usage 本身「整体 optional,缺席≠空对象」同一语义,不是「键在值 undefined」)。
|
|
384
|
-
|
|
424
|
+
emitChromeFireAndForget(ctx, {
|
|
385
425
|
kind: 'last_turn_usage',
|
|
386
426
|
laneProof: MAIN,
|
|
387
427
|
usage,
|
|
@@ -394,7 +434,65 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
394
434
|
...(stopWord !== undefined ? { stopReason: stopWord } : {}),
|
|
395
435
|
});
|
|
396
436
|
}
|
|
397
|
-
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// ── L-228(0.67.0):子流那条腿 —— **additive 第三条腿**,主臂两处 `!isSubFlow` 断闸不动 ────
|
|
440
|
+
// 病(车 E 件⑥ 实抓):core 终局只有合计 `stats.nested`(**无 per-task 分项**),而流里每条
|
|
441
|
+
// 子流 turn_end 都带着它自己那一轮的 usage —— 本包此前在 §E2 断闸处**折而未读**,于是壳的
|
|
442
|
+
// 子代详情面只渲得出 `_sema_usage_absent`。⇒ 分表只能由流内累加得出,这就是那条通道。
|
|
443
|
+
// 🔴 **归属钥匙**:`sourceTaskId` 优先,wire 缺席时回落 `parentToolCallId`(同一只子代的每一轮
|
|
444
|
+
// 至少归得到同一行)。两个都读不出 ⇒ **整条不入表**(编一个 `"unknown"` 行就是把几只子代的
|
|
445
|
+
// 账混成一只)。身份位为什么**不是** `taskId`,见下面那段 🔴。
|
|
446
|
+
// 🔴 **`usageMissing` 与数字同帧并存**:core 明说那一轮的 usage 是 UNKNOWN 不是 0 ⇒ 数字照
|
|
447
|
+
// 累加(它是**下界**),判别位在行上立起来;把那一轮整个丢掉会让下界更假。
|
|
448
|
+
if (isSubFlow) {
|
|
449
|
+
const parentToolCallId = ev.parentToolCallId;
|
|
450
|
+
// 🔴 **身份键读 `sourceTaskId`,不是 `taskId`**(异源对抗复审 [high] 实抓,core 真字节直证):
|
|
451
|
+
// core `TaskEventIdentity` 顶注逐字「the `WorkflowRun.sourceTaskId` family, **NOT a second
|
|
452
|
+
// `taskId`** — that field already exists on `task_progress` and a duplicate would bite
|
|
453
|
+
// consumers」⇒ 子代内容事件上**根本没有** `taskId` 这一位,读它恒缺席、恒回落到父调用 id,
|
|
454
|
+
// 于是**同一个父调用下的多只子任务会并成一行**(sdk 的 `turn_end` 臂也没有声明任何身份位,
|
|
455
|
+
// 两条腿都读 cast —— 这正是「按 d.ts 抄,不按印象猜」那条纪律要防的形)。
|
|
456
|
+
const rawSourceTaskId = ev.sourceTaskId;
|
|
457
|
+
const parent = typeof parentToolCallId === 'string' && parentToolCallId.length > 0 ? parentToolCallId : undefined;
|
|
458
|
+
const sourceTaskId = typeof rawSourceTaskId === 'string' && rawSourceTaskId.length > 0 ? rawSourceTaskId : undefined;
|
|
459
|
+
// 行键:`sourceTaskId` 优先,缺席回落父调用 id(同一只子代的每一轮至少归得到同一行)。
|
|
460
|
+
// ⚠️ 回落**有损**:同父调用多子任务会并成一行 —— 那时终帧那张表的 `partial` 判别位会因
|
|
461
|
+
// 行数对不上 `nested.tasks` 而立起来(诚实缺席优先于假装分得开)。
|
|
462
|
+
const taskId = sourceTaskId ?? parent;
|
|
463
|
+
if (taskId !== undefined && parent !== undefined) {
|
|
464
|
+
// 🔴 发臂条件与主臂 0.65.1 / B-088 **逐字同族**:core 真会发**裸**
|
|
465
|
+
// `{type:'turn_end', usageMissing:true}`(无 usage、无 stopReason),旧条件「有 usage 才发」
|
|
466
|
+
// 会让**最诚实的那一帧**整条静默 —— 那是本仓已定谳的病形,子代这条腿不许再犯一次。
|
|
467
|
+
// 三者任一在场即发;三者皆缺席仍不发。
|
|
468
|
+
if ((usage !== undefined || usageMissing || stopWord !== undefined) && ctx.emitChrome) {
|
|
469
|
+
emitChromeFireAndForget(ctx, {
|
|
470
|
+
kind: 'subagent_turn_usage',
|
|
471
|
+
laneProof: { lane: 'subagent', parentToolCallId: parent },
|
|
472
|
+
taskId,
|
|
473
|
+
...(sourceTaskId !== undefined ? { sourceTaskId } : {}),
|
|
474
|
+
parentToolCallId: parent,
|
|
475
|
+
...(usage !== undefined ? { usage } : {}),
|
|
476
|
+
...(ev.usage !== undefined ? { engineUsage: ev.usage } : {}),
|
|
477
|
+
...(usageMissing ? { usageMissing: true } : {}),
|
|
478
|
+
...(stopWord !== undefined ? { stopReason: stopWord } : {}),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
const row = nestedUsageByTask.get(taskId) ?? { turns: 0, inputTokens: 0, outputTokens: 0 };
|
|
482
|
+
row.turns += 1;
|
|
483
|
+
row.inputTokens += usage?.inputTokens ?? 0;
|
|
484
|
+
row.outputTokens += usage?.outputTokens ?? 0;
|
|
485
|
+
// 🔴 `cacheReadTokens` 读的是**引擎原形** `ev.usage`,不是 CC 镜像:镜像的
|
|
486
|
+
// `cacheReadInputTokens` 是**必填** number,缺席在那儿已经被折成 0
|
|
487
|
+
// (`toCcModelUsage` 的 `finiteOrZero`)⇒ 从镜像读就再也分不出「没报」与「零命中」。
|
|
488
|
+
// ⇒ 一轮都没报过 ⇒ 键**不铸**;报过之后再加 0 的那些轮是真的零命中。
|
|
489
|
+
const cacheRead = ev.usage?.cacheReadTokens;
|
|
490
|
+
if (typeof cacheRead === 'number' && Number.isFinite(cacheRead)) {
|
|
491
|
+
row.cacheReadTokens = (row.cacheReadTokens ?? 0) + cacheRead;
|
|
492
|
+
}
|
|
493
|
+
if (usageMissing)
|
|
494
|
+
row.usageMissing = true;
|
|
495
|
+
nestedUsageByTask.set(taskId, row);
|
|
398
496
|
}
|
|
399
497
|
}
|
|
400
498
|
const outputTokens = ev.usage?.outputTokens;
|
|
@@ -554,6 +652,22 @@ async function* runStreamInner(events, ctx, handle = {}) {
|
|
|
554
652
|
}
|
|
555
653
|
catch { /* fail-soft — the terminal frame below still renders */ }
|
|
556
654
|
}
|
|
655
|
+
// D-3 / B-068 · L-198(0.66.0):**终局对账那一拍**。与终帧超集键
|
|
656
|
+
// (`_sema_cost_breakdown` / `_sema_nested_usage`)同一个读器 `readRunCostFacts` ⇒ 两面同源。
|
|
657
|
+
// 🔴 没有账就不说话:`stats` 读不出(409 拒绝信封 / park 体 / `failed` 事件帧)⇒ 不发。
|
|
658
|
+
// 🔴 子流断闸(§E2)一字不动:子代花费只经本臂的 `nestedMicroUsd`(终局 stats.nested)到账。
|
|
659
|
+
// fail-soft 同 plan_review_park:sink 抛错不影响终帧照常投影。
|
|
660
|
+
if (ev.type === 'done' && ctx.emitChrome) {
|
|
661
|
+
const doneStats = ev.result?.stats;
|
|
662
|
+
const costFacts = readRunCostFacts(doneStats);
|
|
663
|
+
if (costFacts !== undefined) {
|
|
664
|
+
emitChromeFireAndForget(ctx, { kind: 'run_cost_reconciled', laneProof: MAIN, ...costFacts.reconcile });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// L-228:见本表声明处的头注 —— 挂表与铸终帧在**同一个同步步**里,复用 ctx 的并发流不会串账。
|
|
668
|
+
// (`usageMissingObserved` 是 per-ctx 的**单调布尔**,没有「顶掉别人」这一形:复用 ctx 的两条流
|
|
669
|
+
// 里只要有一条观测到缺口,两条的数字就都该按下界读 —— 取并是安全的那一侧。)
|
|
670
|
+
ctx.nestedUsageByTask = nestedUsageByTask;
|
|
557
671
|
yield terminalToSdkResult(ev, ctx);
|
|
558
672
|
return;
|
|
559
673
|
}
|