niceeval 0.2.0 → 0.3.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/README.md +34 -34
- package/README.zh.md +18 -15
- package/docs/architecture.md +20 -20
- package/docs/view.md +5 -2
- package/package.json +17 -7
- package/src/cli.ts +2 -2
- package/src/i18n/en.ts +8 -3
- package/src/i18n/zh-CN.ts +7 -3
- package/src/runner/reporters/console.ts +3 -3
- package/src/runner/reporters/live.ts +17 -5
- package/src/runner/reporters/shared.ts +3 -0
- package/src/runner/reporters/table.ts +1 -0
- package/src/runner/run.ts +2 -0
- package/src/runner/types.ts +3 -1
- package/src/shared/format.ts +8 -2
- package/src/view/app/components/AttemptModal.tsx +9 -2
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/LazyArtifact.tsx +2 -1
- package/src/view/app/i18n.ts +3 -0
- package/src/view/app/lib/artifact-url.ts +6 -0
- package/src/view/client-dist/app.js +16 -16
- package/src/view/index.ts +41 -6
- package/src/view/server.ts +7 -0
package/README.md
CHANGED
|
@@ -12,13 +12,17 @@
|
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
-
NiceEval is an
|
|
15
|
+
NiceEval is an Agent-Native eval tool inspired by [eve](https://eve.dev), built for the best possible DX.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Thanks to its universal design, NiceEval can evaluate almost any agent application.
|
|
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.
|
|
18
21
|
|
|
19
22
|
## Why NiceEval when DeepEval, LangFuse, and BrainTrust already exist
|
|
20
23
|
|
|
21
|
-
NiceEval is an
|
|
24
|
+
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
|
+
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.
|
|
22
26
|
|
|
23
27
|
It also coexists with LangFuse and BrainTrust: use them for tracing, or upload eval results to both (in progress).
|
|
24
28
|
|
|
@@ -32,19 +36,19 @@ NiceEval supports two integration modes, depending on whether the agent under te
|
|
|
32
36
|
evals/*.eval.ts
|
|
33
37
|
│
|
|
34
38
|
▼
|
|
35
|
-
|
|
36
|
-
│
|
|
37
|
-
|
|
39
|
+
┌────────────┐
|
|
40
|
+
│ NiceEval │
|
|
41
|
+
└────────────┘
|
|
38
42
|
│
|
|
39
43
|
│ Agent adapter (official)
|
|
40
44
|
▼
|
|
41
|
-
|
|
42
|
-
│
|
|
43
|
-
│
|
|
44
|
-
│
|
|
45
|
-
│
|
|
46
|
-
│
|
|
47
|
-
|
|
45
|
+
┌────────────────────────────────┐
|
|
46
|
+
│ Docker Sandbox │
|
|
47
|
+
│ ┌────────────────────────┐ │
|
|
48
|
+
│ │ Codex / Claude Code │ │
|
|
49
|
+
│ │ apps needing isolation │ │
|
|
50
|
+
│ └────────────────────────┘ │
|
|
51
|
+
└────────────────────────────────┘
|
|
48
52
|
```
|
|
49
53
|
|
|
50
54
|
**Mode 2: Direct — connect straight to your own AI Agent**
|
|
@@ -53,18 +57,16 @@ NiceEval supports two integration modes, depending on whether the agent under te
|
|
|
53
57
|
evals/*.eval.ts
|
|
54
58
|
│
|
|
55
59
|
▼
|
|
56
|
-
|
|
57
|
-
│
|
|
58
|
-
|
|
60
|
+
┌────────────┐
|
|
61
|
+
│ NiceEval │
|
|
62
|
+
└────────────┘
|
|
59
63
|
│
|
|
60
64
|
│ Agent adapter (official, or your own implementation)
|
|
61
65
|
▼
|
|
62
|
-
|
|
63
|
-
│
|
|
64
|
-
│ (AI SDK·LangGraph·Pi
|
|
65
|
-
|
|
66
|
-
│ no Docker needed) │
|
|
67
|
-
└──────────────────────────────┘
|
|
66
|
+
┌──────────────────────────┐
|
|
67
|
+
│ your own AI Agent │
|
|
68
|
+
│ (AI SDK·LangGraph·Pi) │
|
|
69
|
+
└──────────────────────────┘
|
|
68
70
|
```
|
|
69
71
|
|
|
70
72
|
- **NiceEval core** owns discovery, scheduling, scoring, reporting, and artifacts.
|
|
@@ -74,8 +76,6 @@ NiceEval supports two integration modes, depending on whether the agent under te
|
|
|
74
76
|
|
|
75
77
|
## Example
|
|
76
78
|
|
|
77
|
-
Running an eval takes two files: the eval itself (what to check) and an experiment (which agent to run it against). The CLI won't run a bare eval id — the experiment in `niceeval exp <experiment> <eval prefix>` is what picks the system under test. Here's a real eval against a directly-connected web agent (full project in [`examples/zh/ai-sdk/`](examples/zh/ai-sdk/)), checking that the agent calls a tool for live weather questions and answers from the tool result instead of making it up:
|
|
78
|
-
|
|
79
79
|
```ts
|
|
80
80
|
// evals/eval-tool-call.eval.ts
|
|
81
81
|
import { defineEval } from "niceeval";
|
|
@@ -109,16 +109,15 @@ import { webAgent } from "./adapter"; // your agent adapter, pointed at the syst
|
|
|
109
109
|
|
|
110
110
|
export default defineExperiment({
|
|
111
111
|
agent: webAgent({ baseUrl: "http://127.0.0.1:5188" }),
|
|
112
|
+
model: "gpt-5.5"
|
|
112
113
|
});
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
```sh
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
pnpm exec niceeval exp local eval-tool-call // run only eval-tool-call under the local experiment
|
|
118
|
+
pnpm exec niceeval view // view eval results
|
|
118
119
|
```
|
|
119
120
|
|
|
120
|
-
For coding agents that need an isolated workspace (Codex, Claude Code plugins/skills), see [`examples/zh/coding-agent-skill/`](examples/zh/coding-agent-skill/): evals there use `t.sandbox.uploadDirectory()` to seed the workspace, `t.fileChanged()` / `t.file()` to check what changed, and `t.sandbox.runCommand()` to run tests.
|
|
121
|
-
|
|
122
121
|
## Quick Start
|
|
123
122
|
|
|
124
123
|
```text
|
|
@@ -135,19 +134,20 @@ Start from the scenario that matches what you need to evaluate:
|
|
|
135
134
|
## Roadmap
|
|
136
135
|
Official Adapters
|
|
137
136
|
- [ ] Agent Software
|
|
138
|
-
- [
|
|
139
|
-
- [
|
|
140
|
-
- [
|
|
137
|
+
- [x] Claude Code
|
|
138
|
+
- [x] Codex
|
|
139
|
+
- [x] Bub
|
|
141
140
|
- [ ] OpenClaw
|
|
142
141
|
- [ ] Hermess Agent
|
|
143
142
|
- [ ] Alma
|
|
144
143
|
- [ ] ...
|
|
145
144
|
|
|
146
145
|
- [ ] Agent Frameworks
|
|
147
|
-
- [
|
|
146
|
+
- [x] AI SDK
|
|
147
|
+
- [x] Claude SDK
|
|
148
|
+
- [x] Codex SDK
|
|
149
|
+
- [x] Pi Agent SDK
|
|
148
150
|
- [ ] LangGraph
|
|
149
|
-
- [ ] Claude SDK
|
|
150
|
-
- [ ] Codex SDK
|
|
151
151
|
- [ ] vm0
|
|
152
152
|
- [ ] Cursor Agent SDK
|
|
153
153
|
|
package/README.zh.md
CHANGED
|
@@ -12,12 +12,16 @@
|
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
-
NiceEval 是一个受 [eve](https://eve.dev) 启发的
|
|
15
|
+
NiceEval 是一个受 [eve](https://eve.dev) 启发的 Agent-Native eval 工具,追求极致的DX。
|
|
16
|
+
|
|
17
|
+
通过通用式的设计,使 NiceEval 可以为几乎所有 Agent 应用进行评估。
|
|
18
|
+
无论你是需要评估为 Claude Code / Codex 写的 coding agent 插件、Hook 和 Skill,还是可以评估你自己的 AI Agent 应用都能轻松接入。
|
|
16
19
|
|
|
17
20
|
在 eval 完成之后可以生成易读的报告与查看 Agent 的行为细节。方便 Debug 与理解 Agent 行为。
|
|
18
21
|
|
|
19
22
|
## 为什么有了 DeepEval、LangFuse、BrainTrust 还需要 NiceEval
|
|
20
|
-
NiceEval 是一个
|
|
23
|
+
NiceEval 是一个 Agent-Native 的评估工具。Dataset / golden 那一套「构建 Input 与 Expected Output」的模式,并不适合真实的 Agent 评估。
|
|
24
|
+
现在 Agent 需要在多轮对话、多 agent 协作、工具调用、Skill 加载等细粒度场景下进行评估,NiceEval 能做得更好。
|
|
21
25
|
|
|
22
26
|
同时,NiceEval 也能与 LangFuse、BrainTrust 共存:可以用它们做 tracing,或者把评估结果上传到两者(这部分功能还在开发中)。
|
|
23
27
|
|
|
@@ -40,8 +44,8 @@ NiceEval 支持两种接入方式,取决于被测 agent 是否需要隔离的
|
|
|
40
44
|
┌──────────────────────────────┐
|
|
41
45
|
│ Docker Sandbox │
|
|
42
46
|
│ ┌────────────────────────┐ │
|
|
43
|
-
│ │ Codex / Claude Code
|
|
44
|
-
│ │ 需要隔离文件系统的应用
|
|
47
|
+
│ │ Codex / Claude Code │ │
|
|
48
|
+
│ │ 需要隔离文件系统的应用 │ │
|
|
45
49
|
│ └────────────────────────┘ │
|
|
46
50
|
└──────────────────────────────┘
|
|
47
51
|
```
|
|
@@ -59,9 +63,8 @@ NiceEval 支持两种接入方式,取决于被测 agent 是否需要隔离的
|
|
|
59
63
|
│ Agent 适配器(官方,或者自己实现)
|
|
60
64
|
▼
|
|
61
65
|
┌──────────────────────────────┐
|
|
62
|
-
│ 你自己的 AI Agent
|
|
63
|
-
│ (AI SDK·LangGraph·Pi
|
|
64
|
-
│ agent 框架,无需 Docker) │
|
|
66
|
+
│ 你自己的 AI Agent │
|
|
67
|
+
│ (AI SDK·LangGraph·Pi ) │
|
|
65
68
|
└──────────────────────────────┘
|
|
66
69
|
```
|
|
67
70
|
|
|
@@ -72,8 +75,6 @@ NiceEval 支持两种接入方式,取决于被测 agent 是否需要隔离的
|
|
|
72
75
|
|
|
73
76
|
## 示例
|
|
74
77
|
|
|
75
|
-
跑一条 eval 需要两个文件:eval 本身(测什么)和 experiment(跑哪个 agent)。CLI 不接受裸 eval id——`niceeval exp <experiment> <eval 前缀>` 里的 experiment 才是决定「连哪个被测对象」的地方。下面是一个直连 AI Agent 的真实场景(完整项目见 [`examples/zh/ai-sdk/`](examples/zh/ai-sdk/)),验证 agent 遇到实时天气问题时会调用工具、并基于工具结果作答,而不是凭空编造:
|
|
76
|
-
|
|
77
78
|
```ts
|
|
78
79
|
// evals/eval-tool-call.eval.ts
|
|
79
80
|
import { defineEval } from "niceeval";
|
|
@@ -107,6 +108,7 @@ import { webAgent } from "./adapter"; // 你自己写的 agent adapter,接被
|
|
|
107
108
|
|
|
108
109
|
export default defineExperiment({
|
|
109
110
|
agent: webAgent({ baseUrl: "http://127.0.0.1:5188" }),
|
|
111
|
+
model: "gpt-5.5"
|
|
110
112
|
});
|
|
111
113
|
```
|
|
112
114
|
|
|
@@ -131,19 +133,20 @@ READ https://niceeval.com/INIT.md and install niceeval for this repo.
|
|
|
131
133
|
## Roadmap
|
|
132
134
|
官方适配器
|
|
133
135
|
- [ ] Agent 软件
|
|
134
|
-
- [
|
|
135
|
-
- [
|
|
136
|
-
- [
|
|
136
|
+
- [x] Claude Code
|
|
137
|
+
- [x] Codex
|
|
138
|
+
- [x] Bub
|
|
137
139
|
- [ ] OpenClaw
|
|
138
140
|
- [ ] Hermess Agent
|
|
139
141
|
- [ ] Alma
|
|
140
142
|
- [ ] ...
|
|
141
143
|
|
|
142
144
|
- [ ] Agent 框架
|
|
143
|
-
- [
|
|
145
|
+
- [x] AI SDK
|
|
146
|
+
- [x] Claude SDK
|
|
147
|
+
- [x] Codex SDK
|
|
148
|
+
- [x] Pi Agent SDK
|
|
144
149
|
- [ ] LangGraph
|
|
145
|
-
- [ ] Claude SDK
|
|
146
|
-
- [ ] Codex SDK
|
|
147
150
|
- [ ] vm0
|
|
148
151
|
- [ ] Cursor Agent SDK
|
|
149
152
|
|
package/docs/architecture.md
CHANGED
|
@@ -7,26 +7,26 @@ niceeval 把一个评测过程拆成四段职责:**发现**要跑什么、**驱
|
|
|
7
7
|
## 系统总览
|
|
8
8
|
|
|
9
9
|
```text
|
|
10
|
-
|
|
11
|
-
evals/ │
|
|
12
|
-
└─ *.eval.ts ──────────►│
|
|
13
|
-
│
|
|
14
|
-
│
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
│
|
|
20
|
-
│
|
|
21
|
-
│
|
|
22
|
-
│
|
|
23
|
-
│
|
|
24
|
-
│
|
|
25
|
-
│
|
|
26
|
-
│
|
|
27
|
-
│
|
|
28
|
-
│
|
|
29
|
-
|
|
10
|
+
┌─────────────────────────── core ───────────────────────────┐
|
|
11
|
+
evals/ │ │
|
|
12
|
+
└─ *.eval.ts ──────────►│ Discovery ──► Runner ──► Scoring ──► Outcome ──► Reporters │
|
|
13
|
+
│ (发现) (调度) (评分) (判决) (报告) │
|
|
14
|
+
│ │ 驱动 │ │
|
|
15
|
+
└──────────────┼──────────────────────────────────┼──────────┘
|
|
16
|
+
│ ▼
|
|
17
|
+
▼ 对接口分发,不按名字分支 .niceeval/<run>/
|
|
18
|
+
┌────── Agent(自实现 Adapter) ──────┐
|
|
19
|
+
│ 远程 adapter 沙箱 adapter │
|
|
20
|
+
│ (你的服务) ┌───────────────┐ │
|
|
21
|
+
│ │ claude-code │ │
|
|
22
|
+
│ │ codex / bub │ │
|
|
23
|
+
│ └───────────────┘ │
|
|
24
|
+
│ ▼ │
|
|
25
|
+
│ ┌─── Sandbox ───┐ │
|
|
26
|
+
│ │ docker │ │
|
|
27
|
+
│ │ vercel / 三方 │ │
|
|
28
|
+
│ └───────────────┘ │
|
|
29
|
+
└───────────────────────────────────┘
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
四段职责是**单向数据流**:发现产出一批 `Eval`,运行器逐个对 Agent `send` 得到 `Turn`,评分器把 `Turn` 折叠成 `Assertion[]`,再把全部断言折叠成一个互斥的 `Outcome`;报告器消费 `Outcome` + 工件。没有反向耦合 —— 评分器不知道 agent 的 transport 是 HTTP 还是沙箱 CLI,它只看 `Turn`。
|
package/docs/view.md
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
niceeval view # 起本地 web,自动打开浏览器,读 .niceeval/ 下所有历史运行
|
|
7
7
|
niceeval view .niceeval/<run>/summary.json
|
|
8
8
|
niceeval view --no-open # 只打印 URL,不打开浏览器
|
|
9
|
-
niceeval view --out .niceeval/report.html #
|
|
9
|
+
niceeval view --out .niceeval/report.html # 导出单文件静态 HTML
|
|
10
|
+
niceeval view --out site # 目录式静态导出:index.html + artifact/
|
|
10
11
|
```
|
|
11
12
|
|
|
12
|
-
架构上是**一次性烘焙进单个 HTML+JSON 的静态产物**(`src/view/index.ts` 的 `renderHtml`),不是常驻的多页面 server——`niceeval view` 起的 web 服务每次请求现读现渲染,`--out`
|
|
13
|
+
架构上是**一次性烘焙进单个 HTML+JSON 的静态产物**(`src/view/index.ts` 的 `renderHtml`),不是常驻的多页面 server——`niceeval view` 起的 web 服务每次请求现读现渲染,`--out` 则直接导出。这是刻意的取舍,详见 [References](references.md#调研过判断不值得抄的及理由)。
|
|
14
|
+
|
|
15
|
+
`--out` 按目标形态分两种(`isSingleFileOut`):`*.html` 导出单文件,可当 CI 附件传、单文件分享,但工件视图(代码/transcript/trace)降级;目录则额外把前端会 fetch 的三类工件(`sources.json` / `events.json` / `trace.json`)复制到 `<out>/artifact/<base>/`,与本地 server 的 `/artifact/<rel>` 路径路由同一布局,同一份前端产物在两种托管下用同一个相对 URL(`src/view/app/lib/artifact-url.ts`)。`diff.json` / `o11y.json` 刻意不复制:查看器从不读取,且 diff 可达上百 MB,带上只会拖垮静态部署体积。
|
|
13
16
|
|
|
14
17
|
## 结果版本机制
|
|
15
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "niceeval",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Agent-native eval tool — eval agents, services, functions, and coding-agent fixtures",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -56,13 +56,11 @@
|
|
|
56
56
|
"tar-stream": "^3.1.7",
|
|
57
57
|
"tsx": "^4.19.2"
|
|
58
58
|
},
|
|
59
|
-
"optionalDependencies": {
|
|
60
|
-
"@vercel/sandbox": "^2.2.1",
|
|
61
|
-
"dockerode": "^4.0.2",
|
|
62
|
-
"e2b": "^2.31.0"
|
|
63
|
-
},
|
|
64
59
|
"devDependencies": {
|
|
65
60
|
"@ai-sdk/otel": "^1.0.9",
|
|
61
|
+
"@vercel/sandbox": "^2.2.1",
|
|
62
|
+
"dockerode": "^4.0.2",
|
|
63
|
+
"e2b": "^2.31.0",
|
|
66
64
|
"@opentelemetry/exporter-trace-otlp-http": "^0.219.0",
|
|
67
65
|
"@opentelemetry/sdk-trace-node": "^2.8.0",
|
|
68
66
|
"@radix-ui/react-collapsible": "^1.1.14",
|
|
@@ -95,12 +93,18 @@
|
|
|
95
93
|
"@ai-sdk/otel": ">=1.0.0",
|
|
96
94
|
"@opentelemetry/exporter-trace-otlp-http": ">=0.200.0",
|
|
97
95
|
"@opentelemetry/sdk-trace-node": ">=2.0.0",
|
|
98
|
-
"
|
|
96
|
+
"@vercel/sandbox": ">=2.0.0",
|
|
97
|
+
"ai": ">=5.0.0",
|
|
98
|
+
"dockerode": ">=4.0.0",
|
|
99
|
+
"e2b": ">=2.0.0"
|
|
99
100
|
},
|
|
100
101
|
"peerDependenciesMeta": {
|
|
101
102
|
"@ai-sdk/otel": {
|
|
102
103
|
"optional": true
|
|
103
104
|
},
|
|
105
|
+
"@vercel/sandbox": {
|
|
106
|
+
"optional": true
|
|
107
|
+
},
|
|
104
108
|
"ai": {
|
|
105
109
|
"optional": true
|
|
106
110
|
},
|
|
@@ -109,6 +113,12 @@
|
|
|
109
113
|
},
|
|
110
114
|
"@opentelemetry/sdk-trace-node": {
|
|
111
115
|
"optional": true
|
|
116
|
+
},
|
|
117
|
+
"dockerode": {
|
|
118
|
+
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"e2b": {
|
|
121
|
+
"optional": true
|
|
112
122
|
}
|
|
113
123
|
},
|
|
114
124
|
"scripts": {
|
package/src/cli.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { Console as ConsoleReporter } from "./runner/reporters/console.ts";
|
|
|
17
17
|
import { JUnit } from "./runner/reporters/json.ts";
|
|
18
18
|
import { Live as LiveReporter, type LiveRow } from "./runner/reporters/live.ts";
|
|
19
19
|
import { Artifacts as ArtifactsReporter } from "./runner/reporters/artifacts.ts";
|
|
20
|
-
import { buildView, startViewServer, loadMostRecentResults, IncompatibleResultsError } from "./view/index.ts";
|
|
20
|
+
import { buildView, isSingleFileOut, startViewServer, loadMostRecentResults, IncompatibleResultsError } from "./view/index.ts";
|
|
21
21
|
import { t } from "./i18n/index.ts";
|
|
22
22
|
import { formatThrown } from "./util.ts";
|
|
23
23
|
import type { Config, DiscoveredExperiment, Reporter } from "./types.ts";
|
|
@@ -248,7 +248,7 @@ async function main(): Promise<void> {
|
|
|
248
248
|
if (command === "view") {
|
|
249
249
|
if (flags.out) {
|
|
250
250
|
const out = await buildView({ input: positionals[0], out: flags.out }).catch(exitOnIncompatibleResults);
|
|
251
|
-
process.stdout.write(t("cli.view.exported", { out }));
|
|
251
|
+
process.stdout.write(t(isSingleFileOut(out) ? "cli.view.exported" : "cli.view.exportedDir", { out }));
|
|
252
252
|
process.exit(0);
|
|
253
253
|
}
|
|
254
254
|
const server = await startViewServer({ input: positionals[0], port: flags.port }).catch(exitOnIncompatibleResults);
|
package/src/i18n/en.ts
CHANGED
|
@@ -35,7 +35,9 @@ export const en = {
|
|
|
35
35
|
"Usage:\n" +
|
|
36
36
|
" niceeval exp [group|experiment] [eval-id-prefix…] run experiments\n" +
|
|
37
37
|
" niceeval list list discovered evals\n" +
|
|
38
|
-
" niceeval view [summary.json|dir] [--out
|
|
38
|
+
" niceeval view [summary.json|dir] [--out x.html|dir] [--port n] [--no-open]\n" +
|
|
39
|
+
" --out *.html exports a single-file report; --out <dir> also bundles\n" +
|
|
40
|
+
" artifacts so the directory can be served by any static host\n" +
|
|
39
41
|
" niceeval clean delete .niceeval/ artifacts\n" +
|
|
40
42
|
" niceeval init scaffold config + evals/\n\n" +
|
|
41
43
|
"Flags:\n" +
|
|
@@ -65,6 +67,7 @@ export const en = {
|
|
|
65
67
|
"cli.sandboxFlagRemoved": "`--sandbox` is not a CLI flag. Set `sandbox` in the experiment (or `niceeval.config.ts` as a project-wide fallback) to dockerSandbox() / vercelSandbox() / e2bSandbox() (import from \"niceeval/sandbox\").\n",
|
|
66
68
|
"cli.unimplemented": "Command \"{{command}}\" is not implemented yet (MVP).\n",
|
|
67
69
|
"cli.view.exported": "Exported eval report page: {{out}}\n",
|
|
70
|
+
"cli.view.exportedDir": "Exported static report site: {{out}} (serve the whole directory with any static host; opening index.html via file:// cannot fetch artifacts)\n",
|
|
68
71
|
"cli.view.incompatible": "{{dir}}: written by niceeval {{producer}} (schemaVersion {{schemaVersion}}); this CLI reads schemaVersion {{supported}}.\nRun `{{command}}` to view it.\n",
|
|
69
72
|
"cli.view.url": "niceeval view: {{url}}\n",
|
|
70
73
|
"context.capabilityMissing":
|
|
@@ -97,8 +100,9 @@ export const en = {
|
|
|
97
100
|
"judge.httpError": "judge HTTP {{status}}: {{body}}",
|
|
98
101
|
"judge.probeFailed": "judge precheck failed ({{model}}): {{error}}",
|
|
99
102
|
"judge.probeMissingKey": "judge model {{model}} is missing an API key; configure {{envHint}}",
|
|
100
|
-
"live.running": " Running {{totalRuns}} attempts ({{evals}} evals × {{configs}} configs) {{completed}}/{{total}} done",
|
|
103
|
+
"live.running": " Running {{totalRuns}} attempts ({{evals}} evals × {{configs}} configs, concurrency {{concurrency}}) {{completed}}/{{total}} done",
|
|
101
104
|
"live.runningUnknown": " Running... {{completed}}/{{total}} done",
|
|
105
|
+
"live.waiting": "waiting for a slot...",
|
|
102
106
|
"report.assertionThreshold": " (got {{score}} < {{threshold}})",
|
|
103
107
|
"report.error": "error",
|
|
104
108
|
"report.errored": "errored",
|
|
@@ -106,8 +110,9 @@ export const en = {
|
|
|
106
110
|
"report.gate": "gate",
|
|
107
111
|
"report.passed": "passed",
|
|
108
112
|
"report.result": "\nResult: {{parts}} ({{duration}} · {{tokens}}{{cost}})\n\n",
|
|
109
|
-
"report.runStart": "\nRunning {{count}} evals{{extra}}\n\n",
|
|
113
|
+
"report.runStart": "\nRunning {{count}} evals{{extra}} (concurrency {{concurrency}})\n\n",
|
|
110
114
|
"report.runStartExtra": " × {{configs}} configs = {{totalRuns}} runs",
|
|
115
|
+
"report.viewHint": "Run `pnpm exec niceeval view` to see the results in the graphical viewer.\n",
|
|
111
116
|
"report.skipped": "skipped",
|
|
112
117
|
"report.soft": "soft",
|
|
113
118
|
"report.summary.errored": "{{count}} errored",
|
package/src/i18n/zh-CN.ts
CHANGED
|
@@ -33,7 +33,8 @@ export const zhCN = {
|
|
|
33
33
|
"用法:\n" +
|
|
34
34
|
" niceeval exp [组|实验] [eval-id 前缀…] 跑实验\n" +
|
|
35
35
|
" niceeval list 列出发现到的 eval\n" +
|
|
36
|
-
" niceeval view [summary.json|目录] [--out
|
|
36
|
+
" niceeval view [summary.json|目录] [--out x.html|目录] [--port n] [--no-open]\n" +
|
|
37
|
+
" --out 给 *.html 导出单文件报告;给目录则连工件一起导出,可直接静态托管\n" +
|
|
37
38
|
" niceeval clean 删除 .niceeval/ 历史工件\n" +
|
|
38
39
|
" niceeval init 脚手架 config + evals/\n\n" +
|
|
39
40
|
"标志:\n" +
|
|
@@ -63,6 +64,7 @@ export const zhCN = {
|
|
|
63
64
|
"cli.sandboxFlagRemoved": "`--sandbox` 不是 CLI flag。请在 experiment(或 niceeval.config.ts 做全项目兜底)里把 sandbox 设成 dockerSandbox() / vercelSandbox() / e2bSandbox()(从 \"niceeval/sandbox\" 导入)。\n",
|
|
64
65
|
"cli.unimplemented": "命令 \"{{command}}\" 暂未实现(MVP)。\n",
|
|
65
66
|
"cli.view.exported": "已导出实验查看页:{{out}}\n",
|
|
67
|
+
"cli.view.exportedDir": "已导出静态查看站:{{out}}(整个目录可直接静态托管;本地打开 {{out}}/index.html 需经 http 服务,file:// 下工件 fetch 不可用)\n",
|
|
66
68
|
"cli.view.incompatible": "{{dir}}: 由 niceeval {{producer}} 写入(schemaVersion {{schemaVersion}}),当前 CLI 只读 schemaVersion {{supported}}。\n运行 `{{command}}` 查看这份报告。\n",
|
|
67
69
|
"cli.view.url": "niceeval view: {{url}}\n",
|
|
68
70
|
"context.capabilityMissing":
|
|
@@ -95,8 +97,9 @@ export const zhCN = {
|
|
|
95
97
|
"judge.httpError": "judge HTTP {{status}}: {{body}}",
|
|
96
98
|
"judge.probeFailed": "judge 预检失败({{model}}): {{error}}",
|
|
97
99
|
"judge.probeMissingKey": "judge 模型 {{model}} 缺少 API key —— 请配置 {{envHint}}",
|
|
98
|
-
"live.running": " 正在运行 {{totalRuns}} 次 ({{evals}} eval × {{configs}}
|
|
100
|
+
"live.running": " 正在运行 {{totalRuns}} 次 ({{evals}} eval × {{configs}} 配置, 并发 {{concurrency}}) {{completed}}/{{total}} 完成",
|
|
99
101
|
"live.runningUnknown": " 正在运行… {{completed}}/{{total}} 完成",
|
|
102
|
+
"live.waiting": "排队等待中…",
|
|
100
103
|
"report.assertionThreshold": " (得分 {{score}} < {{threshold}})",
|
|
101
104
|
"report.error": "错误",
|
|
102
105
|
"report.errored": "错误",
|
|
@@ -104,8 +107,9 @@ export const zhCN = {
|
|
|
104
107
|
"report.gate": "gate",
|
|
105
108
|
"report.passed": "通过",
|
|
106
109
|
"report.result": "\n结果:{{parts}} ({{duration}} · {{tokens}}{{cost}})\n\n",
|
|
107
|
-
"report.runStart": "\n本次运行 {{count}} 个 eval{{extra}}\n\n",
|
|
110
|
+
"report.runStart": "\n本次运行 {{count}} 个 eval{{extra}}(并发 {{concurrency}})\n\n",
|
|
108
111
|
"report.runStartExtra": " × {{configs}} 配置 = {{totalRuns}} 次运行",
|
|
112
|
+
"report.viewHint": "运行 `pnpm exec niceeval view` 以图形化查看结果。\n",
|
|
109
113
|
"report.skipped": "跳过",
|
|
110
114
|
"report.soft": "soft",
|
|
111
115
|
"report.summary.errored": "{{count}} 错误",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { EvalResult, Reporter, RunSummary } from "../../types.ts";
|
|
4
4
|
import { t } from "../../i18n/index.ts";
|
|
5
|
-
import { formatDuration, formatTokens, renderRunReport } from "./table.ts";
|
|
5
|
+
import { formatCost, formatDuration, formatTokens, renderRunReport } from "./table.ts";
|
|
6
6
|
import { outcomeSymbol } from "./shared.ts";
|
|
7
7
|
|
|
8
8
|
export function Console(): Reporter {
|
|
@@ -15,14 +15,14 @@ export function Console(): Reporter {
|
|
|
15
15
|
shape && shape.totalRuns > n
|
|
16
16
|
? t("report.runStartExtra", { configs: shape.configs, totalRuns: shape.totalRuns })
|
|
17
17
|
: "";
|
|
18
|
-
process.stdout.write(t("report.runStart", { count: n, extra }));
|
|
18
|
+
process.stdout.write(t("report.runStart", { count: n, extra, concurrency: shape?.maxConcurrency ?? "?" }));
|
|
19
19
|
},
|
|
20
20
|
onEvalComplete(result: EvalResult) {
|
|
21
21
|
const sym = outcomeSymbol(result.outcome);
|
|
22
22
|
const tok = (result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0);
|
|
23
23
|
// requests > 0 但 tokens = 0 → agent 跑了但不上报用量(如 bub);显示 — 而非误导性的 0
|
|
24
24
|
const tokStr = tok > 0 ? `${formatTokens(tok)} tok` : (result.usage?.requests ?? 0) > 0 ? `— tok` : `0 tok`;
|
|
25
|
-
const cost = result.estimatedCostUSD !== undefined ? `
|
|
25
|
+
const cost = result.estimatedCostUSD !== undefined ? ` ${formatCost(result.estimatedCostUSD)}` : "";
|
|
26
26
|
const who = result.model ? `${result.agent}/${result.model}` : result.agent;
|
|
27
27
|
const meta = `(${formatDuration(result.durationMs)} ${tokStr}${cost})`;
|
|
28
28
|
const label = result.outcome === "passed" ? "" : ` ${formatOutcome(result.outcome)}`;
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// spinner 每 80ms 刷新;attempt 完成后行内显示 ✓/✗/~ 符号。
|
|
3
3
|
// onRunComplete 时清除状态表,打印和网页榜单同口径的表格报告。
|
|
4
4
|
|
|
5
|
-
import type { Reporter, RunShape, RunSummary } from "../../types.ts";
|
|
5
|
+
import type { Reporter, ReporterEvent, RunShape, RunSummary } from "../../types.ts";
|
|
6
6
|
import { t } from "../../i18n/index.ts";
|
|
7
7
|
import { renderRunReport } from "./table.ts";
|
|
8
|
-
import { outcomeSymbol } from "./shared.ts";
|
|
8
|
+
import { outcomeSymbol, WAITING_SYM } from "./shared.ts";
|
|
9
9
|
|
|
10
10
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
11
11
|
|
|
@@ -42,6 +42,8 @@ interface RowState extends LiveRow {
|
|
|
42
42
|
completed: number;
|
|
43
43
|
lastMsg: string;
|
|
44
44
|
dominantOutcome: string | undefined;
|
|
45
|
+
/** true 一旦拿到并发名额、attempt effect 真正开始跑;之前是排队等待,不该转圈误导。 */
|
|
46
|
+
started: boolean;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export function Live(rows: LiveRow[], totalAttempts: number): LiveReporter {
|
|
@@ -51,7 +53,7 @@ export function Live(rows: LiveRow[], totalAttempts: number): LiveReporter {
|
|
|
51
53
|
for (const r of rows) {
|
|
52
54
|
const k = `${r.evalId}|${r.who}`;
|
|
53
55
|
if (!stateMap.has(k)) {
|
|
54
|
-
stateMap.set(k, { ...r, completed: 0, lastMsg: "", dominantOutcome: undefined });
|
|
56
|
+
stateMap.set(k, { ...r, completed: 0, lastMsg: "", dominantOutcome: undefined, started: false });
|
|
55
57
|
keyOrder.push(k);
|
|
56
58
|
} else {
|
|
57
59
|
// 同一 (evalId, who) 可能在多个 agentRun 里出现(不应发生,但做防御)
|
|
@@ -71,7 +73,9 @@ export function Live(rows: LiveRow[], totalAttempts: number): LiveReporter {
|
|
|
71
73
|
const done = state.completed >= state.total;
|
|
72
74
|
const sym = done
|
|
73
75
|
? outcomeSymbol(state.dominantOutcome ?? "")
|
|
74
|
-
:
|
|
76
|
+
: state.started
|
|
77
|
+
? SPINNER[frame % SPINNER.length]
|
|
78
|
+
: WAITING_SYM;
|
|
75
79
|
|
|
76
80
|
const evalCol = state.evalId.slice(0, 24).padEnd(24);
|
|
77
81
|
const whoCol = `[${state.who}]`.slice(0, 26).padEnd(26);
|
|
@@ -79,7 +83,7 @@ export function Live(rows: LiveRow[], totalAttempts: number): LiveReporter {
|
|
|
79
83
|
|
|
80
84
|
const prefix = ` ${sym} ${evalCol} ${whoCol} ${cntCol} `;
|
|
81
85
|
const budget = Math.max(0, cols() - prefix.length - 1);
|
|
82
|
-
const msg = done ? "" : state.lastMsg.slice(0, budget);
|
|
86
|
+
const msg = done ? "" : state.started ? state.lastMsg.slice(0, budget) : t("live.waiting").slice(0, budget);
|
|
83
87
|
|
|
84
88
|
return `\x1B[2K${prefix}${msg}`;
|
|
85
89
|
}
|
|
@@ -90,6 +94,7 @@ export function Live(rows: LiveRow[], totalAttempts: number): LiveReporter {
|
|
|
90
94
|
totalRuns: shape.totalRuns,
|
|
91
95
|
evals: shape.evals,
|
|
92
96
|
configs: shape.configs,
|
|
97
|
+
concurrency: shape.maxConcurrency,
|
|
93
98
|
completed: totalCompleted,
|
|
94
99
|
total: totalAttempts,
|
|
95
100
|
})
|
|
@@ -132,6 +137,13 @@ export function Live(rows: LiveRow[], totalAttempts: number): LiveReporter {
|
|
|
132
137
|
// 不在这里 draw();由 interval 驱动,避免每条日志都刷屏
|
|
133
138
|
},
|
|
134
139
|
|
|
140
|
+
onEvent(event: ReporterEvent) {
|
|
141
|
+
if (event.type !== "eval:start") return;
|
|
142
|
+
const who = event.model ? `${event.agent.name}/${event.model}` : event.agent.name;
|
|
143
|
+
const state = stateMap.get(`${event.eval.id}|${who}`);
|
|
144
|
+
if (state) state.started = true;
|
|
145
|
+
},
|
|
146
|
+
|
|
135
147
|
onRunStart(_evals, _agent, s) {
|
|
136
148
|
shape = s;
|
|
137
149
|
// 初始渲染:让用户看到行表
|
|
@@ -13,3 +13,6 @@ export const OUTCOME_SYM: Record<ResultOutcome, string> = {
|
|
|
13
13
|
export function outcomeSymbol(outcome: string): string {
|
|
14
14
|
return OUTCOME_SYM[outcome as ResultOutcome] ?? "?";
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
/** live 表格里「还没抢到并发名额」的行:和转圈的 SPINNER、完成后的 OUTCOME_SYM 三态区分开。 */
|
|
18
|
+
export const WAITING_SYM = "·";
|
package/src/runner/run.ts
CHANGED
|
@@ -113,6 +113,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
113
113
|
evals: runningEvals.length,
|
|
114
114
|
configs: opts.agentRuns.length,
|
|
115
115
|
totalRuns: attempts.length,
|
|
116
|
+
maxConcurrency: opts.maxConcurrency,
|
|
116
117
|
};
|
|
117
118
|
for (const r of opts.reporters) {
|
|
118
119
|
// reporter 只是结果消费方:单个 reporter 抛错记 diagnostic,不能让整次调度崩(P2)。
|
|
@@ -259,6 +260,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
259
260
|
type: "eval:start",
|
|
260
261
|
eval: { id: a.evalDef.id },
|
|
261
262
|
agent: a.run.agent,
|
|
263
|
+
model: a.run.model,
|
|
262
264
|
attempt: a.attempt,
|
|
263
265
|
experimentId: a.run.experimentId,
|
|
264
266
|
}),
|
package/src/runner/types.ts
CHANGED
|
@@ -97,6 +97,8 @@ export interface RunShape {
|
|
|
97
97
|
configs: number;
|
|
98
98
|
/** 总 attempt 数(evals × configs × runs);逐行输出与汇总计数都按它。 */
|
|
99
99
|
totalRuns: number;
|
|
100
|
+
/** 本次运行实际生效的并发数(flag/env/experiment/config/sandbox 默认值解析后的结果)。 */
|
|
101
|
+
maxConcurrency: number;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
export interface Reporter {
|
|
@@ -108,7 +110,7 @@ export interface Reporter {
|
|
|
108
110
|
|
|
109
111
|
export type ReporterEvent =
|
|
110
112
|
| { type: "run:start"; evals: { id: string }[]; agent: Agent; shape: RunShape }
|
|
111
|
-
| { type: "eval:start"; eval: { id: string }; agent: Agent; attempt: number; experimentId?: string }
|
|
113
|
+
| { type: "eval:start"; eval: { id: string }; agent: Agent; model?: string; attempt: number; experimentId?: string }
|
|
112
114
|
| { type: "eval:complete"; result: EvalResult }
|
|
113
115
|
| { type: "run:earlyExit"; evalId: string; experimentId?: string }
|
|
114
116
|
| { type: "run:budgetExceeded"; budget: number; spent: number }
|
package/src/shared/format.ts
CHANGED
|
@@ -15,6 +15,12 @@ export function formatDuration(ms?: number): string {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function formatCost(value?: number): string {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// 查不到价 / 没有 model 时上游传 undefined(见 o11y/cost.ts) —— 显示 "—" 而不是騙人的 $0,
|
|
19
|
+
// 否则「真实但极小的花费」和「压根没算出成本」在界面上长得一模一样。
|
|
20
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return "—";
|
|
21
|
+
if (value >= 1) return "$" + value.toFixed(2);
|
|
22
|
+
if (value >= 0.001) return "$" + value.toFixed(3);
|
|
23
|
+
// 便宜模型 + 小样本(如 tier1 示例)常见的真实成本,3 位小数会整个舍成 0.000,
|
|
24
|
+
// 这里退到 2 位有效数字,保留"确实花了一点点钱"的信号。
|
|
25
|
+
return "$" + value.toPrecision(2);
|
|
20
26
|
}
|