@sema-agent/client-core 0.67.1 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +211 -0
  2. package/README.md +67 -3
  3. package/dist/adapt/arms.js +27 -2
  4. package/dist/adapt/turnFlags.d.ts +14 -0
  5. package/dist/adapt/turnFlags.js +4 -1
  6. package/dist/adapter/activeRunSelfHeal.d.ts +53 -6
  7. package/dist/adapter/activeRunSelfHeal.js +79 -8
  8. package/dist/adapter/downstream/eventToSdkMessage.d.ts +18 -1
  9. package/dist/adapter/downstream/eventToSdkMessage.js +33 -2
  10. package/dist/adapter/downstream/terminalToSdkResult.d.ts +75 -6
  11. package/dist/adapter/downstream/terminalToSdkResult.js +144 -44
  12. package/dist/adapter/runStream.d.ts +22 -2
  13. package/dist/adapter/runStream.js +190 -52
  14. package/dist/adapter/types.d.ts +4 -28
  15. package/dist/autoModeUnavailable.d.ts +17 -9
  16. package/dist/autoModeUnavailable.js +26 -8
  17. package/dist/classifierStatus.d.ts +32 -4
  18. package/dist/classifierStatus.js +5 -3
  19. package/dist/engineErrorCodes.d.ts +52 -0
  20. package/dist/engineErrorCodes.js +117 -0
  21. package/dist/engineNoticeCodes.d.ts +95 -1
  22. package/dist/engineNoticeCodes.js +124 -1
  23. package/dist/gateVocabulary.d.ts +18 -7
  24. package/dist/gateVocabulary.js +21 -8
  25. package/dist/hitl/parkResolver.d.ts +0 -14
  26. package/dist/hitl/parkResolver.js +22 -9
  27. package/dist/hitl/toolApprovalWire.d.ts +2 -1
  28. package/dist/hitl/toolApprovalWire.js +1 -0
  29. package/dist/ownKey.d.ts +34 -0
  30. package/dist/ownKey.js +36 -0
  31. package/dist/retryStatus.d.ts +13 -2
  32. package/dist/retryStatus.js +4 -1
  33. package/dist/runTerminal.d.ts +87 -14
  34. package/dist/runTerminal.js +89 -15
  35. package/dist/toolResult.js +8 -0
  36. package/dist/workflowClient.d.ts +22 -0
  37. package/dist/workflowClient.js +37 -0
  38. package/docs/INTEGRATION-CLIENTS.md +469 -10
  39. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import { stamp } from '../types.js';
2
+ import { putOwnKey } from '../../ownKey.js';
2
3
  // 0.60.0(engine ≥7.64.0 / sdk 8.4.0):终局读数的**单一读器**(两代字节 → 一个带标因由)。
3
4
  import { isReviewPark, readRunTerminal, runTerminalCode, runTerminalGateToolName, } from '../../runTerminal.js';
4
5
  import { toCcModelUsage } from './turnUsageToModelUsage.js';
@@ -160,22 +161,11 @@ function permissionDenialParts(stats) {
160
161
  }
161
162
  /**
162
163
  * 0.67.1 —— **以 wire 给的 id / 键名当对象键**时的唯一落键姿势(`__proto__` 陷阱)。
163
- *
164
- * 🔴 `Object.prototype.__proto__` 是一个 **accessor**:在一只普通对象上写 `o["__proto__"] = v`
165
- * 走的是那只 setter ——**不产生自有属性**(v 是对象时还顺手改了 `o` 的原型),于是那一行在
166
- * `Object.keys` / `JSON.stringify` 里**整条消失**,连行数都少一。而本文件这几张表的键全都来自
167
- * wire(taskId / modelId / core 开集的 costBreakdown 键名),没有任何一条保证它们不等于这个字面。
168
- * ⇒ 落键一律走 `defineProperty`,与本包 `hitl/crashConverged.ts` 交付快照时的处置**同一条**
169
- * (那里逐字:「落键仍走 `defineProperty`(`__proto__` 同理)」)。
170
- *
171
- * 🔴 **不改成 null 原型对象交付**:端拿到的仍是一只正常对象(`hasOwnProperty` / `toString` 都在),
172
- * 本修只改「落键」这一步,不改交付形 —— 换原型会在宿主侧造出一类新的 `TypeError`。
173
- * 描述符与普通赋值**逐位相同**(`writable/enumerable/configurable` 三真),所以除了 `__proto__`
174
- * 这一个字面,其余每一个键的行为一个字节都没变。
164
+ * 🔴 0.68.0 / L-246 B2:实现**下沉到 `src/ownKey.ts`**(单源)—— 修前本文件与
165
+ * `hitl/parkResolver.ts` 各持一份同形实现,理由、陷阱与「不改成 null 原型交付」的取舍都在
166
+ * 那个模块的头注里。本别名保留是为了本文件三十余处调用点零改动。
175
167
  */
