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.
Files changed (94) hide show
  1. package/README.md +164 -0
  2. package/README.zh.md +160 -0
  3. package/bin/niceeval.js +11 -0
  4. package/package.json +96 -0
  5. package/src/agents/bub.ts +223 -0
  6. package/src/agents/builtin.ts +6 -0
  7. package/src/agents/claude-code.ts +96 -0
  8. package/src/agents/codex.ts +122 -0
  9. package/src/agents/index.ts +25 -0
  10. package/src/agents/shared.ts +145 -0
  11. package/src/cli.ts +421 -0
  12. package/src/context/context.test.ts +96 -0
  13. package/src/context/context.ts +427 -0
  14. package/src/context/control-flow.ts +27 -0
  15. package/src/context/session.ts +124 -0
  16. package/src/define.ts +112 -0
  17. package/src/expect/index.ts +243 -0
  18. package/src/i18n/en.ts +131 -0
  19. package/src/i18n/index.ts +39 -0
  20. package/src/i18n/zh-CN.ts +132 -0
  21. package/src/index.ts +39 -0
  22. package/src/loaders/index.ts +56 -0
  23. package/src/o11y/cost.ts +57 -0
  24. package/src/o11y/derive.ts +304 -0
  25. package/src/o11y/otlp/canonical.ts +112 -0
  26. package/src/o11y/otlp/mappers/bub.ts +30 -0
  27. package/src/o11y/otlp/mappers/codex.ts +38 -0
  28. package/src/o11y/otlp/mappers/index.ts +25 -0
  29. package/src/o11y/otlp/parse.ts +330 -0
  30. package/src/o11y/otlp/receiver.ts +100 -0
  31. package/src/o11y/otlp/sandbox-receiver.ts +113 -0
  32. package/src/o11y/otlp/select.ts +82 -0
  33. package/src/o11y/parsers/bub.ts +240 -0
  34. package/src/o11y/parsers/claude-code.ts +270 -0
  35. package/src/o11y/parsers/codex.ts +480 -0
  36. package/src/o11y/parsers/index.ts +37 -0
  37. package/src/o11y/prices.json +9873 -0
  38. package/src/runner/discover.ts +77 -0
  39. package/src/runner/reporters/artifacts.ts +81 -0
  40. package/src/runner/reporters/console.ts +76 -0
  41. package/src/runner/reporters/index.ts +5 -0
  42. package/src/runner/reporters/json.ts +52 -0
  43. package/src/runner/reporters/live.ts +178 -0
  44. package/src/runner/reporters/table.ts +328 -0
  45. package/src/runner/run.ts +860 -0
  46. package/src/runner/sandbox-prep.ts +91 -0
  47. package/src/sandbox/checkpoint.ts +39 -0
  48. package/src/sandbox/docker.ts +539 -0
  49. package/src/sandbox/e2b.ts +203 -0
  50. package/src/sandbox/index.ts +25 -0
  51. package/src/sandbox/registry.ts +60 -0
  52. package/src/sandbox/resolve.ts +131 -0
  53. package/src/sandbox/source-files.ts +30 -0
  54. package/src/sandbox/vercel.ts +236 -0
  55. package/src/scoring/collector.ts +113 -0
  56. package/src/scoring/judge.ts +236 -0
  57. package/src/scoring/scoped.ts +289 -0
  58. package/src/scoring/verdict.ts +20 -0
  59. package/src/source-loc.ts +53 -0
  60. package/src/types.ts +913 -0
  61. package/src/util.test.ts +31 -0
  62. package/src/util.ts +50 -0
  63. package/src/view/app/App.tsx +189 -0
  64. package/src/view/app/components/AttemptModal.tsx +66 -0
  65. package/src/view/app/components/CodeView.tsx +272 -0
  66. package/src/view/app/components/CopyControls.tsx +89 -0
  67. package/src/view/app/components/ExperimentTable.tsx +266 -0
  68. package/src/view/app/components/GroupSelector.tsx +61 -0
  69. package/src/view/app/components/LazyArtifact.tsx +50 -0
  70. package/src/view/app/components/Trace.tsx +100 -0
  71. package/src/view/app/components/Transcript.tsx +130 -0
  72. package/src/view/app/components/primitives.tsx +43 -0
  73. package/src/view/app/components/ui/badge.tsx +21 -0
  74. package/src/view/app/components/ui/dialog.tsx +34 -0
  75. package/src/view/app/components/ui/tabs.tsx +30 -0
  76. package/src/view/app/i18n.ts +341 -0
  77. package/src/view/app/index.html +13 -0
  78. package/src/view/app/lib/cn.ts +7 -0
  79. package/src/view/app/lib/format.ts +73 -0
  80. package/src/view/app/lib/guards.ts +61 -0
  81. package/src/view/app/lib/outcome.ts +96 -0
  82. package/src/view/app/lib/rows.ts +63 -0
  83. package/src/view/app/lib/transcript-data.tsx +121 -0
  84. package/src/view/app/main.tsx +17 -0
  85. package/src/view/app/pages/RunsPage.tsx +83 -0
  86. package/src/view/app/pages/TracesPage.tsx +40 -0
  87. package/src/view/app/shared.ts +10 -0
  88. package/src/view/app/types.ts +114 -0
  89. package/src/view/app/vite.config.ts +26 -0
  90. package/src/view/client-dist/app.css +2 -0
  91. package/src/view/client-dist/app.js +56 -0
  92. package/src/view/index.ts +406 -0
  93. package/src/view/styles.css +1074 -0
  94. package/src/view/template.html +15 -0
