niceeval 0.4.5 → 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 (175) 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/bub.ts +14 -4
  27. package/src/agents/claude-code.ts +1 -1
  28. package/src/agents/codex.ts +2 -2
  29. package/src/agents/streaming.test.ts +1 -1
  30. package/src/agents/types.ts +4 -4
  31. package/src/agents/ui-message-stream.test.ts +1 -1
  32. package/src/cli.ts +104 -14
  33. package/src/context/context.test.ts +1 -1
  34. package/src/context/context.ts +3 -3
  35. package/src/context/session.ts +2 -2
  36. package/src/context/types.ts +2 -2
  37. package/src/i18n/en.ts +29 -7
  38. package/src/i18n/zh-CN.ts +24 -2
  39. package/src/report/aggregate.ts +79 -50
  40. package/src/report/components.tsx +190 -0
  41. package/src/report/compute.ts +149 -84
  42. package/src/report/default-report.tsx +222 -0
  43. package/src/report/dual-face.test.tsx +527 -0
  44. package/src/report/format.ts +29 -0
  45. package/src/report/index.ts +79 -24
  46. package/src/report/load.ts +67 -0
  47. package/src/report/metrics.ts +2 -1
  48. package/src/report/param.ts +18 -0
  49. package/src/report/primitives.tsx +117 -0
  50. package/src/report/react/CaseList.tsx +2 -2
  51. package/src/report/react/DeltaTable.tsx +2 -2
  52. package/src/report/react/MetricBars.tsx +109 -0
  53. package/src/report/react/MetricLine.tsx +200 -0
  54. package/src/report/react/MetricMatrix.tsx +1 -1
  55. package/src/report/react/MetricScatter.tsx +1 -1
  56. package/src/report/react/MetricTable.tsx +1 -1
  57. package/src/report/react/RunOverview.tsx +4 -4
  58. package/src/report/react/Scoreboard.tsx +3 -3
  59. package/src/report/react/cell.tsx +1 -1
  60. package/src/report/react/fixtures.ts +98 -36
  61. package/src/report/react/format.ts +3 -28
  62. package/src/report/react/index.tsx +41 -24
  63. package/src/report/react/render.test.tsx +8 -8
  64. package/src/report/react/styles.css +77 -0
  65. package/src/report/report.test.ts +248 -128
  66. package/src/report/report.ts +69 -0
  67. package/src/report/text/faces.ts +356 -0
  68. package/src/report/text/layout.ts +125 -0
  69. package/src/report/text/plot.ts +127 -0
  70. package/src/report/tree.ts +220 -0
  71. package/src/report/types.ts +75 -27
  72. package/src/report/web.ts +40 -0
  73. package/src/results/copy.ts +95 -41
  74. package/src/results/format.ts +12 -11
  75. package/src/results/index.ts +34 -17
  76. package/src/results/open.ts +201 -81
  77. package/src/results/results.test.ts +595 -191
  78. package/src/results/select.ts +107 -55
  79. package/src/results/types.ts +137 -45
  80. package/src/results/writer.ts +258 -0
  81. package/src/runner/attempt.ts +3 -3
  82. package/src/runner/fingerprint.ts +1 -1
  83. package/src/runner/reporters/artifacts.ts +38 -78
  84. package/src/runner/reporters/braintrust.test.ts +2 -2
  85. package/src/runner/reporters/braintrust.ts +3 -3
  86. package/src/runner/run.ts +1 -1
  87. package/src/runner/types.ts +18 -12
  88. package/src/show/compose.ts +215 -0
  89. package/src/show/index.ts +263 -0
  90. package/src/show/render.ts +456 -0
  91. package/src/show/show.test.ts +505 -0
  92. package/src/view/app/App.tsx +85 -24
  93. package/src/view/app/components/CostScoreChart.tsx +29 -10
  94. package/src/view/app/components/ExperimentTable.tsx +27 -9
  95. package/src/view/app/components/GroupSelector.tsx +1 -1
  96. package/src/view/app/i18n.ts +18 -3
  97. package/src/view/app/lib/attempt-route.test.ts +9 -9
  98. package/src/view/app/lib/attempt-route.ts +5 -5
  99. package/src/view/app/lib/outcome.ts +1 -3
  100. package/src/view/app/lib/rows.ts +95 -15
  101. package/src/view/app/main.tsx +18 -6
  102. package/src/view/app/pages/RunsPage.tsx +4 -7
  103. package/src/view/app/pages/TracesPage.tsx +3 -8
  104. package/src/view/app/types.ts +50 -6
  105. package/src/view/client-dist/app.css +1 -1
  106. package/src/view/client-dist/app.js +20 -20
  107. package/src/view/data.test.ts +254 -0
  108. package/src/view/data.ts +336 -0
  109. package/src/view/index.ts +64 -10
  110. package/src/view/server.ts +31 -9
  111. package/src/view/shared/types.ts +47 -41
  112. package/src/view/styles.css +5 -0
  113. package/src/view/template.html +1 -0
  114. package/src/view/view-report.test.ts +308 -0
  115. package/docs-site/.mintignore +0 -13
  116. package/docs-site/AGENTS.md +0 -46
  117. package/docs-site/concepts/adapter.mdx +0 -287
  118. package/docs-site/concepts/assert.mdx +0 -243
  119. package/docs-site/concepts/drive.mdx +0 -118
  120. package/docs-site/concepts/evals.mdx +0 -108
  121. package/docs-site/concepts/experiment.mdx +0 -37
  122. package/docs-site/concepts/hitl.mdx +0 -83
  123. package/docs-site/concepts/judge.mdx +0 -129
  124. package/docs-site/concepts/overview.mdx +0 -98
  125. package/docs-site/concepts/tier.mdx +0 -42
  126. package/docs-site/docs-ref/00-index.md +0 -23
  127. package/docs-site/docs-ref/01-page-types.md +0 -43
  128. package/docs-site/docs-ref/03-style-rules.md +0 -45
  129. package/docs-site/docs-ref/04-code-examples.md +0 -37
  130. package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
  131. package/docs-site/docs-ref/06-checklists.md +0 -53
  132. package/docs-site/docs.json +0 -256
  133. package/docs-site/example/ai-agent-application.mdx +0 -148
  134. package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
  135. package/docs-site/example/claude-code-codex-skill.mdx +0 -147
  136. package/docs-site/example/showcase.mdx +0 -39
  137. package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
  138. package/docs-site/example/tier1-claude-sdk.mdx +0 -119
  139. package/docs-site/example/tier1-codex-sdk.mdx +0 -111
  140. package/docs-site/example/tier1-langgraph.mdx +0 -119
  141. package/docs-site/example/tier1-pi-sdk.mdx +0 -119
  142. package/docs-site/favicon.svg +0 -5
  143. package/docs-site/github-diff.css +0 -135
  144. package/docs-site/github-diff.js +0 -11
  145. package/docs-site/guides/authoring.mdx +0 -129
  146. package/docs-site/guides/ci-integration.mdx +0 -97
  147. package/docs-site/guides/connect-otel.mdx +0 -209
  148. package/docs-site/guides/connect-your-agent.mdx +0 -221
  149. package/docs-site/guides/dataset-fanout.mdx +0 -98
  150. package/docs-site/guides/experiments.mdx +0 -71
  151. package/docs-site/guides/fixtures.mdx +0 -147
  152. package/docs-site/guides/reporters.mdx +0 -113
  153. package/docs-site/guides/runner.mdx +0 -79
  154. package/docs-site/guides/sandbox-agent.mdx +0 -138
  155. package/docs-site/guides/sandbox-backends.mdx +0 -64
  156. package/docs-site/guides/scoring-guide.mdx +0 -92
  157. package/docs-site/guides/viewing-results.mdx +0 -195
  158. package/docs-site/guides/write-experiment.mdx +0 -81
  159. package/docs-site/guides/write-send.mdx +0 -347
  160. package/docs-site/index.mdx +0 -181
  161. package/docs-site/introduction.mdx +0 -141
  162. package/docs-site/quickstart.mdx +0 -136
  163. package/docs-site/reference/builtin-agents.mdx +0 -161
  164. package/docs-site/reference/capabilities.mdx +0 -76
  165. package/docs-site/reference/cli.mdx +0 -120
  166. package/docs-site/reference/define-agent.mdx +0 -168
  167. package/docs-site/reference/define-config.mdx +0 -41
  168. package/docs-site/reference/define-eval.mdx +0 -160
  169. package/docs-site/reference/events.mdx +0 -131
  170. package/docs-site/reference/expect.mdx +0 -112
  171. package/docs-site/tracker.js +0 -8
  172. package/src/report/react/data.ts +0 -17
  173. package/src/view/aggregate.ts +0 -103
  174. package/src/view/loader.test.ts +0 -61
  175. package/src/view/loader.ts +0 -257
