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
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import type { EvalResult, RunSummary, Usage } from "../../types.ts";
|
|
2
|
+
import { t } from "../../i18n/index.ts";
|
|
3
|
+
|
|
4
|
+
const OUTCOME_ORDER: Record<string, number> = {
|
|
5
|
+
failed: 0,
|
|
6
|
+
errored: 1,
|
|
7
|
+
skipped: 2,
|
|
8
|
+
passed: 3,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const OUTCOME_SYM: Record<string, string> = {
|
|
12
|
+
passed: "✓",
|
|
13
|
+
failed: "✗",
|
|
14
|
+
errored: "!",
|
|
15
|
+
skipped: "○",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface ExperimentRow {
|
|
19
|
+
key: string;
|
|
20
|
+
label: string;
|
|
21
|
+
model: string;
|
|
22
|
+
agent: string;
|
|
23
|
+
avgDurationMs: number;
|
|
24
|
+
passRate: number;
|
|
25
|
+
tokens: number;
|
|
26
|
+
cost?: number;
|
|
27
|
+
evals: number;
|
|
28
|
+
passed: number;
|
|
29
|
+
failed: number;
|
|
30
|
+
errored: number;
|
|
31
|
+
skipped: number;
|
|
32
|
+
attempts: number;
|
|
33
|
+
results: EvalResult[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface EvalRow {
|
|
37
|
+
id: string;
|
|
38
|
+
outcome: EvalResult["outcome"];
|
|
39
|
+
reason: string;
|
|
40
|
+
durationMs: number;
|
|
41
|
+
tokens: number;
|
|
42
|
+
cost?: number;
|
|
43
|
+
passedAttempts: number;
|
|
44
|
+
attempts: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function renderRunReport(summary: RunSummary): string {
|
|
48
|
+
const rows = aggregateExperimentRows(summary.results);
|
|
49
|
+
const lines: string[] = ["", t("report.table.experimentsTitle")];
|
|
50
|
+
lines.push(renderTable(
|
|
51
|
+
[
|
|
52
|
+
t("report.table.experiment"),
|
|
53
|
+
t("report.table.model"),
|
|
54
|
+
t("report.table.agent"),
|
|
55
|
+
t("report.table.avgDuration"),
|
|
56
|
+
t("report.table.successRate"),
|
|
57
|
+
t("report.table.tokens"),
|
|
58
|
+
t("report.table.cost"),
|
|
59
|
+
t("report.table.result"),
|
|
60
|
+
],
|
|
61
|
+
rows.map((row) => [
|
|
62
|
+
row.label,
|
|
63
|
+
row.model,
|
|
64
|
+
row.agent,
|
|
65
|
+
formatDuration(row.avgDurationMs),
|
|
66
|
+
formatPercent(row.passRate),
|
|
67
|
+
formatTokens(row.tokens),
|
|
68
|
+
formatCost(row.cost),
|
|
69
|
+
formatResult(row),
|
|
70
|
+
]),
|
|
71
|
+
));
|
|
72
|
+
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
lines.push("", `${t("report.table.evalTitle")} ${row.label}`);
|
|
75
|
+
lines.push(renderTable(
|
|
76
|
+
[
|
|
77
|
+
t("report.table.status"),
|
|
78
|
+
t("report.table.eval"),
|
|
79
|
+
t("report.table.reason"),
|
|
80
|
+
t("report.table.duration"),
|
|
81
|
+
t("report.table.tokens"),
|
|
82
|
+
t("report.table.cost"),
|
|
83
|
+
t("report.table.runs"),
|
|
84
|
+
],
|
|
85
|
+
aggregateEvalRows(row.results).map((evalRow) => [
|
|
86
|
+
formatOutcome(evalRow.outcome),
|
|
87
|
+
evalRow.id,
|
|
88
|
+
evalRow.reason,
|
|
89
|
+
formatDuration(evalRow.durationMs),
|
|
90
|
+
formatTokens(evalRow.tokens),
|
|
91
|
+
formatCost(evalRow.cost),
|
|
92
|
+
`${evalRow.passedAttempts}/${evalRow.attempts}`,
|
|
93
|
+
]),
|
|
94
|
+
{ maxWidth: 120, flexibleColumn: 2 },
|
|
95
|
+
));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const tok = (summary.usage?.inputTokens ?? 0) + (summary.usage?.outputTokens ?? 0);
|
|
99
|
+
const tokStr = tok > 0 ? `${formatTokens(tok)} tok` : "— tok";
|
|
100
|
+
const cost = summary.estimatedCostUSD !== undefined ? ` · ${formatCost(summary.estimatedCostUSD)}` : "";
|
|
101
|
+
const parts = [
|
|
102
|
+
t("report.summary.passed", { count: summary.passed }),
|
|
103
|
+
t("report.summary.failed", { count: summary.failed }),
|
|
104
|
+
...(summary.errored > 0 ? [t("report.summary.errored", { count: summary.errored })] : []),
|
|
105
|
+
t("report.summary.skipped", { count: summary.skipped }),
|
|
106
|
+
];
|
|
107
|
+
lines.push(t("report.result", {
|
|
108
|
+
parts: parts.join(", "),
|
|
109
|
+
duration: formatDuration(summary.durationMs),
|
|
110
|
+
tokens: tokStr,
|
|
111
|
+
cost,
|
|
112
|
+
}).trimEnd());
|
|
113
|
+
|
|
114
|
+
return `${lines.join("\n")}\n\n`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function formatTokens(n: number): string {
|
|
118
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
119
|
+
return String(n);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function formatDuration(ms: number): string {
|
|
123
|
+
if (!Number.isFinite(ms) || ms <= 0) return "0ms";
|
|
124
|
+
if (ms >= 60_000) return `${(ms / 60_000).toFixed(1)}m`;
|
|
125
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
|
|
126
|
+
return `${Math.round(ms)}ms`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function formatCost(n: number | undefined): string {
|
|
130
|
+
if (n === undefined || n <= 0) return "$0";
|
|
131
|
+
return `$${n.toFixed(n < 1 ? 3 : 2)}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function formatOutcome(outcome: string): string {
|
|
135
|
+
const sym = OUTCOME_SYM[outcome] ?? "?";
|
|
136
|
+
switch (outcome) {
|
|
137
|
+
case "passed": return `${sym} ${t("report.passed")}`;
|
|
138
|
+
case "failed": return `${sym} ${t("report.failed")}`;
|
|
139
|
+
case "errored": return `${sym} ${t("report.errored")}`;
|
|
140
|
+
case "skipped": return `${sym} ${t("report.skipped")}`;
|
|
141
|
+
default: return `${sym} ${outcome}`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function aggregateExperimentRows(results: EvalResult[]): ExperimentRow[] {
|
|
146
|
+
const groups = new Map<string, EvalResult[]>();
|
|
147
|
+
for (const result of results) {
|
|
148
|
+
const key = result.experimentId ? `exp|||${result.experimentId}` : `legacy|||${result.agent}|||${result.model ?? ""}`;
|
|
149
|
+
groups.set(key, [...(groups.get(key) ?? []), result]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return [...groups.entries()]
|
|
153
|
+
.map(([key, groupResults]) => {
|
|
154
|
+
const first = groupResults[0]!;
|
|
155
|
+
const stats = evalLevelStats(groupResults);
|
|
156
|
+
return {
|
|
157
|
+
key,
|
|
158
|
+
label: displayExperimentName(first.experimentId) ?? fallbackExperimentLabel(first),
|
|
159
|
+
model: first.model ?? t("report.table.default"),
|
|
160
|
+
agent: first.agent,
|
|
161
|
+
avgDurationMs: avg(groupResults.map((r) => r.durationMs)),
|
|
162
|
+
passRate: stats.passRate,
|
|
163
|
+
tokens: totalTokens(groupResults.map((r) => r.usage)),
|
|
164
|
+
cost: sumMaybe(groupResults.map((r) => r.estimatedCostUSD)),
|
|
165
|
+
evals: stats.evals,
|
|
166
|
+
passed: stats.passed,
|
|
167
|
+
failed: stats.failed,
|
|
168
|
+
errored: stats.errored,
|
|
169
|
+
skipped: stats.skipped,
|
|
170
|
+
attempts: groupResults.length,
|
|
171
|
+
results: groupResults.slice().sort((a, b) => a.id.localeCompare(b.id) || a.attempt - b.attempt),
|
|
172
|
+
};
|
|
173
|
+
})
|
|
174
|
+
.sort((a, b) => b.passRate - a.passRate || a.label.localeCompare(b.label));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function aggregateEvalRows(results: EvalResult[]): EvalRow[] {
|
|
178
|
+
const byEval = new Map<string, EvalResult[]>();
|
|
179
|
+
for (const result of results) byEval.set(result.id, [...(byEval.get(result.id) ?? []), result]);
|
|
180
|
+
return [...byEval.entries()]
|
|
181
|
+
.map(([id, evalResults]) => {
|
|
182
|
+
const outcome = foldEvalOutcome(evalResults);
|
|
183
|
+
const representative =
|
|
184
|
+
evalResults.find((r) => r.outcome === outcome) ??
|
|
185
|
+
evalResults.find((r) => r.outcome !== "passed") ??
|
|
186
|
+
evalResults[0]!;
|
|
187
|
+
return {
|
|
188
|
+
id,
|
|
189
|
+
outcome,
|
|
190
|
+
reason: outcome === "passed" ? t("report.passed") : reasonFor(representative),
|
|
191
|
+
durationMs: avg(evalResults.map((r) => r.durationMs)),
|
|
192
|
+
tokens: totalTokens(evalResults.map((r) => r.usage)),
|
|
193
|
+
cost: sumMaybe(evalResults.map((r) => r.estimatedCostUSD)),
|
|
194
|
+
passedAttempts: evalResults.filter((r) => r.outcome === "passed").length,
|
|
195
|
+
attempts: evalResults.length,
|
|
196
|
+
};
|
|
197
|
+
})
|
|
198
|
+
.sort((a, b) => OUTCOME_ORDER[a.outcome] - OUTCOME_ORDER[b.outcome] || a.id.localeCompare(b.id));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function evalLevelStats(results: EvalResult[]) {
|
|
202
|
+
const byEval = new Map<string, EvalResult[]>();
|
|
203
|
+
for (const r of results) byEval.set(r.id, [...(byEval.get(r.id) ?? []), r]);
|
|
204
|
+
const counts = { passed: 0, failed: 0, errored: 0, skipped: 0 };
|
|
205
|
+
for (const group of byEval.values()) counts[foldEvalOutcome(group)] += 1;
|
|
206
|
+
const ran = counts.passed + counts.failed + counts.errored;
|
|
207
|
+
return { evals: byEval.size, ...counts, passRate: ran ? counts.passed / ran : 0 };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function foldEvalOutcome(results: EvalResult[]): EvalResult["outcome"] {
|
|
211
|
+
const outcomes = results.map((r) => r.outcome);
|
|
212
|
+
if (outcomes.some((o) => o === "passed")) return "passed";
|
|
213
|
+
if (outcomes.some((o) => o === "failed")) return "failed";
|
|
214
|
+
if (outcomes.some((o) => o === "errored")) return "errored";
|
|
215
|
+
return "skipped";
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function reasonFor(result: EvalResult): string {
|
|
219
|
+
if (result.skipReason) return result.skipReason;
|
|
220
|
+
if (result.error) return truncateOneLine(result.error, 120);
|
|
221
|
+
const failed = result.assertions.filter((a) => !a.passed && a.severity === "gate");
|
|
222
|
+
const assertions = failed.length ? failed : result.assertions.slice(0, 2);
|
|
223
|
+
if (!assertions.length) return "";
|
|
224
|
+
return truncateOneLine(
|
|
225
|
+
assertions
|
|
226
|
+
.map((a) => {
|
|
227
|
+
const threshold = a.threshold !== undefined ? ` ${t("report.assertionThreshold", { score: a.score.toFixed(2), threshold: a.threshold }).trim()}` : "";
|
|
228
|
+
return `${a.severity}: ${a.name}${threshold}`;
|
|
229
|
+
})
|
|
230
|
+
.join(" · "),
|
|
231
|
+
120,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function formatResult(row: ExperimentRow): string {
|
|
236
|
+
const failures = row.failed + row.errored;
|
|
237
|
+
const suffix = row.skipped > 0 ? ` · ${row.skipped} ${t("report.skipped")}` : "";
|
|
238
|
+
return `${row.passed}/${row.evals} ${t("report.passed")} · ${failures} ${t("report.failed")}${suffix}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function displayExperimentName(id: string | undefined): string | undefined {
|
|
242
|
+
if (!id) return undefined;
|
|
243
|
+
return id.split("/").filter(Boolean).at(-1) ?? id;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function fallbackExperimentLabel(result: EvalResult): string {
|
|
247
|
+
if (result.experiment?.id) return displayExperimentName(result.experiment.id) ?? result.experiment.id;
|
|
248
|
+
if (result.model) return `${result.agent}/${result.model}`;
|
|
249
|
+
return result.agent || "ad hoc run";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function totalTokens(items: Array<Usage | undefined>): number {
|
|
253
|
+
return items.reduce((n, u) => n + (u?.inputTokens ?? 0) + (u?.outputTokens ?? 0), 0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function sumMaybe(items: Array<number | undefined>): number | undefined {
|
|
257
|
+
const known = items.filter((n): n is number => n !== undefined);
|
|
258
|
+
return known.length ? known.reduce((sum, n) => sum + n, 0) : undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function avg(items: number[]): number {
|
|
262
|
+
return items.length ? items.reduce((sum, n) => sum + n, 0) / items.length : 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function formatPercent(v: number): string {
|
|
266
|
+
return `${Math.round(v * 100)}%`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function renderTable(headers: string[], rows: string[][], opts: { maxWidth?: number; flexibleColumn?: number } = {}): string {
|
|
270
|
+
const rawRows = [headers, ...rows];
|
|
271
|
+
const widths = headers.map((_, col) => Math.max(...rawRows.map((row) => displayWidth(row[col] ?? ""))));
|
|
272
|
+
const maxWidth = opts.maxWidth ?? 140;
|
|
273
|
+
const tableWidth = widths.reduce((sum, w) => sum + w, 0) + (headers.length - 1) * 3;
|
|
274
|
+
if (tableWidth > maxWidth && opts.flexibleColumn !== undefined) {
|
|
275
|
+
const over = tableWidth - maxWidth;
|
|
276
|
+
widths[opts.flexibleColumn] = Math.max(18, widths[opts.flexibleColumn]! - over);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const lines = [formatRow(headers, widths), widths.map((w) => "-".repeat(w)).join("-+-")];
|
|
280
|
+
for (const row of rows) lines.push(formatRow(row, widths));
|
|
281
|
+
return lines.join("\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function formatRow(row: string[], widths: number[]): string {
|
|
285
|
+
return row.map((cell, i) => padEnd(truncateToWidth(cell ?? "", widths[i]!), widths[i]!)).join(" | ");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function truncateOneLine(s: string, width: number): string {
|
|
289
|
+
return truncateToWidth(s.replace(/\s+/g, " ").trim(), width);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function truncateToWidth(s: string, width: number): string {
|
|
293
|
+
if (displayWidth(s) <= width) return s;
|
|
294
|
+
const ellipsis = "…";
|
|
295
|
+
let out = "";
|
|
296
|
+
for (const char of s) {
|
|
297
|
+
if (displayWidth(out + char + ellipsis) > width) break;
|
|
298
|
+
out += char;
|
|
299
|
+
}
|
|
300
|
+
return out + ellipsis;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function padEnd(s: string, width: number): string {
|
|
304
|
+
return s + " ".repeat(Math.max(0, width - displayWidth(s)));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function displayWidth(s: string): number {
|
|
308
|
+
let width = 0;
|
|
309
|
+
for (const char of s) {
|
|
310
|
+
const code = char.codePointAt(0) ?? 0;
|
|
311
|
+
width += isWide(code) ? 2 : 1;
|
|
312
|
+
}
|
|
313
|
+
return width;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isWide(code: number): boolean {
|
|
317
|
+
return (
|
|
318
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
319
|
+
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
320
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
321
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
322
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
323
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
324
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
325
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
326
|
+
(code >= 0x1f300 && code <= 0x1faff)
|
|
327
|
+
);
|
|
328
|
+
}
|