niceeval 0.1.1 → 0.1.2

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 (133) hide show
  1. package/README.md +8 -8
  2. package/README.zh.md +13 -13
  3. package/docs/README.md +119 -0
  4. package/docs/adapters/README.md +60 -0
  5. package/docs/adapters/authoring.md +179 -0
  6. package/docs/adapters/coding-agent-skills-plugins.md +324 -0
  7. package/docs/adapters/collection.md +128 -0
  8. package/docs/adapters/contract.md +263 -0
  9. package/docs/adapters/reference/agent-eval.md +215 -0
  10. package/docs/adapters/reference/agent-loop-apis.md +69 -0
  11. package/docs/adapters/reference/claude-code-otel-telemetry.md +100 -0
  12. package/docs/adapters/reference/eve-protocol.md +127 -0
  13. package/docs/adapters/reference/otel-genai.md +107 -0
  14. package/docs/adapters/reference/otel-instrumentation.md +65 -0
  15. package/docs/adapters/targets.md +99 -0
  16. package/docs/architecture.md +140 -0
  17. package/docs/assertions.md +388 -0
  18. package/docs/capabilities-by-construction.md +48 -0
  19. package/docs/cli.md +171 -0
  20. package/docs/concepts.md +106 -0
  21. package/docs/e2e-ci.md +295 -0
  22. package/docs/eval-authoring.md +319 -0
  23. package/docs/experiments.md +154 -0
  24. package/docs/getting-started.md +224 -0
  25. package/docs/multi-agent.md +145 -0
  26. package/docs/observability.md +333 -0
  27. package/docs/origin-integration.md +195 -0
  28. package/docs/references.md +35 -0
  29. package/docs/results-format.md +228 -0
  30. package/docs/runner.md +104 -0
  31. package/docs/sandbox.md +276 -0
  32. package/docs/scoring.md +119 -0
  33. package/docs/source-map.md +106 -0
  34. package/docs/tier-sync.md +177 -0
  35. package/docs/view.md +157 -0
  36. package/docs/vision.md +88 -0
  37. package/package.json +35 -5
  38. package/src/agents/ai-sdk-otel.ts +0 -0
  39. package/src/agents/ai-sdk.test.ts +377 -0
  40. package/src/agents/ai-sdk.ts +577 -0
  41. package/src/agents/bub.ts +30 -37
  42. package/src/agents/claude-code.ts +7 -4
  43. package/src/agents/codex.ts +15 -10
  44. package/src/agents/index.ts +43 -1
  45. package/src/agents/sdk-streams.test.ts +145 -0
  46. package/src/agents/sdk-streams.ts +457 -0
  47. package/src/agents/shared.ts +77 -39
  48. package/src/agents/streaming.test.ts +152 -0
  49. package/src/agents/streaming.ts +195 -0
  50. package/src/agents/types.ts +218 -0
  51. package/src/agents/ui-message-stream.test.ts +249 -0
  52. package/src/agents/ui-message-stream.ts +372 -0
  53. package/src/cli.ts +124 -77
  54. package/src/context/context.test.ts +203 -7
  55. package/src/context/context.ts +158 -49
  56. package/src/context/session.test.ts +96 -0
  57. package/src/context/session.ts +119 -9
  58. package/src/context/types.ts +205 -0
  59. package/src/define.ts +4 -14
  60. package/src/expect/index.ts +8 -71
  61. package/src/i18n/core.ts +28 -0
  62. package/src/i18n/en.ts +47 -5
  63. package/src/i18n/index.ts +5 -17
  64. package/src/i18n/zh-CN.ts +47 -5
  65. package/src/index.ts +12 -0
  66. package/src/loaders/index.ts +8 -39
  67. package/src/o11y/otlp/canonical.ts +7 -6
  68. package/src/o11y/otlp/mappers/codex.ts +10 -8
  69. package/src/o11y/otlp/mappers/index.ts +9 -21
  70. package/src/o11y/otlp/receiver.ts +25 -7
  71. package/src/o11y/otlp/sandbox-receiver.ts +57 -21
  72. package/src/o11y/otlp/turn-otel.test.ts +110 -0
  73. package/src/o11y/otlp/turn-otel.ts +125 -0
  74. package/src/o11y/parsers/bub.ts +13 -11
  75. package/src/o11y/parsers/claude-code.ts +27 -51
  76. package/src/o11y/parsers/codex.ts +29 -46
  77. package/src/o11y/parsers/index.ts +5 -14
  78. package/src/o11y/tool-names.test.ts +61 -0
  79. package/src/o11y/tool-names.ts +74 -0
  80. package/src/o11y/types.ts +135 -0
  81. package/src/runner/attempt.ts +456 -0
  82. package/src/runner/fingerprint.ts +59 -0
  83. package/src/runner/remote-sandbox.ts +53 -0
  84. package/src/runner/report.ts +53 -0
  85. package/src/runner/reporters/artifacts.ts +25 -4
  86. package/src/runner/reporters/console.ts +2 -8
  87. package/src/runner/reporters/live.ts +2 -8
  88. package/src/runner/reporters/shared.ts +15 -0
  89. package/src/runner/reporters/table.ts +10 -62
  90. package/src/runner/run.ts +114 -619
  91. package/src/runner/types.ts +237 -0
  92. package/src/sandbox/checkpoint.ts +15 -13
  93. package/src/sandbox/docker-stream.ts +87 -0
  94. package/src/sandbox/docker.ts +42 -120
  95. package/src/sandbox/e2b.ts +24 -75
  96. package/src/sandbox/local-files.ts +33 -0
  97. package/src/sandbox/paths.test.ts +85 -0
  98. package/src/sandbox/paths.ts +45 -0
  99. package/src/sandbox/resolve.ts +10 -34
  100. package/src/sandbox/shell.ts +17 -0
  101. package/src/sandbox/source-files.ts +42 -2
  102. package/src/sandbox/types.ts +158 -0
  103. package/src/sandbox/vercel.ts +55 -71
  104. package/src/scoring/collector.ts +3 -2
  105. package/src/scoring/judge.ts +72 -146
  106. package/src/scoring/match.ts +79 -0
  107. package/src/scoring/types.ts +76 -0
  108. package/src/shared/aggregate.ts +48 -0
  109. package/src/shared/format.ts +20 -0
  110. package/src/shared/outcome.ts +48 -0
  111. package/src/shared/types.ts +37 -0
  112. package/src/types.ts +20 -911
  113. package/src/view/aggregate.ts +103 -0
  114. package/src/view/app/App.tsx +26 -5
  115. package/src/view/app/components/AttemptModal.tsx +12 -3
  116. package/src/view/app/components/CodeView.tsx +9 -5
  117. package/src/view/app/components/CopyControls.tsx +4 -4
  118. package/src/view/app/components/ExperimentTable.tsx +5 -5
  119. package/src/view/app/i18n.ts +31 -7
  120. package/src/view/app/lib/format.ts +17 -20
  121. package/src/view/app/lib/outcome.ts +4 -16
  122. package/src/view/app/main.tsx +3 -5
  123. package/src/view/app/pages/RunsPage.tsx +2 -2
  124. package/src/view/app/pages/TracesPage.tsx +2 -2
  125. package/src/view/app/shared.ts +2 -1
  126. package/src/view/app/types.ts +6 -43
  127. package/src/view/client-dist/app.css +1 -1
  128. package/src/view/client-dist/app.js +18 -18
  129. package/src/view/index.ts +24 -401
  130. package/src/view/loader.ts +234 -0
  131. package/src/view/server.ts +136 -0
  132. package/src/view/shared/types.ts +64 -0
  133. package/src/view/styles.css +60 -9
