niceeval 0.4.6 → 0.5.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 (174) hide show
  1. package/docs-site/zh/concepts/adapter.mdx +6 -6
  2. package/docs-site/zh/concepts/experiment.mdx +7 -7
  3. package/docs-site/zh/concepts/overview.mdx +1 -1
  4. package/docs-site/zh/concepts/tier.mdx +6 -6
  5. package/docs-site/zh/guides/agent-feedback-loop.mdx +63 -20
  6. package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
  7. package/docs-site/zh/guides/custom-reports.mdx +243 -0
  8. package/docs-site/zh/guides/experiments.mdx +3 -3
  9. package/docs-site/zh/guides/report-components.mdx +252 -0
  10. package/docs-site/zh/guides/reporters.mdx +1 -1
  11. package/docs-site/zh/guides/results-data.mdx +224 -0
  12. package/docs-site/zh/guides/runner.mdx +1 -1
  13. package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
  14. package/docs-site/zh/guides/viewing-results.mdx +147 -77
  15. package/docs-site/zh/guides/write-experiment.mdx +9 -9
  16. package/docs-site/zh/guides/write-send.mdx +6 -6
  17. package/docs-site/zh/index.mdx +1 -1
  18. package/docs-site/zh/introduction.mdx +1 -1
  19. package/docs-site/zh/reference/cli.mdx +10 -2
  20. package/docs-site/zh/reference/define-agent.mdx +5 -5
  21. package/docs-site/zh/reference/define-config.mdx +1 -1
  22. package/docs-site/zh/reference/define-eval.mdx +3 -3
  23. package/package.json +3 -2
  24. package/src/agents/ai-sdk.test.ts +1 -1
  25. package/src/agents/ai-sdk.ts +2 -2
  26. package/src/agents/claude-code.ts +1 -1
  27. package/src/agents/codex.ts +2 -2
  28. package/src/agents/streaming.test.ts +1 -1
  29. package/src/agents/types.ts +4 -4
  30. package/src/agents/ui-message-stream.test.ts +1 -1
  31. package/src/cli.ts +104 -14
  32. package/src/context/context.test.ts +1 -1
  33. package/src/context/context.ts +3 -3
  34. package/src/context/session.ts +2 -2
  35. package/src/context/types.ts +2 -2
  36. package/src/i18n/en.ts +29 -7
  37. package/src/i18n/zh-CN.ts +24 -2
  38. package/src/report/aggregate.ts +79 -50
  39. package/src/report/components.tsx +190 -0
  40. package/src/report/compute.ts +149 -84
  41. package/src/report/default-report.tsx +222 -0
  42. package/src/report/dual-face.test.tsx +527 -0
  43. package/src/report/format.ts +29 -0
  44. package/src/report/index.ts +79 -24
  45. package/src/report/load.ts +67 -0
  46. package/src/report/metrics.ts +2 -1
  47. package/src/report/param.ts +18 -0
  48. package/src/report/primitives.tsx +117 -0
  49. package/src/report/react/CaseList.tsx +2 -2
  50. package/src/report/react/DeltaTable.tsx +2 -2
  51. package/src/report/react/MetricBars.tsx +109 -0
  52. package/src/report/react/MetricLine.tsx +200 -0
  53. package/src/report/react/MetricMatrix.tsx +1 -1
  54. package/src/report/react/MetricScatter.tsx +1 -1
  55. package/src/report/react/MetricTable.tsx +1 -1
  56. package/src/report/react/RunOverview.tsx +4 -4
  57. package/src/report/react/Scoreboard.tsx +3 -3
  58. package/src/report/react/cell.tsx +1 -1
  59. package/src/report/react/fixtures.ts +98 -36
  60. package/src/report/react/format.ts +3 -28
  61. package/src/report/react/index.tsx +41 -24
  62. package/src/report/react/render.test.tsx +8 -8
  63. package/src/report/react/styles.css +77 -0
  64. package/src/report/report.test.ts +248 -128
  65. package/src/report/report.ts +69 -0
  66. package/src/report/text/faces.ts +356 -0
  67. package/src/report/text/layout.ts +125 -0
  68. package/src/report/text/plot.ts +127 -0
  69. package/src/report/tree.ts +220 -0
  70. package/src/report/types.ts +75 -27
  71. package/src/report/web.ts +40 -0
  72. package/src/results/copy.ts +95 -41
  73. package/src/results/format.ts +12 -11
  74. package/src/results/index.ts +34 -17
  75. package/src/results/open.ts +201 -81
  76. package/src/results/results.test.ts +595 -191
  77. package/src/results/select.ts +107 -55
  78. package/src/results/types.ts +137 -45
  79. package/src/results/writer.ts +258 -0
  80. package/src/runner/attempt.ts +3 -3
  81. package/src/runner/fingerprint.ts +1 -1
  82. package/src/runner/reporters/artifacts.ts +38 -78
  83. package/src/runner/reporters/braintrust.test.ts +2 -2
  84. package/src/runner/reporters/braintrust.ts +3 -3
  85. package/src/runner/run.ts +1 -1
  86. package/src/runner/types.ts +18 -12
  87. package/src/show/compose.ts +215 -0
  88. package/src/show/index.ts +263 -0
  89. package/src/show/render.ts +456 -0
  90. package/src/show/show.test.ts +505 -0
  91. package/src/view/app/App.tsx +85 -24
  92. package/src/view/app/components/CostScoreChart.tsx +29 -10
  93. package/src/view/app/components/ExperimentTable.tsx +27 -9
  94. package/src/view/app/components/GroupSelector.tsx +1 -1
  95. package/src/view/app/i18n.ts +18 -3
  96. package/src/view/app/lib/attempt-route.test.ts +9 -9
  97. package/src/view/app/lib/attempt-route.ts +5 -5
  98. package/src/view/app/lib/outcome.ts +1 -3
  99. package/src/view/app/lib/rows.ts +95 -15
  100. package/src/view/app/main.tsx +18 -6
  101. package/src/view/app/pages/RunsPage.tsx +4 -7
  102. package/src/view/app/pages/TracesPage.tsx +3 -8
  103. package/src/view/app/types.ts +50 -6
  104. package/src/view/client-dist/app.css +1 -1
  105. package/src/view/client-dist/app.js +20 -20
  106. package/src/view/data.test.ts +254 -0
  107. package/src/view/data.ts +336 -0
  108. package/src/view/index.ts +64 -10
  109. package/src/view/server.ts +31 -9
  110. package/src/view/shared/types.ts +47 -41
  111. package/src/view/styles.css +5 -0
  112. package/src/view/template.html +1 -0
  113. package/src/view/view-report.test.ts +308 -0
  114. package/docs-site/.mintignore +0 -13
  115. package/docs-site/AGENTS.md +0 -46
  116. package/docs-site/concepts/adapter.mdx +0 -287
  117. package/docs-site/concepts/assert.mdx +0 -243
  118. package/docs-site/concepts/drive.mdx +0 -118
  119. package/docs-site/concepts/evals.mdx +0 -108
  120. package/docs-site/concepts/experiment.mdx +0 -37
  121. package/docs-site/concepts/hitl.mdx +0 -83
  122. package/docs-site/concepts/judge.mdx +0 -129
  123. package/docs-site/concepts/overview.mdx +0 -98
  124. package/docs-site/concepts/tier.mdx +0 -42
  125. package/docs-site/docs-ref/00-index.md +0 -23
  126. package/docs-site/docs-ref/01-page-types.md +0 -43
  127. package/docs-site/docs-ref/03-style-rules.md +0 -45
  128. package/docs-site/docs-ref/04-code-examples.md +0 -37
  129. package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
  130. package/docs-site/docs-ref/06-checklists.md +0 -53
  131. package/docs-site/docs.json +0 -256
  132. package/docs-site/example/ai-agent-application.mdx +0 -148
  133. package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
  134. package/docs-site/example/claude-code-codex-skill.mdx +0 -147
  135. package/docs-site/example/showcase.mdx +0 -39
  136. package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
  137. package/docs-site/example/tier1-claude-sdk.mdx +0 -119
  138. package/docs-site/example/tier1-codex-sdk.mdx +0 -111
  139. package/docs-site/example/tier1-langgraph.mdx +0 -119
  140. package/docs-site/example/tier1-pi-sdk.mdx +0 -119
  141. package/docs-site/favicon.svg +0 -5
  142. package/docs-site/github-diff.css +0 -135
  143. package/docs-site/github-diff.js +0 -11
  144. package/docs-site/guides/authoring.mdx +0 -129
  145. package/docs-site/guides/ci-integration.mdx +0 -97
  146. package/docs-site/guides/connect-otel.mdx +0 -209
  147. package/docs-site/guides/connect-your-agent.mdx +0 -221
  148. package/docs-site/guides/dataset-fanout.mdx +0 -98
  149. package/docs-site/guides/experiments.mdx +0 -71
  150. package/docs-site/guides/fixtures.mdx +0 -147
  151. package/docs-site/guides/reporters.mdx +0 -113
  152. package/docs-site/guides/runner.mdx +0 -79
  153. package/docs-site/guides/sandbox-agent.mdx +0 -138
  154. package/docs-site/guides/sandbox-backends.mdx +0 -64
  155. package/docs-site/guides/scoring-guide.mdx +0 -92
  156. package/docs-site/guides/viewing-results.mdx +0 -195
  157. package/docs-site/guides/write-experiment.mdx +0 -81
  158. package/docs-site/guides/write-send.mdx +0 -347
  159. package/docs-site/index.mdx +0 -181
  160. package/docs-site/introduction.mdx +0 -141
  161. package/docs-site/quickstart.mdx +0 -136
  162. package/docs-site/reference/builtin-agents.mdx +0 -161
  163. package/docs-site/reference/capabilities.mdx +0 -76
  164. package/docs-site/reference/cli.mdx +0 -120
  165. package/docs-site/reference/define-agent.mdx +0 -168
  166. package/docs-site/reference/define-config.mdx +0 -41
  167. package/docs-site/reference/define-eval.mdx +0 -160
  168. package/docs-site/reference/events.mdx +0 -131
  169. package/docs-site/reference/expect.mdx +0 -112
  170. package/docs-site/tracker.js +0 -8
  171. package/src/report/react/data.ts +0 -17
  172. package/src/view/aggregate.ts +0 -103
  173. package/src/view/loader.test.ts +0 -61
  174. package/src/view/loader.ts +0 -257
