niceeval 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +63 -20
- 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 +10 -2
- 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/bub.ts +14 -4
- 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 +104 -14
- 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
package/docs-site/.mintignore
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Mintlify automatically ignores these files and directories:
|
|
2
|
-
# .git, .github, .claude, .agents, .idea, node_modules,
|
|
3
|
-
# README.md, LICENSE.md, CHANGELOG.md, CONTRIBUTING.md
|
|
4
|
-
|
|
5
|
-
# Draft content
|
|
6
|
-
drafts/
|
|
7
|
-
*.draft.mdx
|
|
8
|
-
|
|
9
|
-
# Scraped source content (not documentation pages)
|
|
10
|
-
sources/
|
|
11
|
-
|
|
12
|
-
# Agent-only writing reference, not part of the published docs surface
|
|
13
|
-
docs-ref/
|
package/docs-site/AGENTS.md
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# Mintlify 文档站指南
|
|
2
|
-
|
|
3
|
-
`docs-site/` 是 NiceEval 的公开 Mintlify 文档站,面向正在学习和使用 NiceEval 的用户。这里的文档要回答“用户怎么完成任务”,不是记录内部设计讨论;内部设计、取舍和源码地图放在仓库根目录的 `docs/`。
|
|
4
|
-
|
|
5
|
-
## 目录结构
|
|
6
|
-
|
|
7
|
-
- `docs.json`:Mintlify 导航、主题、logo、navbar、redirects。
|
|
8
|
-
- 顶层 `*.mdx`:英文入口页,例如 introduction、quickstart、installation。
|
|
9
|
-
- `concepts/`:英文核心概念页,解释心智模型。
|
|
10
|
-
- `guides/`:英文任务指南,按工作流组织。
|
|
11
|
-
- `reference/`:英文 API / CLI 参考,列完整字段和选项。
|
|
12
|
-
- `zh/`:中文文档。中文定位、概念命名和场景示例是公开叙事的准绳;英文页或 README 与中文冲突时,先按中文和当前代码核对,再同步其它入口。
|
|
13
|
-
|
|
14
|
-
## 术语表
|
|
15
|
-
|
|
16
|
-
- **NiceEval**:产品名。中文正文表达产品时用 `NiceEval`;命令、包名、配置文件、代码标识里用 `niceeval`。
|
|
17
|
-
- **Eval**:一个评测用例。中文可以写“Eval”或“评估用例”
|
|
18
|
-
- **Experiment**:可签入的运行配置。中文写“实验”或保留 `Experiment`,用于说明 agent、model、flags、runs、budget、sandbox 等运行维度。
|
|
19
|
-
- **Adapter**:适配器,负责连接被测系统、鉴权、调用接口、把返回翻译成标准事件流。页面标题和导航可用 `Adapter`。
|
|
20
|
-
- **Agent**:NiceEval 看到的被测对象连接。不要把 Agent 写成某个固定协议;具体协议属于 Adapter。
|
|
21
|
-
- **Sandbox**:沙箱后端,回答“在哪里隔离运行”。不要和 Adapter 混成一层。
|
|
22
|
-
- **Turn**:一次 `t.send()` / `t.respond()` 的结果。中文可写“一轮”或保留 `Turn`。
|
|
23
|
-
- **StreamEvent / events**:标准事件流,是断言和报告读取的事实来源。
|
|
24
|
-
- **HITL**:human-in-the-loop,人工介入。第一次出现时写全称或中文解释。
|
|
25
|
-
- **OTel 接入**:Tier 2 的接入方式。只在讲 `send + OTel` 时使用,不要把 OTel 写进 Tier 1。
|
|
26
|
-
- **Flags**:experiment 传入的 feature flags,经 `ctx.flags` 到 Adapter,经 `t.flags` 到 eval。不要写成 CLI flags,除非指命令行参数。
|
|
27
|
-
- **Runner**:运行器。面向用户文档里避免写 “NiceEval core”;需要表达执行主体时写 NiceEval 或 runner。
|
|
28
|
-
|
|
29
|
-
## 写作规则
|
|
30
|
-
- 英语单词应该以大写开头
|
|
31
|
-
- 只在 @docs-site/zh 下面更新中文版本,英语版本由其它 AI 翻译
|
|
32
|
-
- 新增或重命名页面时同时更新 `docs.json`,必要时加 redirect,避免旧链接断掉。
|
|
33
|
-
- 链接示例必须指向真实存在的 `examples/` 目录;当前完整示例主要在 `examples/zh/`。
|
|
34
|
-
- 工作流写成 guide,字段全集写成 reference,概念边界写成 concepts。不要把一个页面同时写成教程、设计文档和 API 字典。
|
|
35
|
-
- 命令、路径、flag、文件名、包名、代码标识用反引号。
|
|
36
|
-
- 文案使用主动语态和短句。错误信息、限制和前置条件要直接说清楚下一步。
|
|
37
|
-
- 写作指南: docs-site/docs-ref/00-index.md
|
|
38
|
-
|
|
39
|
-
## 校验
|
|
40
|
-
|
|
41
|
-
改 `docs-site/` 后,从仓库根目录运行:
|
|
42
|
-
|
|
43
|
-
```sh
|
|
44
|
-
PATH=/opt/homebrew/opt/node@22/bin:$PATH pnpm run docs:validate
|
|
45
|
-
PATH=/opt/homebrew/opt/node@22/bin:$PATH pnpm run docs:links
|
|
46
|
-
```
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "Adapter: Connect an Agent to NiceEval"
|
|
3
|
-
sidebarTitle: "Adapter"
|
|
4
|
-
description: "The Adapter is the adapter you write. This page explains what the send function receives and returns, how system-under-test configuration is passed in, the three integration tiers, and where the capabilities on t come from."
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
In NiceEval, an `Adapter` is the adaptation layer between the runner and the system under test. The Adapter implements the `send` function: it forwards eval-side actions such as `t.send()` to your application, then translates the application's response into NiceEval's standard `Turn`.
|
|
8
|
-
|
|
9
|
-
Follow-up reading: [Write Send](/guides/write-send)
|
|
10
|
-
|
|
11
|
-
## The adapter
|
|
12
|
-
|
|
13
|
-
If the system under test speaks the standard OpenAI Chat Completions or Responses protocol, use an official adapter directly. For a homegrown frontend-backend protocol (HTTP, gRPC, WebSocket — anything works), write an Adapter: it knows how to authenticate, how to call your application, and how to translate the response into the standard event stream.
|
|
14
|
-
|
|
15
|
-
One full round trip of `t.send()` crosses this boundary twice:
|
|
16
|
-
|
|
17
|
-

