niceeval 0.4.4 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +21 -12
  2. package/README.zh.md +8 -9
  3. package/docs-site/.mintignore +13 -0
  4. package/docs-site/AGENTS.md +46 -0
  5. package/docs-site/concepts/adapter.mdx +287 -0
  6. package/docs-site/concepts/assert.mdx +243 -0
  7. package/docs-site/concepts/drive.mdx +118 -0
  8. package/docs-site/concepts/evals.mdx +108 -0
  9. package/docs-site/concepts/experiment.mdx +37 -0
  10. package/docs-site/concepts/hitl.mdx +83 -0
  11. package/docs-site/concepts/judge.mdx +129 -0
  12. package/docs-site/concepts/overview.mdx +98 -0
  13. package/docs-site/concepts/tier.mdx +42 -0
  14. package/docs-site/docs-ref/00-index.md +23 -0
  15. package/docs-site/docs-ref/01-page-types.md +43 -0
  16. package/docs-site/docs-ref/03-style-rules.md +45 -0
  17. package/docs-site/docs-ref/04-code-examples.md +37 -0
  18. package/docs-site/docs-ref/05-dx-failure-paths.md +45 -0
  19. package/docs-site/docs-ref/06-checklists.md +53 -0
  20. package/docs-site/docs.json +256 -0
  21. package/docs-site/example/ai-agent-application.mdx +148 -0
  22. package/docs-site/example/claude-code-codex-plugin.mdx +162 -0
  23. package/docs-site/example/claude-code-codex-skill.mdx +147 -0
  24. package/docs-site/example/showcase.mdx +39 -0
  25. package/docs-site/example/tier1-ai-sdk-v7.mdx +136 -0
  26. package/docs-site/example/tier1-claude-sdk.mdx +119 -0
  27. package/docs-site/example/tier1-codex-sdk.mdx +111 -0
  28. package/docs-site/example/tier1-langgraph.mdx +119 -0
  29. package/docs-site/example/tier1-pi-sdk.mdx +119 -0
  30. package/docs-site/favicon.svg +5 -0
  31. package/docs-site/github-diff.css +135 -0
  32. package/docs-site/github-diff.js +11 -0
  33. package/docs-site/guides/authoring.mdx +129 -0
  34. package/docs-site/guides/ci-integration.mdx +97 -0
  35. package/docs-site/guides/connect-otel.mdx +209 -0
  36. package/docs-site/guides/connect-your-agent.mdx +221 -0
  37. package/docs-site/guides/dataset-fanout.mdx +98 -0
  38. package/docs-site/guides/experiments.mdx +71 -0
  39. package/docs-site/guides/fixtures.mdx +147 -0
  40. package/docs-site/guides/reporters.mdx +113 -0
  41. package/docs-site/guides/runner.mdx +79 -0
  42. package/docs-site/guides/sandbox-agent.mdx +138 -0
  43. package/docs-site/guides/sandbox-backends.mdx +64 -0
  44. package/docs-site/guides/scoring-guide.mdx +92 -0
  45. package/docs-site/guides/viewing-results.mdx +195 -0
  46. package/docs-site/guides/write-experiment.mdx +81 -0
  47. package/docs-site/guides/write-send.mdx +347 -0
  48. package/docs-site/images/adapter-tiers-zh.svg +60 -0
  49. package/docs-site/images/agent-turn-roundtrip-en.svg +62 -0
  50. package/docs-site/images/agent-turn-roundtrip-zh.svg +77 -0
  51. package/docs-site/images/hitl-handshake-zh.svg +89 -0
  52. package/docs-site/images/logo.svg +6 -0
  53. package/docs-site/index.mdx +181 -0
  54. package/docs-site/introduction.mdx +141 -0
  55. package/docs-site/quickstart.mdx +136 -0
  56. package/docs-site/reference/builtin-agents.mdx +161 -0
  57. package/docs-site/reference/capabilities.mdx +76 -0
  58. package/docs-site/reference/cli.mdx +120 -0
  59. package/docs-site/reference/define-agent.mdx +168 -0
  60. package/docs-site/reference/define-config.mdx +41 -0
  61. package/docs-site/reference/define-eval.mdx +160 -0
  62. package/docs-site/reference/events.mdx +131 -0
  63. package/docs-site/reference/expect.mdx +112 -0
  64. package/docs-site/tracker.js +8 -0
  65. package/docs-site/zh/concepts/adapter.mdx +286 -0
  66. package/docs-site/zh/concepts/assert.mdx +243 -0
  67. package/docs-site/zh/concepts/drive.mdx +118 -0
  68. package/docs-site/zh/concepts/evals.mdx +124 -0
  69. package/docs-site/zh/concepts/experiment.mdx +37 -0
  70. package/docs-site/zh/concepts/hitl.mdx +83 -0
  71. package/docs-site/zh/concepts/judge.mdx +130 -0
  72. package/docs-site/zh/concepts/overview.mdx +109 -0
  73. package/docs-site/zh/concepts/tier.mdx +44 -0
  74. package/docs-site/zh/example/ai-agent-application.mdx +152 -0
  75. package/docs-site/zh/example/claude-code-codex-plugin.mdx +163 -0
  76. package/docs-site/zh/example/claude-code-codex-skill.mdx +147 -0
  77. package/docs-site/zh/example/showcase.mdx +39 -0
  78. package/docs-site/zh/example/tier1-ai-sdk-v7.mdx +140 -0
  79. package/docs-site/zh/example/tier1-claude-sdk.mdx +122 -0
  80. package/docs-site/zh/example/tier1-codex-sdk.mdx +117 -0
  81. package/docs-site/zh/example/tier1-langgraph.mdx +123 -0
  82. package/docs-site/zh/example/tier1-pi-sdk.mdx +122 -0
  83. package/docs-site/zh/guides/agent-feedback-loop.mdx +79 -0
  84. package/docs-site/zh/guides/authoring.mdx +129 -0
  85. package/docs-site/zh/guides/ci-integration.mdx +97 -0
  86. package/docs-site/zh/guides/connect-otel.mdx +208 -0
  87. package/docs-site/zh/guides/connect-your-agent.mdx +226 -0
  88. package/docs-site/zh/guides/dataset-fanout.mdx +83 -0
  89. package/docs-site/zh/guides/experiments.mdx +72 -0
  90. package/docs-site/zh/guides/fixtures.mdx +147 -0
  91. package/docs-site/zh/guides/official-adapters.mdx +132 -0
  92. package/docs-site/zh/guides/reporters.mdx +113 -0
  93. package/docs-site/zh/guides/runner.mdx +82 -0
  94. package/docs-site/zh/guides/sandbox-agent.mdx +139 -0
  95. package/docs-site/zh/guides/sandbox-backends.mdx +75 -0
  96. package/docs-site/zh/guides/scoring-guide.mdx +92 -0
  97. package/docs-site/zh/guides/viewing-results.mdx +195 -0
  98. package/docs-site/zh/guides/write-experiment.mdx +101 -0
  99. package/docs-site/zh/guides/write-send.mdx +353 -0
  100. package/docs-site/zh/index.mdx +180 -0
  101. package/docs-site/zh/introduction.mdx +140 -0
  102. package/docs-site/zh/quickstart.mdx +137 -0
  103. package/docs-site/zh/reference/builtin-agents.mdx +364 -0
  104. package/docs-site/zh/reference/capabilities.mdx +76 -0
  105. package/docs-site/zh/reference/cli.mdx +140 -0
  106. package/docs-site/zh/reference/define-agent.mdx +461 -0
  107. package/docs-site/zh/reference/define-config.mdx +107 -0
  108. package/docs-site/zh/reference/define-eval.mdx +674 -0
  109. package/docs-site/zh/reference/events.mdx +252 -0
  110. package/docs-site/zh/reference/expect.mdx +208 -0
  111. package/package.json +3 -2
  112. package/src/agents/bub.ts +1 -1
  113. package/src/agents/claude-code.ts +8 -5
  114. package/src/agents/codex.ts +9 -5
  115. package/src/agents/sdk-streams.test.ts +4 -4
  116. package/src/agents/sdk-streams.ts +14 -9
  117. package/src/agents/types.ts +40 -1
  118. package/src/agents/ui-message-stream.ts +1 -0
  119. package/src/cli.ts +36 -7
  120. package/src/context/types.ts +140 -0
  121. package/src/expect/index.ts +15 -8
  122. package/src/i18n/en.ts +19 -6
  123. package/src/i18n/zh-CN.ts +19 -6
  124. package/src/o11y/parsers/bub.test.ts +71 -0
  125. package/src/o11y/parsers/bub.ts +5 -0
  126. package/src/o11y/types.ts +27 -2
  127. package/src/runner/reporters/artifacts.ts +11 -3
  128. package/src/runner/reporters/live.ts +45 -8
  129. package/src/runner/run.ts +71 -21
  130. package/src/runner/types.ts +45 -2
  131. package/src/sandbox/types.ts +18 -0
  132. package/src/scoring/types.ts +5 -0
  133. package/src/shared/types.ts +3 -0
  134. package/src/util.test.ts +26 -1
  135. package/src/util.ts +16 -0
  136. package/src/view/app/App.tsx +2 -2
  137. package/src/view/app/components/AttemptModal.tsx +2 -0
  138. package/src/view/app/components/CopyControls.tsx +87 -28
  139. package/src/view/app/i18n.ts +3 -3
  140. package/src/view/client-dist/app.css +1 -1
  141. package/src/view/client-dist/app.js +18 -18
  142. package/docs/README.md +0 -120
  143. package/docs/adapters/README.md +0 -60
  144. package/docs/adapters/authoring.md +0 -179
  145. package/docs/adapters/coding-agent-skills-plugins.md +0 -324
  146. package/docs/adapters/collection.md +0 -128
  147. package/docs/adapters/contract.md +0 -264
  148. package/docs/adapters/reference/agent-eval.md +0 -215
  149. package/docs/adapters/reference/agent-loop-apis.md +0 -69
  150. package/docs/adapters/reference/claude-code-otel-telemetry.md +0 -100
  151. package/docs/adapters/reference/eve-protocol.md +0 -127
  152. package/docs/adapters/reference/otel-genai.md +0 -107
  153. package/docs/adapters/reference/otel-instrumentation.md +0 -65
  154. package/docs/adapters/targets.md +0 -99
  155. package/docs/architecture.md +0 -140
  156. package/docs/assertions.md +0 -387
  157. package/docs/capabilities-by-construction.md +0 -48
  158. package/docs/cli.md +0 -173
  159. package/docs/concepts.md +0 -106
  160. package/docs/e2e-ci.md +0 -332
  161. package/docs/eval-authoring.md +0 -319
  162. package/docs/experiments.md +0 -157
  163. package/docs/getting-started.md +0 -224
  164. package/docs/multi-agent.md +0 -145
  165. package/docs/observability.md +0 -337
  166. package/docs/origin-integration.md +0 -195
  167. package/docs/references.md +0 -35
  168. package/docs/reports.md +0 -551
  169. package/docs/results-format.md +0 -228
  170. package/docs/results-lib.md +0 -191
  171. package/docs/runner.md +0 -104
  172. package/docs/sandbox.md +0 -276
  173. package/docs/scoring.md +0 -119
  174. package/docs/source-map.md +0 -126
  175. package/docs/tier-sync.md +0 -193
  176. package/docs/view.md +0 -194
  177. package/docs/vision.md +0 -88
