niceeval 0.4.4 → 0.4.6
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/README.md +21 -12
- package/README.zh.md +8 -9
- package/docs-site/.mintignore +13 -0
- package/docs-site/AGENTS.md +46 -0
- package/docs-site/concepts/adapter.mdx +287 -0
- package/docs-site/concepts/assert.mdx +243 -0
- package/docs-site/concepts/drive.mdx +118 -0
- package/docs-site/concepts/evals.mdx +108 -0
- package/docs-site/concepts/experiment.mdx +37 -0
- package/docs-site/concepts/hitl.mdx +83 -0
- package/docs-site/concepts/judge.mdx +129 -0
- package/docs-site/concepts/overview.mdx +98 -0
- package/docs-site/concepts/tier.mdx +42 -0
- package/docs-site/docs-ref/00-index.md +23 -0
- package/docs-site/docs-ref/01-page-types.md +43 -0
- package/docs-site/docs-ref/03-style-rules.md +45 -0
- package/docs-site/docs-ref/04-code-examples.md +37 -0
- package/docs-site/docs-ref/05-dx-failure-paths.md +45 -0
- package/docs-site/docs-ref/06-checklists.md +53 -0
- package/docs-site/docs.json +256 -0
- package/docs-site/example/ai-agent-application.mdx +148 -0
- package/docs-site/example/claude-code-codex-plugin.mdx +162 -0
- package/docs-site/example/claude-code-codex-skill.mdx +147 -0
- package/docs-site/example/showcase.mdx +39 -0
- package/docs-site/example/tier1-ai-sdk-v7.mdx +136 -0
- package/docs-site/example/tier1-claude-sdk.mdx +119 -0
- package/docs-site/example/tier1-codex-sdk.mdx +111 -0
- package/docs-site/example/tier1-langgraph.mdx +119 -0
- package/docs-site/example/tier1-pi-sdk.mdx +119 -0
- package/docs-site/favicon.svg +5 -0
- package/docs-site/github-diff.css +135 -0
- package/docs-site/github-diff.js +11 -0
- package/docs-site/guides/authoring.mdx +129 -0
- package/docs-site/guides/ci-integration.mdx +97 -0
- package/docs-site/guides/connect-otel.mdx +209 -0
- package/docs-site/guides/connect-your-agent.mdx +221 -0
- package/docs-site/guides/dataset-fanout.mdx +98 -0
- package/docs-site/guides/experiments.mdx +71 -0
- package/docs-site/guides/fixtures.mdx +147 -0
- package/docs-site/guides/reporters.mdx +113 -0
- package/docs-site/guides/runner.mdx +79 -0
- package/docs-site/guides/sandbox-agent.mdx +138 -0
- package/docs-site/guides/sandbox-backends.mdx +64 -0
- package/docs-site/guides/scoring-guide.mdx +92 -0
- package/docs-site/guides/viewing-results.mdx +195 -0
- package/docs-site/guides/write-experiment.mdx +81 -0
- package/docs-site/guides/write-send.mdx +347 -0
- package/docs-site/images/adapter-tiers-zh.svg +60 -0
- package/docs-site/images/agent-turn-roundtrip-en.svg +62 -0
- package/docs-site/images/agent-turn-roundtrip-zh.svg +77 -0
- package/docs-site/images/hitl-handshake-zh.svg +89 -0
- package/docs-site/images/logo.svg +6 -0
- package/docs-site/index.mdx +181 -0
- package/docs-site/introduction.mdx +141 -0
- package/docs-site/quickstart.mdx +136 -0
- package/docs-site/reference/builtin-agents.mdx +161 -0
- package/docs-site/reference/capabilities.mdx +76 -0
- package/docs-site/reference/cli.mdx +120 -0
- package/docs-site/reference/define-agent.mdx +168 -0
- package/docs-site/reference/define-config.mdx +41 -0
- package/docs-site/reference/define-eval.mdx +160 -0
- package/docs-site/reference/events.mdx +131 -0
- package/docs-site/reference/expect.mdx +112 -0
- package/docs-site/tracker.js +8 -0
- package/docs-site/zh/concepts/adapter.mdx +286 -0
- package/docs-site/zh/concepts/assert.mdx +243 -0
- package/docs-site/zh/concepts/drive.mdx +118 -0
- package/docs-site/zh/concepts/evals.mdx +124 -0
- package/docs-site/zh/concepts/experiment.mdx +37 -0
- package/docs-site/zh/concepts/hitl.mdx +83 -0
- package/docs-site/zh/concepts/judge.mdx +130 -0
- package/docs-site/zh/concepts/overview.mdx +109 -0
- package/docs-site/zh/concepts/tier.mdx +44 -0
- package/docs-site/zh/example/ai-agent-application.mdx +152 -0
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +163 -0
- package/docs-site/zh/example/claude-code-codex-skill.mdx +147 -0
- package/docs-site/zh/example/showcase.mdx +39 -0
- package/docs-site/zh/example/tier1-ai-sdk-v7.mdx +140 -0
- package/docs-site/zh/example/tier1-claude-sdk.mdx +122 -0
- package/docs-site/zh/example/tier1-codex-sdk.mdx +117 -0
- package/docs-site/zh/example/tier1-langgraph.mdx +123 -0
- package/docs-site/zh/example/tier1-pi-sdk.mdx +122 -0
- package/docs-site/zh/guides/agent-feedback-loop.mdx +79 -0
- package/docs-site/zh/guides/authoring.mdx +129 -0
- package/docs-site/zh/guides/ci-integration.mdx +97 -0
- package/docs-site/zh/guides/connect-otel.mdx +208 -0
- package/docs-site/zh/guides/connect-your-agent.mdx +226 -0
- package/docs-site/zh/guides/dataset-fanout.mdx +83 -0
- package/docs-site/zh/guides/experiments.mdx +72 -0
- package/docs-site/zh/guides/fixtures.mdx +147 -0
- package/docs-site/zh/guides/official-adapters.mdx +132 -0
- package/docs-site/zh/guides/reporters.mdx +113 -0
- package/docs-site/zh/guides/runner.mdx +82 -0
- package/docs-site/zh/guides/sandbox-agent.mdx +139 -0
- package/docs-site/zh/guides/sandbox-backends.mdx +75 -0
- package/docs-site/zh/guides/scoring-guide.mdx +92 -0
- package/docs-site/zh/guides/viewing-results.mdx +195 -0
- package/docs-site/zh/guides/write-experiment.mdx +101 -0
- package/docs-site/zh/guides/write-send.mdx +353 -0
- package/docs-site/zh/index.mdx +180 -0
- package/docs-site/zh/introduction.mdx +140 -0
- package/docs-site/zh/quickstart.mdx +137 -0
- package/docs-site/zh/reference/builtin-agents.mdx +364 -0
- package/docs-site/zh/reference/capabilities.mdx +76 -0
- package/docs-site/zh/reference/cli.mdx +140 -0
- package/docs-site/zh/reference/define-agent.mdx +461 -0
- package/docs-site/zh/reference/define-config.mdx +107 -0
- package/docs-site/zh/reference/define-eval.mdx +674 -0
- package/docs-site/zh/reference/events.mdx +252 -0
- package/docs-site/zh/reference/expect.mdx +208 -0
- package/package.json +3 -2
- package/src/agents/bub.ts +15 -5
- package/src/agents/claude-code.ts +8 -5
- package/src/agents/codex.ts +9 -5
- package/src/agents/sdk-streams.test.ts +4 -4
- package/src/agents/sdk-streams.ts +14 -9
- package/src/agents/types.ts +40 -1
- package/src/agents/ui-message-stream.ts +1 -0
- package/src/cli.ts +36 -7
- package/src/context/types.ts +140 -0
- package/src/expect/index.ts +15 -8
- package/src/i18n/en.ts +19 -6
- package/src/i18n/zh-CN.ts +19 -6
- package/src/o11y/parsers/bub.test.ts +71 -0
- package/src/o11y/parsers/bub.ts +5 -0
- package/src/o11y/types.ts +27 -2
- package/src/runner/reporters/artifacts.ts +11 -3
- package/src/runner/reporters/live.ts +45 -8
- package/src/runner/run.ts +71 -21
- package/src/runner/types.ts +45 -2
- package/src/sandbox/types.ts +18 -0
- package/src/scoring/types.ts +5 -0
- package/src/shared/types.ts +3 -0
- package/src/util.test.ts +26 -1
- package/src/util.ts +16 -0
- package/src/view/app/App.tsx +2 -2
- package/src/view/app/components/AttemptModal.tsx +2 -0
- package/src/view/app/components/CopyControls.tsx +87 -28
- package/src/view/app/i18n.ts +3 -3
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +18 -18
- package/docs/README.md +0 -120
- package/docs/adapters/README.md +0 -60
- package/docs/adapters/authoring.md +0 -179
- package/docs/adapters/coding-agent-skills-plugins.md +0 -324
- package/docs/adapters/collection.md +0 -128
- package/docs/adapters/contract.md +0 -264
- package/docs/adapters/reference/agent-eval.md +0 -215
- package/docs/adapters/reference/agent-loop-apis.md +0 -69
- package/docs/adapters/reference/claude-code-otel-telemetry.md +0 -100
- package/docs/adapters/reference/eve-protocol.md +0 -127
- package/docs/adapters/reference/otel-genai.md +0 -107
- package/docs/adapters/reference/otel-instrumentation.md +0 -65
- package/docs/adapters/targets.md +0 -99
- package/docs/architecture.md +0 -140
- package/docs/assertions.md +0 -387
- package/docs/capabilities-by-construction.md +0 -48
- package/docs/cli.md +0 -173
- package/docs/concepts.md +0 -106
- package/docs/e2e-ci.md +0 -332
- package/docs/eval-authoring.md +0 -319
- package/docs/experiments.md +0 -157
- package/docs/getting-started.md +0 -224
- package/docs/multi-agent.md +0 -145
- package/docs/observability.md +0 -337
- package/docs/origin-integration.md +0 -195
- package/docs/references.md +0 -35
- package/docs/reports.md +0 -551
- package/docs/results-format.md +0 -228
- package/docs/results-lib.md +0 -191
- package/docs/runner.md +0 -104
- package/docs/sandbox.md +0 -276
- package/docs/scoring.md +0 -119
- package/docs/source-map.md +0 -126
- package/docs/tier-sync.md +0 -193
- package/docs/view.md +0 -194
- package/docs/vision.md +0 -88
package/README.md
CHANGED
|
@@ -12,19 +12,16 @@
|
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
-
NiceEval is an
|
|
15
|
+
NiceEval is an agent eval tool that helps teams measure, evaluate, and improve AI in production. With NiceEval, teams can compare models, iterate on agents, catch regressions, and keep improving their AI applications using real user data.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
Whether you need to evaluate coding agent plugins, Hooks, and Skills written for Claude Code / Codex, or your own AI agent application — both plug in easily.
|
|
19
|
-
|
|
20
|
-
After the eval completes, it generates readable reports and lets you view agent behavior details. Convenient for debugging and understanding agent behavior.
|
|
17
|
+
NiceEval is local-first at its core: your evals run in your own environment. When your team needs to share evals or track regressions, you can push a Report to platforms like BrainTrust, or export a custom report.
|
|
21
18
|
|
|
22
19
|
## Why NiceEval when DeepEval, LangFuse, and BrainTrust already exist
|
|
23
20
|
|
|
24
21
|
NiceEval is an Agent-Native eval tool. The Dataset / golden pattern of building an Input and an Expected Output doesn't fit real agent evaluation.
|
|
25
22
|
Agents today need to be evaluated at a finer grain — multi-turn conversations, multi-agent collaboration, tool calls, skill loading — and NiceEval does this better.
|
|
26
23
|
|
|
27
|
-
It also coexists with LangFuse and BrainTrust: use them for tracing, or upload eval results to both
|
|
24
|
+
It also coexists with LangFuse and BrainTrust: use them for tracing, or upload eval results to both.
|
|
28
25
|
|
|
29
26
|
## Architecture
|
|
30
27
|
|
|
@@ -73,6 +70,17 @@ NiceEval supports two integration modes, depending on whether the agent under te
|
|
|
73
70
|
- **Agent adapters** are the open boundary: you decide how to call the system under test.
|
|
74
71
|
- Coding agents that need filesystem isolation run inside the **Docker Sandbox**; your own AI agent can connect directly, without Docker.
|
|
75
72
|
|
|
73
|
+
## Core concepts at a glance
|
|
74
|
+
|
|
75
|
+
| Concept | In one line |
|
|
76
|
+
|---|---|
|
|
77
|
+
| Eval | A test case: written in `evals/*.eval.ts`, describing what to check. |
|
|
78
|
+
| Experiment | A checked-in run configuration: which Adapter, which model, which flags. |
|
|
79
|
+
| Adapter | The layer that connects to the system under test: implement one `send`, get back a standard event stream. |
|
|
80
|
+
| Sandbox | Only needed for coding agents that require an isolated workspace; a direct web agent doesn't need one. |
|
|
81
|
+
| Tier | Three levels of Adapter integration effort: Tier 1 wires up `send` only, Tier 2 adds OTel for a call waterfall, Tier 3 makes invasive changes for feature A/B testing. |
|
|
82
|
+
|
|
83
|
+
See the full glossary in the [architecture overview](https://niceeval.com/docs/concepts/overview).
|
|
76
84
|
|
|
77
85
|
## Example
|
|
78
86
|
|
|
@@ -126,9 +134,9 @@ READ https://niceeval.com/INIT.md and install niceeval for this repo.
|
|
|
126
134
|
|
|
127
135
|
Start from the scenario that matches what you need to evaluate:
|
|
128
136
|
|
|
129
|
-
- [Claude Code / Codex plugin eval](https://niceeval.com/docs/
|
|
130
|
-
- [Claude Code / Codex skill eval](https://niceeval.com/docs/
|
|
131
|
-
- [AI Agent application eval](https://niceeval.com/docs/
|
|
137
|
+
- [Claude Code / Codex plugin eval](https://niceeval.com/docs/example/claude-code-codex-plugin)
|
|
138
|
+
- [Claude Code / Codex skill eval](https://niceeval.com/docs/example/claude-code-codex-skill)
|
|
139
|
+
- [AI Agent application eval](https://niceeval.com/docs/example/ai-agent-application)
|
|
132
140
|
|
|
133
141
|
|
|
134
142
|
## Roadmap
|
|
@@ -157,8 +165,9 @@ Official Adapters
|
|
|
157
165
|
|
|
158
166
|
# Acknowledgements
|
|
159
167
|
This project was inspired by — or had its code learned by AI from — the projects below:
|
|
160
|
-
[eve](https://eve.dev)
|
|
161
|
-
[agent eval](https://github.com/vercel-labs/agent-eval)
|
|
162
|
-
[ponytail](https://github.com/DietrichGebert/ponytail)
|
|
168
|
+
- [eve](https://eve.dev): the main DX and API inspiration
|
|
169
|
+
- [agent eval](https://github.com/vercel-labs/agent-eval)
|
|
170
|
+
- [ponytail](https://github.com/DietrichGebert/ponytail)
|
|
163
171
|
|
|
164
172
|
Thanks to the following communities
|
|
173
|
+
- WIP
|
package/README.zh.md
CHANGED
|
@@ -12,18 +12,16 @@
|
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
-
NiceEval
|
|
15
|
+
NiceEval 是一个 Agent 评估工具,帮助团队衡量、评估并改进生产环境中的 AI。借助 NiceEval,团队可以比较模型、迭代 Agent、发现回归问题,并利用真实用户数据持续改进 AI 应用。
|
|
16
|
+
|
|
17
|
+
NiceEval 以本地优先为核心:你的评估在你自己的环境中运行。当你的团队需要分享评估、回归跟踪时,可以通过 Report 上报到 BrainTrust 等平台,或者自定义的报告导出。
|
|
16
18
|
|
|
17
|
-
通过通用式的设计,使 NiceEval 可以为几乎所有 Agent 应用进行评估。
|
|
18
|
-
无论你是需要评估为 Claude Code / Codex 写的 coding agent 插件、Hook 和 Skill,还是可以评估你自己的 AI Agent 应用都能轻松接入。
|
|
19
|
-
|
|
20
|
-
在 eval 完成之后可以生成易读的报告与查看 Agent 的行为细节。方便 Debug 与理解 Agent 行为。
|
|
21
19
|
|
|
22
20
|
## 为什么有了 DeepEval、LangFuse、BrainTrust 还需要 NiceEval
|
|
23
21
|
NiceEval 是一个 Agent-Native 的评估工具。Dataset / golden 那一套「构建 Input 与 Expected Output」的模式,并不适合真实的 Agent 评估。
|
|
24
22
|
现在 Agent 需要在多轮对话、多 agent 协作、工具调用、Skill 加载等细粒度场景下进行评估,NiceEval 能做得更好。
|
|
25
23
|
|
|
26
|
-
同时,NiceEval 也能与 LangFuse、BrainTrust 共存:可以用它们做 tracing
|
|
24
|
+
同时,NiceEval 也能与 LangFuse、BrainTrust 共存:可以用它们做 tracing,或者把评估结果上传到两者。
|
|
27
25
|
|
|
28
26
|
## 架构
|
|
29
27
|
|
|
@@ -167,8 +165,9 @@ READ https://niceeval.com/INIT.md and install niceeval for this repo.
|
|
|
167
165
|
|
|
168
166
|
# 感谢
|
|
169
167
|
该项目受下面项目所启发或者是由AI从下面项目中学习代码所写
|
|
170
|
-
[eve](https://eve.dev)
|
|
171
|
-
[agent eval](https://github.com/vercel-labs/agent-eval)
|
|
172
|
-
[ponytail](https://github.com/DietrichGebert/ponytail)
|
|
168
|
+
- [eve](https://eve.dev): 主要的 DX 与 API 受 Eve 所启发
|
|
169
|
+
- [agent eval](https://github.com/vercel-labs/agent-eval)
|
|
170
|
+
- [ponytail](https://github.com/DietrichGebert/ponytail)
|
|
173
171
|
|
|
174
172
|
感谢下列社区
|
|
173
|
+
- WIP
|
|
@@ -0,0 +1,13 @@
|
|
|
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/
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
```
|
|
@@ -0,0 +1,287 @@
|
|
|
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.
|