niceeval 0.1.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 (94) hide show
  1. package/README.md +164 -0
  2. package/README.zh.md +160 -0
  3. package/bin/niceeval.js +11 -0
  4. package/package.json +96 -0
  5. package/src/agents/bub.ts +223 -0
  6. package/src/agents/builtin.ts +6 -0
  7. package/src/agents/claude-code.ts +96 -0
  8. package/src/agents/codex.ts +122 -0
  9. package/src/agents/index.ts +25 -0
  10. package/src/agents/shared.ts +145 -0
  11. package/src/cli.ts +421 -0
  12. package/src/context/context.test.ts +96 -0
  13. package/src/context/context.ts +427 -0
  14. package/src/context/control-flow.ts +27 -0
  15. package/src/context/session.ts +124 -0
  16. package/src/define.ts +112 -0
  17. package/src/expect/index.ts +243 -0
  18. package/src/i18n/en.ts +131 -0
  19. package/src/i18n/index.ts +39 -0
  20. package/src/i18n/zh-CN.ts +132 -0
  21. package/src/index.ts +39 -0
  22. package/src/loaders/index.ts +56 -0
  23. package/src/o11y/cost.ts +57 -0
  24. package/src/o11y/derive.ts +304 -0
  25. package/src/o11y/otlp/canonical.ts +112 -0
  26. package/src/o11y/otlp/mappers/bub.ts +30 -0
  27. package/src/o11y/otlp/mappers/codex.ts +38 -0
  28. package/src/o11y/otlp/mappers/index.ts +25 -0
  29. package/src/o11y/otlp/parse.ts +330 -0
  30. package/src/o11y/otlp/receiver.ts +100 -0
  31. package/src/o11y/otlp/sandbox-receiver.ts +113 -0
  32. package/src/o11y/otlp/select.ts +82 -0
  33. package/src/o11y/parsers/bub.ts +240 -0
  34. package/src/o11y/parsers/claude-code.ts +270 -0
  35. package/src/o11y/parsers/codex.ts +480 -0
  36. package/src/o11y/parsers/index.ts +37 -0
  37. package/src/o11y/prices.json +9873 -0
  38. package/src/runner/discover.ts +77 -0
  39. package/src/runner/reporters/artifacts.ts +81 -0
  40. package/src/runner/reporters/console.ts +76 -0
  41. package/src/runner/reporters/index.ts +5 -0
  42. package/src/runner/reporters/json.ts +52 -0
  43. package/src/runner/reporters/live.ts +178 -0
  44. package/src/runner/reporters/table.ts +328 -0
  45. package/src/runner/run.ts +860 -0
  46. package/src/runner/sandbox-prep.ts +91 -0
  47. package/src/sandbox/checkpoint.ts +39 -0
  48. package/src/sandbox/docker.ts +539 -0
  49. package/src/sandbox/e2b.ts +203 -0
  50. package/src/sandbox/index.ts +25 -0
  51. package/src/sandbox/registry.ts +60 -0
  52. package/src/sandbox/resolve.ts +131 -0
  53. package/src/sandbox/source-files.ts +30 -0
  54. package/src/sandbox/vercel.ts +236 -0
  55. package/src/scoring/collector.ts +113 -0
  56. package/src/scoring/judge.ts +236 -0
  57. package/src/scoring/scoped.ts +289 -0
  58. package/src/scoring/verdict.ts +20 -0
  59. package/src/source-loc.ts +53 -0
  60. package/src/types.ts +913 -0
  61. package/src/util.test.ts +31 -0
  62. package/src/util.ts +50 -0
  63. package/src/view/app/App.tsx +189 -0
  64. package/src/view/app/components/AttemptModal.tsx +66 -0
  65. package/src/view/app/components/CodeView.tsx +272 -0
  66. package/src/view/app/components/CopyControls.tsx +89 -0
  67. package/src/view/app/components/ExperimentTable.tsx +266 -0
  68. package/src/view/app/components/GroupSelector.tsx +61 -0
  69. package/src/view/app/components/LazyArtifact.tsx +50 -0
  70. package/src/view/app/components/Trace.tsx +100 -0
  71. package/src/view/app/components/Transcript.tsx +130 -0
  72. package/src/view/app/components/primitives.tsx +43 -0
  73. package/src/view/app/components/ui/badge.tsx +21 -0
  74. package/src/view/app/components/ui/dialog.tsx +34 -0
  75. package/src/view/app/components/ui/tabs.tsx +30 -0
  76. package/src/view/app/i18n.ts +341 -0
  77. package/src/view/app/index.html +13 -0
  78. package/src/view/app/lib/cn.ts +7 -0
  79. package/src/view/app/lib/format.ts +73 -0
  80. package/src/view/app/lib/guards.ts +61 -0
  81. package/src/view/app/lib/outcome.ts +96 -0
  82. package/src/view/app/lib/rows.ts +63 -0
  83. package/src/view/app/lib/transcript-data.tsx +121 -0
  84. package/src/view/app/main.tsx +17 -0
  85. package/src/view/app/pages/RunsPage.tsx +83 -0
  86. package/src/view/app/pages/TracesPage.tsx +40 -0
  87. package/src/view/app/shared.ts +10 -0
  88. package/src/view/app/types.ts +114 -0
  89. package/src/view/app/vite.config.ts +26 -0
  90. package/src/view/client-dist/app.css +2 -0
  91. package/src/view/client-dist/app.js +56 -0
  92. package/src/view/index.ts +406 -0
  93. package/src/view/styles.css +1074 -0
  94. package/src/view/template.html +15 -0
