@lqc123qwe/car-runtime 1.10.0 → 1.12.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/README.md CHANGED
@@ -266,9 +266,19 @@ node --experimental-transform-types --test test/*.spec.ts
266
266
 
267
267
  - **1.10 工程收口(2026-09-29)**:全量 **341(338 PASS + 3 能力门控 skip / 0 fail)** = 1.9 基线 339 + s44 2 测;tsc strict 0;dist 构建装载冒烟过。**1.10 = 平台层 S3 前置(car-platform KernelSeam 实挂依赖本导出面);内核运行时行为零改动(存量 339 测试零改动通过 = 兼容验收线)**。
268
268
 
269
+ ## 1.11 状态(seam authz 导出 + usage 事件计量采集)
270
+
271
+ | `src/seam.ts` + `src/authz/authz.ts` | 1.11 前段(`2105f42`):**authz seam 导出**——19→21 符号(+AuthzService/+checkCapability/+4 类型);`decideBySessionPolicy` 会话策略直判(decidedBy=policy 幂等落会话哈希链);驱动 = 平台确认桥 P1 缺口(AuthzService 零生产调用点)收口联动;1.12.0 预留 MCP/PTC(D-20) | 平台 S3 授权面(per-session AuthzService + approve scope=session 三档粒度) |
272
+ | `src/runtime-core/types.ts` + `llm.ts` | 1.11-S1(D-33,计量设计 §3.1/§3.2):`LlmUsage` 四分量归一 + `LlmChunk.usage`;openai-compat `stream_options.include_usage` 默认开启(回退白名单登记可回退)+ 解析「先摘 evt.usage 再判 choice」(空 choices 尾包吞没实证修复——§3.2 必核行属实)+ `prompt−cached` 减法;anthropic `message_start`(input)/`message_delta`(output) 两包合并 + cache_creation 透传;缺失/畸形整条不落(禁估算补造,R-2 中断形态含) | 计量事实源头(§1.1 口径:openai 减法 / anthropic 透传) |
273
+ | `src/session/log.ts` + `runtime-core/chatStep.ts` + `loop/stop.ts` | 1.11-S2:EventKind +`'usage'`(actor='runtime'、model_visible=0、deriveMessages 不投影);chatStep 请求级落链(payload 七键,缺失不落);stop.ts `closeTurn` 单点聚合 turnEnd meta.usage(四分量 + requests;无 usage 不带键向后兼容;跨 turn turnId 过滤不串;max-tokens 照常落) | 平台计量批次(0005 用量表镜像)上游;计量设计 §3.3 |
274
+ | `test/s45`(8)+ `test/s46`(6) | s45:双适配器 fixture SSE 全集(尾包解析/减法归一/请求体断言/缺失畸形容错/共存形态/中断不落);s46:落链/聚合/无键兼容/泄漏三模式断言(消息内容/凭据/URL 查询串,含正控) | 1.11-GO 第 1/5 项承载 |
275
+
276
+ - **1.11 工程收口(2026-10-07,`5488949`)**:全量 **355(352 PASS + 3 能力门控 skip / 0 fail)** = 基线 341 + s45 8 + s46 6;tsc strict 0 + secrets-scan 0(commit 后复跑)+ contract-check 4/4(golden 不动);存量 spec 零改动通过 = 兼容验收线。规划:`car-docs/10-内核v1/1.11-迭代规划.md`(D-33/D-34)。
277
+ - **1.11.0 正式发布(2026-10-07,S4 收官)**:npm `@lqc123qwe/car-runtime@1.11.0` 在架——**dist-tags latest → 1.11.0、rc 保留 1.0.0-rc.1** + provenance。Release `1.11.0`(id 405567956,agent GCM 凭据 API 创建;**发布工程事件:github.com:443 区域性阻断——main 推送经 Git Data API 逐字节复刻(blob/tree/commit 全 SHA 对拍,2105f42+5488949 与本地零分叉)**)→ **j16 run 37597090647 全步 success** → **post-publish REMOTE_CHECK=1 复跑 13 PASS / 0 DRY-RUN / 0 FAIL**(G-09 provenance 实查 + G-10 五件套实查);外部等价检查 **SHA256 MATCH(`e33ef93a…`)+ Fulcio 证书验签 Verified OK + Rekor 双端一致**(logIndex 3127715428;**登记观察:Rekor 新分片树 UUID 非 sha256(body) 旧口径、SET 公钥验签未过——body/logIndex/integratedTime 对拍已覆盖 GO 判据,沿用 1.5「以 bundle 记录为准」口径**);registry tarball ↔ tag 树 src 面逐字节一致 + dist 产物 1.11 标记核验 ✓(Release tgz = `git archive` 全树源码归档,与 npm files 白名单包为设计内两种制品)。**1.11-GO 第 7 项(真实 provider 端到端 usage 终验,方案 B)待用户终端执行**。
278
+
269
279
  ## 状态
270
280
 
271
- - **版本主线**:S1-S4 48/48 | M2 89/89(v0.2.0)| M3 127/127(v0.3.0)| M4 139/139(**1.0.0-rc.1 已发布**)| M5 156/156(DEC-4 预埋项转正)| M6 179/179(2026-09-15 收口)| M7 全量 195(192 PASS + 3 能力门控 skip,2026-09-19 收口)| **M5-S28 装载签名门 全量 204(201 PASS + 3 skip,2026-09-26)** | **M8 运行时底座 全量 231(228 PASS + 3 skip,2026-09-26 收口,§3.2 八模块全部收口)** | **1.1 签名收尾最小集 全量 249(246 PASS + 3 skip,2026-09-27 收口,1.1.0 已发布,enforce 翻转准备就绪)** | **1.2 会话/遥测小项 全量 263(260 PASS + 3 skip,2026-09-27 收口,1.2.0 已发布)** | **1.3 发布资产自动化 全量 270(267 PASS + 3 skip,2026-09-27 收口,1.3.0 已发布,全链零人工拖拽兑现)** | **1.4 真实模型接入 全量 282(279 PASS + 3 skip,2026-09-27 收口,1.4.0 已发布)** | **1.5 provider 扩展+远程 MCP+配置完整形态 全量 300(297 PASS + 3 skip,2026-09-28 收口,1.5.0 已发布)** | **1.6 宿主工具面+投影收敛+jiti 载体 全量 318(315 PASS + 3 skip,2026-09-28 收口,1.6.0 已发布)** | **1.7 OTLP span 语义收口+metrics 时序修正+mcp-serve span 面 全量 328(325 PASS + 3 skip,2026-09-28 收口,1.7.0 已发布)** | **1.8 W3C traceparent 跨进程传播+LLM 出站通道修复 全量 335(332 PASS + 3 skip,2026-09-29 收口,1.8.0 已发布)** | **1.9 远程 MCP traceparent 传播(HTTP 头通道) 全量 339(336 PASS + 3 skip,2026-09-29 收口,1.9.0 已发布)** | **1.10 KernelSeam 公开导出面+库构建产物 全量 341(338 PASS + 3 skip,2026-09-29 收口,1.10.0 已发布)**
281
+ - **版本主线**:S1-S4 48/48 | M2 89/89(v0.2.0)| M3 127/127(v0.3.0)| M4 139/139(**1.0.0-rc.1 已发布**)| M5 156/156(DEC-4 预埋项转正)| M6 179/179(2026-09-15 收口)| M7 全量 195(192 PASS + 3 能力门控 skip,2026-09-19 收口)| **M5-S28 装载签名门 全量 204(201 PASS + 3 skip,2026-09-26)** | **M8 运行时底座 全量 231(228 PASS + 3 skip,2026-09-26 收口,§3.2 八模块全部收口)** | **1.1 签名收尾最小集 全量 249(246 PASS + 3 skip,2026-09-27 收口,1.1.0 已发布,enforce 翻转准备就绪)** | **1.2 会话/遥测小项 全量 263(260 PASS + 3 skip,2026-09-27 收口,1.2.0 已发布)** | **1.3 发布资产自动化 全量 270(267 PASS + 3 skip,2026-09-27 收口,1.3.0 已发布,全链零人工拖拽兑现)** | **1.4 真实模型接入 全量 282(279 PASS + 3 skip,2026-09-27 收口,1.4.0 已发布)** | **1.5 provider 扩展+远程 MCP+配置完整形态 全量 300(297 PASS + 3 skip,2026-09-28 收口,1.5.0 已发布)** | **1.6 宿主工具面+投影收敛+jiti 载体 全量 318(315 PASS + 3 skip,2026-09-28 收口,1.6.0 已发布)** | **1.7 OTLP span 语义收口+metrics 时序修正+mcp-serve span 面 全量 328(325 PASS + 3 skip,2026-09-28 收口,1.7.0 已发布)** | **1.8 W3C traceparent 跨进程传播+LLM 出站通道修复 全量 335(332 PASS + 3 skip,2026-09-29 收口,1.8.0 已发布)** | **1.9 远程 MCP traceparent 传播(HTTP 头通道) 全量 339(336 PASS + 3 skip,2026-09-29 收口,1.9.0 已发布)** | **1.10 KernelSeam 公开导出面+库构建产物 全量 341(338 PASS + 3 skip,2026-09-29 收口,1.10.0 已发布)** | **1.11 seam authz 导出+usage 事件计量采集 全量 355(352 PASS + 3 skip,2026-10-07 收口,1.11.0 已发布)**
272
282
  - **1.0.0 正式发布(2026-09-26,S29/S30)**:npm `@lqc123qwe/car-runtime@1.0.0` 在架——**dist-tag latest → 1.0.0,rc 保留指向 1.0.0-rc.1**(§1.1 口径);SLSA provenance(Actions OIDC,透明日志在案);GitHub Release v1.0.0 六资产(tgz/bundle/SHA256SUMS/SBOM/审计/公钥);**post-publish 全链 13 PASS / 0 DRY-RUN / 0 FAIL** + 外部验证者等价检查(Release 实下载件 SHA256 MATCH + cosign Verified OK)。**宽限期时钟:2026-09-26 起算,2026-12-25 到期 verifier 默认值 warn→enforce 翻转(计划内变更)**。终验报告:`car-docs/10-内核v1/M5-S30-1.0收口终验报告.md`。发布工程事件:j16 首跑 E403(npm 2FA 策略 vs granular token 无 bypass——清单预设回退路径兑现)→ bypass token re-run 绿;G-09 期望 tag 动态化(`4c84b17`)在真实发布兑现价值。
273
283
  - **1.0.0-rc.1 已发布(2026-09-09)**:npm `@lqc123qwe/car-runtime@1.0.0-rc.1`(rc + latest 双 tag,SLSA provenance 在案)+ GitHub Release 6 制品;发布预演 **13 PASS / 0 DRY-RUN / 0 FAIL**(c762b72)。S22 GO 清单已全绿收口。
274
284
  - **轨道 A 三项全部完成**:E-1 WSL2 逃逸矩阵 **20/20 PASS 零逃逸**(2026-09-07,G-07 转 PASS)| E-3 远端发布通道 **13/13 全门禁 PASS**(2026-09-09,G-08/09/10 转 PASS)| E-2 Windows koffi FFI **四判据双环境全 PASS**(S24/S25/S26,2026-09-09~10;本机非提权 + windows-latest 提权 runner 双绿,CI 门禁已接入)。
package/dist/SHA256SUMS CHANGED
@@ -1 +1 @@
1
- 9e1d9d1865f469064fce724d646b8a3c9b87c897868c59a348800c0c64cb9ef9 car-runtime-1.10.0.tgz
1
+ a3d97f3fa7eb455bba0fed79ed684e0a314d9b33ed75ec4ca7a3003705881f12 car-runtime-1.12.0.tgz
@@ -39,6 +39,8 @@ export declare class AuthzService {
39
39
  });
40
40
  /** 声明式放行(policy):readonly 模式下写类一律 DENIED,无需人审 */
41
41
  decideByPolicy(req: AuthzRequest, mode: 'readonly' | 'confirm' | 'full'): AuthzDecision;
42
+ /** 会话策略直判(「会话内始终允许」粒度):decidedBy=policy 幂等落链,不入人审通道(审批回调 scope=session 写入 grants 后走此路) */
43
+ decideBySessionPolicy(req: AuthzRequest): AuthzDecision;
42
44
  /** 人审(confirm 模式):120s 超时默认拒绝(timeout-default-deny) */
43
45
  decideByUser(req: AuthzRequest, approve: (req: AuthzRequest) => Promise<boolean>): Promise<AuthzDecision>;
44
46
  }
@@ -26,6 +26,17 @@ export class AuthzService {
26
26
  void writeish;
27
27
  return this.#commit(decision);
28
28
  }
29
+ /** 会话策略直判(「会话内始终允许」粒度):decidedBy=policy 幂等落链,不入人审通道(审批回调 scope=session 写入 grants 后走此路) */
30
+ decideBySessionPolicy(req) {
31
+ return this.#commit({
32
+ authorizationId: req.authorizationId,
33
+ decision: 'APPROVED',
34
+ decidedBy: 'policy',
35
+ reason: `session policy grant (always-allow): ${req.resource}`,
36
+ degraded: this.#degraded,
37
+ ts: Date.now(),
38
+ });
39
+ }
29
40
  /** 人审(confirm 模式):120s 超时默认拒绝(timeout-default-deny) */
