@lqc123qwe/car-runtime 1.3.0 → 1.4.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 +3 -2
- package/package.json +1 -1
- package/src/cli.ts +158 -12
- package/src/dx/doctor.ts +49 -2
- package/src/load/config.ts +118 -2
- package/src/load/loader.ts +21 -1
- package/src/loop/stop.ts +12 -1
- package/src/mcp/stdioClient.ts +75 -0
- package/src/ptc/runCode.ts +10 -0
- package/src/runtime-core/chatStep.ts +10 -2
- package/src/runtime-core/llm.ts +17 -1
- package/src/runtime-core/telemetry.ts +42 -6
- package/src/runtime-core/tools.ts +27 -0
- package/src/runtime-core/trace.ts +63 -0
- package/src/runtime-core/types.ts +4 -1
- package/src/session/log.ts +3 -2
- package/test/contract/eventmap.golden.json +1 -1
- package/test/s26-signature-closeout.spec.ts +1 -1
- package/test/s28-session-index-followup.spec.ts +1 -1
- package/test/s31-real-model-wiring.spec.ts +329 -0
- package/test/s32-telemetry-mcp-ptc.spec.ts +218 -0
package/src/ptc/runCode.ts
CHANGED
|
@@ -116,6 +116,16 @@ export function makePtcToolDefinition(opts: { tools: Map<string, ToolBridge>; au
|
|
|
116
116
|
return {
|
|
117
117
|
name: 'run_code',
|
|
118
118
|
description: '执行一段 erasable TypeScript 程序(PTC)——程序体内经 tools.<name>(args) 调用已注册工具',
|
|
119
|
+
// 1.4-S7:run_code 入参 schema(进模型声明面——此前无 parameters,模型看不到 code/description 形状)
|
|
120
|
+
parameters: {
|
|
121
|
+
type: 'object',
|
|
122
|
+
properties: {
|
|
123
|
+
code: { type: 'string', description: 'erasable TS 程序体(async 函数体;经 tools.<name>(args) 调用已注册工具)' },
|
|
124
|
+
description: { type: 'string', description: '本段程序的一句话描述(审计留痕)' },
|
|
125
|
+
},
|
|
126
|
+
required: ['code', 'description'],
|
|
127
|
+
additionalProperties: false,
|
|
128
|
+
},
|
|
119
129
|
// T-22 语义:PTC 与 bash 同信任级别 → 强制 write 最高约束(不可声明为 readonly)
|
|
120
130
|
declaredSideEffect: 'write' as const,
|
|
121
131
|
run: async (args: { code: string; description: string; toolCallId?: string }) => {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* 由 runTurn 落 turnEnd);工具调用事件由 runTurn executeBatch 落(本层不重复)。
|
|
16
16
|
*/
|
|
17
17
|
import type { SessionLog } from '../session/log.ts'
|
|
18
|
-
import type
|
|
18
|
+
import { TurnAborted, type ModelStep, type ToolCall } from '../loop/stop.ts'
|
|
19
19
|
import type { RuntimeCore } from './llm.ts'
|
|
20
20
|
import type { ToolDefinition } from './types.ts'
|
|
21
21
|
import { StreamRedactor } from './redaction.ts'
|
|
@@ -32,6 +32,8 @@ export interface ChatStepOptions {
|
|
|
32
32
|
tools: ToolDefinition[]
|
|
33
33
|
maxTokens?: number
|
|
34
34
|
adapterId?: string
|
|
35
|
+
/** 1.4-S4(D-19):turn 取消信号——chunk 间隙与流结束后(finish 检查前)检查,命中抛 TurnAborted */
|
|
36
|
+
signal?: { aborted: boolean }
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/** ModelStep 加法扩展:脱敏计数留痕(S15 secretsRedacted 同名口径) */
|
|
@@ -49,7 +51,7 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
|
|
|
49
51
|
// 2. 请求消息 = 投影(角色系统:user/assistant/toolResult 与 LlmRequest 对齐;
|
|
50
52
|
// assistant 工具调用以 { toolCall } content 形态原样透传——deriveMessages 投影即模型可见流)
|
|
51
53
|
const messages = log.deriveMessages().map(m => ({
|
|
52
|
-
role: m.role as 'user' | 'assistant' | 'toolResult',
|
|
54
|
+
role: m.role as 'system' | 'user' | 'assistant' | 'toolResult',
|
|
53
55
|
content: m.content,
|
|
54
56
|
}))
|
|
55
57
|
|
|
@@ -66,9 +68,12 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
|
|
|
66
68
|
tools,
|
|
67
69
|
...(maxTokens != null ? { maxTokens } : {}),
|
|
68
70
|
...(adapterId ? { adapterId } : {}),
|
|
71
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
69
72
|
...(opts.log.sessionId ? { metadata: { sessionId: opts.log.sessionId, turnId, traceId: `${opts.log.sessionId}:${turnId}` } } : {}),
|
|
70
73
|
})
|
|
71
74
|
for await (const chunk of stream) {
|
|
75
|
+
// 1.4-S4(D-19):chunk 间隙取消——此刻 assistant 尚未落链(流完才 append),无半截消息
|
|
76
|
+
if (opts.signal?.aborted) throw new TurnAborted()
|
|
72
77
|
if (chunk.delta !== undefined) text += redactor.push(chunk.delta)
|
|
73
78
|
if (chunk.toolCallDelta) {
|
|
74
79
|
const idx = chunk.toolCallDelta.index ?? 0
|
|
@@ -81,6 +86,9 @@ export async function chatStep(opts: ChatStepOptions): Promise<ChatStepResult> {
|
|
|
81
86
|
if (chunk.error) errorDetail = chunk.error.message
|
|
82
87
|
if (chunk.finishReason !== undefined) stopReason = chunk.finishReason === 'aborted' ? 'error' : chunk.finishReason
|
|
83
88
|
}
|
|
89
|
+
// 1.4-S4(D-19):finish 检查前先查取消(适配器取消路径静默收口无 finishReason——
|
|
90
|
+
// 必须先于此处的契约兜底,否则取消被误判为适配器违规)
|
|
91
|
+
if (opts.signal?.aborted) throw new TurnAborted()
|
|
84
92
|
if (stopReason === undefined) {
|
|
85
93
|
// 适配器层已保证首 finishReason(AL-05);防御性兜底显式化
|
|
86
94
|
throw new Error('CAR-E-LLM-FINISH: 流结束无 finishReason(适配器契约违规)')
|
package/src/runtime-core/llm.ts
CHANGED
|
@@ -199,7 +199,13 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
|
|
|
199
199
|
const id = opts.id ?? 'openai-compat'
|
|
200
200
|
const baseUrl = opts.baseUrl.replace(/\/+$/, '')
|
|
201
201
|
if (!baseUrl.startsWith('https://')) {
|
|
202
|
-
|
|
202
|
+
// 1.4-S4(D-12a,1.2 规划期裁决):http 仅豁免 loopback——本地模型服务(Ollama 等)与
|
|
203
|
+
// 本地 E2E;非回环地址维持 TLS 强制(§3.2.M8.4)。豁免集显式列举,不扩大到局域网。
|
|
204
|
+
let loopback = false
|
|
205
|
+
try { const host = new URL(baseUrl).hostname.replace(/^\[|\]$/g, ''); loopback = host === '127.0.0.1' || host === 'localhost' || host === '::1' } catch { /* 解析失败 = 非法 URL,按非回环拒绝 */ }
|
|
206
|
+
if (!loopback) {
|
|
207
|
+
throw new Error(`CAR-E-LLM-TLS: baseUrl 必须 https(TLS 强制,§3.2.M8.4);http 仅豁免 loopback(127.0.0.1/[::1]/localhost,D-12a)——远程明文部署请经 TLS 代理或自建适配器`)
|
|
208
|
+
}
|
|
203
209
|
}
|
|
204
210
|
const url = `${baseUrl}/chat/completions`
|
|
205
211
|
const fetchImpl = opts.fetchImpl ?? fetch
|
|
@@ -210,6 +216,9 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
|
|
|
210
216
|
const backoffMs = opts.backoffMs ?? [1_000, 2_000]
|
|
211
217
|
|
|
212
218
|
const toOpenAI = (req: LlmRequest, apiKey: string) => ({
|
|
219
|
+
// 1.4-BUG-1:method: 'POST' 缺失——M8 最小实现潜伏缺陷(注入式 fetch 的单测忽略 method,
|
|
220
|
+
// 真 undici 对 GET+body 直接拒绝);car run 真路径接线时暴露(生产调用点核查产出)
|
|
221
|
+
method: 'POST',
|
|
213
222
|
headers: {
|
|
214
223
|
'content-type': 'application/json',
|
|
215
224
|
authorization: `Bearer ${apiKey}`,
|
|
@@ -250,6 +259,8 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
|
|
|
250
259
|
const payload = toOpenAI(req, apiKey)
|
|
251
260
|
|
|
252
261
|
for (let attempt = 0; ; attempt++) {
|
|
262
|
+
// 1.4-S4(D-19):外部取消 ≠ 失败——attempt 前检查,静默收口(无 chunk、无 retry)
|
|
263
|
+
if (req.signal?.aborted) return
|
|
253
264
|
const controller = new AbortController()
|
|
254
265
|
const started = Date.now()
|
|
255
266
|
let firstByteSeen = false
|
|
@@ -267,6 +278,9 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
|
|
|
267
278
|
if (!res.body) throw new Retryable('响应无 body 流')
|
|
268
279
|
let sawFinish = false
|
|
269
280
|
for await (const chunk of parseSseStream(res.body)) {
|
|
281
|
+
// 1.4-S4(D-19):chunk 间隙检查取消——abort 连接并静默收口(不产 error chunk,
|
|
282
|
+
// 不违反 finishReason 不可变;上层 chatStep 以自身 signal 检查抛 TurnAborted)
|
|
283
|
+
if (req.signal?.aborted) { controller.abort(); return }
|
|
270
284
|
if (!firstByteSeen) { firstByteSeen = true; clearTimeout(firstByteTimer) }
|
|
271
285
|
if (chunk.finishReason !== undefined) sawFinish = true
|
|
272
286
|
yield chunk
|
|
@@ -279,6 +293,8 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
|
|
|
279
293
|
}
|
|
280
294
|
return
|
|
281
295
|
} catch (e) {
|
|
296
|
+
// 1.4-S4(D-19):外部取消(abort 连锁的 fetch 异常)≠ 失败——静默收口,不走 retry
|
|
297
|
+
if (req.signal?.aborted) return
|
|
282
298
|
const beforeFirstByte = !firstByteSeen
|
|
283
299
|
clearTimeout(firstByteTimer)
|
|
284
300
|
// 首块前失败 = 网络错误/429/5xx/首字节超时(§3.5.4 重试条件全集)——未消费任何
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* 口径(§3.2.M8.5 关键约束,逐条落地):
|
|
5
5
|
* - 默认关:无端点配置时不初始化任何 exporter——无配置 = noop 句柄 + 零出站(可机器断言);
|
|
6
6
|
* - 显式开:用户配置端点即视为开启(CAR_OTEL_ENDPOINT 或显式 config 传入);
|
|
7
|
-
* - 端点归用户:数据只发往用户自配 E-05;OTLP/HTTP JSON(
|
|
7
|
+
* - 端点归用户:数据只发往用户自配 E-05;OTLP/HTTP JSON(node:http(s) 出站——1.4-BUG-2:全局
|
|
8
|
+
* fetch/undici 在 win32 进程退出时触发 libuv 断言崩溃),导出超时 5s;
|
|
8
9
|
* 1.2-S3 生产级策略(D-12b 口径修订,2026-09-27):**0 重试 → 有界重试 ≤2**(仅 429/5xx/
|
|
9
10
|
* 网络错误,退避 1s/2s;4xx 业务错不重试)——重试耗尽静默丢弃 + droppedExports 计数,
|
|
10
11
|
* 永不抛错(BD-05「不抛错、不落审计」收紧保持);隐私三原则不变(数据仍只发用户自配端点);
|
|
@@ -22,6 +23,8 @@
|
|
|
22
23
|
* 不引入 @opentelemetry/* 依赖(项目零依赖红线,README engines 纪律)。
|
|
23
24
|
*/
|
|
24
25
|
import { randomBytes } from 'node:crypto'
|
|
26
|
+
import { request as httpRequest } from 'node:http'
|
|
27
|
+
import { request as httpsRequest } from 'node:https'
|
|
25
28
|
|
|
26
29
|
export type SamplingSpec = 'always_on' | 'always_off' | { ratio: number }
|
|
27
30
|
|
|
@@ -55,10 +58,16 @@ export interface TelemetrySpan {
|
|
|
55
58
|
recordException(err: unknown): this
|
|
56
59
|
end(): void
|
|
57
60
|
readonly traceId: string
|
|
61
|
+
/** 1.4-S6:子 span 挂链需要父 spanId(TurnTracer turn→step 层级) */
|
|
62
|
+
readonly spanId: string
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
export interface TelemetryTracer {
|
|
61
|
-
startSpan(name: string, opts?: {
|
|
66
|
+
startSpan(name: string, opts?: {
|
|
67
|
+
attributes?: Record<string, string | number | boolean>
|
|
68
|
+
/** 1.4-S6:trace 透传(traceId 复用 + parentSpanId 挂链);缺省 = 新独立 trace */
|
|
69
|
+
trace?: { traceId: string; parentSpanId?: string }
|
|
70
|
+
}): TelemetrySpan
|
|
62
71
|
}
|
|
63
72
|
|
|
64
73
|
export interface TelemetryCounter {
|
|
@@ -102,6 +111,7 @@ const noopSpan = (): TelemetrySpan => ({
|
|
|
102
111
|
recordException() { return noopSpan() },
|
|
103
112
|
end() {},
|
|
104
113
|
traceId: '',
|
|
114
|
+
spanId: '',
|
|
105
115
|
})
|
|
106
116
|
const NOOP_TRACER: TelemetryTracer = { startSpan: () => noopSpan() }
|
|
107
117
|
const NOOP_METER: TelemetryMeter = { createCounter: () => ({ add() {} }) }
|
|
@@ -164,7 +174,25 @@ export function createTelemetryFacade(
|
|
|
164
174
|
}
|
|
165
175
|
}
|
|
166
176
|
|
|
167
|
-
|
|
177
|
+
// 1.4-BUG-2:出站默认走 node:http(s)(非全局 fetch/undici)——win32 上 undici socket +
|
|
178
|
+
// 进程退出触发 libuv 断言崩溃(UV_HANDLE_CLOSING, 0xC0000409,Node 23.5 实测、22 不复现);
|
|
179
|
+
// 测试仍可注入 fetchImpl(缝保留)。res.resume() 排空响应体(连接干净归还)。
|
|
180
|
+
const fetchImpl = opts.fetchImpl ?? ((url: string | URL, init?: RequestInit): Promise<{ ok: boolean; status: number }> => new Promise(resolve => {
|
|
181
|
+
try {
|
|
182
|
+
const u = String(url)
|
|
183
|
+
const mod = u.startsWith('https:') ? httpsRequest : httpRequest
|
|
184
|
+
const req = mod(u, { method: init?.method ?? 'GET', headers: init?.headers as Record<string, string> | undefined }, res => {
|
|
185
|
+
res.resume()
|
|
186
|
+
res.on('end', () => {
|
|
187
|
+
const st = res.statusCode ?? 0
|
|
188
|
+
resolve({ ok: st >= 200 && st < 300, status: st })
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
req.setTimeout(timeoutMs, () => req.destroy(new Error('timeout')))
|
|
192
|
+
req.on('error', () => resolve({ ok: false, status: 599 })) // 网络/超时错误 → 599 可重试类
|
|
193
|
+
req.end(init?.body ?? null)
|
|
194
|
+
} catch { resolve({ ok: false, status: 599 }) }
|
|
195
|
+
}))
|
|
168
196
|
const now = opts.now ?? Date.now
|
|
169
197
|
const sleep = config?.sleep ?? (ms => new Promise<void>(r => setTimeout(r, ms)))
|
|
170
198
|
const timeoutMs = config?.exportTimeoutMs ?? 5_000
|
|
@@ -194,12 +222,16 @@ export function createTelemetryFacade(
|
|
|
194
222
|
*/
|
|
195
223
|
const exportOtlp = async (path: string, body: unknown): Promise<boolean> => {
|
|
196
224
|
for (let attempt = 0; ; attempt++) {
|
|
225
|
+
// 1.4-BUG-2:AbortSignal.timeout 的内部定时器在进程退出时触发 libuv win32 断言崩溃
|
|
226
|
+
// (UV_HANDLE_CLOSING, 0xC0000409)——改手动 AbortController + clearTimeout 确定性清理
|
|
227
|
+
const controller = new AbortController()
|
|
228
|
+
const abortTimer = setTimeout(() => controller.abort(), timeoutMs)
|
|
197
229
|
try {
|
|
198
230
|
const res = await fetchImpl(endpoint + path, {
|
|
199
231
|
method: 'POST',
|
|
200
232
|
headers: baseHeaders,
|
|
201
233
|
body: JSON.stringify(body),
|
|
202
|
-
signal:
|
|
234
|
+
signal: controller.signal,
|
|
203
235
|
})
|
|
204
236
|
if (res.ok) return true
|
|
205
237
|
if ((res.status === 429 || res.status >= 500) && attempt < retryLimit) {
|
|
@@ -214,6 +246,8 @@ export function createTelemetryFacade(
|
|
|
214
246
|
continue
|
|
215
247
|
}
|
|
216
248
|
return false
|
|
249
|
+
} finally {
|
|
250
|
+
clearTimeout(abortTimer)
|
|
217
251
|
}
|
|
218
252
|
}
|
|
219
253
|
}
|
|
@@ -277,9 +311,10 @@ export function createTelemetryFacade(
|
|
|
277
311
|
const activeTracer: TelemetryTracer = {
|
|
278
312
|
startSpan(name, startOpts) {
|
|
279
313
|
const record: SpanRecord = {
|
|
280
|
-
traceId
|
|
314
|
+
// 1.4-S6:trace 透传(traceId 复用 + parentSpanId 挂链)——缺省仍为独立随机 trace(1.1 行为不变)
|
|
315
|
+
traceId: startOpts?.trace?.traceId ?? randomBytes(16).toString('hex'),
|
|
281
316
|
spanId: randomBytes(8).toString('hex'),
|
|
282
|
-
parentSpanId: null,
|
|
317
|
+
parentSpanId: startOpts?.trace?.parentSpanId ?? null,
|
|
283
318
|
name,
|
|
284
319
|
startMs: now(),
|
|
285
320
|
endMs: 0,
|
|
@@ -287,6 +322,7 @@ export function createTelemetryFacade(
|
|
|
287
322
|
}
|
|
288
323
|
return {
|
|
289
324
|
get traceId() { return record.traceId },
|
|
325
|
+
get spanId() { return record.spanId },
|
|
290
326
|
setAttribute(key, value) { record.attributes[key] = value; return this },
|
|
291
327
|
addEvent(evtName, attrs) { record.attributes[`event.${evtName}`] = attrs ? JSON.stringify(attrs) : evtName; return this },
|
|
292
328
|
recordException(err) { record.attributes['exception.message'] = String((err as Error)?.message ?? err); return this },
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1.4-S1 · 工具声明面转换(执行面 Map → 模型声明面 ToolDefinition[])
|
|
3
|
+
*
|
|
4
|
+
* 口径(1.4 规划 W1-3 / D-18 / T-22 / 声明面=授权面红线):
|
|
5
|
+
* - **执行面与声明面同一对象**:ToolDef(loop/stop.ts,declaredSideEffect+run)即声明来源——
|
|
6
|
+
* 插件注册的 ToolReg 经 bindCore 收集后直接进本转换器,模型看到的工具集合 = 授权门放行的集合
|
|
7
|
+
* (多渲一个 = 无授权执行面,少渲一个 = 授权白给——PTC SDK 同款红线);
|
|
8
|
+
* - T-22 收口:declaredSideEffect 缺省 write(未声明按最高约束——mcp/gateway.ts 注册点同款);
|
|
9
|
+
* - 跨源同名显式报错(禁静默覆盖):Map 键即名字,插件/MCP 工具合并由调用方先行去重——
|
|
10
|
+
* 本转换器对重名(Map 键重复不可能,但 ToolDef.name 与键不一致时)显式报错;
|
|
11
|
+
* - declaredSideEffect 不出站(llm.ts 出站映射只带 name/description/parameters——权限面只进权限门)。
|
|
12
|
+
*/
|
|
13
|
+
import type { ToolDef } from '../loop/stop.ts'
|
|
14
|
+
import type { ToolDefinition } from './types.ts'
|
|
15
|
+
|
|
16
|
+
export function toToolDefinitions(tools: Map<string, ToolDef>): ToolDefinition[] {
|
|
17
|
+
const out: ToolDefinition[] = []
|
|
18
|
+
for (const [name, t] of tools) {
|
|
19
|
+
out.push({
|
|
20
|
+
name,
|
|
21
|
+
...(t.description !== undefined ? { description: t.description } : {}),
|
|
22
|
+
...(t.parameters !== undefined ? { parameters: t.parameters } : {}),
|
|
23
|
+
declaredSideEffect: t.declaredSideEffect ?? 'write',
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
return out
|
|
27
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1.4-S6 · Turn 级 trace 面(W1 遥测未入链收尾):turn span 包全程、step span 挂每次 model() 调用
|
|
3
|
+
*
|
|
4
|
+
* 口径(1.4 规划 W1 遥测块 / telemetry.ts 头注释):
|
|
5
|
+
* - 层级:turn(根 span,traceId 在此生成)→ step(parentSpanId = turn.spanId)——同一 turn 共享 trace;
|
|
6
|
+
* - **挂点在装配层**(car run 的 model 回调闭包),loop 层零改动——遥测治理属调用方(telemetry.ts
|
|
7
|
+
* 「内容治理属调用方」口径);设计微调登记:规划原文「runTurn opts.telemetry 注入面」改为装配层
|
|
8
|
+
* 闭包挂点,语义等价(turn/step 层级与出站面不变),loop 层保持纯停止语义;
|
|
9
|
+
* - attributes 零内容面:turn_id/step 序号/outcome 枚举——无会话内容/提示词/路径。
|
|
10
|
+
*/
|
|
11
|
+
import { randomBytes } from 'node:crypto'
|
|
12
|
+
import type { TelemetryFacade } from './telemetry.ts'
|
|
13
|
+
|
|
14
|
+
export interface StepSpanHandle {
|
|
15
|
+
end(outcome: string): void
|
|
16
|
+
recordException(e: unknown): void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TurnSpanHandle {
|
|
20
|
+
traceId: string
|
|
21
|
+
spanId: string
|
|
22
|
+
end(outcome: string): void
|
|
23
|
+
stepSpan(step: number): StepSpanHandle
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class TurnTracer {
|
|
27
|
+
readonly #facade: TelemetryFacade | null
|
|
28
|
+
|
|
29
|
+
constructor(facade: TelemetryFacade | null | undefined) {
|
|
30
|
+
// 默认关(无端点)= noop 句柄(零出站零积累——telemetry 三原则)
|
|
31
|
+
this.#facade = facade?.enabled ? facade : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
startTurn(turnId: string): TurnSpanHandle {
|
|
35
|
+
if (!this.#facade) {
|
|
36
|
+
return { traceId: '', spanId: '', end() {}, stepSpan: () => ({ end() {}, recordException() {} }) }
|
|
37
|
+
}
|
|
38
|
+
const tracer = this.#facade.getTracer()
|
|
39
|
+
const traceId = randomBytes(16).toString('hex')
|
|
40
|
+
const turn = tracer.startSpan('car.turn', { attributes: { 'car.turn_id': turnId }, trace: { traceId } })
|
|
41
|
+
return {
|
|
42
|
+
traceId,
|
|
43
|
+
spanId: turn.spanId,
|
|
44
|
+
end(outcome) {
|
|
45
|
+
turn.setAttribute('car.outcome', outcome)
|
|
46
|
+
turn.end()
|
|
47
|
+
},
|
|
48
|
+
stepSpan(step: number): StepSpanHandle {
|
|
49
|
+
const s = tracer.startSpan('car.step', {
|
|
50
|
+
attributes: { 'car.turn_id': turnId, 'car.step': step },
|
|
51
|
+
trace: { traceId, parentSpanId: turn.spanId },
|
|
52
|
+
})
|
|
53
|
+
return {
|
|
54
|
+
end(outcome) {
|
|
55
|
+
s.setAttribute('car.outcome', outcome)
|
|
56
|
+
s.end()
|
|
57
|
+
},
|
|
58
|
+
recordException(e) { s.recordException(e) },
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -19,9 +19,12 @@ export interface LlmRequest {
|
|
|
19
19
|
tools: ToolDefinition[]
|
|
20
20
|
maxTokens?: number
|
|
21
21
|
metadata?: { sessionId: string; turnId: string; traceId: string }
|
|
22
|
-
// —— 规格外加法扩展(登记:M8 收口文档 §4)——
|
|
22
|
+
// —— 规格外加法扩展(登记:M8 收口文档 §4 / 1.4-S4)——
|
|
23
23
|
/** 多适配器在册时显式路由;缺省用默认适配器 */
|
|
24
24
|
adapterId?: string
|
|
25
|
+
/** 1.4-S4(D-19):turn 取消信号(runTurn 的 {aborted} 形态透传)——适配器在 attempt 前 /
|
|
26
|
+
* chunk 间隙检查;外部取消 ≠ 失败(不 retry、不产 error chunk,静默收口由 chatStep 抛 TurnAborted) */
|
|
27
|
+
signal?: { aborted: boolean }
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/** 工具调用流式增量(跨 chunk 聚合出完整 ToolCall) */
|
package/src/session/log.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { createHash } from 'node:crypto'
|
|
|
10
10
|
import { readFileSync } from 'node:fs'
|
|
11
11
|
import { decodeLogBuffer, splitJsonlLines } from './format.ts'
|
|
12
12
|
|
|
13
|
-
export type EventKind = 'user' | 'assistant' | 'toolCall' | 'toolResult' | 'turnEnd' | 'goalUpdate' | 'hostRaw' | 'fork' // M3-S11 加法扩展:多宿主归一化降级通道(pattern 未命中留痕,零静默);M5-DEC4 加法扩展:跨宿主 fork
|
|
13
|
+
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
14
|
export type Actor = 'user' | 'model' | 'plugin' | 'runtime'
|
|
15
15
|
export interface SessionEvent {
|
|
16
16
|
seq: number
|
|
@@ -88,7 +88,8 @@ export class SessionLog {
|
|
|
88
88
|
const out: Array<{ role: string; [k: string]: unknown }> = []
|
|
89
89
|
for (const e of this.events) {
|
|
90
90
|
if (upTo !== undefined && e.seq >= upTo) break
|
|
91
|
-
if (e.kind === '
|
|
91
|
+
if (e.kind === 'system') out.push({ role: 'system', content: e.payload })
|
|
92
|
+
else if (e.kind === 'user') out.push({ role: 'user', content: e.payload })
|
|
92
93
|
else if (e.kind === 'assistant') out.push({ role: 'assistant', content: e.payload })
|
|
93
94
|
else if (e.kind === 'toolCall') out.push({ role: 'assistant', toolCall: e.payload })
|
|
94
95
|
else if (e.kind === 'toolResult') out.push({ role: 'toolResult', content: e.payload })
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"comment": "J-09 contract 基线:SessionEventMap/SessionEvent 签名字段集。只增不删——本文件中任何键从 src/session/log.ts 的当前签名中消失即 FAIL(M2部署设计增补 §2.3 J-09)",
|
|
3
|
-
"eventKinds": ["user", "assistant", "toolCall", "toolResult", "turnEnd", "goalUpdate", "hostRaw", "fork"],
|
|
3
|
+
"eventKinds": ["user", "assistant", "toolCall", "toolResult", "turnEnd", "goalUpdate", "hostRaw", "fork", "system"],
|
|
4
4
|
"actors": ["user", "model", "plugin", "runtime"],
|
|
5
5
|
"sessionEventFields": ["seq", "ts", "actor", "kind", "turnId", "payload", "meta", "prevHash", "hash"],
|
|
6
6
|
"sessionLogApi": ["append", "snapshotModelRequest", "verifyChain", "deriveMessages", "assertModelVisibleLogged", "stats", "fromEvents", "exportJSONL"]
|
|
@@ -273,7 +273,7 @@ test('S26: CLI 配置通道真实消费——car run 经 car.config.json 走 enf
|
|
|
273
273
|
assert.equal(r1.code, 1)
|
|
274
274
|
assert.match(r1.stderr, /签名门禁拒绝.*signature missing/)
|
|
275
275
|
// env 显式 0 压过配置 → warn 放行到装配段
|
|
276
|
-
const r2 = await car(['run', 'plugin.ts'], { cwd: dir, env: { CAR_SIG_ENFORCE: '0' } })
|
|
276
|
+
const r2 = await car(['run', 'plugin.ts', '--demo'], { cwd: dir, env: { CAR_SIG_ENFORCE: '0' } }) // 1.4 D-13:stub 流挂 --demo
|
|
277
277
|
assert.equal(r2.code, 0, r2.stderr)
|
|
278
278
|
assert.match(r2.stdout, /\[1\/5 装配\] OK/)
|
|
279
279
|
// 坏配置 fail-visible 中止(exit 2,不进入装载)
|
|
@@ -213,7 +213,7 @@ test('S28: 生产调用点——car run 真进程 append → cwd sessions-index.
|
|
|
213
213
|
// 工具名避开 demo_tool:CLI 演示脚手架硬编码注册 demo_tool,同名插件工具触发预存在的 provide
|
|
214
214
|
// 撞名边缘(登记不修——W1 重做 car run 装配面时出清)
|
|
215
215
|
writeFileSync(join(dir, 'p.ts'), `export const manifest = { name: 'p', version: '1.0.0' }\nexport default function apply(api) {\n api.registerTool({ name: 't', run: async () => 'ok' })\n}\n`)
|
|
216
|
-
const r = await car(['run', 'p.ts'], { cwd: dir })
|
|
216
|
+
const r = await car(['run', 'p.ts', '--demo'], { cwd: dir }) // 1.4 D-13:stub 流挂 --demo
|
|
217
217
|
assert.equal(r.code, 0, r.stderr)
|
|
218
218
|
const dbPath = join(dir, 'sessions-index.db')
|
|
219
219
|
assert.ok(existsSync(dbPath), 'car run 运行后 cwd 索引自举在位')
|