@@ -0,0 +1,480 @@
1
+ // OpenAI Codex CLI transcript 解析器(关键路径)。
2
+ // 两类输入都吃:
3
+ // 1. `codex exec --json` 打到 stdout 的事件 JSONL(thread.* / turn.* / item.* / response.*);
4
+ // 2. 磁盘上 session rollout JSONL(ResponseItem:message / function_call / reasoning ...)。
5
+ // 唯一硬活:把这堆五花八门的原始事件归一成 niceeval 的 StreamEvent[]。
6
+
7
+ import type { StreamEvent, Usage, ToolName, JsonValue } from "../../types.ts";
8
+ import type { ParsedTranscript } from "./index.ts";
9
+
10
+ // ───────────────────────── 工具名归一 ─────────────────────────
11
+
12
+ /** Codex 工具名 → 规范 ToolName。 */
13
+ function normalizeToolName(name: string): ToolName {
14
+ const toolMap: Record<string, ToolName> = {
15
+ // 文件
16
+ read_file: "file_read",
17
+ write_file: "file_write",
18
+ create_file: "file_write",
19
+ delete_file: "file_write",
20
+ edit_file: "file_edit",
21
+ patch_file: "file_edit",
22
+ apply_patch: "file_edit",
23
+ file_change: "file_edit",
24
+ update_plan: "agent_task",
25
+
26
+ // shell
27
+ shell: "shell",
28
+ bash: "shell",
29
+ exec: "shell",
30
+ execute: "shell",
31
+ run: "shell",
32
+ terminal: "shell",
33
+ command_execution: "shell",
34
+ local_shell: "shell",
35
+
36
+ // web
37
+ fetch: "web_fetch",
38
+ http_request: "web_fetch",
39
+ curl: "web_fetch",
40
+ web_fetch: "web_fetch",
41
+ web_search: "web_search",
42
+ search: "web_search",
43
+
44
+ // 检索 / 导航
45
+ glob: "glob",
46
+ find_files: "glob",
47
+ list_files: "glob",
48
+ grep: "grep",
49
+ search_files: "grep",
50
+ ripgrep: "grep",
51
+ ls: "list_dir",
52
+ list_directory: "list_dir",
53
+ dir: "list_dir",
54
+ };
55
+ return toolMap[name.toLowerCase()] || "unknown";
56
+ }
57
+
58
+ // ───────────────────────── 小工具 ─────────────────────────
59
+
60
+ /** 宽松取一个对象字段(原始 JSON 是 any,这里只做存在性收口)。 */
61
+ function get(obj: unknown, key: string): unknown {
62
+ return obj && typeof obj === "object" ? (obj as Record<string, unknown>)[key] : undefined;
63
+ }
64
+
65
+ /** 字符串则尝试 JSON.parse,失败原样返回;对象原样返回。 */
66
+ function coerceArgs(value: unknown): JsonValue {
67
+ if (typeof value === "string") {
68
+ try {
69
+ return JSON.parse(value) as JsonValue;
70
+ } catch {
71
+ return value;
72
+ }
73
+ }
74
+ return (value ?? {}) as JsonValue;
75
+ }
76
+
77
+ /** 从 content(string | block[])里抠出纯文本。 */
78
+ function extractText(content: unknown): string {
79
+ if (typeof content === "string") return content;
80
+ if (Array.isArray(content)) {
81
+ const parts: string[] = [];
82
+ for (const block of content) {
83
+ if (typeof block === "string") {
84
+ parts.push(block);
85
+ continue;
86
+ }
87
+ const t = get(block, "text") ?? get(block, "content") ?? get(block, "thinking");
88
+ if (typeof t === "string") parts.push(t);
89
+ }
90
+ return parts.join("\n");
91
+ }
92
+ return "";
93
+ }
94
+
95
+ /** 从 reasoning item 抠文本(text / content / summary[].text)。 */
96
+ function extractReasoning(item: unknown): string {
97
+ const direct = get(item, "text") ?? get(item, "content");
98
+ if (typeof direct === "string" && direct) return direct;
99
+ const summary = get(item, "summary");
100
+ if (Array.isArray(summary)) return extractText(summary);
101
+ return extractText(direct);
102
+ }
103
+
104
+ // ───────────────────────── usage 聚合 ─────────────────────────
105
+
106
+ /** 从一个 usage-like 对象读出增量(支持 input/output_tokens 与 prompt/completion_tokens 两套命名)。 */
107
+ function readUsage(u: unknown): { input: number; output: number; cacheRead: number } | null {
108
+ if (!u || typeof u !== "object") return null;
109
+ const o = u as Record<string, unknown>;
110
+ const num = (...keys: string[]): number => {
111
+ for (const k of keys) {
112
+ const v = o[k];
113
+ if (typeof v === "number" && Number.isFinite(v)) return v;
114
+ }
115
+ return 0;
116
+ };
117
+ const input = num("input_tokens", "prompt_tokens", "inputTokens", "promptTokens");
118
+ const output = num("output_tokens", "completion_tokens", "outputTokens", "completionTokens");
119
+ const cacheRead = num(
120
+ "cached_input_tokens",
121
+ "cache_read_input_tokens",
122
+ "cache_read_tokens",
123
+ "cacheReadTokens",
124
+ );
125
+ if (input === 0 && output === 0 && cacheRead === 0) return null;
126
+ return { input, output, cacheRead };
127
+ }
128
+
129
+ /** 防御式地从一行事件里找到第一处 usage(优先级顺序,每行至多取一次,避免重复计数)。 */
130
+ function pickUsage(data: unknown): unknown {
131
+ return (
132
+ get(data, "usage") ??
133
+ get(get(data, "payload"), "usage") ??
134
+ get(get(data, "item"), "usage") ??
135
+ get(get(data, "turn"), "usage") ??
136
+ get(get(data, "response"), "usage") ??
137
+ null
138
+ );
139
+ }
140
+
141
+ // ───────────────────────── compaction 标记 ─────────────────────────
142
+
143
+ /** 仅在出现可信压缩标记时才认(保守):type / item.type 含 compact / summariz / context_truncat。 */
144
+ function isCompactionMarker(eventType: unknown, data: unknown): boolean {
145
+ const candidates = [eventType, get(get(data, "item"), "type"), get(data, "subtype")];
146
+ for (const c of candidates) {
147
+ if (typeof c !== "string") continue;
148
+ const s = c.toLowerCase();
149
+ if (s.includes("compact") || s.includes("summariz") || s.includes("context_truncat")) return true;
150
+ }
151
+ return false;
152
+ }
153
+
154
+ // ───────────────────────── 主解析 ─────────────────────────
155
+
156
+ export function parseCodexTranscript(raw: string | undefined): ParsedTranscript {
157
+ const events: StreamEvent[] = [];
158
+ let inputTokens = 0;
159
+ let outputTokens = 0;
160
+ let cacheReadTokens = 0;
161
+ let requests = 0;
162
+ let compactions = 0;
163
+ let parseSuccess = true;
164
+
165
+ if (!raw || !raw.trim()) {
166
+ return { events, usage: { inputTokens: 0, outputTokens: 0 }, compactions: 0, parseSuccess: true };
167
+ }
168
+
169
+ // 配对状态:已 started 的 callId(命令类工具的 started/completed 收口),
170
+ // 以及无显式 call_id 时的 FIFO 兜底队列(老式 function_call_output 配对)。
171
+ const startedCallIds = new Set<string>();
172
+ const pendingCallIds: string[] = [];
173
+ let synth = 0;
174
+ const nextSynthId = (prefix: string): string => `${prefix}_${++synth}`;
175
+
176
+ const addUsageFrom = (data: unknown): void => {
177
+ const u = readUsage(pickUsage(data));
178
+ if (!u) return;
179
+ inputTokens += u.input;
180
+ outputTokens += u.output;
181
+ cacheReadTokens += u.cacheRead;
182
+ requests += 1;
183
+ };
184
+
185
+ const emitCall = (callId: string, name: string, input: JsonValue, tool: ToolName): void => {
186
+ startedCallIds.add(callId);
187
+ events.push({ type: "action.called", callId, name, input, tool });
188
+ };
189
+
190
+ const emitResult = (
191
+ callId: string,
192
+ output: JsonValue | undefined,
193
+ status: "completed" | "failed" | "rejected",
194
+ ): void => {
195
+ events.push({ type: "action.result", callId, output, status });
196
+ };
197
+
198
+ // 处理 item.started / item.completed 里的 item。
199
+ const handleItem = (item: unknown, isCompleted: boolean): void => {
200
+ if (!item || typeof item !== "object") return;
201
+ const itemType = get(item, "type");
202
+ const idRaw = get(item, "id");
203
+ const baseId = typeof idRaw === "string" || typeof idRaw === "number" ? String(idRaw) : "";
204
+
205
+ switch (itemType) {
206
+ case "reasoning":
207
+ case "thinking": {
208
+ // 思考只在 completed 落一次,避免 started/completed 重复。
209
+ if (!isCompleted) return;
210
+ const text = extractReasoning(item);
211
+ if (text) events.push({ type: "thinking", text });
212
+ return;
213
+ }
214
+
215
+ case "agent_message":
216
+ case "assistant_message":
217
+ case "message": {
218
+ if (!isCompleted) return;
219
+ const text = extractText(get(item, "text") ?? get(item, "content") ?? get(item, "message"));
220
+ if (text) events.push({ type: "message", role: "assistant", text });
221
+ return;
222
+ }
223
+
224
+ case "command_execution":
225
+ case "local_shell_call": {
226
+ const callId = baseId || nextSynthId("cmd");
227
+ const command = get(item, "command") ?? get(get(item, "action"), "command");
228
+ if (!isCompleted) {
229
+ emitCall(callId, "command_execution", { command } as JsonValue, "shell");
230
+ return;
231
+ }
232
+ if (!startedCallIds.has(callId)) emitCall(callId, "command_execution", { command } as JsonValue, "shell");
233
+ const exit = get(item, "exit_code");
234
+ const statusStr = get(item, "status");
235
+ const success =
236
+ exit === 0 || (exit == null && statusStr !== "failed" && statusStr !== "error");
237
+ emitResult(
238
+ callId,
239
+ {
240
+ output: (get(item, "aggregated_output") ?? get(item, "output") ?? null) as JsonValue,
241
+ exit_code: (exit ?? null) as JsonValue,
242
+ },
243
+ success ? "completed" : "failed",
244
+ );
245
+ return;
246
+ }
247
+
248
+ case "mcp_tool_call": {
249
+ const callId = baseId || nextSynthId("mcp");
250
+ const server = get(item, "server");
251
+ const toolRaw = get(item, "tool") ?? get(item, "name") ?? "unknown";
252
+ const originalName =
253
+ typeof server === "string" ? `${server}.${String(toolRaw)}` : String(toolRaw);
254
+ const input = coerceArgs(get(item, "arguments") ?? get(item, "input"));
255
+ if (!isCompleted) {
256
+ emitCall(callId, originalName, input, normalizeToolName(String(toolRaw)));
257
+ return;
258
+ }
259
+ if (!startedCallIds.has(callId)) emitCall(callId, originalName, input, normalizeToolName(String(toolRaw)));
260
+ const statusStr = get(item, "status");
261
+ const success = !get(item, "error") && statusStr !== "failed";
262
+ emitResult(callId, (get(item, "result") ?? null) as JsonValue, success ? "completed" : "failed");
263
+ return;
264
+ }
265
+
266
+ case "web_search": {
267
+ const callId = baseId || nextSynthId("web");
268
+ const query = get(item, "query") ?? get(item, "search");
269
+ if (!isCompleted) {
270
+ emitCall(callId, "web_search", { query } as JsonValue, "web_search");
271
+ return;
272
+ }
273
+ if (!startedCallIds.has(callId)) emitCall(callId, "web_search", { query } as JsonValue, "web_search");
274
+ emitResult(callId, (get(item, "results") ?? get(item, "result") ?? null) as JsonValue, "completed");
275
+ return;
276
+ }
277
+
278
+ case "file_change":
279
+ case "patch":
280
+ case "file_patch": {
281
+ if (!isCompleted) return;
282
+ const changes = get(item, "changes");
283
+ const list = Array.isArray(changes) ? changes : [{ path: get(item, "path") }];
284
+ list.forEach((ch, i) => {
285
+ const path = get(ch, "path") ?? get(ch, "file");
286
+ const kindRaw = get(ch, "kind") ?? get(ch, "type") ?? get(item, "kind");
287
+ const kind = typeof kindRaw === "string" ? kindRaw.toLowerCase() : "update";
288
+ const tool: ToolName = kind === "add" || kind === "delete" ? "file_write" : "file_edit";
289
+ const callId = `${baseId || "patch"}#${i}`;
290
+ emitCall(callId, "file_change", { path, kind } as JsonValue, tool);
291
+ emitResult(callId, { path, kind } as JsonValue, "completed");
292
+ });
293
+ return;
294
+ }
295
+
296
+ case "error": {
297
+ const msg = get(item, "message") ?? get(item, "text") ?? "error";
298
+ events.push({ type: "error", message: String(msg) });
299
+ return;
300
+ }
301
+
302
+ default:
303
+ return; // todo_list / 其它 item 类型暂不映射
304
+ }
305
+ };
306
+
307
+ const lines = raw.split("\n");
308
+ for (const line of lines) {
309
+ const trimmed = line.trim();
310
+ if (!trimmed) continue;
311
+
312
+ let data: unknown;
313
+ try {
314
+ data = JSON.parse(trimmed);
315
+ } catch {
316
+ parseSuccess = false;
317
+ continue;
318
+ }
319
+
320
+ try {
321
+ addUsageFrom(data);
322
+
323
+ const eventType = (get(data, "type") ?? get(data, "event") ?? get(data, "kind")) as unknown;
324
+
325
+ if (isCompactionMarker(eventType, data)) {
326
+ events.push({ type: "compaction" });
327
+ compactions += 1;
328
+ continue;
329
+ }
330
+
331
+ switch (eventType) {
332
+ // ── 控制流:仅在失败时落 error ──
333
+ case "thread.started":
334
+ case "thread.completed":
335
+ case "turn.started":
336
+ case "turn.completed":
337
+ case "response.created":
338
+ case "response.completed":
339
+ case "response.cancelled":
340
+ break;
341
+
342
+ case "turn.failed":
343
+ case "response.failed": {
344
+ const err = get(data, "error");
345
+ const msg = get(err, "message") ?? get(data, "message") ?? "turn failed";
346
+ events.push({ type: "error", message: String(msg) });
347
+ break;
348
+ }
349
+
350
+ // ── 主路径:item.* ──
351
+ case "item.started":
352
+ handleItem(get(data, "item"), false);
353
+ break;
354
+ case "item.updated":
355
+ // 中间态忽略,等 completed
356
+ break;
357
+ case "item.completed":
358
+ handleItem(get(data, "item"), true);
359
+ break;
360
+
361
+ // ── 老式 / session rollout ResponseItem ──
362
+ case "message":
363
+ case "chat": {
364
+ const roleRaw = get(data, "role") ?? (get(data, "from") === "assistant" ? "assistant" : "user");
365
+ const role = roleRaw === "user" ? "user" : "assistant";
366
+ const text = extractText(get(data, "content") ?? get(data, "text") ?? get(data, "message"));
367
+ if (text) events.push({ type: "message", role, text });
368
+ break;
369
+ }
370
+
371
+ case "reasoning":
372
+ case "thinking":
373
+ case "thought": {
374
+ const text = extractReasoning(data);
375
+ if (text) events.push({ type: "thinking", text });
376
+ break;
377
+ }
378
+
379
+ case "function_call":
380
+ case "tool_call":
381
+ case "tool_use":
382
+ case "custom_tool_call":
383
+ case "action":
384
+ case "local_shell_call": {
385
+ const isShell = eventType === "local_shell_call";
386
+ const nameRaw = isShell
387
+ ? "shell"
388
+ : (get(data, "name") ?? get(get(data, "function"), "name") ?? get(data, "tool") ?? get(data, "action") ?? "unknown");
389
+ const name = String(nameRaw);
390
+ const rawArgs = isShell
391
+ ? (get(data, "action") ?? get(data, "input"))
392
+ : (get(get(data, "function"), "arguments") ?? get(data, "arguments") ?? get(data, "input") ?? get(data, "params"));
393
+ const input = coerceArgs(rawArgs);
394
+ const explicit = get(data, "call_id") ?? get(data, "id") ?? get(data, "tool_call_id");
395
+ const callId = explicit != null ? String(explicit) : nextSynthId("call");
396
+ if (explicit == null) pendingCallIds.push(callId);
397
+ emitCall(callId, name, input, normalizeToolName(name));
398
+ break;
399
+ }
400
+
401
+ case "function_call_output":
402
+ case "tool_result":
403
+ case "tool_response":
404
+ case "action_result":
405
+ case "local_shell_call_output": {
406
+ const explicit = get(data, "call_id") ?? get(data, "tool_call_id") ?? get(data, "id");
407
+ const callId =
408
+ explicit != null ? String(explicit) : pendingCallIds.shift() ?? nextSynthId("result");
409
+ const rawOut = get(data, "output") ?? get(data, "result") ?? get(data, "content");
410
+ const { output, status } = interpretOutput(rawOut, data);
411
+ emitResult(callId, output, status);
412
+ break;
413
+ }
414
+
415
+ case "error": {
416
+ const err = get(data, "error");
417
+ const msg = get(err, "message") ?? get(data, "message") ?? get(data, "content") ?? "error";
418
+ events.push({ type: "error", message: String(msg) });
419
+ break;
420
+ }
421
+
422
+ // output_text.delta / output_text.done 是同一条 assistant 文本的流式分片,
423
+ // 最终文本会经 agent_message item / message ResponseItem 落地,这里直接丢弃避免重复。
424
+ case "output_text.delta":
425
+ case "output_text.done":
426
+ break;
427
+
428
+ default: {
429
+ // 兜底:按结构猜。
430
+ const role = get(data, "role");
431
+ if (role === "assistant" || role === "user") {
432
+ const text = extractText(get(data, "content") ?? get(data, "text"));
433
+ if (text) events.push({ type: "message", role, text });
434
+ }
435
+ }
436
+ }
437
+ } catch {
438
+ // 单行处理异常不应拖垮整条 transcript。
439
+ parseSuccess = false;
440
+ }
441
+ }
442
+
443
+ const usage: Usage = { inputTokens, outputTokens };
444
+ if (cacheReadTokens > 0) usage.cacheReadTokens = cacheReadTokens;
445
+ if (requests > 0) usage.requests = requests;
446
+
447
+ return { events, usage, compactions, parseSuccess };
448
+ }
449
+
450
+ /** 解读一条工具结果输出:抠 exit_code / error 推断成败,返回归一化 output + status。 */
451
+ function interpretOutput(
452
+ rawOut: unknown,
453
+ data: unknown,
454
+ ): { output: JsonValue | undefined; status: "completed" | "failed" | "rejected" } {
455
+ // 显式 success / error 优先。
456
+ const explicitSuccess = get(data, "success");
457
+ if (explicitSuccess === false) return { output: (rawOut ?? null) as JsonValue, status: "failed" };
458
+ if (get(data, "error")) return { output: (rawOut ?? null) as JsonValue, status: "failed" };
459
+
460
+ // 字符串输出尝试解析 exit_code(codex shell 结果常见)。
461
+ let parsed: unknown = rawOut;
462
+ if (typeof rawOut === "string") {
463
+ try {
464
+ parsed = JSON.parse(rawOut);
465
+ } catch {
466
+ parsed = rawOut;
467
+ }
468
+ }
469
+ const exit =
470
+ get(parsed, "exit_code") ?? get(parsed, "exitCode") ?? get(get(parsed, "metadata"), "exit_code");
471
+ if (typeof exit === "number") {
472
+ return { output: (parsed ?? null) as JsonValue, status: exit === 0 ? "completed" : "failed" };
473
+ }
474
+ return { output: (parsed ?? null) as JsonValue, status: "completed" };
475
+ }
476
+
477
+ /** 便捷形态:只要 StreamEvent[]。 */
478
+ export function parseCodex(raw: string | undefined): StreamEvent[] {
479
+ return parseCodexTranscript(raw).events;
480
+ }
@@ -0,0 +1,37 @@
1
+ // o11y 解析器入口:把各 agent 的原始 transcript 归一成 niceeval StreamEvent[]。
2
+ // 每个解析器都返回同一个 ParsedTranscript 形状,parseTranscript 按 agent 名分派。
3
+
4
+ import type { StreamEvent, Usage } from "../../types.ts";
5
+ import { parseCodexTranscript, parseCodex } from "./codex.ts";
6
+ import { parseClaudeCodeTranscript, parseClaudeCode } from "./claude-code.ts";
7
+ import { parseBubTranscript, parseBub } from "./bub.ts";
8
+
9
+ /** 解析结果:事件流为核心,外加 usage / 压缩次数 / 是否全程解析成功。 */
10
+ export interface ParsedTranscript {
11
+ events: StreamEvent[];
12
+ usage: Usage;
13
+ compactions: number;
14
+ parseSuccess: boolean;
15
+ }
16
+
17
+ export {
18
+ parseCodexTranscript,
19
+ parseCodex,
20
+ parseClaudeCodeTranscript,
21
+ parseClaudeCode,
22
+ parseBubTranscript,
23
+ parseBub,
24
+ };
25
+
26
+ /**
27
+ * 按 agent 名分派到对应解析器;认不出来的走通用(bub)兜底。
28
+ * agent 名做包含匹配,兼容 "vercel-ai-gateway/codex"、"claude-code-1m" 之类的变体。
29
+ */
30
+ export function parseTranscript(raw: string | undefined, agent: string): ParsedTranscript {
31
+ const a = (agent || "").toLowerCase();
32
+ if (a.includes("codex")) return parseCodexTranscript(raw);
33
+ if (a.includes("claude")) return parseClaudeCodeTranscript(raw);
34
+ if (a.includes("bub")) return parseBubTranscript(raw);
35
+ // 默认:通用 JSONL 兜底解析器。
36
+ return parseBubTranscript(raw);
37
+ }