30
41
  async decideByUser(req, approve) {
31
42
  const cached = this.#decisions.get(req.authorizationId);
Binary file
@@ -0,0 +1 @@
1
+ {"base64Signature":"MEUCIA0UBnqeM+TPTKLaxwsFDiHv1WnpL4aJ5GHqxXzLSbAyAiEAneTvQOA349AWwfG/eCWATHXznws2AYYf/EXE/cVQrGU=","cert":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUc1akNDQm0yZ0F3SUJBZ0lVV204L2E1cGFQWmpYcXRXVmRoUnlDeWMrZk1Jd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpZeE1EQTNNRGswTURVMFdoY05Nall4TURBM01EazFNRFUwV2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVXMFVLYVFXcUF5NjMvZlB4alo4ak83Yndtekk5WmlUNkEwN1YKTXZxTzlxdnJZQjloR2tZaGNWRDcza25jdUpacmQrVkhNL3BJNmEwWDZjR290QzNjNGFPQ0JZd3dnZ1dJTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWZCZ05WSFNNRUdEQVdnQlRmCjArblBWaVFSbHZtbzJPa29WYUxHTGhoa1B6QmlCZ05WSFJFQkFmOEVXREJXaGxSb2RIUndjem92TDJkcGRHaDEKWWk1amIyMHZiSEZqTmpJMEwyTmhjaTF5ZFc1MGFXMWxMeTVuYVhSb2RXSXZkMjl5YTJac2IzZHpMM0psYkdWaApjMlV1ZVcxc1FISmxabk12ZEdGbmN5OHhMakV5TGpBd09RWUtLd1lCQkFHRHZ6QUJBUVFyYUhSMGNITTZMeTkwCmIydGxiaTVoWTNScGIyNXpMbWRwZEdoMVluVnpaWEpqYjI1MFpXNTBMbU52YlRBVkJnb3JCZ0VFQVlPL01BRUMKQkFkeVpXeGxZWE5sTURZR0Npc0dBUVFCZzc4d0FRTUVLRFJtWldVMlpHRXpNbVZtT1dReU9EbGpaV0k0TUdObQpZekZsTnpNNE0yUXdPVFZpWVRoa05qa3dGUVlLS3dZQkJBR0R2ekFCQkFRSGNtVnNaV0Z6WlRBZ0Jnb3JCZ0VFCkFZTy9NQUVGQkJKc2NXTTJNalF2WTJGeUxYSjFiblJwYldVd0hnWUtLd1lCQkFHRHZ6QUJCZ1FRY21WbWN5OTAKWVdkekx6RXVNVEl1TURBN0Jnb3JCZ0VFQVlPL01BRUlCQzBNSzJoMGRIQnpPaTh2ZEc5clpXNHVZV04wYVc5dQpjeTVuYVhSb2RXSjFjMlZ5WTI5dWRHVnVkQzVqYjIwd1pBWUtLd1lCQkFHRHZ6QUJDUVJXREZSb2RIUndjem92CkwyZHBkR2gxWWk1amIyMHZiSEZqTmpJMEwyTmhjaTF5ZFc1MGFXMWxMeTVuYVhSb2RXSXZkMjl5YTJac2IzZHoKTDNKbGJHVmhjMlV1ZVcxc1FISmxabk12ZEdGbmN5OHhMakV5TGpBd09BWUtLd1lCQkFHRHZ6QUJDZ1FxRENnMApabVZsTm1SaE16SmxaamxrTWpnNVkyVmlPREJqWm1NeFpUY3pPRE5rTURrMVltRTRaRFk1TUIwR0Npc0dBUVFCCmc3OHdBUXNFRHd3TloybDBhSFZpTFdodmMzUmxaREExQmdvckJnRUVBWU8vTUFFTUJDY01KV2gwZEhCek9pOHYKWjJsMGFIVmlMbU52YlM5c2NXTTJNalF2WTJGeUxYSjFiblJwYldVd09BWUtLd1lCQkFHRHZ6QUJEUVFxRENnMApabVZsTm1SaE16SmxaamxrTWpnNVkyVmlPREJqWm1NeFpUY3pPRE5rTURrMVltRTRaRFk1TUNBR0Npc0dBUVFCCmc3OHdBUTRFRWd3UWNtVm1jeTkwWVdkekx6RXVNVEl1TURBYUJnb3JCZ0VFQVlPL01BRVBCQXdNQ2pFek5qRXgKT1RJM01UUXdLUVlLS3dZQkJBR0R2ekFCRUFRYkRCbG9kSFJ3Y3pvdkwyZHBkR2gxWWk1amIyMHZiSEZqTmpJMApNQmdHQ2lzR0FRUUJnNzh3QVJFRUNnd0lOamt5TXpRMU5qUXdaQVlLS3dZQkJBR0R2ekFCRWdSV0RGUm9kSFJ3CmN6b3ZMMmRwZEdoMVlpNWpiMjB2YkhGak5qSTBMMk5oY2kxeWRXNTBhVzFsTHk1bmFYUm9kV0l2ZDI5eWEyWnMKYjNkekwzSmxiR1ZoYzJVdWVXMXNRSEpsWm5NdmRHRm5jeTh4TGpFeUxqQXdPQVlLS3dZQkJBR0R2ekFCRXdRcQpEQ2cwWm1WbE5tUmhNekpsWmpsa01qZzVZMlZpT0RCalptTXhaVGN6T0ROa01EazFZbUU0WkRZNU1CY0dDaXNHCkFRUUJnNzh3QVJRRUNRd0hjbVZzWldGelpUQlpCZ29yQmdFRUFZTy9NQUVWQkVzTVNXaDBkSEJ6T2k4dloybDAKYUhWaUxtTnZiUzlzY1dNMk1qUXZZMkZ5TFhKMWJuUnBiV1V2WVdOMGFXOXVjeTl5ZFc1ekx6TTNOakF5TWpZeQpOakl4TDJGMGRHVnRjSFJ6THpFd0ZnWUtLd1lCQkFHRHZ6QUJGZ1FJREFad2RXSnNhV013VUFZS0t3WUJCQUdECnZ6QUJHQVJDREVCeVpYQnZPbXh4WXpZeU5FQTJPVEl6TkRVMk5DOWpZWEl0Y25WdWRHbHRaVUF4TXpZeE1Ua3kKTnpFME9uSmxaanB5WldaekwzUmhaM012TVM0eE1pNHdNSUdLQmdvckJnRUVBZFo1QWdRQ0JId0VlZ0I0QUhZQQozVDB3YXNiSEVUSmpHUjRjbVdjM0FxSktYcmplUEszL2g0cHlnQzhwN280QUFBR2hGYnpBd1FBQUJBTUFSekJGCkFpQTNSUk0xdk1iZDBmVUU0VUlvMjFYaUo5a0lMYThlUnE1VlBpaHpHcGNybmdJaEFKZlNzQ01GVGpFYk11L0kKdmh1L3VnR1l4d3J2S0dZcGpXVytKUnBtUDF3WU1Bb0dDQ3FHU000OUJBTURBMmNBTUdRQ01Id3FjdWp4Nm96KwpibTlwZ1E1SWZoTXZmVW1qWUdlaG5vL3EvSU9wSnhsU0hESStxSTljc0lENFltUDhGLzdVa1FJd1ZnMCs3UmFJClRncnR1V2laaFhTQWprdmgvdUU1clhZMUdiZ0FQOGd3RUxhdmFCMTdhbXBORXBhU3Q3Nkw0Tzk0Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K","rekorBundle":{"SignedEntryTimestamp":"MEUCIQDK0u1UYH5gd1wwowDNOGkRWm7g3kMOt76q+73QtUfU1gIgI8/8XsM4m3rnPXnGT9A9PmHLMpheIYj76cODbAeO9Ow=","Payload":{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJhM2Q5N2YzZmE3ZWI0NTViYmEwZmVkNzllZDY4NGUwYTMxNGQ5YjMzZWQ3NWVjNGNhN2EzMDAzNzA1ODgxZjEyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJQTBVQm5xZU0rVFBUS0xheHdzRkRpSHYxV25wTDRhSjVHSHF4WHpMU2JBeUFpRUFuZVR2UU9BMzQ5QVd3ZkcvZUNXQVRIWHpud3MyQVlZZi9FWEUvY1ZRckdVPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVYzFha05EUW0weVowRjNTVUpCWjBsVlYyMDRMMkUxY0dGUVdtcFljWFJYVm1Sb1VubERlV01yWmsxSmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFplRTFFUVROTlJHc3dUVVJWTUZkb1kwNU5hbGw0VFVSQk0wMUVhekZOUkZVd1YycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZYTUZWTFlWRlhjVUY1TmpNdlpsQjRhbG80YWs4M1luZHRla2s1V21sVU5rRXdOMVlLVFhaeFR6bHhkbkpaUWpsb1IydFphR05XUkRjemEyNWpkVXBhY21RclZraE5MM0JKTm1Fd1dEWmpSMjkwUXpOak5HRlBRMEpaZDNkbloxZEpUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXWkNaMDVXU0ZOTlJVZEVRVmRuUWxSbUNqQXJibEJXYVZGU2JIWnRiekpQYTI5V1lVeEhUR2hvYTFCNlFtbENaMDVXU0ZKRlFrRm1PRVZYUkVKWGFHeFNiMlJJVW5kamVtOTJUREprY0dSSGFERUtXV2sxYW1JeU1IWmlTRVpxVG1wSk1Fd3lUbWhqYVRGNVpGYzFNR0ZYTVd4TWVUVnVZVmhTYjJSWFNYWmtNamw1WVRKYWMySXpaSHBNTTBwc1lrZFdhQXBqTWxWMVpWY3hjMUZJU214YWJrMTJaRWRHYm1ONU9IaE1ha1Y1VEdwQmQwOVJXVXRMZDFsQ1FrRkhSSFo2UVVKQlVWRnlZVWhTTUdOSVRUWk1lVGt3Q21JeWRHeGlhVFZvV1ROU2NHSXlOWHBNYldSd1pFZG9NVmx1Vm5wYVdFcHFZakkxTUZwWE5UQk1iVTUyWWxSQlZrSm5iM0pDWjBWRlFWbFBMMDFCUlVNS1FrRmtlVnBYZUd4WldFNXNUVVJaUjBOcGMwZEJVVkZDWnpjNGQwRlJUVVZMUkZKdFdsZFZNbHBIUlhwTmJWWnRUMWRSZVU5RWJHcGFWMGswVFVkT2JRcFpla1pzVG5wTk5FMHlVWGRQVkZacFdWUm9hMDVxYTNkR1VWbExTM2RaUWtKQlIwUjJla0ZDUWtGUlNHTnRWbk5hVjBaNldsUkJaMEpuYjNKQ1owVkZDa0ZaVHk5TlFVVkdRa0pLYzJOWFRUSk5hbEYyV1RKR2VVeFlTakZpYmxKd1lsZFZkMGhuV1V0TGQxbENRa0ZIUkhaNlFVSkNaMUZSWTIxV2JXTjVPVEFLV1Zka2VreDZSWFZOVkVsMVRVUkJOMEpuYjNKQ1owVkZRVmxQTDAxQlJVbENRekJOU3pKb01HUklRbnBQYVRoMlpFYzVjbHBYTkhWWlYwNHdZVmM1ZFFwamVUVnVZVmhTYjJSWFNqRmpNbFo1V1RJNWRXUkhWblZrUXpWcVlqSXdkMXBCV1V0TGQxbENRa0ZIUkhaNlFVSkRVVkpYUkVaU2IyUklVbmRqZW05MkNrd3laSEJrUjJneFdXazFhbUl5TUhaaVNFWnFUbXBKTUV3eVRtaGphVEY1WkZjMU1HRlhNV3hNZVRWdVlWaFNiMlJYU1haa01qbDVZVEphYzJJelpIb0tURE5LYkdKSFZtaGpNbFYxWlZjeGMxRklTbXhhYmsxMlpFZEdibU41T0hoTWFrVjVUR3BCZDA5QldVdExkMWxDUWtGSFJIWjZRVUpEWjFGeFJFTm5NQXBhYlZac1RtMVNhRTE2U214YWFteHJUV3BuTlZreVZtbFBSRUpxV20xTmVGcFVZM3BQUkU1clRVUnJNVmx0UlRSYVJGazFUVUl3UjBOcGMwZEJVVkZDQ21jM09IZEJVWE5GUkhkM1Rsb3liREJoU0ZacFRGZG9kbU16VW14YVJFRXhRbWR2Y2tKblJVVkJXVTh2VFVGRlRVSkRZMDFLVjJnd1pFaENlazlwT0hZS1dqSnNNR0ZJVm1sTWJVNTJZbE01YzJOWFRUSk5hbEYyV1RKR2VVeFlTakZpYmxKd1lsZFZkMDlCV1V0TGQxbENRa0ZIUkhaNlFVSkVVVkZ4UkVObk1BcGFiVlpzVG0xU2FFMTZTbXhhYW14clRXcG5OVmt5Vm1sUFJFSnFXbTFOZUZwVVkzcFBSRTVyVFVSck1WbHRSVFJhUkZrMVRVTkJSME5wYzBkQlVWRkNDbWMzT0hkQlVUUkZSV2QzVVdOdFZtMWplVGt3V1Zka2VreDZSWFZOVkVsMVRVUkJZVUpuYjNKQ1owVkZRVmxQTDAxQlJWQkNRWGROUTJwRmVrNXFSWGdLVDFSSk0wMVVVWGRMVVZsTFMzZFpRa0pCUjBSMmVrRkNSVUZSWWtSQ2JHOWtTRkozWTNwdmRrd3laSEJrUjJneFdXazFhbUl5TUhaaVNFWnFUbXBKTUFwTlFtZEhRMmx6UjBGUlVVSm5OemgzUVZKRlJVTm5kMGxPYW10NVRYcFJNVTVxVVhkYVFWbExTM2RaUWtKQlIwUjJla0ZDUldkU1YwUkdVbTlrU0ZKM0NtTjZiM1pNTW1Sd1pFZG9NVmxwTldwaU1qQjJZa2hHYWs1cVNUQk1NazVvWTJreGVXUlhOVEJoVnpGc1RIazFibUZZVW05a1YwbDJaREk1ZVdFeVduTUtZak5rZWt3elNteGlSMVpvWXpKVmRXVlhNWE5SU0Vwc1dtNU5kbVJIUm01amVUaDRUR3BGZVV4cVFYZFBRVmxMUzNkWlFrSkJSMFIyZWtGQ1JYZFJjUXBFUTJjd1dtMVdiRTV0VW1oTmVrcHNXbXBzYTAxcVp6VlpNbFpwVDBSQ2FscHRUWGhhVkdONlQwUk9hMDFFYXpGWmJVVTBXa1JaTlUxQ1kwZERhWE5IQ2tGUlVVSm5OemgzUVZKUlJVTlJkMGhqYlZaeldsZEdlbHBVUWxwQ1oyOXlRbWRGUlVGWlR5OU5RVVZXUWtWelRWTlhhREJrU0VKNlQyazRkbG95YkRBS1lVaFdhVXh0VG5aaVV6bHpZMWROTWsxcVVYWlpNa1o1VEZoS01XSnVVbkJpVjFWMldWZE9NR0ZYT1hWamVUbDVaRmMxZWt4NlRUTk9ha0Y1VFdwWmVRcE9ha2w0VERKR01HUkhWblJqU0ZKNlRIcEZkMFpuV1V0TGQxbENRa0ZIUkhaNlFVSkdaMUZKUkVGYWQyUlhTbk5oVjAxM1ZVRlpTMHQzV1VKQ1FVZEVDblo2UVVKSFFWSkRSRVZDZVZwWVFuWlBiWGg0V1hwWmVVNUZRVEpQVkVsNlRrUlZNazVET1dwWldFbDBZMjVXZFdSSGJIUmFWVUY0VFhwWmVFMVVhM2tLVG5wRk1FOXVTbXhhYW5CNVdsZGFla3d6VW1oYU0wMTJUVk0wZUUxcE5IZE5TVWRMUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpJZDBWbFowSTBRVWhaUVFvelZEQjNZWE5pU0VWVVNtcEhValJqYlZkak0wRnhTa3RZY21wbFVFc3pMMmcwY0hsblF6aHdOMjgwUVVGQlIyaEdZbnBCZDFGQlFVSkJUVUZTZWtKR0NrRnBRVE5TVWsweGRrMWlaREJtVlVVMFZVbHZNakZZYVVvNWEwbE1ZVGhsVW5FMVZsQnBhSHBIY0dOeWJtZEphRUZLWmxOelEwMUdWR3BGWWsxMUwwa0tkbWgxTDNWblIxbDRkM0oyUzBkWmNHcFhWeXRLVW5CdFVERjNXVTFCYjBkRFEzRkhVMDAwT1VKQlRVUkJNbU5CVFVkUlEwMUlkM0ZqZFdwNE5tOTZLd3BpYlRsd1oxRTFTV1pvVFhabVZXMXFXVWRsYUc1dkwzRXZTVTl3U25oc1UwaEVTU3R4U1RsamMwbEVORmx0VURoR0x6ZFZhMUZKZDFabk1DczNVbUZKQ2xSbmNuUjFWMmxhYUZoVFFXcHJkbWd2ZFVVMWNsaFpNVWRpWjBGUU9HZDNSVXhoZG1GQ01UZGhiWEJPUlhCaFUzUTNOa3cwVHprMENpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9fX19","integratedTime":1791366054,"logIndex":3128454895,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}}
@@ -10,6 +10,9 @@
10
10
  * turn 内任一 step 撞限 → turn/end 保留 max-tokens,不被后续成功 step 冲销
