niceeval 0.4.3 → 0.4.5

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 (179) hide show
  1. package/README.md +21 -12
  2. package/README.zh.md +8 -9
  3. package/docs-site/.mintignore +13 -0
  4. package/docs-site/AGENTS.md +46 -0
  5. package/docs-site/concepts/adapter.mdx +287 -0
  6. package/docs-site/concepts/assert.mdx +243 -0
  7. package/docs-site/concepts/drive.mdx +118 -0
  8. package/docs-site/concepts/evals.mdx +108 -0
  9. package/docs-site/concepts/experiment.mdx +37 -0
  10. package/docs-site/concepts/hitl.mdx +83 -0
  11. package/docs-site/concepts/judge.mdx +129 -0
  12. package/docs-site/concepts/overview.mdx +98 -0
  13. package/docs-site/concepts/tier.mdx +42 -0
  14. package/docs-site/docs-ref/00-index.md +23 -0
  15. package/docs-site/docs-ref/01-page-types.md +43 -0
  16. package/docs-site/docs-ref/03-style-rules.md +45 -0
  17. package/docs-site/docs-ref/04-code-examples.md +37 -0
  18. package/docs-site/docs-ref/05-dx-failure-paths.md +45 -0
  19. package/docs-site/docs-ref/06-checklists.md +53 -0
  20. package/docs-site/docs.json +256 -0
  21. package/docs-site/example/ai-agent-application.mdx +148 -0
  22. package/docs-site/example/claude-code-codex-plugin.mdx +162 -0
  23. package/docs-site/example/claude-code-codex-skill.mdx +147 -0
  24. package/docs-site/example/showcase.mdx +39 -0
  25. package/docs-site/example/tier1-ai-sdk-v7.mdx +136 -0
  26. package/docs-site/example/tier1-claude-sdk.mdx +119 -0
  27. package/docs-site/example/tier1-codex-sdk.mdx +111 -0
  28. package/docs-site/example/tier1-langgraph.mdx +119 -0
  29. package/docs-site/example/tier1-pi-sdk.mdx +119 -0
  30. package/docs-site/favicon.svg +5 -0
  31. package/docs-site/github-diff.css +135 -0
  32. package/docs-site/github-diff.js +11 -0
  33. package/docs-site/guides/authoring.mdx +129 -0
  34. package/docs-site/guides/ci-integration.mdx +97 -0
  35. package/docs-site/guides/connect-otel.mdx +209 -0
  36. package/docs-site/guides/connect-your-agent.mdx +221 -0
  37. package/docs-site/guides/dataset-fanout.mdx +98 -0
  38. package/docs-site/guides/experiments.mdx +71 -0
  39. package/docs-site/guides/fixtures.mdx +147 -0
  40. package/docs-site/guides/reporters.mdx +113 -0
  41. package/docs-site/guides/runner.mdx +79 -0
  42. package/docs-site/guides/sandbox-agent.mdx +138 -0
  43. package/docs-site/guides/sandbox-backends.mdx +64 -0
  44. package/docs-site/guides/scoring-guide.mdx +92 -0
  45. package/docs-site/guides/viewing-results.mdx +195 -0
  46. package/docs-site/guides/write-experiment.mdx +81 -0
  47. package/docs-site/guides/write-send.mdx +347 -0
  48. package/docs-site/images/adapter-tiers-zh.svg +60 -0
  49. package/docs-site/images/agent-turn-roundtrip-en.svg +62 -0
  50. package/docs-site/images/agent-turn-roundtrip-zh.svg +77 -0
  51. package/docs-site/images/hitl-handshake-zh.svg +89 -0
  52. package/docs-site/images/logo.svg +6 -0
  53. package/docs-site/index.mdx +181 -0
  54. package/docs-site/introduction.mdx +141 -0
  55. package/docs-site/quickstart.mdx +136 -0
  56. package/docs-site/reference/builtin-agents.mdx +161 -0
  57. package/docs-site/reference/capabilities.mdx +76 -0
  58. package/docs-site/reference/cli.mdx +120 -0
  59. package/docs-site/reference/define-agent.mdx +168 -0
  60. package/docs-site/reference/define-config.mdx +41 -0
  61. package/docs-site/reference/define-eval.mdx +160 -0
  62. package/docs-site/reference/events.mdx +131 -0
  63. package/docs-site/reference/expect.mdx +112 -0
  64. package/docs-site/tracker.js +8 -0
  65. package/docs-site/zh/concepts/adapter.mdx +286 -0
  66. package/docs-site/zh/concepts/assert.mdx +243 -0
  67. package/docs-site/zh/concepts/drive.mdx +118 -0
  68. package/docs-site/zh/concepts/evals.mdx +124 -0
  69. package/docs-site/zh/concepts/experiment.mdx +37 -0
  70. package/docs-site/zh/concepts/hitl.mdx +83 -0
  71. package/docs-site/zh/concepts/judge.mdx +130 -0
  72. package/docs-site/zh/concepts/overview.mdx +109 -0
  73. package/docs-site/zh/concepts/tier.mdx +44 -0
  74. package/docs-site/zh/example/ai-agent-application.mdx +152 -0
  75. package/docs-site/zh/example/claude-code-codex-plugin.mdx +163 -0
  76. package/docs-site/zh/example/claude-code-codex-skill.mdx +147 -0
  77. package/docs-site/zh/example/showcase.mdx +39 -0
  78. package/docs-site/zh/example/tier1-ai-sdk-v7.mdx +140 -0
  79. package/docs-site/zh/example/tier1-claude-sdk.mdx +122 -0
  80. package/docs-site/zh/example/tier1-codex-sdk.mdx +117 -0
  81. package/docs-site/zh/example/tier1-langgraph.mdx +123 -0
  82. package/docs-site/zh/example/tier1-pi-sdk.mdx +122 -0
  83. package/docs-site/zh/guides/agent-feedback-loop.mdx +79 -0
  84. package/docs-site/zh/guides/authoring.mdx +129 -0
  85. package/docs-site/zh/guides/ci-integration.mdx +97 -0
  86. package/docs-site/zh/guides/connect-otel.mdx +208 -0
  87. package/docs-site/zh/guides/connect-your-agent.mdx +226 -0
  88. package/docs-site/zh/guides/dataset-fanout.mdx +83 -0
  89. package/docs-site/zh/guides/experiments.mdx +72 -0
  90. package/docs-site/zh/guides/fixtures.mdx +147 -0
  91. package/docs-site/zh/guides/official-adapters.mdx +132 -0
  92. package/docs-site/zh/guides/reporters.mdx +113 -0
  93. package/docs-site/zh/guides/runner.mdx +82 -0
  94. package/docs-site/zh/guides/sandbox-agent.mdx +139 -0
  95. package/docs-site/zh/guides/sandbox-backends.mdx +75 -0
  96. package/docs-site/zh/guides/scoring-guide.mdx +92 -0
  97. package/docs-site/zh/guides/viewing-results.mdx +195 -0
  98. package/docs-site/zh/guides/write-experiment.mdx +101 -0
  99. package/docs-site/zh/guides/write-send.mdx +353 -0
  100. package/docs-site/zh/index.mdx +180 -0
  101. package/docs-site/zh/introduction.mdx +140 -0
  102. package/docs-site/zh/quickstart.mdx +137 -0
  103. package/docs-site/zh/reference/builtin-agents.mdx +364 -0
  104. package/docs-site/zh/reference/capabilities.mdx +76 -0
  105. package/docs-site/zh/reference/cli.mdx +140 -0
  106. package/docs-site/zh/reference/define-agent.mdx +461 -0
  107. package/docs-site/zh/reference/define-config.mdx +107 -0
  108. package/docs-site/zh/reference/define-eval.mdx +674 -0
  109. package/docs-site/zh/reference/events.mdx +252 -0
  110. package/docs-site/zh/reference/expect.mdx +208 -0
  111. package/package.json +3 -2
  112. package/src/agents/bub.ts +1 -1
  113. package/src/agents/claude-code.ts +8 -5
  114. package/src/agents/codex.ts +9 -5
  115. package/src/agents/sdk-streams.test.ts +4 -4
  116. package/src/agents/sdk-streams.ts +14 -9
  117. package/src/agents/types.ts +40 -1
  118. package/src/agents/ui-message-stream.ts +1 -0
  119. package/src/cli.ts +40 -10
  120. package/src/context/types.ts +140 -0
  121. package/src/expect/index.ts +15 -8
  122. package/src/i18n/en.ts +20 -6
  123. package/src/i18n/zh-CN.ts +20 -6
  124. package/src/o11y/parsers/bub.test.ts +71 -0
  125. package/src/o11y/parsers/bub.ts +5 -0
  126. package/src/o11y/types.ts +27 -2
  127. package/src/runner/attempt.ts +2 -1
  128. package/src/runner/reporters/artifacts.ts +11 -3
  129. package/src/runner/reporters/live.ts +45 -8
  130. package/src/runner/run.test.ts +50 -0
  131. package/src/runner/run.ts +135 -41
  132. package/src/runner/types.ts +55 -2
  133. package/src/sandbox/types.ts +18 -0
  134. package/src/scoring/types.ts +5 -0
  135. package/src/shared/types.ts +3 -0
  136. package/src/util.test.ts +26 -1
  137. package/src/util.ts +16 -0
  138. package/src/view/app/App.tsx +2 -2
  139. package/src/view/app/components/AttemptModal.tsx +2 -0
  140. package/src/view/app/components/CopyControls.tsx +87 -28
  141. package/src/view/app/i18n.ts +3 -3
  142. package/src/view/client-dist/app.css +1 -1
  143. package/src/view/client-dist/app.js +18 -18
  144. package/docs/README.md +0 -120
  145. package/docs/adapters/README.md +0 -60
  146. package/docs/adapters/authoring.md +0 -179
  147. package/docs/adapters/coding-agent-skills-plugins.md +0 -324
  148. package/docs/adapters/collection.md +0 -128
  149. package/docs/adapters/contract.md +0 -264
  150. package/docs/adapters/reference/agent-eval.md +0 -215
  151. package/docs/adapters/reference/agent-loop-apis.md +0 -69
  152. package/docs/adapters/reference/claude-code-otel-telemetry.md +0 -100
  153. package/docs/adapters/reference/eve-protocol.md +0 -127
  154. package/docs/adapters/reference/otel-genai.md +0 -107
  155. package/docs/adapters/reference/otel-instrumentation.md +0 -65
  156. package/docs/adapters/targets.md +0 -99
  157. package/docs/architecture.md +0 -140
  158. package/docs/assertions.md +0 -387
  159. package/docs/capabilities-by-construction.md +0 -48
  160. package/docs/cli.md +0 -173
  161. package/docs/concepts.md +0 -106
  162. package/docs/e2e-ci.md +0 -332
  163. package/docs/eval-authoring.md +0 -319
  164. package/docs/experiments.md +0 -157
  165. package/docs/getting-started.md +0 -224
  166. package/docs/multi-agent.md +0 -145
  167. package/docs/observability.md +0 -337
  168. package/docs/origin-integration.md +0 -195
  169. package/docs/references.md +0 -35
  170. package/docs/reports.md +0 -551
  171. package/docs/results-format.md +0 -228
  172. package/docs/results-lib.md +0 -191
  173. package/docs/runner.md +0 -104
  174. package/docs/sandbox.md +0 -276
  175. package/docs/scoring.md +0 -119
  176. package/docs/source-map.md +0 -126
  177. package/docs/tier-sync.md +0 -193
  178. package/docs/view.md +0 -194
  179. package/docs/vision.md +0 -88