@@ -0,0 +1,100 @@
1
+ # Claude Code 自带的 OTel 遥测 —— 能不能替代"等 CLI 跑完再读 transcript"
2
+
3
+ **来源:** [Claude Code 官方 Monitoring 文档](https://code.claude.com/docs/en/monitoring-usage)(2026-07 抓取原始页面核对,不是转述摘要);对照读了 `/Users/ctrdh/Code/agent-eval`(vercel-labs/agent-eval)的 `claude-code.ts` + `o11y/parsers/claude-code.ts` 源码。
4
+
5
+ ## 先回答问题
6
+
7
+ **能不能"马上拿到"、还是要等全部结果:两者之间——是"近实时增量",不是"瞬时推送",也不是现在这样"全部跑完才给"。**
8
+
9
+ - Claude Code 的 OTel 导出是**定时批量导出**(OTel SDK 标准的 BatchProcessor 行为),不是每条事件产生就立刻 POST 一次。间隔可调,metrics 默认 60s、logs/traces 默认 5s,**最低可以调到 1000ms**(见下表),进程退出时也会把剩余缓冲区强制 flush 一次。
10
+ - 所以"增量"是相对的:比起 niceeval 现在的 claude-code adapter(`src/agents/claude-code.ts`)—— `runCommand` 等整个 `claude --print` 进程退出,再一次性读回整份 transcript JSONL —— 这条路径能在**进程运行过程中**分批收到 span,粒度是秒级,不是"跑完才有"。
11
+ - 但也不是 eve / 自建 remote agent 那种"事件发生即推"的上限:批量间隔就是延迟下限,想要更低延迟只能继续调小间隔(有性能/开销代价,官方文档明确说"debug 用完记得调回去")。
12
+
13
+ 结论细化到"该不该接"见文末[结论](#结论这条路径值不值得接)。
14
+
15
+ ## agent-eval 不是答案来源:它压根没用 OTel
16
+
17
+ 先排除一个容易搞混的点:`/Users/ctrdh/Code/agent-eval`(vercel-labs 的 agent-eval)里的 Claude Code 适配(`packages/agent-eval/src/lib/agents/claude-code.ts`)**完全没有用 OTel**——它是纯粹的"沙箱里跑 `claude --print`,`sandbox.runCommand()` 等命令退出,再 `ls -t ~/.claude/projects/**/*.jsonl | head -1` 找最新一份磁盘 transcript 整份读回来"(`captureTranscript()`),`captureTranscriptBestEffort()` 只在命令完成 / abort / catch 分支里调用一次。跟 niceeval 自己的 `src/agents/claude-code.ts` 是**同一套磁盘旁读策略**,niceeval 这边还额外记过一篇源码阅读笔记([agent-eval 参考](agent-eval.md))。
18
+
19
+ 也就是说:agent-eval 对"能不能提前拿到结果"这个问题没有帮助——它对这个问题给出的答案是"不能,等 CLI 退出"。真正有价值的信息来自 Claude Code CLI **自己**另开的一条官方能力:内建的 OpenTelemetry 导出器,和 agent-eval 怎么适配它无关。
20
+
21
+ ## Claude Code 的 OTel 导出:三种信号,两种成熟度
22
+
23
+ | 信号 | 状态 | 启用变量 | 默认导出间隔 | 可调最低间隔 |
24
+ |---|---|---|---|---|
25
+ | **Metrics**(计数器/累计值) | GA | `CLAUDE_CODE_ENABLE_TELEMETRY=1` + `OTEL_METRICS_EXPORTER=otlp` | `OTEL_METRIC_EXPORT_INTERVAL` 默认 **60000ms** | 官方 debug 示例用到 1000ms |
26
+ | **Logs/Events**(结构化事件,如 `user_prompt`、`tool_result`、`api_request`…) | GA | 同上 + `OTEL_LOGS_EXPORTER=otlp` | `OTEL_LOGS_EXPORT_INTERVAL` 默认 **5000ms** | 文档给出 1000 / 10000 两档示例 |
27
+ | **Traces/Spans**(时间轨,span 树) | **Beta**,默认关 | `CLAUDE_CODE_ENABLE_TELEMETRY=1` + `CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1` + `OTEL_TRACES_EXPORTER=otlp` | `OTEL_TRACES_EXPORT_INTERVAL` 默认 **5000ms** | 同上,1000ms 起 |
28
+
29
+ 三种信号的间隔各自独立配置,互不影响。**这一段跟 niceeval 现有观测设计对上号的是 Traces**——metrics/logs 是聚合计数和审计日志,不是"这一步工具调用花了多久"这种瀑布图数据。
30
+
31
+ ### Span 层级(启用 traces 后长这样)
32
+
33
+ ```text
34
+ claude_code.interaction ← 每个 user prompt 起一个根 span
35
+ ├── claude_code.llm_request ← 每次 API 请求
36
+ ├── claude_code.hook ← 需要更深一档的 detailed beta tracing 才有
37
+ └── claude_code.tool ← 每次工具调用
38
+ ├── claude_code.tool.blocked_on_user ← 等权限决策的时间
39
+ ├── claude_code.tool.execution ← 真正执行的时间
40
+ └── (Agent / Task 工具时)子 agent 的 llm_request / tool span 嵌在这里
41
+ ```
42
+
43
+ `llm_request`、`tool.execution`、`hook` 三种 span 失败时会带 OTel `status=ERROR`,其余 span 恒为 `UNSET`——不是靠属性猜成功与否,是标准 OTel status。子 agent(Task 工具生的)的 span 会正确嵌套在父 `claude_code.tool` 下面,不是摊平的。
44
+
45
+ ### 关键限制:内容默认全部脱敏
46
+
47
+ **span 默认只有结构和计时,没有内容。** `user_prompt`、`tool_input`、工具执行的输入输出内容,默认都不出现在 span 属性里,要显式加:
48
+
49
+ ```bash
50
+ export OTEL_LOG_USER_PROMPTS=1 # 把 prompt 原文带进去
51
+ export OTEL_LOG_TOOL_DETAILS=1 # 工具参数(Bash 命令、MCP server/tool 名、skill 名…)
52
+ export OTEL_LOG_TOOL_CONTENT=1 # 工具的输入输出内容本身
53
+ ```
54
+
55
+ 也就是说,不开这三个 flag,拿到的 span 只能回答"跑了哪些工具、每步花多久、有没有出错",回答不了"这次 Read 读的是哪个文件、Bash 跑的是什么命令、模型说了什么"——而这些恰恰是 niceeval 磁盘旁读 transcript 现在**免费**能拿到的东西。开了这三个 flag 才能拉平,但意味着把敏感的 prompt / 工具明文经 OTLP 发到我们自己起的本机接收器——链路仍在本机(容器 → 宿主的临时端口),不出网,风险可控,但这是要显式做的取舍,不是零成本升级。
56
+
57
+ ### Gating:这条路径对 `-p` 模式是开放的
58
+
59
+ > "In interactive CLI sessions, this also requires your organization to be allowlisted for the feature. Agent SDK and non-interactive `-p` sessions are not gated."
60
+
61
+ niceeval 的 `src/agents/claude-code.ts` 一直是拿 `--print`(即 `-p`)跑的沙箱型 adapter——**刚好落在不受组织白名单限制的那一半**,不用等 Anthropic 给账号开权限就能试。这是这条路径对 niceeval 场景友好的地方。
62
+
63
+ ## 跟 niceeval 现有 OTLP 接收器天然兼容
64
+
65
+ niceeval 已经有一套本机 OTLP 接收器(`src/o11y/otlp/receiver.ts`),给 remote agent 的 `capabilities.tracing` 用:每个沙箱起一个临时端口,只认 `POST .../v1/traces`,`src/o11y/otlp/parse.ts` 同时吃 **OTLP/JSON** 和 **OTLP/protobuf** 两种线编码(手写了一个够用的 protobuf reader,没有额外依赖)。
66
+
67
+ Claude Code 的 traces 导出器支持的协议是 `grpc` / `http/json` / `http/protobuf` 三选一(`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL`)。**只要选 `http/json` 或 `http/protobuf`,不选 `grpc`**(现有接收器是个普通 `http.createServer`,不认 gRPC 的 HTTP/2 帧),Claude Code 就能把 span 直接导出到现有接收器,格式层面**不需要新写解析代码**——`parseOtlpTraces` 已经覆盖。
68
+
69
+ 理论上可以这样接(未验证,只是根据文档推出的配置):
70
+
71
+ ```bash
72
+ CLAUDE_CODE_ENABLE_TELEMETRY=1
73
+ CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1
74
+ OTEL_TRACES_EXPORTER=otlp
75
+ OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf # 或 http/json,别选 grpc
76
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=<TraceReceiver.endpoint(host)> # 沙箱里跑 claude 时复用现有 receiver
77
+ OTEL_TRACES_EXPORT_INTERVAL=1000 # 拉到近实时,eval 场景短会话不必顾虑"生产环境该调大"
78
+ ```
79
+
80
+ ## 跟现有采集矩阵的关系
81
+
82
+ [采集设计](../collection.md#采集矩阵现状与-src-对齐)那张表里"时间轨"一行,claude-code 当前写的是"无原生 OTel → transcript 时间戳合成 span"——**这句话需要更新**:Claude Code 现在有原生 OTel trace 导出,只是处于 beta、默认关、内容默认脱敏。准确说法应该是"有 beta 版原生 OTLP trace 导出(需要一串 env 才能打开、且默认无内容),目前 niceeval 仍选择更省事、内容更全的 transcript 时间戳合成方案"。
83
+
84
+ ## 结论:这条路径值不值得接
85
+
86
+ **不建议现在替换行为轨(`StreamEvent[]`)的采集方式。** 磁盘旁读 transcript 免费给全量内容(prompt / 工具参数 / 工具输出 / 助手文本),OTel 版本要拿到同等内容得开三个额外 flag、还处于 beta(字段随时可能变,`user_prompt` 等内容属性文档原文写明"不是稳定 span schema 的一部分")。负断言完整性([契约](../contract.md#负断言的完整性规则))经不起一个 beta 功能说变就变。
87
+
88
+ **值得作为时间轨(`TraceSpan[]`)的可选升级路径考虑,分两档:**
89
+
90
+ 1. **保底档(不用动 adapter 代码):** 什么都不接,继续用 transcript 时间戳合成 span——这是现状,降级安全,`view` 少一些真实的 span 层级细节(比如工具"等权限"和"真正执行"分不开),但断言不受影响。
91
+ 2. **升级档(可选,给需要更真实瀑布图的用户):** `claudeCodeAgent` 配置里加一个开关,setup 阶段给沙箱里跑 `claude` 的进程注入上面那组 env(复用沙箱已经有的 `TraceReceiver`,跟 remote agent 的 tracing 走同一个接收器/同一个 `o11y/otlp/mappers/` 归一管线),用户自己决定要不要为了更真实的瀑布图打开内容脱敏(`OTEL_LOG_TOOL_DETAILS` 等)。这是**加法**,不影响现有行为轨,失败也只是"没拿到 trace",不影响断言——符合[采集设计](../collection.md)里"时间轨缺数据是降级,不是契约问题"的既有原则。
92
+
93
+ **不建议**因为这个能力去改变现有"等 `runCommand` 返回再读 transcript"的行为轨轮询模型:近实时的是 span(计时结构),不是行为数据本身——`StreamEvent[]` 需要的完整内容(尤其是断言要读的工具参数/输出/助手文本)目前只有磁盘旁读的完整 transcript 能免费给,OTel 版本要么没内容要么要额外开脱敏 flag 且不保证字段稳定性。
94
+
95
+ ## 相关阅读
96
+
97
+ - [采集设计](../collection.md) —— 双轨四通道设计、claude-code 现有采集矩阵。
98
+ - [agent-eval 参考](agent-eval.md) —— agent-eval 的 claude-code 适配源码阅读(纯磁盘旁读,不涉及 OTel)。
99
+ - [OTel GenAI 等标准参考](otel-genai.md) —— OTel GenAI semconv 本身讲的是"字段该叫什么",这篇讲的是"Claude Code 到底发不发、多快发"。
100
+ - [Observability](../../observability.md) —— niceeval 的 `TraceReceiver` / OTLP 解析管线现状。
@@ -0,0 +1,127 @@
1
+ # eve 的协议机制:自产自销的运行时事件流(源码阅读记录)
2
+
3
+ **来源:** 直接读 `/Users/ctrdh/Code/eve`(本机 checkout)的源码。关键文件:
4
+
5
+ - 协议定义:`packages/eve/src/protocol/message.ts`(`HandleMessageStreamEvent` 联合,530 行)
6
+ - action / input 字段:`packages/eve/src/runtime/actions/types.ts`、`runtime/input/types.ts`
7
+ - eval 侧消费:`packages/eve/src/evals/session.ts`、`evals/runner/derive-run-facts.ts`
8
+
9
+ 和另两篇参考对照着读,三条路线正好补齐:[agent-eval](agent-eval.md) 是**逆向适配别人的协议**(读未接管理层的 CLI 的 transcript,转换成自定义闭集),[OTel GenAI](otel-genai.md) 是**标准化遥测**(span 树,行业公约),eve 是第三条——**协议不是转换出来的,是运行时原生吐的**。eve 同时拥有 agent 运行时(Vercel AI SDK `streamText` 外包一层 harness)和 eval 框架,两者共享同一套 wire 协议,**没有采集层、没有转换层**:
10
+
11
+ ```text
12
+ 模型 API → AI SDK streamText → eve harness(唯一规范化层)
13
+ → HandleMessageStreamEvent(NDJSON over HTTP)
14
+ → eval client 逐行 parse → t.events 直接就是它,零二次转换
15
+ ```
16
+
17
+ niceeval 的位置一句话:**契约形状学 eve(强类型事件、callId、`rejected`、parked),采集方式像 agent-eval(读未接管理层的 CLI transcript),trace 归一到 OTel**。本篇记录 eve 这套协议到底怎么收集、收集了什么字段——为 niceeval 的 `StreamEvent` 演进提供上限参照。
18
+
19
+ ## 采集机制:没有"采集",只有"传输"
20
+
21
+ 因为运行时是自己的,"怎么拿到数据"退化成传输问题,但传输层有几个值得记的机制设计:
22
+
23
+ - **显式协议版本。** `EVE_MESSAGE_STREAM_VERSION = "16"`,连同 `x-eve-session-id` / `x-eve-stream-format` 作为 HTTP 头随流下发(content-type `application/x-ndjson`)。协议演进有版本号,消费端能识别拒绝——对比 agent-eval 面对 CLI 格式漂移只能写 `data.type || data.event || data.kind` 这种轮试兜底。
24
+ - **HITL 续接是一等公民。** 请求体是联合类型:首轮 `{ message }`;续轮 `{ continuationToken, message }` **或** `{ continuationToken, inputResponses }`。resume 靠 token 而不是 CLI flag;回答 HITL 是结构化的 `InputResponse[]`——`{ requestId, optionId?, text? }`,**带 requestId 定位到具体哪个请求**,不是"再发一条用户消息"。
25
+ - **事件持久化 + 可回放。** 每个事件可带 `meta: { at }` 时间戳,运行时在写入 workflow-owned stream 前盖章,"so replay preserves the original timing"——事件流同时是持久会话记录,回放不失真。
26
+
27
+ ## 事件词汇:26 种类型,带三级坐标
28
+
29
+ `HandleMessageStreamEvent` 是 26 个成员的判别联合。和 niceeval `StreamEvent`(9 种)最大的结构差异:**几乎每个事件带 `sequence`(全序)+ `turnId` + `stepIndex` 坐标**——事件不止有顺序,还有归属;而 niceeval 的事件只有数组顺序,turn 归属靠"哪次 `send` 返回的"隐式表达。
30
+
31
+ 按层列出(字段为源码原文):
32
+
33
+ | 层 | 事件 | 关键字段 |
34
+ |---|---|---|
35
+ | **session** | `session.started` | `runtime?: RuntimeIdentity`(见下)、`invocation?`(subagent 元数据:parentCallId / parentSessionId / parentTurnId) |
36
+ | | `session.waiting` | `{ wait: "next-user-message" }`——干净停在等输入 |
37
+ | | `session.completed` / `session.failed` | failed 带 `{ code, message, details? }` |
38
+ | **turn**(一次用户输入到稳定) | `turn.started` / `turn.completed` / `turn.failed` | `turnId` + `sequence`;failed 带结构化 `code / message` |
39
+ | **step**(turn 内的一次模型调用) | `step.started` / `step.completed` / `step.failed` | completed 带 `finishReason` + **`usage { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens }`**——用量按 step 记,不是按 turn 聚合 |
40
+ | **内容** | `message.received` | 用户消息回显(niceeval 由 runner 侧补 user message 事件,同一目的) |
41
+ | | `message.appended` / `message.completed` | appended 带 `messageDelta / messageSoFar`(流式);completed 带 `message` + `finishReason` |
42
+ | | `reasoning.appended` / `reasoning.completed` | 思考块,同样 delta + 完整两段式 |
43
+ | | `result.completed` | `{ result: JsonValue }`——结构化输出(对应 niceeval 的 `Turn.data`,但 eve 是事件) |
44
+ | **动作** | `actions.requested` | `{ actions: RuntimeActionRequest[] }`(见下);文档明确"consumers must correlate action lifecycles by call ID" |
45
+ | | `action.result` | `{ result, status: "completed" \| "failed" \| "rejected", error?: { code, message } }`——**结构化错误**,"keeps UI consumers from having to parse provider- or tool-specific output strings" |
46
+ | | `input.requested` | `{ requests: InputRequest[] }`(见下) |
47
+ | | `authorization.required` / `authorization.completed` | OAuth / 连接授权,**独立于 HITL 问答**;completed 带 `outcome: "authorized" \| "declined" \| "failed" \| "timed-out"` |
48
+ | **subagent** | `subagent.called` | `{ callId, childSessionId, sessionId, name, toolName, workflowId, remote?: { url } }` |
49
+ | | `subagent.started` / `subagent.completed` | completed 带 `{ callId, output }` |
50
+ | | `subagent.event` | `{ callId, subagentName, event: HandleMessageStreamEvent }`——**子 agent 的事件递归嵌套转发**,保留归属 |
51
+ | **压缩** | `compaction.requested` / `compaction.completed` | requested 带 `usageInputTokens`(触发时的上下文水位) |
52
+
53
+ ### `RuntimeActionRequest`:action 是 kind 判别联合
54
+
55
+ ```typescript
56
+ type RuntimeActionRequest =
57
+ | { kind: "tool-call"; callId: string; toolName: string; input: JsonObject }
58
+ | { kind: "load-skill"; callId: string; input: JsonObject }
59
+ | { kind: "subagent-call"; callId: string; name: string; subagentName: string; nodeId: string; description: string; input: JsonObject }
60
+ | { kind: "remote-agent-call"; callId: string; name: string; remoteAgentName: string; nodeId: string; description: string; input: JsonObject };
61
+ ```
62
+
63
+ `load-skill` 在 eve 里真的是一种 action kind——这就是 niceeval `t.loadedSkill` = `calledTool("load_skill")` 语法糖的出处;subagent 调用也是 action 的一种,再由运行时展开成 `subagent.*` 事件。
64
+
65
+ ### `InputRequest`:HITL 请求的完整形状
66
+
67
+ ```typescript
68
+ interface InputRequest {
69
+ requestId: string; // 稳定 id,回答时用它定位
70
+ prompt: string;
71
+ action: RuntimeToolCallActionRequest; // 停在哪个工具调用上——完整对象,不是字符串
72
+ display?: "confirmation" | "select" | "text"; // 渲染提示;审批 = confirmation + approve/deny 两个 option
73
+ options?: { id: string; label: string; description?: string; style?: "primary" | "danger" | "default" }[];
74
+ allowFreeform?: boolean; // 是否允许自由文本代替选项
75
+ }
76
+ ```
77
+
78
+ niceeval 的 `InputRequest` 与它几乎同构但全字段 optional(要兼容信息量少的 agent);eve 的 `action` 是完整工具调用对象而非 `action?: string`,`allowFreeform` / `style` 是 niceeval 没有的。
79
+
80
+ ### `RuntimeIdentity`:被测对象自报身份
81
+
82
+ ```typescript
83
+ interface RuntimeIdentity {
84
+ agentId: string; agentName?: string;
85
+ eveVersion: string;
86
+ build?: { deployedAt?, gitBranch?, gitSha? };
87
+ modelId: string; // ← 实际在用的模型,协议里直接给
88
+ }
89
+ ```
90
+
91
+ 对照 agent-eval 的痛点:codex 经网关后"实际用的模型"要从磁盘 session 文件里抠 `turn_context.payload.model`([笔记](agent-eval.md#采集层两份-parser-之前原始数据从哪来));eve 因为拥有运行时,直接在 `session.started` 里自报。
92
+
93
+ ## eval 侧怎么消费
94
+
95
+ - **`t.events` 就是原始协议事件**,没有第二层 eval 专用 schema。作用域断言直接查这条流。
96
+ - **派生事实**(`derive-run-facts.ts` → `EveEvalDerivedFacts`):`toolCalls / toolCallCount / subagentCalls / subagentCallCount / inputRequests / parked / messageCount / reasoningBlockCount / failureCode?`。比 niceeval 的 `DerivedFacts` 多 `reasoningBlockCount` 和 `failureCode`(顶层失败码,从 `*.failed` 事件抠)。
97
+ - **轮次边界在协议里**:`isCurrentTurnBoundaryEvent` = `session.completed | session.failed | session.waiting`——客户端读到边界事件就知道这轮到头了,不靠连接关闭或超时猜。
98
+ - **parked 判定同思路**:`endedParkedOnInput(events)`,niceeval 的"最后一条有意义事件是 `input.requested`"与之对齐。
99
+
100
+ ## 对 niceeval 适配器设计的启发
101
+
102
+ **先泼冷水:eve 是上限,不是榜样。** 它不需要采集 / 转换,是因为它拥有运行时;niceeval 面对未接管理层的 coding agent CLI 永远做不到。但有一个例外方向:**remote agent(用户自己的 agent)里,用户就是运行时的主人**——`toStreamEvents` 理论上可以做到 eve 级保真。分档表的 T1 对 remote agent 的天花板,比 sandbox agent 高得多。
103
+
104
+ 具体字段层面,值得记进 `StreamEvent` 的演进候选(都不是现在就加,是"需要时有先例"):
105
+
106
+ 1. **坐标系(`sequence` / `turnId` / `stepIndex`)。** niceeval 单轮内的 step 边界(一次 CLI 运行内多次模型调用)目前丢失;claude-code transcript 里其实带这个信息。要支持"第几步做了什么"级别的断言或 view 分组时,eve 的三级坐标是现成方案。
107
+ 2. **`action.result.error: { code, message }`。** niceeval 只有 `status`,失败原因埋在 `output` 字符串里;"消费方不该解析工具私有输出来判断失败"这个理由对 view 和断言同样成立。
108
+ 3. **usage 按 step 记。** niceeval 的 `usage` 挂在 `Turn`(整轮聚合);eve 挂在 `step.completed`(每次模型调用一份)。transcript 里常有 per-step 用量,聚合前丢掉了瀑布图想要的粒度。
109
+ 4. **agent 自报元数据(`RuntimeIdentity`)。** `Turn` 或 `session.started` 级的可选 `runtime?: { modelId, version, … }`,让"实际用的模型"从抠磁盘变成契约字段——网关场景的成本核算靠它才准。
110
+ 5. **带 `requestId` 的逐请求回答。** eve 的回答是 `{ requestId, optionId?, text? }`;niceeval 的 `respond(...responses)` 把回答 join 成一段普通 send 文本,多个待答请求并发时表达不了"哪个回答给哪个请求"(`respondAll` 只能全选同一 option)。将来要支持,对比一眼可见:
111
+
112
+ ```typescript
113
+ await t.respond("yes"); // 现状:回答作为下一轮文本,靠 agent 自己对上
114
+ await t.respond({ requestId: req.requestId, optionId: "approve" }); // eve 形状:显式定位
115
+ ```
116
+
117
+ 6. **`authorization.*` 与 `input.requested` 分开。** 授权(OAuth / 连接)和 HITL 问答是两种"停",eve 分成两组事件、各带 outcome;niceeval 目前只有一种。评测带三方连接的 agent 时会需要。
118
+ 7. **落盘工件带 schema 版本。** `StreamEvent` 是进程内模型可以不带版本,但 `.niceeval/<run>/` 的事件流 JSON 文件是跨版本读的;eve 的 `x-eve-stream-version` 头是先例。niceeval 的具体取舍见 [Results Format · 版本与升级设计](../../results-format.md#版本与升级设计):版本号放在 run 级 `summary.json` manifest 里,attempt 文件继续保持裸 JSON array/object。
119
+
120
+ 没抄的也记一笔:**流式 delta(`message.appended` 的 `messageDelta / messageSoFar`)不需要**——评测离线跑,整段的 `message` 事件就够;AG-UI 的三段式同理(见 [otel-genai 笔记](otel-genai.md#ag-ui--和-niceeval-streamevent-同形态的扁平事件流))。要做"实时看 agent 跑"的 view 时再回头看。
121
+
122
+ ## 相关阅读
123
+
124
+ - [agent-eval 笔记](agent-eval.md) —— 逆向适配路线:采集 / 转换 / 落地三层与顺序配对的坑。
125
+ - [OTel GenAI 等标准](otel-genai.md) —— 标准化遥测路线;三条路线的汇总对照表在那篇。
126
+ - [Adapter 契约](../contract.md) —— niceeval 自己的 `StreamEvent` 词汇与逐 API 义务。
127
+ - [Observability](../../observability.md) —— 双轨设计:StreamEvent 断言 + canonical GenAI trace。
@@ -0,0 +1,107 @@
1
+ # OTel GenAI 与其它「agent 行为怎么记」的标准(调研记录)
2
+
3
+ **来源:** OpenTelemetry 官方文档 / [semantic-conventions-genai 仓库](https://github.com/open-telemetry/semantic-conventions-genai)、各标准官方 spec(2026-07 抓取)。这是**调研记录**,和 [agent-eval 笔记](agent-eval.md)对照着读:同一个问题——"agent 干了什么,用什么 schema 记下来"——agent-eval 选择**自己定义一套闭集事件类型**,OTel GenAI 是**行业标准的另一套**。本篇记录 OTel GenAI 到底定义了什么、还有哪些标准在解决同一问题、以及这些对 niceeval 的 `StreamEvent` / trace 双轨设计意味着什么。
4
+
5
+ niceeval 自己的选择已经定了(见 [Observability](../../observability.md#otlp-traces--统一瀑布图)):**断言走自定义 `StreamEvent[]`,trace 归一到 OTel GenAI semconv,不发明私有 trace schema**。本篇是这个决定背后的对照材料。
6
+
7
+ ## 问题的两条路线
8
+
9
+ | | agent-eval(自定义) | OTel GenAI(标准) |
10
+ |---|---|---|
11
+ | 载体 | 扁平 JSON 事件数组(`TranscriptEvent[]`),无时间层级 | span 树(带起止时间、父子嵌套)+ 属性 + 事件 |
12
+ | 类型集 | 自定闭集:`message` / `tool_call` / `tool_result` / `thinking` / `error` | `gen_ai.operation.name` 枚举 + `gen_ai.*` 属性命名空间 |
13
+ | call/result 配对 | 无 id,靠顺序假设(并发会错配) | `gen_ai.tool.call.id` 显式配对 |
14
+ | 工具名 | 自建 canonical `ToolName` 映射表(每 agent 一份) | 保留原名(`gen_ai.tool.name`),**不做跨 agent 归一** |
15
+ | 消息内容 | transcript 全量保留(离线评测需要) | **opt-in**(敏感数据顾虑,默认不采) |
16
+ | 服务的场景 | 离线断言 / 评分 | 线上可观测 / APM 后端 |
17
+
18
+ 这两条路线不是竞争关系,是**场景不同**:eval 需要"完整、可断言的行为记录",可观测需要"标准、可跨系统聚合的遥测"。niceeval 两者都要,所以双轨。
19
+
20
+ ## OTel GenAI 定义了什么
21
+
22
+ 原本在 semantic-conventions 主仓库,2025 年后迁到独立仓库 [semantic-conventions-genai](https://github.com/open-telemetry/semantic-conventions-genai)(YAML model 生成 markdown,用 Weaver 管理对核心 semconv 的依赖)。**整体仍是 Development 状态**——键名可能变,做 mapper 时要留升级余量。参与方包括 Amazon、Elastic、Google、IBM、Microsoft。
23
+
24
+ ### Spans(核心)
25
+
26
+ 推理 span:`gen_ai.operation.name` = `chat` / `text_completion` 等,配 `gen_ai.request.model`、`gen_ai.request.temperature`、`gen_ai.usage.input_tokens` / `output_tokens`。
27
+
28
+ 工具执行 span:`gen_ai.operation.name = "execute_tool"`,配 `gen_ai.tool.name`、`gen_ai.tool.call.id`、`gen_ai.tool.description`、`gen_ai.tool.type`。
29
+
30
+ **agent spans**(比 niceeval [observability.md 的 kind 映射表](../../observability.md#canonical-目标--opentelemetry-genai-语义约定不发明私有-schema)收录的更多,mapper 可按需扩展):
31
+
32
+ | `gen_ai.operation.name` | span 命名 | 含义 |
33
+ |---|---|---|
34
+ | `create_agent` | `create_agent {gen_ai.agent.name}` | 创建(远程)agent |
35
+ | `invoke_agent` | `invoke_agent {gen_ai.agent.name}` | 调用 agent(分 client / internal 两种形态) |
36
+ | `invoke_workflow` | `invoke_workflow {gen_ai.workflow.name}` | 多 agent 编排的一次工作流 |
37
+ | `plan` | `plan {gen_ai.agent.name}` | agent 的规划 / 任务分解阶段 |
38
+
39
+ agent span 的必填属性是 `gen_ai.operation.name`(+远程形态的 `gen_ai.provider.name`);有值就填 `gen_ai.agent.name` / `gen_ai.agent.id`;消息内容(`gen_ai.input.messages` / `gen_ai.output.messages`)、`gen_ai.tool.definitions`、`gen_ai.system_instructions` 都是 **opt-in**。
40
+
41
+ ### Events(输入输出的全量记录)
42
+
43
+ **没有** per-message 事件(早期草案的 `gen_ai.user.message` / `gen_ai.assistant.message` 那套已废)。现在只有两个事件:
44
+
45
+ - `gen_ai.client.inference.operation.details` —— 一次补全的完整细节(聊天历史 + 参数),opt-in,"可以独立于 trace 存储输入输出"。消息体里工具调用是结构化 part:`{ "type": "tool_call", "id", "name", "arguments" }`,响应是 `{ "type": "tool_call_response", "id", … }` —— **也是显式 id 配对**。
46
+ - `gen_ai.evaluation.result` —— 评测结果(质量 / 准确性打分)。OTel 自己也在往 eval 场景伸手,值得持续关注。
47
+
48
+ ### 其它
49
+
50
+ - **MCP conventions**(`mcp.*` 命名空间):MCP 调用怎么记 span。coding agent 普遍带 MCP,以后 mapper 可能用上。
51
+ - **Provider-specific**:anthropic / openai / aws-bedrock / azure-ai-inference 各有一页扩展属性(`openai.*` 等)。
52
+ - **Metrics / Exceptions**:请求量、时延、token 计数等聚合指标;异常记法。
53
+
54
+ ## 其它标准:同一个问题的更多答案
55
+
56
+ ### OpenInference(Arize Phoenix)—— span kind 分类学最全
57
+
58
+ OTel 兼容的另一套约定,核心是每条 span 必带 `openinference.span.kind`,枚举比 OTel GenAI 的 operation.name 丰富得多:`LLM` / `TOOL` / `CHAIN` / `AGENT` / `RETRIEVER` / `RERANKER` / `EMBEDDING` / `GUARDRAIL` / `EVALUATOR` / `PROMPT`。消息不走事件,**打平成索引属性**:`llm.input_messages.<i>.message.role` / `.content`;工具调用带 `tool_call.id` + `tool_call.function.name` / `.arguments`;token 与成本:`llm.token_count.prompt|completion|total`(含 cache read/write 细分)、`llm.cost.prompt|completion|total`(USD)。多模态内容有 `message_content.type`(`text` / `image` / `audio` / `reasoning` / `tool_use`)。
59
+
60
+ 对照价值:`GUARDRAIL` / `EVALUATOR` / `RERANKER` 这些 kind 说明"span 语义角色"的枚举可以比 niceeval 现在的 `turn | model | tool | agent | other` 细;`llm.cost.*` 直接把成本进 trace,niceeval 是在 runner 侧按价格表算的(`src/runner/pricing.ts`),两种放法。
61
+
62
+ ### OpenLLMetry(Traceloop)—— gen_ai.* 的前身推手
63
+
64
+ 同样是 OTel 扩展,历史意义大于当下差异:它早期的 `llm.*` 属性实践直接推动了 OTel GenAI `gen_ai.*` 的标准化,现在基本向 gen_ai 收敛。另有一层实体 span(workflow / task / agent / tool)描述应用结构。可以当作"社区实现先行、再被标准吸收"的案例——niceeval 的 mapper 归一到 gen_ai 而不是任何厂商方言,就是押这个方向。
65
+
66
+ ### OpenAI Agents SDK traces —— 带类型的私有 span 树
67
+
68
+ 自己的一套(可经 processor 导出到 OTel 系后端):`Trace{ workflow_name, trace_id, group_id }` + 强类型 span:`agent_span` / `generation_span` / `function_span`(工具调用)/ `handoff_span` / `guardrail_span` / `transcription_span` / `speech_span` / `custom_span`。两个值得记的点:**`handoff_span` 是它独有的语义**(agent 间移交,OTel 里只能拿 `invoke_agent` 近似);**敏感数据是开关**(`RunConfig.trace_include_sensitive_data` 控制 generation/function span 是否含输入输出)——和 OTel 把消息内容做成 opt-in 是同一个顾虑的不同解法。
69
+
70
+ ### AG-UI —— 和 niceeval `StreamEvent` 同形态的扁平事件流
71
+
72
+ 前面几个都是 trace(span 树);AG-UI 不是 telemetry,是 **agent ↔ 前端的流式事件协议**,但它的形态和 niceeval 的 `StreamEvent[]` 最像:扁平事件序列 + 显式 id 配对。事件词汇:生命周期 `RunStarted` / `RunFinished` / `RunError` / `StepStarted|Finished`;消息三段式 `TextMessageStart`(定 `messageId` + role)→ `TextMessageContent`(delta)→ `TextMessageEnd`;工具调用镜像同构 `ToolCallStart`(定 `toolCallId` + name)→ `ToolCallArgs`(delta)→ `ToolCallEnd` → **`ToolCallResult`**;另有状态同步(`StateSnapshot` / `StateDelta`,RFC 6902 JSON Patch)和 `Reasoning*` 系列。
73
+
74
+ 对照价值:**又一个用显式 id(`toolCallId`)配对 call/result 的设计**;它把"流式增量"(start/content/end 三段式)做进了词汇,而 niceeval 的 `StreamEvent` 是事后整段的——评测离线跑,不需要增量,这是场景差异不是缺陷。若将来 view 要做"实时看 agent 跑",AG-UI 是现成参考。
75
+
76
+ ### Langfuse —— trace + 观测树,建在 OTel 上
77
+
78
+ LLM 工程平台的数据模型:`Trace`(一次请求)+ 嵌套 `Observation`(span / generation / event 几类),外加 `Session`(聚合多 trace 的多轮会话)和 `Score`(评分挂到 trace/observation 上)。底层已改为 OTel。对照价值:它的 `Session → Trace → Observation` 三层和 niceeval 的 `run → session → turn → events` 作用域链是同构的;`Score` 挂在任意层的设计对应 niceeval "断言作用域由接收者决定"。
79
+
80
+ ## 汇总对照
81
+
82
+ | 标准 | 形态 | 工具调用配对 | 消息内容 | 工具名归一 | 状态 |
83
+ |---|---|---|---|---|---|
84
+ | agent-eval(自定义) | 扁平事件数组 | **顺序假设,无 id** | 全量 | 自建 canonical 表 | 私有 |
85
+ | [eve `HandleMessageStreamEvent`](eve-protocol.md) | **扁平事件流**(带 sequence / turnId / stepIndex 坐标) | `callId` | 全量(流式 delta) | 无需(自有运行时) | 私有,显式版本号(v16) |
86
+ | OTel GenAI | span 树 + opt-in 事件 | `gen_ai.tool.call.id` | opt-in | 无(保留原名) | Development |
87
+ | OpenInference | span 树(打平属性) | `tool_call.id` | 打平进属性 | 无 | 稳定使用中 |
88
+ | OpenLLMetry | span 树 | 同 gen_ai | 可配 | 无 | 向 gen_ai 收敛 |
89
+ | OpenAI Agents SDK | 私有类型化 span 树 | function_span 内 | 开关控制 | 无 | 私有,可导出 |
90
+ | AG-UI | **扁平事件流** | `toolCallId` | 全量(流式 delta) | 无 | 协议 v1 |
91
+ | niceeval `StreamEvent` | **扁平事件流** | `callId` | 全量 | `tool: ToolName` | 本仓库 |
92
+
93
+ ## 对 niceeval 的印证与启发
94
+
95
+ 1. **显式 id 配对是共识,agent-eval 是孤例。** OTel(`gen_ai.tool.call.id`)、OpenInference(`tool_call.id`)、AG-UI(`toolCallId`)、OTel 事件体(`tool_call` / `tool_call_response` 的 `id`)全部显式配对;只有 agent-eval 靠顺序。niceeval `StreamEvent` 的 `callId` 站在多数这边(契约纪律见 [Adapter 契约](../contract.md#标准事件流))。
96
+ 2. **工具名跨 agent 归一是 eval 特有的需求,没有任何标准替你做。** 所有标准都保留原始工具名——它们的用户只看自己一个系统,不需要"Claude 的 `Bash` 和 Codex 的 `shell` 是同一件事"。要写 `calledTool("shell")` 这种跨 agent 断言,canonical `ToolName` 映射表(agent-eval 先做,niceeval 沿用)必须自己维护,这层没法外包给标准。
97
+ 3. **"拿 OTel 当唯一数据源"不可行,transcript 侧不可替代。** OTel 把消息内容、工具入参这些 eval 最需要断言的东西做成 opt-in(线上敏感数据顾虑);我们又不控制 agent 的 instrumentation(coding agent CLI 发什么是什么)。所以断言数据源必须走 transcript → `StreamEvent[]`,trace 只补"各花了多久、谁套谁"——双轨不是冗余,是两个 schema 各自只解决一半问题。
98
+ 4. **mapper 的归一目标可以逐步加宽。** agent spans 新增了 `invoke_workflow` / `plan`,provider 页和 `mcp.*` 在增长;OTel 自己出了 `gen_ai.evaluation.result` 事件。niceeval 的 `SpanKind` 现在收 `turn | model | tool | agent | other` 够用,将来要区分 plan / handoff / guardrail 时,先看标准里有没有现成词,别自造。
99
+ 5. **semconv 仍是 Development,mapper 要按"键名会变"来写。** 归一逻辑集中在每 agent 一个薄 mapper 里(而不是散在 view / select),正是为了键名漂移时只改一处——这个仓库结构决定(见 [Observability](../../observability.md#每个-agent-一个薄-mapper))被"标准还没稳"进一步坐实。
100
+
101
+ ## 相关阅读
102
+
103
+ - [agent-loop-apis.md](agent-loop-apis.md) —— 四个主流 agent loop 的原生 API 面(这些标准的生产者侧)。
104
+ - [otel-instrumentation.md](otel-instrumentation.md) —— 应用侧埋点里内容默认采不采(数据可得性)。
105
+ - [agent-eval 笔记](agent-eval.md) —— 自定义路线的具体实现(采集 / 转换 / 落地、顺序配对的坑)。
106
+ - [Observability](../../observability.md) —— niceeval 的双轨:StreamEvent 断言 + canonical GenAI trace。
107
+ - [Adapter 契约](../contract.md) —— `StreamEvent` 词汇与逐断言的数据义务。
@@ -0,0 +1,65 @@
1
+ # 应用侧 OTel 埋点生态 —— span 里到底有没有 eval 要的数据(调研记录)
2
+
3
+ **来源:** 各生态官方文档 / spec / 源码(2026-07 抓取,URL 见各节)。**问题:** 如果被测 agent 已经接了 OTel(应用把 `OTEL_EXPORTER_OTLP_ENDPOINT` 指向 niceeval 起的本机接收器),能不能从 spans 里还原出「调了什么工具、入参出参、说了什么、用了多少 token」——这是评估"从 span 派生事件"这条路线是否可行的数据可得性调研(结论:不划算,niceeval 现在事件流一律走 `send`,OTel 只用来画 trace 瀑布图,见 [Observability](../../observability.md#otlp-traces--统一瀑布图))。[otel-genai.md](otel-genai.md) 讲的是这些生态的 **schema 长什么样**;本篇只回答**内容默认采不采、字段在哪**。
4
+
5
+ ## 结论先行
6
+
7
+ | 生态 | 工具入参/出参 | assistant 消息文本 | token | 内容默认状态 |
8
+ |---|---|---|---|---|
9
+ | **Vercel AI SDK** `experimental_telemetry` | `ai.toolCall.args` / `.result`(单属性直拿) | `ai.prompt.messages` / `ai.response.text` | `ai.usage.*` + 标准 `gen_ai.usage.*` | **默认全采**(`recordInputs/recordOutputs` 默认 true;telemetry 总开关 `isEnabled` 需应用自己开) |
10
+ | **OpenLLMetry**(Traceloop) | `gen_ai.{prompt,completion}.{i}.tool_calls.{j}.name/.arguments`(工具**执行结果**只在框架级埋点的 tool span 里有) | `gen_ai.prompt/completion.{i}.content` | `gen_ai.usage.*` | **默认全采**(`TRACELOOP_TRACE_CONTENT` 默认 true) |
11
+ | **OpenInference**(Arize) | `tool_call.function.name/arguments` + TOOL span 的 `input.value` / `output.value` | `llm.input_messages` / `llm.output_messages` | `llm.token_count.*`(含 cache/reasoning 细分) | **默认全采**(`OPENINFERENCE_HIDE_*` 是 opt-out,屏蔽值写 `__REDACTED__` 哨兵) |
12
+ | **OTel 官方 contrib** / gen_ai semconv | `gen_ai.tool.call.arguments/result`(**Opt-In**) | `gen_ai.input/output.messages`(**Opt-In**) | `gen_ai.usage.*`(Recommended,默认有) | **内容一律 opt-in,默认只有元数据 + token**(`OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT`) |
13
+
14
+ 关键事实:**三个第三方生态内容默认全采、opt-out;只有 OTel 官方埋点默认不采。** 数据最全且最结构化的是 OpenInference(span kind 分类 + 工具/消息/token 全套);TS 应用最常见的现成埋点是 AI SDK 的 `ai.*`(工具入出参一个属性直拿)。
15
+
16
+ ## 各生态细节
17
+
18
+ ### Vercel AI SDK `experimental_telemetry`
19
+
20
+ - span 集:`ai.generateText`(根)→ `ai.generateText.doGenerate`(每次 provider 调用)、`ai.toolCall`(每次工具执行);streamText / embed 同构。
21
+ - `ai.toolCall` span:`ai.toolCall.name` / `.id` / `.args` / `.result`(执行成功且可序列化时)。**工具名、入参、出参默认全在**。
22
+ - 文本:根 span `ai.prompt` / `ai.response.text` / `ai.response.toolCalls`;doGenerate span `ai.prompt.messages`(发给 provider 的完整消息)。
23
+ - 命名空间:主体 `ai.*` 私有,但 doGenerate span 同时带标准 `gen_ai.system` / `gen_ai.request.*` / `gen_ai.response.*` / `gen_ai.usage.input_tokens` / `output_tokens`——**token 有标准名,内容只有私有名**。
24
+ - 来源:https://ai-sdk.dev/docs/ai-sdk-core/telemetry
25
+
26
+ ### OpenLLMetry(Traceloop)
27
+
28
+ - 覆盖:OpenAI / Anthropic / Bedrock / Gemini 等 LLM SDK(Python+TS),LangChain / LlamaIndex / OpenAI Agents / CrewAI / LiteLLM 等框架(多为 Python-only),外加向量库和 MCP。
29
+ - 内容:官方原话 "By default, OpenLLMetry logs prompts, completions, and embeddings to span attributes";工具调用 flatten 成 `gen_ai.{prompt,completion}.{i}.tool_calls.{j}.id/.name/.arguments`(源码确认)。
30
+ - 注意:这套 `gen_ai.prompt/completion.{i}.*` 带索引写法已被最新 OTel GenAI semconv 标 deprecated(traceloop/openllmetry#3515 在迁移)——**采得全,但词汇是旧/自有的,接收侧要按 OpenLLMetry 方言解析**。
31
+ - 来源:https://www.traceloop.com/docs/openllmetry/{tracing/supported,privacy/traces,contributing/semantic-conventions}
32
+
33
+ ### OpenInference(Arize)
34
+
35
+ - 覆盖面最广:Python 20+ 框架(OpenAI / Anthropic / **Claude Agent SDK** / LangChain / OpenAI Agents / PydanticAI / smolagents / MCP …);JS 含 OpenAI / Anthropic / LangChain.js / **Vercel AI SDK**(可把 `ai.*` span 转成 OpenInference 词汇)。
36
+ - 属性:`openinference.span.kind`(LLM / TOOL / AGENT / CHAIN …)+ `llm.input_messages` / `llm.output_messages` / `message.tool_calls.{i}.tool_call.function.name/.arguments` / `tool_call.id`;专门的 TOOL span 有 `tool.name` + `input.value` / `output.value`。
37
+ - 已知坑:langchain instrumentation 的 `input.value` / `output.value` 曾被写成 Python repr 而非 JSON(Arize-ai/openinference#2827),解析要容错。
38
+ - 来源:https://github.com/Arize-ai/openinference (spec/semantic_conventions.md、spec/tool_calling.md、spec/configuration.md)
39
+
40
+ ### OTel 官方 contrib(instrumentation-genai)
41
+
42
+ - 现有 8 个包全部 development 状态:`openai-v2`、`anthropic`、`claude-agent-sdk`、`google-genai`、`vertexai`、`langchain`、`openai-agents-v2`、`weaviate`。LangGraph 无独立包(靠 langchain callbacks 覆盖)。
43
+ - 内容捕获:官方原话 "Message content such as … function arguments and return values are not captured by default";开启后可选 `span_only` / `event_only` / `span_and_event` 决定内容落 span 属性还是 log event。
44
+ - gen_ai semconv 本身仍全部 **Development**(独立仓库 semantic-conventions-genai);execute_tool span 的 `gen_ai.tool.call.arguments` / `.result` 要求级别 **Opt-In**。
45
+ - 来源:https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation-genai 、https://github.com/open-telemetry/semantic-conventions-genai
46
+
47
+ ## OTLP 接收侧(进程内,不装 collector)
48
+
49
+ - OTLP/HTTP = POST `{endpoint}/v1/traces`,body 是 `ExportTraceServiceRequest`,`Content-Type` 二选一:`application/x-protobuf`(Node SDK 默认)或 `application/json`。应用侧 `OTEL_EXPORTER_OTLP_PROTOCOL=http/json|http/protobuf` 可切。
50
+ - OTLP/JSON 对 proto3 JSON mapping 有**规范级偏离**:`traceId` / `spanId` 是 **hex 字符串而非 base64**(spec 明文)——用通用 proto3 JSON 解析器会静默解错 ID。
51
+ - 响应契约:回 `ExportTraceServiceResponse`,且响应 Content-Type 要与请求编码一致(Langfuse 曾因对 protobuf 请求回 JSON 被客户端报错,langfuse#11550)。
52
+ - 先例:Langfuse 自建 OTLP endpoint 只收 HTTP 的 JSON + protobuf(明确 "gRPC not supported yet"),且**同时映射四套词汇**(gen_ai semconv / OpenInference / OpenLLMetry / MLflow)——证明「进程内 HTTP 收 OTLP + 接收侧按方言归一」是被大规模验证过的做法。niceeval 的 `src/o11y/otlp/receiver.ts` 已经是同款形状(JSON + protobuf、gzip、按 content-type 回应)。
53
+ - 来源:https://opentelemetry.io/docs/specs/otlp/ 、https://langfuse.com/integrations/native/opentelemetry
54
+
55
+ ## 对 niceeval 的印证与启发
56
+
57
+ 1. **mixin 的数据可得性成立,但按生态分层。** 被测应用用的是 AI SDK telemetry / OpenLLMetry / OpenInference 之一(TS/Python AI 应用的大多数)→ 工具入出参和消息文本**默认就在 span 里**,「从 spans 派生 `StreamEvent[]`」有米下锅;用的是 OTel 官方 instrumentation → 默认只有骨架 + token,mixin 要提示用户开 `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` 才能解锁内容断言。
58
+ 2. **方言表是接收侧的固定成本,Langfuse 已趟过路。** 四套词汇(gen_ai 新版 / OpenLLMetry 旧式索引 / OpenInference / `ai.*`)各自的工具调用字段都不同,但每套内部结构清楚、有显式 call id——一方言一个薄解析器,与 niceeval 既有的「每 agent 一个薄 mapper」是同一个形状。
59
+ 3. **接收器不用动。** 现有 receiver 已满足 OTLP/HTTP 双编码 + gzip + 正确响应契约;它现在只服务 trace 瀑布图,见 [Observability](../../observability.md#otlp-traces--统一瀑布图)。
60
+
61
+ ## 相关阅读
62
+
63
+ - [agent-loop-apis.md](agent-loop-apis.md) —— 四个 agent loop 的原生 API 面(转换路线的对照组)。
64
+ - [otel-genai.md](otel-genai.md) —— 这些生态的 schema 全景与 niceeval 双轨结论。
65
+ - [Observability · OTLP traces](../../observability.md#otlp-traces--统一瀑布图) —— OTel 在当前设计里的实际角色(只画瀑布图,不产出事件)。
@@ -0,0 +1,99 @@
1
+ # 接入目标矩阵 —— 12 个被测对象怎么接、代码共享在哪(调研 + 路线图)
2
+
3
+ > 状态:调研结论 + 接入路线图(2026-07 外部调研,来源见文末),尚未实现的目标都只是提案。通道定义见 [collection.md](collection.md)。事件流的实际接入路线是[官方转换器 / SDK 通道 0 或手写映射](collection.md#接入路线的优先级);OTel 只喂 trace 瀑布图,见 [Observability](../observability.md#otlp-traces--统一瀑布图)。
4
+
5
+ 回答三个问题:各家 OTel 的 span"标题"统一吗;每个目标好不好接;接的时候哪段代码共享。
6
+
7
+ ## 一、OTel 标题不统一,而且短期不会统一
8
+
9
+ 同一次「LLM 调用 + 工具调用」,六家的 span/事件名各是一样:
10
+
11
+ | 被测对象 | "标题"长什么样 | kind 字段 | 内容(prompt/工具入出参)默认 |
12
+ |---|---|---|---|
13
+ | OTel GenAI semconv(规范本体) | `chat {model}` / `invoke_agent {agent}` / `execute_tool {tool}` | `gen_ai.operation.name` | **opt-in**(`gen_ai.input/output.messages` 属性) |
14
+ | AI SDK 新模式(`@ai-sdk/otel`,官方推荐) | 遵循 semconv:`invoke_agent {modelId}` → `chat {modelId}` → `execute_tool {tool}` | `gen_ai.operation.name` | **默认全采**(`recordInputs/Outputs` 可关) |
15
+ | AI SDK legacy(`experimental_telemetry`) | `ai.generateText(.doGenerate)` / `ai.toolCall` | `operation.name`(`ai.*` 私有) | 默认全采 |
16
+ | LangGraph(LangSmith OTel 路线) | span 名 = run name(`ChatOpenAI`、节点名),非 semconv 格式 | `langsmith.span.kind` + 部分 `gen_ai.*` 混合 | 默认全采 |
17
+ | OpenAI Agents SDK(官方 contrib 转换后) | 严格 semconv;handoff 是**规范外自造**:`agent_handoff {agent}`(`gen_ai.operation.name="agent_handoff"`) | `gen_ai.operation.name` | 默认采(env 可关) |
18
+ | Claude Code(traces beta) | `claude_code.interaction` → `claude_code.llm_request` / `claude_code.tool` | 私有 `claude_code.*` 掺部分 `gen_ai.*` 属性 | **默认 `<REDACTED>`**(三个 `OTEL_LOG_*` 开关) |
19
+ | codex CLI | 文档只承认 `codex.*` log events;实测 OTLP 里另有内部 tracing spans(`codex.exec` / `run_sampling_request`…),`mappers/codex.ts` 就是对着它写的 | 私有 | `log_user_prompt` 默认 false |
20
+
21
+ 背景事实:GenAI semconv 已迁到独立仓库 `semantic-conventions-genai`,整套仍标 **Development**,规范明文允许各家自定义 span 名格式;`gen_ai.operation.name` 的取值集合里**没有 handoff**(contrib 自造,与 [multi-agent 提案](../multi-agent.md)的 `handoff` 事件互为印证)。内容记录方式已从 events 改为 span 属性(`gen_ai.input/output.messages`),旧的 `gen_ai.prompt/completion` 已弃用但第三方仍在用。
22
+
23
+ 两个结论,都印证既有设计、并给它添料:
24
+
25
+ 1. **收敛点只能放 mapper 层**([observability.md](../observability.md) 的"canonical = GenAI semconv,每家一个薄 mapper,view 只认 canonical"是唯一现实做法)。新料:AI SDK 官方新模式和 OpenClaw(见下)原生就说 semconv——这两家的 mapper 近乎透传;canonical 层要给 `agent_handoff` 这类规范外 operation 留通道。
26
+ 2. **OTel 现在只能当时间轨,不管框架埋点采不采内容都一样。** 曾经设想过用"内容默认矩阵"判断 OTel 能不能当行为轨(框架系埋点默认全采 → 直接派生事件),这条路线已经从实现里移除——不管是框架系埋点(AI SDK / LangSmith / OpenInference / OpenLLMetry)还是 CLI 系(claude-code / codex)默认脱敏,行为轨(事件流)一律走 transcript / 官方转换器 / 手写映射,OTel 只画瀑布图。
27
+
28
+ ## 二、矩阵 · Agent Software(沙箱型 CLI,`defineSandboxAgent` 路线)
29
+
30
+ | | claude-code | codex | bub | **OpenClaw** | **Hermes Agent** | **Alma** |
31
+ |---|---|---|---|---|---|---|
32
+ | 状态 | ✅ 已接 | ✅ 已接 | ✅ 已接 | 提案 | 提案(低优先) | ❌ 不接 |
33
+ | 驱动面 | `claude --print` | `codex exec --json` | `bub run --session-id` | `openclaw agent --message --json`(`--local` 免 gateway) | `hermes chat -q`(无 `--json` 契约) | 无:闭源桌面 GUI,无 CLI/API |
34
+ | 会话续接 | `--resume <id>` | `codex exec resume <id>` | `--session-id` | `--session-id` / `--session-key` | `--resume <id>` / `-c` | — |
35
+ | 行为轨通道 | 磁盘旁读 JSONL | stdout 捕获 | 磁盘旁读 tape | 磁盘旁读 JSONL:`~/.openclaw/agents/<id>/sessions/<sid>.jsonl`(role/content 块/toolCall/usage.cost) | 磁盘旁读 **SQLite**:`~/.hermes/state.db`(messages 表含 tool_calls,FTS5) | — |
36
+ | 时间轨(OTel) | 私有 schema,默认脱敏 → transcript 合成 span | 实测 spans,`mappers/codex.ts` | OTLP/protobuf,`mappers/bub.ts` | **原生 OTLP + GenAI semconv**(`diagnostics-otel` 插件,`captureContent.*` 可开内容) | 第三方插件(gen_ai + OpenInference 双语义) | — |
37
+ | 安装(沙箱) | npm | npm | uv | npm(`onboard --non-interactive`) | curl 脚本,依赖重(Python+Node+ffmpeg),无官方 Docker 路径 | 仅 .dmg/.exe |
38
+ | 难度 | — | — | — | **低:五个接入面全绿** | 中:采集要 SQLite 读取件,镜像要预制 | 不可接 |
39
+
40
+ - **OpenClaw 是"下一个该接的"**:驱动面形状与 claude-code 同构(headless 单发 + session id + JSONL 旁读),`defineSandboxAgent` + `shared.captureLatestJsonl` 骨架直接复用,只写一个 parser;时间轨 mapper 近乎透传(原生 semconv)。它是三类目标里唯一"原生说标准话"的。
41
+ - **Hermes 的增量成本在两处**:采集层缺一个 SQLite 读取件(通道 1 的变体——旁读的不是 JSONL 是 db 文件;读出行后转换层纪律不变),沙箱镜像要预制(依赖重,同 e2b 模板的经验)。OTel 靠第三方插件,时间轨列为可选。
42
+ - **Alma 展示决策树第一问的价值**:没有程序化驱动面就没有 adapter 可写,直接出局,不要为它发明逆向路线。
43
+
44
+ ## 三、矩阵 · Agent Frameworks(进程内 / 服务型,`defineAgent` 路线)
45
+
46
+ | | AI SDK | **Claude SDK** | **Codex SDK** | **LangGraph** | **Cursor Agent SDK** | **vm0** |
47
+ |---|---|---|---|---|---|---|
48
+ | 状态 | ✅ 已接(`fromAiSdk` / `uiMessageStreamAgent`) | 提案 | 提案 | 提案 | 提案(观察 beta) | ❌ 暂不接 |
49
+ | 调用面 | `generateText` | `query()` → AsyncGenerator\<SDKMessage> | `startThread()` → `thread.run()`(SDK 内部 spawn CLI) | `graph.invoke()` 或未接管理层的服务 | `Agent.create()` → `run.stream()`(local/cloud/self-hosted 三 runtime) | 平台非库:CLI/REST + Ably,无 npm SDK |
50
+ | 事件流 | steps 带 `toolCallId` | content 块 + `parent_tool_use_id`(subagent 归属) | ThreadItem(item 内嵌输入输出,免配对) | messages 通道显式配对;或 spans | `SDKMessage`:`tool_call` 带 call_id/args/result,**`request` 事件 = 现成 HITL** | 事件 schema 未公开 |
51
+ | 会话 | 工厂自管 | `resume` / `forkSession` | `resumeThread(threadId)` | thread_id(checkpointer) | `Agent.resume(agentId)` | resume API 未公开 |
52
+ | HITL | v7 tool approval ✅ | `canUseTool` 回调 | 缺口:审批在 app-server 协议层,SDK 无回调,eval 场景只能 `never` | `interrupt()`(v0.2+) | `request` 事件 + hooks | 未公开 |
53
+ | 遥测 | legacy `ai.*` / 新 `@ai-sdk/otel` semconv | 同 claude-code CLI(beta,SDK 路径 span 树有已知残缺) | 同 codex CLI | LangSmith OTel 三 env(混合 schema)或三方埋点 | **无 OTel**(官方建议拿 requestId 自己关联) | 内部 Axiom,无用户导出面 |
54
+ | 难度 | — | 中 | **低** | 中(要写精品转换器或手写映射,不再有 mixin 捷径) | 中(beta 风险) | 高且不稳定 |
55
+
56
+ - **Codex SDK 是复用率最高的一个**:ThreadItem 与 `codex exec --json` 的 item 词汇同源(与 app-server 共享),`o11y/parsers/codex.ts` 的映射逻辑大半直接复用——把"stdout 捕获 + 抠 JSONL"升级成"通道 0 SDK 直构",采集层整段消失。设计判断:作为**现有 codex adapter 的第二形态**(进程内、不需要沙箱时用),不是替代。
57
+ - **Claude SDK 同理**:SDKMessage 的 content 块结构 ≈ transcript 行形状,`parsers/claude-code.ts` 的块翻译逻辑可抽出共享核心;SDK 还有官方 `getSessionMessages()`,连磁盘旁读都省了。`parent_tool_use_id` 直接喂 [multi-agent 提案](../multi-agent.md)的归属字段。
58
+ - **LangGraph 要写精品转换器或手写映射**:事件流需要消费 LangGraph 自己的 messages 结构(手写映射,或将来补一个精品转换器),OTel(`LANGSMITH_OTEL_ENABLED` 三个 env)只用来画瀑布图;HITL/会话按契约补 `send`。
59
+ - **Cursor Agent SDK 值得盯**:事件词汇是六家里最贴 niceeval 契约的(`tool_call` 生命周期事件、`request` ≈ `input.requested`、usage 字段最全),`fromCursorSdk` 会比 `fromAiSdk` 还薄;但 public beta 声明 API 会变、无 OTel、三套 runtime——等 GA 再动。
60
+ - **vm0 记为观察项**:事件 schema / resume / usage 全未公开,定位还在从 runtime 向托管 teammate 漂移;等接入面稳定,不要对着移动目标写 adapter。
61
+
62
+ ## 四、代码共享在哪:三层各自的共享件
63
+
64
+ 接入成本 = 驱动 × 采集 × 转换 三层之和;每层能共享的件和为新目标要新写的件:
65
+
66
+ ```text
67
+ 驱动层 defineAgent / defineSandboxAgent / uiMessageStreamAgent(已有)
68
+ 新增:无。OpenClaw/Hermes 走 defineSandboxAgent;四个 SDK 走 defineAgent
69
+ 采集层 shared 工具袋:ensureInstalled / captureLatestJsonl / extractJsonlFromStdout /
70
+ firstJsonField / sessionIdFrom*(已有)
71
+ 新增:SQLite 读取件(仅 Hermes 要);SDK 直构目标(Codex/Claude/Cursor SDK)采集层为零
72
+ 转换层 ① CLI parser(o11y/parsers/*):每 CLI 一个纯函数
73
+ 复用对:codex CLI ↔ Codex SDK(同一 item 词汇)、claude-code ↔ Claude SDK(同块结构)
74
+ ——共享的是映射核心,不是文件本身;新写:OpenClaw、Hermes 各一个
75
+ ② 精品转换器(fromAiSdk 已有):高频 + 进程内 + 有 HITL 的框架才配
76
+ 候选:fromCursorSdk(等 GA)
77
+ ③ 长尾框架仍需手写映射或精品转换器,没有免写事件映射的捷径
78
+ 方言:gen_ai semconv(OpenClaw / AI SDK 新模式 / Agents SDK contrib 共用一个)、
79
+ ai.* legacy、OpenInference、OpenLLMetry、LangSmith 混合
80
+ 时间轨 OTLP receiver + canonical mapper(已有)
81
+ 新写 mapper:OpenClaw 近乎透传;LangSmith 混合方言一个;其余复用或跳过
82
+ ```
83
+
84
+ [collection.md 决策树](collection.md#接新被测对象的决策树)相应多一问,插在"应用 HTTP 接口天生带完整调用记录吗 → 否"之后:**官方有没有 SDK 包装?有 → 当通道 0 接(SDK 直构),比逆向它的 stdout/磁盘干净**——Codex SDK 与 Cursor SDK 证明"未接管理层的 CLI"正在变成"有官方 SDK 包装的对象",这条通道会越来越常见。
85
+
86
+ ## 五、优先级建议
87
+
88
+ 1. **OpenClaw**——五面全绿,`defineSandboxAgent` 第四个内置,新代码只有一个 parser + 一个近透传 mapper。
89
+ 2. **Codex SDK**——现有 codex adapter 的进程内第二形态,parser 复用大半,验证"SDK 通道 0"这条新决策树分支。
90
+ 3. **LangGraph**——写手写映射或精品转换器消费 LangGraph 的 messages 结构。
91
+ 4. **Claude SDK**——受益于 multi-agent 提案(`parent_tool_use_id` 归属),两个提案互相解锁。
92
+ 5. **Cursor Agent SDK**——等 GA;**Hermes**——等有真实需求再付镜像成本。
93
+ 6. 不接:**Alma**(无驱动面)、**vm0**(接入面未稳定)。记下判据,免得反复重查。
94
+
95
+ ## 来源
96
+
97
+ - 三路外部调研(2026-07):OpenClaw(docs.openclaw.ai:cli/agent、gateway/opentelemetry、concepts/session)、Hermes Agent(hermes-agent.nousresearch.com docs、briancaffey/hermes-otel)、Alma(alma.now、yetone/alma-releases)、Codex SDK(developers.openai.com/codex/sdk、openai/codex sdk/typescript)、Cursor Agent SDK(cursor.com/docs/sdk/typescript)、vm0(github.com/vm0-ai/vm0)。
98
+ - OTel 核实:semantic-conventions-genai 仓库(gen-ai-spans / agent-spans / events / registry)、ai-sdk.dev telemetry 文档、docs.langchain.com trace-with-opentelemetry、openai-agents contrib `span_processor.py` 源码、code.claude.com monitoring-usage、developers.openai.com codex config-advanced。
99
+ - 站内既有:[agent-loop-apis.md](reference/agent-loop-apis.md)(Claude SDK / LangGraph 细节)、[otel-instrumentation.md](reference/otel-instrumentation.md)(埋点内容矩阵)、[collection.md](collection.md)(通道)。