@@ -0,0 +1,243 @@
1
+ // 值级断言匹配器(expect)。每个匹配器产出一个 ValueAssertion:纯函数 score +
2
+ // 可链式改严重级 / 阈值。链式方法返回全新的不可变 ValueAssertion,复用同一个 score。
3
+
4
+ import type { Severity, ValueAssertion } from "../types.ts";
5
+ import { stripComments } from "../util.ts";
6
+
7
+ /** includes / excludes 的可选项。stripComments:先剥注释再匹配(只对真实代码生效)。 */
8
+ export interface MatchOptions {
9
+ stripComments?: boolean;
10
+ }
11
+
12
+ // ───────────────────────── 内部工厂 ─────────────────────────
13
+
14
+ /**
15
+ * 唯一的内部工厂。gate()/atLeast() 都基于它返回新的不可变实例,
16
+ * 共享同一个 score(只换 severity / threshold)。
17
+ */
18
+ function createAssertion(
19
+ name: string,
20
+ severity: Severity,
21
+ score: (value: unknown) => number | Promise<number>,
22
+ threshold?: number,
23
+ ): ValueAssertion {
24
+ const self: ValueAssertion = {
25
+ name,
26
+ severity,
27
+ threshold,
28
+ score,
29
+ // 转成硬门槛(失败即整条 eval 不通过)。
30
+ gate: (t?: number) => createAssertion(name, "gate", score, t),
31
+ // 软阈值:默认不改变 outcome;--strict 下软阈值失败也会使 outcome=failed。
32
+ atLeast: (t: number) => createAssertion(name, "soft", score, t),
33
+ };
34
+ return Object.freeze(self);
35
+ }
36
+
37
+ // ───────────────────────── 工具函数 ─────────────────────────
38
+
39
+ /** 给断言起个可读名时用,JSON.stringify 失败 / 为 undefined 时回退到 String。 */
40
+ function safeLabel(v: unknown): string {
41
+ try {
42
+ return JSON.stringify(v) ?? String(v);
43
+ } catch {
44
+ return String(v);
45
+ }
46
+ }
47
+
48
+ /** 小而全的深比较:处理基本值、NaN、数组、Date、纯对象。 */
49
+ function deepEqual(a: unknown, b: unknown): boolean {
50
+ if (a === b) return true;
51
+
52
+ // NaN === NaN
53
+ if (typeof a === "number" && typeof b === "number") {
54
+ return Number.isNaN(a) && Number.isNaN(b);
55
+ }
56
+
57
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
58
+ return false;
59
+ }
60
+
61
+ // Date 按时间戳比
62
+ if (a instanceof Date || b instanceof Date) {
63
+ return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
64
+ }
65
+
66
+ const aIsArr = Array.isArray(a);
67
+ const bIsArr = Array.isArray(b);
68
+ if (aIsArr !== bIsArr) return false;
69
+
70
+ if (aIsArr && bIsArr) {
71
+ if (a.length !== b.length) return false;
72
+ for (let i = 0; i < a.length; i++) {
73
+ if (!deepEqual(a[i], b[i])) return false;
74
+ }
75
+ return true;
76
+ }
77
+
78
+ const ao = a as Record<string, unknown>;
79
+ const bo = b as Record<string, unknown>;
80
+ const ak = Object.keys(ao);
81
+ const bk = Object.keys(bo);
82
+ if (ak.length !== bk.length) return false;
83
+ for (const k of ak) {
84
+ if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
85
+ if (!deepEqual(ao[k], bo[k])) return false;
86
+ }
87
+ return true;
88
+ }
89
+
90
+ /** 经典 DP 编辑距离(滚动两行)。 */
91
+ function levenshtein(a: string, b: string): number {
92
+ const m = a.length;
93
+ const n = b.length;
94
+ if (m === 0) return n;
95
+ if (n === 0) return m;
96
+
97
+ let prev = new Array<number>(n + 1);
98
+ let curr = new Array<number>(n + 1);
99
+ for (let j = 0; j <= n; j++) prev[j] = j;
100
+
101
+ for (let i = 1; i <= m; i++) {
102
+ curr[0] = i;
103
+ for (let j = 1; j <= n; j++) {
104
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
105
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
106
+ }
107
+ const tmp = prev;
108
+ prev = curr;
109
+ curr = tmp;
110
+ }
111
+ return prev[n];
112
+ }
113
+
114
+ // ───────────────────────── 匹配器 ─────────────────────────
115
+
116
+ /** 把 value 转成待匹配字符串;opts.stripComments 时先剥注释。 */
117
+ function toMatchTarget(value: unknown, opts?: MatchOptions): string {
118
+ const s = String(value);
119
+ return opts?.stripComments ? stripComments(s) : s;
120
+ }
121
+
122
+ /** String(value) 含子串 / 命中正则则 1,否则 0。默认硬门槛。opts.stripComments 时只看真实代码。 */
123
+ export function includes(needle: string | RegExp, opts?: MatchOptions): ValueAssertion {
124
+ const label = needle instanceof RegExp ? needle.toString() : safeLabel(needle);
125
+ const suffix = opts?.stripComments ? ", stripComments" : "";
126
+ return createAssertion(`includes(${label}${suffix})`, "gate", (value) => {
127
+ const s = toMatchTarget(value, opts);
128
+ if (needle instanceof RegExp) return needle.test(s) ? 1 : 0;
129
+ return s.includes(needle) ? 1 : 0;
130
+ });
131
+ }
132
+
133
+ /** includes 的取反:不含子串 / 不命中正则则 1,否则 0。默认硬门槛。opts.stripComments 时只看真实代码。 */
134
+ export function excludes(needle: string | RegExp, opts?: MatchOptions): ValueAssertion {
135
+ const label = needle instanceof RegExp ? needle.toString() : safeLabel(needle);
136
+ const suffix = opts?.stripComments ? ", stripComments" : "";
137
+ return createAssertion(`excludes(${label}${suffix})`, "gate", (value) => {
138
+ const s = toMatchTarget(value, opts);
139
+ if (needle instanceof RegExp) return needle.test(s) ? 0 : 1;
140
+ return s.includes(needle) ? 0 : 1;
141
+ });
142
+ }
143
+
144
+ /** 深相等则 1,否则 0。默认硬门槛。 */
145
+ export function equals(expected: unknown): ValueAssertion {
146
+ return createAssertion(`equals(${safeLabel(expected)})`, "gate", (value) =>
147
+ deepEqual(value, expected) ? 1 : 0,
148
+ );
149
+ }
150
+
151
+ /**
152
+ * 用 schema 校验 value。优先 Standard Schema(schema['~standard'].validate),
153
+ * 否则退化到 zod 风格的 .safeParse / .parse。校验通过 1,否则 0;任何异常 → 0。
154
+ */
155
+ export function matches(schema: unknown): ValueAssertion {
156
+ return createAssertion("matches(schema)", "gate", async (value) => {
157
+ try {
158
+ const std = (schema as { ["~standard"]?: { validate?: (v: unknown) => unknown } } | null)?.[
159
+ "~standard"
160
+ ];
161
+ if (std && typeof std.validate === "function") {
162
+ // validate 可能同步也可能返回 Promise;成功结果不带 issues。
163
+ const result = (await std.validate(value)) as { issues?: unknown } | null | undefined;
164
+ return result != null && result.issues == null ? 1 : 0;
165
+ }
166
+
167
+ const zodish = schema as {
168
+ safeParse?: (v: unknown) => { success?: boolean };
169
+ parse?: (v: unknown) => unknown;
170
+ } | null;
171
+
172
+ if (zodish && typeof zodish.safeParse === "function") {
173
+ const result = zodish.safeParse(value);
174
+ return result && result.success ? 1 : 0;
175
+ }
176
+ if (zodish && typeof zodish.parse === "function") {
177
+ zodish.parse(value);
178
+ return 1;
179
+ }
180
+ return 0;
181
+ } catch {
182
+ return 0;
183
+ }
184
+ });
185
+ }
186
+
187
+ /** 归一化 Levenshtein 相似度 [0,1]。默认软分,阈值 0.6。 */
188
+ export function similarity(expected: string): ValueAssertion {
189
+ return createAssertion(
190
+ `similarity(${safeLabel(expected)})`,
191
+ "soft",
192
+ (value) => {
193
+ const a = String(value);
194
+ const b = expected;
195
+ const maxLen = Math.max(a.length, b.length);
196
+ if (maxLen === 0) return 1; // 两个空串视为完全相同
197
+ return 1 - levenshtein(a, b) / maxLen;
198
+ },
199
+ 0.6,
200
+ );
201
+ }
202
+
203
+ /** 谓词为真则 1,否则 0。默认硬门槛;name 带上 label 便于报告辨认。 */
204
+ export function satisfies(predicate: (v: unknown) => boolean, label?: string): ValueAssertion {
205
+ const name = label ? `satisfies(${label})` : "satisfies(predicate)";
206
+ return createAssertion(name, "gate", (value) => (predicate(value) ? 1 : 0));
207
+ }
208
+
209
+ /** value 非 null / 非 undefined 则 1,否则 0。省掉 `x !== undefined` + isTrue 的样板。 */
210
+ export function isDefined(label?: string): ValueAssertion {
211
+ const name = label ? `isDefined(${label})` : "isDefined()";
212
+ return createAssertion(name, "gate", (value) => (value != null ? 1 : 0));
213
+ }
214
+
215
+ /** value === true 则 1,否则 0。带 label 的布尔断言(fileExists 等检查用)。 */
216
+ export function isTrue(label?: string): ValueAssertion {
217
+ const name = label ? `isTrue(${label})` : "isTrue()";
218
+ return createAssertion(name, "gate", (value) => (value === true ? 1 : 0));
219
+ }
220
+
221
+ /** CommandResult.exitCode === 0 则通过。 */
222
+ export function commandSucceeded(): ValueAssertion {
223
+ return createAssertion("commandSucceeded()", "gate", (value) => {
224
+ if (value === null || typeof value !== "object") return 0;
225
+ return (value as { exitCode?: unknown }).exitCode === 0 ? 1 : 0;
226
+ });
227
+ }
228
+
229
+ /** value === false 则 1,否则 0。带 label 的布尔断言。 */
230
+ export function isFalse(label?: string): ValueAssertion {
231
+ const name = label ? `isFalse(${label})` : "isFalse()";
232
+ return createAssertion(name, "gate", (value) => (value === false ? 1 : 0));
233
+ }
234
+
235
+ /** 自定义断言工厂:直接给名字 / 严重级 / 阈值 / score。severity 省略默认 gate。 */
236
+ export function makeAssertion(spec: {
237
+ name: string;
238
+ severity?: Severity;
239
+ threshold?: number;
240
+ score: (value: unknown) => number | Promise<number>;
241
+ }): ValueAssertion {
242
+ return createAssertion(spec.name, spec.severity ?? "gate", spec.score, spec.threshold);
243
+ }
package/src/i18n/en.ts ADDED
@@ -0,0 +1,131 @@
1
+ import type { Messages } from "./zh-CN.ts";
2
+
3
+ export const en = {
4
+ "agent.installFailed": "Install failed: {{key}}\n{{tail}}",
5
+ "agent.unknown": "Unknown agent \"{{name}}\". Registered agents: {{known}}.",
6
+ "bub.diagnose.exitCode": "bub run exited with code {{code}}",
7
+ "bub.diagnose.lastError": "last error: {{message}}",
8
+ "bub.diagnose.noTape": "tape was not generated",
9
+ "bub.diagnose.outputTail": "output tail: {{tail}}",
10
+ "bub.diagnose.zeroEvents": "tape exists but contains 0 events",
11
+ "bub.installFailed": "bub install failed after {{attempts}} attempts:\n{{tail}}",
12
+ "checkpoint.emptyTar": "checkpoint: tar is empty (paths: {{paths}})",
13
+ "cli.all": "(all)",
14
+ "cli.browserOpenFailed": "Could not open the browser automatically. Open manually: {{url}}\n",
15
+ "cli.clean.done": "Deleted .niceeval/ historical run artifacts.\n",
16
+ "cli.config.missing": "Could not find niceeval.config.ts (run this from the project root).",
17
+ "cli.config.noDefault": "niceeval.config.ts must default export defineConfig(...).",
18
+ "cli.dry.header": "\n[dry] {{evals}} evals × {{configs}} run configs:\n",
19
+ "cli.dry.noMatches": "(no matches)",
20
+ "cli.dry.row": " {{who}}{{experiment}}: {{evals}} ×{{runs}}\n",
21
+ "cli.error": "niceeval error: {{error}}\n",
22
+ "cli.eval.noMatch": "No eval matched: {{patterns}}.\n",
23
+ "cli.eval.noMatchHintExperiment": "Hint: \"{{pattern}}\" is an experiment{{kind}}; you probably meant: niceeval exp {{pattern}}\n",
24
+ "cli.eval.noMatchKnown": "Discovered {{count}} evals: {{evals}}\n",
25
+ "cli.exp.agentModelFlagUnsupported": "`--agent` / `--model` cannot override an experiment. Add or copy a config file under experiments/ instead.\n",
26
+ "cli.experiment.noMatch": "No experiment matched: {{arg}}. Discovered: {{experiments}}\n",
27
+ "cli.experimentGroup": " group",
28
+ "cli.fallbackCleanupTimeout": "\ngraceful cleanup timed out; force-cleaning sandboxes...\n",
29
+ "cli.forceCleanupExit": "\nForce-cleaning sandboxes and exiting...\n",
30
+ "cli.init.done": "Created evals/ and starter niceeval.config.ts.\n",
31
+ "cli.interruptCleanup": "\nInterrupted; cleaning up sandbox containers... (press again to force cleanup and exit)\n",
32
+ "cli.list.header": "Discovered {{count}} evals:\n",
33
+ "cli.noAgent": "No agent specified (use --agent <name>).\n",
34
+ "cli.none": "(none)",
35
+ "cli.pressCtrlC": "Press Ctrl+C to exit.\n",
36
+ "cli.run.experimentRequired": "Run evals through an experiment: use `niceeval exp [group|config] [eval id prefix]`.\n",
37
+ "cli.run.experimentRequiredHint": "Hint: \"{{pattern}}\" is an experiment{{kind}}; you probably meant: niceeval exp {{pattern}}\n",
38
+ "cli.run.experimentRequiredKnown": "Discovered experiments: {{experiments}}\n",
39
+ "cli.unimplemented": "Command \"{{command}}\" is not implemented yet (MVP).\n",
40
+ "cli.view.exported": "Exported eval report page: {{out}}\n",
41
+ "cli.view.url": "niceeval view: {{url}}\n",
42
+ "context.skipEmpty": "skip() requires a non-empty reason.",
43
+ "context.turnFailed": "This send returned failed (turn status = failed): {{message}}",
44
+ "context.turnFailedDefault": "This send returned failed (turn status = failed)",
45
+ "define.agentNameRequired": "defineAgent requires name.",
46
+ "define.evalIdRejected": "defineEval does not accept id; ids are derived from file paths.",
47
+ "define.evalTestRequired": "defineEval requires an async test(t) function.",
48
+ "define.experimentAgentRequired": "defineExperiment requires agent.",
49
+ "define.experimentIdRejected": "defineExperiment does not accept id; ids are derived from file paths.",
50
+ "define.sandboxAgentNameRequired": "defineSandboxAgent requires name.",
51
+ "define.sandboxCreateRequired": "defineSandbox requires a create() function.",
52
+ "define.sandboxNameRequired": "defineSandbox requires name.",
53
+ "docker.commandTimeout": "Command timed out after {{timeoutMs}}ms",
54
+ "docker.containerNotInitialized": "Container not initialized",
55
+ "docker.imagePullDone": "Docker image ready: {{image}}",
56
+ "docker.imagePullStart": "Pulling Docker image: {{image}}...",
57
+ "docker.readFileFailed": "Failed to read file {{path}}: {{stderr}}",
58
+ "docker.unsupportedRuntime": "Unsupported runtime: {{runtime}}",
59
+ "judge.apiKeyMissing": "judge is missing an API key (CODEX_API_KEY / OPENAI_API_KEY).",
60
+ "judge.httpError": "judge HTTP {{status}}: {{body}}",
61
+ "judge.probeFailed": "judge precheck failed ({{model}}): {{error}}",
62
+ "judge.probeMissingKey": "judge model {{model}} is missing an API key; configure {{envHint}}",
63
+ "live.running": " Running {{totalRuns}} attempts ({{evals}} evals × {{configs}} configs) {{completed}}/{{total}} done",
64
+ "live.runningUnknown": " Running... {{completed}}/{{total}} done",
65
+ "report.assertionThreshold": " (got {{score}} < {{threshold}})",
66
+ "report.error": "error",
67
+ "report.errored": "errored",
68
+ "report.failed": "failed",
69
+ "report.gate": "gate",
70
+ "report.passed": "passed",
71
+ "report.result": "\nResult: {{parts}} ({{duration}} · {{tokens}}{{cost}})\n\n",
72
+ "report.runStart": "\nRunning {{count}} evals{{extra}}\n\n",
73
+ "report.runStartExtra": " × {{configs}} configs = {{totalRuns}} runs",
74
+ "report.skipped": "skipped",
75
+ "report.soft": "soft",
76
+ "report.summary.errored": "{{count}} errored",
77
+ "report.summary.failed": "{{count}} failed",
78
+ "report.summary.passed": "{{count}} passed",
79
+ "report.summary.skipped": "{{count}} skipped",
80
+ "report.table.agent": "Agent",
81
+ "report.table.avgDuration": "Avg Duration",
82
+ "report.table.cost": "Cost",
83
+ "report.table.default": "default",
84
+ "report.table.duration": "Duration",
85
+ "report.table.eval": "Eval",
86
+ "report.table.evalTitle": "Eval Results:",
87
+ "report.table.experiment": "Experiment",
88
+ "report.table.experimentsTitle": "Experiments",
89
+ "report.table.model": "Model",
90
+ "report.table.reason": "Reason",
91
+ "report.table.result": "Result",
92
+ "report.table.runs": "Runs",
93
+ "report.table.status": "Status",
94
+ "report.table.successRate": "Success Rate",
95
+ "report.table.tokens": "Tokens",
96
+ "runner.diffProgress": "captured diff: {{changed}} changed / {{deleted}} deleted",
97
+ "runner.driveAgent": "driving agent...",
98
+ "runner.evalSetup": "eval setup (installing dependencies)...",
99
+ "runner.interrupted": " · interrupted: sandbox containers cleaned up; printing partial results completed so far.\n",
100
+ "runner.judgePrecheck": " · prechecking judge config...\n",
101
+ "runner.otlpInSandbox": "OTLP in-sandbox collector -> {{endpoint}}{{proto}}",
102
+ "runner.otlpOverride": "OTLP receiver (host override) -> {{endpoint}}",
103
+ "runner.otlpReceiver": "OTLP receiver -> {{endpoint}}{{proto}}",
104
+ "runner.remoteSandboxUnavailable": "remote agents do not have sandbox.{{method}}; use a sandbox agent or remove workspace assertions.",
105
+ "runner.reporterDiagnostic": " · [diagnostic] {{stage}} failed (ignored): {{message}}\n",
106
+ "runner.scoreJudge": "scoring / judge...",
107
+ "runner.skip": "skip: {{reason}}",
108
+ "runner.startAgentSetup": "agent setup (install CLI / write config)...",
109
+ "runner.startAgentTracing": "agent tracing (write OTEL export config)...",
110
+ "runner.startSandbox": "starting sandbox...",
111
+ "runner.timeout": "attempt timed out ({{timeoutMs}}ms)\nRecent progress:\n{{recentLogs}}",
112
+ "runner.traceSelected": " -> kept {{count}} semantic spans",
113
+ "runner.resumeCarry": " · reusing {{carried}} passing results from last run, re-running {{retry}} evals\n",
114
+ "runner.useRemoteAgent": "using remote agent (no sandbox created)...",
115
+ "sandbox.backendNotImplemented": "{{backend}} sandbox backend is not implemented; use docker, vercel, or e2b",
116
+ "sandbox.dependencyMissing.docker": "Docker sandbox requires 'dockerode'. Install it with: pnpm add dockerode @types/dockerode",
117
+ "sandbox.dependencyMissing.e2b": "E2B sandbox requires 'e2b'. Install it with: pnpm add e2b",
118
+ "sandbox.dependencyMissing.vercel": "Vercel sandbox requires '@vercel/sandbox'. Install it with: pnpm add @vercel/sandbox",
119
+ "sandbox.forceCleanup": " · [sandbox] force-cleaning {{count}} sandboxes...\n",
120
+ "sandbox.stopFailed": " · [sandbox] failed to stop sandbox {{id}} (ignored; backend TTL should clean it up): {{message}}\n",
121
+ "sandbox.stopTimeout": "stop timed out ({{timeoutMs}}ms)",
122
+ "scoring.evalError": "evaluation error: {{error}}",
123
+ "session.fileFallback": "[file]",
124
+ "session.tools": "{{count}} tools",
125
+ "session.turn.primary": "turn {{turn}}",
126
+ "session.turn.secondary": "session {{session}} · turn {{turn}}",
127
+ "util.requiredEnv": "Missing required environment variable {{name}} (configure it in .env).",
128
+ "vercel.fileNotFound": "File not found: {{path}}",
129
+ "vercel.rotateFailed": "[VercelSandbox] session rotate failed ({{seconds}}s): {{error}}",
130
+ "vercel.rotated": "[VercelSandbox] session rotated after {{seconds}}s -> {{sessionId}}",
131
+ } satisfies Messages;
@@ -0,0 +1,39 @@
1
+ import { en } from "./en.ts";
2
+ import { zhCN, type MessageKey, type Messages } from "./zh-CN.ts";
3
+
4
+ export type Locale = "zh-CN" | "en";
5
+
6
+ type Vars = Record<string, string | number | boolean | undefined>;
7
+
8
+ const dictionaries: Record<Locale, Messages> = {
9
+ "zh-CN": zhCN,
10
+ en,
11
+ };
12
+
13
+ function normalizeLocale(raw: string | undefined): Locale | undefined {
14
+ if (!raw) return undefined;
15
+ const value = raw.trim().toLowerCase().replace("_", "-");
16
+ if (!value) return undefined;
17
+ if (value === "c" || value === "posix") return undefined;
18
+ if (value.startsWith("zh")) return "zh-CN";
19
+ if (value.startsWith("en")) return "en";
20
+ return "en";
21
+ }
22
+
23
+ export function detectLocale(env: NodeJS.ProcessEnv = process.env): Locale {
24
+ return (
25
+ normalizeLocale(env.NICEEVAL_LANG) ??
26
+ normalizeLocale(env.NICEEVAL_LOCALE) ??
27
+ normalizeLocale(env.LC_ALL) ??
28
+ normalizeLocale(env.LC_MESSAGES) ??
29
+ normalizeLocale(env.LANG) ??
30
+ "zh-CN"
31
+ );
32
+ }
33
+
34
+ export function t(key: MessageKey, vars: Vars = {}): string {
35
+ const message = dictionaries[detectLocale()][key];
36
+ return message.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_match, name: string) =>
37
+ vars[name] === undefined ? "" : String(vars[name]),
38
+ );
39
+ }
@@ -0,0 +1,132 @@
1
+ export const zhCN = {
2
+ "agent.installFailed": "安装失败:{{key}}\n{{tail}}",
3
+ "agent.unknown": "未知 agent \"{{name}}\"。已注册:{{known}}。",
4
+ "bub.diagnose.exitCode": "bub run 退出码 {{code}}",
5
+ "bub.diagnose.lastError": "最后错误:{{message}}",
6
+ "bub.diagnose.noTape": "tape 未生成",
7
+ "bub.diagnose.outputTail": "输出末尾:{{tail}}",
8
+ "bub.diagnose.zeroEvents": "tape 存在但 0 事件",
9
+ "bub.installFailed": "bub 安装失败(重试 {{attempts}} 次):\n{{tail}}",
10
+ "checkpoint.emptyTar": "checkpoint: tar 为空(paths: {{paths}})",
11
+ "cli.all": "(全部)",
12
+ "cli.browserOpenFailed": "无法自动打开浏览器,请手动访问:{{url}}\n",
13
+ "cli.clean.done": "已删除 .niceeval/ 历史运行工件。\n",
14
+ "cli.config.missing": "找不到 niceeval.config.ts(请在项目根运行)。",
15
+ "cli.config.noDefault": "niceeval.config.ts 需要 default export(defineConfig(...))。",
16
+ "cli.dry.header": "\n[dry] {{evals}} 个 eval × {{configs}} 个运行配置:\n",
17
+ "cli.dry.noMatches": "(无匹配)",
18
+ "cli.dry.row": " {{who}}{{experiment}}: {{evals}} ×{{runs}}\n",
19
+ "cli.error": "niceeval 出错:{{error}}\n",
20
+ "cli.eval.noMatch": "没有匹配的 eval:{{patterns}}。\n",
21
+ "cli.eval.noMatchHintExperiment": "提示:\"{{pattern}}\" 是实验{{kind}},你大概想跑:niceeval exp {{pattern}}\n",
22
+ "cli.eval.noMatchKnown": "已发现 {{count}} 个 eval:{{evals}}\n",
23
+ "cli.exp.agentModelFlagUnsupported": "`--agent` / `--model` 不能覆盖 experiment。请在 experiments/ 里新增或复制一个配置文件。\n",
24
+ "cli.experiment.noMatch": "没有匹配的实验:{{arg}}。已发现:{{experiments}}\n",
25
+ "cli.experimentGroup": "组",
26
+ "cli.fallbackCleanupTimeout": "\ngraceful 清理超时,强制清理沙箱…\n",
27
+ "cli.forceCleanupExit": "\n强制清理沙箱并退出…\n",
28
+ "cli.init.done": "已创建 evals/ 和 niceeval.config.ts 起始文件。\n",
29
+ "cli.interruptCleanup": "\n收到中断,正在清理沙箱容器…(再按一次强制清理并退出)\n",
30
+ "cli.list.header": "发现 {{count}} 个 eval:\n",
31
+ "cli.noAgent": "未指定 agent(用 --agent <name>)。\n",
32
+ "cli.none": "(无)",
33
+ "cli.pressCtrlC": "按 Ctrl+C 退出。\n",
34
+ "cli.run.experimentRequired": "运行 eval 必须通过 experiment:用 `niceeval exp [实验组|配置] [eval id 前缀]`。\n",
35
+ "cli.run.experimentRequiredHint": "提示:\"{{pattern}}\" 是实验{{kind}},你大概想跑:niceeval exp {{pattern}}\n",
36
+ "cli.run.experimentRequiredKnown": "已发现实验:{{experiments}}\n",
37
+ "cli.unimplemented": "命令 \"{{command}}\" 暂未实现(MVP)。\n",
38
+ "cli.view.exported": "已导出实验查看页:{{out}}\n",
39
+ "cli.view.url": "niceeval view: {{url}}\n",
40
+ "context.skipEmpty": "skip() 需要一个非空理由。",
41
+ "context.turnFailed": "本轮 send 返回 failed(turn status = failed):{{message}}",
42
+ "context.turnFailedDefault": "本轮 send 返回 failed(turn status = failed)",
43
+ "define.agentNameRequired": "defineAgent 需要 name。",
44
+ "define.evalIdRejected": "defineEval 不接受 id —— id 由文件路径推导。",
45
+ "define.evalTestRequired": "defineEval 需要一个 async test(t) 函数。",
46
+ "define.experimentAgentRequired": "defineExperiment 需要 agent。",
47
+ "define.experimentIdRejected": "defineExperiment 不接受 id —— id 由文件路径推导。",
48
+ "define.sandboxAgentNameRequired": "defineSandboxAgent 需要 name。",
49
+ "define.sandboxCreateRequired": "defineSandbox 需要一个 create() 函数。",
50
+ "define.sandboxNameRequired": "defineSandbox 需要 name。",
51
+ "docker.commandTimeout": "Command timed out after {{timeoutMs}}ms",
52
+ "docker.containerNotInitialized": "Container not initialized",
53
+ "docker.imagePullDone": "Docker image ready: {{image}}",
54
+ "docker.imagePullStart": "Pulling Docker image: {{image}}...",
55
+ "docker.readFileFailed": "Failed to read file {{path}}: {{stderr}}",
56
+ "docker.unsupportedRuntime": "Unsupported runtime: {{runtime}}",
57
+ "judge.apiKeyMissing": "judge 缺少 API key(CODEX_API_KEY / OPENAI_API_KEY)。",
58
+ "judge.httpError": "judge HTTP {{status}}: {{body}}",
59
+ "judge.probeFailed": "judge 预检失败({{model}}): {{error}}",
60
+ "judge.probeMissingKey": "judge 模型 {{model}} 缺少 API key —— 请配置 {{envHint}}",
61
+ "live.running": " 正在运行 {{totalRuns}} 次 ({{evals}} eval × {{configs}} 配置) {{completed}}/{{total}} 完成",
62
+ "live.runningUnknown": " 正在运行… {{completed}}/{{total}} 完成",
63
+ "report.assertionThreshold": " (得分 {{score}} < {{threshold}})",
64
+ "report.error": "错误",
65
+ "report.errored": "错误",
66
+ "report.failed": "失败",
67
+ "report.gate": "gate",
68
+ "report.passed": "通过",
69
+ "report.result": "\n结果:{{parts}} ({{duration}} · {{tokens}}{{cost}})\n\n",
70
+ "report.runStart": "\n本次运行 {{count}} 个 eval{{extra}}\n\n",
71
+ "report.runStartExtra": " × {{configs}} 配置 = {{totalRuns}} 次运行",
72
+ "report.skipped": "跳过",
73
+ "report.soft": "soft",
74
+ "report.summary.errored": "{{count}} 错误",
75
+ "report.summary.failed": "{{count}} 失败",
76
+ "report.summary.passed": "{{count}} 通过",
77
+ "report.summary.skipped": "{{count}} 跳过",
78
+ "report.table.agent": "Agent",
79
+ "report.table.avgDuration": "平均耗时",
80
+ "report.table.cost": "预估成本",
81
+ "report.table.default": "默认",
82
+ "report.table.duration": "耗时",
83
+ "report.table.eval": "Eval",
84
+ "report.table.evalTitle": "各 Eval:",
85
+ "report.table.experiment": "实验",
86
+ "report.table.experimentsTitle": "实验",
87
+ "report.table.model": "模型",
88
+ "report.table.reason": "原因",
89
+ "report.table.result": "结果",
90
+ "report.table.runs": "轮次",
91
+ "report.table.status": "状态",
92
+ "report.table.successRate": "成功率",
93
+ "report.table.tokens": "Tokens",
94
+ "runner.diffProgress": "采 diff:{{changed}} 改 / {{deleted}} 删",
95
+ "runner.driveAgent": "驱动 agent…",
96
+ "runner.evalSetup": "eval setup(装依赖)…",
97
+ "runner.interrupted": " · 已中断:沙箱容器已清理,输出本次已完成的部分结果。\n",
98
+ "runner.judgePrecheck": " · 预检 judge 配置…\n",
99
+ "runner.otlpInSandbox": "OTLP in-sandbox collector → {{endpoint}}{{proto}}",
100
+ "runner.otlpOverride": "OTLP 接收器(覆盖 host) → {{endpoint}}",
101
+ "runner.otlpReceiver": "OTLP 接收器 → {{endpoint}}{{proto}}",
102
+ "runner.remoteSandboxUnavailable": "remote agent 没有 sandbox.{{method}};请改用 sandbox agent 或移除 workspace 断言。",
103
+ "runner.reporterDiagnostic": " · [diagnostic] {{stage}} 失败(已忽略):{{message}}\n",
104
+ "runner.scoreJudge": "评分 / judge…",
105
+ "runner.skip": "skip:{{reason}}",
106
+ "runner.startAgentSetup": "agent setup(装 CLI / 写配置)…",
107
+ "runner.startAgentTracing": "agent tracing(写 otel 导出配置)…",
108
+ "runner.startSandbox": "起沙箱…",
109
+ "runner.timeout": "attempt 超时({{timeoutMs}}ms)\n最近进度:\n{{recentLogs}}",
110
+ "runner.traceSelected": " → 留 {{count}}(按语义)",
111
+ "runner.resumeCarry": " · 复用上次 {{carried}} 个通过的结果,重跑 {{retry}} 个 eval\n",
112
+ "runner.useRemoteAgent": "使用 remote agent(不创建沙箱)…",
113
+ "sandbox.backendNotImplemented": "{{backend}} sandbox backend not implemented; use docker, vercel, or e2b",
114
+ "sandbox.dependencyMissing.docker": "Docker sandbox requires 'dockerode'. Install it with: pnpm add dockerode @types/dockerode",
115
+ "sandbox.dependencyMissing.e2b": "E2B sandbox requires 'e2b'. Install it with: pnpm add e2b",
116
+ "sandbox.dependencyMissing.vercel": "Vercel sandbox requires '@vercel/sandbox'. Install it with: pnpm add @vercel/sandbox",
117
+ "sandbox.forceCleanup": " · [sandbox] 强制清理 {{count}} 个沙箱…\n",
118
+ "sandbox.stopFailed": " · [sandbox] 停沙箱 {{id}} 失败(已忽略,靠后端过期兜底):{{message}}\n",
119
+ "sandbox.stopTimeout": "stop 超时({{timeoutMs}}ms)",
120
+ "scoring.evalError": "评估出错: {{error}}",
121
+ "session.fileFallback": "[file]",
122
+ "session.tools": "{{count}} 工具",
123
+ "session.turn.primary": "第{{turn}}轮",
124
+ "session.turn.secondary": "会话{{session}}·第{{turn}}轮",
125
+ "util.requiredEnv": "缺少必需的环境变量 {{name}}(请在 .env 里配置)。",
126
+ "vercel.fileNotFound": "File not found: {{path}}",
127
+ "vercel.rotateFailed": "[VercelSandbox] session rotate failed ({{seconds}}s): {{error}}",
128
+ "vercel.rotated": "[VercelSandbox] session rotated after {{seconds}}s → {{sessionId}}",
129
+ } as const;
130
+
131
+ export type MessageKey = keyof typeof zhCN;
132
+ export type Messages = Record<MessageKey, string>;
package/src/index.ts ADDED
@@ -0,0 +1,39 @@
1
+ // niceeval 公开导出(import { … } from "niceeval")。
2
+ // Agent/Adapter 相关见 "niceeval/adapter";Sandbox 相关见 "niceeval/sandbox"。
3
+
4
+ export { defineEval, defineConfig, defineExperiment } from "./define.ts";
5
+
6
+ export { requireEnv, getEnv, stripComments } from "./util.ts";
7
+
8
+ // 类型(eval 作者会用到;跑哪个 agent / 用哪个 sandbox 见对应子路径)
9
+ export type {
10
+ StreamEvent,
11
+ ToolName,
12
+ JsonValue,
13
+ Usage,
14
+ Turn,
15
+ TurnInput,
16
+ InputFile,
17
+ TurnHandle,
18
+ SessionHandle,
19
+ TestContext,
20
+ ToolMatch,
21
+ ValueAssertion,
22
+ Severity,
23
+ ResultOutcome,
24
+ EvalDef,
25
+ ExperimentDef,
26
+ Config,
27
+ LocalizedText,
28
+ JudgeConfig,
29
+ Reporter,
30
+ ReporterEvent,
31
+ EvalResult,
32
+ RunSummary,
33
+ O11ySummary,
34
+ TraceSpan,
35
+ SpanKind,
36
+ DerivedFacts,
37
+ } from "./types.ts";
38
+
39
+ export type { ParsedTranscript } from "./o11y/parsers/index.ts";
@@ -0,0 +1,56 @@
1
+ // 数据集加载器:把 YAML / JSON 读进来,配 .map(row => defineEval(...)) 扇出。
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+
6
+ export async function loadJson<T = unknown>(path: string): Promise<T> {
7
+ const raw = await readFile(resolve(process.cwd(), path), "utf-8");
8
+ return JSON.parse(raw) as T;
9
+ }
10
+
11
+ export async function loadYaml<T = unknown>(path: string): Promise<T> {
12
+ const raw = await readFile(resolve(process.cwd(), path), "utf-8");
13
+ // 尽量用真正的 yaml 解析器(若项目装了);否则退回极简解析。
14
+ // 用变量 specifier 避免 tsc 静态解析这个可选依赖。
15
+ const yamlPkg = "yaml";
16
+ try {
17
+ const yaml = (await import(yamlPkg)) as { parse(s: string): unknown };
18
+ return yaml.parse(raw) as T;
19
+ } catch {
20
+ return parseSimpleYaml(raw) as T;
21
+ }
22
+ }
23
+
24
+ /** 极简 YAML(只够 cases 列表这类扁平结构;复杂结构请 `pnpm add yaml`)。 */
25
+ function parseSimpleYaml(text: string): unknown {
26
+ const lines = text.split("\n").filter((l) => l.trim() && !l.trim().startsWith("#"));
27
+ const root: Record<string, unknown> = {};
28
+ let currentKey: string | undefined;
29
+ let list: Record<string, unknown>[] | undefined;
30
+ let item: Record<string, unknown> | undefined;
31
+ for (const line of lines) {
32
+ const m = /^(\s*)(- )?([\w.-]+)?:?\s?(.*)$/.exec(line);
33
+ if (!m) continue;
34
+ const [, indent, dash, key, value] = m;
35
+ if (indent === "" && key && !dash) {
36
+ currentKey = key;
37
+ list = [];
38
+ root[currentKey] = list;
39
+ } else if (dash) {
40
+ item = {};
41
+ list?.push(item);
42
+ if (key) item[key] = coerce(value);
43
+ } else if (key && item) {
44
+ item[key] = coerce(value);
45
+ }
46
+ }
47
+ return root;
48
+ }
49
+
50
+ function coerce(v: string): unknown {
51
+ const t = v.trim().replace(/^["']|["']$/g, "");
52
+ if (t === "true") return true;
53
+ if (t === "false") return false;
54
+ if (t !== "" && !Number.isNaN(Number(t))) return Number(t);
55
+ return t;
56
+ }