176
- function putOwn(table, key, value) {
177
- Object.defineProperty(table, key, { value, enumerable: true, writable: true, configurable: true });
178
- }
168
+ const putOwn = putOwnKey;
179
169
  /** 有限数窄化(非数 / 非有限 ⇒ 缺席;`0` 是事实不是缺席)。 */
180
170
  function finiteOrAbsent(v) {
181
171
  return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
@@ -185,21 +175,26 @@ function finiteOrAbsent(v) {
185
175
  *
186
176
  * 🔴 它为什么必须是一只函数、而不是两处各写一遍的表达式:下界位有**两个来源**——
187
177
  * · `stats.usageMissing`:只在带得出 `TaskResult` 的终帧上有;
188
- * · `ctx.usageMissingObserved`:流内观测(`failed` 事件帧 / 409 拒绝信封 / park 体**没有 stats**,
178
+ * · **本条流**的流内观测(`failed` 事件帧 / 409 拒绝信封 / park 体**没有 stats**,
189
179
  * 只读 stats 的话一条已经观测到缺口的 run 会在终帧上被读成「每一轮都报了 usage」)。
190
180
  * 修前取并只包在**终帧**那一面({@link costFactParts}),而 chrome 对账臂的铸点直接展开
191
181
  * `readRunCostFacts(stats).reconcile` ⇒ 「流内观测到缺口、终局 stats 对此缄默」那一形上两面各说
192
182
  * 各的(终帧铸了 `_sema_usage_lower_bound`、同一拍的臂上没有 `usageLowerBound`)——
193
183
  * [paired-mechanisms-must-share-premise] 的教科书形。⇒ 取并**下沉到这里**,两面共用。
194
184
  *
185
+ * 🔴 **第二参是「这一条流」的观测快照,不是一个跨流留存的状态**(0.67.2 / 车 I 件 I-1):它此前住在
186
+ * `EmitContext` 上、只写 `true` 永不清,而 ctx 是调用方的对象、可以复用给多条流 ⇒ 上一条流的缺口
187
+ * 会把下一条账数得全的流标成下界(顺序复用),或两条并发流互相串。现在由 `runStream` 按流持有、
188
+ * 终局**按值**交给两个投影口;端自建管线同样是**按次调用**传入。
189
+ *
195
190
  * 🔴 **严格 true 才认**(core 契约:`true` 或缺席,恒不写 `false`/`null`);`stats` 不是可读对象时
196
191
  * 它那一半读作「没说」,而流内观测那一半**照旧成立**。
197
192
  */
198
- function usageLowerBoundOf(stats, ctx) {
193
+ function usageLowerBoundOf(stats, observed) {
199
194
  const statsSaid = stats !== null && typeof stats === 'object' && !Array.isArray(stats)
200
195
  ? stats.usageMissing === true
201
196
  : false;
202
- return statsSaid || ctx?.usageMissingObserved === true;
197
+ return statsSaid || observed?.usageMissingObserved === true;
203
198
  }
204
199
  /**
205
200
  * D-3 / B-068 · L-198 —— 终局成本事实的**唯一读器**(终帧超集键与 chrome 对账臂共用)。
@@ -207,13 +202,15 @@ function usageLowerBoundOf(stats, ctx) {
207
202
  * 🔴 `stats` 不是可读对象(409 拒绝信封 / park 体 / `failed` 事件帧)⇒ 返 `undefined` =
208
203
  * **这条帧没有账**,调用方据此「不说话」(不发臂、不铸键),而不是发一条全缺席的空账。
209
204
  *
210
- * 🔴 **第二参 `ctx`(0.67.1 / B-091,additive)**:流内观测面。给了它,对账段上的
205
+ * 🔴 **第二参 `observed`(0.67.1 / B-091,additive)**:**这一条流**的流内观测快照。给了它,对账段上的
211
206
  * {@link RunCostReconcile.usageLowerBound} 就是**取并后**的读数(见 {@link usageLowerBoundOf});
212
207
  * 不给(旧签名)⇒ 只读 `stats` 那一半,既有端逐位不变。
213
208
  * ⚠️ 包内的两个调用点(`costFactParts` 与 `runStream` 的 `run_cost_reconciled` 铸点)**都必须**
214
209
  * 传它 —— 少传一处就是把本件修的那条不对称原样种回去。
210
+ * 🔴 0.67.2 / 车 I 件 I-1:它是**按次调用的入参**,不再是 `EmitContext` 上的一格 —— 共享 ctx 被复用
211
+ * 给多条流时,那一格会把别的流的缺口串进这一条(见 {@link usageLowerBoundOf} 的第三段 🔴)。
215
212
  */
216
- export function readRunCostFacts(stats, ctx) {
213
+ export function readRunCostFacts(stats, observed) {
217
214
  // 数组也不是「一份账」:`typeof [] === 'object'` 会把一条畸形载体放进来,然后它的每一格都读不出
218
215
  // ⇒ 发出一条「own 没定价」的臂,而真相是**这条帧根本没有账**(两句话又折成一句)。
219
216
  if (stats === null || typeof stats !== 'object' || Array.isArray(stats))
@@ -260,7 +257,7 @@ export function readRunCostFacts(stats, ctx) {
260
257
  // 0.67.0(core 7.14.0):usage 下界位。**严格 true 才铸**(core 契约:`true` 或缺席,恒不写
261
258
  // `false`/`null`;认宽了就会把一个 falsy 值渲成「数得不全」)。它与成本三段正交,所以读在这里、
262
259
  // 与三段同一只读器出 —— 两面(终帧超集键 / chrome 对账臂)因此永远不会各算各的。
263
- const usageLowerBound = usageLowerBoundOf(stats, ctx);
260
+ const usageLowerBound = usageLowerBoundOf(stats, observed);
264
261
  const reconcile = {
265
262
  ...(ownMicroUsd !== undefined ? { ownMicroUsd } : { costAbsent: true }),
266
263
  ...(usageLowerBound ? { usageLowerBound: true } : {}),
@@ -283,8 +280,8 @@ export function readRunCostFacts(stats, ctx) {
283
280
  * (超集键纪律:CC 形上已有的位不许塞我们自己的含义)。「fully-reconciled spend」由消费方按
284
281
  * 这两个超集键自己加 —— 包给的是**可对账的事实**,不是一个改了口径的数。
285
282
  */
286
- function costFactParts(stats, ctx) {
287
- const facts = readRunCostFacts(stats, ctx);
283
+ function costFactParts(stats, observed) {
284
+ const facts = readRunCostFacts(stats, observed);
288
285
  // 🔴 **下界位与成本三段分开算**(异源对抗复审 [medium] 实抓):`stats` 读不出(`failed` 事件帧 /
289
286
  // 409 拒绝信封 / park 体)时**成本**那三段确实没有账、一条都不该说;但「这条流观测到过一轮
290
287
  // 没有 usage」这件事**照旧成立**,而那种终帧的 `usage` 恰恰是 `flattenUsage(undefined)` 的
@@ -293,7 +290,7 @@ function costFactParts(stats, ctx) {
293
290
  // 🔴 0.67.1 / B-091:取并本身已经**下沉**到 {@link usageLowerBoundOf} —— 这里与读器内部、与
294
291
  // chrome 对账臂读的是**同一只函数**,三面不会各算各的(`facts === undefined` 时读器整只不
295
292
  // 返回,所以这一行必须自己再调一次那只判据,而不是回头读 `facts`)。
296
- const lowerBound = usageLowerBoundOf(stats, ctx);
293
+ const lowerBound = usageLowerBoundOf(stats, observed);
297
294
  const lowerBoundPart = lowerBound ? { _sema_usage_lower_bound: true } : {};
298
295
  if (facts === undefined)
299
296
  return lowerBoundPart;
@@ -312,6 +309,47 @@ function costFactParts(stats, ctx) {
312
309
  // (本位在上面与流内观测取并后已铸;这里不再重复。)
313
310
  };
314
311
  }
312
+ // ══ 0.68.0(L-244 包侧半场)—— 子代用量「这笔账是下界」的**自有位**,与终帧那一位**分名** ══
313
+ //
314
+ // ── 病形(三重复审 A4/B10 在壳上实抓)──────────────────────────────────────────────────────
315
+ // 壳的子代用量 store 把「这一行还没收口(`partial`)」∪「这一轮引擎没报账(`usageMissing`)」两件事
316
+ // 铸成了一个**与终帧同名**的键 `_sema_usage_lower_bound`。两者是**同名异义**:
317
+ // · 终帧那一位答的是「**这条 run 的合计**是下界」(来源 = `stats.usageMissing` ∪ 流内观测);
318
+ // · 子代面那一位答的是「**这一只子代的这一行**现在还不是最终数」(来源 = 行还没收口 / 那一轮没报账)。
319
+ // 同名的代价是消费面分不出自己读到的是哪一个:一个按键名做聚合的面(把所有 `_sema_usage_lower_bound`
320
+ // 收起来渲一句「本次会话的账是下界」)会把一条**只是还没收口的子代行**算成整条 run 的账不可信。
321
+ //
322
+ // ⇒ 包侧给出**自有名**与**唯一判据**,壳/web/desktop 三端照它渲,谁都不再自己拼一个键名:
323
+ /**
324
+ * 子代用量面的**下界判别位键名**(`_sema_subagent_usage_partial`)。
325
+ *
326
+ * 🔴 它与终帧的 `_sema_usage_lower_bound` **刻意不同名**(见上段):两者答的是两个问题,
327
+ * 一个消费面同时拿到两者时必须分得出来。
328
+ * 🔴 本包**不在任何 wire 帧上铸它** —— 它是**渲染面**的位(端把一行子代用量交给自己的视图时用)。
329
+ * 包给名与判据,是为了三端零自拼(同 `gateIdentity` 的三条身份键字面同一条纪律)。
330
+ */
331
+ export const SEMA_SUBAGENT_USAGE_PARTIAL_KEY = '_sema_subagent_usage_partial';
332
+ /**
333
+ * 一行子代用量的数字**是不是下界**({@link SEMA_SUBAGENT_USAGE_PARTIAL_KEY} 该不该立)。
334
+ *
335
+ * 三个来源**取并**,每一条都能独立让这一行的数字不是最终数:
336
+ * · `row.usageMissing` —— 这只子代**至少有一轮**引擎没报账(数字照累加,但它是下界);
337
+ * · `row.keyCollision` —— 两个命名空间的 id 撞了字面,这一行是**几只子代的账并起来的**;
338
+ * · `opts.tablePartial` —— 整张表对不上引擎的权威合计(`_sema_nested_usage_by_task_partial`)⇒
339
+ * 表里**每一行**都不可证完整。
340
+ *
341
+ * 🔴 **`false` 不是「这一行是最终数」的证据**:它只说「本端没有任何一条理由认为它是下界」。
342
+ * 行本身还没收口(端自己的 store 知道,包不知道)时,端要自己把那一条并进来 —— 所以这只谓词
343
+ * 收一个 `opts`,而不是假装它掌握全部真相。
344
+ * 🔴 非对象 / 缺席入参 ⇒ `false`(答不出,不是断言);坏形位(非 `true` 的值)不当真。
345
+ */
346
+ export function subagentUsageIsPartial(row, opts) {
347
+ if (opts?.tablePartial === true)
348
+ return true;
349
+ if (typeof row !== 'object' || row === null)
350
+ return false;
351
+ return row.usageMissing === true || row.keyCollision === true;
352
+ }
315
353
  /**
316
354
  * L-228 —— 流内子代分表 → 终帧两个超集键的**唯一 mint 点**(成功臂与错误信封共用)。
317
355
  *
@@ -332,31 +370,74 @@ function costFactParts(stats, ctx) {
332
370
  * 本判据只会**多**铸 partial(把一张其实完整的表说成不完整),**永远不会**把一张残表说成完整。
333
371
  * 🔴 **两个键不互证、也不相加**:`_sema_nested_usage`(合计,引擎报的)与本表(流内看见的)是
334
372
  * 两份独立的账;`partial` 在场时两者**本来就该不等**,消费方不许拿其中一份去「修正」另一份。
373
+ * 🔴 **0.67.2(车 I 件 I-2)身份键碰撞**:累加表的键按出身隔离(`s:` / `p:`),交付面按裸 id 归并 ——
374
+ * 跨命名空间的同字面合并成一行、立 {@link SemaSubagentUsageRow.keyCollision},并让 `partial` 恒立。
375
+ * 🔴 **0.67.2(车 I 件 I-2b)这张表是「本流快照」,不再从 `EmitContext` 上读**(与件 I-1 逐字同形的
376
+ * 同形存量,轮二异源复审实抓):流结束后它此前**留在调用方对象上**,而本文件这三只终帧投影器是
377
+ * **公面导出** —— 端「A 走 runStream、B 直调终帧投影」共用一个 ctx 时,B 的终帧会带出 **A 的**分表;
378
+ * B 的 `nested` 计数若恰好与那张表对得上(`tasks`/`turns` 相等),`partial` 还不铸 = 一张属于别人的
379
+ * 表被标成「可证完整」。⇒ 表随终帧那一拍**按值**传进来;没传 ⇒ 两个键都不铸(诚实缺席:这次投影
380
+ * 一条子流都没看见),**绝不**回头读残留。
335
381
  */
336
382
  function nestedUsageByTaskParts(stats, rollup) {
337
383
  // 🔴 一行都没有 ⇒ **什么都不说**:空表会被读成「这条 run 一个子代都没委派」,而真相可能是
338
384
  // 「委派了,但这条流没看见任何一轮」(重连车道)。两句话不许折成一句。
339
385
  if (rollup === undefined || rollup.size === 0)
340
386
  return {};
341
- const rows = {};
342
387
  let turnsSeen = 0;
343
388
  // 🔴 0.67.1:行键的**出身**统计(见 {@link MutableSubagentUsageRow.keyFromParentFallback})。
344
- // 出身按行是均匀的 —— 一行的键要么恒是 `sourceTaskId`、要么恒是回落的父调用 id
345
- // (`taskId = sourceTaskId ?? parent`:真身份在场时永远不会落到回落键那一行上)。
389
+ // 出身按行是均匀的 —— 0.67.2 起这件事由**键空间**保证:累加表的键带 `s:` / `p:` 前缀,所以
390
+ // 一行的每一轮必然来自同一个命名空间(修前靠的是「`taskId = sourceTaskId ?? parent`」那条推理,
391
+ // 而那条推理在两个空间撞字面时不成立)。
346
392
  let fallbackKeyedRows = 0;
347
393
  let sourceKeyedRows = 0;
348
- for (const [taskId, r] of rollup) {
394
+ // ── 🔴 0.67.2(车 I 件 I-2)—— 交付面按**裸 id** 归并,跨命名空间的同字面 = **碰撞** ──────────
395
+ // 交付面的键必须是裸 id(端零改),而累加表的键带出身前缀 ⇒ 两个命名空间里字面相同的两行会在这里
396
+ // 落到同一个交付键上。本层**没有任何读数**能把它们拆回去(core 的合同不保证两个命名空间互斥),
397
+ // 于是:合并数字(不偷偷丢一边)+ 立 `keyCollision`(如实说这一行是混的)+ `partial` 恒立
398
+ // (证不出逐任务归属就不许说完整)。
399
+ // 🔴 为什么不「两行分列」:交付形是 `Record<taskId, row>`,同一个裸 id 不可能占两格 —— 要分列就得
400
+ // 改交付键的形(把出身前缀推到 wire 上),那是三端 BREAKING,而且把本层的内部编码变成端要解的
401
+ // 身份语义。合并 + 显式标记是**同一批事实**下唯一不撒谎的交付形。
402
+ const merged = new Map();
403
+ for (const r of rollup.values()) {
349
404
  turnsSeen += r.turns;
350
405
  if (r.keyFromParentFallback === true)
351
406
  fallbackKeyedRows += 1;
352
407
  else
353
408
  sourceKeyedRows += 1;
409
+ const prev = merged.get(r.taskId);
410
+ if (prev === undefined) {
411
+ merged.set(r.taskId, {
412
+ turns: r.turns,
413
+ inputTokens: r.inputTokens,
414
+ outputTokens: r.outputTokens,
415
+ ...(r.cacheReadTokens !== undefined ? { cacheReadTokens: r.cacheReadTokens } : {}),
416
+ ...(r.usageMissing === true ? { usageMissing: true } : {}),
417
+ });
418
+ continue;
419
+ }
420
+ prev.turns += r.turns;
421
+ prev.inputTokens += r.inputTokens;
422
+ prev.outputTokens += r.outputTokens;
423
+ // `cacheReadTokens` 的缺席语义在合并处也守住:两边都没报过 ⇒ 键仍不铸(0 会被读成「零命中」)。
424
+ if (r.cacheReadTokens !== undefined)
425
+ prev.cacheReadTokens = (prev.cacheReadTokens ?? 0) + r.cacheReadTokens;
426
+ if (r.usageMissing === true)
427
+ prev.usageMissing = true;
428
+ prev.keyCollision = true;
429
+ }
430
+ const keyCollisions = [...merged.values()].filter((v) => v.keyCollision === true).length;
431
+ const rows = {};
432
+ for (const [taskId, v] of merged) {
433
+ // 🔴 落键仍走 `putOwn`(`__proto__` 陷阱):合并臂不是绕过那条纪律的第二条路(见 `putOwn` 头注)。
354
434
  putOwn(rows, taskId, {
355
- turns: r.turns,
356
- inputTokens: r.inputTokens,
357
- outputTokens: r.outputTokens,
358
- ...(r.cacheReadTokens !== undefined ? { cacheReadTokens: r.cacheReadTokens } : {}),
359
- ...(r.usageMissing === true ? { usageMissing: true } : {}),
435
+ turns: v.turns,
436
+ inputTokens: v.inputTokens,
437
+ outputTokens: v.outputTokens,
438
+ ...(v.cacheReadTokens !== undefined ? { cacheReadTokens: v.cacheReadTokens } : {}),
439
+ ...(v.usageMissing === true ? { usageMissing: true } : {}),
440
+ ...(v.keyCollision === true ? { keyCollision: true } : {}),
360
441
  });
361
442
  }
362
443
  const nested = stats !== null && typeof stats === 'object' && !Array.isArray(stats)
@@ -369,7 +450,11 @@ function nestedUsageByTaskParts(stats, rollup) {
369
450
  // 出身一致时既有两条对账才是充分的:全真身份 ⇒ 一行一任务;全回落 ⇒ 多任务共父会让行数 <
370
451
  // `nested.tasks`,那一格自己会翻。失效方向仍是安全的那一侧(只会**多**铸 partial)。
371
452
  const mixedKeyOrigin = fallbackKeyedRows > 0 && sourceKeyedRows > 0;
372
- const complete = !mixedKeyOrigin && authTurns !== undefined && authTasks !== undefined &&
453
+ // 🔴 0.67.2:碰撞也单独挡一格。按构造 `keyCollisions > 0 ⇒ mixedKeyOrigin`(同一个命名空间里的键
454
+ // 本来就唯一,撞字面只能跨空间发生),所以这一格今天是**冗余的第二道**;留着是因为两条判据问的
455
+ // 不是同一件事(「这条流上有两种出身」vs「这一行里混了两种出身」),而上游哪天再加一个身份来源
456
+ // 时,前者的判法要改、后者不用。
457
+ const complete = !mixedKeyOrigin && keyCollisions === 0 && authTurns !== undefined && authTasks !== undefined &&
373
458
  authTurns === turnsSeen && authTasks === Object.keys(rows).length;
374
459
  return {
375
460
  _sema_nested_usage_by_task: rows,
@@ -628,10 +713,10 @@ function errorResult(ctx, parts) {
628
713
  // D-1 / L-192①:两句话不再折成一句 —— 清单 + 「有没有这本账」的判别位,见 permissionDenialParts。
629
714
  ...permissionDenialParts(parts.stats),
630
715
  // D-3 / B-068:失败/到限/park 的 run 一样花过钱,账不因结局不好就不报。
631
- ...costFactParts(parts.stats, ctx),
716
+ ...costFactParts(parts.stats, parts.observed),
632
717
  // L-228(0.67.0):流内 per-subagent 分表的收口快照(判据本体在 `nestedUsageByTaskParts`)。
633
718
  // 🔴 与成本三段同理 —— 失败的 run 一样委派过,账不因结局不好就不报。
634
- ...nestedUsageByTaskParts(parts.stats, ctx.nestedUsageByTask),
719
+ ...nestedUsageByTaskParts(parts.stats, parts.observed?.nestedUsageByTask),
635
720
  errors: [...parts.errors],
636
721
  ...(parts.errorCode !== undefined && parts.errorCode.length > 0 ? { errorCode: parts.errorCode } : {}),
637
722
  ...(parts.degraded !== undefined ? { degraded: parts.degraded } : {}),
@@ -641,7 +726,13 @@ function errorResult(ctx, parts) {
641
726
  });
642
727
  }
643
728
  /** `done` → SDKResultSuccess (contract 02 §2.10 / 08 CS-10). */
644
- export function doneToSdkResult(ev, ctx) {
729
+ export function doneToSdkResult(ev, ctx,
730
+ /**
731
+ * 0.67.2:**这一条流**的收口快照 —— usage 缺口观测(见 {@link usageLowerBoundOf})与 per-subagent
732
+ * 累加表(见 {@link nestedUsageByTaskParts})。两格都是 per-stream 的事实,缺席 ⇒ 这次投影没有流内面
733
+ * (只读 `stats` 那一半、两个分表键都不铸),**绝不**回头去读调用方对象上可能残留的上一条流。
734
+ */
735
+ observed) {
645
736
  // sdk 4.1.0([2395]E 调和):`done.result` 是判别联合 `TaskResult | ActiveRunConflictDoneResult`。
646
737
  // 🔄 全窗复审 ADAPTER-1/-2 收口(2026-08-03):首版接线用 `'stats' in r` 铸 tr 并让 result/model/
647
738
  // degraded 全走 tr?.——把「stats 在不在」错当成了这些键的门控(anchor-on-the-deciding-quantity
@@ -693,7 +784,7 @@ export function doneToSdkResult(ev, ctx) {
693
784
  // failed + limits.max_{tokens,walltime}_exceeded 两族共用)。
694
785
  const degraded = degradedOf(r);
695
786
  /** 四个 error 臂共享的固定位(信封其余 13 位见 `errorResult`)。 */
696
- const errorBase = { durationMs, stats, model: r.model, errorCode, degraded };
787
+ const errorBase = { durationMs, stats, model: r.model, errorCode, degraded, observed };
697
788
  if (terminal?.kind === 'failed') {
698
789
  // [909]B1 — failed 臂 subtype 语义化(见文件头对表);core 5.8.0([2489])起到限码全部改名,
699
790
  // 映射本身已收进单点 `subtypeForErrorCode`(退役批后只认新码)。
@@ -824,10 +915,10 @@ export function doneToSdkResult(ev, ctx) {
824
915
  // D-1 / L-192①:同形第二处 —— 与错误信封共用**同一个** mint 点(修前两处各一个字面量 [])。
825
916
  ...permissionDenialParts(stats),
826
917
  // D-3 / B-068:成本明细与子代那本账(micro-USD 原值);`total_cost_usd` 语义一字不动。
827
- ...costFactParts(stats, ctx),
918
+ ...costFactParts(stats, observed),
828
919
  // L-228(0.67.0):流内 per-subagent 分表的收口快照;与 `_sema_nested_usage`(引擎报的合计)
829
920
  // 是**两份独立的账**,不相加、不互证(见 `nestedUsageByTaskParts` 顶注)。
830
- ...nestedUsageByTaskParts(stats, ctx.nestedUsageByTask),
921
+ ...nestedUsageByTaskParts(stats, observed?.nestedUsageByTask),
831
922
  // MF-25 — the effective served model id (`done.result.model`, e.g. "deepseek-v4-pro"). The CC
832
923
  // SDKResultSuccess schema has no `model` field, so this rides as an additive seam field a cost/overview
833
924
  // consumer reads (it is ALSO surfaced as the `modelUsage` key). Omitted when the wire didn't carry it.
@@ -835,7 +926,9 @@ export function doneToSdkResult(ev, ctx) {
835
926
  });
836
927
  }
837
928
  /** `failed` → SDKResultError (contract 02 §2.11 / 08 CS-11). */
838
- export function failedToSdkResult(ev, ctx) {
929
+ export function failedToSdkResult(ev, ctx,
930
+ /** 0.67.2:同 {@link doneToSdkResult} 的第三参。 */
931
+ observed) {
839
932
  // Flatten the 4 CC error subtypes onto the single neutral errorCode.
840
933
  // error_max_budget_usd is a non-error "budget exceeded" notice, not a crash
841
934
  // (contract 02 §2.11) — the renderer branches on subtype.
@@ -851,6 +944,7 @@ export function failedToSdkResult(ev, ctx) {
851
944
  // `failed` 事件帧本体不带 stats/model/degraded,所以这三个位如实缺席 —— 不是「这里少算了」。
852
945
  return errorResult(ctx, {
853
946
  subtype,
947
+ observed,
854
948
  durationMs: elapsedMs(ctx),
855
949
  // [909]B1 — errorCode 透传(additive seam 字段;done 臂同款):`failed` 事件的 cancelled/
856
950
  // limits.* 等引擎值原样给集成面。
@@ -866,7 +960,13 @@ export function failedToSdkResult(ev, ctx) {
866
960
  ],
867
961
  });
868
962
  }
869
- /** Dispatch a terminal AgentEvent to its SDKResult arm. */
870
- export function terminalToSdkResult(ev, ctx) {
871
- return ev.type === 'done' ? doneToSdkResult(ev, ctx) : failedToSdkResult(ev, ctx);
963
+ /**
964
+ * Dispatch a terminal AgentEvent to its SDKResult arm.
965
+ *
966
+ * 🔴 第三参(0.67.2 / 车 I 件 I-1):**这一条流**的 usage 观测快照。`runStream` 在终帧那一拍**按值**
967
+ * 交给它与 `run_cost_reconciled` 铸臂 —— 两个投影口读的是**同一份本流快照**,而不是一个可能被别的流
968
+ * 写过的共享位。缺席(端直调)⇒ 与 0.67.1 的旧签名逐位同行为。
969
+ */
970
+ export function terminalToSdkResult(ev, ctx, observed) {
971
+ return ev.type === 'done' ? doneToSdkResult(ev, ctx, observed) : failedToSdkResult(ev, ctx, observed);
872
972
  }
@@ -32,7 +32,7 @@
32
32
  */
33
33
  import type { AgentEvent } from '@sema-agent/sdk';
34
34
  import { type SDKMessage, type EmitContext, type ModelUsage } from './types.js';
35
- import type { EngineTurnUsage } from './downstream/turnUsageToModelUsage.js';
35
+ import { type EngineTurnUsage } from './downstream/turnUsageToModelUsage.js';
36
36
  /**
37
37
  * 409 body 的 pendingGate 材料(A-028.1,2026-08-12 具名化并补 `governanceForced` 位 ——
38
38
  * 此前包侧只有 {kind, decidePath} 两位,壳侧抄件已多出该位 = 同一 wire 位两份解析器形不同,
@@ -178,14 +178,34 @@ export declare function _resetDroppedFrameReportForTest(): void;
178
178
  * 证不了「表有没有涨」;两者在到顶之后恰好分道扬镳,所以必须直接读表)。 */
179
179
  export declare function _droppedFrameMemoSizeForTest(): number;
180
180
  export interface RunStreamHandle {
181
- /** The latest folded turn usage (footer counters); updated on each turn_end. */
181
+ /**
182
+ * The latest folded turn usage (footer counters).
183
+ *
184
+ * 🔴 **0.68.0 收窄成「最近一次**已测量**的 turn」**(core 7.17.0 #711 的跟车修;异源对抗复审实抓):
185
+ * #711 之后,一轮没量出账时引擎发的是**六个 0 + `usageMissing:true`**(不再是「不发 usage」)。
186
+ * 修前这里是无条件覆盖 ⇒ 那种轮会把一份**占位全零**盖进来,而本形上**没有任何判别位** ——
187
+ * 只吃这个出口的 footer 于是把「不知道」渲成一笔精确的零账(消息臂与 chrome 臂上的判别位
188
+ * 保护不到这个出口)。⇒ 未测量的那一轮**不覆盖**(保留上一次真读数,与 #711 之前的行为逐字
189
+ * 相同),并由 {@link latestUsageMissing} 说出「最新那一轮没测出账」。
190
+ */
182
191
  latestUsage?: ModelUsage;
183
192
  /**
184
193
  * [2295] 裁 ② 逐字通道:与 latestUsage 同拍更新的引擎 `turn_end.usage` **原形**(六键含
185
194
  * `totalInputTokens`)。镜像键求和≠总量(仅 cache 族一致时相等),总量消费面吃这份。
186
195
  * 旧引擎(core <3.0.0)wire 缺形时为 undefined —— 诚实缺席,不造零值。
196
+ * 🔴 0.68.0:与 {@link latestUsage} **同拍同律** —— 未测量的轮不覆盖。
187
197
  */
188
198
  latestEngineUsage?: EngineTurnUsage | undefined;
199
+ /**
200
+ * 🔴 0.68.0 —— **最新那一轮的账知不知道**。在场(恒 `true`)= 最近走过的那个 `turn_end`
201
+ * **没测出账**(`usageMissing:true`),或者它是一条**契约违约**帧(连 usage 都没有);
202
+ * 此时上面两份读数属于**更早的**那一轮,别当成最新那一轮的账。
203
+ *
204
+ * 🔴 **never false**:测量到账的那一轮**把这一位删掉**(键不在场 ⇔ 上面两份就是最新那一轮的账)。
205
+ * 它是**逐轮**的判别位,不是「这条流上曾经有过缺口」的单调位 —— 后者在终帧上
206
+ * (`_sema_usage_lower_bound`),两者答的是两个问题。
207
+ */
208
+ latestUsageMissing?: true;
189
209
  }
190
210
  export declare function isRunStreamActive(): boolean;
191
211
  export declare function runStream(events: AsyncIterable<AgentEvent>, ctx: EmitContext, handle?: RunStreamHandle): AsyncGenerator<SDKMessage>;