@@ -0,0 +1,252 @@
1
+ ---
2
+ title: "报告组件一览"
3
+ sidebarTitle: "报告组件"
4
+ description: "报告文件里能摆的全部官方双面组件:每个组件回答什么问题、怎么调用、网页面长什么样、终端字符输出长什么样。"
5
+ ---
6
+
7
+ 报告文件里的每个组件都是双面的:网页面是 React 渲染,终端面是字符渲染,两面吃同一份算好的数据,走哪扇门由宿主决定(见[自定义报告](/zh/guides/custom-reports))。每个组件的数据算法挂在它自己身上——`<MetricTable data={await MetricTable.data(selection, …)} />`,组件名后打个点就找到,算和画永远配对。本页逐个列出官方组件:它回答什么问题、在报告里怎么调用、终端输出长什么样。
8
+
9
+ 所有组件共守同一套契约,下面不再逐个重复:
10
+
11
+ - **诚实渲染。** 缺数据渲染 `—` 不补 0;覆盖不全的格子带 `12/15` 角标;截断如实标注剩余数量与原始工件路径。
12
+ - **排序与方向随指标的 `better`。** higher 的指标降序、lower 的升序,「好」的一头恒在上、在右上。
13
+ - **证据室深链。** 网页面的格子、点和条目深链到 attempt 弹窗;终端面在条目下印 `niceeval show <eval id>` 下钻命令。
14
+ - **两面同口径,网页面不 hydrate 也完整。** 排序在计算时由 `sort` 定死,两面预排一致;下钻是普通链接、展开折叠用 `<details>`;网页面不提供终端面没有的口径操作,静态导出零客户端 JS。
15
+
16
+ ## 排版原语
17
+
18
+ `Row` / `Col` / `Section` / `Text` 负责摆版面,一次摆放两个面各自成立:`Col` 纵向堆叠;`Row` 网页横排、终端字符分栏(宽度不够自动降级纵向);`Section` 是带标题的块;`Text` 是说明文字。另有 `<Style>{css}</Style>` 给自定义组件带样式:web 面吐 `<style>` 标签、text 面渲染为空——静态导出不打包用户代码,`className` 引用的 CSS 靠它随树走。
19
+
20
+ ```tsx
21
+ <Col>
22
+ <Section title="考试成绩单">
23
+ <Row>
24
+ <Scoreboard data={board} />
25
+ <Text>algebra 权重 ×2,满分 100。</Text>
26
+ </Row>
27
+ </Section>
28
+ </Col>
29
+ ```
30
+
31
+ ```text
32
+ 考试成绩单
33
+ agent 总分 algebra geometry │ algebra 权重 ×2,
34
+ bub 86.5/100 45/50 41.5/50 │ 满分 100。
35
+ codex 78.0/100 40/50 38/50 │
36
+ ```
37
+
38
+ ## `DefaultReport` —— 官方水位整块
39
+
40
+ 官方两扇门裸跑时渲染的就是它:跨 run 合成的现刻榜单加失败清单,表头如实标注合成来源。零 props、纯声明——官方水位只读瘦身条目,宿主总是替你备好,经上下文注入。把官方水位铺进自己的报告时摆上它,不要就不摆:
41
+
42
+ ```tsx
43
+ <DefaultReport />
44
+ ```
45
+
46
+ ```text
47
+ Current verdicts · 2 experiments · composed from 3 runs · latest 2026-07-09T10-00-00-000Z
48
+
49
+ experiment evals pass cost duration
50
+ compare/bub-gpt-5.4 13/15 87% $0.42 4m 12s
51
+ compare/codex-gpt-5.4 12/15 80% $0.51 5m 03s
52
+
53
+ Failing:
54
+ ✗ weather/brooklyn compare/codex-gpt-5.4 gate calledTool("get_weather") 41s ago
55
+ ```
56
+
57
+ ## `RunOverview` —— 页头 KPI 条
58
+
59
+ 每张报告开头「这批数据是什么」:几个配置、几道题、通过分布、总成本、何时跑的;选集里的挑选警告随数据一起进来、直接显示在条内,诚实不靠你记得接线。
60
+
61
+ ```tsx
62
+ <RunOverview data={await RunOverview.data(selection)} />
63
+ ```
64
+
65
+ ```text
66
+ 2 experiments · 15 evals · 42 attempts · composed from 3 runs · latest 2026-07-09T10-00
67
+ passed 26 · failed 12 · errored 4 · skipped 0 · $0.93 · 9m 15s
68
+ ! snapshot compare/codex-gpt-5.4 predates latest run by 2 days
69
+ ```
70
+
71
+ ## `MetricTable` —— 榜单
72
+
73
+ 一行一个维度值、一列一个指标的对比表,回答「谁整体更好」。行维度、指标列、排序全部可换,自定义指标(`defineMetric`)与内置指标同列。
74
+
75
+ ```tsx
76
+ <MetricTable data={await MetricTable.data(selection, {
77
+ rows: "agent",
78
+ columns: [passRate, codeLines, costUSD],
79
+ sort: passRate,
80
+ })} />
81
+ ```
82
+
83
+ ```text
84
+ agent pass rate code lines cost
85
+ bub 87% 312 lines $0.42
86
+ codex 80% 12/15 355 lines $0.51
87
+ ```
88
+
89
+ 两面都按 `sort` 预排,看到同一份顺序——要换排序改一行重跑,不做单面才有的交互口径。`12/15` 角标表示该格 15 个 attempt 里只有 12 个测得了这个指标。
90
+
91
+ ## `MetricMatrix` —— 逐题格子
92
+
93
+ 行 × 列两个维度、格子里一个指标,回答「哪道题谁挂了」。稀疏渲染:没有样本的格子空着,不编数。
94
+
95
+ ```tsx
96
+ <MetricMatrix data={await MetricMatrix.data(selection, { rows: "eval", columns: "agent", cell: passRate })} />
97
+ ```
98
+
99
+ ```text
100
+ eval bub codex
101
+ algebra/quadratic 100% 100%
102
+ algebra/matrix 100% 67%
103
+ geometry/area 50% —
104
+
105
+ next: niceeval show geometry/area
106
+ ```
107
+
108
+ 网页面点格子深链到该格的 attempt;终端面在表下印下钻命令。
109
+
110
+ ## `MetricBars` —— 分组条形
111
+
112
+ 同一份矩阵数据的另一种摆法(`MetricBars.data` 就是 `MetricMatrix.data` 的别名):按组并排比大小,回答「每个科目上谁领先、差多少」。组维度一组条、系列维度一根条、条长是指标值——benchmark 发布图(Terminal-Bench、BrowseComp 各一组,每个 agent 一根柱)就是这个形状。
113
+
114
+ ```tsx
115
+ <MetricBars data={await MetricBars.data(selection, {
116
+ rows: "evalGroup", // 一组条 = 一个科目/benchmark
117
+ columns: "agent", // 一根条 = 一个 agent
118
+ cell: passRate,
119
+ })} />
120
+ ```
121
+
122
+ ```text
123
+ algebra
124
+ bub ██████████████████░░ 91.9%
125
+ codex █████████████████░░░ 88.0%
126
+ geometry
127
+ bub ██████████░░░░░░░░░░ 50.0%
128
+ codex —
129
+ ```
130
+
131
+ 网页面是竖向分组柱:柱顶标数值,系列颜色与其它组件的稳定配色一致,图例自动生成。终端面横向条形,字符宽度即刻度,同组内按值排序(方向随 `better`)。`better: "lower"` 的指标(成本、耗时)条形反向填充,短条恒为「好」。
132
+
133
+ ## `Scoreboard` —— 考试成绩单
134
+
135
+ 总分 + 分科小计,回答「这套题它能得几分」。逐题分值制:权重按 eval id 前缀配置,分母对所有被打分者恒定,没跑到的题挣 0 分并如实报 `missing`。
136
+
137
+ ```tsx
138
+ <Scoreboard data={await Scoreboard.data(selection, {
139
+ rows: "agent",
140
+ subjects: "evalGroup",
141
+ weights: { "algebra/": 2 },
142
+ fullMarks: 100,
143
+ })} />
144
+ ```
145
+
146
+ ```text
147
+ agent total algebra geometry
148
+ bub 86.5/100 45/50 41.5/50
149
+ codex 71.0/100 40/50 31/50 (1 missing)
150
+ ```
151
+
152
+ ## `MetricScatter` —— 质量 × 成本 frontier
153
+
154
+ 每个点一个配置、两个指标各占一轴,回答「又好又便宜的是谁」。`series` 把同 agent 不同档位的点连成线;`better` 驱动轴向——`lower` 的轴反向画,「好」的角落恒在右上。
155
+
156
+ ```tsx
157
+ <MetricScatter data={await MetricScatter.data(selection, {
158
+ points: "experiment", // 每个点 = 一个配置的聚合
159
+ series: "agent", // 同 agent 的档位连成线
160
+ x: costUSD,
161
+ y: passRate,
162
+ })} />
163
+ ```
164
+
165
+ ```text
166
+ pass ↑ (好 → 右上)
167
+ 90%│ A
168
+ │ B
169
+ 75%│ C
170
+ 60%│ D
171
+ └──────────────────────────────────────→ cost(轴反向:越右越省)
172
+ $0.60 $0.45 $0.30 $0.15
173
+
174
+ A bub-high B bub-medium C codex-high D codex-low
175
+ ```
176
+
177
+ 网页面点带 hover(值与 `samples/total`)、同系列连线、点击深链下钻。终端面用字母标点、图例列在图下;x 或 y 缺数据的点两个面都不画,注脚如实报「n 个点缺数据」;点太密排不下时降级为坐标表,不硬挤。维度槽也收 `param()`——用 experiment 声明的变量分组,见[自定义报告](/zh/guides/custom-reports)的「变量来自配置」一节。
178
+
179
+ ## `MetricLine` —— 趋势线
180
+
181
+ x 是有序变量、每个系列一条线,回答「变量拧大,分数怎么走」——并行 agent 数 × 模拟延迟 × 得分这类 scaling 图就是它。与 `MetricScatter` 的分工:scatter 的两轴都是测出来的指标(找 frontier),line 的 x 是你配置的变量(看趋势)。变量在 experiment 的 `params` 里声明,报告用 `param()` 直接引用,不从 experiment 命名里解析。
182
+
183
+ ```tsx
184
+ <MetricLine data={await MetricLine.data(selection, {
185
+ x: param("latencyMs", { label: "Simulated latency", unit: "ms" }),
186
+ series: param("agents", { label: (v) => `${v} agents` }),
187
+ y: passRate,
188
+ })} />
189
+ ```
190
+
191
+ ```text
192
+ pass ↑
193
+ 80%│ C···C
194
+ │ C···C B
195
+ 60%│ C B···B
196
+ │ B···B A
197
+ 40%│ A···A
198
+ │ A
199
+ 20%│
200
+ └───────────────────────────────────────────→ Simulated latency
201
+ 1 min 5 min 10 min 20 min
202
+
203
+ A 1 agents B 4 agents C 16 agents
204
+ ```
205
+
206
+ 每个点是一个 experiment 的聚合,与其它组件同一台两级聚合引擎,`samples` / `total` / `refs` 一应俱全;同系列的点按 x 排序连线。网页面每个点可 hover、可深链下钻。终端面同系列共用一个字母、沿 x 排布,趋势肉眼可读;y 缺数据的点不画、注脚报数,点太密降级为坐标表。
207
+
208
+ ## `DeltaTable` —— 成对对比
209
+
210
+ 每行一对配置、每列一个指标,格子里 A、B、Δ 三个值,回答「这个开关值不值」「这次修复翻转了什么」。涨跌好坏由 `better` 判定,任一侧缺数据 Δ 显示为缺,不硬算。「这次 vs 上次」(同一配置的两个快照)也是它:`pairs` 的 `a` / `b` 除 experiment id 外也收快照键 `<experimentId> @ <startedAt>`——时间轴对比要用到旧快照,`results.latest()` 里没有,配手挑的快照数组(如 `[exp.latest, exp.snapshots[1]]`)用。
211
+
212
+ ```tsx
213
+ <DeltaTable data={await DeltaTable.data(selection, {
214
+ pairs: [
215
+ { a: "compare/bub", b: "compare/bub--agents-md", label: "bub" },
216
+ { a: "compare/codex", b: "compare/codex--agents-md", label: "codex" },
217
+ ],
218
+ metrics: [passRate, costUSD],
219
+ })} />
220
+ ```
221
+
222
+ ```text
223
+ pair pass rate cost
224
+ bub 87% → 93% +6% $0.42 → $0.45 +$0.03
225
+ codex 80% → 80% ±0 $0.51 → — —
226
+ ```
227
+
228
+ ## `CaseList` —— 失败案例清单
229
+
230
+ 失败与出错的 attempt 逐条列出:失败断言、error 摘要、judge 评语。榜单回答「多少」,它回答「为什么」。
231
+
232
+ ```tsx
233
+ <CaseList data={await CaseList.data(selection, { outcomes: ["failed", "errored"], limit: 20 })} />
234
+ ```
235
+
236
+ ```text
237
+ ✗ weather/brooklyn · compare/codex-gpt-5.4 · failed · 41s · $0.04
238
+ gate calledTool("get_weather") — tool was never called
239
+ soft judge("回答基于实时数据") — 0.2/1: reply invents a temperature
240
+ → niceeval show weather/brooklyn
241
+ ✗ fixtures/button · compare/bub-gpt-5.4 · failed · 2m 10s · $0.09
242
+ gate fileChanged("src/components/Button.tsx")
243
+ → niceeval show fixtures/button
244
+
245
+ (3 more not shown · limit 20)
246
+ ```
247
+
248
+ 自由文本(error、断言 detail、judge 评语)发布前要消毒时,用 `CaseList.data` 的 `redact` 钩子。
249
+
250
+ ## 官方组件之外
251
+
252
+ 以上摆法都表达不了时,用 `defineComponent` 写自己的双面组件——网页怎么渲染、终端字符怎么排,两个面你都说了算,写法见[自定义报告](/zh/guides/custom-reports)的「换形态」一节。
@@ -46,7 +46,7 @@ export default defineEval({
46
46
  - 每个 attempt 是 Braintrust 里的一行;`runs: 3` 会产生三行,靠 metadata 里的 `attempt` 区分。
47
47
  - soft 断言按名字记为 score;gate 断言记在 `gate:` 前缀下。这样在 Braintrust 的实验对比里,gate 回归和 soft 分数回归用同一套 diff 看。
48
48
  - metrics 带开始/结束时间、token 用量和估算成本;缺的数据不写,不补零。
49
- - metadata 带 `agent`、`model`、`experiment`、`flags`、`outcome` 和失败断言明细,方便在 Braintrust 里按维度过滤。
49
+ - metadata 带 `agent`、`model`、`experiment`、`params`、`outcome` 和失败断言明细,方便在 Braintrust 里按维度过滤。
50
50
 
51
51
  ### 配置项
52
52
 
@@ -0,0 +1,224 @@
1
+ ---
2
+ title: "用 niceeval/results 直接读写结果数据"
3
+ sidebarTitle: "结果数据 API"
4
+ description: "报告积木脚下的数据层:openResults 把 .niceeval/ 的落盘工件 parse 成「实验 → 快照 → eval → attempt」的类型化层次,createRunWriter 把别家结果写成 NiceEval 格式,copySnapshots 负责发布瘦身。"
5
+ ---
6
+
7
+ [自定义报告](/zh/guides/custom-reports)的积木——指标、计算函数、双面组件——脚下还有一层:`niceeval/results`,落盘工件的 parser。官方两扇门和全部报告积木读的都是这一层,没有私有数据通道;报告表达不了的口径,下到这层直接拿数据算。
8
+
9
+ 什么时候下到这层:
10
+
11
+ - **口径连计算函数都折不出。** 表格、矩阵、成绩单、散点都是「折叠」;分布类的看法(直方图、逐事件分析)不是折叠,要自己遍历 attempt。
12
+ - **把结果喂进自己的系统。** 数据仓库、告警、内部平台——吃类型化数据,不吃磁盘布局。
13
+ - **把别家平台的结果转成 NiceEval 格式。** 写入面保证「写出去的就是读得回的」,转完 `niceeval show` / `view` / 报告积木全套直接能用。
14
+
15
+ 这一层只管数据的读与写,不预设任何看法:不合并、不聚合、不去重,忠实反映磁盘。「怎么看」归上层的报告积木。
16
+
17
+ ## 读:`openResults`
18
+
19
+ 输入是 `.niceeval/` 目录(或单个 run 目录),输出是四层数据:**实验 → 快照(单次跑的实验)→ eval → attempt**。你从此不碰路径、不判断文件存在性、不解析 JSON:
20
+
21
+ ```typescript
22
+ import { openResults } from "niceeval/results";
23
+
24
+ const results = await openResults(".niceeval");
25
+
26
+ results.experiments; // Experiment[]:每个实验一项,挂着自己的全部历史
27
+ results.skipped; // 读不了的落盘:{ dir, reason, schemaVersion?, producer? }[]
28
+ ```
29
+
30
+ **第一层,实验**——同一个 experiment id 的历次运行归在一起:
31
+
32
+ ```typescript
33
+ const exp = results.experiments.find((e) => e.id === "compare/bub-gpt-5.4")!;
34
+
35
+ exp.snapshots; // Snapshot[]:历次快照,最新在前
36
+ exp.latest; // 最新一次(= snapshots[0])
37
+ exp.evalIds; // 历史覆盖过的 eval 并集,残缺检测的依据
38
+ ```
39
+
40
+ **第二层,快照**——单次跑的实验。它是谁、什么时候跑的、**用什么写的**,都在这一层:
41
+
42
+ ```typescript
43
+ const snap = exp.latest;
44
+
45
+ snap.agent; // 本快照自己的 agent
46
+ snap.model;
47
+ snap.startedAt; // 这次实验什么时候跑的
48
+ snap.producer; // { name: "niceeval", version: "0.4.6" } —— 谁写的这份结果
49
+ snap.schemaVersion; // 结果格式版本
50
+ ```
51
+
52
+ **第三层,eval**——这次实验里的每道题,attempt(重试历史)挂在题下面:
53
+
54
+ ```typescript
55
+ for (const ev of snap.evals) {
56
+ ev.id; // "algebra/quadratic"
57
+ ev.attempts; // AttemptHandle[]:该题的全部 attempt
58
+ }
59
+ snap.attempts; // 不关心题目边界时,全部 attempt 平铺
60
+ ```
61
+
62
+ **第四层,attempt**——瘦身条目直接在手上,重工件全部懒加载。唯一带 `Handle` 后缀的类型就是它:上面三层是纯数据,这一层的方法会碰磁盘:
63
+
64
+ ```typescript
65
+ const attempt = snap.evals[0].attempts[0];
66
+
67
+ attempt.evalId; // "algebra/quadratic" —— 属于哪道题,不用绕 result
68
+ attempt.experimentId; // 属于哪个实验
69
+ attempt.result; // EvalResult:判决、断言、用量、成本、experiment 元数据
70
+ attempt.ref; // { run, result }:证据引用,与 view 深链、报告格子的 refs 同一身份
71
+ await attempt.events(); // StreamEvent[] | null
72
+ await attempt.trace(); // TraceSpan[] | null
73
+ await attempt.o11y(); // O11ySummary | null
74
+ await attempt.diff(); // DiffData | null(可达上百 MB,所以必须懒)
75
+ await attempt.sources(); // SourceArtifact[] | null
76
+ ```
77
+
78
+ 三条要点:
79
+
80
+ - **懒加载即存在性判断。** 工件缺失返回 `null`,不抛错——remote agent 没有 `diff.json` 时,`await attempt.diff()` 就是 `null`,你据此决定跳过还是报缺。
81
+ - **读不了的落盘不静默。** 版本不兼容、目录损坏的 run 进 `skipped` 并带原因;要不要展示由你定,但缺口永远被算出来。
82
+ - **同进程内按句柄记忆化。** 两处都读同一个 `diff()` 不会把上百 MB 读两遍。
83
+
84
+ ## 版本:谁写的、读不读得了
85
+
86
+ 每个快照都带自己的出身:`producer` 是写这份结果的工具与版本(niceeval 自己,或经写入面转换的第三方 harness),`schemaVersion` 是磁盘格式版本。格式只在破坏兼容时递增版本,读取器只认相同版本——**版本不兼容的落盘不解析、不迁移、不猜**,整个 run 进 `skipped`:
87
+
88
+ ```typescript
89
+ for (const s of results.skipped) {
90
+ s.reason; // "incompatible-version" | "malformed" | "incomplete"
91
+ s.schemaVersion; // 那份结果声明的格式版本
92
+ s.producer; // { name, version }:谁写的它
93
+ }
94
+ ```
95
+
96
+ `"incomplete"` 与版本无关:run 中途 crash、没走到收尾写出 summary 的目录进这一类——已完成的 attempt 工件仍在盘上供手工排查,但没有 summary 的落盘对读取面不可见。
97
+
98
+ 旧版本的结果不会丢:磁盘上原样还在,用写它的那个工具就能看——`producer.name` 是 `"niceeval"` 时,提示用户 `npx niceeval@<producer.version> view`;是第三方 harness 时如实报出它的名字和版本,别拼一句错误的 npx 命令。`skipped` 给你的信息刚好够做对这个分支。
99
+
100
+ ## 为什么快照不是 run 目录
101
+
102
+ 一次 CLI 调用写一个 run 目录,但一个 run 目录里可以装多个 experiment(`niceeval exp compare` 整组对照进同一份汇总)。「每个 experiment 最新一次」因此没法用目录粒度表达:周一跑了整组,周二只重跑 `compare/bub-gpt-5.4`,bub 的最新在周二的目录里,codex 的还在周一的目录里。
103
+
104
+ 快照把「单次跑的实验」这层身份显式化——**一个 experiment 在一次落盘里的那部分结果**。物理目录仍可通过 `results.runDirs` 与 `snap.runDir` 触达(发布、排查工件路径时用),多数消费方不需要碰。
105
+
106
+ ## 选快照:`results.latest()`
107
+
108
+ 多数场景先回答「现在什么水位」,选择器替你挑「每个实验最新一次」——这也是官方榜单的口径。返回的是一个**选集(Selection)**,快照和警告绑在一起走:
109
+
110
+ ```typescript
111
+ const latest = results.latest({
112
+ experiments: "compare/", // 可选:experiment id 前缀过滤(string | string[]),同 CLI 前缀语义
113
+ });
114
+
115
+ latest.snapshots; // Snapshot[]:每个实验最新一次
116
+ latest.warnings; // 结构化警告,不是渲染好的文本
117
+ ```
118
+
119
+ 「最新」可能残缺:只重跑一道题是正常的 debug 姿势,它产出的最新快照就只有一道题。选择器把每个选中快照的覆盖与该实验的历史并集(`exp.evalIds`)对比,缩水就写进 `warnings`:
120
+
121
+ ```typescript
122
+ latest.warnings[0];
123
+ // {
124
+ // kind: "partial-coverage",
125
+ // experimentId: "midterm/bub-gpt-5.4",
126
+ // covered: 1,
127
+ // total: 50,
128
+ // message: "snapshot covers 1 of 50 evals seen in history; re-run `niceeval exp midterm/bub-gpt-5.4` for a full snapshot",
129
+ // }
130
+ ```
131
+
132
+ 字段供程序判断——CI 里「覆盖缩水就 fail」直接判 `covered < total`,不解析文本;`message` 是渲染好的英文句子,要展示就原样打。渲染与否在你,缺口永远被算出来。警告不止这一种:快照落后于选集中最新的落盘、experimentId 缺失被合成键等同样进 `warnings`,每种都带 `kind`、可判断的结构化字段和渲染好的 `message`。
133
+
134
+ 选集是[报告积木](/zh/guides/custom-reports)和下文 `copySnapshots` 的通用输入:收 `Selection` 时 warnings 随行(`RunOverview` 之类的组件会如实展示),手工挑的 `Snapshot[]` 数组照收。微调官方口径不用降级成裸数组:`latest.filter((s) => s.experimentId !== "compare/broken")` 返回新选集——快照被删减,warnings 修剪到幸存的实验,provenance 不丢。`filter` 只做删减;「换成该实验上一个完整快照」这类**替换式**重挑不是它的事,回到 `exp.snapshots` 自己拿——手工挑的数组没有挑选过程,自然没有 warnings 可带,也如实。
135
+
136
+ ## 一个真实脚本:分布不是折叠
137
+
138
+ 「每个 agent 的 shell 命令数分布」画不成表格——那是分布,不是折叠。直接遍历:
139
+
140
+ ```typescript
141
+ import { openResults } from "niceeval/results";
142
+
143
+ const results = await openResults(".niceeval");
144
+ const points = [];
145
+ for (const exp of results.experiments) {
146
+ for (const attempt of exp.latest.attempts) {
147
+ const o11y = await attempt.o11y();
148
+ points.push({
149
+ agent: exp.latest.agent,
150
+ eval: attempt.evalId,
151
+ passed: attempt.result.outcome === "passed",
152
+ shellCommands: o11y?.shellCommands.length ?? 0,
153
+ });
154
+ }
155
+ }
156
+ ```
157
+
158
+ 即使在这条最深的路径上也不碰磁盘布局:路径拼接、存在性、版本过滤、快照切分全被库消化。要把这份分布摆进报告页,用 [`defineComponent`](/zh/guides/custom-reports) 包一个双面组件即可。
159
+
160
+ 一条跨快照累计时的义务:`--resume` 会把上一轮已通过的结果原样合入新 run,同一个 attempt 因此可能存在于多份落盘。合入的条目不是空壳:它的工件仍在原 run 目录,懒加载会自动回退到那里读(原 run 目录被清理后如实返回 `null`);`ref` 指向条目所在的落盘。身份键 `(experimentId, evalId, attempt, startedAt)` 的四个字段都在数据上——前两个是 attempt 的直达字段,序号与 `startedAt` 在 `attempt.result` 上。reader 忠实反映这份重复;跨快照聚合前用 `dedupeAttempts` 按身份键去重,重复保留最新 run 目录里的那份——报告积木的计算函数内置这条,自己写脚本时记得过一遍:
161
+
162
+ ```typescript
163
+ import { dedupeAttempts } from "niceeval/results";
164
+
165
+ const all = exp.snapshots.flatMap((s) => s.attempts); // 跨快照累计:历史全量
166
+ const { attempts, warnings } = dedupeAttempts(all); // resume 合入的重复被折掉
167
+ ```
168
+
169
+ ## 写:`createRunWriter`
170
+
171
+ 写入面和读取面是同一组类型的两半,签名就是 roundtrip 的证明:reader 的 `attempt.result` 由两部分拼成——快照级字段(agent / model / startedAt / producer)来自 `writer.snapshot()` 的一次声明,其余全部字段就是 `writeAttempt` 第一个参数的类型;第二个参数是 reader 懒加载能拿到的那几样工件的类型。「writeAttempt 参数 + snapshot() 声明 = reader 读回的全部」由类型拼合背书:快照级字段不在 attempt 参数的类型里,不存在「谁的值为准」。用它把别家平台、自研 harness 的结果转成 NiceEval 格式:
172
+
173
+ ```typescript
174
+ import { createRunWriter } from "niceeval/results";
175
+
176
+ const writer = await createRunWriter(".niceeval", {
177
+ producer: { name: "my-harness", version: "1.0.0" },
178
+ });
179
+
180
+ const snap = writer.snapshot({ // 快照级元数据的家:一个 experiment 开一个
181
+ experiment: "compare/my-agent",
182
+ agent: "my-agent",
183
+ model: "gpt-5.4",
184
+ startedAt: sourceRun.startedAt, // 必填:去重身份键以它为锚
185
+ });
186
+
187
+ for (const r of convertedResults) {
188
+ await snap.writeAttempt(r.result, { // 第一参 = attempt 级条目;快照级字段在 snapshot() 声明
189
+ events: r.events, // 第二参 = 工件,都可选;缺哪样读取面就懒加载出 null
190
+ diff: r.diff,
191
+ }); // 拆工件文件、算目录、回填引用,全在库内发生
192
+ }
193
+
194
+ await writer.finish(); // summary 从已写入的 attempt 推导,计数永远和条目对得上
195
+ ```
196
+
197
+ `writer.snapshot()` 就是读取面「一个 run 装多份快照」的镜像:转多个 experiment 就开多个快照,agent / model / startedAt 这些快照级元数据在这里声明,不用塞进每条 attempt;可选的 `knownEvalIds`(该实验已知的 eval 并集)也在这里声明——它是残缺检测的分母,转换只覆盖部分题目时如实交代全集,下游的覆盖警告就能算出来(`copySnapshots` 发布时会自动补记这个字段,见下文)。转完的目录就是标准结果目录:`niceeval show` / `niceeval view` 直接能看,报告积木直接能算,不用抄格式文档;`producer` 会原样出现在读取面的 `snap.producer` 上。attempt 工件按完成增量落盘,但 summary 是 `finish()` 写出的收尾事实:中途 crash、没走到 `finish()` 的目录,已完成的工件仍在盘上供手工排查,读取面则把它归入 `skipped("incomplete")`,不认半份落盘。
198
+
199
+ ## 发布:`copySnapshots`
200
+
201
+ 把选中的快照按格式感知地复制到发布目录——只带指定工件、只带选中的 attempt:
202
+
203
+ ```typescript
204
+ import { openResults, copySnapshots } from "niceeval/results";
205
+
206
+ const results = await openResults(".niceeval");
207
+ await copySnapshots(results.latest(), "site-data/run", {
208
+ artifacts: ["sources", "events", "trace"], // diff 可达百 MB,发布时常见地不带
209
+ });
210
+ ```
211
+
212
+ 第一个参数收 `Selection` 或手工挑的 `Snapshot[]`——和报告积木同一个输入约定。`artifacts` 的合法值是 `"events" | "trace" | "o11y" | "diff" | "sources"`,缺省全带。目标目录已存在且非空时报错,不静默覆盖——发布脚本要幂等就自己先清目标目录。
213
+
214
+ 复制不改工件内容、不消毒;发布前给自由文本消毒用报告积木 `CaseList.data` 的 `redact` 钩子。唯一随行补记的是挑选时的**覆盖事实**:`partial-coverage` 警告的分母是实验的历史并集,而发布目录没有历史——所以每个复制出的快照带上 `knownEvalIds`(复制时刻该实验已知的 eval 并集),reader 端把它并进 `exp.evalIds` 的计算(取本地历史与快照携带值的并集)。发布目录上重新 `openResults().latest()`,残缺警告被同一套机制重新算出来,不靠发布者转述。CI 静态托管的完整流程见[查看结果 · 导出与静态托管](/zh/guides/viewing-results#导出与静态托管)。
215
+
216
+ ## 分层速览
217
+
218
+ | 层 | 入口 | 回答 |
219
+ | --- | --- | --- |
220
+ | 官方两扇门 | `niceeval show` / `niceeval view` | 零代码看官方摆法 |
221
+ | 报告积木 | [自定义报告](/zh/guides/custom-reports)、[报告组件](/zh/guides/report-components) | 自己的口径与摆法 |
222
+ | 结果数据 API(本页) | `niceeval/results` | 折叠表达不了的算法、接自己的系统、读写格式本身 |
223
+
224
+ 每层都建立在下一层之上,同一份落盘工件是唯一事实来源——上层的派生物删了随时可重算。
@@ -29,7 +29,7 @@ npx niceeval exp local weather
29
29
  npx niceeval exp local fixtures/button
30
30
  ```
31
31
 
32
- 不要把 agent 名放进位置参数。agent/model/flags 写进 experiment。
32
+ 不要把 agent 名放进位置参数。agent/model/params 写进 experiment。
33
33
 
34
34
  ## 并发
35
35
 
@@ -115,9 +115,9 @@ export default defineSandboxAgent({
115
115
  });
116
116
  ```
117
117
 
118
- ## `ctx.model` 与 `ctx.flags`
118
+ ## `ctx.model` 与 `ctx.params`
119
119
 
120
- CLI 传入的模型和自定义 flags 会出现在 adapter context 中。adapter 可以决定如何把它们转成 CLI 参数或 HTTP payload。
120
+ CLI 传入的模型和自定义 params 会出现在 adapter context 中。adapter 可以决定如何把它们转成 CLI 参数或 HTTP payload。
121
121
 
122
122
  ## 在 experiment 中使用自定义 agent
123
123