@@ -0,0 +1,243 @@
1
+ ---
2
+ title: "niceeval assertions: values, scoped facts, tests, and cost"
3
+ sidebarTitle: "Assert"
4
+ description: "niceeval's assertion vocabulary — value assertions, scoped assertions, test-as-scoring, and efficiency checks — plus gate vs soft severity and the outcome rules that fold them into a result."
5
+ ---
6
+
7
+ Assertion is the process of taking everything an agent did during an eval — every message, tool call, file change, and token spent — and folding it into a single, actionable outcome. niceeval gives you four assertion mechanisms that complement each other: some check values immediately, some assess the whole run after it completes, some execute tests inside the sandbox, and some measure efficiency. All four produce the same `Assertion` type and feed into the same outcome rules. For the fifth mechanism — asking a language model to judge open-ended quality — see [Judge](/concepts/judge).
8
+
9
+ ## The four assertion mechanisms
10
+
11
+ <CardGroup cols={2}>
12
+ <Card title="1. Value assertions" icon="equals">
13
+ `t.check(value, matcher)` and `t.require(value, matcher)` evaluate a specific value immediately against a matcher from `niceeval/expect`. Use these for facts you can verify inline.
14
+ </Card>
15
+ <Card title="2. Scoped assertions" icon="crosshairs">
16
+ `t.succeeded()`, `t.calledTool()`, `t.messageIncludes()`, and friends are registered during `test(t)` but evaluated **after** the function returns, against the complete turn data. Use these for whole-run facts.
17
+ </Card>
18
+ <Card title="3. Test-as-scoring" icon="flask">
19
+ For sandbox evals, run project tests, build scripts, or focused probe commands from `test(t)`. Use this for coding tasks where file content and build results are the ground truth.
20
+ </Card>
21
+ <Card title="4. Efficiency assertions" icon="gauge">
22
+ `t.maxTokens()` and `t.maxCost()` turn token usage and estimated cost into scoreable dimensions. An agent that answers correctly but burns ten times the expected tokens should not score the same as one that answers efficiently.
23
+ </Card>
24
+ </CardGroup>
25
+
26
+ ## Gate vs soft severity
27
+
28
+ Every assertion carries a **severity** that determines how it influences the final outcome. There are exactly two severities:
29
+
30
+ <Tabs>
31
+ <Tab title="gate">
32
+ A gate assertion is a hard requirement. If it fails, the entire eval is immediately classified as `failed` — regardless of how well every other assertion passed. Use gate for facts that must be true: "the agent called the correct tool", "the response parsed as valid JSON", "no shell commands errored."
33
+
34
+ Most matchers in `niceeval/expect` (`includes`, `equals`, `matches`, `satisfies`) default to gate. Scoped assertions like `t.succeeded()` and `t.calledTool()` also default to gate.
35
+ </Tab>
36
+ <Tab title="soft">
37
+ A soft assertion is a quality score with a numeric threshold. If the score falls below the threshold, the eval becomes `passed` rather than `failed` — a signal that there is a quality regression, but not a hard breakage. Soft failures only count as failures when you run with `--strict`.
38
+
39
+ Use soft for continuous judgments where the answer is "how good" rather than "correct or not": similarity scoring, LLM-as-judge factuality ratings, cost budgets you want to track without blocking CI.
40
+
41
+ Matchers that produce a continuous score (`similarity`) and all judge calls default to soft.
42
+ </Tab>
43
+ </Tabs>
44
+
45
+ You can override the default severity with a chain method on any matcher or assertion:
46
+
47
+ ```ts
48
+ t.check(t.reply, includes("confirmed")); // gate (default)
49
+ t.check(t.reply, similarity(expected).gate()); // promote to gate
50
+ t.maxTokens(80_000).atLeast(0.7); // demote to soft with a threshold
51
+ ```
52
+
53
+ ## Outcome rules
54
+
55
+ Once all assertions are collected, the runner folds them into a single outcome in this order:
56
+
57
+ ```
58
+ Execution error (timeout / thrown exception / author mistake) → failed
59
+ Explicit t.skip(reason) was called → skipped
60
+ Any gate assertion failed → failed
61
+ All gates passed, but at least one soft is below its threshold → passed (flagged under --strict)
62
+ Otherwise → passed
63
+ ```
64
+
65
+ <CardGroup cols={2}>
66
+ <Card title="passed" icon="circle-check" color="#22c55e">
67
+ No errors, all gate assertions passed, all soft assertions met their thresholds.
68
+ </Card>
69
+ <Card title="failed" icon="circle-xmark" color="#ef4444">
70
+ Execution error **or** at least one gate assertion did not pass. Hard failure.
71
+ </Card>
72
+ <Card title="passed (scored)" icon="chart-bar" color="#f59e0b">
73
+ All gates passed, but at least one soft fell below its threshold. Quality regression — silent by default, red under `--strict`.
74
+ </Card>
75
+ <Card title="skipped" icon="forward" color="#6b7280">
76
+ `t.skip("reason")` was called. Excluded from pass-rate calculations entirely.
77
+ </Card>
78
+ </CardGroup>
79
+
80
+ When you run an eval more than once (`runs > 1`), the per-eval summary becomes a **pass rate** (the fraction of runs that produced `passed`) and an average latency, rather than a single outcome.
81
+
82
+ ## 1. Value assertions — `niceeval/expect` matchers
83
+
84
+ `t.check(value, assertion)` evaluates the assertion immediately and records the result. `t.require(value, assertion)` does the same but **throws immediately** if the assertion fails, aborting the rest of the test function. Use `t.require` for preconditions: if a required fact is false, there is no point continuing.
85
+
86
+ The matchers available from `niceeval/expect`:
87
+
88
+ ```ts
89
+ import {
90
+ includes, // substring or regex match (default: gate)
91
+ equals, // deep equality (default: gate)
92
+ matches, // Standard Schema (Zod etc.) check (default: gate)
93
+ similarity, // normalized Levenshtein 0–1 (default: soft)
94
+ satisfies, // custom predicate + label (default: gate)
95
+ } from "niceeval/expect";
96
+ ```
97
+
98
+ Usage examples:
99
+
100
+ ```ts
101
+ // Check that the agent's reply contains a specific string
102
+ t.check(t.reply, includes("order confirmed"));
103
+
104
+ // Deep-equal check on structured output
105
+ t.check(turn.data, equals({ status: "refund", amount: 42 }));
106
+
107
+ // Validate structured output against a Zod schema
108
+ t.check(turn.data, matches(z.object({ intent: z.enum(["refund", "ship"]) })));
109
+
110
+ // Similarity with an explicit threshold
111
+ t.check(t.reply, similarity("expected answer").atLeast(0.8));
112
+
113
+ // Custom predicate
114
+ t.check(turn.data, satisfies((d) => d.total > 0, "total is positive"));
115
+ ```
116
+
117
+ Matchers are pure functions — `(value) => number` — so you can write your own and pass them to `t.check` without any special registration.
118
+
119
+ ## 2. Scoped assertions
120
+
121
+ Scoped assertions are registered during `test(t)` but evaluated **after the function returns**, against the complete accumulated turn data. They read from the standard event stream that `t.send()` produces (see [Drive](/concepts/drive)) and its derived facts — so as long as your adapter produces correct events, these assertions work identically for every agent.
122
+
123
+ <Warning>
124
+ Scoped assertions only appear on `t` if the agent has declared the corresponding capability. Calling `t.calledTool()` when the agent has not declared `toolObservability: true` is a compile error.
125
+ </Warning>
126
+
127
+ ### Run / session dimension
128
+
129
+ ```ts
130
+ t.succeeded(); // run completed with no failed actions and no unresolved HITL
131
+ t.parked(); // cleanly stopped on a HITL input.requested event
132
+ t.messageIncludes("Regards,"); // all message events concatenated contain this string/regex
133
+ ```
134
+
135
+ ### Tool / action dimension
136
+
137
+ ```ts
138
+ t.calledTool("bash", { input: { command: /^pwd/ }, count: 1 });
139
+ t.notCalledTool("shell", { input: { command: /npm i/ } });
140
+ t.toolOrder(["read_file", "write_file"]); // relative order of tool calls
141
+ t.usedNoTools();
142
+ t.maxToolCalls(5);
143
+ t.loadedSkill("memory-v2"); // sugar for calledTool("load_skill", ...)
144
+ t.calledSubagent("researcher", { remoteUrl: /api\.example/ });
145
+ t.noFailedActions(); // no tool, subagent, or skill has status "failed"
146
+ ```
147
+
148
+ The `input` argument to `calledTool` and `notCalledTool` supports a small matching language: a plain object performs deep partial matching, a `RegExp` matches against the serialized input, and a predicate function receives the raw input value.
149
+
150
+ ### Event stream dimension (low-level escape hatch)
151
+
152
+ ```ts
153
+ t.event("input.requested", { count: 1 });
154
+ t.notEvent("error");
155
+ t.eventOrder(["action.called", "subagent.called"]);
156
+ t.eventsSatisfy("read before write", (events) => /* custom predicate */ true);
157
+ ```
158
+
159
+ All scoped assertions above are syntactic sugar for these low-level event stream queries. When none of the higher-level assertions fit your use case, you can drop down to `eventsSatisfy` and write an arbitrary predicate over the raw `StreamEvent[]`.
160
+
161
+ ### Structured output (on `turn`, not `t`)
162
+
163
+ ```ts
164
+ const turn = await t.send("Return the result as JSON");
165
+ turn.outputEquals({ status: "ok" }); // deep equality on turn.data
166
+ turn.outputMatches(z.object({ status: z.string() })); // Standard Schema validation
167
+ ```
168
+
169
+ ### Workspace dimension (sandbox agents only)
170
+
171
+ ```ts
172
+ t.sandbox.fileChanged("src/Button.tsx");
173
+ t.sandbox.fileDeleted("src/old.ts");
174
+ t.sandbox.diff.isEmpty(); // no repository files were modified this turn
175
+ t.sandbox.notInDiff(/sk-[A-Za-z0-9]/); // diff contains no secrets / inline styles
176
+ t.check(await t.sandbox.runCommand("npm", ["test"]), commandSucceeded()); // npm test exited 0
177
+ t.check(await t.sandbox.runCommand("npm", ["run", "build"]), commandSucceeded()); // npm run build exited 0
178
+ t.sandbox.noFailedShellCommands();
179
+ ```
180
+
181
+ `t.sandbox.diff` is a queryable object. `t.sandbox.diff.get("src/Button.tsx")` returns the file's post-change content; `t.sandbox.diff.isEmpty()` checks whether any files changed; `t.sandbox.diff.matches(re)` and `t.sandbox.notInDiff(re)` run a regex over the full diff text.
182
+
183
+ Scoped assertions follow one rule everywhere: **the receiver decides the scope, not the assertion name.** `t.*` aggregates every turn of the whole eval run (including any `t.newSession()` sessions); `session.*` (from `t.newSession()`) scopes to that one session; `turn.*` (from `t.send()`'s return value) scopes to that single turn only. Same vocabulary, different receiver — see [Drive](/concepts/drive) for what each receiver is.
184
+
185
+ ## 3. Test-as-scoring (sandbox evals)
186
+
187
+ For sandbox coding evals, run validation commands inside `test(t)` and record their result as assertions.
188
+
189
+ ```ts
190
+ import { commandSucceeded, includes } from "niceeval/expect";
191
+
192
+ const testResult = await t.sandbox.runCommand("npm", ["test"]);
193
+ t.check(testResult, commandSucceeded());
194
+
195
+ const src = await t.sandbox.readSourceFiles({ extensions: ["ts"] });
196
+ t.check(src.text(), includes(/z\.object\s*\(/));
197
+ ```
198
+
199
+ You can also assert behavior through the standard event stream: `t.calledTool(...)`, `t.sandbox.noFailedShellCommands()`, `t.eventsSatisfy(...)`, and diff assertions such as `t.sandbox.fileChanged(...)`.
200
+
201
+ ## 4. Efficiency / cost assertions
202
+
203
+ Token usage is a first-class scoring dimension. An agent that answers correctly but burns far more tokens than expected should not score identically to one that answers efficiently.
204
+
205
+ ```ts
206
+ t.maxTokens(50_000); // hard token limit for the entire run (gate by default)
207
+ t.maxCost(0.5); // estimated cost cap in USD (requires a price table in config)
208
+ t.maxTokens(80_000).atLeast(0.7); // soft variant — tracked but only red under --strict
209
+ t.check(t.usage.outputTokens, satisfies((n) => n < 10_000, "not verbose"));
210
+ ```
211
+
212
+ `t.usage` is available anywhere inside `test(t)` and exposes `{ inputTokens, outputTokens, cacheReadTokens?, … }`. For sandbox agents, token counts are extracted from the transcript by the adapter; for remote agents, they are returned in `Turn.usage`.
213
+
214
+ ## Custom scorers
215
+
216
+ A value assertion is just a function `(value) => number | Promise<number>`. You can write custom matchers using `makeAssertion`:
217
+
218
+ ```ts
219
+ import { makeAssertion } from "niceeval/expect";
220
+ import type { Assertion } from "niceeval/expect";
221
+
222
+ function jsonValid(): Assertion {
223
+ return makeAssertion({
224
+ name: "jsonValid",
225
+ severity: "gate",
226
+ score: (value) => {
227
+ try { JSON.parse(String(value)); return 1; }
228
+ catch { return 0; }
229
+ },
230
+ });
231
+ }
232
+
233
+ t.check(t.reply, jsonValid());
234
+ ```
235
+
236
+ Custom matchers compose with the same chain methods as built-ins: `.gate()`, `.atLeast(0.7)`.
237
+
238
+ ## Related pages
239
+
240
+ - [Drive](/concepts/drive) — `t.send()`, `t.newSession()`, and HITL: how you produce the Turn data these assertions read from.
241
+ - [Judge](/concepts/judge) — the fifth scoring mechanism, for open-ended quality that can't be expressed as a fixed rule.
242
+ - [Adapter](/concepts/adapter) — how the standard event stream is produced, which scoped assertions depend on.
243
+ - [Evals](/concepts/evals) — how assertions fold into the eval lifecycle and outcome types.
@@ -0,0 +1,118 @@
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.
@@ -0,0 +1,108 @@
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.
@@ -0,0 +1,37 @@
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.
@@ -0,0 +1,83 @@
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.