niceeval 0.4.6 → 0.5.1

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 +64 -21
  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 +11 -3
  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 +120 -18
  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
@@ -1,118 +0,0 @@
1
- ---
2
- title: "How NiceEval drives agents: send, sessions, and HITL"
3
- sidebarTitle: "Drive"
4
- description: "t.send() and the Turn it returns, t.sendFile(), multi-turn conversations, t.newSession(), and HITL through t.respond() / t.respondAll()."
5
- ---
6
-
7
- Before assertions or judge scoring, you need to make the agent do something. **Drive** is the part of `test(t)` that sends input and gets results back: `t.send()`, `t.sendFile()`, `t.newSession()`, and HITL through `t.respond()` / `t.respondAll()`. Every drive API produces a **Turn**. All assertions and judge checks in NiceEval read from Turn data. See [Assert](/concepts/assert) and [Judge](/concepts/judge) for the scoring side.
8
-
9
- ## `t.send()` and the `Turn` it returns
10
-
11
- `t.send(input)` is the core verb. Under the hood it calls the Adapter's `send(input, ctx)` and normalizes the return value into a `Turn`:
12
-
13
- ```ts
14
- const turn = await t.send("What's the weather like in Brooklyn today?");
15
-
16
- turn.message; // last assistant message
17
- turn.data; // structured output, only when this send declared output
18
- turn.status; // "completed" | "failed" | "waiting"
19
- turn.events; // StreamEvent[] for this turn
20
- turn.usage; // { inputTokens, outputTokens, ... } when the adapter reports it
21
- turn.succeeded(); // scored assertion: this turn didn't fail and isn't parked on HITL
22
- ```
23
-
24
- `t.reply` is shorthand for the last assistant message in the main session, equivalent to `turn.message` from the most recent `t.send()`. `t.events` is the full accumulated event stream for the main session.
25
-
26
- When you need structured output, declare `output` on the send call. The declaration is both a requirement for the app and the type source for `turn.data`:
27
-
28
- ```ts
29
- const turn = await t.send("What is the total on this invoice?", {
30
- output: z.object({ amount: z.number(), currency: z.string() }),
31
- });
32
- t.check(turn.data.amount, equals(3200));
33
- ```
34
-
35
- The runner validates `data` against that schema immediately after the adapter returns. If the shape does not match, the turn fails and the diff is reported. If you did not declare `output`, there is no `turn.data`.
36
-
37
- <Tip>
38
- `turn.succeeded()` scopes to this turn only: it checks this turn's `status` and whether it's parked on an unanswered HITL request. Call it right after a `t.send()` that later turns depend on, so a failure surfaces here instead of a pile of confusing follow-on failures.
39
- </Tip>
40
-
41
- ## One turn with a file: `t.sendFile()`
42
-
43
- `t.sendFile(path, text?)` reads a local file relative to the eval directory, infers the MIME type from the extension, and attaches it to the turn input as a data URL:
44
-
45
- ```ts
46
- const turn = await t.sendFile("fixtures/invoice.png", "What is the total on this invoice?");
47
- t.check(turn.message, includes("$"));
48
- ```
49
-
50
- ## Multi-turn conversations
51
-
52
- Every `await t.send(...)` is a new turn on the same conversation. Keep each return value in a local variable when you want to assert on that turn specifically:
53
-
54
- ```ts
55
- const draft = await t.send("Draft a follow-up email.");
56
- draft.succeeded();
57
- t.check(draft.message, includes("Best"));
58
- draft.judge.autoevals.closedQA("Is the tone professional?").atLeast(0.6);
59
-
60
- await t.send("Looks good, send it.");
61
- t.calledTool("send_email");
62
- ```
63
-
64
- Whether multi-turn calls actually continue the same conversation depends on whether the adapter's `send` uses the continuation helpers from `ctx.session` (`history()` or `id` + `capture()`). If it does not, each turn becomes a fresh conversation. See [Adapter](/concepts/adapter) and [How to write send](/guides/write-send).
65
-
66
- ## Independent sessions: `t.newSession()`
67
-
68
- `t.newSession()` opens a second conversation line that runs in parallel with the main one and should not share state. The returned handle has the same drive APIs (`send`, `sendFile`, `respond`, `respondAll`) and the same scoped assertions, but only sees its own events:
69
-
70
- ```ts
71
- await t.send("My name is Alice.");
72
- await t.send("What is my name?");
73
- t.check(t.reply, includes("Alice"));
74
-
75
- const fresh = t.newSession();
76
- await fresh.send("What is my name?");
77
- t.check(fresh.reply, satisfies((r) => !r.includes("Alice"), "no memory leak"));
78
- ```
79
-
80
- <Warning>
81
- Do not assume `t.newSession()` guarantees isolation by itself. The runner only guarantees a fresh session handle. The adapter still has to open a fresh underlying conversation. If the adapter always resumes the same backend context, `t.newSession()` will quietly share state. Verify isolation with an eval like the one above before trusting it.
82
- </Warning>
83
-
84
- ## Human-in-the-loop (HITL)
85
-
86
- Some agents stop in the middle of a turn and wait for approval or missing information instead of finishing immediately. In that case the turn ends with `status: "waiting"` and one or more `input.requested` events that describe what it is waiting for. The full model is in [HITL](/concepts/hitl); here the focus is how evals drive it.
87
-
88
- ```ts
89
- const draft = await t.send("Draft a follow-up email, but don't send it until I confirm.");
90
- draft.parked();
91
-
92
- const request = t.requireInputRequest({
93
- prompt: /send it/,
94
- optionIds: ["approve", "reject"],
95
- });
96
-
97
- await t.respond({ request, optionId: "approve" });
98
- t.calledTool("send_email");
99
- ```
100
-
101
- `t.requireInputRequest(filter)` turns a pending HITL request into a concrete value you can inspect and answer. It throws when 0 or more than 1 requests match, so fill as many filter fields as you can (`id`, `prompt`, `display`, `action`, `optionIds`, `input`) to disambiguate. `t.respond(...)` answers it and emits the next turn. String arguments answer requests in order; object arguments such as `{ request, optionId }` target a specific request directly. Under the hood this is still just another `send`: the answer text goes into `input.text`, and structured answers go into `input.responses`.
102
-
103
- When the current turn has multiple pending requests of the same kind and they should all get the same answer, use `t.respondAll(optionId)`:
104
-
105
- ```ts
106
- await t.send("Ask for approval on each of these file changes.");
107
- t.requireInputRequest({ display: /approval/ });
108
-
109
- await t.respondAll("approve");
110
- t.succeeded();
111
- ```
112
-
113
- ## Related reading
114
-
115
- - [HITL](/concepts/hitl) - The full concept for paused turns, request/response handshakes, and adapter obligations.
116
- - [Assert](/concepts/assert) - Assertions that read from `Turn.events` and `Turn.data`.
117
- - [Judge](/concepts/judge) - What `t.judge`, `session.judge`, and `turn.judge` score by default.
118
- - [Adapter](/concepts/adapter) - Where session continuation, HITL, and tool assertions come from.
@@ -1,108 +0,0 @@
1
- ---
2
- title: "Evals in NiceEval: lifecycle, outcome, and files"
3
- sidebarTitle: "Evals"
4
- description: "An eval is a test case: description, optional agent reference, and test function. Learn how evals are discovered, scheduled, scored, and reported."
5
- ---
6
-
7
- An eval is a runnable test case. It usually lives in a `*.eval.ts` file and is declared with `defineEval`.
8
-
9
- ## What an eval contains
10
-
11
- ```ts
12
- import { defineEval } from "niceeval";
13
- import { includes } from "niceeval/expect";
14
-
15
- export default defineEval({
16
- description: "Brooklyn weather query",
17
- async test(t) {
18
- await t.send("What's the weather like in Brooklyn today?");
19
- t.succeeded();
20
- t.calledTool("get_weather", { input: { city: "Brooklyn" }, count: 1 });
21
- t.check(t.reply, includes("sunny"));
22
- },
23
- });
24
- ```
25
-
26
- | Field | Meaning |
27
- |---|---|
28
- | `description` | Human-readable description shown in reports |
29
- | `agent` | Which Adapter to use, usually supplied by the experiment |
30
- | `test(t)` | Interaction and assertion logic |
31
-
32
- <Note>
33
- Do not hand-write `id` or `name`. NiceEval derives the eval ID from the file path.
34
- </Note>
35
-
36
- ## Path is identity
37
-
38
- `evals/weather/brooklyn.eval.ts` has ID `weather/brooklyn`. Positional arguments after the experiment name filter by ID prefix:
39
-
40
- ```bash
41
- npx niceeval exp local weather
42
- npx niceeval exp local weather/brooklyn
43
- ```
44
-
45
- ## Lifecycle
46
-
47
- <Steps>
48
- <Step title="Discovery">Load `*.eval.ts` files and fixture directories.</Step>
49
- <Step title="Scheduling">Build the execution plan from concurrency, cache, runs, attempts, and early exit.</Step>
50
- <Step title="agent.send">`t.send()` calls the selected adapter and receives a standard `Turn`.</Step>
51
- <Step title="Scoring">NiceEval collects value assertions, scoped assertions, judge scores, and test results.</Step>
52
- <Step title="Outcome">All assertion results fold into one final outcome.</Step>
53
- <Step title="Report">Reporters write console output and `.niceeval/` artifacts.</Step>
54
- </Steps>
55
-
56
- ## Outcome types
57
-
58
- <CardGroup cols={2}>
59
- <Card title="passed" icon="circle-check" color="#22c55e">
60
- All gate assertions passed and no hard failure occurred.
61
- </Card>
62
- <Card title="failed" icon="circle-xmark" color="#ef4444">
63
- A gate assertion failed or the run itself failed.
64
- </Card>
65
- <Card title="passed" icon="chart-bar" color="#f59e0b">
66
- No gate failed, but soft scores are preserved as scores.
67
- </Card>
68
- <Card title="skipped" icon="forward" color="#6b7280">
69
- The eval was skipped, usually through `t.skip(reason)`.
70
- </Card>
71
- </CardGroup>
72
-
73
- ## Gate and soft
74
-
75
- `gate` is a hard threshold. Failure makes the eval fail. `soft` contributes a score without necessarily failing the eval. See [Assert](/concepts/assert).
76
-
77
- ## File convention
78
-
79
- Only files ending in `.eval.ts` are discovered. Use directories for grouping:
80
-
81
- ```text
82
- evals/
83
- └─ billing/
84
- └─ refund.eval.ts # id: billing/refund
85
- ```
86
-
87
- ## Dataset fan-out
88
-
89
- A file can also default-export an array of `defineEval(...)` calls to generate multiple cases from the same logic:
90
-
91
- ```ts
92
- export default rows.map((row) =>
93
- defineEval({
94
- description: row.task,
95
- async test(t) {
96
- await t.send(row.prompt);
97
- t.check(t.reply, equals(row.expected));
98
- },
99
- }),
100
- );
101
- ```
102
-
103
- This generates IDs such as `sql/0000` and `sql/0001`. See [Dataset Fan-out](/guides/dataset-fanout).
104
-
105
- ## Related reading
106
-
107
- - [Experiment](/concepts/experiment) - The other half: who to evaluate, how to run it, and why evals are separate from experiments.
108
- - [Assert](/concepts/assert) - The full outcome rules for gate and soft assertions.
@@ -1,37 +0,0 @@
1
- ---
2
- title: "Experiment: who to evaluate and how to run"
3
- sidebarTitle: "Experiment"
4
- description: "An experiment is a commit-ready run configuration: which agent to evaluate, which model to use, which flags to enable, and how many times to run the same eval suite."
5
- ---
6
-
7
- An **experiment** chooses which agent to evaluate, which model to use, which flags to enable, and how many times to run. Use it for A/B tests, model comparisons, and feature comparisons.
8
-
9
- ## What an experiment contains
10
-
11
- ```ts
12
- import { defineExperiment } from "niceeval";
13
- import { webAgent } from "../agents/web-agent.ts";
14
-
15
- export default defineExperiment({
16
- description: "Concise prompt variant",
17
- agent: webAgent({ baseUrl: "https://staging.example.com" }), // configured agent instance
18
- model: "gpt-5.4", // forwarded to the adapter through ctx.model
19
- flags: { promptVariant: "concise" }, // forwarded through ctx.flags; visible in evals as t.flags
20
- runs: 3, // how many times to run each eval
21
- });
22
- ```
23
-
24
- - `agent`: who to evaluate. Put a configured instance here. The subject under test URL and credentials belong in the adapter factory, not as ad hoc experiment fields.
25
- - `model` / `flags`: transparent pass-through values. NiceEval does not interpret them. They are forwarded through `ctx`, and the adapter decides how to use them. This is the path that [Tier](/concepts/tier) uses for model comparisons (Tier 1) and feature A/B tests (Tier 3).
26
- - `runs`, `budget`, concurrency, `sandbox`, and related settings: how to run, and how much to run. See [Write Experiments](/guides/write-experiment) for the full field list.
27
-
28
- ## Matrix comparisons
29
-
30
- Write one experiment file per variant you want to compare. Two models means two files that differ by one `model` line. Prompt A/B means two files that differ by one flag. The same eval suite runs under each experiment, and `niceeval view` shows pass rate, cost, and latency side by side. See [Experiments](/guides/experiments) for which comparisons fit this pattern and how to read the output.
31
-
32
- ## Related reading
33
-
34
- - [Write Experiments](/guides/write-experiment) - The full `defineExperiment` field set: runs, budget, concurrency, and sandbox.
35
- - [Experiments](/guides/experiments) - How to organize and read cross-agent, cross-model, and cross-flag comparisons.
36
- - [Evals](/concepts/evals) - The other half: what an eval is, plus its lifecycle and outcomes.
37
- - [Tier](/concepts/tier) - Where `model` and `flags` become effective.
@@ -1,83 +0,0 @@
1
- ---
2
- title: "Human-in-the-loop (HITL): pause, answer, continue"
3
- sidebarTitle: "HITL"
4
- description: "What human-in-the-loop means in Claude Code, Codex, and AI SDK apps, and how NiceEval turns it into an eval path through waiting turns, input.requested events, and t.respond()."
5
- ---
6
-
7
- HITL (human-in-the-loop) means the agent pauses in the middle of execution and hands the decision back to a person. It is not an error. It is an intentional safety gate or information gate: approve sensitive actions, provide missing information, or choose between multiple options.
8
-
9
- You probably already use it every day:
10
-
11
- - **Claude Code** pauses before risky commands or out-of-scope file edits and asks for approval.
12
- - **Codex** approval modes such as suggest and auto-edit decide which actions need confirmation first.
13
- - **AI SDK apps** often surface tool calls in the UI when the tool does not have `execute`, let the human confirm, then return the result with `addToolResult`.
14
-
15
- That branch needs regression coverage like any other behavior: does the action happen after approval, stay blocked after rejection, and stop at the right point? But evals run automatically, without a human. NiceEval compresses the human into three programmable actions: notice that the agent parked (`t.parked()`), inspect what it is waiting for (`t.requireInputRequest()`), and answer in place of the human (`t.respond()`).
16
-
17
- ## How a pause is represented: `waiting` + `input.requested`
18
-
19
- The `Turn.status` returned from `send` has three values: `"completed"`, `"failed"`, and `"waiting"`. `"waiting"` means the turn has not finished. The agent is paused for human input. It is not a failure. Once the answer arrives, the same turn continues instead of restarting the request or opening a new conversation.
20
-
21
- The paused turn must also say **what** it is waiting for. Each pending question emits one `input.requested` event with a stable request `id`, the `action` where it paused, and optional `options` such as approve / deny. Both parts matter: `waiting` tells evals they should answer, and `input.requested` tells them what to answer.
22
-
23
- ## One complete handshake
24
-
25
- `t.respond` does not use a separate channel. It is just **another normal `send`**, and the human decision arrives as structured input:
26
-
27
- ![A full HITL handshake: t.send reaches an approval gate, returns waiting with input.requested, t.parked and t.requireInputRequest inspect the pause, then t.respond sends the structured decision and the same turn continues to completion.](/images/hitl-handshake-zh.svg)
28
-
29
- Two easy mistakes to avoid:
30
-
31
- - **The response turn is not a new conversation.** It continues on the same session line, so `ctx.session` still points to the same conversation.
32
- - **The adapter should not infer answers from free text.** `input.responses` already carries `{ requestId, optionId }` per request, or `{ requestId, text }` for free text answers.
33
-
34
- ## On the eval side: three actions
35
-
36
- ```ts
37
- const draft = await t.send("Send a weekly status email to my manager.");
38
- draft.parked();
39
-
40
- const req = t.requireInputRequest({ action: "send_email" });
41
-
42
- await t.respond({ request: req, optionId: "approve" });
43
- t.calledTool("send_email", { status: "completed" });
44
- ```
45
-
46
- - `t.parked()` asserts that the turn really stopped in `waiting`.
47
- - `t.requireInputRequest(filter)` selects exactly one pending request, matching fields such as `id`, `prompt`, `action`, or `optionIds`.
48
- - `t.respond(...)` answers it and emits the next turn. Use `t.respondAll(optionId)` when multiple pending requests of the same kind should get the same answer.
49
-
50
- Approval and rejection are two branches of the same gate. Write both evals if you want full coverage: one for "it happened after approval" and one for "it stayed blocked after rejection".
51
-
52
- ## On the adapter side: two obligations, one continuation
53
-
54
- HITL asks the adapter to do three things. The first two happen on the paused turn, the third happens when the answer arrives:
55
-
56
- 1. Return `status: "waiting"` honestly when the turn pauses.
57
- 2. Emit one `input.requested` event for each pending question, with a stable `id` and as many populated fields as possible.
58
- 3. On the next `send`, hand the answer back to the application by `requestId`, then continue from the paused point instead of replaying the original request.
59
-
60
- The paused execution state lives on the session line in `ctx.session`, typically as `hold(...)` on the paused turn and `take()` on the response turn. See [How to write send](/guides/write-send#step-5-hitl) for the full streaming skeleton.
61
-
62
- Like the rest of NiceEval capabilities, HITL is not declared with a boolean flag. **If you implement it, it exists.** If `send` returns `"waiting"` and emits `input.requested`, `t.respond` works. If it does not, the eval fails explicitly at `parked()` or `requireInputRequest()`.
63
-
64
- ## Rejection is not a failure
65
-
66
- Human denial and tool failure are different things. Use different statuses in the event stream. A human-blocked tool call should produce `action.result` with `status: "rejected"`, not `"failed"`. That way `t.noFailedActions()` still passes for an intentional rejection, and `t.calledTool("deploy", { status: "rejected" })` can assert the rejection branch precisely.
67
-
68
- ## Which agents do not need HITL
69
-
70
- Not every agent has this branch. Skip the whole feature when it does not exist:
71
-
72
- - **Single-shot agents** that answer in one pass and never wait for a person.
73
- - **Approval gates disabled in the runtime mode**. Claude Code and Codex support HITL as products, but the built-in `claude-code`, `codex`, and `bub` sandbox agents usually run fully automated and therefore do not expose HITL.
74
- - **Human review outside the execution loop**, such as a ticket system that reviews agent output later. That does not happen inside `send`, so NiceEval should not model it as HITL.
75
-
76
- The rule is simple: use HITL only when your agent's execution loop has a branch that stops for a human and then continues from the same point.
77
-
78
- ## Related reading
79
-
80
- - [Drive](/concepts/drive#human-in-the-loop-hitl) - The full eval-side APIs: `t.parked()`, `t.requireInputRequest()`, `t.respond()`, and `t.respondAll()`.
81
- - [Adapter](/concepts/adapter#inputs-for-the-different-answers) - The structured adapter inputs that carry human replies.
82
- - [How to write send](/guides/write-send) - The practical adapter-side skeleton with `ctx.session.hold` / `take`.
83
- - [Connect Your Agent](/guides/connect-your-agent) - The full integration map.
@@ -1,129 +0,0 @@
1
- ---
2
- title: "LLM-as-judge in niceeval: scoring open-ended output"
3
- sidebarTitle: "Judge"
4
- description: "How t.judge, session.judge, and turn.judge use a separate evaluator model to score factuality, closed-ended quality, and summarization — plus model resolution and severity."
5
- ---
6
-
7
- Judge assertions are the fifth scoring mechanism, alongside the four covered in [Assert](/concepts/assert). Use them when correctness cannot be expressed as a rule — for open-ended prose, tone, factual consistency, or summarization quality. The evaluator model is **entirely separate** from the agent under test, which prevents self-evaluation bias: the same model grading its own output would be prone to rating itself generously.
8
-
9
- ```ts
10
- t.judge.autoevals.factuality(reference).atLeast(0.8); // factual consistency with a reference text
11
- t.judge.autoevals.closedQA("Is the response appropriate for a 10-year-old?");
12
- t.judge.autoevals.summarizes(sourceDocument); // faithful summarization check
13
- t.judge.autoevals.closedQA("Custom scoring rubric description", { on: t.reply });
14
- ```
15
-
16
- ## The three judge shapes
17
-
18
- `t.judge.autoevals` exposes exactly three fixed forms, whatever you're evaluating maps onto one of them:
19
-
20
- - **`closedQA(question, opts?)`** — a closed-ended judgment: does the output satisfy this criterion? Best for rubric-style checks ("is the tone polite and specific", "does this avoid inventing policy details").
21
- - **`factuality(reference, opts?)`** — factual consistency between the output and a reference text you provide.
22
- - **`summarizes(source, opts?)`** — whether the output is a faithful summary of a source document.
23
-
24
- The `{ on }` option specifies which value to evaluate; it defaults based on the receiver (see below). The `{ model }` option overrides the judge model for a single call.
25
-
26
- ## Where judge lives: `t`, `session`, and `turn`
27
-
28
- Like scoped assertions, judge calls follow the rule that **the receiver decides the default material, not the call site**:
29
-
30
- ```ts
31
- t.judge.autoevals.closedQA("...") // defaults to the primary session's full conversation text
32
- session.judge.autoevals.closedQA("...") // (from t.newSession()) defaults to that session's conversation text
33
- turn.judge.autoevals.closedQA("...") // (from t.send()'s return value) defaults to turn.message only
34
- ```
35
-
36
- `t.judge` and `session.judge` are session-level: they're the right choice for judging overall reply quality or cross-turn consistency across a whole conversation. `turn.judge` is turn-level: it only sees the message from that one turn, which is what you want when different turns in a multi-turn eval need different rubrics:
37
-
38
- ```ts
39
- const turn1 = await t.send("What's in this image?"); // turn 1: image
40
- const turn2 = await t.send("What color is the background?"); // turns 2–3: text follow-ups
41
- const turn3 = await t.send("What color is the shape in the middle?"); // testing cross-turn memory
42
-
43
- t.judge.autoevals
44
- .closedQA("Does the assistant stay grounded in the image from turn 1 throughout?")
45
- .atLeast(0.7);
46
-
47
- turn3.judge.autoevals.closedQA("Does this turn answer the shape's color?").gate();
48
- ```
49
-
50
- To judge material that isn't the conversation itself — a sandbox diff, file content, or anything else — pass it explicitly with `{ on }`, regardless of which receiver you're calling from:
51
-
52
- ```ts
53
- t.judge.autoevals.closedQA("Is the generated code idiomatic TypeScript?", {
54
- on: t.sandbox.diff.get("src/handler.ts"),
55
- }).atLeast(0.7);
56
- ```
57
-
58
- ## Judge model resolution
59
-
60
- The judge model is resolved from most- to least-specific:
61
-
62
- ```
63
- Per-call { model } option
64
-
65
- Per-eval judge.model
66
-
67
- Global config judge.model
68
- ```
69
-
70
- ```ts
71
- // niceeval.config.ts — global default
72
- defineConfig({ judge: { model: "anthropic/claude-haiku-4-5" } });
73
-
74
- // A specific eval that needs a more capable judge
75
- defineEval({
76
- judge: { model: "anthropic/claude-opus-4-8" },
77
- async test(t) {
78
- t.judge.autoevals.factuality(reference); // uses claude-opus-4-8 for this eval
79
- },
80
- });
81
-
82
- // A single call override
83
- t.judge.autoevals.closedQA("rubric", { on: t.reply, model: "openai/gpt-4o" });
84
- ```
85
-
86
- If all three levels are unset, NiceEval falls back to the environment variable `NICEEVAL_JUDGE_MODEL`. If that is also unset, the judge call fails as a configuration error. There is no built-in default judge model.
87
-
88
- ## Judge endpoint and key: OpenAI-compatible protocol
89
-
90
- Judge calls use an **OpenAI-compatible `/chat/completions`** endpoint. OpenAI, DeepSeek, and any compatible gateway can act as the judge backend:
91
-
92
- ```ts
93
- defineConfig({
94
- judge: {
95
- model: "deepseek-chat",
96
- baseUrl: "https://api.deepseek.com/v1",
97
- apiKeyEnv: "DEEPSEEK_API_KEY",
98
- },
99
- });
100
- ```
101
-
102
- `baseUrl` and the API key are both resolved from most-specific to most-generic:
103
-
104
- | Item | Resolution order |
105
- |---|---|
106
- | Endpoint | `judge.baseUrl` -> `NICEEVAL_JUDGE_BASE` -> `CODEX_BASE_URL` -> `OPENAI_BASE_URL` -> `https://api.openai.com/v1` |
107
- | Key | env var named by `judge.apiKeyEnv` -> `NICEEVAL_JUDGE_KEY` -> `CODEX_API_KEY` -> `OPENAI_API_KEY` |
108
-
109
- <Warning>
110
- When no API key resolves, judge assertions are skipped silently: no error, no score, and the eval keeps running. After configuring keys, run one eval with `t.judge` and confirm the score appears in `niceeval view`.
111
- </Warning>
112
-
113
- ## Severity: judge defaults to soft
114
-
115
- Judge calls are scoring functions like any other assertion, so they follow the same gate/soft mechanics described in [Assert · Gate vs soft severity](/concepts/assert#gate-vs-soft-severity) — but their **default** is different from most value matchers:
116
-
117
- ```ts
118
- t.judge.autoevals.closedQA("Is the tone polite?"); // no threshold → soft, pure score, never fails the eval
119
- t.judge.autoevals.closedQA("Is the tone polite?").atLeast(0.7); // soft with a threshold → fails only under --strict
120
- t.judge.autoevals.closedQA("Is the tone polite?").gate(); // promoted to gate → fails immediately if below threshold
121
- ```
122
-
123
- With no `.atLeast()` and no `.gate()`, a bare judge call is recorded purely as a quality score — it shows up on the eval's score chip but can never fail the run on its own. This is the right default for judge: it's a probabilistic model scoring another probabilistic model's output, and treating every rubric miss as a hard failure would make suites flaky. Reach for `.gate()` only when the judged property is truly load-bearing (for example, "the response must not fabricate account details").
124
-
125
- ## Related pages
126
-
127
- - [Assert](/concepts/assert) — gate vs soft severity in full, plus the outcome rules judge scores feed into.
128
- - [Drive](/concepts/drive) — `t.send()`, `t.newSession()`, and the `Turn` / session handles that `t.judge`, `session.judge`, and `turn.judge` hang off of.
129
- - [Evals](/concepts/evals) — how judge scores fold into the eval lifecycle and outcome types.
@@ -1,98 +0,0 @@
1
- ---
2
- title: "NiceEval architecture: evals, agents, and sandboxes"
3
- sidebarTitle: "Overview"
4
- description: "Understand how NiceEval, Adapters, and sandbox backends work together to evaluate any AI agent through one API."
5
- ---
6
-
7
- NiceEval separates **what you evaluate** from **how the subject under test is connected**. NiceEval discovers evals, schedules runs, scores assertions, and writes reports. Adapters call the system under test. Sandbox backends isolate filesystems for coding-agent tasks.
8
-
9
- ## Four layers
10
-
11
- ```text
12
- Eval files / fixtures
13
- |
14
- v
15
- NiceEval
16
- |
17
- v
18
- Adapter
19
- |
20
- v
21
- Subject under test / Sandbox backend
22
- ```
23
-
24
- ## What NiceEval owns
25
-
26
- <CardGroup cols={2}>
27
- <Card title="Eval discovery" icon="magnifying-glass">
28
- Finds `*.eval.ts` files and fixture directories, then derives stable IDs from paths.
29
- </Card>
30
- <Card title="Scheduling" icon="timeline">
31
- Controls concurrency, retries, attempts, runs, and early exit.
32
- </Card>
33
- <Card title="Assertions and scoring" icon="chart-bar">
34
- Collects `t.check`, scoped assertions, judge scores, and test results.
35
- </Card>
36
- <Card title="Cache" icon="database">
37
- Skips cases that already passed when the fingerprint is unchanged.
38
- </Card>
39
- <Card title="Reports" icon="file-lines">
40
- Writes console, JSON, JUnit, and local viewer data.
41
- </Card>
42
- <Card title="Artifacts" icon="folder-open">
43
- Saves summaries, event streams, transcripts, diffs, traces, and logs.
44
- </Card>
45
- </CardGroup>
46
-
47
- ## Adapter
48
-
49
- <Note>
50
- `Agent` is the abstraction NiceEval calls. `Adapter` is the concrete implementation you write. NiceEval does not know your private protocol, CLI arguments, or auth rules.
51
- </Note>
52
-
53
- That boundary keeps the runner and scorers generic. Evaluating your own AI app, Claude Code, Codex, or a custom agent should not change core scoring behavior.
54
-
55
- ## Sandbox
56
-
57
- <Tabs>
58
- <Tab title="Docker">
59
- Local container backend for coding-agent evals in development and CI.
60
- </Tab>
61
- <Tab title="Vercel Sandbox">
62
- Cloud sandbox backend for stronger isolation or larger resources.
63
- </Tab>
64
- <Tab title="Third-party backend">
65
- Any service can integrate by implementing the `Sandbox` interface.
66
- </Tab>
67
- </Tabs>
68
-
69
- ## Key terms
70
-
71
- <AccordionGroup>
72
- <Accordion title="Eval">A test case: description, optional agent reference, and `test(t)`.</Accordion>
73
- <Accordion title="Agent">The connection NiceEval calls by name. It returns a standard `Turn`.</Accordion>
74
- <Accordion title="Adapter">The concrete implementation that knows how to call your runtime or CLI.</Accordion>
75
- <Accordion title="Sandbox">The isolated runtime used by coding agents.</Accordion>
76
- <Accordion title="Turn">The immutable result snapshot from one `t.send()` or `t.respond()`.</Accordion>
77
- <Accordion title="Artifact">A structured result file written after a run.</Accordion>
78
- <Accordion title="Experiment">A checked-in run configuration for comparing agents, models, flags, runs, budgets, and sandboxes.</Accordion>
79
- </AccordionGroup>
80
-
81
- ## End-to-end flow
82
-
83
- <Steps>
84
- <Step title="Discovery">Find eval files and fixtures.</Step>
85
- <Step title="Scheduling">Plan attempts from concurrency, cache, runs, and early-exit settings.</Step>
86
- <Step title="Agent send">Call the selected adapter and receive a standard `Turn`.</Step>
87
- <Step title="Scoring">Run assertions, judge checks, and tests.</Step>
88
- <Step title="Outcome">Fold results into `passed`, `failed`, `errored`, or `skipped`.</Step>
89
- <Step title="Reporting & artifacts">Write reports and structured files.</Step>
90
- </Steps>
91
-
92
- ## Related
93
-
94
- - [Evals](/concepts/evals) - what an eval is and how its lifecycle works.
95
- - [Adapter](/concepts/adapter) - how to write an adapter and use it in an experiment.
96
- - [Drive](/concepts/drive) - `t.send()`, sessions, and HITL from the eval side.
97
- - [Assert](/concepts/assert) - assertion vocabulary and outcome rules.
98
- - [Judge](/concepts/judge) - LLM-as-judge for open-ended quality.
@@ -1,42 +0,0 @@
1
- ---
2
- title: "Integration Tiers: Three Levels of Investment, Three Sets of Capabilities"
3
- sidebarTitle: "Tier"
4
- description: "Grouped by where the Adapter connects and what extra observability data it receives, integration comes in three tiers: Tier 1 is send only, Tier 2 adds OTel, Tier 3 modifies the application to expose experiment flags. What each tier buys and when to move up."
5
- ---
6
-
7
- Connecting to NiceEval is not an all-or-nothing one-time project. Grouped by where the Adapter connects and what extra observability data it receives, integration comes in three tiers — each tier adds one kind of investment on top of the previous one and buys a new set of capabilities, and every eval you have already written keeps working unchanged the whole way up.
8
-
9
- ## Tier 1: send only
10
-
11
- Your application code stays untouched; the integration is a single Adapter file — `send` talks to the same interface your user-facing frontend already uses.
12
-
13
- **The full assertion set lives at this tier**: text assertions and judges, structured output validation, multi-turn and session isolation, HITL approval flows, tool assertions from event mapping, and usage assertions from the `usage` returned by `send` — every verdict is based entirely on the `Turn` returned by `send`, and the next two tiers add no further assertions. If the application interface itself exposes model selection, **model comparison** experiments also live at this tier — the experiment's `model` reaches the Adapter via `ctx.model` and is simply forwarded with the request.
14
-
15
- Most teams start here, and for many this tier is all they ever need.
16
-
17
- ## Tier 2: send + OTel
18
-
19
- Same `send`, same event mapping — the only change is that your application also sends its OTel spans to NiceEval. If the app is already instrumented (AI SDK telemetry, LangGraph, OpenLLMetry / OpenInference, hand-rolled gen_ai spans), this is zero change; if not, what you add is a piece of generic OTel initialization — that is observability infrastructure, not an eval-specific modification.
20
-
21
- What this tier buys is **observability**: the call waterfall in `niceeval view` — every model call and tool execution inside the application, each with its own latency and tokens, laid out as a per-turn timeline. Assertions are unaffected: spans only feed the waterfall; they never enter the event stream or feed assertions. See [OTel Integration](/guides/connect-otel) for the walkthrough.
22
-
23
- ## Tier 3: application changes + experiment flags
24
-
25
- When you want to evaluate "which prompt / tool set / feature toggle works better" — a **feature A/B test** — the point of comparison lives inside the application, out of reach for the first two tiers. This tier changes the application's internal code to expose the variants as externally selectable configuration: the experiment's `flags` pass through `ctx.flags` to the Adapter, the Adapter hands them to the application with the request (an HTTP header, a request body field, or an environment variable all work), and the application switches variants by flag.
26
-
27
- Note that what gets modified is the application (exposing variants as configuration), not the integration surface — the Adapter itself stays the same and still calls the application only through its external interface.
28
-
29
- ## Non-intrusive is the baseline for the first two tiers
30
-
31
- The first two tiers are both **non-intrusive**: you start the application your own way (`pnpm start`, deployed wherever) — the eval side never spawns the application process or opens a new port. The Adapter only talks to **the same interface your user-facing frontend already uses** (an HTTP endpoint, an SSE stream), and reports a clear "start the application first" error when it cannot connect. The only difference between the two tiers is observability data: Tier 1 has only the events returned by `send`; Tier 2 adds a copy of spans sent to NiceEval, and the waterfall appears with them.
32
-
33
- ## How to move up
34
-
35
- The three tiers are progressive, not mutually exclusive: start with Tier 1 to get baselines and model comparison running; move up to Tier 2 when you want the call waterfall; move up to Tier 3 when you need to compare variants inside the application. Each upgrade only adds something to the Adapter or the application — how to organize comparisons on the experiment side is covered in [Experiments](/concepts/experiment).
36
-
37
- ## Related reading
38
-
39
- - [Connect Your Agent](/guides/connect-your-agent) — The integration overview: minimal integration and parameter channels.
40
- - [Adapter](/concepts/adapter) — The contract itself: what `send` receives and returns, and how `ctx.model` / `ctx.telemetry` / `ctx.flags` appear per tier.
41
- - [OTel Integration](/guides/connect-otel) — The full Tier 2 walkthrough.
42
- - [Experiments](/concepts/experiment) — How model / flags comparisons are declared on the experiment side.
@@ -1,23 +0,0 @@
1
- # 给 Agent 的 docs 写作指南
2
-
3
- 整理日期:2026-07-04。蒸馏自 [`../dev-docs-dx-sources/`](../dev-docs-dx-sources/00-index.md) 的来源级提取(Diátaxis、Divio、Google / Microsoft / GitHub 风格指南、Write the Docs、The Good Docs Project、MDN、I'd Rather Be Writing、GetDX / Twilio / Phodal 的 DX 资料)。
4
-
5
- 这套指南回答"怎么写好一页用户文档",按写作流程组织。仓库特定的术语表、目录职责和校验命令以 [`docs-site/AGENTS.md`](../../AGENTS.md) 为准,本指南不重复维护;两者冲突时以 AGENTS.md 为准。
6
-
7
- ## 使用顺序
8
-
9
- 写任何 `docs-site/` 页面前,按顺序过一遍:
10
-
11
- 1. [01-page-types.md](./01-page-types.md):先判定这页是 Tutorial、How-to、Reference、Explanation 还是 Troubleshooting。这是最重要的一步,页面类型错了后面全错。
12
- 3. [03-style-rules.md](./03-style-rules.md):行文、步骤、可扫描性、标题与搜索意图。
13
- 4. [04-code-examples.md](./04-code-examples.md):代码示例的写法和维护义务。
14
- 5. [05-dx-failure-paths.md](./05-dx-failure-paths.md):失败路径覆盖和"文档是 DX 的一部分"视角。
15
- 6. [06-checklists.md](./06-checklists.md):动笔前、按类型、发布前三层检查清单。
16
-
17
- ## 三条最高原则
18
-
19
- 如果只记三件事:
20
-
21
- 1. **一页只服务一种需求。** 学习、干活、查事实、理解,四选一。页面边界模糊是文档难用的首要原因。(Diátaxis)
22
- 2. **"刚好足够"胜过"全面覆盖"。** 更多内容会让所有内容更难找;优先写高频高价值路径,不为完整性堆低频分支。(GitHub Docs)
23
- 3. **错误的文档比没有文档更糟。** 每个声明的命令、字段、路径、能力,写之前先对当前代码或真实运行结果核实;未实现的能力不进正式文档。(Write the Docs / MDN)