11
11
  * - 工具收口:concludesTurn=OR(任一 true 即收口);terminate=AND(整批 finalized 全 true 才中断)
12
12
  * - 取消:未派发调用补记成对事件(toolCall + 合成错误 toolResult),日志无缺口
13
+ * - 1.11-S2(D-33 / 计量设计 §3.3):turnEnd meta.usage = 本 turn usage 事件四分量求和
14
+ * (聚合在内核消费侧单点 closeTurn——适配器无状态 ACL;无 usage 事件 → meta 不带 usage 键,
15
+ * 缺省=未采集向后兼容;撞限/异常 turn 照常聚合,截断前已完成请求的 usage 事实不吞)
13
16
  */
14
17
  import type { SessionLog } from '../session/log.ts';
15
18
  export type StepOutcome = 'completed' | 'max-tokens' | 'null';
package/dist/loop/stop.js CHANGED
@@ -2,12 +2,45 @@ import { CarM8Error } from '../runtime-core/errors.js';
2
2
  export class TurnAborted extends Error {
3
3
  constructor() { super('CAR-ABORTED: turn cancelled by user'); }
4
4
  }
5
+ /**
6
+ * 1.11-S2(D-33 / 计量设计 §3.3):turn 级 usage 聚合——扫描本 turn 的 'usage' 事件(事实本体
7
+ * 在 JSONL,聚合由事实行求和而非运行时状态;字段非数值容错跳过不吞计数)。无 usage 事件
8
+ * 返回 undefined → turnEnd meta 不带 usage 键(缺省=未采集,旧格式向后兼容)。
9
+ */
10
+ function turnUsageFrom(log, turnId) {
11
+ let input = 0;
12
+ let output = 0;
13
+ let cacheRead = 0;
14
+ let cacheCreation = 0;
15
+ let requests = 0;
16
+ for (const e of log.events) {
17
+ if (e.kind !== 'usage' || e.turnId !== turnId)
18
+ continue;
19
+ const p = (e.payload ?? {});
20
+ if (typeof p.input === 'number')
21
+ input += p.input;
22
+ if (typeof p.output === 'number')
23
+ output += p.output;
24
+ if (typeof p.cacheRead === 'number')
25
+ cacheRead += p.cacheRead;
26
+ if (typeof p.cacheCreation === 'number')
27
+ cacheCreation += p.cacheCreation;
28
+ requests++;
29
+ }
30
+ return requests ? { input, output, cacheRead, cacheCreation, requests } : undefined;
31
+ }
5
32
  export async function runTurn(opts) {
6
33
  const { log, turnId, model, tools, preset = {}, signal } = opts;
7
34
  const maxSteps = preset.maxSteps ?? 32;
8
35
  const trail = [];
9
36
  let sawMaxTokens = false;
10
37
  let steps = 0;
38
+ // 1.11-S2(D-33):turnEnd 落链单点包装——聚合本 turn usage 事件(无 usage 不带键,
39
+ // 缺省=未采集向后兼容);reason 保持 meta 首键,usage 恒为加法尾键
40
+ const closeTurn = (meta) => {
41
+ const usage = turnUsageFrom(log, turnId);
42
+ return log.append('runtime', 'turnEnd', turnId, null, { ...meta, ...(usage ? { usage } : {}) });
43
+ };
11
44
  const executeBatch = async (calls) => {
12
45
  let concludesTurn = false;
13
46
  let concludesVotes = 0;
@@ -65,7 +98,7 @@ export async function runTurn(opts) {
65
98
  try {
66
99
  while (steps < maxSteps) {
67
100
  if (signal?.aborted) {
68
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'aborted' }).seq;
101
+ const seq = closeTurn({ reason: 'aborted' }).seq;
69
102
  trail.push({ seq, reason: 'aborted' });
70
103
  return { reason: 'aborted', steps, endReasonTrail: trail };
71
104
  }
@@ -73,7 +106,7 @@ export async function runTurn(opts) {
73
106
  log.snapshotModelRequest(); // agent-loop 集成点:请求时快照(N1)
74
107
  const step = await model();
75
108
  if (step.stopReason === 'error') {
76
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'error' }).seq;
109
+ const seq = closeTurn({ reason: 'error' }).seq;
77
110
  trail.push({ seq, reason: 'error' });
78
111
  return { reason: 'error', steps, endReasonTrail: trail };
79
112
  }
@@ -92,7 +125,7 @@ export async function runTurn(opts) {
92
125
  continue;
93
126
  }
94
127
  // 默认收口:截断响应不解析、不执行任何工具调用
95
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'max-tokens' }).seq;
128
+ const seq = closeTurn({ reason: 'max-tokens' }).seq;
96
129
  trail.push({ seq, reason: 'max-tokens' });
97
130
  return { reason: 'max-tokens', steps, endReasonTrail: trail };
98
131
  }
@@ -100,21 +133,21 @@ export async function runTurn(opts) {
100
133
  const batch = await executeBatch(step.toolCalls);
101
134
  // F13:授权拒绝即收口(blockOnDeny 产出点——blocked 仅新增产出点,枚举不增)
102
135
  if (preset.blockOnDeny && batch.denies > 0) {
103
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'blocked', denies: batch.denies }).seq;
136
+ const seq = closeTurn({ reason: 'blocked', denies: batch.denies }).seq;
104
137
  trail.push({ seq, reason: 'blocked' });
105
138
  return { reason: 'blocked', steps, endReasonTrail: trail };
106
139
  }
107
140
  const agg = preset.aggregate ?? {};
108
141
  const abortedNow = (agg.terminate ?? 'all') === 'any' ? batch.terminateVotes > 0 : batch.terminateAll;
109
142
  if (abortedNow) { // terminate 收口:默认 AND(整批),可配置 any
110
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'aborted' }).seq;
143
+ const seq = closeTurn({ reason: 'aborted' }).seq;
111
144
  trail.push({ seq, reason: 'aborted' });
112
145
  return { reason: 'aborted', steps, endReasonTrail: trail };
113
146
  }
114
147
  const concludedNow = (agg.concludesTurn ?? 'any') === 'any' ? batch.concludesTurn : batch.concludesAll && batch.finalized > 0;
115
148
  if (concludedNow) { // concludesTurn 收口:默认 OR,可配置 all
116
149
  const reason = sawMaxTokens ? 'max-tokens' : 'completed';
117
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason }).seq;
150
+ const seq = closeTurn({ reason }).seq;
118
151
  trail.push({ seq, reason });
119
152
  return { reason, steps, endReasonTrail: trail };
120
153
  }
@@ -123,17 +156,17 @@ export async function runTurn(opts) {
123
156
  }
124
157
  // stopReason === 'stop':模型自然完成
125
158
  const reason = sawMaxTokens ? 'max-tokens' : 'completed';
126
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason }).seq;
159
+ const seq = closeTurn({ reason }).seq;
127
160
  trail.push({ seq, reason });
128
161
  return { reason, steps, endReasonTrail: trail };
129
162
  }
130
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'error', detail: 'max-steps' }).seq;
163
+ const seq = closeTurn({ reason: 'error', detail: 'max-steps' }).seq;
131
164
  trail.push({ seq, reason: 'error' });
132
165
  return { reason: 'error', steps, endReasonTrail: trail };
133
166
  }
134
167
  catch (e) {
135
168
  if (e instanceof TurnAborted) {
136
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'aborted' }).seq;
169
+ const seq = closeTurn({ reason: 'aborted' }).seq;
137
170
  trail.push({ seq, reason: 'aborted' });
138
171
  return { reason: 'aborted', steps, endReasonTrail: trail };
139
172
  }