@@ -1,135 +0,0 @@
1
- /* 本文件由 scripts/gen-diff-code.ts 生成(pnpm run gen:diff-code),不要手工编辑 */
2
- /* GitHub PR 式 diff 视图,配合生成的 diff MDX 页面使用 */
3
-
4
- .gd-file {
5
- margin: 1rem 0;
6
- border: 1px solid #d0d7de;
7
- border-radius: 8px;
8
- overflow: hidden;
9
- font-size: 12px;
10
- }
11
- .dark .gd-file { border-color: #30363d; }
12
-
13
- .gd-head {
14
- display: flex;
15
- align-items: center;
16
- gap: 8px;
17
- padding: 8px 12px;
18
- background: #f6f8fa;
19
- border-bottom: 1px solid #d0d7de;
20
- user-select: none;
21
- }
22
- .dark .gd-head { background: #161b22; border-bottom-color: #30363d; }
23
-
24
- .gd-name {
25
- font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
26
- font-weight: 600;
27
- color: #1f2328;
28
- }
29
- .dark .gd-name { color: #e6edf3; }
30
-
31
- .gd-stats { margin-left: auto; font-weight: 600; display: flex; gap: 6px; }
32
- .gd-plus { color: #1a7f37; }
33
- .gd-minus { color: #cf222e; }
34
- .dark .gd-plus { color: #3fb950; }
35
- .dark .gd-minus { color: #f85149; }
36
-
37
- .gd-body { overflow-x: auto; background: #ffffff; }
38
- .dark .gd-body { background: #0d1117; }
39
-
40
- .gd-note { padding: 12px; color: #656d76; background: #ffffff; }
41
- .dark .gd-note { color: #8b949e; background: #0d1117; }
42
-
43
- table.gd-table {
44
- width: 100%;
45
- margin: 0;
46
- border-collapse: collapse;
47
- font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
48
- font-size: 12px;
49
- line-height: 20px;
50
- display: table;
51
- }
52
- .gd-table tr { border: 0; background: transparent; }
53
- .gd-table td { border: 0; padding: 0; background: transparent; }
54
-
55
- td.gd-ln {
56
- width: 1%;
57
- min-width: 40px;
58
- padding: 0 10px;
59
- text-align: right;
60
- color: #656d76;
61
- user-select: none;
62
- vertical-align: top;
63
- }
64
- .dark td.gd-ln { color: #6e7681; }
65
-
66
- td.gd-sign {
67
- width: 1%;
68
- padding: 0 4px;
69
- text-align: center;
70
- user-select: none;
71
- color: #1f2328;
72
- vertical-align: top;
73
- }
74
- .dark td.gd-sign { color: #e6edf3; }
75
-
76
- td.gd-code {
77
- padding: 0 10px 0 4px;
78
- white-space: pre;
79
- color: #1f2328;
80
- tab-size: 2;
81
- }
82
- .dark td.gd-code { color: #e6edf3; }
83
-
84
- /* 背景挂在 td 而不是 tr 上:tr 背景在 Safari / 非整数缩放下行间会出 hairline */
85
- tr.gd-add td { background: #e6ffec; }
86
- tr.gd-add td.gd-ln { background: #ccffd8; }
87
- .dark tr.gd-add td { background: rgba(46, 160, 67, 0.15); }
88
- .dark tr.gd-add td.gd-ln { background: rgba(63, 185, 80, 0.3); color: #c9d1d9; }
89
-
90
- tr.gd-del td { background: #ffebe9; }
91
- tr.gd-del td.gd-ln { background: #ffd7d5; }
92
- .dark tr.gd-del td { background: rgba(248, 81, 73, 0.1); }
93
- .dark tr.gd-del td.gd-ln { background: rgba(248, 81, 73, 0.3); color: #c9d1d9; }
94
-
95
- /* 页面开头的变更统计表(JSX 表格:同页有超长 JSX 行时 GFM 管道表格会压爆 MDX 编译栈) */
96
- table.gd-summary { border-collapse: collapse; margin: 1rem 0; font-size: 14px; display: table; width: auto; }
97
- .gd-summary th, .gd-summary td { border: 1px solid #d0d7de; padding: 6px 12px; text-align: left; background: transparent; }
98
- .gd-summary th { background: #f6f8fa; font-weight: 600; }
99
- .gd-summary th:nth-child(n+2), .gd-summary td:nth-child(n+2) { text-align: right; font-variant-numeric: tabular-nums; }
100
- tr.gd-total td { font-weight: 600; background: #f6f8fa; }
101
- .dark .gd-summary th, .dark .gd-summary td { border-color: #30363d; }
102
- .dark .gd-summary th { background: #161b22; }
103
- .dark tr.gd-total td { background: #161b22; }
104
-
105
- /* 折叠的未变更行 + GitHub 式蓝色展开条(点击逻辑在 github-diff.js) */
106
- tr.gd-fold { display: none; }
107
- tr.gd-expand { cursor: pointer; }
108
- tr.gd-expand td { background: #ddf4ff; }
109
- tr.gd-expand td.gd-ln { color: #0969da; text-align: center; }
110
- tr.gd-expand td.gd-code { color: #57606a; }
111
- tr.gd-expand:hover td { background: #b6e3ff; }
112
- .dark tr.gd-expand td { background: rgba(56, 139, 253, 0.15); }
113
- .dark tr.gd-expand td.gd-ln { color: #58a6ff; }
114
- .dark tr.gd-expand td.gd-code { color: #8b949e; }
115
- .dark tr.gd-expand:hover td { background: rgba(56, 139, 253, 0.3); }
116
-
117
- /* Shiki 调色板(github-light / github-dark) */
118
- .gdt0 { color: #24292E; }
119
- .dark .gdt0 { color: #E1E4E8; }
120
- .gdt1 { color: #005CC5; }
121
- .dark .gdt1 { color: #79B8FF; }
122
- .gdt2 { color: #032F62; }
123
- .dark .gdt2 { color: #9ECBFF; }
124
- .gdt3 { color: #22863A; }
125
- .dark .gdt3 { color: #85E89D; }
126
- .gdt4 { color: #D73A49; }
127
- .dark .gdt4 { color: #F97583; }
128
- .gdt5 { color: #6F42C1; }
129
- .dark .gdt5 { color: #B392F0; }
130
- .gdt6 { color: #6A737D; }
131
- .dark .gdt6 { color: #6A737D; }
132
- .gdt7 { color: #E36209; }
133
- .dark .gdt7 { color: #FFAB70; }
134
- .gdt8 { color: #032F62; }
135
- .dark .gdt8 { color: #DBEDFF; }
@@ -1,11 +0,0 @@
1
- // 本文件由 scripts/gen-diff-code.ts 生成(pnpm run gen:diff-code),不要手工编辑
2
- // GitHub 式 diff 展开条:点击后显示折叠的未变更行(配合 github-diff.css 的 .gd-fold)
3
- document.addEventListener("click", (e) => {
4
- const tr = e.target && e.target.closest ? e.target.closest("tr.gd-expand") : null;
5
- if (!tr) return;
6
- const id = tr.getAttribute("data-fold");
7
- const tbody = tr.closest("tbody");
8
- if (!id || !tbody) return;
9
- tbody.querySelectorAll("tr." + id).forEach((row) => row.classList.remove("gd-fold"));
10
- tr.remove();
11
- });
@@ -1,129 +0,0 @@
1
- ---
2
- title: "Authoring evals: single-turn, multi-turn, and dataset patterns"
3
- sidebarTitle: "Authoring Evals"
4
- description: "Learn how to write NiceEval evals with defineEval, including single-turn assertions, multi-turn conversations, dataset fan-out, and sandbox workspaces."
5
- ---
6
-
7
- Every NiceEval eval is a TypeScript file that default-exports `defineEval(...)`. The core rules are simple: **path is identity**, **one file is one eval**, and **write linearly with assertions next to the interaction they check**.
8
-
9
- ## The `defineEval` shape
10
-
11
- ```ts
12
- import { defineEval } from "niceeval";
13
-
14
- export default defineEval({
15
- description?: string;
16
- tags?: string[];
17
- judge?: JudgeConfig;
18
- reporters?: Reporter[];
19
- timeoutMs?: number;
20
- metadata?: Record<string, unknown>;
21
- async test(t) { /* interactions + assertions */ },
22
- });
23
- ```
24
-
25
- <Warning>
26
- Do not set `id` or `name`. `evals/weather/brooklyn.eval.ts` automatically becomes the ID `weather/brooklyn`.
27
- </Warning>
28
-
29
- ## Single-turn evals
30
-
31
- ```ts
32
- import { defineEval } from "niceeval";
33
- import { includes } from "niceeval/expect";
34
-
35
- export default defineEval({
36
- description: "Brooklyn weather query",
37
- async test(t) {
38
- await t.send("What's the weather like in Brooklyn today?");
39
- t.succeeded();
40
- t.calledTool("get_weather", { input: { city: "Brooklyn" }, count: 1 });
41
- t.check(t.reply, includes("sunny"));
42
- },
43
- });
44
- ```
45
-
46
- `t.send()` drives one interaction. `t.succeeded()` and `t.calledTool()` are scoped assertions. `t.check()` records an immediate value assertion.
47
-
48
- ### The `Turn` object
49
-
50
- | Property | Meaning |
51
- |---|---|
52
- | `turn.events` | Standard event stream, the main fact source |
53
- | `turn.data` | Structured output |
54
- | `turn.status` | `"completed"`, `"failed"`, or `"waiting"` |
55
- | `turn.usage` | usage such as tokens and cost |
56
- | `turn.message` | assistant text reply |
57
- | `turn.toolCalls` | tool calls in this turn |
58
-
59
- ## Multi-turn evals
60
-
61
- ```ts
62
- export default defineEval({
63
- description: "Draft an email, then send it on confirmation",
64
- async test(t) {
65
- const draft = await t.send("Draft a follow-up email.");
66
- draft.succeeded();
67
- t.check(draft.message, includes("Best"));
68
-
69
- await t.send("Looks good, send it.");
70
- t.calledTool("send_email");
71
- },
72
- });
73
- ```
74
-
75
- Use `t.newSession()` when you need multiple independent conversations in parallel.
76
-
77
- ## Dataset fan-out
78
-
79
- One file can export an eval array:
80
-
81
- ```ts
82
- export default rows.map((row) =>
83
- defineEval({
84
- description: row.task,
85
- async test(t) {
86
- await t.send(row.prompt);
87
- t.check(t.reply, equals(row.expected));
88
- },
89
- }),
90
- );
91
- ```
92
-
93
- That generates IDs such as `sql/0000`, `sql/0001`, and so on. See [Dataset fan-out](/guides/dataset-fanout).
94
-
95
- ## Sandbox workspace
96
-
97
- Coding-agent evals are still normal `.eval.ts` files. They just prepare a sandbox workspace, send a task, and inspect files or tests:
98
-
99
- ```ts
100
- import { defineEval } from "niceeval";
101
-
102
- export default defineEval({
103
- description: "Create a Button component",
104
- async test(t) {
105
- await t.sandbox.uploadDirectory("../workspaces/ts-starter");
106
- await t.send("Create src/components/Button.tsx with label and onClick props.").then((turn) => turn.succeeded());
107
- t.sandbox.fileChanged("src/components/Button.tsx");
108
- },
109
- });
110
- ```
111
-
112
- See [Fixtures](/guides/fixtures) for the full pattern.
113
-
114
- ## Naming conventions
115
-
116
- <CardGroup cols={2}>
117
- <Card title="Filename" icon="file">
118
- Only `.eval.ts` files are discovered by the runner.
119
- </Card>
120
- <Card title="Directory grouping" icon="folder">
121
- `evals/billing/refund.eval.ts` becomes the ID `billing/refund`.
122
- </Card>
123
- <Card title="Dataset" icon="database">
124
- Good for many cases with the same structure and different inputs.
125
- </Card>
126
- <Card title="Sandbox workspace" icon="box">
127
- Good for coding agents that need a real file system, commands, and diffs.
128
- </Card>
129
- </CardGroup>
@@ -1,97 +0,0 @@
1
- ---
2
- title: "Run NiceEval in GitHub Actions and CI"
3
- sidebarTitle: "CI Integration"
4
- description: "Add NiceEval to GitHub Actions or any CI. Failed evals exit non-zero, JUnit XML can be emitted, and caching can speed up repeated runs."
5
- ---
6
-
7
- Evals should live in CI the same way tests do. They catch agent regressions on pull requests and track model, cost, and latency changes in nightly jobs.
8
-
9
- ## Exit codes
10
-
11
- By default, NiceEval exits non-zero when any gate fails. In CI you usually want `--strict` so failure is explicit:
12
-
13
- ```bash
14
- npx niceeval exp ci --strict
15
- ```
16
-
17
- ## GitHub Actions example
18
-
19
- ```yaml
20
- name: evals
21
- on:
22
- pull_request:
23
- workflow_dispatch:
24
-
25
- jobs:
26
- evals:
27
- runs-on: ubuntu-latest
28
- steps:
29
- - uses: actions/checkout@v4
30
- - uses: actions/setup-node@v4
31
- with:
32
- node-version: 22
33
- cache: npm
34
- - run: npm ci
35
- - run: npx niceeval exp ci --strict
36
- env:
37
- OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
38
- ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
39
- ```
40
-
41
- ## Configure secrets
42
-
43
- <Steps>
44
- <Step title="Add repository secrets">
45
- Put provider tokens into GitHub Actions secrets.
46
- </Step>
47
- <Step title="Pass them as env in the workflow">
48
- Expose only the variables the eval step needs.
49
- </Step>
50
- <Step title="Verify they exist">
51
- Use a minimal eval, or run `list` / a dry run first to confirm configuration.
52
- </Step>
53
- </Steps>
54
-
55
- ## JUnit reporter
56
-
57
- ```ts
58
- import { defineConfig } from "niceeval";
59
- import { Console, JUnit } from "niceeval/reporters";
60
-
61
- export default defineConfig({
62
- reporters: [Console(), JUnit(".niceeval/junit.xml")],
63
- });
64
- ```
65
-
66
- CI can upload the JUnit XML so failures appear in the test-report UI.
67
-
68
- ## Discovery only
69
-
70
- ```bash
71
- npx niceeval list
72
- ```
73
-
74
- This is useful when you only want to confirm that eval files load and configuration is sane.
75
-
76
- ## Cache `.niceeval/`
77
-
78
- You can cache `.niceeval/` in CI, but make sure the fingerprint covers every input that affects the result. For nightly benchmarks, keeping the full artifacts is usually more valuable.
79
-
80
- ## Control concurrency
81
-
82
- ```bash
83
- npx niceeval exp ci --max-concurrency 2
84
- ```
85
-
86
- Sandbox evals should stay conservative on a standard GitHub-hosted runner. Remote HTTP evals can usually run with higher concurrency if the service allows it.
87
-
88
- ## Recommended patterns
89
-
90
- <CardGroup cols={2}>
91
- <Card title="PR runs" icon="code-pull-request">
92
- Run only critical evals and high-risk paths so feedback stays fast.
93
- </Card>
94
- <Card title="Nightly full-matrix runs" icon="moon">
95
- Run the full experiment matrix and record pass rate, cost, and latency trends.
96
- </Card>
97
- </CardGroup>
@@ -1,209 +0,0 @@
1
- ---
2
- title: "OTel Integration"
3
- sidebarTitle: "OTel Integration"
4
- description: "Send a copy of the OTel spans your app already emits to NiceEval and get a per-turn call waterfall in niceeval view. Assertions do not come from here — they are complete the moment send is wired up."
5
- ---
6
-
7
- First, what this integration does **not** change: assertions. Every verdict — `t.calledTool`, `t.maxTokens`, timing — is based entirely on the `Turn` (`events` + `usage`) your adapter returns from `send`. The moment `send` is wired up, the full assertion set is available, and none of it involves OTel (see [Connect Your Agent](/guides/connect-your-agent)).
8
-
9
- What OTel integration buys you is something else: **the call waterfall in `niceeval view`**. Every model call and tool execution inside your app, each with its duration and tokens, laid out as a per-turn timeline — why a failing eval failed, or which step made a slow turn slow, is often obvious at a glance in the waterfall.
10
-
11
- If your app already emits OTel traces — AI SDK telemetry, LangGraph's LangSmith export, OpenLLMetry / OpenInference auto-instrumentation, or your own spans following the GenAI semantic conventions — you are already producing the waterfall data: just have the app send a copy of its spans to NiceEval. Application code stays untouched, and the integration remains non-invasive (see [Tier](/concepts/tier)).
12
-
13
- ## How it works (one paragraph)
14
-
15
- At run time NiceEval starts a local OTLP receiver, and your app sends spans to it. Spans received during each `send` are attributed to that turn, normalized into GenAI semantics, and written into `EvalResult.trace`; after the run, `npx niceeval view` is the waterfall. **Spans only feed the waterfall — never the event stream, never assertions.** Missing instrumentation, late spans, or dropped batches only degrade waterfall completeness; they never affect any verdict.
16
-
17
- ## Wiring it up
18
-
19
- **1. Adapter side** — write `send` as usual (the event mapping is still your mapping), plus one extra line: forward this turn's `traceparent` with the request:
20
-
21
- ```ts
22
- // agents/support-bot.ts
23
- import { defineAgent } from "niceeval/adapter";
24
-
25
- export default defineAgent({
26
- name: "support-bot",
27
- async send(input, ctx) {
28
- const r = await fetch("http://localhost:5188/chat", {
29
- method: "POST",
30
- // Forward traceparent with the request: this turn's spans attach to NiceEval's trace,
31
- // which is what makes attribution precise under concurrency
32
- headers: { "content-type": "application/json", ...ctx.telemetry?.headers },
33
- body: JSON.stringify({ message: input.text }),
34
- signal: ctx.signal,
35
- });
36
- const body = await r.json();
37
- return {
38
- status: r.ok ? "completed" : "failed",
39
- events: mapToEvents(body), // Assertions are based on this, same as without OTel
40
- };
41
- },
42
- });
43
- ```
44
-
45
- Built-ins do not need this step: `uiMessageStreamAgent` always merges `ctx.telemetry.headers` into the request headers automatically.
46
-
47
- **2. How the endpoint reaches your app** — NiceEval's receiver endpoint is **startup-time configuration, never passed through send** (standard OTel SDKs cannot switch endpoints at run time either: `OTEL_*` environment variables are read once at process startup). Pick by deployment shape:
48
-
49
- - **Your own long-running service (most common)**: use **fixed-port mode** — pin the receiver port in `niceeval.config.ts`. Writing this config is what turns OTel integration on:
50
-
51
- ```ts
52
- // niceeval.config.ts
53
- export default defineConfig({
54
- telemetry: { port: 4318 }, // Receiver always listens on http://localhost:4318/v1/traces
55
- });
56
- ```
57
-
58
- Configure `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces` once when the service starts; it stays valid no matter how many eval runs follow. The trade-off: sharing a port means only one niceeval process can run on the machine at a time; OTel Collector fan-out setups point at this fixed endpoint the same way. If `port` is already taken by another process, NiceEval fails immediately with an error telling you to pick a free port — it never fails silently.
59
-
60
- The receiver hostname reported to your app defaults to `127.0.0.1`. When Docker-based sandbox tracing needs `host.docker.internal`, or a tunneled remote setup needs another hostname, override it with `host`: `telemetry: { host: "host.docker.internal", port: 4318 }`. These two fields are the only way to configure the OTLP receiver in NiceEval; it does not read environment variables.
61
-
62
- - **Child processes / processes NiceEval spawns** (CLI-style agents): nothing to do. `ctx.telemetry.env` (standard `OTEL_*` environment variables, ready to spread) is injected into the process environment; each run is a fresh process and reads the fresh endpoint.
63
-
64
- **3. App side** — a few lines of configuration, depending on your instrumentation ecosystem:
65
-
66
- <Tabs>
67
- <Tab title="AI SDK">
68
- The official OTel integration (`@ai-sdk/otel`, which emits standard GenAI semantics) is recommended; the older `experimental_telemetry` (`ai.*`) renders too:
69
-
70
- ```ts
71
- import { generateText } from "ai";
72
-
73
- const result = await generateText({
74
- model, tools, messages,
75
- experimental_telemetry: { isEnabled: true },
76
- });
77
- ```
78
-
79
- The exporter is the standard OTel Node SDK, with the endpoint pointed at NiceEval (the injected env or the fixed port). Runnable examples: app-side instrumentation in [`examples/zh/origin/ai-sdk-v7`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/origin/ai-sdk-v7) (`src/backend/otel.ts`, official `@ai-sdk/otel`); the full eval project after integration in [`examples/zh/tier1/ai-sdk-v7`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/tier1/ai-sdk-v7).
80
- </Tab>
81
- <Tab title="LangGraph / LangChain">
82
- The zero-dependency route: three environment variables:
83
-
84
- ```bash
85
- LANGSMITH_TRACING=true \
86
- LANGSMITH_OTEL_ENABLED=true \
87
- LANGSMITH_OTEL_ONLY=true \
88
- OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces \
89
- node server.js # Endpoint value per "How the endpoint reaches your app": injected env or fixed port
90
- ```
91
-
92
- `LANGSMITH_OTEL_ONLY` means OTLP only, no LangSmith cloud (drop it to dual-send). This is truly zero-code with the **Python** `langsmith` SDK (it hooks OTel automatically at import time); the **JS** version (`langsmith@0.7.x`) additionally needs one explicit call to `initializeOTEL()` (from `langsmith/experimental/otel/setup`), otherwise it only logs a warning and produces no spans — the three environment variables themselves stay the same. Runnable example (Python backend, zero wiring code): [`examples/zh/origin/langgraph`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/origin/langgraph); the full eval project after integration in [`examples/zh/tier1/langgraph`](https://github.com/CorrectRoadH/niceeval/tree/main/examples/zh/tier1/langgraph).
93
- </Tab>
94
- <Tab title="OpenLLMetry">
95
- ```ts
96
- import * as traceloop from "@traceloop/node-server-sdk";
97
-
98
- traceloop.initialize({ disableBatch: true });
99
- // Endpoint via the standard OTEL_EXPORTER_OTLP_ENDPOINT environment variable
100
- ```
101
- </Tab>
102
- <Tab title="OpenInference">
103
- ```python
104
- from openinference.instrumentation.langchain import LangChainInstrumentor
105
- from phoenix.otel import register
106
-
107
- register() # Or the standard OTel SDK; endpoint via OTEL_EXPORTER_OTLP_ENDPOINT
108
- LangChainInstrumentor().instrument()
109
- ```
110
- </Tab>
111
- <Tab title="Hand-rolled gen_ai">
112
- Instrument per the [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/): name model-call spans `chat {model}` and tool spans `execute_tool {tool}`, with the `gen_ai.operation.name` and `gen_ai.tool.name` / `gen_ai.tool.call.id` attributes. This is the target semantics NiceEval normalizes into, and it produces the most complete waterfall.
113
- </Tab>
114
- </Tabs>
115
-
116
- ## How spans are attributed to turns
117
-
118
- When evals run in parallel, one receiver receives spans from multiple sessions at once. NiceEval attributes them to their turns via two paths:
119
-
120
- - **traceparent (recommended, concurrency-safe)**: when `send` makes its request, spread `ctx.telemetry.headers` (W3C trace context, a fresh `traceparent` per turn) into the request headers. If the app's instrumentation supports context propagation (standard OTel HTTP server instrumentation does), this turn's spans automatically attach under the trace NiceEval provided, and attribution is exact by traceId.
121
- - **Time window (fallback)**: when the app does not propagate trace context, spans are attributed by the time window around `send`. The window is only reliable when turns run serially, so in this case NiceEval executes this agent's turns serially and says so in the logs — it never silently mixes streams. Once traceparent is confirmed working, concurrency resumes automatically.
122
-
123
- Export promptly on the app side: what the waterfall cares about is "this turn's spans arrive in time". Use `SimpleSpanProcessor` (or flush every turn) — `BatchSpanProcessor`'s buffering makes spans arrive late across turns; when the waterfall occasionally has a missing tail, that is usually why.
124
-
125
- ## Already have your own OTel backend? Dual-export, no switch
126
-
127
- Your app is most likely already sending traces to its own observability backend (Langfuse / SigNoz / a production collector). Connecting NiceEval **requires neither switching backends nor a second layer of instrumentation**: a TracerProvider supports multiple SpanProcessors — the same spans, two exits:
128
-
129
- ```ts
130
- // instrumentation.ts — initialize once at app startup: one set of instrumentation, two exits
131
- import { NodeTracerProvider, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-node";
132
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
133
-
134
- export const provider = new NodeTracerProvider({
135
- spanProcessors: [
136
- // Exit 1: your own backend, always on
137
- new SimpleSpanProcessor(new OTLPTraceExporter({ url: process.env.MY_COLLECTOR_URL })),
138
- // Exit 2: NiceEval. Without this variable in production, this exit simply
139
- // does not exist — same code in production and in evals
140
- ...(process.env.OTEL_EXPORTER_OTLP_ENDPOINT
141
- ? [new SimpleSpanProcessor(new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT }))]
142
- : []),
143
- ],
144
- });
145
- provider.register();
146
- ```
147
-
148
- If touching app code is inconvenient, fan out through an OTel Collector — the app sends only to the collector, and the collector configures two exporters (your backend + NiceEval's fixed endpoint). The cost is one more component to operate.
149
-
150
- ## How accurate is the waterfall: what you map is what lights up
151
-
152
- Before drawing the waterfall, NiceEval normalizes every span into GenAI semantics. The verdict looks at exactly two things: `gen_ai.operation.name` (the standard operation name) and the normalized `kind` (semantic role). The mapping table — what you provide on a span, and what lights up in the waterfall:
153
-
154
- | What you provide on the span | What lights up in the waterfall |
155
- |---|---|
156
- | `gen_ai.operation.name: "chat"` (or `text_completion` / `embeddings`) | Colored and grouped as a **model call** |
157
- | `gen_ai.operation.name: "execute_tool"`, or a `tool_name` attribute | Colored and grouped as a **tool execution** |
158
- | `gen_ai.operation.name: "invoke_agent"` / `"create_agent"` | Colored and grouped as a **sub-agent** |
159
- | A `call_id` attribute equal to a `callId` in your send events | That tool span gets the real input/output backfilled (`io.input` / `io.output`, joined by callId from the events `send` returned — many instrumentations do not capture content by default, and this path is unaffected) |
160
- | Span status = error | Marked red |
161
- | Nothing recognizable (normalized to `other`) | The timeline is still there; in large traces (>150 spans) it is folded away as internal noise |
162
- | Any other attributes | Preserved as-is, drillable in the view |
163
-
164
- If your app instruments directly per the GenAI semconv (the "Hand-rolled gen_ai" tab above), all of this holds automatically; the common shapes of mainstream formats (AI SDK, LangSmith, OpenLLMetry / OpenInference) are also within the generic fallback's recognition range.
165
-
166
- ### Private instrumentation: write your own mapping
167
-
168
- When your instrumentation has an app-private shape the generic fallback cannot recognize, there are two routes:
169
-
170
- - **Fix the instrumentation (recommended)**: add a `gen_ai.operation.name` attribute to the spans on the app side — a one-line change, and your own observability backend benefits equally.
171
- - **Write a `spanMapper`**: when touching the app is inconvenient, declare a pure function on the agent that translates private spans into the semantics of the table above before rendering. Both `tagSpan` (writes the verdict back onto the span; existing attributes are only added to, never changed) and `heuristicTag` (the generic fallback verdict) are exported from `niceeval/adapter`:
172
-
173
- ```ts
174
- import { defineAgent, tagSpan, heuristicTag } from "niceeval/adapter";
175
- import type { TraceSpan } from "niceeval";
176
-
177
- function mapMySpans(spans: TraceSpan[]): TraceSpan[] {
178
- return spans.map((span) => {
179
- // Private naming → standard semantics: op goes into gen_ai.operation.name, kind sets the coloring
180
- if (span.name === "my.llm.request") return tagSpan(span, { op: "chat", kind: "model" });
181
- if (span.name === "my.tool.exec") {
182
- // Copy the private call id into call_id while you're at it, so tool I/O joins in from the send events
183
- const attributes = { ...span.attributes, call_id: String(span.attributes?.["my.callId"] ?? "") };
184
- return tagSpan({ ...span, attributes }, { op: "execute_tool", kind: "tool" });
185
- }
186
- return tagSpan(span, heuristicTag(span)); // leave the rest to the generic fallback
187
- });
188
- }
189
-
190
- export default defineAgent({
191
- name: "my-bot",
192
- spanMapper: mapMySpans,
193
- async send(input, ctx) { /* same wiring as above */ },
194
- });
195
- ```
196
-
197
- The built-in `mapCodexSpans` (also exported from `niceeval/adapter`) is a ready-made spanMapper implementation to use as a reference. A spanMapper is pure observability code: get it wrong and at worst the waterfall coloring is off — assertions are never affected.
198
-
199
- ## Boundaries
200
-
201
- - **Everything assertion-related lives in send**. To assert on tool calls, map them into `events` (an official converter or a hand-written mapping, see [Write send](/guides/write-send)); to assert on usage, include it in the `send` return. There is no path where "the span has it, events do not, so the assertion reads the span".
202
- - **Multi-turn sessions and HITL are not span concerns**. Spans have no "waiting for human input" semantics, and session continuation is application-protocol work — both keep happening in `send` as usual (session continuation in [Write send](/guides/write-send), the HITL concept in [HITL](/concepts/hitl)).
203
- - **You get a warning when no spans arrive**. A turn with zero spans usually means the endpoint is not hooked up (env not injected, service not restarted); NiceEval says so in the logs. The waterfall is empty; assertions judge as usual.
204
-
205
- ## Related reading
206
-
207
- - [Connect Your Agent](/guides/connect-your-agent) — where assertions come from: the event mapping in `send`.
208
- - [Write send](/guides/write-send) — the full tutorial for hand-writing an adapter; step six is this page's adapter-side wiring.
209
- - [Events reference](/reference/events) — what the events assertions consume look like.