@@ -1,337 +0,0 @@
1
- # Observability —— transcript、工件与报告
2
-
3
- 评测的价值不止"过/挂",更在"为什么"。这一篇讲三件事:agent 的 **transcript** 如何被归一化成统一 trace、跑完落盘的**工件**长什么样、**报告器**如何把结果回传。
4
-
5
- 这是 niceeval "看得快"承诺的落点,见 [Vision](vision.md#看得快)。
6
-
7
- ## Transcript → 标准事件流
8
-
9
- 每个 agent 都吐自己格式的 transcript(Claude Code 一种 JSONL、Codex 另一种、bub 又一种)。直接消费这些就得到处写 `if (agent === ...)`。adapter 的核心活,就是把它**归一化**成那条[标准事件流 `StreamEvent[]`](adapters/contract.md#标准事件流) —— 它既是 trace,也是整套断言的唯一数据源,断言和报告只面对它。
10
-
11
- 每个 agent 一个解析器,住在 `o11y/parsers/<agent>.ts`,把原始 JSONL 映射成标准 `StreamEvent[]`。**这是接新 agent 的第二件事**(第一件是 adapter 的 `send`):没有解析器,trace 就退化成不透明字符串。归一化失败不崩:保留原始 JSONL,并在该 eval 的 `result.json` 上标 `parseSuccess: false`。
12
-
13
- 事件里工具调用的名字(`action.called.name`)被归一化到一组**规范名**,便于跨 agent 断言:
14
-
15
- ```typescript
16
- type ToolName =
17
- | "file_read" | "file_write" | "file_edit"
18
- | "shell" | "web_fetch" | "web_search"
19
- | "glob" | "grep" | "list_dir" | "agent_task" | "unknown";
20
- ```
21
-
22
- core 再从这条流派生两样:`deriveRunFacts(events)`(toolCalls / subagents / parked,供断言,见 [Adapter 契约](adapters/contract.md#派生事实core-算共享agent-无关)),以及下面给人/给沙箱内手工跑的验证测试看的 o11y 摘要。
23
-
24
- 原始 transcript 具体怎么从 agent CLI 弄到手(磁盘旁读 / stdout 捕获 / OTLP 推送)、采集层与转换层的边界怎么分,属于"怎么写 adapter"的范畴,见 [Adapter 写法 · 采集层](adapters/authoring.md#采集层原始数据怎么从-agent-cli-弄到手)。
25
-
26
- ## o11y 派生摘要
27
-
28
- 从归一化事件派生出一份给人和给断言看的摘要:
29
-
30
- ```typescript
31
- interface O11ySummary {
32
- totalTurns: number;
33
- toolCalls: Record<ToolName, number>; // { file_read: 15, shell: 8, … }
34
- totalToolCalls: number;
35
- filesRead: string[];
36
- filesModified: string[];
37
- shellCommands: { command: string; exitCode?: number; success?: boolean }[];
38
- webFetches: { url: string; status?: number; success?: boolean }[];
39
- errors: string[];
40
- thinkingBlocks: number;
41
- durationMs: number; // 本次运行的 wall-clock 耗时(运行器计时)
42
- usage: Usage; // 累加这个 attempt 所有轮的 token 用量
43
- estimatedCostUSD?: number; // usage × 价格表换算(见下)
44
- }
45
- ```
46
-
47
- ### 注入沙箱:让测试断言「行为」
48
-
49
- 这份摘要被写进沙箱的 `__niceeval__/results.json`,于是你在沙箱里手工跑的验证测试能断言 agent **干了什么**,而不只是**产出了什么**:
50
-
51
- ```typescript
52
- const o11y = JSON.parse(readFileSync("__niceeval__/results.json", "utf-8")).o11y;
53
-
54
- // 用了正确的脚手架,而不是手搓
55
- expect(o11y.shellCommands.some((c) => c.command.includes("create-next-app"))).toBe(true);
56
- // 没有读不该读的文件
57
- expect(o11y.filesRead).not.toContain(".env");
58
- // 工具调用没失控
59
- expect(o11y.totalToolCalls).toBeLessThan(50);
60
- ```
61
-
62
- 这把"过程正确性"也纳入了评分,而不只是"结果正确性"。
63
-
64
- ## OTLP traces → 统一瀑布图
65
-
66
- `StreamEvent` 回答「做了什么」;**trace 回答「各花了多久、谁套谁」**。配了 OTel 接入的 agent(沙箱型声明 `tracing` 块;remote agent 配 `defineConfig({ telemetry })`)经 OpenTelemetry 把 OTLP traces 导出到运行器(沙箱型每个沙箱起一个本机 OTLP/HTTP 接收器,remote agent 共享一个固定端口接收器,端点经 `ctx.telemetry.endpoint` 交给 agent),跑完归一成 `TraceSpan[]` 挂到 `EvalResult.trace`,`niceeval view` 画成瀑布图。**这条线完全独立于事件流:span 只喂瀑布图,不产出任何 `StreamEvent`,也不影响任何断言**——事件流永远来自 `send` 返回的 `Turn.events`。
67
-
68
- 这条线分两层,两层都得归一,但**含义层(语义约定)才是接新 agent 的真功夫**:
69
-
70
- | 层 | 干什么 | 谁做 |
71
- |---|---|---|
72
- | **线格式层** | OTLP/JSON(codex)、OTLP/protobuf(bub)→ 统一的 `TraceSpan[]` | core `o11y/otlp/parse.ts`,通用,接新 agent 不用碰 |
73
- | **语义层** | span 名 / 属性的**含义**(「这是模型调用」「这是工具执行」) | **每个 agent 一个薄 mapper**(见下) |
74
-
75
- ### canonical 目标 = OpenTelemetry GenAI 语义约定(不发明私有 schema)
76
-
77
- 不同 agent 的 span 命名 / 属性约定天差地别(codex 的 `codex.exec`、bub 插件的 `agent.step` / `execute_tool`)。直接把原生 span 喂给 view 就是**苹果对橘子**:名字、属性键都不一样,跨 agent 没法叠加对比 —— 而横向对比是本套件的全部意义(同一任务、不同 memory 条件 / 不同 agent 比通过率 × 时间 × 成本)。
78
-
79
- **定下来的规矩:canonical 目标就是 OpenTelemetry 官方的 [GenAI 语义约定](https://opentelemetry.io/docs/specs/semconv/gen-ai/),不另造 niceeval 私有 schema。** 理由:
80
-
81
- 1. **它是行业标准**,codex 的 OTLP 已部分遵循、bub 的 otel 插件可配置直接发 `gen_ai.*`。
82
- 2. **我们不控制 agent 的 instrumentation** —— codex(Rust)、claude 发什么是什么。造私有 schema 也强迫不了它们原生发,最终只能在我们这侧归一;那不如归一到一个公认标准,而不是又一套只有 niceeval 认得的键。
83
-
84
- canonical 的核心是用 `gen_ai.operation.name` 把 span 分成几类语义角色(view 据此着色 / 分组 / 对比):
85
-
86
- | `gen_ai.operation.name` | niceeval `kind` | 含义 |
87
- |---|---|---|
88
- | `chat` / `text_completion` | `model` | 一次模型调用 |
89
- | `execute_tool` | `tool` | 一次工具执行 |
90
- | `invoke_agent` / `create_agent` | `agent` / `turn` | 一次 agent / 回合调用 |
91
- | (其余 / 未识别) | `other` | plumbing,view 默认折叠 |
92
-
93
- 配套属性一律走 GenAI 键:`gen_ai.request.model`、`gen_ai.usage.input_tokens` / `output_tokens`(`derive.ts` 的 `extractUsageFromSpans` 已经在认这套)、`gen_ai.tool.name`、`gen_ai.tool.call.id`、`gen_ai.agent.name`。
94
-
95
- ### 每个 agent 一个薄 mapper
96
-
97
- 和 transcript 解析器(`o11y/parsers/<agent>.ts`)**完全对称**:每个 agent 再加一个 span mapper,把它的原生 span 归一到 canonical GenAI semconv。mapper 只做一件事 —— 认出「这条 span 是模型调用 / 工具执行 / 回合」,补上 `gen_ai.operation.name` 与相关 `gen_ai.*` 属性,**保留 raw `name` / `attributes` 供下钻**。
98
-
99
- > **mapper 越薄越好:能在源头对齐就别在 mapper 里补。** codex 的 `config.toml`、bub 插件的配置尽量让它们直接发 `gen_ai.*`;源头发对了的 agent,mapper 近乎透传。mapper 是「上游不肯按标准发」时的兜底,不是主力。
100
-
101
- 这把 `o11y/otlp/select.ts` 里那串「猜各 agent 命名约定」的正则全删掉 —— agent 特定知识回到 agent 自己手里(和 parser 同一个归属原则),`select` 退化成纯通用逻辑:按 `kind != "other"` 留、按 firehose 频率丢。
102
-
103
- ### view 只认 canonical
104
-
105
- **view 不读任何原生 span 名 / 原生属性。** 它只消费归一后的字段:`gen_ai.operation.name` → `kind` 着色分组,`gen_ai.*` 取模型 / 工具 / 用量。后果:
106
-
107
- - 接新 agent **不用动 view** —— 只要 mapper 把它归一到 canonical。
108
- - 两个 agent 的瀑布图**天然对齐、可叠加对比**(同一种颜色 = 同一种语义)。
109
- - 没写 mapper(或 mapper 没认出)的 span 落进 `other`,view 折叠不渲染细节 —— **降级但不崩**,也不污染对比。
110
-
111
- ### agent 定义里 otel 怎么放(两块责任分开)
112
-
113
- otel 在 agent 定义里其实是**两个互不相干的责任,分开放**,别都塞进 `setup` / `send`:
114
-
115
- 1. **导出配置(adapter 侧的 `tracing` 块)** —— 「怎么让这个 CLI 把 OTLP 发到 endpoint」。从 `setup`/`send` 抽出来,做成 agent 定义里一个声明式 `tracing` 块(见 `AgentTracing`)——这个块存在,运行器就为该 agent 开 OTLP 接收,不需要另外声明什么开关。两种投递方式(按 CLI 而定,互不排斥):
116
-
117
- ```typescript
118
- defineSandboxAgent({
119
- tracing: {
120
- protocol: "http/protobuf",
121
- // env-based(标准 OTEL_* env,如 bub/Python OTel SDK):给 endpoint → 返回 env。
122
- // 运行器把它算进 ctx.telemetry.env,send 直接 `{ ...ctx.telemetry?.env }` 注入。
123
- env: (endpoint) => ({ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: endpoint, /* … */ }),
124
- // file-based(CLI 自有配置文件,如 codex 的 config.toml [otel] 块):给 sandbox + ctx,
125
- // 自己写/追加配置。运行器在 setup 之后、首次 send 之前调一次(子表天然落在主配置之后)。
126
- configure: async (sandbox, ctx) => { /* 把 [otel] 块追加进 config.toml */ },
127
- },
128
- async setup(sb) { /* 只装 CLI / 写主配置,不碰 otel */ },
129
- async send(input, ctx) { /* env: { ...auth(), ...ctx.telemetry?.env } */ },
130
- });
131
- ```
132
-
133
- 为什么要 env / configure 两条路:bub(Python OTel SDK)读标准 `OTEL_*` env,codex(Rust)**不读** env、只认自己 `config.toml` 的 `[otel]` 块 —— 这是上游差异,抹不平,所以两种投递都得支持。
134
-
135
- 2. **span mapper(core o11y 侧)** —— 「原生 span → canonical」。**纯数据变换,不碰沙箱**,和 transcript parser 一样住 core 的 o11y(`o11y/otlp/mappers/<agent>.ts`),可独立单测。分派靠接口不靠名字:adapter 在 `defineSandboxAgent` / `defineAgent` 里用 `spanMapper` 声明自己的 mapper,运行器只调 `agent.spanMapper`,未声明的走通用 heuristic 兜底 —— core 不出现 agent 名字的行为分支。
136
-
137
- **为什么要分:** 导出配置是「沙箱里怎么发」,mapper 是「发回来怎么读」—— 一个需要沙箱、一个是纯函数,生命周期和测试方式都不同。混在 `setup`/`send` 里,既难单测 mapper、又让 adapter 把 otel 拼装逻辑揉进主流程。`ctx.telemetry` 则统一带上 `{ endpoint, env? }`:env-based agent 拿 `env` 直接 spread,file-based agent 在 `configure` 里用 `endpoint`。
138
-
139
- > **claude-code:** 已接原生 OTLP(beta 遥测):adapter 的 `tracing.env` 注入 `CLAUDE_CODE_ENABLE_TELEMETRY=1` + `CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1` + `OTEL_TRACES_*`(见 `src/agents/claude-code.ts`)。span 层级 `claude_code.interaction → llm_request / tool`,只有结构与计时、内容默认脱敏——内容与断言仍来自 transcript 旁读,trace 只补瀑布图。span 量级小(每回合个位数),`selectTraceSpans` 的 small-trace 路径整段保留,不需要专属 mapper。曾考虑过的「transcript 时间戳合成 span」方案不再需要。
140
-
141
- ### span 怎么归属到轮
142
-
143
- spans 是异步推来的,必须知道「这批 span 属于哪一轮 `send`」。接收器的**粒度**跟**被测进程**走,不是跟 attempt 走:
144
-
145
- - **沙箱型 agent**:每沙箱一个接收器。每个沙箱是独立进程,env 注入各自端点,attempt 之间端口天然隔离。
146
- - **remote agent**:整个 run **共享一个接收器**(`defineConfig({ telemetry })` 钉住的固定端口)。被测应用只有一条全局 OTel 管线、一个导出目标,做不到"给每条并行 eval 发不同端点"——并行 attempts 的 span 混在同一条流里,这是共享被测对象的物理事实,不是实现选择。
147
-
148
- 共享流之下的归属阶梯:
149
-
150
- - **traceparent(并发正确性的必要条件)**:`ctx.telemetry.headers` 是每轮一个新值的 W3C trace context,`send` 把它 spread 进请求头;支持 context 传播的埋点(标准 OTel HTTP 服务端埋点、Claude Code 的 `TRACEPARENT`、LangSmith 检测 global provider)把本轮 span 挂到这个 trace 下,按 traceId 精确归属,并发随便开。
151
- - **窗口法(兜底,仅串行可靠)**:runner 在 `send` 前记时间戳,`send` 返回后取窗口内的 span。并发 attempts 的窗口互相重叠,窗口法归属必然混流。
152
- - **并发守卫**:共享接收器 + 未确认 traceparent 生效(收到的 span 不带我们发的 traceId)+ 该 agent 并发 > 1 → runner 把该 agent 的 attempts 降为串行并提示。宁可慢,不可静默混流;确认 traceparent 生效后解除。
153
-
154
- ## 工件落盘
155
-
156
- 每次运行落一份结构化工件到 `.niceeval/<时间戳>/`。默认 `Artifacts()` reporter 的当前目录结构是:
157
-
158
- ```text
159
- .niceeval/<run>/
160
- summary.json
161
- <evalId>/<agent>/<model>/a<attempt>/
162
- events.json
163
- sources.json
164
- trace.json
165
- o11y.json
166
- diff.json
167
- ```
168
-
169
- `summary.json` 是瘦身后的 `RunSummary`:保留榜单、判决、断言、usage/cost 和 attempt 工件引用,不内联 `events` / `sources` / `trace` / `o11y` / `diff` / `rawTranscript` 这些重数据。每个 attempt 的重数据按需写入自己的目录,文件内容都是 JSON array/object,不是 JSONL / NDJSON。完整 schema、版本号设计、路径转义规则和 view 读取规则见 [Results Format](results-format.md)。
170
-
171
- `summary.json` 形如:
172
-
173
- ```json
174
- {
175
- "agent": "codex",
176
- "model": "gpt-5",
177
- "startedAt": "2026-06-28T10:30:45.000Z",
178
- "completedAt": "2026-06-28T10:31:23.000Z",
179
- "passed": 8, "failed": 1, "skipped": 0, "errored": 0,
180
- "results": [
181
- { "id": "weather/brooklyn", "outcome": "passed",
182
- "agent": "codex", "model": "gpt-5", "attempt": 1,
183
- "durationMs": 2184,
184
- "artifactsDir": "weather/brooklyn/codex/gpt-5/a1",
185
- "hasEvents": true, "hasTrace": false, "hasSources": true,
186
- "assertions": [
187
- { "name": "succeeded", "severity": "gate", "score": 1, "passed": true },
188
- { "name": "calledTool(get_weather)", "severity": "gate", "score": 1, "passed": true }
189
- ] }
190
- ]
191
- }
192
- ```
193
-
194
- `outcome` 只有 `passed` / `failed` / `errored` / `skipped` 四态,没有 `scored` 中间态(soft 断言的分数就在 `assertions[].score` 里如实记录,不影响这四态)。`summary.failed` 与 `summary.errored` 是互斥计数:前者表示断言/评分不通过,后者表示环境、超时、adapter 或 agent runtime 这类执行错误。JUnit reporter 也按这个口径输出 `<failure>` 与 `<error>`。
195
-
196
- 工件是机器可读的,可回放、可二次分析、可喂给下游 dashboard。
197
-
198
- ## 用量与成本(token / 计费)
199
-
200
- 评测很贵 —— 每个 case 可能是几十次模型调用。**「花了多少 token / 多少钱」是一等公民**,因为评 coding agent 时最值钱的对比维度是**质量 × 成本**:同一批 eval 跑 claude-code / codex / bub,谁的通过率高、谁更省钱,一目了然。
201
-
202
- 参考项目这块都是空的:eve 在模型层有 token 数但 eval 不聚合成本;agent-eval 连抠都没抠(opencode 解析器里只留了句 "could extract token usage if needed" 的 TODO)。niceeval 把它补齐。
203
-
204
- ### 用量从哪来
205
-
206
- `Usage`(`{ inputTokens, outputTokens, cacheReadTokens?, cacheWriteTokens?, requests? }`)按 transport 取得,作者通常**什么都不用做**:
207
-
208
- - **远程 agent** —— 你在 `send` 里把模型返回的 usage(或你服务响应里带的 usage,若它回了)一并返回。
209
- - **沙箱 coding agent** —— **不必手填**:agent 的 JSONL transcript 里本就逐条带 token 用量,transcript 解析器(`o11y/parsers/<agent>.ts`)抠出来。这正是 agent-eval 留下的 TODO。
210
-
211
- 每轮的用量来源二选一:remote agent 由 `Turn.usage` 直接给,sandbox agent 由解析器从该轮 transcript 抠出。运行器把每轮累加 → 单 eval 用量(落进 `O11ySummary.usage`);reporter 再跨 eval 累加 → 整轮用量。
212
-
213
- ### 换算成本:价格表从哪来
214
-
215
- token 数能可靠拿到;难点是 token→$ 的价格表 —— 价格会随时间、provider、网关、企业折扣、自托管而变,写死必然过期。所以成本解析是**分层的,且"实测优先于估算"**:
216
-
217
- 1. **网关实测成本(最高优先)。** 不少网关(Vercel AI Gateway、OpenRouter…)每次请求直接回真实 cost。只要 agent 把它带进 `Turn.usage.costUSD`,就直接用它 —— **根本不需要价格表**。这绕开了一大半场景。
218
- 2. **内置默认价格表 ⊕ 用户覆盖。** 没有实测时,用观测到的模型查价。niceeval 内置一份**带版本的快照**覆盖常见模型(零配置即有 $),用户在 config 里**覆盖或补充**(网关/企业折扣/自托管/自定义费率,用户赢):
219
-
220
- ```typescript
221
- // niceeval.config.ts —— 合并在内置默认之上,用户优先
222
- defineConfig({
223
- pricing: {
224
- "anthropic/claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25, cacheReadPerMTok: 0.5 },
225
- "openai/gpt-5.2-codex": { inputPerMTok: 1.25, outputPerMTok: 10 },
226
- "my-selfhosted/*": { inputPerMTok: 0, outputPerMTok: 0 }, // 自托管=免费
227
- },
228
- });
229
- ```
230
- 3. **未知模型 → 只报 token、不报 $,并打一行 warning** 列出没映射的模型(绝不静默瞎猜)。
231
-
232
- `estimatedCostUSD = inputTokens×单价 + outputTokens×单价 + cache…`,落进 `O11ySummary` 与 `summary.json`。字段名带 **estimated** 是有意的:它是估算,真实账单以 provider 发票为准 —— 这也正是「网关实测」和「用户覆盖」两条通道存在的原因。
233
-
234
- > 设计取舍:价格是**会过期的数据**,所以内置快照只为「零配置能用」,不写死进核心逻辑;准确性靠用户覆盖与网关实测兜底,未知则诚实降级。快照随版本更新,也可考虑 `pricing: "auto"` 从社区维护的价目拉取(默认仍用离线快照,保证确定性)。
235
-
236
- ### 报告里长什么样
237
-
238
- 控制台每个 eval 末尾带用量,整轮带合计与按 agent 的对比:
239
-
240
- ```text
241
- ✓ recall-across-sessions (42s) 38.2k tok $0.31
242
- ✓ remember-styling-conv (51s) 61.7k tok $0.48
243
-
244
- Run totals: 3 evals · 142k tok · $1.12 (agent: claude-code)
245
- ```
246
-
247
- `summary.json` 增加(注意:**时间 / token / 成本三件套始终成组出现**):
248
-
249
- ```json
250
- {
251
- "durationMs": 93400,
252
- "usage": { "inputTokens": 980000, "outputTokens": 64000, "cacheReadTokens": 410000, "requests": 73 },
253
- "estimatedCostUSD": 1.12,
254
- "perEval": [
255
- { "id": "recall-across-sessions", "durationMs": 42100, "usage": { … }, "estimatedCostUSD": 0.31 }
256
- ]
257
- }
258
- ```
259
-
260
- 这让「跨 agent 对比」从只有 pass-rate 变成 **pass-rate × 时间 × $**,也能在 reporter 层算出 pass@$1(单位成本下的通过率)这类指标。
261
-
262
- ### 时间也是一等指标(效率三件套)
263
-
264
- 成本不是新指标里唯一的一个。**wall-clock 时间一直就记录**(`StreamEvent` 里每个工具调用有 `durationMs`,运行器还包住每个 `send` 和整个 eval 计时),现在和 token / 成本**并排**成组:
265
-
266
- | 维度 | 粒度 | 来源 |
267
- |---|---|---|
268
- | **时间(wall-clock)** | 每 turn / 每 eval / 整轮(+ 平均) | 运行器计时,adapter 不用做事 |
269
- | **token 用量** | 同 | 标准事件流 / transcript 抠出 |
270
- | **估算成本 $** | 同 | usage × 价格表(或网关实测) |
271
-
272
- 三个都留是因为**它们不总相关**:命中缓存的运行可能便宜但慢,推理重的可能贵但快 —— 只看一个会误判。所以控制台 `(42s) 38.2k tok $0.31` 三个并列,`niceeval view` 也能画「质量 × 成本 × 延迟」。
273
-
274
- ### 把成本变成可断言 / 可护栏的维度
275
-
276
- - **断言效率**(见 [Scoring](scoring.md#5-效率成本断言)):`t.maxTokens(50_000)` / `t.maxCost(0.5)` —— agent 答对了但烧太多,也判失败。
277
- - **预算护栏**:`--budget <usd>` 给整轮设上限,累计花费超了就停止派发新 attempt(借鉴 crabbox 的 spend cap),避免一次跑爆账单。
278
-
279
- ## 结果可视化:`niceeval view`
280
-
281
- 控制台和 `summary.json` 是「当下」的;但你常常想**事后看图**:这次比上次贵了多少?哪个 agent 性价比高?所以 niceeval 提供一个本地查看器(对标 agent-eval 的 playground:一个读结果目录的 web UI),只读 `.niceeval/<时间戳>/` 这些**结构化工件**,不连任何外部服务。结果落盘格式见 [Results Format](results-format.md);查看器现状、已知的文档差异和计划中的功能(比如挑两次运行对比)见 [View](view.md)。
282
-
283
- 可视化能力完全建立在「工件结构化 + 带 usage/cost」之上 —— 换句话说,**只要数据采全了,图是免费的**;不想用内置查看器,同一份工件也能喂给下游 dashboard。
284
-
285
- 托管看板走 reporter 通道(见下),把每次运行作为一个实验上报到 Braintrust 这类平台,跨提交比较与团队共享。
286
-
287
- ## Reporters
288
-
289
- 报告器消费运行结果,实现三个回调:
290
-
291
- ```typescript
292
- interface Reporter {
293
- onRunStart(evals: Eval[], agent: Agent): void | Promise<void>;
294
- onEvalComplete(result: EvalResult): void | Promise<void>;
295
- onRunComplete(summary: RunSummary): void | Promise<void>;
296
- }
297
- ```
298
-
299
- 报告器在**独立串行队列**上被回调,不阻塞执行池(见 [Runner](runner.md#调度有界并发))。内置:
300
-
301
- - **`Console()`** —— 默认,流式逐行输出,失败断言内联展开。
302
- - **`Artifacts()`** —— 默认写 `.niceeval/<timestamp>/summary.json` 与 attempt 级 JSON 工件(`events.json`、`sources.json`、`trace.json`、`o11y.json`、`diff.json`),供 `niceeval view` 读取。具体格式见 [Results Format](results-format.md)。
303
- - **`JUnit(path)`** —— JUnit XML,接 CI 测试报告 UI。
304
- - **`Json(path)`** —— 机器可读全量。
305
- - **`Braintrust(config?)`** —— 把一次运行作为一个 Braintrust experiment 上报,每个 attempt 一行:soft 断言按名字记分,gate 断言记在 `gate:` 前缀下(实验 diff 里 gate 回归和 soft 分数回归用同一套机制看);metrics 带 start/end、token 用量与估算成本,metadata 带 agent / model / experiment / flags 身份维度与失败断言明细。`braintrust` 包是可选 peer 依赖(动态 import,没装时 onRunStart 报错并提示安装);鉴权走 `BRAINTRUST_API_KEY` 或工厂参数 `apiKey`。源码 `src/runner/reporters/braintrust.ts`。
306
-
307
- 配置全局或单 eval 专用:
308
-
309
- ```typescript
310
- import { Braintrust, JUnit } from "niceeval/reporters";
311
-
312
- // niceeval.config.ts —— 全局,观测所有 eval(Console / Artifacts 由 CLI 始终自带,不用写)
313
- defineConfig({ reporters: [JUnit(".niceeval/junit.xml"), Braintrust({ project: "weather" })] });
314
-
315
- // 某个 eval 专用:实例只观测引用它的 eval
316
- defineEval({ reporters: [Braintrust({ project: "weather" })], async test(t) { ... } });
317
- ```
318
-
319
- eval 级 reporter 经作用域包装接入(`scopeReporter`,见 `src/runner/report.ts`):`onEvalComplete` 按 eval id 过滤,`onRunComplete` 收到重新计数的子集汇总;同一实例被多个 eval 引用时合并观测集(共享一个目的地,比如同一个 Braintrust 实验),已经挂在全局 `reporters` 里的实例在 eval 上再列一遍也不会重复上报。
320
-
321
- ## 失败分类(可选,沙箱型)
322
-
323
- 沙箱型可开 AI 失败分类:跑完用一个小模型(给它只读探索结果目录的工具:list/read/grep)把失败归为三类:
324
-
325
- - **model** —— agent 试了但代码不对(测试挂)。
326
- - **infra** —— 基础设施坏了(API 错误、限流、崩溃、无 transcript)。
327
- - **timeout** —— 撞了时限。
328
-
329
- 分类缓存进 `classification.json`。这让你在一大批失败里快速分清"是模型不行"还是"环境抖了",而不用逐个翻日志。
330
-
331
- ## 相关阅读
332
-
333
- - [Scoring](scoring.md) —— 作用域断言如何消费 o11y。
334
- - [Runner](runner.md) —— 报告队列与工件落盘的调度。
335
- - [Results Format](results-format.md) —— `.niceeval/<run>/` 的目录结构与 JSON 文件契约。
336
- - [Adapter 写法](adapters/authoring.md) —— 接新 agent 需要的解析器、采集层怎么弄到原始数据。
337
- - [agent-eval 参考:采集 / 转换 / 落地三层](adapters/reference/agent-eval.md) —— Vercel agent-eval 怎么写 adapter 的学习记录。
@@ -1,195 +0,0 @@
1
- # Origin 应用接入手册
2
-
3
- 这份文档最初是**工单**:`examples/zh/origin/` 下五个独立应用,按应用逐个说明怎么把它们接进 niceeval。**五个应用已经全部接完**(见文末「现状」),产出按分档铺在 `examples/zh/tier1|tier2|tier3/<name>/`(分层索引见 [examples/zh · 接入分层](../examples/zh/origin/README.md#接入分层origin--tier1--tier2--tier3))。本文现在的作用是记录接入时踩过的坑和当前实现的形状,再接一个类似的应用时可以照抄;当前实现的权威来源永远是各 tier 目录的源码和各自的 README,本文只是导读。
4
-
5
- 先记住三条铁律(接类似应用时依然适用):
6
-
7
- 1. **不改 origin 的任何文件。** 接入产物放 `examples/zh/tier1/<同名>/`:从 origin 复制整个应用,被复制的文件保持逐字节不变(除 `package.json` / `pnpm-workspace.yaml` / `tsconfig.json` 三个集成脚手架文件,以及 `.env.example`——tier 侧要补 judge 独立凭证等 eval 变量),接入代码全部是**新增**文件。`pnpm run gen:diff-code` 会 diff origin 和 tier1 两个目录生成 before/after 文档页,"应用侧零改动"是这些页面的核心卖点,改一个字节都会破坏它;这条铁律由 CI 的 `pnpm tiers:check`(verbatim 校验)看守,见 [tier-sync](tier-sync.md)。
8
- 2. **协议以实际输出为准。** 动手写映射之前,先把应用跑起来,`curl -N` 打一轮 `/api/chat` 把 SSE 帧看一遍。本文的帧格式描述来自当前代码,但代码会演化,别背文档。
9
- 3. **被测应用由你自己按它的方式启动,eval 不代管进程、不另开端口。** adapter 只经环境变量(如 `CODEX_SDK_URL`)指向一个已经在跑的实例——没有 `server-lifecycle.ts` 这类"eval 侧拉起子进程"的机制,这是刻意的取舍,理由见[接入你的 Agent · 为什么不直调](../docs-site/zh/guides/connect-your-agent.mdx)同一条脉络(eval 不代管被测进程)。
10
-
11
- ## Tier 是什么,产出长什么样
12
-
13
- 接入分三档(定义见 [docs-site · Tier](../docs-site/zh/concepts/tier.mdx)):**Tier 1(只接 send)**、**Tier 2(send + OTel)**、**Tier 3(侵入改造 + experiment flags)**。
14
-
15
- 三档现在各有物化目录,同一个应用逐层叠 delta:`tier1/<name>`(纯无侵入,全套断言)、`tier2/<name>`(有 OTel 输出的三个应用:ai-sdk-v7、codex-sdk、langgraph——加 telemetry 配置与 spanMapper/收尾宽限,换瀑布图)、`tier3/<name>`(五个应用都有——按文末「Tier 3 侵入点」改应用内部代码,暴露 experiment flags)。哪个应用有哪几层见 [examples/zh 分层索引](../examples/zh/origin/README.md#接入分层origin--tier1--tier2--tier3);层间用 `pnpm tiers:sync` 保持同步(机制见 [tier-sync](tier-sync.md))。本文余下部分讲 Tier 1 的接入配方——那是每个应用的地基。
16
-
17
- ## 统一的接入配方
18
-
19
- 五个应用的形态高度一致(HTTP 服务 + SSE 流式响应),所以 adapter 的骨架也一致。差异只在:帧格式怎么翻、session 字段叫什么、有没有审批流、有没有 OTel。
20
-
21
- ### 目录布局
22
-
23
- ```text
24
- examples/zh/tier1/<name>/
25
- ├── (origin 的完整副本,逐字节不变,除三个集成脚手架文件)
26
- ├── package.json / pnpm-workspace.yaml / tsconfig.json 集成脚手架(diff 页如实展示,允许)
27
- ├── niceeval.config.ts 新增
28
- ├── agents/<name>.ts 新增:adapter 本体——只剩传输粘合,没有 server 生命周期代码
29
- ├── evals/*.eval.ts 新增
30
- └── experiments/*.ts 新增
31
- ```
32
-
33
- ### adapter 骨架
34
-
35
- adapter 的 `send` 每轮做的事,按顺序,不声明任何 `capabilities`——`t` 上解锁什么由 `send` 实际做到了什么决定(见[契约 · 能力从哪来](adapters/contract.md#能力从哪来构造证明不是问卷)):
36
-
37
- ```ts
38
- // agents/<name>.ts
39
- import { defineAgent, sseJsonFrames, driveFrameStream } from "niceeval/adapter";
40
-
41
- const BASE_URL = process.env.<NAME>_URL ?? "http://127.0.0.1:<port>"; // 应用自己的端口,eval 不代管进程
42
-
43
- export default defineAgent({
44
- name: "<name>",
45
- // 有 OTel 的应用才需要:spanMapper 把应用私有 span 归一成 canonical,只影响瀑布图。
46
- // spanMapper: mapCodexSpans,
47
- async send(input, ctx) {
48
- // 回答轮:先查有没有停轮现场(HITL),有就把裁决交回应用,接着读同一条流
49
- const pending = ctx.session.take<Pending>();
50
- if (pending) {
51
- const approved = input.responses?.[0]?.optionId === "approve";
52
- await postApprove(pending.requestId, approved, ctx.signal);
53
- return readStream(pending.cursor, ctx, pending.stream);
54
- }
55
-
56
- // 正常轮:发请求。session 续接:ctx.session.id 新会话线自然是 undefined,
57
- // 拿到应用回的 id 后用 ctx.session.capture() 写回(只在还没记过时落地)
58
- const res = await fetch(`${BASE_URL}/api/chat`, {
59
- method: "POST",
60
- headers: { "content-type": "application/json" },
61
- body: JSON.stringify({ message: input.text, sessionId: ctx.session.id }),
62
- signal: ctx.signal,
63
- });
64
-
65
- // 逐帧读 SSE,用官方转换器(有的话)或手写映射翻成 StreamEvent;
66
- // 碰到审批帧就 ctx.session.hold() 存住现场、返回 waiting;流正常结束就返回 completed
67
- return driveFrameStream(sseJsonFrames(res.body!), reducer, ctx, onFrame);
68
- },
69
- });
70
- ```
71
-
72
- `driveFrameStream` / `sseJsonFrames` 从 `niceeval/adapter` 导出,是逐帧驱动循环的官方件,五个 adapter 共用思路(langgraph/pi 的自定义帧、claude 的 SDKMessage、codex 的 ThreadEvent 都是这一种传输)。有官方转换器的用官方转换器(`fromClaudeSdkMessages` / `fromPiAgentEvents` / `fromCodexThreadEvents`),没有的手写一张"帧类型 → StreamEvent"映射表。
73
-
74
- 事件词汇表(`message` / `action.called` / `action.result` / `input.requested` …)见 [docs-site 事件流参考](../docs-site/zh/reference/events.mdx)。映射三要点:按真实顺序、`callId` 配对、不漏帧——漏帧只是让这条 eval 的负断言不可信,不是运行时错误。
75
-
76
- 模型对比怎么做:多数应用走 `AGENT_MODEL` 环境变量(启动应用时指定),ai-sdk-v7 例外——它的接口本身收请求级 `model` 字段,`ctx.model` 直接透传,同一个 server 实例就能测多个模型,不用重启。
77
-
78
- ### HITL:审批流怎么接
79
-
80
- 先理解应用侧的机制:**应用在等审批时,SSE 流保持打开**——服务端把执行卡在一个 Promise/队列上,审批决定走**另一个** `POST /api/chat/approve` 请求,resolve 之后原来那条 SSE 继续吐帧直到结束。
81
-
82
- adapter 要这样做:
83
-
84
- 1. `send` 读流,读到审批帧(各应用帧名见小节)时**不要关流**——把「读了一半的流 + 待批准的 callId」存进 `ctx.session.hold()`(不是模块级 `Map`——这是逃过 attempt/session 边界串用状态的关键),返回 `waiting` + `input.requested` 事件。
85
- 2. 下一次 `send`(就是 eval 里的 `t.respond("approve"/"deny")`)先 `ctx.session.take()` 取回停轮现场:有,就按 `input.responses[0].optionId`(不是解析 `text`)判断批准与否,`POST /api/chat/approve`(body 字段名各应用不同!claude/pi 是 `toolUseId`,langgraph 是 `toolCallId`),然后**继续读原来那条流**到结束,把剩余帧作为这一轮的 events 返回。
86
- 3. 拒绝(`deny`)时,把被拒工具的 `action.result` 的 `status` 置 `"rejected"`,不是 `"failed"`。
87
-
88
- 没有审批流的应用(codex-sdk)跳过这一整节,永远不返回 `waiting`。
89
-
90
- ### OTel:只画瀑布图,和事件映射完全无关(这一节的产物在 tier2)
91
-
92
- 不管应用有没有 OTel 输出,事件映射(上面几节讲的)都一样要做——**OTel 现在只喂 `niceeval view` 的调用瀑布图,不产出任何事件,也不影响任何断言**,详见 [Observability · OTLP traces](observability.md#otlp-traces--统一瀑布图)。
93
-
94
- 五个应用里,ai-sdk-v7、codex-sdk、langgraph 发 OTel span,claude-sdk、pi-sdk 不发(claude-sdk 的 CLI 遥测只有 metrics+logs,niceeval 只消费 trace spans;pi-agent-core 没有官方 OTel 集成)。这层接线是 Tier 2 的 delta,落在 `examples/zh/tier2/<name>/`(tier1 不配 telemetry、没有瀑布图);有 span 的应用接法都一样:
95
-
96
- 1. `niceeval.config.ts` 钉住固定接收端口:`defineConfig({ telemetry: { port: 4318 } })`——写了这个配置就等于给非沙箱 agent 打开了 OTel 接收。
97
- 2. 启动应用时,把标准 OTel 环境变量指向这个端口(`OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces` 或应用自己的等价配置)。
98
- 3. 应用私有的 span 命名如果不是标准 GenAI semconv(比如 codex 自家 span),给 agent 声明 `spanMapper`(codex 有内置 `mapCodexSpans`)把它归一成 canonical,瀑布图才能正确着色分组。
99
- 4. 没有 span 到齐的问题(`BatchSpanProcessor` 缓冲导致最后一批 span 晚到)属于观测完整性,不是断言问题——ai-sdk-v7、langgraph 的 adapter 都在流结束后主动等一小段(`settleMs` / grace period)把收集窗口拉宽,只影响瀑布图,断言与它无关。
100
-
101
- **并发须知**:五个 origin 的 HTTP 层都没接 OTel 服务端埋点,不会传播 `ctx.telemetry.headers` 里的 traceparent——所以 span 按时间窗口归属,该 agent 的轮次自动串行(runner 会打日志提示,宁可慢不混流)。这是正确行为,不是 bug;要解锁并发得应用侧接 W3C trace context 传播(属于 Tier 3 范畴的应用侧改动)。
102
-
103
- ## 各应用速查
104
-
105
- | | 端口 | 请求体 | 帧格式 | session 字段 | HITL | 模型选择 | OTel(仅瀑布图) |
106
- |---|---|---|---|---|---|---|---|
107
- | ai-sdk-v7 | 34001 | `{messages[], model}` | AI SDK UI Message Stream | 无(整份 messages 重放) | 流内(SDK 机制) | 请求体 `model` | ✅ 官方 `@ai-sdk/otel`,标准 GenAI 语义 |
108
- | claude-sdk | 32001 | `{message, sessionId}` | SDKMessage 原样透传 | `sessionId`(SDK 落盘) | `/api/chat/approve` `toolUseId` | env `AGENT_MODEL` | ❌ 只有 metrics+logs |
109
- | codex-sdk | 31001 | `{message, threadId}` | ThreadEvent 原样透传 | `threadId`(SDK 落盘) | 无 | env `AGENT_MODEL` | ✅ codex 自家 span,`spanMapper: mapCodexSpans` 归一 |
110
- | pi-sdk | 33001 | `{message, sessionId}` | AgentEvent 透传 + 3 种自定义帧 | `sessionId`(服务端内存) | `/api/chat/approve` `toolUseId` | env `AGENT_MODEL` | ❌ 无 |
111
- | langgraph | 35000 | `{message, sessionId}` | 自定义 JSON 帧 | `sessionId` = thread_id(进程内存) | `/api/chat/approve` `toolCallId` | env `AGENT_MODEL` | ✅ LangSmith OTel 导出 |
112
-
113
- 启动命令、必需的 key 见各 `examples/zh/tier1/<name>/README.md` 和 `.env.example`。下面只写映射和陷阱,详细实现以 `agents/<name>.ts` 源码为准。
114
-
115
- ### ai-sdk-v7
116
-
117
- - 内置的 `uiMessageStreamAgent`(`niceeval/adapter` 导出)整个托管了这个应用——不需要手写 `send`,adapter 缩成纯配置(`url` + `body(ctx)` 透传 `ctx.model` + `settleMs`)。会话续接(客户端全量重放)、HITL 审批改写重发、事件直构全部是这个内置件的事。
118
- - 模型对比:请求体 `model` 字段,`ctx.model` 直接透传,server 不用重启,可选值看 `GET /api/models`。
119
- - OTel:应用用官方 `@ai-sdk/otel`,产标准 GenAI span;`niceeval.config.ts` 钉固定端口,应用启动时环境变量指过来。应用用 `BatchSpanProcessor`,span 可能晚到几秒——`settleMs: 600` 把收集窗口拉宽一点,只影响瀑布图完整性。
120
- - 备注:**进程内直调不被推荐**(被测对象是用户实际部署的应用,走 HTTP;测函数不等于测生产路径,详见[接入你的 Agent · 为什么不直调](../docs-site/zh/guides/connect-your-agent.mdx))——这个示例统一走无侵入 HTTP 接入,不提供进程内直调的对照版本。
121
-
122
- ### claude-sdk
123
-
124
- - 帧是原生 `SDKMessage`,官方转换器 `fromClaudeSdkMessages`(`niceeval/adapter` 导出)直接映射:`system`(带 `session_id`,写回 `ctx.session.capture()`)→ `assistant`(content blocks)→ `user`(`tool_result` 按 `tool_use_id` 配对)→ `result`(usage/cost)。逐帧驱动是官方件 `driveFrameStream`。
125
- - **HITL 没有显式的"等审批"帧**——`canUseTool` 把流卡住,`driveFrameStream` 的 `onFrame` 钩子扫 derived 事件,认出被门控的工具(`mcp__demo-tools__calculate`,写死在 adapter 里,必须和应用 `agent.ts` 里的 `GATED_TOOL_NAME` 完全一致)就返回 `{ pause }`。approve 端点偶发 404(SDK 内部注册 resolver 有竞态)时短退避重试几次,不是真的没有这次审批。
126
- - 无 trace spans(CLI 原生遥测只有 metrics+logs,niceeval 不消费),不声明 `spanMapper`,瀑布图这个应用没有——写进 eval README,不是失误。
127
- - 模型:`AGENT_MODEL` 注入子进程(代码默认 `deepseek-v4-flash`,`.env.example` 是 `claude-sonnet-5`,以 `.env.example` 为准)。
128
-
129
- ### codex-sdk
130
-
131
- - 帧是原生 `ThreadEvent`,官方转换器 `fromCodexThreadEvents`(`niceeval/adapter` 导出)映射:`thread.started`(带 `thread_id`,写回 session)→ `item.*` 系列(`agent_message` → `message`;`command_execution` / `file_change` / `mcp_tool_call` → 配对的 `action.called`/`action.result`)→ `turn.completed`(usage)/ `turn.failed` / `error`。
132
- - 无 HITL,永不返回 `waiting`。它是编码 agent,eval 测「在工作目录里写文件、跑命令」这类真实任务,用 `node:fs` 直接核实磁盘上的真实内容,不只信模型自述。
133
- - OTel:codex CLI 原生 OTLP,长驻服务必须 run 级共享接收器(固定端口模式)。span 是 codex 自家命名,声明 `spanMapper: mapCodexSpans`(`niceeval/adapter` 公开导出)归一后瀑布图和内置 `codexAgent` 一致——**事件断言的数据来源始终是 `ThreadEvent` 流,和 span 无关**。
134
- - 模型:`AGENT_MODEL`(默认 `gpt-5.4`),自定义 provider 走 `CODEX_BASE_URL`。
135
-
136
- ### pi-sdk
137
-
138
- - 手写映射路线最完整的示范:无 OTel、有 HITL、服务端内存 session。帧 = 原生 `AgentEvent`(官方转换器 `fromPiAgentEvents` 映射)+ 三种自定义传输帧:`{type:"session", sessionId}`(写回 session)、`{type:"approval_request", toolCallId, toolName, args}`(→ `input.requested` + `waiting`,`driveFrameStream` 的 `onFrame` 里返回 `{ pause }`)、`{type:"server_error", message}`(→ `failed`)。
139
- - HITL 走标准配方,approve 端点字段 `toolUseId`。
140
- - session 在服务端内存里,**跑多轮 eval 时不要重启应用**,重启即丢会话。
141
- - 无 OTel。模型:`AGENT_MODEL`,只有 `deepseek-v4-flash` / `deepseek-v4-pro` 两个可选。
142
-
143
- ### langgraph
144
-
145
- - 唯一的 Python 应用,也是唯一**完全手写帧映射、零 OTel 依赖用于事件**的应用:自定义 JSON 帧(`tool-input` → `action.called`、`tool-output` → `action.result`(completed,帧带 `isError: true` 时 failed)、`tool-output-denied` → `action.result`(rejected)、`text-delta` 累积成 `message`、`tool-approval-request` → `input.requested` + `waiting`)。工具异常靠 `ToolRetryMiddleware(max_retries=0, on_failure="continue")` 落成 `status="error"` 的 ToolMessage——`create_agent` 默认让工具异常炸穿整张图,没有这层就表达不了"执行了但失败"。
146
- - HITL 标准配方,**approve 端点字段是 `toolCallId`**(别照抄 claude/pi 的 `toolUseId`)。
147
- - session 是 `InMemorySaver`,同 pi:应用不要中途重启。
148
- - OTel:LangSmith 导出的 span 只用来画瀑布图(`niceeval.config.ts` 固定端口 + 应用侧 `LANGSMITH_TRACING` / `LANGSMITH_OTEL_ENABLED` / `LANGSMITH_OTEL_ONLY` 三个环境变量),事件断言完全不依赖它。LangSmith 的 `BatchSpanProcessor` 调度和 SSE 流关闭是两条独立时间线,adapter 在轮次结束后主动等一小段(grace period)把最后一批 span 收进瀑布图。
149
-
150
- ## 每个应用要写的 eval(最低集合)
151
-
152
- 1. **基础问答**:`t.send` 一轮,`t.succeeded()` + 文本断言。
153
- 2. **工具调用**:触发工具,`t.calledTool` / `t.toolOrder` / `t.noFailedActions`。
154
- 3. **多轮记忆 + 隔离**:第一轮报名字、第二轮问名字;`t.newSession()` 再问,新会话不应知道——这条专门验证会话续接没写错(最常见 bug:adapter 没有正确使用 `ctx.session` 存取器,导致跨会话线串用历史,隔离静默失真)。
155
- 4. **HITL 批准 + 拒绝**(有审批流的应用):`waiting` → `respond("approve")` → `calledTool(..., {status:"completed"})`;拒绝分支断 `status:"rejected"`。
156
- 5. **用量/成本**(能拿到 usage 的应用):`t.maxTokens` 冒烟。
157
-
158
- experiment 至少两个:单配置基线 + 一个 `compare-models/` 实验组(ai-sdk-v7 / claude-sdk / pi-sdk 有多模型可比)。
159
-
160
- ## 验收清单(每个应用)
161
-
162
- - [ ] `git status` 确认 origin 目录零改动;tier1 目录里被复制的应用文件与 origin 逐字节一致(除三个集成脚手架文件)
163
- - [ ] `pnpm run typecheck` 通过
164
- - [ ] `npx niceeval exp <基线>` 全绿;`npx niceeval view` 里事件流完整(message + action 配对)
165
- - [ ] 多轮记忆和 `newSession()` 隔离两条 eval 都过
166
- - [ ] 有 HITL 的:approve / deny 两条都过,deny 的工具结果是 `rejected` 不是 `failed`
167
- - [ ] 有 OTel 的:view 瀑布图非空
168
- - [ ] eval README 写明这个应用做不到什么(没有 OTel 的写清楚、负断言不完全靠事件完整性证明的写清楚)
169
-
170
- ## Tier 3 侵入点(已实现,产出在 examples/zh/tier3/)
171
-
172
- feature A/B test 已落地:每个应用挑一个最小侵入点做成**请求体可选字段**(不是环境变量——环境变量意味着换变体要重启应用,一次 experiment 运行里 A/B 不了;默认行为不变的铁律不变):
173
-
174
- - ai-sdk-v7:system prompt(`instructions`)、工具子集(`tools`)→ `tier3/ai-sdk-v7`;
175
- - claude-sdk:system prompt → `tier3/claude-sdk`;
176
- - codex-sdk:`threadOptions` 的 sandbox mode → `tier3/codex-sdk`;
177
- - pi-sdk:system prompt → `tier3/pi-sdk`;
178
- - langgraph:system prompt(变体各自编译图、共享 checkpointer)→ `tier3/langgraph`。
179
-
180
- experiment 侧用 `flags` → `ctx.flags` 透传,写法见 [Experiments](experiments.md);每个 tier3 目录的 README 有完整的 flags 流动链路。
181
-
182
- ## 现状(五个应用已全部接完,三档分层落位)
183
-
184
- 本工单描述的五个应用已全部接入并实测通过,Tier 1 产出在 `examples/zh/tier1/<name>/`;OTel 接线后来按分档定义剥进 `tier2/`,侵入改造落在 `tier3/`(见上文各节)。实现比本工单最初设想的更简单,主要是两处收紧:
185
-
186
- - **没有 `server-lifecycle.ts` 这类"eval 侧拉起应用子进程"的机制。** 五个应用都由使用者自己按各自方式启动(`pnpm start` / `python server.py`),adapter 只经环境变量(`<NAME>_URL`)指向一个已经在跑的实例。这与"eval 不代管被测进程"的既定原则一致,比最初设想的自动拉起子进程更简单也更贴合真实用法。
187
- - **OTel 只画瀑布图,不派生任何事件。** 五个应用的事件断言全部来自 `send` 里的官方转换器或手写帧映射,与有没有接 OTel 无关。
188
-
189
- 接入沉淀出的官方件,现在都还在:
190
-
191
- - **`uiMessageStreamAgent`**(`src/agents/ui-message-stream.ts`,从 `niceeval/adapter` 导出):AI SDK UI Message Stream 协议的内置无侵入 adapter,`tier1/ai-sdk-v7` 的 adapter 已缩成纯配置。
192
- - **`fromCodexThreadEvents` 补全**:ThreadEvent 的工具项(`command_execution` / `mcp_tool_call` / `file_change` → 配对的 `action.*`)与 `turn.completed` 的 usage 聚合,现在是 `tier1/codex-sdk` 事件断言的唯一数据来源(不再依赖任何 span 派生)。
193
- - **`mapCodexSpans`**(`src/o11y/otlp/mappers/codex.ts`,从 `niceeval/adapter` 导出):把 codex 自家 span 命名归一成 canonical GenAI 语义,只用来让瀑布图和内置 `codexAgent` 保持一致。
194
-
195
- 五个 `niceeval exp` 基线在 Tier 1 阶段全绿实测过(`ai-sdk-v7` 的多模型对比也验证过),五个 before/after 文档页已生成并挂进 `docs-site/docs.json` 导航。tier3 的五组 compare-* 实验也已对着真实 key 整轮实测:四个 prompt A/B 全绿(pi-sdk 的极简变体曾让模型心算跳过工具,见 `tier3/pi-sdk/experiments/compare-prompts/concise.ts` 注释),codex-sdk 的 compare-sandbox 按预期呈现 read-only 变体下 create-file 断言失败。这轮实测还顺带揪出并修掉了 runner 的一个丢结果 bug(`memory/runner-earlyexit-key-misses-experiment.md`)。
@@ -1,35 +0,0 @@
1
- # References —— 从其它项目学到什么
2
-
3
- 已经落地的借鉴大多分散记在各篇文档自己的"来源"脚注里(比如 [README](README.md) 的整体形状借鉴、[assertions.md](assertions.md#来源) 里逐条断言标来源、[experiments.md](experiments.md#从agent-eval砍掉了什么以及为什么) 里 `defineExperiment` 对照 agent-eval 的 `ExperimentConfig`)。这篇文档不重复那些,专门记录**调研某个外部项目时学到的东西**——抄了什么、还没抄但值得抄什么、调研过判断不值得抄的及理由——方便以后再研究别的项目时按同样的格式续写,也方便回头核对"这个设计当初是照着谁的形状定的"。
4
-
5
- 每次调研一个外部项目开一个二级标题,格式固定:是什么 → 已经借鉴的 → 这次新学到、值得抄的 → 调研过但不打算抄的(及理由)。
6
-
7
- ## Vercel agent-eval —— `packages/playground`
8
-
9
- **来源路径:** `/Users/ctrdh/Code/agent-eval/packages/playground`(本机另一个项目,不在这个仓库里,记路径方便下次再核对实现有没有变)。
10
-
11
- **是什么:** `@vercel/agent-eval-playground`,独立发布的 Next.js 结果查看器。零数据库、零 API 路由——所有页面是 Server Component,`lib/data.ts` 直接 `fs.readdirSync`/`readFileSync` 读 `results/`(实验结果)和 `evals/`(eval fixture)两个目录,`force-dynamic` 保证每次请求都读最新的盘上数据。CLI(`bin.mjs`)把目录路径塞进环境变量后 `spawn` 包内置的 `next start`。
12
-
13
- ### 已经借鉴的(更早调研,已经在别处记过来源)
14
-
15
- `niceeval view` 本身("本地结果查看器")、sandbox / diff 的工程形状、transcript 归一化与可观测、experiment 层——这些在 [README](README.md#niceeval-是什么) 和 [source-map.md](source-map.md) 里已经标了来源,这里不重复。
16
-
17
- ### 这次新学到、值得抄的
18
-
19
- 1. **`/compare`——挑两次运行对比。** playground 靠 `results/<experiment>/<ISO-timestamp>/` 天然分层的目录结构,能选任意两个时间戳的 run,对比整体通过率 / 平均耗时 / per-eval 通过率 delta。niceeval 现在完全没有这个能力——`aggregateRows` 把所有历史 run 合并成一行,选不出"这次 vs 上次"。这只是 view 里计划新增的一个小 tab,设计方案见 [View · Compare](view.md#compare-挑两次运行对比)。
20
- 2. **eval fixture 目录页(`/evals`)。** 独立于"跑过的结果",单纯浏览 `evals/` 目录下每个 fixture 的 `PROMPT.md` 和文件列表,不用先跑一次才能看"有哪些 eval、prompt 写的什么"。niceeval 的 `view` 目前完全是结果驱动的,没有这种纯浏览eval 定义的入口——值得抄,但优先级低于 compare,先记在这里。
21
- 3. **"每次 run 是独立时间戳快照"这个数据原则。** playground 的 `getExperiment` 保留 `timestamps: string[]` 整个历史列表,`/compare` 就是靠这个地基做的。niceeval 要抄的是这个**原则**(不要在聚合时提前合并掉快照身份),不是照搬它的目录 / API 形状。
22
-
23
- ### 调研过、判断不值得抄的(及理由)
24
-
25
- 1. **Tool 遥测是固定的 10 项 `ToolName` 枚举** (`file_read`/`shell`/`web_fetch`/…) + Badge 计数(`O11ySummary.tsx`)。niceeval 走的是 OTel GenAI 语义约定的 canonical trace/mapper(见 [Observability](observability.md#标准事件流与-streamevent)),覆盖面和跨 agent 一致性都更好——这块 niceeval 已经比它强,不用倒退抄。
26
- 2. **整个架构是"每次请求都读 fs 的 Next.js 多页面 live server"。** 没有数据库、没有 API 路由,但需要一个常驻的 `next start` 进程。niceeval 的 `view` 是"一次性烘焙 HTML+JSON 静态产物"(`src/view/index.ts` 的 `renderHtml`),导出目录扔给任何静态托管就能看,不需要常驻进程。这是刻意的取舍,不打算改成常驻多页应用——如果要抄 `/compare`,数据仍然要在生成 HTML 时一次性烘焙进去,不能假设前端能随时再查 fs。
27
- 3. **`bin.mjs` 的 `--watch` flag。** 只把 `WATCH=true` 塞进环境变量,代码里没有看到被消费的地方,像是半成品,没必要照抄这个具体实现。
28
-
29
- ## 相关阅读
30
-
31
- - [View](view.md) —— 上面几条学到的东西,具体设计在这篇(Compare 是其中一个计划中的小功能)。
32
- - [Observability](observability.md#结果可视化niceeval-view) —— `niceeval view` 现有能力全貌,对照着看这篇的"还差什么"更清楚。
33
- - [agent-eval 适配笔记](adapters/reference/agent-eval.md) —— agent-eval 的 adapter 实现(采集 / 转换 / 落地)的源码阅读记录。
34
- - [OTel GenAI 等标准参考](adapters/reference/otel-genai.md) —— "agent 行为怎么记"的行业标准调研,对比 agent-eval 的自定义方案。
35
- - [eve 协议机制](adapters/reference/eve-protocol.md) —— eve 运行时原生事件流的字段与采集机制,StreamEvent 演进的上限参照。