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 CHANGED
@@ -12,13 +12,17 @@
12
12
 
13
13
  </div>
14
14
 
15
- NiceEval is an agent-native eval tool inspired by [eve](https://eve.dev). It has an excellent DX design — anyone can get started and configured in about 10 minutes. It's also versatile: it can eval plugins, Hooks, and Skills written for Claude Code/Codex coding agents, and can directly eval your own AI agent application or framework (AI SDK, LangGraph, Pi, or any custom agent loop).
15
+ NiceEval is an Agent-Native eval tool inspired by [eve](https://eve.dev), built for the best possible DX.
16
16
 
17
- After the eval completes, it generates readable reports and lets you view agent behavior details. Convenient for debugging and optimization.
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 AI-native eval tool. In tools built around Dataset/golden-style Input vs. Expected Output, that shape doesn't fit real agent evaluation well. NiceEval is built for evaluating agents at a finer grain — multi-turn conversations, multi-agent setups, tool calls, skill loading, and more.
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
- NiceEval
37
- └─────────────────────┘
39
+ ┌────────────┐
40
+ NiceEval
41
+ └────────────┘
38
42
 
39
43
  │ Agent adapter (official)
40
44
 
41
- ┌──────────────────────────────┐
42
- Docker Sandbox │
43
- ┌────────────────────────┐ │
44
- │ Codex / Claude Code / │ │
45
- │ apps needing isolation │ │
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
- NiceEval
58
- └─────────────────────┘
60
+ ┌────────────┐
61
+ NiceEval
62
+ └────────────┘
59
63
 
60
64
  │ Agent adapter (official, or your own implementation)
61
65
 
62
- ┌──────────────────────────────┐
63
- your own AI Agent
64
- │ (AI SDK·LangGraph·Pi and
65
- │ other agent frameworks — │
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
- npx niceeval exp local eval-tool-call // run only eval-tool-call under the local experiment
117
- npx niceeval view
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
- - [ ] Claude Code
139
- - [ ] Codex
140
- - [ ] Bub
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
- - [ ] AI SDK
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) 启发的 agent-native eval 工具,追求足够好的开发体验:大多数项目可以在 10 分钟左右接入并配置。它的设计也足够通用——既可以用来 eval 给 Claude Code / Codex 写的 coding agent 插件、Hook 和 Skill,也可以直接 eval 你自己的 AI Agent 应用或框架(无论基于 AI SDK、LangGraph、Pi 还是自研 agent loop,都能轻松接入)。
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 是一个 AI-native 的评估工具。Dataset / golden 那一套「构建 Input 与 Expected Output」的模式,并不适合真实的 agent 评估;当 agent 需要在多轮对话、多 agent 协作、工具调用、Skill 加载等细粒度场景下被评估时,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
- - [ ] Claude Code
135
- - [ ] Codex
136
- - [ ] Bub
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
- - [ ] AI SDK
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
 
@@ -7,26 +7,26 @@ niceeval 把一个评测过程拆成四段职责:**发现**要跑什么、**驱
7
7
  ## 系统总览
8
8
 
9
9
  ```text
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
- └────────────────────────────────────────────────┘
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 # 导出静态 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` 则直接导出成一个可以当 CI 附件传、单文件分享的 HTML。这是刻意的取舍,详见 [References](references.md#调研过判断不值得抄的及理由)。
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.2.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
- "ai": ">=5.0.0"
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 d] [--port n] [--no-open]\n" +
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 目录] [--port n] [--no-open]\n" +
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}} 配置) {{completed}}/{{total}} 完成",
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 ? ` $${result.estimatedCostUSD.toFixed(3)}` : "";
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
- : SPINNER[frame % SPINNER.length];
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 = "·";
@@ -104,6 +104,7 @@ export function renderRunReport(summary: RunSummary): string {
104
104
  tokens: tokStr,
105
105
  cost,
106
106
  }).trimEnd());
107
+ lines.push(t("report.viewHint").trimEnd());
107
108
 
108
109
  return `${lines.join("\n")}\n\n`;
109
110
  }
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
  }),
@@ -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 }
@@ -15,6 +15,12 @@ export function formatDuration(ms?: number): string {
15
15
  }
16
16
 
17
17
  export function formatCost(value?: number): string {
18
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return "$0";
19
- return "$" + value.toFixed(value < 1 ? 3 : 2);
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
  }