niceeval 0.1.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 +164 -0
- package/README.zh.md +160 -0
- package/bin/niceeval.js +11 -0
- package/package.json +96 -0
- package/src/agents/bub.ts +223 -0
- package/src/agents/builtin.ts +6 -0
- package/src/agents/claude-code.ts +96 -0
- package/src/agents/codex.ts +122 -0
- package/src/agents/index.ts +25 -0
- package/src/agents/shared.ts +145 -0
- package/src/cli.ts +421 -0
- package/src/context/context.test.ts +96 -0
- package/src/context/context.ts +427 -0
- package/src/context/control-flow.ts +27 -0
- package/src/context/session.ts +124 -0
- package/src/define.ts +112 -0
- package/src/expect/index.ts +243 -0
- package/src/i18n/en.ts +131 -0
- package/src/i18n/index.ts +39 -0
- package/src/i18n/zh-CN.ts +132 -0
- package/src/index.ts +39 -0
- package/src/loaders/index.ts +56 -0
- package/src/o11y/cost.ts +57 -0
- package/src/o11y/derive.ts +304 -0
- package/src/o11y/otlp/canonical.ts +112 -0
- package/src/o11y/otlp/mappers/bub.ts +30 -0
- package/src/o11y/otlp/mappers/codex.ts +38 -0
- package/src/o11y/otlp/mappers/index.ts +25 -0
- package/src/o11y/otlp/parse.ts +330 -0
- package/src/o11y/otlp/receiver.ts +100 -0
- package/src/o11y/otlp/sandbox-receiver.ts +113 -0
- package/src/o11y/otlp/select.ts +82 -0
- package/src/o11y/parsers/bub.ts +240 -0
- package/src/o11y/parsers/claude-code.ts +270 -0
- package/src/o11y/parsers/codex.ts +480 -0
- package/src/o11y/parsers/index.ts +37 -0
- package/src/o11y/prices.json +9873 -0
- package/src/runner/discover.ts +77 -0
- package/src/runner/reporters/artifacts.ts +81 -0
- package/src/runner/reporters/console.ts +76 -0
- package/src/runner/reporters/index.ts +5 -0
- package/src/runner/reporters/json.ts +52 -0
- package/src/runner/reporters/live.ts +178 -0
- package/src/runner/reporters/table.ts +328 -0
- package/src/runner/run.ts +860 -0
- package/src/runner/sandbox-prep.ts +91 -0
- package/src/sandbox/checkpoint.ts +39 -0
- package/src/sandbox/docker.ts +539 -0
- package/src/sandbox/e2b.ts +203 -0
- package/src/sandbox/index.ts +25 -0
- package/src/sandbox/registry.ts +60 -0
- package/src/sandbox/resolve.ts +131 -0
- package/src/sandbox/source-files.ts +30 -0
- package/src/sandbox/vercel.ts +236 -0
- package/src/scoring/collector.ts +113 -0
- package/src/scoring/judge.ts +236 -0
- package/src/scoring/scoped.ts +289 -0
- package/src/scoring/verdict.ts +20 -0
- package/src/source-loc.ts +53 -0
- package/src/types.ts +913 -0
- package/src/util.test.ts +31 -0
- package/src/util.ts +50 -0
- package/src/view/app/App.tsx +189 -0
- package/src/view/app/components/AttemptModal.tsx +66 -0
- package/src/view/app/components/CodeView.tsx +272 -0
- package/src/view/app/components/CopyControls.tsx +89 -0
- package/src/view/app/components/ExperimentTable.tsx +266 -0
- package/src/view/app/components/GroupSelector.tsx +61 -0
- package/src/view/app/components/LazyArtifact.tsx +50 -0
- package/src/view/app/components/Trace.tsx +100 -0
- package/src/view/app/components/Transcript.tsx +130 -0
- package/src/view/app/components/primitives.tsx +43 -0
- package/src/view/app/components/ui/badge.tsx +21 -0
- package/src/view/app/components/ui/dialog.tsx +34 -0
- package/src/view/app/components/ui/tabs.tsx +30 -0
- package/src/view/app/i18n.ts +341 -0
- package/src/view/app/index.html +13 -0
- package/src/view/app/lib/cn.ts +7 -0
- package/src/view/app/lib/format.ts +73 -0
- package/src/view/app/lib/guards.ts +61 -0
- package/src/view/app/lib/outcome.ts +96 -0
- package/src/view/app/lib/rows.ts +63 -0
- package/src/view/app/lib/transcript-data.tsx +121 -0
- package/src/view/app/main.tsx +17 -0
- package/src/view/app/pages/RunsPage.tsx +83 -0
- package/src/view/app/pages/TracesPage.tsx +40 -0
- package/src/view/app/shared.ts +10 -0
- package/src/view/app/types.ts +114 -0
- package/src/view/app/vite.config.ts +26 -0
- package/src/view/client-dist/app.css +2 -0
- package/src/view/client-dist/app.js +56 -0
- package/src/view/index.ts +406 -0
- package/src/view/styles.css +1074 -0
- package/src/view/template.html +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# NiceEval
|
|
4
|
+
|
|
5
|
+
**Progressive, full-featured, excellent DX lightweight ai agent evals tool**
|
|
6
|
+
|
|
7
|
+
[](tsconfig.json)
|
|
8
|
+
[](package.json)
|
|
9
|
+
[](docs/README.md)
|
|
10
|
+
|
|
11
|
+
[中文](README.zh.md)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
NiceEval is a general-purpose agent 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 very versatile: it can eval plugins, Hooks, and Skills written for Claude Code/Codex coding agents, and can directly eval your own AI Agent framework (no matter if it's based on AI SDK, LangGraph, Pi, or any other interface, it's easy to integrate).
|
|
16
|
+
|
|
17
|
+
After the eval completes, it generates readable reports and lets you view agent behavior details. Convenient for debugging and optimization.
|
|
18
|
+
|
|
19
|
+
## Why NiceEval when DeepEval, LangFuse, and BrainTrust already exist
|
|
20
|
+
|
|
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.
|
|
22
|
+
|
|
23
|
+
It also coexists with LangFuse and BrainTrust: use them for tracing, or upload eval results to both (in progress).
|
|
24
|
+
|
|
25
|
+
## Architecture
|
|
26
|
+
|
|
27
|
+
NiceEval supports two integration modes, depending on whether the system under test needs an isolated sandbox filesystem.
|
|
28
|
+
|
|
29
|
+
**Mode 1: Sandbox (Docker, E2B) — run coding agents like Codex and Claude Code that need a sandbox**
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
evals/*.eval.ts
|
|
33
|
+
│
|
|
34
|
+
▼
|
|
35
|
+
┌─────────────────────┐
|
|
36
|
+
│ NiceEval │
|
|
37
|
+
└─────────────────────┘
|
|
38
|
+
│
|
|
39
|
+
│ Agent adapter (official)
|
|
40
|
+
▼
|
|
41
|
+
┌──────────────────────────────┐
|
|
42
|
+
│ Docker Sandbox │
|
|
43
|
+
│ ┌────────────────────────┐ │
|
|
44
|
+
│ │ Codex / Claude Code / │ │
|
|
45
|
+
│ │ apps needing isolation │ │
|
|
46
|
+
│ └────────────────────────┘ │
|
|
47
|
+
└──────────────────────────────┘
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Mode 2: Direct — connect straight to your own AI Agent**
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
evals/*.eval.ts
|
|
54
|
+
│
|
|
55
|
+
▼
|
|
56
|
+
┌─────────────────────┐
|
|
57
|
+
│ NiceEval │
|
|
58
|
+
└─────────────────────┘
|
|
59
|
+
│
|
|
60
|
+
│ Agent adapter (official, or your own implementation)
|
|
61
|
+
▼
|
|
62
|
+
┌──────────────────────────────┐
|
|
63
|
+
│ your own Web Agent │
|
|
64
|
+
│ (HTTP / AI SDK·LangGraph· │
|
|
65
|
+
│ Pi and other frameworks — │
|
|
66
|
+
│ no Docker needed) │
|
|
67
|
+
└──────────────────────────────┘
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- **NiceEval core** owns discovery, scheduling, scoring, reporting, and artifacts.
|
|
71
|
+
- **Agent adapters** are the open boundary: you decide how to call the system under test.
|
|
72
|
+
- Coding agents that need filesystem isolation run inside the **Docker Sandbox**; your own Web Agent can connect directly, without Docker.
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## Example
|
|
76
|
+
|
|
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
|
+
```ts
|
|
80
|
+
// evals/eval-tool-call.eval.ts
|
|
81
|
+
import { defineEval } from "niceeval";
|
|
82
|
+
|
|
83
|
+
export default defineEval({
|
|
84
|
+
description: "Verify the agent calls the weather tool and answers from its result",
|
|
85
|
+
|
|
86
|
+
async test(t) {
|
|
87
|
+
const turn = await t.send("What's the weather in Beijing today?");
|
|
88
|
+
t.succeeded();
|
|
89
|
+
|
|
90
|
+
await t.group("calls get_weather with the right city", () => {
|
|
91
|
+
t.calledTool("get_weather", { input: { city: "Beijing" } });
|
|
92
|
+
t.messageIncludes(/°C|sunny|cloudy|rain/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const second = await t.send("What about Shanghai tomorrow?");
|
|
96
|
+
second.messageIncludes("Shanghai");
|
|
97
|
+
|
|
98
|
+
t.judge.autoevals
|
|
99
|
+
.closedQA("Does the reply use the tool's weather data instead of making up a temperature?")
|
|
100
|
+
.atLeast(0.7);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// experiments/local.ts
|
|
107
|
+
import { defineExperiment } from "niceeval";
|
|
108
|
+
import { webAgent } from "./adapter"; // your agent adapter, pointed at the system under test
|
|
109
|
+
|
|
110
|
+
export default defineExperiment({
|
|
111
|
+
agent: webAgent({ baseUrl: "http://127.0.0.1:5188" }),
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
npx niceeval exp local eval-tool-call // run only eval-tool-call under the local experiment
|
|
117
|
+
npx niceeval view
|
|
118
|
+
```
|
|
119
|
+
|
|
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
|
+
## Quick Start
|
|
123
|
+
|
|
124
|
+
```text
|
|
125
|
+
READ https://raw.githubusercontent.com/CorrectRoadH/niceeval/refs/heads/main/INIT.md and install niceeval for this repo.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Start from the scenario that matches what you need to evaluate:
|
|
129
|
+
|
|
130
|
+
- [Claude Code / Codex plugin eval](https://niceeval.com/docs/zh/example/claude-code-codex-plugin)
|
|
131
|
+
- [Claude Code / Codex skill eval](https://niceeval.com/docs/zh/example/claude-code-codex-skill)
|
|
132
|
+
- [AI Agent application eval](https://niceeval.com/docs/zh/example/ai-agent-application)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
## Roadmap
|
|
136
|
+
Official Adapters
|
|
137
|
+
- [ ] Agent Software
|
|
138
|
+
- [ ] Claude Code
|
|
139
|
+
- [ ] Codex
|
|
140
|
+
- [ ] Bub
|
|
141
|
+
- [ ] OpenClaw
|
|
142
|
+
- [ ] Hermess Agent
|
|
143
|
+
- [ ] Alma
|
|
144
|
+
- [ ] ...
|
|
145
|
+
|
|
146
|
+
- [ ] Agent Frameworks
|
|
147
|
+
- [ ] AI SDK
|
|
148
|
+
- [ ] LangGraph
|
|
149
|
+
- [ ] Claude SDK
|
|
150
|
+
- [ ] Codex SDK
|
|
151
|
+
- [ ] vm0
|
|
152
|
+
- [ ] Cursor Agent SDK
|
|
153
|
+
|
|
154
|
+
## Documentation
|
|
155
|
+
|
|
156
|
+
- [Quickstart](https://niceeval.com/docs/quickstart)
|
|
157
|
+
|
|
158
|
+
# Acknowledgements
|
|
159
|
+
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)
|
|
163
|
+
|
|
164
|
+
Thanks to the following communities
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# NiceEval
|
|
4
|
+
|
|
5
|
+
**渐进式、Agent Native、DX优秀的轻量 ai agent evals 工具**
|
|
6
|
+
|
|
7
|
+
[](tsconfig.json)
|
|
8
|
+
[](package.json)
|
|
9
|
+
[](docs/README.md)
|
|
10
|
+
|
|
11
|
+
[English](README.md)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
NiceEval 是一个受[eve](https://eve.dev)启发的通用型 agent eval 工具。首先有非常优秀的 DX 设计,任何人可以在 10 分钟左右上手并配置。并且设计非常的通用。即可以用来 eval 给 Claude Code/Codex 写的 coding agent 的插件、Hook还有Skill。更可以直接 eval 自己的 AI Agent 框架(无论是基于 AI SDK、LangGraph、Pi都可以轻松接入)。
|
|
16
|
+
|
|
17
|
+
在 eval 完成之后可以生成易读的报告与查看 Agent 的行为细节。方便 Debug 与理解 Agent 行为。
|
|
18
|
+
|
|
19
|
+
## 为什么有了 DeepEval、LangFuse、BrainTrust 还需要 NiceEval
|
|
20
|
+
NiceEval 是一个 AI Native 的 Eval 的评估工具。在这里工具里面像 Dataset 与 golden 构建 Input 与 Execpt Output 并不适合于真实的 Agent 评估。其次现在 Agent 需要对于多次用户对话、多 Agent、工具调用、Skill加载等等细粒进行评估时,NiceEval可以做的更好。
|
|
21
|
+
|
|
22
|
+
同时与LangFuse、BrainTrust 也是可以共存的。可以使用前者来做 tracing。或者把评估结果上传到两者(实现中)
|
|
23
|
+
|
|
24
|
+
## 架构
|
|
25
|
+
|
|
26
|
+
NiceEval 支持两种接入方式,取决于被测系统是否需要隔离的沙箱文件系统。
|
|
27
|
+
|
|
28
|
+
**模式一:Sandbox(Docker、E2B)—— 跑 Codex、Claude Code 等需要 sandbox 的 coding agent**
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
evals/*.eval.ts
|
|
32
|
+
│
|
|
33
|
+
▼
|
|
34
|
+
┌─────────────────────┐
|
|
35
|
+
│ NiceEval │
|
|
36
|
+
└─────────────────────┘
|
|
37
|
+
│
|
|
38
|
+
│ Agent 适配器(官方)
|
|
39
|
+
▼
|
|
40
|
+
┌──────────────────────────────┐
|
|
41
|
+
│ Docker Sandbox │
|
|
42
|
+
│ ┌────────────────────────┐ │
|
|
43
|
+
│ │ Codex / Claude Code | │ │
|
|
44
|
+
│ │ 需要隔离文件系统的应用 │ | |
|
|
45
|
+
│ └────────────────────────┘ │
|
|
46
|
+
└──────────────────────────────┘
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**模式二:直连 —— 直接连接你自己的 AI Agent**
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
evals/*.eval.ts
|
|
53
|
+
│
|
|
54
|
+
▼
|
|
55
|
+
┌─────────────────────┐
|
|
56
|
+
│ NiceEval │
|
|
57
|
+
└─────────────────────┘
|
|
58
|
+
│
|
|
59
|
+
│ Agent 适配器(官方,或者自己实现)
|
|
60
|
+
▼
|
|
61
|
+
┌──────────────────────────────┐
|
|
62
|
+
│ 你自己的 Web Agent │
|
|
63
|
+
│ (HTTP / AI SDK·LangGraph │
|
|
64
|
+
│ Pi 等自有框架,无需 Docker) │
|
|
65
|
+
└──────────────────────────────┘
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **NiceEval 核心** 负责发现 eval、调度运行、打分、生成报告与 artifacts。
|
|
69
|
+
- **Agent 适配器** 是开放的边界:你来决定如何调用被测系统。
|
|
70
|
+
- 需要文件系统隔离的 coding agent 走 **Docker Sandbox**;自有的 Web Agent 可以直连,无需 Docker。
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
## 示例
|
|
74
|
+
|
|
75
|
+
跑一条 eval 需要两个文件:eval 本身(测什么)和 experiment(跑哪个 agent)。CLI 不接受裸 eval id——`niceeval exp <experiment> <eval 前缀>` 里的 experiment 才是决定「连哪个被测对象」的地方。下面是一个直连 Web Agent 的真实场景(完整项目见 [`examples/zh/ai-sdk/`](examples/zh/ai-sdk/)),验证 agent 遇到实时天气问题时会调用工具、并基于工具结果作答,而不是凭空编造:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// evals/eval-tool-call.eval.ts
|
|
79
|
+
import { defineEval } from "niceeval";
|
|
80
|
+
|
|
81
|
+
export default defineEval({
|
|
82
|
+
description: "测试 agent 在实时天气问题中正确调用工具并基于结果作答的能力",
|
|
83
|
+
|
|
84
|
+
async test(t) {
|
|
85
|
+
const turn = await t.send("北京今天天气怎么样?");
|
|
86
|
+
t.succeeded();
|
|
87
|
+
|
|
88
|
+
await t.group("调用 get_weather 且城市正确", () => {
|
|
89
|
+
t.calledTool("get_weather", { input: { city: "北京" } });
|
|
90
|
+
t.messageIncludes(/°C|气温|天气|晴|多云|雨/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const second = await t.send("上海明天天气怎么样?");
|
|
94
|
+
second.messageIncludes("上海");
|
|
95
|
+
|
|
96
|
+
t.judge.autoevals
|
|
97
|
+
.closedQA("助手是否基于工具返回的天气数据作答,而不是凭空编造温度?")
|
|
98
|
+
.atLeast(0.7);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// experiments/local.ts
|
|
105
|
+
import { defineExperiment } from "niceeval";
|
|
106
|
+
import { webAgent } from "./adapter"; // 你自己写的 agent adapter,接被测 web agent
|
|
107
|
+
|
|
108
|
+
export default defineExperiment({
|
|
109
|
+
agent: webAgent({ baseUrl: "http://127.0.0.1:5188" }),
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
pnpm exec niceeval exp local eval-tool-call // 用 local experiment 只跑 eval-tool-call
|
|
115
|
+
pnpm exec niceeval view // 查看评估结果
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## 快速开始
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
READ https://raw.githubusercontent.com/CorrectRoadH/niceeval/refs/heads/main/INIT.md and install niceeval for this repo.
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
从你的场景开始:
|
|
125
|
+
|
|
126
|
+
- [如果你需要 eval 你的 Claude Code / Codex 插件](https://niceeval.com/docs/zh/example/claude-code-codex-plugin)
|
|
127
|
+
- [如果你需要 eval 你的 Claude Code / Codex Skill](https://niceeval.com/docs/zh/example/claude-code-codex-skill)
|
|
128
|
+
- [如果你需要 eval 你的 AI Agent 应用](https://niceeval.com/docs/zh/example/ai-agent-application)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
## Roadmap
|
|
132
|
+
官方适配器
|
|
133
|
+
- [ ] Agent 软件
|
|
134
|
+
- [ ] Claude Code
|
|
135
|
+
- [ ] Codex
|
|
136
|
+
- [ ] Bub
|
|
137
|
+
- [ ] OpenClaw
|
|
138
|
+
- [ ] Hermess Agent
|
|
139
|
+
- [ ] Alma
|
|
140
|
+
- [ ] ...
|
|
141
|
+
|
|
142
|
+
- [ ] Agent 框架
|
|
143
|
+
- [ ] AI SDK
|
|
144
|
+
- [ ] LangGraph
|
|
145
|
+
- [ ] Claude SDK
|
|
146
|
+
- [ ] Codex SDK
|
|
147
|
+
- [ ] vm0
|
|
148
|
+
- [ ] Cursor Agent SDK
|
|
149
|
+
|
|
150
|
+
## 文档
|
|
151
|
+
|
|
152
|
+
- [快速开始](https://www.niceeval.com/docs/zh/quickstart)
|
|
153
|
+
|
|
154
|
+
# 感谢
|
|
155
|
+
该项目受下面项目所启发或者是由AI从下面项目中学习代码所写
|
|
156
|
+
[eve](https://eve.dev)
|
|
157
|
+
[agent eval](https://github.com/vercel-labs/agent-eval)
|
|
158
|
+
[ponytail](https://github.com/DietrichGebert/ponytail)
|
|
159
|
+
|
|
160
|
+
感谢下列社区
|
package/bin/niceeval.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// niceeval 入口:注册 tsx 的 ESM loader(让我们能直接 import 用户的 .ts:
|
|
3
|
+
// niceeval.config.ts、evals/*.eval.ts、agents/*.ts),再加载真正的 CLI。
|
|
4
|
+
// 这样框架与被测项目都不需要编译步骤。
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { register } from "tsx/esm/api";
|
|
7
|
+
|
|
8
|
+
register();
|
|
9
|
+
|
|
10
|
+
const cliUrl = new URL("../src/cli.ts", import.meta.url);
|
|
11
|
+
await import(fileURLToPath(cliUrl));
|
package/package.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "niceeval",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lightweight TypeScript agent eval tool — eval agents, services, functions, and coding-agent fixtures",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/CorrectRoadH/niceeval"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"niceeval": "bin/niceeval.js"
|
|
16
|
+
},
|
|
17
|
+
"types": "./src/index.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./src/index.ts",
|
|
21
|
+
"import": "./src/index.ts"
|
|
22
|
+
},
|
|
23
|
+
"./sandbox": {
|
|
24
|
+
"types": "./src/sandbox/index.ts",
|
|
25
|
+
"import": "./src/sandbox/index.ts"
|
|
26
|
+
},
|
|
27
|
+
"./adapter": {
|
|
28
|
+
"types": "./src/agents/index.ts",
|
|
29
|
+
"import": "./src/agents/index.ts"
|
|
30
|
+
},
|
|
31
|
+
"./expect": {
|
|
32
|
+
"types": "./src/expect/index.ts",
|
|
33
|
+
"import": "./src/expect/index.ts"
|
|
34
|
+
},
|
|
35
|
+
"./reporters": {
|
|
36
|
+
"types": "./src/runner/reporters/index.ts",
|
|
37
|
+
"import": "./src/runner/reporters/index.ts"
|
|
38
|
+
},
|
|
39
|
+
"./loaders": {
|
|
40
|
+
"types": "./src/loaders/index.ts",
|
|
41
|
+
"import": "./src/loaders/index.ts"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"src",
|
|
46
|
+
"bin"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:watch": "vitest",
|
|
52
|
+
"niceeval": "node bin/niceeval.js",
|
|
53
|
+
"view:build": "vite build --config src/view/app/vite.config.ts",
|
|
54
|
+
"prepare": "pnpm run view:build",
|
|
55
|
+
"site:dev": "vite site --host 127.0.0.1 --port 5174",
|
|
56
|
+
"site:build": "vite build site",
|
|
57
|
+
"docs:dev": "cd docs-site && npx --yes mint@latest dev",
|
|
58
|
+
"docs:validate": "cd docs-site && npx --yes mint@latest validate",
|
|
59
|
+
"docs:links": "cd docs-site && npx --yes mint@latest broken-links --check-anchors --check-redirects"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"autoevals": "0.0.132",
|
|
63
|
+
"effect": "^3.21.4",
|
|
64
|
+
"tar-stream": "^3.1.7",
|
|
65
|
+
"tsx": "^4.19.2"
|
|
66
|
+
},
|
|
67
|
+
"optionalDependencies": {
|
|
68
|
+
"@vercel/sandbox": "^2.2.1",
|
|
69
|
+
"dockerode": "^4.0.2",
|
|
70
|
+
"e2b": "^2.31.0"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@radix-ui/react-collapsible": "^1.1.14",
|
|
74
|
+
"@radix-ui/react-dialog": "^1.1.17",
|
|
75
|
+
"@radix-ui/react-tabs": "^1.1.15",
|
|
76
|
+
"@tailwindcss/vite": "^4.3.1",
|
|
77
|
+
"@types/dockerode": "^3.3.31",
|
|
78
|
+
"@types/node": "^22.0.0",
|
|
79
|
+
"@types/react": "^19.2.17",
|
|
80
|
+
"@types/react-dom": "^19.2.3",
|
|
81
|
+
"@types/tar-stream": "^3.1.3",
|
|
82
|
+
"@vitejs/plugin-react": "^6.0.3",
|
|
83
|
+
"class-variance-authority": "^0.7.1",
|
|
84
|
+
"clsx": "^2.1.1",
|
|
85
|
+
"lucide-react": "^1.21.0",
|
|
86
|
+
"prism-react-renderer": "^2.4.1",
|
|
87
|
+
"react": "^19.2.7",
|
|
88
|
+
"react-dom": "^19.2.7",
|
|
89
|
+
"react-grab": "^0.1.47",
|
|
90
|
+
"tailwind-merge": "^3.6.0",
|
|
91
|
+
"tailwindcss": "^4.3.1",
|
|
92
|
+
"typescript": "^5.6.0",
|
|
93
|
+
"vite": "^8.1.0",
|
|
94
|
+
"vitest": "^4.1.9"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { defineSandboxAgent } from "../define.ts";
|
|
2
|
+
import { requireEnv, getEnv } from "../util.ts";
|
|
3
|
+
import { shared } from "./shared.ts";
|
|
4
|
+
import { createCheckpoint, restoreCheckpoint } from "../sandbox/checkpoint.ts";
|
|
5
|
+
import type { Agent, Sandbox, StreamEvent } from "../types.ts";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { join, dirname } from "node:path";
|
|
10
|
+
import { t } from "../i18n/index.ts";
|
|
11
|
+
|
|
12
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
13
|
+
// bub 的 agent adapter(沙箱型)。
|
|
14
|
+
//
|
|
15
|
+
// ⚠️ 现实校正:bub 是 PyPI 上的 `bub`(alpha,Python 3.12),不是 npm 包。
|
|
16
|
+
// · 安装:uv tool install bub(uv 自带 python 3.12,免 root)。
|
|
17
|
+
// · 调用:bub run "<prompt>" --session-id <id> --workspace <path>
|
|
18
|
+
// · 模型 + 代理:BUB_MODEL=openai:<model>、BUB_API_BASE、BUB_API_KEY。
|
|
19
|
+
// · 记忆:tape(总是开),落盘在 ~/.bub/tapes/<md5(ws)[:16]>__<md5(sess)[:16]>.jsonl。
|
|
20
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface BubConfig {
|
|
23
|
+
/** OpenAI 兼容代理的 API key。省略时读 BUB_API_KEY env。 */
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
/** OpenAI 兼容代理的 base URL。省略时读 BUB_API_BASE env。 */
|
|
26
|
+
apiBase?: string;
|
|
27
|
+
/**
|
|
28
|
+
* 额外装进 bub tool 环境的 Python 包(pip 名或 git URL)。
|
|
29
|
+
* 每个沙箱 setup 时作为 `uv tool install --with <pkg>` 追加到 bub 环境里。
|
|
30
|
+
* 示例:["bub-plugin-memory", "git+https://github.com/..."]
|
|
31
|
+
*/
|
|
32
|
+
pythonPlugins?: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const UV = "$HOME/.local/bin/uv";
|
|
36
|
+
// bub 二进制:优先用镜像里(预制模板)烘焙在 PATH 上的 bub,否则用 uv 装到 $HOME/.local/bin 的那个。
|
|
37
|
+
// 预制模板把 bub 装到 /usr/local/bin(见 sandbox/docker/Dockerfile),command -v 命中即用 → 跳过安装。
|
|
38
|
+
const BUB = "$(command -v bub || echo $HOME/.local/bin/bub)";
|
|
39
|
+
|
|
40
|
+
const BUB_OVERRIDE = "bub @ git+https://github.com/CorrectRoadH/bub.git@fix/streaming-usage-include-usage";
|
|
41
|
+
const BUB_OVERRIDE_FILE = "/tmp/bub-override.txt";
|
|
42
|
+
const OTEL_PLUGIN =
|
|
43
|
+
"git+https://github.com/CorrectRoadH/bub-contrib.git@fix/tapestore-otel-tape-entry-validation" +
|
|
44
|
+
"#subdirectory=packages/bub-tapestore-otel";
|
|
45
|
+
|
|
46
|
+
const INSTALL_SPEC = `bub --override(${BUB_OVERRIDE}) --with ${OTEL_PLUGIN}`;
|
|
47
|
+
const INSTALL_HASH = createHash("md5").update(INSTALL_SPEC).digest("hex").slice(0, 12);
|
|
48
|
+
const DEFAULT_WORKSPACE = "/home/sandbox/workspace";
|
|
49
|
+
|
|
50
|
+
function diskCachePath(home: string): string {
|
|
51
|
+
const homeKey = createHash("md5").update(home).digest("hex").slice(0, 8);
|
|
52
|
+
return join(homedir(), ".cache", "niceeval", `bub-checkpoint-${homeKey}-${INSTALL_HASH}.bin`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// in-memory checkpoint + mutex keyed by sandbox $HOME,
|
|
56
|
+
// so Docker(/home/node)と Vercel(/home/vercel-sandbox)でキャッシュが混ざらない。
|
|
57
|
+
const memCheckpoints = new Map<string, Buffer>();
|
|
58
|
+
const installsInProgress = new Map<string, Promise<void>>();
|
|
59
|
+
|
|
60
|
+
async function ensureBub(sb: Sandbox, home: string): Promise<void> {
|
|
61
|
+
// 预制模板已把 bub 烘焙进镜像(PATH 上)→ 直接用,跳过 uv 安装 + checkpoint 全套。
|
|
62
|
+
if ((await sb.runShell("command -v bub >/dev/null 2>&1")).exitCode === 0) return;
|
|
63
|
+
|
|
64
|
+
const checkpointPaths = [`${home}/.local`, `${home}/.cache/uv`];
|
|
65
|
+
const cachePath = diskCachePath(home);
|
|
66
|
+
|
|
67
|
+
const mem = memCheckpoints.get(home);
|
|
68
|
+
if (mem) { await restoreCheckpoint(sb, mem); return; }
|
|
69
|
+
|
|
70
|
+
const disk = await readFile(cachePath).catch(() => undefined);
|
|
71
|
+
if (disk) {
|
|
72
|
+
try { await restoreCheckpoint(sb, disk); memCheckpoints.set(home, disk); return; } catch { /* 损坏,回退 */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const inflight = installsInProgress.get(home);
|
|
76
|
+
if (inflight) {
|
|
77
|
+
await inflight;
|
|
78
|
+
const after = memCheckpoints.get(home);
|
|
79
|
+
if (after) { await restoreCheckpoint(sb, after); return; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let resolveInstall!: () => void;
|
|
83
|
+
let rejectInstall!: (e: unknown) => void;
|
|
84
|
+
const installPromise = new Promise<void>((res, rej) => { resolveInstall = res; rejectInstall = rej; });
|
|
85
|
+
installsInProgress.set(home, installPromise);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
await sb.runShell(`test -x ${UV} || (curl -LsSf https://astral.sh/uv/install.sh | sh)`);
|
|
89
|
+
await sb.runShell(`printf '%s\\n' '${BUB_OVERRIDE}' > ${BUB_OVERRIDE_FILE}`);
|
|
90
|
+
let last = { stdout: "", stderr: "" };
|
|
91
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
92
|
+
const install = await sb.runShell(
|
|
93
|
+
`${UV} tool install --reinstall --python 3.12 --prerelease allow 'bub' --overrides ${BUB_OVERRIDE_FILE} --with '${OTEL_PLUGIN}'`,
|
|
94
|
+
);
|
|
95
|
+
if (install.exitCode === 0) break;
|
|
96
|
+
last = install;
|
|
97
|
+
if (attempt === 3) {
|
|
98
|
+
throw new Error(t("bub.installFailed", {
|
|
99
|
+
attempts: 3,
|
|
100
|
+
tail: (last.stdout + last.stderr).split("\n").slice(-15).join("\n"),
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const cp = await createCheckpoint(sb, checkpointPaths);
|
|
105
|
+
memCheckpoints.set(home, cp);
|
|
106
|
+
await mkdir(dirname(cachePath), { recursive: true }).catch(() => {});
|
|
107
|
+
await writeFile(cachePath, cp).catch(() => {});
|
|
108
|
+
resolveInstall();
|
|
109
|
+
} catch (e) {
|
|
110
|
+
rejectInstall(e);
|
|
111
|
+
installsInProgress.delete(home);
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function tapePath(workspace: string, sessionId: string, bubHome: string): string {
|
|
117
|
+
const w = createHash("md5").update(workspace).digest("hex").slice(0, 16);
|
|
118
|
+
const s = createHash("md5").update(sessionId).digest("hex").slice(0, 16);
|
|
119
|
+
return `${bubHome}/tapes/${w}__${s}.jsonl`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function diagnose(
|
|
123
|
+
res: { exitCode: number; stdout: string; stderr: string },
|
|
124
|
+
events: StreamEvent[],
|
|
125
|
+
rawTape: string | undefined,
|
|
126
|
+
): string {
|
|
127
|
+
const parts: string[] = [t("bub.diagnose.exitCode", { code: res.exitCode })];
|
|
128
|
+
if (rawTape === undefined) parts.push(t("bub.diagnose.noTape"));
|
|
129
|
+
else if (events.length === 0) parts.push(t("bub.diagnose.zeroEvents"));
|
|
130
|
+
const lastErr = [...events].reverse().find((e) => e.type === "error") as { type: "error"; message: string } | undefined;
|
|
131
|
+
if (lastErr) parts.push(t("bub.diagnose.lastError", { message: lastErr.message }));
|
|
132
|
+
const errTail = tail(res.stderr) || tail(res.stdout);
|
|
133
|
+
if (errTail) parts.push(t("bub.diagnose.outputTail", { tail: errTail }));
|
|
134
|
+
return parts.join(" · ");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function tail(s: string, n = 6): string {
|
|
138
|
+
return s.trim().split("\n").filter(Boolean).slice(-n).join(" ⏎ ").slice(0, 600);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function bubAgent(config?: BubConfig): Agent {
|
|
142
|
+
const getApiKey = () => config?.apiKey ?? requireEnv("BUB_API_KEY");
|
|
143
|
+
const getApiBase = () => config?.apiBase ?? requireEnv("BUB_API_BASE");
|
|
144
|
+
// sandboxId → { home, workspace }; persists values detected in setup() so send() can use them.
|
|
145
|
+
const sessionInfo = new Map<string, { home: string; workspace: string }>();
|
|
146
|
+
|
|
147
|
+
return defineSandboxAgent({
|
|
148
|
+
name: "bub",
|
|
149
|
+
capabilities: { conversation: true, toolObservability: true, workspace: true, compactionObservability: true, tracing: true },
|
|
150
|
+
|
|
151
|
+
tracing: {
|
|
152
|
+
protocol: "http/protobuf",
|
|
153
|
+
env: (endpoint) => ({
|
|
154
|
+
BUB_TAPESTORE_OTEL_ENABLED: "true",
|
|
155
|
+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: endpoint,
|
|
156
|
+
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL: "http/protobuf",
|
|
157
|
+
}),
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
async setup(sb) {
|
|
161
|
+
const home = (await sb.runShell("printf '%s' $HOME")).stdout.trim() || "/home/node";
|
|
162
|
+
const workspace = DEFAULT_WORKSPACE;
|
|
163
|
+
sessionInfo.set(sb.sandboxId, { home, workspace });
|
|
164
|
+
await ensureBub(sb, home);
|
|
165
|
+
|
|
166
|
+
if (config?.pythonPlugins?.length) {
|
|
167
|
+
const extraWith = config.pythonPlugins.map((p) => `--with '${p}'`).join(" ");
|
|
168
|
+
await sb.runShell(
|
|
169
|
+
`${UV} tool install --reinstall --python 3.12 --prerelease allow 'bub' --overrides ${BUB_OVERRIDE_FILE} --with '${OTEL_PLUGIN}' ${extraWith}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!(await sb.fileExists(`${workspace}/AGENTS.md`))) {
|
|
174
|
+
await shared.writeFile(
|
|
175
|
+
sb,
|
|
176
|
+
`${workspace}/AGENTS.md`,
|
|
177
|
+
[
|
|
178
|
+
`You are a coding agent working in a Next.js project at ${workspace}.`,
|
|
179
|
+
``,
|
|
180
|
+
`Implement the requested feature by writing files directly to disk with the available tools:`,
|
|
181
|
+
`- fs_write(path, content): create or overwrite a file`,
|
|
182
|
+
`- fs_edit(path, old, new): edit an existing file`,
|
|
183
|
+
`- bash(cmd): run shell commands`,
|
|
184
|
+
``,
|
|
185
|
+
`Do NOT respond with only a text explanation — write the actual code files.`,
|
|
186
|
+
`After writing, verify with bash("cd ${workspace} && npm run build").`,
|
|
187
|
+
].join("\n"),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
async send(input, ctx) {
|
|
193
|
+
const sb = ctx.sandbox;
|
|
194
|
+
const info = sessionInfo.get(sb.sandboxId) ?? { home: "/home/node", workspace: DEFAULT_WORKSPACE };
|
|
195
|
+
const { home, workspace } = info;
|
|
196
|
+
const bubHome = `${home}/.bub`;
|
|
197
|
+
const model = ctx.model ?? "gpt-5.4";
|
|
198
|
+
const sessionId = `fe-${sb.sandboxId}`;
|
|
199
|
+
ctx.session.id = sessionId;
|
|
200
|
+
|
|
201
|
+
const env = {
|
|
202
|
+
BUB_API_KEY: getApiKey(),
|
|
203
|
+
BUB_API_BASE: getApiBase(),
|
|
204
|
+
BUB_MODEL: `openai:${model}`,
|
|
205
|
+
BUB_HOME: bubHome,
|
|
206
|
+
...ctx.telemetry?.env,
|
|
207
|
+
};
|
|
208
|
+
const escaped = input.text.replace(/'/g, "'\\''");
|
|
209
|
+
const res = await sb.runShell(
|
|
210
|
+
`${BUB} --workspace ${workspace} run '${escaped}' --session-id ${sessionId}`,
|
|
211
|
+
{ env, stream: true },
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const raw = await sb.readFile(tapePath(workspace, sessionId, bubHome)).catch(() => undefined);
|
|
215
|
+
const parsed = shared.parseBub(raw);
|
|
216
|
+
const events = [...parsed.events];
|
|
217
|
+
if (res.exitCode !== 0) events.push({ type: "error", message: diagnose(res, parsed.events, raw) });
|
|
218
|
+
return { events, usage: parsed.usage, status: res.exitCode === 0 ? "completed" : "failed" };
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export default bubAgent();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import claudeCodeDefault from "./claude-code.ts";
|
|
2
|
+
import codexDefault from "./codex.ts";
|
|
3
|
+
import bubDefault from "./bub.ts";
|
|
4
|
+
import type { Agent } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
export const BUILTIN_AGENTS: readonly Agent[] = [claudeCodeDefault, codexDefault, bubDefault];
|