|
|
18
|
-
|
|
19
|
-
Outbound: the driving verbs in the eval (`t.send` / `t.sendFile` / `t.respond`) assemble a `TurnInput` and the `ctx` for this conversation line, then call the Adapter's `send` once.
|
|
20
|
-
|
|
21
|
-
Inbound: the Adapter translates the application's raw response into a `Turn` — where `events` is an array of event objects ordered by when they happened (real values below).
|
|
22
|
-
|
|
23
|
-
The system under test's URL, auth, and protocol details are passed in through the Adapter factory's configuration. `defineExperiment` receives an already configured agent instance; the Adapter consumes that configuration inside the `send` closure, and forwards per-turn dynamic values (such as `ctx.model`, `ctx.flags`, `ctx.telemetry`) to the application with each request:
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
// agents/web-agent.ts
|
|
27
|
-
import { defineAgent } from "niceeval/adapter";
|
|
28
|
-
import type { Agent } from "niceeval/adapter";
|
|
29
|
-
|
|
30
|
-
interface WebAgentOptions {
|
|
31
|
-
baseUrl: string;
|
|
32
|
-
apiKey?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function webAgent(options: WebAgentOptions): Agent {
|
|
36
|
-
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
37
|
-
|
|
38
|
-
return defineAgent({
|
|
39
|
-
name: "web-agent",
|
|
40
|
-
async send(input, ctx) {
|
|
41
|
-
const response = await fetch(`${baseUrl}/api/turn`, {
|
|
42
|
-
method: "POST",
|
|
43
|
-
headers: {
|
|
44
|
-
"content-type": "application/json",
|
|
45
|
-
...(options.apiKey ? { authorization: `Bearer ${options.apiKey}` } : {}),
|
|
46
|
-
...ctx.telemetry?.headers,
|
|
47
|
-
},
|
|
48
|
-
body: JSON.stringify({
|
|
49
|
-
message: input.text,
|
|
50
|
-
files: input.files,
|
|
51
|
-
model: ctx.model,
|
|
52
|
-
flags: ctx.flags,
|
|
53
|
-
}),
|
|
54
|
-
signal: ctx.signal,
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
if (!response.ok) {
|
|
58
|
-
return {
|
|
59
|
-
status: "failed",
|
|
60
|
-
events: [{ type: "error", message: `HTTP ${response.status}` }],
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const body = await response.json();
|
|
65
|
-
return {
|
|
66
|
-
status: "completed",
|
|
67
|
-
events: body.events,
|
|
68
|
-
data: body.data,
|
|
69
|
-
usage: body.usage,
|
|
70
|
-
};
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// experiments/staging.ts
|
|
76
|
-
import { defineExperiment } from "niceeval";
|
|
77
|
-
import { webAgent } from "../agents/web-agent.ts";
|
|
78
|
-
|
|
79
|
-
export default defineExperiment({
|
|
80
|
-
agent: webAgent({
|
|
81
|
-
baseUrl: "https://staging.example.com",
|
|
82
|
-
apiKey: process.env.STAGING_AGENT_API_KEY,
|
|
83
|
-
}),
|
|
84
|
-
model: "gpt-5.4",
|
|
85
|
-
flags: { promptVariant: "concise" },
|
|
86
|
-
});
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Integration tiers
|
|
90
|
-
|
|
91
|
-
Grouped by where the Adapter connects and what extra observability data it receives, integration comes in three tiers: **Tier 1 is send only** (not a single line of application code changes, and the full assertion set is already complete at this tier), **Tier 2 is send + OTel** (the application sends spans to NiceEval, buying the call waterfall in `niceeval view`), **Tier 3 is application changes + experiment flags** (feature A/B). What each tier costs, what it buys, and when to move up: see [Tier](/concepts/tier).
|
|
92
|
-
|
|
93
|
-
The eval-side driving API — `t.send()`, `t.sendFile()`, `t.newSession()`, and HITL's `t.respond()` / `t.respondAll()` —
|
|
94
|
-
all uniformly call the Adapter's `send`.
|
|
95
|
-
|
|
96
|
-
How they converge and how to handle them in `send`: see [Write Send](/guides/write-send).
|
|
97
|
-
|
|
98
|
-
## The send function
|
|
99
|
-
|
|
100
|
-
Whether the Adapter connects to an HTTP service or a CLI in a sandbox, the interface exposed to the runner is exactly the same:
|
|
101
|
-
|
|
102
|
-
```ts
|
|
103
|
-
interface Agent {
|
|
104
|
-
readonly name: string;
|
|
105
|
-
send(input: TurnInput, ctx: AgentContext): Promise<Turn>;
|
|
106
|
-
// Optional: setup / teardown (once-per-sandbox lifecycle);
|
|
107
|
-
// optional observability members are not part of send — see "OTel Integration"
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
`send` is the only function you must implement. There are only three types in its signature; take them one at a time.
|
|
112
|
-
|
|
113
|
-
### Input: `TurnInput`
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
interface TurnInput {
|
|
117
|
-
readonly text: string; // Every turn: the text being sent; on answer turns it's the answer text (multiple answers joined by newlines)
|
|
118
|
-
readonly files?: readonly InputFile[]; // t.sendFile turns only: attached files (base64)
|
|
119
|
-
readonly responses?: readonly InputResponse[]; // Answer turns only (t.respond / t.respondAll): one structured answer per request
|
|
120
|
-
readonly outputSchema?: JsonSchema; // Only when this turn declares output: the structured output requirement, lowered to JSON Schema
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
interface InputFile {
|
|
124
|
-
readonly filename?: string; // Optional, for the adapter / model to reference
|
|
125
|
-
readonly mimeType: string; // e.g. image/png
|
|
126
|
-
readonly dataBase64: string; // base64 content, JSON-friendly, can go straight into the request body
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
interface InputResponse {
|
|
130
|
-
readonly requestId: string; // Always present: which input.requested request this answers; used to match up when multiple requests are paused
|
|
131
|
-
readonly optionId?: string; // Either this or text: the answer hit one of the request's option ids (approve / deny...)
|
|
132
|
-
readonly text?: string; // Either this or optionId: a free-text answer (the request has no options, or the answer matches none)
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Whether the eval side calls `t.send()`, `t.sendFile()`, or `t.respond()`, it arrives at the Adapter as one ordinary `send` — the entire difference is in the fields:
|
|
137
|
-
|
|
138
|
-
If your application interface does not accept files, ignore `files`.
|
|
139
|
-
|
|
140
|
-
| Eval side | `text` | `files` | `responses` | `outputSchema` |
|
|
141
|
-
|---|---|---|---|---|
|
|
142
|
-
| `t.send(text)` | Sent text | — | — | Only when `output` is declared |
|
|
143
|
-
| `t.sendFile(path, text?)` | Caption (may be empty string) | Present | — | Same |
|
|
144
|
-
| `t.respond(...)` / `t.respondAll(...)` | Answer text | — | One per request | Same |
|
|
145
|
-
|
|
146
|
-
`files`: ignore it if the interface does not accept files;
|
|
147
|
-
`outputSchema`: forward it if the application interface accepts a schema (Chat Completions' `response_format`, Responses' `text.format`); it's fine if it doesn't — validation happens in the runner anyway (see the `Turn.data` rule below).
|
|
148
|
-
|
|
149
|
-
#### Inputs for the different answers
|
|
150
|
-
|
|
151
|
-
On HITL answer turns, the human verdict arrives in structured form via `input.responses` — the Adapter never needs to parse `text` to guess which sentence answers which request or whether it counts as approval. Every answer carries a `requestId`; `optionId` and `text` are mutually exclusive: if the answer hits one of the request's option ids it becomes `optionId` (the eval side has already validated it exists, so no typo can slip through silently); otherwise the whole sentence lands in `text` as free text. The four typical shapes:
|
|
152
|
-
|
|
153
|
-
```ts
|
|
154
|
-
// 1) A single pending request, answer hits an option (approve / deny work the same, only the optionId differs)
|
|
155
|
-
await t.respond("approve");
|
|
156
|
-
// → { text: "approve",
|
|
157
|
-
// responses: [{ requestId: "req_1", optionId: "approve" }] }
|
|
158
|
-
|
|
159
|
-
// 2) Multiple requests paused at once — the object form matches them up explicitly
|
|
160
|
-
await t.respond({ request, optionId: "deny" });
|
|
161
|
-
// → { text: "deny",
|
|
162
|
-
// responses: [{ requestId: request.id, optionId: "deny" }] }
|
|
163
|
-
|
|
164
|
-
// 3) The answer is none of the options → free text (requests waiting for extra info look like this)
|
|
165
|
-
await t.respond("Change the recipient to ceo@corp.com");
|
|
166
|
-
// → { text: "Change the recipient to ceo@corp.com",
|
|
167
|
-
// responses: [{ requestId: "req_1", text: "Change the recipient to ceo@corp.com" }] }
|
|
168
|
-
|
|
169
|
-
// 4) respondAll: one answer per pending request, same optionId
|
|
170
|
-
await t.respondAll("approve");
|
|
171
|
-
// → { text: "approve\napprove",
|
|
172
|
-
// responses: [{ requestId: "req_1", optionId: "approve" },
|
|
173
|
-
// { requestId: "req_2", optionId: "approve" }] }
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
The Adapter's corresponding obligations: hand the verdict back to the application by `requestId` (don't guess by order); for calls a human rejected, set the `action.result` event's `status` to `"rejected"` rather than `"failed"` — rejection is a human decision, not a tool failure, so `noFailedActions()` doesn't misfire and `calledTool(..., { status: "rejected" })` can assert it precisely.
|
|
177
|
-
|
|
178
|
-
### Context: `AgentContext`
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
interface AgentContext {
|
|
182
|
-
// The three that are present every turn
|
|
183
|
-
readonly signal: AbortSignal; // The runner's timeout and cancellation: pass it through to every request you make
|
|
184
|
-
readonly session: AgentSession; // The state slot for this conversation line
|
|
185
|
-
log(msg: string): void; // Writes to this turn's log, visible in niceeval view
|
|
186
|
-
|
|
187
|
-
// The three that appear per integration tier (see "Integration tiers")
|
|
188
|
-
readonly model?: string; // Tier 1 model comparison: experiment.model passed through; forward it if the application interface accepts model selection
|
|
189
|
-
readonly reasoningEffort?: string; // Reasoning effort: experiment.reasoningEffort passed through, same ownership as model
|
|
190
|
-
readonly telemetry?: Telemetry; // Tier 2: this turn's W3C trace context (headers, a fresh traceparent per turn)
|
|
191
|
-
readonly flags: Readonly<Record<string, unknown>>; // Tier 3 feature A/B: experiment.flags passed through; {} when unset
|
|
192
|
-
|
|
193
|
-
readonly sandbox?: Sandbox; // Only Agents constructed by defineSandboxAgent receive this
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
interface AgentSession {
|
|
197
|
-
// Session continuation: server keeps the history (the shape for interfaces that accept a session id)
|
|
198
|
-
readonly id?: string; // The session id recorded for this line; undefined on a new conversation line
|
|
199
|
-
capture(id: string | undefined): void; // Record the returned id; only lands if none has been recorded yet
|
|
200
|
-
|
|
201
|
-
// Session continuation: client carries the full history (Chat Completions shape)
|
|
202
|
-
history<TMsg>(): { get(): TMsg[]; commit(messages: TMsg[]): void };
|
|
203
|
-
|
|
204
|
-
// HITL: store and retrieve the paused-turn state (take clears on read — consumed once)
|
|
205
|
-
hold<T>(state: T): void;
|
|
206
|
-
take<T>(): T | undefined;
|
|
207
|
-
|
|
208
|
-
// Escape hatch: a free-form state slot, starts as {}, the framework never writes to it
|
|
209
|
-
readonly state: Record<string, unknown>;
|
|
210
|
-
}
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
There are no flags to check on `ctx`. All three tier fields have "pass-through" semantics: `model` and `flags` are declared by the experiment and handed over verbatim by the runner — the Adapter's only job is to forward them to the application with the request, never to interpret them; `telemetry` only appears when OTel integration is configured, and inside `send` you only need to spread its `headers` into the request headers — the receiving endpoint is fixed in `defineConfig` and pointed at by the application at startup, not passed from here; see [OTel Integration](/guides/connect-otel).
|
|
214
|
-
|
|
215
|
-
`session` is the private state of one conversation line, and NiceEval promises exactly one thing about it: **every `send` on the same conversation line receives the same `ctx.session`, and a new conversation line (the first turn of an eval, or after `t.newSession()`) receives a brand-new one.** The session continuation accessors (`id`/`capture`, `history`) and the HITL paused-turn accessors (`hold`/`take`) all live on it, and "the first turn" is simply the natural shape of a new conversation line — `id` is `undefined`, `history.get()` is an empty array, with no branch to write; `state` is the escape hatch beyond those accessors, and the framework never writes data into it.
|
|
216
|
-
|
|
217
|
-
### Return: `Turn`
|
|
218
|
-
|
|
219
|
-
```ts
|
|
220
|
-
interface Turn {
|
|
221
|
-
readonly events: StreamEvent[]; // ★ The standard event stream — every Adapter's core product
|
|
222
|
-
readonly data?: JsonValue; // This turn's structured output; only filled when input.outputSchema is present, validated by the runner against the declaration
|
|
223
|
-
readonly status: "completed" | "failed" | "waiting"; // waiting = paused for a human (HITL)
|
|
224
|
-
readonly usage?: Usage;
|
|
225
|
-
}
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
`events` is just a **plain JS array**: one object per thing that happened this turn, in the order it actually happened. For a turn like "How warm is Beijing today?", where the agent checks the weather before answering, `events` looks like this:
|
|
229
|
-
|
|
230
|
-
```ts
|
|
231
|
-
[
|
|
232
|
-
{ type: "action.called", callId: "c1", name: "get_weather", input: { city: "Beijing" } },
|
|
233
|
-
{ type: "action.result", callId: "c1", output: { temp: 21 }, status: "completed" },
|
|
234
|
-
{ type: "message", role: "assistant", text: "Beijing is 21°C today." },
|
|
235
|
-
]
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
There are nine object types in total (`message`, `action.*`, `input.requested`… full list in the [Events Reference](/reference/events)). Assertions read this exact array: `t.calledTool("get_weather")` counts `action.called`, `t.reply` takes the last assistant `message`. Note that each `send` returns only **this turn's** array — stitching turns into a whole conversation line is the runner's job, covered in the next section. In most cases you don't hand-write these objects either: an official converter's return value is already a complete `Turn` with `events`, `usage`, and `status` filled in — how to pick a converter is covered in [Write Send](/guides/write-send).
|
|
239
|
-
|
|
240
|
-
`data` is not a "put whatever" pocket; it has exactly one rule: **what goes in it is declared by the eval with a schema on the send; only a declaration produces data, and what you get is the declared type.**
|
|
241
|
-
|
|
242
|
-
```ts
|
|
243
|
-
// Eval side: the output declaration is both the requirement given to the application and the type source for turn.data
|
|
244
|
-
const turn = await t.send("What's the total on this invoice?", {
|
|
245
|
-
output: z.object({ amount: z.number(), currency: z.string() }),
|
|
246
|
-
});
|
|
247
|
-
turn.data.amount; // Typed as number — not unknown, no manual narrowing
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
The declaration, lowered to JSON Schema, reaches the Adapter via `input.outputSchema`; the Adapter puts the structured output from the application's response into `data` (e.g. `JSON.parse(reply.content)`). **Validation is enforced by the runner**: if `data` doesn't match the declaration, the turn fails immediately with the diff reported — there is no path where "a wrong object got stuffed in and the assertion silently passed". Conversely, turns with no `output` declaration have no `data`: applications that only return text never touch this field — don't copy the raw response body into it to fill space.
|
|
251
|
-
|
|
252
|
-
### After send returns: where the four Turn fields go
|
|
253
|
-
|
|
254
|
-
The moment `send` returns, the Adapter's work is done — everything after that is the runner's job. Each of the four fields has a definite destination:
|
|
255
|
-
|
|
256
|
-
| Turn field | What the runner does with it | Where the eval author feels it |
|
|
257
|
-
|---|---|---|
|
|
258
|
-
| `events` | Appended to this conversation line's event stream; `deriveRunFacts` folds it into facts (`toolCalls`, `parked`, `messageCount`…); the last assistant `message` also updates `t.reply` | All scoped assertions: `t.calledTool()`, `t.messageIncludes()`…; `t.events` is directly queryable |
|
|
259
|
-
| `status` | Records the turn's outcome; on `"waiting"`, collects this turn's `input.requested` into the pending list | `t.succeeded()` / `t.parked()`; after `"waiting"`, continue with `t.respond()` |
|
|
260
|
-
| `usage` | Accumulated per turn into the conversation line and the whole run | `maxTokens` / `maxCost` scorers, usage in reports |
|
|
261
|
-
| `data` | Validated against the `output` schema declared on the send — mismatch fails the turn immediately; saved only if it passes | `turn.data` (strongly typed by the declaration), `outputEquals` |
|
|
262
|
-
|
|
263
|
-
The handle you get from `await t.send()` is a view onto this one Turn: turn-level assertions (things like `draft.parked()`) look only at this turn's `events`, while `t`-level assertions look at the whole conversation line's accumulation. So the Adapter never needs to "cooperate" with any assertion — fill in these four fields correctly, and assertions, scoring, and reporting all happen automatically downstream.
|
|
264
|
-
|
|
265
|
-
## Where capabilities come from: proof by construction, not a questionnaire
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
| What unlocks on `t` | What the evidence is | What you write |
|
|
269
|
-
|---|---|---|
|
|
270
|
-
| `t.send`, `t.sendFile`, `t.check`, `t.judge`… | Every Agent has them | Nothing |
|
|
271
|
-
| Multiple `t.send()`, `t.reply`, `t.newSession()` | `send` wired up `ctx.session`'s continuation accessors (`history()` or `id` + `capture()`) — without them every turn is a fresh conversation | Nothing — the accessors are right on `ctx` |
|
|
272
|
-
| `t.respond()` / `t.parked()` and other HITL | send has returned `"waiting"` + an `input.requested` event | Nothing — doing it is having it |
|
|
273
|
-
| `t.calledTool()` / `t.toolOrder()` and other positive assertions | `action.*` present in events | Nothing — having events means you can assert |
|
|
274
|
-
| `t.notCalledTool()` / `t.usedNoTools()` and other negative assertions being **trustworthy** | Events come from an official converter with a completeness proof | Nothing — the proof travels with the converter's return value |
|
|
275
|
-
| `t.sandbox`, `t.sandbox.fileChanged()`, etc. | Constructed by `defineSandboxAgent` | Nothing |
|
|
276
|
-
| `EvalResult.trace`, the `niceeval view` waterfall | A `tracing` block exists (spans only feed the waterfall, never assertions) | Nothing — you were writing the block anyway |
|
|
277
|
-
|
|
278
|
-
The negative-assertion row is the only tiered one: `notCalledTool` asserts "it did not happen", which presupposes "the events are complete" — and that only holds when the source itself carries a completeness contract (SDK-native event stream pass-through, AI SDK's `result.steps`, Responses' `output`). Hand-written mappings carry no proof, so negative assertions warn at runtime that they are untrustworthy instead of silently passing. This also means there is no "declared but can't deliver" drift: trustworthiness follows where the events came from, not the author's self-assessment. The precise per-capability obligations are in the [Capabilities Reference](/reference/capabilities).
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
## Related reading
|
|
282
|
-
|
|
283
|
-
- [Write Send](/guides/write-send) — The hands-on tutorial: from sending one message to a full integration, seven steps, each unlocking a set of assertions.
|
|
284
|
-
- [Connect Your Agent](/guides/connect-your-agent) — The integration overview: minimal integration, parameter channels, and the incremental map.
|
|
285
|
-
- [Drive](/concepts/drive) — The eval-side view: how to use `t.send()`, `t.newSession()`, and HITL.
|
|
286
|
-
- [Assert](/concepts/assert) — The complete assertion vocabulary driven by the standard event stream.
|
|
287
|
-
- [Architecture Overview](/concepts/overview) — The four layers and their boundaries.
|
|
@@ -1,243 +0,0 @@
|
|
|
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.
|