@lqc123qwe/car-runtime 1.9.0 → 1.11.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 (182) hide show
  1. package/README.md +17 -1
  2. package/dist/SHA256SUMS +1 -0
  3. package/dist/authz/authz.d.ts +46 -0
  4. package/dist/authz/authz.js +76 -0
  5. package/dist/car-runtime-1.11.0.tgz +0 -0
  6. package/dist/car-runtime-1.11.0.tgz.sig.bundle +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +825 -0
  9. package/dist/dx/doctor.d.ts +62 -0
  10. package/dist/dx/doctor.js +127 -0
  11. package/dist/governance/dump.d.ts +47 -0
  12. package/dist/governance/dump.js +17 -0
  13. package/dist/host/facade.d.ts +13 -0
  14. package/dist/host/facade.js +154 -0
  15. package/dist/host/hostGateway.d.ts +117 -0
  16. package/dist/host/hostGateway.js +65 -0
  17. package/dist/host/mappings.d.ts +27 -0
  18. package/dist/host/mappings.js +52 -0
  19. package/dist/host/stdio.d.ts +14 -0
  20. package/dist/host/stdio.js +83 -0
  21. package/dist/kernel/context.d.ts +76 -0
  22. package/dist/kernel/context.js +203 -0
  23. package/dist/kernel/events.d.ts +37 -0
  24. package/dist/kernel/events.js +82 -0
  25. package/dist/load/config.d.ts +79 -0
  26. package/dist/load/config.js +285 -0
  27. package/dist/load/loader.d.ts +72 -0
  28. package/dist/load/loader.js +162 -0
  29. package/dist/load/registry.d.ts +44 -0
  30. package/dist/load/registry.js +58 -0
  31. package/dist/load/report.d.ts +98 -0
  32. package/dist/load/report.js +301 -0
  33. package/dist/load/sigGate.d.ts +22 -0
  34. package/dist/load/sigGate.js +68 -0
  35. package/dist/load/sign.d.ts +25 -0
  36. package/dist/load/sign.js +47 -0
  37. package/dist/load/verifier.d.ts +35 -0
  38. package/dist/load/verifier.js +56 -0
  39. package/dist/loop/goal.d.ts +31 -0
  40. package/dist/loop/goal.js +34 -0
  41. package/dist/loop/recover.d.ts +17 -0
  42. package/dist/loop/recover.js +39 -0
  43. package/dist/loop/stop.d.ts +82 -0
  44. package/dist/loop/stop.js +184 -0
  45. package/dist/mcp/gateway.d.ts +96 -0
  46. package/dist/mcp/gateway.js +69 -0
  47. package/dist/mcp/httpClient.d.ts +27 -0
  48. package/dist/mcp/httpClient.js +196 -0
  49. package/dist/mcp/stdioClient.d.ts +8 -0
  50. package/dist/mcp/stdioClient.js +73 -0
  51. package/dist/ptc/budget.d.ts +21 -0
  52. package/dist/ptc/budget.js +36 -0
  53. package/dist/ptc/erasable.d.ts +14 -0
  54. package/dist/ptc/erasable.js +27 -0
  55. package/dist/ptc/runCode.d.ts +65 -0
  56. package/dist/ptc/runCode.js +140 -0
  57. package/dist/ptc/schemaRender.d.ts +15 -0
  58. package/dist/ptc/schemaRender.js +90 -0
  59. package/dist/ptc/sdk.d.ts +27 -0
  60. package/dist/ptc/sdk.js +23 -0
  61. package/dist/ptc/worker-entry.d.ts +1 -0
  62. package/dist/ptc/worker-entry.js +71 -0
  63. package/dist/release-audit.jsonl +4 -0
  64. package/dist/runtime-core/chatStep.d.ts +43 -0
  65. package/dist/runtime-core/chatStep.js +121 -0
  66. package/dist/runtime-core/credentials.d.ts +49 -0
  67. package/dist/runtime-core/credentials.js +118 -0
  68. package/dist/runtime-core/errors.d.ts +30 -0
  69. package/dist/runtime-core/errors.js +36 -0
  70. package/dist/runtime-core/llm.d.ts +76 -0
  71. package/dist/runtime-core/llm.js +638 -0
  72. package/dist/runtime-core/redaction.d.ts +39 -0
  73. package/dist/runtime-core/redaction.js +79 -0
  74. package/dist/runtime-core/telemetry.d.ts +88 -0
  75. package/dist/runtime-core/telemetry.js +332 -0
  76. package/dist/runtime-core/tools.d.ts +15 -0
  77. package/dist/runtime-core/tools.js +12 -0
  78. package/dist/runtime-core/trace.d.ts +55 -0
  79. package/dist/runtime-core/trace.js +137 -0
  80. package/dist/runtime-core/types.d.ts +104 -0
  81. package/dist/runtime-core/types.js +13 -0
  82. package/dist/sandbox/sandbox.d.ts +68 -0
  83. package/dist/sandbox/sandbox.js +153 -0
  84. package/dist/sbom.json +263 -0
  85. package/dist/seam.d.ts +24 -0
  86. package/dist/seam.js +23 -0
  87. package/dist/security/secrets-baseline.d.ts +43 -0
  88. package/dist/security/secrets-baseline.js +176 -0
  89. package/dist/security/secrets.d.ts +45 -0
  90. package/dist/security/secrets.js +138 -0
  91. package/dist/session/export.d.ts +55 -0
  92. package/dist/session/export.js +81 -0
  93. package/dist/session/fork.d.ts +51 -0
  94. package/dist/session/fork.js +63 -0
  95. package/dist/session/format.d.ts +21 -0
  96. package/dist/session/format.js +49 -0
  97. package/dist/session/indexStore.d.ts +44 -0
  98. package/dist/session/indexStore.js +265 -0
  99. package/dist/session/log.d.ts +79 -0
  100. package/dist/session/log.js +157 -0
  101. package/dist/session/store.d.ts +9 -0
  102. package/dist/session/store.js +37 -0
  103. package/dist/telemetry/metrics.d.ts +25 -0
  104. package/dist/telemetry/metrics.js +26 -0
  105. package/package.json +17 -1
  106. package/src/authz/authz.ts +12 -0
  107. package/src/loop/stop.ts +43 -10
  108. package/src/runtime-core/chatStep.ts +22 -2
  109. package/src/runtime-core/llm.ts +96 -13
  110. package/src/runtime-core/types.ts +22 -1
  111. package/src/seam.ts +24 -0
  112. package/src/session/log.ts +1 -1
  113. package/.gitattributes +0 -11
  114. package/.github/workflows/ci.yml +0 -293
  115. package/.github/workflows/release.yml +0 -54
  116. package/adr/ADR-001.md +0 -4
  117. package/adr/ADR-002.md +0 -5
  118. package/adr/ADR-003.md +0 -35
  119. package/adr/host-onboarding.md +0 -68
  120. package/adr/mlps-audit-checklist.md +0 -41
  121. package/adr/win32-spike.md +0 -45
  122. package/docs/plugin-authoring.md +0 -82
  123. package/native/landlock-run/Makefile +0 -19
  124. package/native/landlock-run/landlock-run.c +0 -162
  125. package/scripts/contract-check.ts +0 -56
  126. package/scripts/invariant-n1.ts +0 -96
  127. package/scripts/package-check.ts +0 -29
  128. package/scripts/ptc-baseline/run-baseline.ts +0 -118
  129. package/scripts/release-artifacts.ts +0 -111
  130. package/scripts/release-pipeline.ts +0 -245
  131. package/scripts/secrets-baseline.ts +0 -33
  132. package/scripts/secrets-scan.ts +0 -56
  133. package/scripts/win32-ffi-debug.ts +0 -143
  134. package/scripts/win32-koffi-spike.ts +0 -537
  135. package/test/contract/eventmap.golden.json +0 -7
  136. package/test/fixtures/hello-plugin.ts +0 -5
  137. package/test/kernel.spec.ts +0 -136
  138. package/test/registry-e2e/run-e2e.ts +0 -213
  139. package/test/s11.spec.ts +0 -129
  140. package/test/s12.spec.ts +0 -135
  141. package/test/s13.spec.ts +0 -117
  142. package/test/s14.spec.ts +0 -117
  143. package/test/s15.spec.ts +0 -80
  144. package/test/s16.spec.ts +0 -133
  145. package/test/s17.spec.ts +0 -102
  146. package/test/s18.spec.ts +0 -81
  147. package/test/s19.spec.ts +0 -138
  148. package/test/s2.spec.ts +0 -245
  149. package/test/s20.spec.ts +0 -241
  150. package/test/s21.spec.ts +0 -231
  151. package/test/s22-m7-store-format.spec.ts +0 -135
  152. package/test/s23-m7-export-index.spec.ts +0 -189
  153. package/test/s24-signature-gate.spec.ts +0 -185
  154. package/test/s25-m8-runtime-core.spec.ts +0 -600
  155. package/test/s26-signature-closeout.spec.ts +0 -349
  156. package/test/s27-host-load-wiring.spec.ts +0 -134
  157. package/test/s28-session-index-followup.spec.ts +0 -236
  158. package/test/s29-otel-producer.spec.ts +0 -252
  159. package/test/s3.spec.ts +0 -179
  160. package/test/s30-release-artifacts.spec.ts +0 -127
  161. package/test/s31-real-model-wiring.spec.ts +0 -329
  162. package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
  163. package/test/s33-anthropic-adapter.spec.ts +0 -354
  164. package/test/s34-mcp-remote-transport.spec.ts +0 -250
  165. package/test/s35-config-ts-zod.spec.ts +0 -145
  166. package/test/s36-host-tool-surface.spec.ts +0 -250
  167. package/test/s37-projection-dedupe.spec.ts +0 -161
  168. package/test/s38-config-jiti.spec.ts +0 -116
  169. package/test/s39-otlp-span-semantics.spec.ts +0 -100
  170. package/test/s4.spec.ts +0 -169
  171. package/test/s40-metrics-cumulative.spec.ts +0 -67
  172. package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
  173. package/test/s42-traceparent-propagation.spec.ts +0 -415
  174. package/test/s43-remote-http-traceparent.spec.ts +0 -262
  175. package/test/s5.spec.ts +0 -95
  176. package/test/s6.spec.ts +0 -169
  177. package/test/s7.spec.ts +0 -136
  178. package/test/s7b-baseline.spec.ts +0 -20
  179. package/test/s9.spec.ts +0 -89
  180. package/test/sandbox-escape/j17-orchestrate.sh +0 -30
  181. package/test/sandbox-escape/run-matrix.sh +0 -98
  182. package/tsconfig.json +0 -18
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 ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * KernelSeam 公开导出面(1.10.0 加法——D16 T-6 冻结面口径:本文件即内核库形态的 semver 承诺面)
3
+ *
4
+ * 消费方:CAR 平台层(KernelSeam 适配器);约定:
5
+ * - 仅暴露进程内挂接所需的最窄面(装配/会话/turn/遥测),禁止透出内核内部模块路径语义;
6
+ * - 破坏性变更须内核 major;加法演进走 minor 并在本文件头登记;
7
+ * - 语义不变:runTurn/chatStep/SessionLog 即 CLI 与测试承载的同一实现(非转发壳)。
8
+ *
9
+ * 加法登记:1.11.0 —— AuthzService/checkCapability 出面(平台确认桥幂等决策 + 会话策略直判承载;F4 US-3 语义同源)。
10
+ */
11
+ export { SessionLog, type SessionEvent, type EventKind, type Actor } from './session/log.ts'
12
+ export { SessionFileStore } from './session/store.ts'
13
+ export { runTurn, type TurnEndReason, type TurnPreset, type TurnResult } from './loop/stop.ts'
14
+ export { chatStep } from './runtime-core/chatStep.ts'
15
+ export { RuntimeCore, createOpenAICompatAdapter, createAnthropicAdapter } from './runtime-core/llm.ts'
16
+ export { CredentialService } from './runtime-core/credentials.ts'
17
+ export type { LlmAdapter, LlmRequest, LlmChunk } from './runtime-core/types.ts'
18
+ export { mountPlugin } from './load/loader.ts'
19
+ export { Context } from './kernel/context.ts'
20
+ export { loadCarConfig, mergeLlmConfig, mergeSignatureGate, warmCarConfig, type CarConfig, type LlmConfig } from './load/config.ts'
21
+ export { probeCapabilities, SandboxExecutor } from './sandbox/sandbox.ts'
22
+ export { AuthzService, checkCapability, type Decision, type AuthzRequest, type AuthzDecision, type DeclaredCapabilities } from './authz/authz.ts'
23
+ export { createTelemetryFacade, telemetryConfigFromEnv } from './runtime-core/telemetry.ts'
24
+ export { TurnTracer, type TurnSpanHandle } 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
package/.gitattributes DELETED
@@ -1,11 +0,0 @@
1
- # 行尾纪律:Windows runner checkout 默认 autocrlf=true 会把文本文件打成 CRLF,
2
- # WSL 内 bash 脚本遇 \r 即炸(run 35314482866:set: pipefail\r invalid option)。
3
- * text=auto
4
- *.sh text eol=lf
5
- *.c text eol=lf
6
- *.h text eol=lf
7
- Makefile text eol=lf
8
- *.md text eol=lf
9
- *.yml text eol=lf
10
- *.json text eol=lf
11
- *.ts text eol=lf