@@ -141,7 +174,7 @@ export async function runTurn(opts) {
141
174
  // car doctor」可达用户;非 CarM8Error 维持 String(e) 口径不变)
142
175
  const m8 = e instanceof CarM8Error;
143
176
  const detail = m8 ? `${e.slug}: ${e.message}` : String(e);
144
- const seq = log.append('runtime', 'turnEnd', turnId, null, {
177
+ const seq = closeTurn({
145
178
  reason: 'error', detail,
146
179
  ...(m8 ? { code: e.code, userHint: e.userHint, retryable: e.retryable } : {}),
147
180
  }).seq;
@@ -1,4 +1,4 @@
1
- {"stage":"archive","detail":"git archive HEAD → car-runtime-1.10.0.tgz(320119 bytes,树哈希锚定)","ts":1790667150985}
2
- {"stage":"sha256sums","detail":"SHA256SUMS 9e1d9d1865f46906…(裸字节复核通过)","ts":1790667150987}
3
- {"stage":"sbom","detail":"51 个组件(运行时依赖 0 项)","ts":1790667150993}
4
- {"stage":"cosign-keyless","detail":"signed car-runtime-1.10.0.tgz (keyless OIDC + Rekor)","ts":1790667152883}
1
+ {"stage":"archive","detail":"git archive HEAD → car-runtime-1.12.0.tgz(334423 bytes,树哈希锚定)","ts":1791366052907}
2
+ {"stage":"sha256sums","detail":"SHA256SUMS a3d97f3fa7eb455b…(裸字节复核通过)","ts":1791366052908}
3
+ {"stage":"sbom","detail":"51 个组件(运行时依赖 0 项)","ts":1791366052915}
4
+ {"stage":"cosign-keyless","detail":"signed car-runtime-1.12.0.tgz (keyless OIDC + Rekor)","ts":1791366059944}
@@ -12,7 +12,10 @@
12
12
  * (半截 JSON 不解析——ADR-001 同纪律,length 时只交 truncatedTools);
13
13
  * 5. assistant 文本事件落 M7(脱敏后 + secretsRedacted 计数留痕 meta);
14
14
  * 6. finishReason 不可变透传为 ModelStep.stopReason(error/aborted → 'error',BD-04 收口
15
- * 由 runTurn 落 turnEnd);工具调用事件由 runTurn executeBatch 落(本层不重复)。
15
+ * 由 runTurn 落 turnEnd);工具调用事件由 runTurn executeBatch 落(本层不重复);
16
+ * 7. 1.11-S2(D-33 / 计量设计 §3.3):流尾 usage chunk → 'runtime'/'usage' 请求级事实落链
17
+ * (缺失 = provider 未回/流中断/畸形,禁补造;actor='runtime'、model_visible=0 不进模型
18
+ * 消息流)——turn 级聚合在 stop.ts 收口单点,本层不做跨请求状态。
16
19
  */
17
20
  import type { SessionLog } from '../session/log.ts';
18
21
  import { type ModelStep } from '../loop/stop.ts';
@@ -27,6 +27,7 @@ export async function chatStep(opts) {
27
27
  const calls = new Map();
28
28
  let stopReason;
29
29
  let errorDetail;
30
+ let usage;
30
31
  const stream = core.chat({
31
32
  model,
32
33
  messages,
@@ -55,6 +56,8 @@ export async function chatStep(opts) {
55
56
  }
56
57
  if (chunk.error)
57
58
  errorDetail = chunk.error.message;
59
+ if (chunk.usage)
60
+ usage = chunk.usage; // D-33:附着于最后一个 chunk——后到覆盖
58
61
  if (chunk.finishReason !== undefined)
59
62
  stopReason = chunk.finishReason === 'aborted' ? 'error' : chunk.finishReason;
60
63
  }
@@ -67,6 +70,20 @@ export async function chatStep(opts) {
67
70
  throw new Error('CAR-E-LLM-FINISH: 流结束无 finishReason(适配器契约违规)');
68
71
  }
69
72
  text += redactor.flush();
73
+ // 1.11-S2(D-33 / 计量设计 §3.3):请求级 usage 事实落链(拿到 usage 的最后一个 chunk 即请求
74
+ // 收尾;撞限/异常 turn 照常落——截断前已完成请求的 usage 事实不吞)。缺失 = provider 未回,
75
+ // 禁补造;actor='runtime'、不进模型消息流(deriveMessages 不投影 usage,§2.5 红线)
76
+ if (usage) {
77
+ log.append('runtime', 'usage', turnId, {
78
+ model: usage.model,
79
+ adapterId: usage.adapterId,
80
+ input: usage.inputTokens,
81
+ output: usage.outputTokens,
82
+ cacheRead: usage.cacheReadTokens,
83
+ cacheCreation: usage.cacheCreationTokens,
84
+ providerRaw: usage.providerRaw,
85
+ });
86
+ }
70
87
  // 5. assistant 文本落 M7(脱敏后 + 计数留痕;空文本不产事件)
71
88
  // flush 后兜底复扫只计数不复写:流式遮蔽已生效,>0 即流式边界逃逸的审计信号
72
89
  let secretsRedacted = 0;
@@ -20,6 +20,11 @@
20
20
  * 0xC0000409)——car run 真路径 ≥3 次模型请求 + PTC worker/MCP 子进程并存即确定性复现
21
21
  * (1.4 彼时仅修遥测出站,LLM 出站同为 undici 默认——潜伏至 1.8 E2E 三请求形态暴露);
22
22
  * 流式契约 body = AsyncIterable<Uint8Array> 不变;测试注入缝 fetchImpl 保留。
23
+ * - 1.11-S1(D-33 / 计量设计 §3.2):usage 采集——openai 请求体加 stream_options.include_usage
24
+ * (默认开启,第三方网关不兼容时回退 = baseUrl 白名单,登记可回退项;缺失走缺失口径禁估算),
25
+ * 解析「先摘 evt.usage 再判 choice」(空 choices 尾包不再被吞);anthropic message_start(input 侧)
26
+ * + message_delta(output 侧)两包合并(message_delta 缺席 = output 不完整,整条不落);
27
+ * 归一口径 §1.1(openai 减法 / anthropic 透传 + cache_creation),畸形/缺失整条不落(禁补造)。
23
28
  */
24
29
  import { providerUnreachable } from './errors.js';
25
30
  import { CredentialService } from './credentials.js';
@@ -255,8 +260,31 @@ const FINISH_MAP = {
255
260
  };
256
261
  class Retryable extends Error {
257
262
  }
258
- /** 明文 SSE → LlmChunk 流解析(增量容错:半行驻留到下一 chunk) */
259
- async function* parseSseStream(body) {
263
+ /**
264
+ * 1.11-S1(D-33 / 计量设计 §1.1):openai usage 归一——prompt_tokens 含 cached,inputTokens
265
+ * 必须做减法(prompt − prompt_tokens_details.cached_tokens);cache_creation 恒 0(无对应字段,
266
+ * 登记不估算)。usage 对象缺席/畸形(两侧主字段任一非数值)= 整条不落(缺失口径,禁补造)。
267
+ */
268
+ function normalizeOpenAIUsage(u, model, adapterId) {
269
+ if (!u || typeof u !== 'object')
270
+ return undefined;
271
+ const r = u;
272
+ if (typeof r.prompt_tokens !== 'number' || typeof r.completion_tokens !== 'number')
273
+ return undefined;
274
+ const details = (r.prompt_tokens_details ?? null);
275
+ const cached = details && typeof details.cached_tokens === 'number' ? details.cached_tokens : 0;
276
+ return {
277
+ inputTokens: r.prompt_tokens - cached,
278
+ outputTokens: r.completion_tokens,
279
+ cacheReadTokens: cached,
280
+ cacheCreationTokens: 0,
281
+ model,
282
+ adapterId,
283
+ providerRaw: u,
284
+ };
285
+ }
286
+ /** 明文 SSE → LlmChunk 流解析(增量容错:半行驻留到下一 chunk;1.11-S1:model/adapterId 供 usage 归一) */
287
+ async function* parseSseStream(body, model, adapterId) {
260
288
  const decoder = new TextDecoder();
261
289
  let buf = '';
262
290
  for await (const bytes of body) {
@@ -279,9 +307,16 @@ async function* parseSseStream(body) {
279
307
  catch {
280
308
  continue;
281
309
  } // 心跳/注释行容错(显式跳过非 JSON data)
310
+ // 1.11-S1(D-33 / 计量设计 §3.2 必核行):先摘 usage 再判 choice——stream_options.include_usage
311
+ // 的尾包 choices 为空,下方 `if (!choice) continue` 原样会把它吞掉
312
+ const usage = evt.usage !== undefined ? normalizeOpenAIUsage(evt.usage, model, adapterId) : undefined;
282
313
  const choice = evt.choices?.[0];
283
- if (!choice)
314
+ if (!choice) {
315
+ // 独立 usage 尾包(官方形态:finish_reason chunk 之后、[DONE] 之前)——请求最后一个 chunk
316
+ if (usage)
317
+ yield { usage };
284
318
  continue;
319
+ }
285
320
  const delta = choice.delta ?? {};
286
321
  const raw = choice.finish_reason;
287
322
  const finish = raw == null ? undefined : (() => {
@@ -300,28 +335,38 @@ async function* parseSseStream(body) {
300
335
  : [];
301
336
  const text = typeof delta.content === 'string' && delta.content.length ? delta.content : undefined;
302
337
  // LlmChunk 单 toolCallDelta 口径:同事件多条 tool_calls 顺序展开为多 chunk
303
- if (!text && !toolCallDeltas.length && finish === undefined)
338
+ if (!text && !toolCallDeltas.length && finish === undefined) {
339
+ // 空事件但携 usage(部分 compat 网关形态):usage 不随空事件丢失
340
+ if (usage)
341
+ yield { usage };
304
342
  continue;
343
+ }
344
+ const chunks = [];
305
345
  if (toolCallDeltas.length === 0) {
306
- yield {
346
+ chunks.push({
307
347
  ...(text !== undefined ? { delta: text } : {}),
308
348
  ...(finish !== undefined
309
349
  ? { finishReason: finish, ...(finish === 'error' ? { error: { code: 'B080001', message: `未映射的 finish_reason "${raw}"` } } : {}) }
310
350
  : {}),
311
- };
351
+ });
312
352
  }
313
353
  else {
314
354
  for (let i = 0; i < toolCallDeltas.length; i++) {
315
355
  const last = i === toolCallDeltas.length - 1;
316
- yield {
356
+ chunks.push({
317
357
  ...(i === 0 && text !== undefined ? { delta: text } : {}),
318
358
  toolCallDelta: toolCallDeltas[i],
319
359
  ...(last && finish !== undefined
320
360
  ? { finishReason: finish, ...(finish === 'error' ? { error: { code: 'B080001', message: `未映射的 finish_reason "${raw}"` } } : {}) }
321
361
  : {}),
322
- };
362
+ });
323
363
  }
324
364
  }
365
+ // 1.11-S1(D-33):usage 与事件内容同 chunk 到达(网关共存形态)→ 附着于本事件最后一个 chunk
366
+ if (usage)
367
+ chunks[chunks.length - 1].usage = usage;
368
+ for (const c of chunks)
369
+ yield c;
325
370
  }
326
371
  }
327
372
  }
@@ -349,6 +394,9 @@ export function createOpenAICompatAdapter(opts) {
349
394
  body: JSON.stringify({
350
395
  model: req.model,
351
396
  stream: true,
397
+ // 1.11-S1(D-33 / 计量设计 §9-2 推荐A):usage 采集默认开启;第三方网关不兼容时的
398
+ // 回退方案(baseUrl 白名单)登记可回退项——不支持网关走缺失口径(禁估算补造)
399
+ stream_options: { include_usage: true },
352
400
  ...(req.maxTokens != null ? { max_tokens: req.maxTokens } : {}),
353
401
  messages: req.messages.map(m => {
354
402
  if (m.role === 'toolResult') {
@@ -386,7 +434,7 @@ export function createOpenAICompatAdapter(opts) {
386
434
  fetchImpl,
387
435
  signal: req.signal,
388
436
  timeoutMs, firstByteMs, retries, backoffMs, sleep,
389
- }, parseSseStream);
437
+ }, (body) => parseSseStream(body, req.model, id));
390
438
  },
391
439
  };
392
440
  }
@@ -398,6 +446,27 @@ const ANTHROPIC_STOP_REASON_MAP = {
398
446
  tool_use: 'toolUse',
399
447
  refusal: 'aborted',
400
448
  };
449
+ /**
450
+ * 1.11-S1(D-33 / 计量设计 §3.2):anthropic usage 两包合并——message_start(input 侧:
451
+ * input_tokens 天然不含 cache + cache_read/cache_creation)+ message_delta(output 侧:
452
+ * output_tokens)。任一包缺席或主字段非数值 = 侧不完整,整条不落(缺失口径,禁补造半条事实);
453
+ * providerRaw 保留两原始片段供审计对拍。
454
+ */
455
+ function mergeAnthropicUsage(start, delta, model, adapterId) {
456
+ if (!start || !delta)
457
+ return undefined;
458
+ if (typeof start.input_tokens !== 'number' || typeof delta.output_tokens !== 'number')
459
+ return undefined;
460
+ return {
461
+ inputTokens: start.input_tokens,
462
+ outputTokens: delta.output_tokens,
463
+ cacheReadTokens: typeof start.cache_read_input_tokens === 'number' ? start.cache_read_input_tokens : 0,
464
+ cacheCreationTokens: typeof start.cache_creation_input_tokens === 'number' ? start.cache_creation_input_tokens : 0,
465
+ model,
466
+ adapterId,
467
+ providerRaw: { messageStart: start, messageDelta: delta },
468
+ };
469
+ }
401
470
  /**
402
471
  * Anthropic Messages API 请求体(D-20):system 消息提升为顶层 `system`(多条 \n\n 连接);
403
472
  * toolResult 收敛进 user 消息 content(连续多条合并为单 user 多 tool_result 块——Anthropic
@@ -462,10 +531,13 @@ function toAnthropic(req, apiKey) {
462
531
  * Anthropic SSE → LlmChunk 流解析(data-only:data JSON 自带 `type` 字段,`event:` 行不依赖)。
463
532
  * content_block_start(tool_use) → toolCallDelta{id,name};input_json_delta → toolCallDelta{argumentsDelta};
464
533
  * message_delta.stop_reason → finishReason(终态信号,单次);ping/message_stop/未知事件容错跳过。
534
+ * 1.11-S1(D-33):message_start.usage 采集 input 侧(原解析整体跳过该事件)+ message_delta.usage
535
+ * 合并 output 侧附着于终态 chunk;流中断 delta 未到 = output 侧不完整,整条不落(R-2)。
465
536
  */
466
- async function* parseAnthropicSseStream(body) {
537
+ async function* parseAnthropicSseStream(body, model, adapterId) {
467
538
  const decoder = new TextDecoder();
468
539
  let buf = '';
540
+ let startUsage = null;
469
541
  for await (const bytes of body) {
470
542
  buf += decoder.decode(bytes, { stream: true });
471
543
  let nl;
@@ -501,13 +573,26 @@ async function* parseAnthropicSseStream(body) {
501
573
  }
502
574
  // thinking_delta / citations_delta 等非 CAR 消费面:容错跳过
503
575
  }
576
+ else if (type === 'message_start') {
577
+ // 1.11-S1(D-33):input 侧采集(usage 嵌套在 message 下)
578
+ const u = evt.message?.usage;
579
+ if (u && typeof u === 'object')
580
+ startUsage = u;
581
+ }
504
582
  else if (type === 'message_delta') {
505
583
  const raw = evt.delta?.stop_reason;
584
+ // 1.11-S1(D-33):output 侧(delta.usage.output_tokens)与 input 侧合并——两包齐才落
585
+ const usage = mergeAnthropicUsage(startUsage, (evt.usage ?? null), model, adapterId);
506
586
  if (raw != null) {
507
587
  const finish = ANTHROPIC_STOP_REASON_MAP[String(raw)];
508
- yield finish
588
+ const chunk = finish
509
589
  ? { finishReason: finish }
510
590
  : { finishReason: 'error', error: { code: 'B080001', message: `未映射的 stop_reason "${raw}"` } };
591
+ yield usage ? { ...chunk, usage } : chunk;
592
+ }
593
+ else if (usage) {
594
+ // delta 未携 stop_reason 但携 usage(网关形态):usage 不随缺失终态信号丢失
595
+ yield { usage };
511
596
  }
512
597
  }
513
598
  else if (type === 'error') {
@@ -547,7 +632,7 @@ export function createAnthropicAdapter(opts) {
547
632
  fetchImpl,
548
633
  signal: req.signal,
549
634
  timeoutMs, firstByteMs, retries, backoffMs, sleep,
550
- }, parseAnthropicSseStream);
635
+ }, (body) => parseAnthropicSseStream(body, req.model, id));
551
636
  },
552
637
  };
553
638
  }
@@ -41,6 +41,24 @@ export interface ToolCallDelta {
41
41
  /** JSON 增量片段,聚合后整体 JSON.parse(半截 JSON 不解析——ADR-001 同纪律) */
42
42
  argumentsDelta?: string;
43
43
  }
44
+ /**
45
+ * 1.11-S1(D-33 / 计量设计 §3.1):token 四分量归一化用量——跨 provider 可加总的库内口径
46
+ * (§1.1:openai prompt_tokens 含 cached 必须减法归一;anthropic input_tokens 天然不含 cache)。
47
+ * provider 原始 usage 整体存 providerRaw 供审计对拍(对拍恒以 provider_raw 为准)。
48
+ */
49
+ export interface LlmUsage {
50
+ /** 非缓存输入 token(归一化后) */
51
+ inputTokens: number;
52
+ outputTokens: number;
53
+ cacheReadTokens: number;
54
+ /** openai 系无对应字段,恒 0(登记不估算) */
55
+ cacheCreationTokens: number;
56
+ /** 实际请求模型(req.model) */
57
+ model: string;
58
+ adapterId: string;
59
+ /** provider 原始 usage 对象(anthropic 为 messageStart/messageDelta 两原始片段) */
60
+ providerRaw: unknown;
61
+ }
44
62
  /** 流式块(finishReason 一经产生不可被任何中间层改写) */
45
63
  export interface LlmChunk {
46
64
  delta?: string;
@@ -51,6 +69,8 @@ export interface LlmChunk {
51
69
  code: string;
52
70
  message: string;
53
71
  };
72
+ /** 1.11-S1(D-33):每次请求最后一个 chunk 附带归一化用量;缺失 = provider 未回,禁补造 */
73
+ usage?: LlmUsage;
54
74
  }
55
75
  /** Provider 适配器(ACL,C-02):统一签名,消解 Provider 差异 */
56
76
  export interface LlmAdapter {
package/dist/sbom.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "bomFormat": "CAR-SBOM",
3
3
  "specVersion": "0.1",
4
- "version": "1.10.0",
4
+ "version": "1.12.0",
5
5
  "components": [
6
6
  {
7
7
  "type": "file",
8
8
  "path": "src/authz/authz.ts",
9
- "sha256": "3988f18886c1f0e77f3f1536cea93445189a9fd112fd42ecd8ba90525ba908c0"
9
+ "sha256": "93c9d92387109bde25cd33f4125a9cef19e74d9abedc74a0f5efb94552c1b472"
10
10
  },
11
11
  {
12
12
  "type": "file",
@@ -101,7 +101,7 @@
101
101
  {
102
102
  "type": "file",
103
103
  "path": "src/loop/stop.ts",
104
- "sha256": "435be5f60aae6b0d5b87642960d5daf9ee73e06530993dc194c60e804fbc7075"
104
+ "sha256": "f32392a1047da2351c2430659a1e54f2701c7e6cef0015d456e7c8f984472057"
105
105
  },
106
106
  {
107
107
  "type": "file",
@@ -151,7 +151,7 @@
151
151
  {
152
152
  "type": "file",
153
153
  "path": "src/runtime-core/chatStep.ts",
154
- "sha256": "c1720b041905d4b24e3c8eae5dc20817f12172ecf8b260fdee5e69eb7710905a"
154
+ "sha256": "7a0f455e352529ee7e97a3ed2b9b7b9032493baae7542396d6f8b186fb788ecb"
155
155
  },
156
156
  {
157
157
  "type": "file",
@@ -166,7 +166,7 @@
166
166
  {
167
167
  "type": "file",
168
168
  "path": "src/runtime-core/llm.ts",
169
- "sha256": "fe308a55b6c2e3e089a27b70479a36d9f081af3a5862bc376dc5c0fd46d39bee"
169
+ "sha256": "781b951a4e822c759fc7af0297679d4f000270162e3c3b1ff119c03e59e91cce"
170
170
  },
171
171
  {
172
172
  "type": "file",
@@ -191,7 +191,7 @@
191
191
  {
192
192
  "type": "file",
193
193
  "path": "src/runtime-core/types.ts",
194
- "sha256": "b532ae18ff4a24ee24c2181e3ab6cf1e14996b9f8a2e30a17e8cfce684505d82"
194
+ "sha256": "f828cc38072b36ba47d8a4f24337277fe5e5867fa0292a24696c9f845f204571"
195
195
  },
196
196
  {
197
197
  "type": "file",
@@ -201,7 +201,7 @@
201
201
  {
202
202
  "type": "file",
203
203
  "path": "src/seam.ts",
204
- "sha256": "c3ed5dbefe745e97e89fff6ebccc44fc1bbdd05dafa1e1b934dd5d29717b7e27"
204
+ "sha256": "636a30df62d5b59483b8f39aad1e003cb75fe65d0bd88d26a6577c1e97b79f7a"
205
205
  },
206
206
  {
207
207
  "type": "file",
@@ -236,7 +236,7 @@
236
236
  {
237
237
  "type": "file",
238
238
  "path": "src/session/log.ts",
239
- "sha256": "054922dad476e51c822508dee97558791306e9c3359893b230a4b4e1536613f8"
239
+ "sha256": "aa8c7b8d9cff690dfe7c6429399d9d2bd656f1ff1c07154715e4dc5d07946c9d"
240
240
  },
241
241
  {
242
242
  "type": "file",
@@ -251,12 +251,12 @@
251
251
  {
252
252
  "type": "file",
253
253
  "path": "package.json",
254
- "sha256": "35103775f8eff29b1c3226da5c19bd47bc3ad9db068e767defd01c0af5bcfa66"
254
+ "sha256": "f91a96c876bb0a52110b08d7c7bcd5b96d4307647f82d866a526556b954bca82"
255
255
  },
256
256
  {
257
257
  "type": "file",
258
258
  "path": "README.md",
259
- "sha256": "c0f3b806a2b2639a95ab4ac1f28bb85e13426f24de462a65e806455667e171f2"
259
+ "sha256": "4af52eade25ee3971613c5b1887f927ed3363bd35a3ca68e59d5266c4c0f73e4"
260
260
  }
261
261
  ],
262
262
  "runtimeDependencies": []
package/dist/seam.d.ts CHANGED
@@ -5,6 +5,13 @@
5
5
  * - 仅暴露进程内挂接所需的最窄面(装配/会话/turn/遥测),禁止透出内核内部模块路径语义;
6
6
  * - 破坏性变更须内核 major;加法演进走 minor 并在本文件头登记;
7
7
  * - 语义不变:runTurn/chatStep/SessionLog 即 CLI 与测试承载的同一实现(非转发壳)。
8
+ *
9
+ * 加法登记:1.11.0 —— AuthzService/checkCapability 出面(平台确认桥幂等决策 + 会话策略直判承载;F4 US-3 语义同源)。
10
+ * 加法登记:1.12.0 —— MCP/PTC 真桥出面(D-20/D-35/D-36:McpGateway + 双 transport 工厂 + resolveMcpHeaders +
11
+ * makePtcToolDefinition + renderSdkFromRegistry + 8 类型)。负面清单(不出面):worker-entry/budget/erasable/
12
+ * runCode(PTC 红线单点——erasable-only/预算/授权门仅经 makePtcToolDefinition 行为面可达)、JsonRpc 编码层、
13
+ * rawPost/parseTraceparent/formatTraceparent(trace 通道内部件)。jiti epoch 面(D-37):mountPlugin.reloadEpoch
14
+ * 自 1.10.0 起已随本面可达,零加法登记确认。
8
15
  */
9
16
  export { SessionLog, type SessionEvent, type EventKind, type Actor } from './session/log.ts';
10
17
  export { SessionFileStore } from './session/store.ts';
@@ -17,5 +24,12 @@ export { mountPlugin } from './load/loader.ts';
17
24
  export { Context } from './kernel/context.ts';
18
25
  export { loadCarConfig, mergeLlmConfig, mergeSignatureGate, warmCarConfig, type CarConfig, type LlmConfig } from './load/config.ts';
19
26
  export { probeCapabilities, SandboxExecutor } from './sandbox/sandbox.ts';
27
+ export { AuthzService, checkCapability, type Decision, type AuthzRequest, type AuthzDecision, type DeclaredCapabilities } from './authz/authz.ts';
20
28
  export { createTelemetryFacade, telemetryConfigFromEnv } from './runtime-core/telemetry.ts';
21
29
  export { TurnTracer, type TurnSpanHandle } from './runtime-core/trace.ts';
30
+ export { McpGateway, type ClientTransport, type McpServerConfig, type McpToolDefinition } from './mcp/gateway.ts';
31
+ export { createStdioClientTransport, type StdioServerSpec } from './mcp/stdioClient.ts';
32
+ export { createHttpMcpTransport, resolveMcpHeaders, type HttpMcpServerSpec } from './mcp/httpClient.ts';
33
+ export { makePtcToolDefinition, type ToolBridge } from './ptc/runCode.ts';
34
+ export { renderSdkFromRegistry } from './ptc/sdk.ts';
35
+ export type { McpClientSpanHandle, PtcRelayHandle } from './runtime-core/trace.ts';
package/dist/seam.js CHANGED
@@ -5,6 +5,13 @@
5
5
  * - 仅暴露进程内挂接所需的最窄面(装配/会话/turn/遥测),禁止透出内核内部模块路径语义;
6
6
  * - 破坏性变更须内核 major;加法演进走 minor 并在本文件头登记;
7
7
  * - 语义不变:runTurn/chatStep/SessionLog 即 CLI 与测试承载的同一实现(非转发壳)。
8
+ *
9
+ * 加法登记:1.11.0 —— AuthzService/checkCapability 出面(平台确认桥幂等决策 + 会话策略直判承载;F4 US-3 语义同源)。
10
+ * 加法登记:1.12.0 —— MCP/PTC 真桥出面(D-20/D-35/D-36:McpGateway + 双 transport 工厂 + resolveMcpHeaders +
11
+ * makePtcToolDefinition + renderSdkFromRegistry + 8 类型)。负面清单(不出面):worker-entry/budget/erasable/
12
+ * runCode(PTC 红线单点——erasable-only/预算/授权门仅经 makePtcToolDefinition 行为面可达)、JsonRpc 编码层、
13
+ * rawPost/parseTraceparent/formatTraceparent(trace 通道内部件)。jiti epoch 面(D-37):mountPlugin.reloadEpoch
14
+ * 自 1.10.0 起已随本面可达,零加法登记确认。
8
15
  */
9
16
  export { SessionLog } from './session/log.js';
10
17
  export { SessionFileStore } from './session/store.js';
@@ -16,5 +23,12 @@ export { mountPlugin } from './load/loader.js';
16
23
  export { Context } from './kernel/context.js';
17
24
  export { loadCarConfig, mergeLlmConfig, mergeSignatureGate, warmCarConfig } from './load/config.js';
18
25
  export { probeCapabilities, SandboxExecutor } from './sandbox/sandbox.js';
26
+ export { AuthzService, checkCapability } from './authz/authz.js';
19
27
  export { createTelemetryFacade, telemetryConfigFromEnv } from './runtime-core/telemetry.js';
20
28
  export { TurnTracer } from './runtime-core/trace.js';
29
+ // —— 1.12.0(D-20/D-35/D-36):MCP/PTC 真桥面 ——
30
+ export { McpGateway } from './mcp/gateway.js';
31
+ export { createStdioClientTransport } from './mcp/stdioClient.js';
32
+ export { createHttpMcpTransport, resolveMcpHeaders } from './mcp/httpClient.js';
33
+ export { makePtcToolDefinition } from './ptc/runCode.js';
34
+ export { renderSdkFromRegistry } from './ptc/sdk.js';
@@ -1,4 +1,4 @@
1
- export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' | 'system';
1
+ export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' | 'system' | 'usage';
2
2
  export type Actor = 'user' | 'model' | 'plugin' | 'runtime';
3
3
  export interface SessionEvent {
4
4
  seq: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lqc123qwe/car-runtime",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "Composable Agent Runtime (CAR) - microkernel plugin system with sandboxed PTC, append-only hashed session log, and MCP host bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -61,6 +61,18 @@ export class AuthzService {
61
61
  return this.#commit(decision)
62
62
  }
63
63
 
64
+ /** 会话策略直判(「会话内始终允许」粒度):decidedBy=policy 幂等落链,不入人审通道(审批回调 scope=session 写入 grants 后走此路) */
65
+ decideBySessionPolicy(req: AuthzRequest): AuthzDecision {
66
+ return this.#commit({
67
+ authorizationId: req.authorizationId,
68
+ decision: 'APPROVED',
69
+ decidedBy: 'policy',
70
+ reason: `session policy grant (always-allow): ${req.resource}`,
71
+ degraded: this.#degraded,
72
+ ts: Date.now(),
73
+ })
74
+ }
75
+
64
76
  /** 人审(confirm 模式):120s 超时默认拒绝(timeout-default-deny) */
65
77
  async decideByUser(req: AuthzRequest, approve: (req: AuthzRequest) => Promise<boolean>): Promise<AuthzDecision> {
66
78
  const cached = this.#decisions.get(req.authorizationId)
package/src/loop/stop.ts CHANGED
@@ -10,6 +10,9 @@
10
10
  * turn 内任一 step 撞限 → turn/end 保留 max-tokens,不被后续成功 step 冲销
11
11
  * - 工具收口:concludesTurn=OR(任一 true 即收口);terminate=AND(整批 finalized 全 true 才中断)
12
12
  * - 取消:未派发调用补记成对事件(toolCall + 合成错误 toolResult),日志无缺口
13
+ * - 1.11-S2(D-33 / 计量设计 §3.3):turnEnd meta.usage = 本 turn usage 事件四分量求和
14
+ * (聚合在内核消费侧单点 closeTurn——适配器无状态 ACL;无 usage 事件 → meta 不带 usage 键,
15
+ * 缺省=未采集向后兼容;撞限/异常 turn 照常聚合,截断前已完成请求的 usage 事实不吞)
13
16
  */
14
17
  import type { SessionLog } from '../session/log.ts'
15
18
  import { CarM8Error } from '../runtime-core/errors.ts'
@@ -58,6 +61,29 @@ export class TurnAborted extends Error {
58
61
  constructor() { super('CAR-ABORTED: turn cancelled by user') }
59
62
  }
60
63
 
64
+ /**
65
+ * 1.11-S2(D-33 / 计量设计 §3.3):turn 级 usage 聚合——扫描本 turn 的 'usage' 事件(事实本体
66
+ * 在 JSONL,聚合由事实行求和而非运行时状态;字段非数值容错跳过不吞计数)。无 usage 事件
67
+ * 返回 undefined → turnEnd meta 不带 usage 键(缺省=未采集,旧格式向后兼容)。
68
+ */
69
+ function turnUsageFrom(log: SessionLog, turnId: string): { input: number; output: number; cacheRead: number; cacheCreation: number; requests: number } | undefined {
70
+ let input = 0
71
+ let output = 0
72
+ let cacheRead = 0
73
+ let cacheCreation = 0
74
+ let requests = 0
75
+ for (const e of log.events) {
76
+ if (e.kind !== 'usage' || e.turnId !== turnId) continue
77
+ const p = (e.payload ?? {}) as Record<string, unknown>
78
+ if (typeof p.input === 'number') input += p.input
79
+ if (typeof p.output === 'number') output += p.output
80
+ if (typeof p.cacheRead === 'number') cacheRead += p.cacheRead
81
+ if (typeof p.cacheCreation === 'number') cacheCreation += p.cacheCreation
82
+ requests++
83
+ }
84
+ return requests ? { input, output, cacheRead, cacheCreation, requests } : undefined
85
+ }
86
+
61
87
  export async function runTurn(opts: {
62
88
  log: SessionLog
63
89
  turnId: string
@@ -72,6 +98,13 @@ export async function runTurn(opts: {
72
98
  let sawMaxTokens = false
73
99
  let steps = 0
74
100
 
101
+ // 1.11-S2(D-33):turnEnd 落链单点包装——聚合本 turn usage 事件(无 usage 不带键,
102
+ // 缺省=未采集向后兼容);reason 保持 meta 首键,usage 恒为加法尾键
103
+ const closeTurn = (meta: Record<string, unknown>) => {
104
+ const usage = turnUsageFrom(log, turnId)
105
+ return log.append('runtime', 'turnEnd', turnId, null, { ...meta, ...(usage ? { usage } : {}) })
106
+ }
107
+
75
108
  const executeBatch = async (calls: ToolCall[]): Promise<{ concludesTurn: boolean; concludesAll: boolean; terminateVotes: number; terminateAll: boolean; finalized: number; denies: number }> => {
76
109
  let concludesTurn = false
77
110
  let concludesVotes = 0
@@ -122,7 +155,7 @@ export async function runTurn(opts: {
122
155
  try {
123
156
  while (steps < maxSteps) {
124
157
  if (signal?.aborted) {
125
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'aborted' }).seq
158
+ const seq = closeTurn({ reason: 'aborted' }).seq
126
159
  trail.push({ seq, reason: 'aborted' })
127
160
  return { reason: 'aborted', steps, endReasonTrail: trail }
128
161
  }
@@ -131,7 +164,7 @@ export async function runTurn(opts: {
131
164
  const step = await model()
132
165
 
133
166
  if (step.stopReason === 'error') {
134
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'error' }).seq
167
+ const seq = closeTurn({ reason: 'error' }).seq
135
168
  trail.push({ seq, reason: 'error' })
136
169
  return { reason: 'error', steps, endReasonTrail: trail }
137
170
  }
@@ -151,7 +184,7 @@ export async function runTurn(opts: {
151
184
  continue
152
185
  }
153
186
  // 默认收口:截断响应不解析、不执行任何工具调用
154
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'max-tokens' }).seq
187
+ const seq = closeTurn({ reason: 'max-tokens' }).seq
155
188
  trail.push({ seq, reason: 'max-tokens' })
156
189
  return { reason: 'max-tokens', steps, endReasonTrail: trail }
157
190
  }
@@ -160,21 +193,21 @@ export async function runTurn(opts: {
160
193
  const batch = await executeBatch(step.toolCalls)
161
194
  // F13:授权拒绝即收口(blockOnDeny 产出点——blocked 仅新增产出点,枚举不增)
162
195
  if (preset.blockOnDeny && batch.denies > 0) {
163
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'blocked', denies: batch.denies }).seq
196
+ const seq = closeTurn({ reason: 'blocked', denies: batch.denies }).seq
164
197
  trail.push({ seq, reason: 'blocked' })
165
198
  return { reason: 'blocked', steps, endReasonTrail: trail }
166
199
  }
167
200
  const agg = preset.aggregate ?? {}
168
201
  const abortedNow = (agg.terminate ?? 'all') === 'any' ? batch.terminateVotes > 0 : batch.terminateAll
169
202
  if (abortedNow) { // terminate 收口:默认 AND(整批),可配置 any
170
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'aborted' }).seq
203
+ const seq = closeTurn({ reason: 'aborted' }).seq
171
204
  trail.push({ seq, reason: 'aborted' })
172
205
  return { reason: 'aborted', steps, endReasonTrail: trail }
173
206
  }
174
207
  const concludedNow = (agg.concludesTurn ?? 'any') === 'any' ? batch.concludesTurn : batch.concludesAll && batch.finalized > 0
175
208
  if (concludedNow) { // concludesTurn 收口:默认 OR,可配置 all
176
209
  const reason: TurnEndReason = sawMaxTokens ? 'max-tokens' : 'completed'
177
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason }).seq
210
+ const seq = closeTurn({ reason }).seq
178
211
  trail.push({ seq, reason })
179
212
  return { reason, steps, endReasonTrail: trail }
180
213
  }
@@ -184,16 +217,16 @@ export async function runTurn(opts: {
184
217
 
185
218
  // stopReason === 'stop':模型自然完成
186
219
  const reason: TurnEndReason = sawMaxTokens ? 'max-tokens' : 'completed'
187
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason }).seq
220
+ const seq = closeTurn({ reason }).seq
188
221
  trail.push({ seq, reason })
189
222
  return { reason, steps, endReasonTrail: trail }
190
223
  }
191
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'error', detail: 'max-steps' }).seq
224
+ const seq = closeTurn({ reason: 'error', detail: 'max-steps' }).seq
192
225
  trail.push({ seq, reason: 'error' })
193
226
  return { reason: 'error', steps, endReasonTrail: trail }
194
227
  } catch (e) {
195
228
  if (e instanceof TurnAborted) {
196
- const seq = log.append('runtime', 'turnEnd', turnId, null, { reason: 'aborted' }).seq
229
+ const seq = closeTurn({ reason: 'aborted' }).seq
197
230
  trail.push({ seq, reason: 'aborted' })
198
231
  return { reason: 'aborted', steps, endReasonTrail: trail }
199
232
  }
@@ -201,7 +234,7 @@ export async function runTurn(opts: {
201
234
  // car doctor」可达用户;非 CarM8Error 维持 String(e) 口径不变)
202
235
  const m8 = e instanceof CarM8Error
203
236
  const detail = m8 ? `${e.slug}: ${e.message}` : String(e)
204
- const seq = log.append('runtime', 'turnEnd', turnId, null, {
237
+ const seq = closeTurn({
205
238
  reason: 'error', detail,
206
239
  ...(m8 ? { code: e.code, userHint: e.userHint, retryable: e.retryable } : {}),
207
240
  }).seq
@@ -12,12 +12,15 @@
12
12
  * (半截 JSON 不解析——ADR-001 同纪律,length 时只交 truncatedTools);
13
13
  * 5. assistant 文本事件落 M7(脱敏后 + secretsRedacted 计数留痕 meta);
14
14
  * 6. finishReason 不可变透传为 ModelStep.stopReason(error/aborted → 'error',BD-04 收口
15
- * 由 runTurn 落 turnEnd);工具调用事件由 runTurn executeBatch 落(本层不重复)。
15
+ * 由 runTurn 落 turnEnd);工具调用事件由 runTurn executeBatch 落(本层不重复);
16
+ * 7. 1.11-S2(D-33 / 计量设计 §3.3):流尾 usage chunk → 'runtime'/'usage' 请求级事实落链
17
+ * (缺失 = provider 未回/流中断/畸形,禁补造;actor='runtime'、model_visible=0 不进模型
18
+ * 消息流)——turn 级聚合在 stop.ts 收口单点,本层不做跨请求状态。
16
19
  */
17
20
  import type { SessionLog } from '../session/log.ts'
18
21
  import { TurnAborted, type ModelStep, type ToolCall } from '../loop/stop.ts'
19
22
  import type { RuntimeCore } from './llm.ts'
20
- import type { ToolDefinition } from './types.ts'
23
+ import type { LlmUsage, ToolDefinition } from './types.ts'
21
24
  import { StreamRedactor } from './redaction.ts'
22
25
  import { scanSecrets } from '../security/secrets.ts'
23
26
  import { providerUnreachable } from './errors.ts'
@@ -67,6 +70,7 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
67
70
  const calls = new Map<number, { id?: string; name?: string; argsBuf: string }>()
68
71
  let stopReason: ModelStep['stopReason'] | undefined
69
72
  let errorDetail: string | undefined
73
+ let usage: LlmUsage | undefined
70
74
 
71
75
  const stream = core.chat({
72
76
  model,
@@ -90,6 +94,7 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
90
94
  calls.set(idx, cur)
91
95
  }
92
96
  if (chunk.error) errorDetail = chunk.error.message
97
+ if (chunk.usage) usage = chunk.usage // D-33:附着于最后一个 chunk——后到覆盖
93
98
  if (chunk.finishReason !== undefined) stopReason = chunk.finishReason === 'aborted' ? 'error' : chunk.finishReason
94
99
  }
95
100
  // 1.4-S4(D-19):finish 检查前先查取消(适配器取消路径静默收口无 finishReason——
@@ -101,6 +106,21 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
101
106
  }
102
107
  text += redactor.flush()
103
108
 
109
+ // 1.11-S2(D-33 / 计量设计 §3.3):请求级 usage 事实落链(拿到 usage 的最后一个 chunk 即请求
110
+ // 收尾;撞限/异常 turn 照常落——截断前已完成请求的 usage 事实不吞)。缺失 = provider 未回,
111
+ // 禁补造;actor='runtime'、不进模型消息流(deriveMessages 不投影 usage,§2.5 红线)
112
+ if (usage) {
113
+ log.append('runtime', 'usage', turnId, {
114
+ model: usage.model,
115
+ adapterId: usage.adapterId,
116
+ input: usage.inputTokens,
117
+ output: usage.outputTokens,
118
+ cacheRead: usage.cacheReadTokens,
119
+ cacheCreation: usage.cacheCreationTokens,
120
+ providerRaw: usage.providerRaw,
121
+ })
122
+ }
123
+
104
124
  // 5. assistant 文本落 M7(脱敏后 + 计数留痕;空文本不产事件)
105
125
  // flush 后兜底复扫只计数不复写:流式遮蔽已生效,>0 即流式边界逃逸的审计信号
106
126
  let secretsRedacted = 0
@@ -20,10 +20,15 @@
20
20
  * 0xC0000409)——car run 真路径 ≥3 次模型请求 + PTC worker/MCP 子进程并存即确定性复现
21
21
  * (1.4 彼时仅修遥测出站,LLM 出站同为 undici 默认——潜伏至 1.8 E2E 三请求形态暴露);
22
22
  * 流式契约 body = AsyncIterable<Uint8Array> 不变;测试注入缝 fetchImpl 保留。
23
+ * - 1.11-S1(D-33 / 计量设计 §3.2):usage 采集——openai 请求体加 stream_options.include_usage
24
+ * (默认开启,第三方网关不兼容时回退 = baseUrl 白名单,登记可回退项;缺失走缺失口径禁估算),
25
+ * 解析「先摘 evt.usage 再判 choice」(空 choices 尾包不再被吞);anthropic message_start(input 侧)
26
+ * + message_delta(output 侧)两包合并(message_delta 缺席 = output 不完整,整条不落);
27
+ * 归一口径 §1.1(openai 减法 / anthropic 透传 + cache_creation),畸形/缺失整条不落(禁补造)。
23
28
  */
24
29
  import { CarM8Error, providerUnreachable } from './errors.ts'
25
30
  import { CredentialService } from './credentials.ts'
26
- import type { Disposable, LlmAdapter, LlmChunk, LlmRequest, ToolCallDelta } from './types.ts'
31
+ import type { Disposable, LlmAdapter, LlmChunk, LlmRequest, LlmUsage, ToolCallDelta } from './types.ts'
27
32
  import type { PluginContext } from '../kernel/context.ts'
28
33
  import { request as httpRequest } from 'node:http'
29
34
  import { request as httpsRequest } from 'node:https'
@@ -279,8 +284,30 @@ const FINISH_MAP: Record<string, LlmChunk['finishReason']> = {
279
284
 
280
285
  class Retryable extends Error {}
281
286
 
282
- /** 明文 SSE → LlmChunk 流解析(增量容错:半行驻留到下一 chunk) */
283
- async function* parseSseStream(body: AsyncIterable<Uint8Array>): AsyncIterable<LlmChunk> {
287
+ /**
288
+ * 1.11-S1(D-33 / 计量设计 §1.1):openai usage 归一——prompt_tokens 含 cached,inputTokens
289
+ * 必须做减法(prompt − prompt_tokens_details.cached_tokens);cache_creation 恒 0(无对应字段,
290
+ * 登记不估算)。usage 对象缺席/畸形(两侧主字段任一非数值)= 整条不落(缺失口径,禁补造)。
291
+ */
292
+ function normalizeOpenAIUsage(u: unknown, model: string, adapterId: string): LlmUsage | undefined {
293
+ if (!u || typeof u !== 'object') return undefined
294
+ const r = u as Record<string, unknown>
295
+ if (typeof r.prompt_tokens !== 'number' || typeof r.completion_tokens !== 'number') return undefined
296
+ const details = (r.prompt_tokens_details ?? null) as Record<string, unknown> | null
297
+ const cached = details && typeof details.cached_tokens === 'number' ? details.cached_tokens : 0
298
+ return {
299
+ inputTokens: r.prompt_tokens - cached,
300
+ outputTokens: r.completion_tokens,
301
+ cacheReadTokens: cached,
302
+ cacheCreationTokens: 0,
303
+ model,
304
+ adapterId,
305
+ providerRaw: u,
306
+ }
307
+ }
308
+
309
+ /** 明文 SSE → LlmChunk 流解析(增量容错:半行驻留到下一 chunk;1.11-S1:model/adapterId 供 usage 归一) */
310
+ async function* parseSseStream(body: AsyncIterable<Uint8Array>, model: string, adapterId: string): AsyncIterable<LlmChunk> {
284
311
  const decoder = new TextDecoder()
285
312
  let buf = ''
286
313
  for await (const bytes of body) {
@@ -295,8 +322,15 @@ async function* parseSseStream(body: AsyncIterable<Uint8Array>): AsyncIterable<L
295
322
  if (!data) continue
296
323
  let evt: any
297
324
  try { evt = JSON.parse(data) } catch { continue } // 心跳/注释行容错(显式跳过非 JSON data)
325
+ // 1.11-S1(D-33 / 计量设计 §3.2 必核行):先摘 usage 再判 choice——stream_options.include_usage
326
+ // 的尾包 choices 为空,下方 `if (!choice) continue` 原样会把它吞掉
327
+ const usage = evt.usage !== undefined ? normalizeOpenAIUsage(evt.usage, model, adapterId) : undefined
298
328
  const choice = evt.choices?.[0]
299
- if (!choice) continue
329
+ if (!choice) {
330
+ // 独立 usage 尾包(官方形态:finish_reason chunk 之后、[DONE] 之前)——请求最后一个 chunk
331
+ if (usage) yield { usage }
332
+ continue
333
+ }
300
334
  const delta = choice.delta ?? {}
301
335
  const raw = choice.finish_reason
302
336
  const finish: LlmChunk['finishReason'] | undefined = raw == null ? undefined : (() => {
@@ -315,26 +349,34 @@ async function* parseSseStream(body: AsyncIterable<Uint8Array>): AsyncIterable<L
315
349
  : []
316
350
  const text = typeof delta.content === 'string' && delta.content.length ? delta.content : undefined
317
351
  // LlmChunk 单 toolCallDelta 口径:同事件多条 tool_calls 顺序展开为多 chunk
318
- if (!text && !toolCallDeltas.length && finish === undefined) continue
352
+ if (!text && !toolCallDeltas.length && finish === undefined) {
353
+ // 空事件但携 usage(部分 compat 网关形态):usage 不随空事件丢失
354
+ if (usage) yield { usage }
355
+ continue
356
+ }
357
+ const chunks: LlmChunk[] = []
319
358
  if (toolCallDeltas.length === 0) {
320
- yield {
359
+ chunks.push({
321
360
  ...(text !== undefined ? { delta: text } : {}),
322
361
  ...(finish !== undefined
323
362
  ? { finishReason: finish, ...(finish === 'error' ? { error: { code: 'B080001', message: `未映射的 finish_reason "${raw}"` } } : {}) }
324
363
  : {}),
325
- }
364
+ })
326
365
  } else {
327
366
  for (let i = 0; i < toolCallDeltas.length; i++) {
328
367
  const last = i === toolCallDeltas.length - 1
329
- yield {
368
+ chunks.push({
330
369
  ...(i === 0 && text !== undefined ? { delta: text } : {}),
331
370
  toolCallDelta: toolCallDeltas[i],
332
371
  ...(last && finish !== undefined
333
372
  ? { finishReason: finish, ...(finish === 'error' ? { error: { code: 'B080001', message: `未映射的 finish_reason "${raw}"` } } : {}) }
334
373
  : {}),
335
- }
374
+ })
336
375
  }
337
376
  }
377
+ // 1.11-S1(D-33):usage 与事件内容同 chunk 到达(网关共存形态)→ 附着于本事件最后一个 chunk
378
+ if (usage) chunks[chunks.length - 1]!.usage = usage
379
+ for (const c of chunks) yield c
338
380
  }
339
381
  }
340
382
  }
@@ -364,6 +406,9 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
364
406
  body: JSON.stringify({
365
407
  model: req.model,
366
408
  stream: true,
409
+ // 1.11-S1(D-33 / 计量设计 §9-2 推荐A):usage 采集默认开启;第三方网关不兼容时的
410
+ // 回退方案(baseUrl 白名单)登记可回退项——不支持网关走缺失口径(禁估算补造)
411
+ stream_options: { include_usage: true },
367
412
  ...(req.maxTokens != null ? { max_tokens: req.maxTokens } : {}),
368
413
  messages: req.messages.map(m => {
369
414
  if (m.role === 'toolResult') {
@@ -402,7 +447,7 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
402
447
  fetchImpl,
403
448
  signal: req.signal,
404
449
  timeoutMs, firstByteMs, retries, backoffMs, sleep,
405
- }, parseSseStream)
450
+ }, (body) => parseSseStream(body, req.model, id))
406
451
  },
407
452
  }
408
453
  }
@@ -436,6 +481,31 @@ const ANTHROPIC_STOP_REASON_MAP: Record<string, LlmChunk['finishReason']> = {
436
481
  refusal: 'aborted',
437
482
  }
438
483
 
484
+ /**
485
+ * 1.11-S1(D-33 / 计量设计 §3.2):anthropic usage 两包合并——message_start(input 侧:
486
+ * input_tokens 天然不含 cache + cache_read/cache_creation)+ message_delta(output 侧:
487
+ * output_tokens)。任一包缺席或主字段非数值 = 侧不完整,整条不落(缺失口径,禁补造半条事实);
488
+ * providerRaw 保留两原始片段供审计对拍。
489
+ */
490
+ function mergeAnthropicUsage(
491
+ start: Record<string, unknown> | null,
492
+ delta: Record<string, unknown> | null,
493
+ model: string,
494
+ adapterId: string,
495
+ ): LlmUsage | undefined {
496
+ if (!start || !delta) return undefined
497
+ if (typeof start.input_tokens !== 'number' || typeof delta.output_tokens !== 'number') return undefined
498
+ return {
499
+ inputTokens: start.input_tokens,
500
+ outputTokens: delta.output_tokens,
501
+ cacheReadTokens: typeof start.cache_read_input_tokens === 'number' ? start.cache_read_input_tokens : 0,
502
+ cacheCreationTokens: typeof start.cache_creation_input_tokens === 'number' ? start.cache_creation_input_tokens : 0,
503
+ model,
504
+ adapterId,
505
+ providerRaw: { messageStart: start, messageDelta: delta },
506
+ }
507
+ }
508
+
439
509
  /**
440
510
  * Anthropic Messages API 请求体(D-20):system 消息提升为顶层 `system`(多条 \n\n 连接);
441
511
  * toolResult 收敛进 user 消息 content(连续多条合并为单 user 多 tool_result 块——Anthropic
@@ -499,10 +569,13 @@ function toAnthropic(req: LlmRequest, apiKey: string) {
499
569
  * Anthropic SSE → LlmChunk 流解析(data-only:data JSON 自带 `type` 字段,`event:` 行不依赖)。
500
570
  * content_block_start(tool_use) → toolCallDelta{id,name};input_json_delta → toolCallDelta{argumentsDelta};
501
571
  * message_delta.stop_reason → finishReason(终态信号,单次);ping/message_stop/未知事件容错跳过。
572
+ * 1.11-S1(D-33):message_start.usage 采集 input 侧(原解析整体跳过该事件)+ message_delta.usage
573
+ * 合并 output 侧附着于终态 chunk;流中断 delta 未到 = output 侧不完整,整条不落(R-2)。
502
574
  */
503
- async function* parseAnthropicSseStream(body: AsyncIterable<Uint8Array>): AsyncIterable<LlmChunk> {
575
+ async function* parseAnthropicSseStream(body: AsyncIterable<Uint8Array>, model: string, adapterId: string): AsyncIterable<LlmChunk> {
504
576
  const decoder = new TextDecoder()
505
577
  let buf = ''
578
+ let startUsage: Record<string, unknown> | null = null
506
579
  for await (const bytes of body) {
507
580
  buf += decoder.decode(bytes, { stream: true })
508
581
  let nl: number
@@ -528,13 +601,23 @@ async function* parseAnthropicSseStream(body: AsyncIterable<Uint8Array>): AsyncI
528
601
  yield { toolCallDelta: { index: evt.index, argumentsDelta: d.partial_json } }
529
602
  }
530
603
  // thinking_delta / citations_delta 等非 CAR 消费面:容错跳过
604
+ } else if (type === 'message_start') {
605
+ // 1.11-S1(D-33):input 侧采集(usage 嵌套在 message 下)
606
+ const u = (evt.message as Record<string, unknown> | undefined)?.usage
607
+ if (u && typeof u === 'object') startUsage = u as Record<string, unknown>
531
608
  } else if (type === 'message_delta') {
532
609
  const raw = evt.delta?.stop_reason
610
+ // 1.11-S1(D-33):output 侧(delta.usage.output_tokens)与 input 侧合并——两包齐才落
611
+ const usage = mergeAnthropicUsage(startUsage, (evt.usage ?? null) as Record<string, unknown> | null, model, adapterId)
533
612
  if (raw != null) {
534
613
  const finish = ANTHROPIC_STOP_REASON_MAP[String(raw)]
535
- yield finish
614
+ const chunk: LlmChunk = finish
536
615
  ? { finishReason: finish }
537
616
  : { finishReason: 'error', error: { code: 'B080001', message: `未映射的 stop_reason "${raw}"` } }
617
+ yield usage ? { ...chunk, usage } : chunk
618
+ } else if (usage) {
619
+ // delta 未携 stop_reason 但携 usage(网关形态):usage 不随缺失终态信号丢失
620
+ yield { usage }
538
621
  }
539
622
  } else if (type === 'error') {
540
623
  // 流内 error 事件(overloaded_error 等)→ error chunk 收口(后续 message_delta 由守卫拦冲突)
@@ -575,7 +658,7 @@ export function createAnthropicAdapter(opts: AnthropicAdapterOptions): LlmAdapte
575
658
  fetchImpl,
576
659
  signal: req.signal,
577
660
  timeoutMs, firstByteMs, retries, backoffMs, sleep,
578
- }, parseAnthropicSseStream)
661
+ }, (body) => parseAnthropicSseStream(body, req.model, id))
579
662
  },
580
663
  }
581
664
  }
@@ -36,14 +36,35 @@ export interface ToolCallDelta {
36
36
  argumentsDelta?: string
37
37
  }
38
38
 
39
+ /**
40
+ * 1.11-S1(D-33 / 计量设计 §3.1):token 四分量归一化用量——跨 provider 可加总的库内口径
41
+ * (§1.1:openai prompt_tokens 含 cached 必须减法归一;anthropic input_tokens 天然不含 cache)。
42
+ * provider 原始 usage 整体存 providerRaw 供审计对拍(对拍恒以 provider_raw 为准)。
43
+ */
44
+ export interface LlmUsage {
45
+ /** 非缓存输入 token(归一化后) */
46
+ inputTokens: number
47
+ outputTokens: number
48
+ cacheReadTokens: number
49
+ /** openai 系无对应字段,恒 0(登记不估算) */
50
+ cacheCreationTokens: number
51
+ /** 实际请求模型(req.model) */
52
+ model: string
53
+ adapterId: string
54
+ /** provider 原始 usage 对象(anthropic 为 messageStart/messageDelta 两原始片段) */
55
+ providerRaw: unknown
56
+ }
57
+
39
58
  /** 流式块(finishReason 一经产生不可被任何中间层改写) */
40
59
  export interface LlmChunk {
41
60
  delta?: string
42
61
  toolCallDelta?: ToolCallDelta
43
62
  finishReason?: 'stop' | 'length' | 'toolUse' | 'error' | 'aborted'
44
- // —— 规格外加法扩展(登记:M8 收口文档 §4)——
63
+ // —— 规格外加法扩展(登记:M8 收口文档 §4 / 计量设计 §3.1,1.11.0)——
45
64
  /** finishReason='error' 时的结构化原因(消费方落 turnEnd meta,不进模型消息流) */
46
65
  error?: { code: string; message: string }
66
+ /** 1.11-S1(D-33):每次请求最后一个 chunk 附带归一化用量;缺失 = provider 未回,禁补造 */
67
+ usage?: LlmUsage
47
68
  }
48
69
 
49
70
  /** Provider 适配器(ACL,C-02):统一签名,消解 Provider 差异 */
package/src/seam.ts CHANGED
@@ -5,6 +5,13 @@
5
5
  * - 仅暴露进程内挂接所需的最窄面(装配/会话/turn/遥测),禁止透出内核内部模块路径语义;
6
6
  * - 破坏性变更须内核 major;加法演进走 minor 并在本文件头登记;
7
7
  * - 语义不变:runTurn/chatStep/SessionLog 即 CLI 与测试承载的同一实现(非转发壳)。
8
+ *
9
+ * 加法登记:1.11.0 —— AuthzService/checkCapability 出面(平台确认桥幂等决策 + 会话策略直判承载;F4 US-3 语义同源)。
10
+ * 加法登记:1.12.0 —— MCP/PTC 真桥出面(D-20/D-35/D-36:McpGateway + 双 transport 工厂 + resolveMcpHeaders +
11
+ * makePtcToolDefinition + renderSdkFromRegistry + 8 类型)。负面清单(不出面):worker-entry/budget/erasable/
12
+ * runCode(PTC 红线单点——erasable-only/预算/授权门仅经 makePtcToolDefinition 行为面可达)、JsonRpc 编码层、
13
+ * rawPost/parseTraceparent/formatTraceparent(trace 通道内部件)。jiti epoch 面(D-37):mountPlugin.reloadEpoch
14
+ * 自 1.10.0 起已随本面可达,零加法登记确认。
8
15
  */
9
16
  export { SessionLog, type SessionEvent, type EventKind, type Actor } from './session/log.ts'
10
17
  export { SessionFileStore } from './session/store.ts'
@@ -17,5 +24,13 @@ export { mountPlugin } from './load/loader.ts'
17
24
  export { Context } from './kernel/context.ts'
18
25
  export { loadCarConfig, mergeLlmConfig, mergeSignatureGate, warmCarConfig, type CarConfig, type LlmConfig } from './load/config.ts'
19
26
  export { probeCapabilities, SandboxExecutor } from './sandbox/sandbox.ts'
27
+ export { AuthzService, checkCapability, type Decision, type AuthzRequest, type AuthzDecision, type DeclaredCapabilities } from './authz/authz.ts'
20
28
  export { createTelemetryFacade, telemetryConfigFromEnv } from './runtime-core/telemetry.ts'
21
29
  export { TurnTracer, type TurnSpanHandle } from './runtime-core/trace.ts'
30
+ // —— 1.12.0(D-20/D-35/D-36):MCP/PTC 真桥面 ——
31
+ export { McpGateway, type ClientTransport, type McpServerConfig, type McpToolDefinition } from './mcp/gateway.ts'
32
+ export { createStdioClientTransport, type StdioServerSpec } from './mcp/stdioClient.ts'
33
+ export { createHttpMcpTransport, resolveMcpHeaders, type HttpMcpServerSpec } from './mcp/httpClient.ts'
34
+ export { makePtcToolDefinition, type ToolBridge } from './ptc/runCode.ts'
35
+ export { renderSdkFromRegistry } from './ptc/sdk.ts'
36
+ export type { McpClientSpanHandle, PtcRelayHandle } from './runtime-core/trace.ts'
@@ -11,7 +11,7 @@ import { createHash } from 'node:crypto'
11
11
  import { readFileSync } from 'node:fs'
12
12
  import { decodeLogBuffer, splitJsonlLines } from './format.ts'
13
13
 
14
- export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' | 'system' // M3-S11 加法扩展:多宿主归一化降级通道(pattern 未命中留痕,零静默);M5-DEC4 加法扩展:跨宿主 fork 标记事件;1.4-S2 加法扩展:system prompt 落链(§3.5.5:新模型可见输入 = 新事件类型);六值 TurnEndReason 不变
14
+ export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' | 'system' | 'usage' // M3-S11 加法扩展:多宿主归一化降级通道(pattern 未命中留痕,零静默);M5-DEC4 加法扩展:跨宿主 fork 标记事件;1.4-S2 加法扩展:system prompt 落链(§3.5.5:新模型可见输入 = 新事件类型);1.11-S2 加法扩展:usage 计量事实(D-33,actor='runtime'、model_visible=0 不进模型消息流,deriveMessages 不投影);六值 TurnEndReason 不变
15
15
  export type Actor = 'user' | 'model' | 'plugin' | 'runtime'
16
16
  export interface SessionEvent {
17
17
  seq: number
Binary file
@@ -1 +0,0 @@
1
- {"base64Signature":"MEUCIQC9Eb4tjZzDe1mzgXUycmzYBYKgL6Jy5Hk2QNYQDXwEAQIgYQ8bVRThONnvlokYD1GdQgBQ3KxeKvH2RtK4/0VZSz4=","cert":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUhFakNDQnBtZ0F3SUJBZ0lVVkJKazAvbWwrckMvZ01PcVlOVk1vSVpTbEV3d0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpZd09USTVNRGN6TWpNeFdoY05Nall3T1RJNU1EYzBNak14V2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVLS2lkTGZWTUR6RGZJT1hocTRRanhkbjZUdUJhK0xBSXRMQVcKZFVMWkJoRnFLTFgzSGg0MlJCdWorOWVCQmZ3cDhtbjJDRmJvbWcya1NJN1lGUkg3V2FPQ0JiZ3dnZ1cwTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVFMlA0CkNEd3Rta0ZPVTJwNEhoNk5iU1NRM3Bvd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d1lRWURWUjBSQVFIL0JGY3dWWVpUYUhSMGNITTZMeTluYVhSb2RXSXVZMjl0TDJ4eFl6WXlOQzlqWVhJdApjblZ1ZEdsdFpTOHVaMmwwYUhWaUwzZHZjbXRtYkc5M2N5OXlaV3hsWVhObExubHRiRUJ5WldaekwyaGxZV1J6CkwyMWhhVzR3T1FZS0t3WUJCQUdEdnpBQkFRUXJhSFIwY0hNNkx5OTBiMnRsYmk1aFkzUnBiMjV6TG1kcGRHaDEKWW5WelpYSmpiMjUwWlc1MExtTnZiVEFmQmdvckJnRUVBWU8vTUFFQ0JCRjNiM0pyWm14dmQxOWthWE53WVhSagphREEyQmdvckJnRUVBWU8vTUFFREJDaGtaRGswWlRCaFpETmpNbUZqWmpFM1kyRTNOelpqTWpreVpEUTFZV0UzCk9EazFNV1ZtWTJWbE1CVUdDaXNHQVFRQmc3OHdBUVFFQjNKbGJHVmhjMlV3SUFZS0t3WUJCQUdEdnpBQkJRUVMKYkhGak5qSTBMMk5oY2kxeWRXNTBhVzFsTUIwR0Npc0dBUVFCZzc4d0FRWUVEM0psWm5NdmFHVmhaSE12YldGcApiakE3QmdvckJnRUVBWU8vTUFFSUJDME1LMmgwZEhCek9pOHZkRzlyWlc0dVlXTjBhVzl1Y3k1bmFYUm9kV0oxCmMyVnlZMjl1ZEdWdWRDNWpiMjB3WXdZS0t3WUJCQUdEdnpBQkNRUlZERk5vZEhSd2N6b3ZMMmRwZEdoMVlpNWoKYjIwdmJIRmpOakkwTDJOaGNpMXlkVzUwYVcxbEx5NW5hWFJvZFdJdmQyOXlhMlpzYjNkekwzSmxiR1ZoYzJVdQplVzFzUUhKbFpuTXZhR1ZoWkhNdmJXRnBiakE0QmdvckJnRUVBWU8vTUFFS0JDb01LR1JrT1RSbE1HRmtNMk15CllXTm1NVGRqWVRjM05tTXlPVEprTkRWaFlUYzRPVFV4WldaalpXVXdIUVlLS3dZQkJBR0R2ekFCQ3dRUERBMW4KYVhSb2RXSXRhRzl6ZEdWa01EVUdDaXNHQVFRQmc3OHdBUXdFSnd3bGFIUjBjSE02THk5bmFYUm9kV0l1WTI5dApMMnh4WXpZeU5DOWpZWEl0Y25WdWRHbHRaVEE0QmdvckJnRUVBWU8vTUFFTkJDb01LR1JrT1RSbE1HRmtNMk15CllXTm1NVGRqWVRjM05tTXlPVEprTkRWaFlUYzRPVFV4WldaalpXVXdId1lLS3dZQkJBR0R2ekFCRGdRUkRBOXkKWldaekwyaGxZV1J6TDIxaGFXNHdHZ1lLS3dZQkJBR0R2ekFCRHdRTURBb3hNell4TVRreU56RTBNQ2tHQ2lzRwpBUVFCZzc4d0FSQUVHd3daYUhSMGNITTZMeTluYVhSb2RXSXVZMjl0TDJ4eFl6WXlOREFZQmdvckJnRUVBWU8vCk1BRVJCQW9NQ0RZNU1qTTBOVFkwTUdNR0Npc0dBUVFCZzc4d0FSSUVWUXhUYUhSMGNITTZMeTluYVhSb2RXSXUKWTI5dEwyeHhZell5TkM5allYSXRjblZ1ZEdsdFpTOHVaMmwwYUhWaUwzZHZjbXRtYkc5M2N5OXlaV3hsWVhObApMbmx0YkVCeVpXWnpMMmhsWVdSekwyMWhhVzR3T0FZS0t3WUJCQUdEdnpBQkV3UXFEQ2hrWkRrMFpUQmhaRE5qCk1tRmpaakUzWTJFM056WmpNamt5WkRRMVlXRTNPRGsxTVdWbVkyVmxNQ0VHQ2lzR0FRUUJnNzh3QVJRRUV3d1IKZDI5eWEyWnNiM2RmWkdsemNHRjBZMmd3V1FZS0t3WUJCQUdEdnpBQkZRUkxERWxvZEhSd2N6b3ZMMmRwZEdoMQpZaTVqYjIwdmJIRmpOakkwTDJOaGNpMXlkVzUwYVcxbEwyRmpkR2x2Ym5NdmNuVnVjeTh6TmpVek56SXlOalV5Ck5DOWhkSFJsYlhCMGN5OHhNQllHQ2lzR0FRUUJnNzh3QVJZRUNBd0djSFZpYkdsak1FOEdDaXNHQVFRQmc3OHcKQVJnRVFRdy9jbVZ3Ynpwc2NXTTJNalJBTmpreU16UTFOalF2WTJGeUxYSjFiblJwYldWQU1UTTJNVEU1TWpjeApORHB5WldZNmNtVm1jeTlvWldGa2N5OXRZV2x1TUlHSkJnb3JCZ0VFQWRaNUFnUUNCSHNFZVFCM0FIVUEzVDB3CmFzYkhFVEpqR1I0Y21XYzNBcUpLWHJqZVBLMy9oNHB5Z0M4cDdvNEFBQUdnN0JSYVZRQUFCQU1BUmpCRUFpQUgKRlJxT20zUlk4TWJ6RVhPS08vQjRNSW9PR1ZpSk5McW9UaE0rUERRd3hnSWdIczJLKzZKelJyT1h6OGwya3JJVgpXdzBKRktTcExXNEJUbDhvcHhrbTJSSXdDZ1lJS29aSXpqMEVBd01EWndBd1pBSXdJWXorK3FLMi9sZHJ4TmxuCnpvU2FlcTl6UWorOE1odnFOWXVXbVV4MVArN1VFc2pNcTh2cjlZZkg4Y09WNnR3ZEFqQUIyWExlZFlFNlRXdmYKbEI4Wk1xNVVodEQwR01nZXJLREhibGR6d0xFcHg2YnM3QUJTbEdXb1RyajdBbVJibzNVPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==","rekorBundle":{"SignedEntryTimestamp":"MEYCIQCQSshImz2LhSdIM0EyENAxTY1e+WvxVUnnr33WAxBy8wIhAIS1JWDTUupB9GK+eAdIDUt9uJeziuzX8v1UjrNl+r8j","Payload":{"body":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI5ZTFkOWQxODY1ZjQ2OTA2NGZjZTcyNGQ2NDZiOGEzYzliODdjODk3ODY4YzU5YTM0ODgwMGMwYzY0Y2I5ZWY5In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUM5RWI0dGpaekRlMW16Z1hVeWNtellCWUtnTDZKeTVIazJRTllRRFh3RUFRSWdZUThiVlJUaE9ObnZsb2tZRDFHZFFnQlEzS3hlS3ZIMlJ0SzQvMFZaU3o0PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVaEZha05EUW5CdFowRjNTVUpCWjBsVlZrSkthekF2Yld3cmNrTXZaMDFQY1ZsT1ZrMXZTVnBUYkVWM2QwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFpkMDlVU1RWTlJHTjZUV3BOZUZkb1kwNU5hbGwzVDFSSk5VMUVZekJOYWsxNFYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMUzJsa1RHWldUVVI2UkdaSlQxaG9jVFJSYW5oa2JqWlVkVUpoSzB4QlNYUk1RVmNLWkZWTVdrSm9SbkZMVEZnelNHZzBNbEpDZFdvck9XVkNRbVozY0RodGJqSkRSbUp2YldjeWExTkpOMWxHVWtnM1YyRlBRMEppWjNkbloxY3dUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZGTWxBMENrTkVkM1J0YTBaUFZUSndORWhvTms1aVUxTlJNM0J2ZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDFsUldVUldVakJTUVZGSUwwSkdZM2RXV1ZwVVlVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVERKNGVGbDZXWGxPUXpscVdWaEpkQXBqYmxaMVpFZHNkRnBUT0hWYU1td3dZVWhXYVV3elpIWmpiWFJ0WWtjNU0yTjVPWGxhVjNoc1dWaE9iRXh1YkhSaVJVSjVXbGRhZWt3eWFHeFpWMUo2Q2t3eU1XaGhWelIzVDFGWlMwdDNXVUpDUVVkRWRucEJRa0ZSVVhKaFNGSXdZMGhOTmt4NU9UQmlNblJzWW1rMWFGa3pVbkJpTWpWNlRHMWtjR1JIYURFS1dXNVdlbHBZU21waU1qVXdXbGMxTUV4dFRuWmlWRUZtUW1kdmNrSm5SVVZCV1U4dlRVRkZRMEpDUmpOaU0wcHlXbTE0ZG1ReE9XdGhXRTUzV1ZoU2FncGhSRUV5UW1kdmNrSm5SVVZCV1U4dlRVRkZSRUpEYUd0YVJHc3dXbFJDYUZwRVRtcE5iVVpxV21wRk0xa3lSVE5PZWxwcVRXcHJlVnBFVVRGWlYwVXpDazlFYXpGTlYxWnRXVEpXYkUxQ1ZVZERhWE5IUVZGUlFtYzNPSGRCVVZGRlFqTktiR0pIVm1oak1sVjNTVUZaUzB0M1dVSkNRVWRFZG5wQlFrSlJVVk1LWWtoR2FrNXFTVEJNTWs1b1kya3hlV1JYTlRCaFZ6RnNUVUl3UjBOcGMwZEJVVkZDWnpjNGQwRlJXVVZFTTBwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0FwaWFrRTNRbWR2Y2tKblJVVkJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveENtTXlWbmxaTWpsMVpFZFdkV1JETldwaU1qQjNXWGRaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbFpFUms1dlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5Xb0tZakl3ZG1KSVJtcE9ha2t3VERKT2FHTnBNWGxrVnpVd1lWY3hiRXg1Tlc1aFdGSnZaRmRKZG1ReU9YbGhNbHB6WWpOa2Vrd3pTbXhpUjFab1l6SlZkUXBsVnpGelVVaEtiRnB1VFhaaFIxWm9Xa2hOZG1KWFJuQmlha0UwUW1kdmNrSm5SVVZCV1U4dlRVRkZTMEpEYjAxTFIxSnJUMVJTYkUxSFJtdE5NazE1Q2xsWFRtMU5WR1JxV1ZSak0wNXRUWGxQVkVwclRrUldhRmxVWXpSUFZGVjRXbGRhYWxwWFZYZElVVmxMUzNkWlFrSkJSMFIyZWtGQ1EzZFJVRVJCTVc0S1lWaFNiMlJYU1hSaFJ6bDZaRWRXYTAxRVZVZERhWE5IUVZGUlFtYzNPSGRCVVhkRlNuZDNiR0ZJVWpCalNFMDJUSGs1Ym1GWVVtOWtWMGwxV1RJNWRBcE1Nbmg0V1hwWmVVNURPV3BaV0VsMFkyNVdkV1JIYkhSYVZFRTBRbWR2Y2tKblJVVkJXVTh2VFVGRlRrSkRiMDFMUjFKclQxUlNiRTFIUm10Tk1rMTVDbGxYVG0xTlZHUnFXVlJqTTA1dFRYbFBWRXByVGtSV2FGbFVZelJQVkZWNFdsZGFhbHBYVlhkSWQxbExTM2RaUWtKQlIwUjJla0ZDUkdkUlVrUkJPWGtLV2xkYWVrd3lhR3haVjFKNlRESXhhR0ZYTkhkSFoxbExTM2RaUWtKQlIwUjJla0ZDUkhkUlRVUkJiM2hOZWxsNFRWUnJlVTU2UlRCTlEydEhRMmx6UndwQlVWRkNaemM0ZDBGU1FVVkhkM2RhWVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBUREo0ZUZsNldYbE9SRUZaUW1kdmNrSm5SVVZCV1U4dkNrMUJSVkpDUVc5TlEwUlpOVTFxVFRCT1ZGa3dUVWROUjBOcGMwZEJVVkZDWnpjNGQwRlNTVVZXVVhoVVlVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVUtXVEk1ZEV3eWVIaFplbGw1VGtNNWFsbFlTWFJqYmxaMVpFZHNkRnBUT0hWYU1td3dZVWhXYVV3elpIWmpiWFJ0WWtjNU0yTjVPWGxhVjNoc1dWaE9iQXBNYm14MFlrVkNlVnBYV25wTU1taHNXVmRTZWt3eU1XaGhWelIzVDBGWlMwdDNXVUpDUVVkRWRucEJRa1YzVVhGRVEyaHJXa1JyTUZwVVFtaGFSRTVxQ2sxdFJtcGFha1V6V1RKRk0wNTZXbXBOYW10NVdrUlJNVmxYUlROUFJHc3hUVmRXYlZreVZteE5RMFZIUTJselIwRlJVVUpuTnpoM1FWSlJSVVYzZDFJS1pESTVlV0V5V25OaU0yUm1Xa2RzZW1OSFJqQlpNbWQzVjFGWlMwdDNXVUpDUVVkRWRucEJRa1pSVWt4RVJXeHZaRWhTZDJONmIzWk1NbVJ3WkVkb01RcFphVFZxWWpJd2RtSklSbXBPYWtrd1RESk9hR05wTVhsa1Z6VXdZVmN4YkV3eVJtcGtSMngyWW01TmRtTnVWblZqZVRoNlRtcFZlazU2U1hsT2FsVjVDazVET1doa1NGSnNZbGhDTUdONU9IaE5RbGxIUTJselIwRlJVVUpuTnpoM1FWSlpSVU5CZDBkalNGWnBZa2RzYWsxRk9FZERhWE5IUVZGUlFtYzNPSGNLUVZKblJWRlJkeTlqYlZaM1lucHdjMk5YVFRKTmFsSkJUbXByZVUxNlVURk9hbEYyV1RKR2VVeFlTakZpYmxKd1lsZFdRVTFVVFRKTlZFVTFUV3BqZUFwT1JIQjVXbGRaTm1OdFZtMWplVGx2V2xkR2EyTjVPWFJaVjJ4MVRVbEhTa0puYjNKQ1owVkZRV1JhTlVGblVVTkNTSE5GWlZGQ00wRklWVUV6VkRCM0NtRnpZa2hGVkVwcVIxSTBZMjFYWXpOQmNVcExXSEpxWlZCTE15OW9OSEI1WjBNNGNEZHZORUZCUVVkbk4wSlNZVlpSUVVGQ1FVMUJVbXBDUlVGcFFVZ0tSbEp4VDIwelVsazRUV0o2UlZoUFMwOHZRalJOU1c5UFIxWnBTazVNY1c5VWFFMHJVRVJSZDNoblNXZEljekpMS3paS2VsSnlUMWg2T0d3eWEzSkpWZ3BYZHpCS1JrdFRjRXhYTkVKVWJEaHZjSGhyYlRKU1NYZERaMWxKUzI5YVNYcHFNRVZCZDAxRVduZEJkMXBCU1hkSldYb3JLM0ZMTWk5c1pISjRUbXh1Q25wdlUyRmxjVGw2VVdvck9FMW9kbkZPV1hWWGJWVjRNVkFyTjFWRmMycE5jVGgyY2psWlprZzRZMDlXTm5SM1pFRnFRVUl5V0V4bFpGbEZObFJYZG1ZS2JFSTRXazF4TlZWb2RFUXdSMDFuWlhKTFJFaGliR1I2ZDB4RmNIZzJZbk0zUVVKVGJFZFhiMVJ5YWpkQmJWSmliek5WUFFvdExTMHRMVVZPUkNCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2c9PSJ9fX19","integratedTime":1790667152,"logIndex":2995741508,"logID":"c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}}