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.
- package/docs-site/zh/concepts/adapter.mdx +6 -6
- package/docs-site/zh/concepts/experiment.mdx +7 -7
- package/docs-site/zh/concepts/overview.mdx +1 -1
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +64 -21
- package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
- package/docs-site/zh/guides/custom-reports.mdx +243 -0
- package/docs-site/zh/guides/experiments.mdx +3 -3
- package/docs-site/zh/guides/report-components.mdx +252 -0
- package/docs-site/zh/guides/reporters.mdx +1 -1
- package/docs-site/zh/guides/results-data.mdx +224 -0
- package/docs-site/zh/guides/runner.mdx +1 -1
- package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
- package/docs-site/zh/guides/viewing-results.mdx +147 -77
- package/docs-site/zh/guides/write-experiment.mdx +9 -9
- package/docs-site/zh/guides/write-send.mdx +6 -6
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/introduction.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +11 -3
- package/docs-site/zh/reference/define-agent.mdx +5 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/package.json +3 -2
- package/src/agents/ai-sdk.test.ts +1 -1
- package/src/agents/ai-sdk.ts +2 -2
- package/src/agents/claude-code.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/streaming.test.ts +1 -1
- package/src/agents/types.ts +4 -4
- package/src/agents/ui-message-stream.test.ts +1 -1
- package/src/cli.ts +120 -18
- package/src/context/context.test.ts +1 -1
- package/src/context/context.ts +3 -3
- package/src/context/session.ts +2 -2
- package/src/context/types.ts +2 -2
- package/src/i18n/en.ts +29 -7
- package/src/i18n/zh-CN.ts +24 -2
- package/src/report/aggregate.ts +79 -50
- package/src/report/components.tsx +190 -0
- package/src/report/compute.ts +149 -84
- package/src/report/default-report.tsx +222 -0
- package/src/report/dual-face.test.tsx +527 -0
- package/src/report/format.ts +29 -0
- package/src/report/index.ts +79 -24
- package/src/report/load.ts +67 -0
- package/src/report/metrics.ts +2 -1
- package/src/report/param.ts +18 -0
- package/src/report/primitives.tsx +117 -0
- package/src/report/react/CaseList.tsx +2 -2
- package/src/report/react/DeltaTable.tsx +2 -2
- package/src/report/react/MetricBars.tsx +109 -0
- package/src/report/react/MetricLine.tsx +200 -0
- package/src/report/react/MetricMatrix.tsx +1 -1
- package/src/report/react/MetricScatter.tsx +1 -1
- package/src/report/react/MetricTable.tsx +1 -1
- package/src/report/react/RunOverview.tsx +4 -4
- package/src/report/react/Scoreboard.tsx +3 -3
- package/src/report/react/cell.tsx +1 -1
- package/src/report/react/fixtures.ts +98 -36
- package/src/report/react/format.ts +3 -28
- package/src/report/react/index.tsx +41 -24
- package/src/report/react/render.test.tsx +8 -8
- package/src/report/react/styles.css +77 -0
- package/src/report/report.test.ts +248 -128
- package/src/report/report.ts +69 -0
- package/src/report/text/faces.ts +356 -0
- package/src/report/text/layout.ts +125 -0
- package/src/report/text/plot.ts +127 -0
- package/src/report/tree.ts +220 -0
- package/src/report/types.ts +75 -27
- package/src/report/web.ts +40 -0
- package/src/results/copy.ts +95 -41
- package/src/results/format.ts +12 -11
- package/src/results/index.ts +34 -17
- package/src/results/open.ts +201 -81
- package/src/results/results.test.ts +595 -191
- package/src/results/select.ts +107 -55
- package/src/results/types.ts +137 -45
- package/src/results/writer.ts +258 -0
- package/src/runner/attempt.ts +3 -3
- package/src/runner/fingerprint.ts +1 -1
- package/src/runner/reporters/artifacts.ts +38 -78
- package/src/runner/reporters/braintrust.test.ts +2 -2
- package/src/runner/reporters/braintrust.ts +3 -3
- package/src/runner/run.ts +1 -1
- package/src/runner/types.ts +18 -12
- package/src/show/compose.ts +215 -0
- package/src/show/index.ts +263 -0
- package/src/show/render.ts +456 -0
- package/src/show/show.test.ts +505 -0
- package/src/view/app/App.tsx +85 -24
- package/src/view/app/components/CostScoreChart.tsx +29 -10
- package/src/view/app/components/ExperimentTable.tsx +27 -9
- package/src/view/app/components/GroupSelector.tsx +1 -1
- package/src/view/app/i18n.ts +18 -3
- package/src/view/app/lib/attempt-route.test.ts +9 -9
- package/src/view/app/lib/attempt-route.ts +5 -5
- package/src/view/app/lib/outcome.ts +1 -3
- package/src/view/app/lib/rows.ts +95 -15
- package/src/view/app/main.tsx +18 -6
- package/src/view/app/pages/RunsPage.tsx +4 -7
- package/src/view/app/pages/TracesPage.tsx +3 -8
- package/src/view/app/types.ts +50 -6
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +20 -20
- package/src/view/data.test.ts +254 -0
- package/src/view/data.ts +336 -0
- package/src/view/index.ts +64 -10
- package/src/view/server.ts +31 -9
- package/src/view/shared/types.ts +47 -41
- package/src/view/styles.css +5 -0
- package/src/view/template.html +1 -0
- package/src/view/view-report.test.ts +308 -0
- package/docs-site/.mintignore +0 -13
- package/docs-site/AGENTS.md +0 -46
- package/docs-site/concepts/adapter.mdx +0 -287
- package/docs-site/concepts/assert.mdx +0 -243
- package/docs-site/concepts/drive.mdx +0 -118
- package/docs-site/concepts/evals.mdx +0 -108
- package/docs-site/concepts/experiment.mdx +0 -37
- package/docs-site/concepts/hitl.mdx +0 -83
- package/docs-site/concepts/judge.mdx +0 -129
- package/docs-site/concepts/overview.mdx +0 -98
- package/docs-site/concepts/tier.mdx +0 -42
- package/docs-site/docs-ref/00-index.md +0 -23
- package/docs-site/docs-ref/01-page-types.md +0 -43
- package/docs-site/docs-ref/03-style-rules.md +0 -45
- package/docs-site/docs-ref/04-code-examples.md +0 -37
- package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
- package/docs-site/docs-ref/06-checklists.md +0 -53
- package/docs-site/docs.json +0 -256
- package/docs-site/example/ai-agent-application.mdx +0 -148
- package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
- package/docs-site/example/claude-code-codex-skill.mdx +0 -147
- package/docs-site/example/showcase.mdx +0 -39
- package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
- package/docs-site/example/tier1-claude-sdk.mdx +0 -119
- package/docs-site/example/tier1-codex-sdk.mdx +0 -111
- package/docs-site/example/tier1-langgraph.mdx +0 -119
- package/docs-site/example/tier1-pi-sdk.mdx +0 -119
- package/docs-site/favicon.svg +0 -5
- package/docs-site/github-diff.css +0 -135
- package/docs-site/github-diff.js +0 -11
- package/docs-site/guides/authoring.mdx +0 -129
- package/docs-site/guides/ci-integration.mdx +0 -97
- package/docs-site/guides/connect-otel.mdx +0 -209
- package/docs-site/guides/connect-your-agent.mdx +0 -221
- package/docs-site/guides/dataset-fanout.mdx +0 -98
- package/docs-site/guides/experiments.mdx +0 -71
- package/docs-site/guides/fixtures.mdx +0 -147
- package/docs-site/guides/reporters.mdx +0 -113
- package/docs-site/guides/runner.mdx +0 -79
- package/docs-site/guides/sandbox-agent.mdx +0 -138
- package/docs-site/guides/sandbox-backends.mdx +0 -64
- package/docs-site/guides/scoring-guide.mdx +0 -92
- package/docs-site/guides/viewing-results.mdx +0 -195
- package/docs-site/guides/write-experiment.mdx +0 -81
- package/docs-site/guides/write-send.mdx +0 -347
- package/docs-site/index.mdx +0 -181
- package/docs-site/introduction.mdx +0 -141
- package/docs-site/quickstart.mdx +0 -136
- package/docs-site/reference/builtin-agents.mdx +0 -161
- package/docs-site/reference/capabilities.mdx +0 -76
- package/docs-site/reference/cli.mdx +0 -120
- package/docs-site/reference/define-agent.mdx +0 -168
- package/docs-site/reference/define-config.mdx +0 -41
- package/docs-site/reference/define-eval.mdx +0 -160
- package/docs-site/reference/events.mdx +0 -131
- package/docs-site/reference/expect.mdx +0 -112
- package/docs-site/tracker.js +0 -8
- package/src/report/react/data.ts +0 -17
- package/src/view/aggregate.ts +0 -103
- package/src/view/loader.test.ts +0 -61
- 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
|
-

|
|
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)
|