niceeval 0.5.2 → 0.5.4
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/docs-site/zh/concepts/adapter.mdx +11 -6
- package/docs-site/zh/concepts/experiment.mdx +8 -6
- package/docs-site/zh/concepts/overview.mdx +1 -1
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/guides/ci-integration.mdx +2 -0
- package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
- package/docs-site/zh/guides/custom-reports.mdx +13 -13
- package/docs-site/zh/guides/experiments.mdx +4 -3
- package/docs-site/zh/guides/publish-report.mdx +91 -0
- package/docs-site/zh/guides/report-components.mdx +4 -4
- package/docs-site/zh/guides/reporters.mdx +1 -1
- package/docs-site/zh/guides/results-data.mdx +2 -2
- package/docs-site/zh/guides/runner.mdx +1 -1
- package/docs-site/zh/guides/sandbox-agent.mdx +11 -6
- package/docs-site/zh/guides/sandbox-backends.mdx +27 -0
- package/docs-site/zh/guides/viewing-results.mdx +5 -21
- package/docs-site/zh/guides/write-experiment.mdx +24 -10
- package/docs-site/zh/guides/write-send.mdx +6 -6
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/introduction.mdx +1 -1
- package/docs-site/zh/reference/builtin-agents.mdx +4 -0
- package/docs-site/zh/reference/cli.mdx +9 -6
- package/docs-site/zh/reference/define-agent.mdx +19 -3
- package/docs-site/zh/reference/define-config.mdx +3 -1
- package/docs-site/zh/reference/define-eval.mdx +9 -3
- package/docs-site/zh/reference/events.mdx +4 -0
- package/docs-site/zh/reference/expect.mdx +4 -0
- package/package.json +3 -2
- package/src/agents/ai-sdk.test.ts +1 -1
- package/src/agents/ai-sdk.ts +2 -2
- package/src/agents/bub.ts +5 -1
- package/src/agents/claude-code.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/index.ts +2 -0
- package/src/agents/streaming.test.ts +1 -1
- package/src/agents/types.ts +10 -2
- package/src/agents/ui-message-stream.test.ts +1 -1
- package/src/cli.ts +38 -14
- package/src/context/context.test.ts +1 -1
- package/src/context/context.ts +6 -3
- package/src/context/session.ts +5 -2
- package/src/context/types.ts +2 -2
- package/src/define.test.ts +82 -0
- package/src/define.ts +68 -7
- package/src/i18n/en.ts +4 -2
- package/src/i18n/zh-CN.ts +4 -2
- package/src/report/aggregate.ts +16 -16
- package/src/report/components.tsx +1 -1
- package/src/report/compute.ts +7 -7
- package/src/report/dual-face.test.tsx +1 -1
- package/src/report/{param.ts → flag.ts} +6 -6
- package/src/report/index.ts +3 -3
- package/src/report/react/MetricLine.tsx +1 -1
- package/src/report/react/fixtures.ts +1 -1
- package/src/report/report.test.ts +17 -17
- package/src/report/report.ts +1 -1
- package/src/report/types.ts +11 -11
- package/src/results/results.test.ts +2 -2
- package/src/runner/attempt.ts +53 -6
- package/src/runner/fingerprint.ts +1 -1
- package/src/runner/reporters/braintrust.test.ts +2 -2
- package/src/runner/reporters/braintrust.ts +3 -3
- package/src/runner/run.ts +1 -1
- package/src/runner/types.ts +8 -8
- package/src/sandbox/types.ts +45 -4
- package/src/show/compose.ts +1 -1
- package/src/view/app/App.tsx +1 -1
- package/src/view/app/css.d.ts +3 -0
- package/src/view/app/i18n.ts +3 -3
- package/src/view/app/lib/rows.ts +5 -5
- package/src/view/app/types.ts +1 -1
- package/src/view/client-dist/app.js +2 -2
- package/src/view/data.test.ts +26 -2
- package/src/view/data.ts +28 -4
- package/src/view/shared/types.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// MetricLine:趋势线——x 是 experiment 声明的
|
|
1
|
+
// MetricLine:趋势线——x 是 experiment 声明的 flag(有序变量),每个系列一条线。
|
|
2
2
|
// 与 MetricScatter 的分工:scatter 的两轴都是测出来的指标(找 frontier),
|
|
3
3
|
// line 的 x 是你配置的变量(看趋势)。x 轴正常升序;y 轴向随 better;
|
|
4
4
|
// x 或 y 缺数据的点不画,注脚如实报数;每个点带 <title> hover,可经 pointHref 下钻。
|
|
@@ -207,7 +207,7 @@ export const lineData: LineData = {
|
|
|
207
207
|
y: { value: 0.7, display: "70%", samples: 6, total: 6, refs: [] },
|
|
208
208
|
},
|
|
209
209
|
{
|
|
210
|
-
// 未声明
|
|
210
|
+
// 未声明 flag 的 experiment:作轴不画点,注脚报数
|
|
211
211
|
key: "ultra/legacy",
|
|
212
212
|
series: "1 agents",
|
|
213
213
|
x: null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// niceeval/report 计算层的单元测试:全部用内存 fake(Snapshot / AttemptHandle 按
|
|
2
2
|
// niceeval/results 的读取契约手工构造),专门覆盖 docs/reports.md 点名的坑 ——
|
|
3
3
|
// 两级聚合 vs 平铺、pass@k、examScore 空真、skipped 稀释、scoreboard 固定分母与
|
|
4
|
-
// 最长前缀、scatter/delta 的 null 语义、快照键对比、
|
|
4
|
+
// 最长前缀、scatter/delta 的 null 语义、快照键对比、flag 维度与轴、
|
|
5
5
|
// cases 的 redact/truncated、身份键去重、Selection warnings 随行。
|
|
6
6
|
|
|
7
7
|
import { describe, expect, it } from "vitest";
|
|
@@ -10,7 +10,7 @@ import type { AssertionResult, EvalResult, ResultOutcome, RunSummary } from "../
|
|
|
10
10
|
import type { AttemptHandle, RunDir, Selection, SelectionWarning, Snapshot } from "../results/index.ts";
|
|
11
11
|
import type { Dimension, MetricCell } from "./types.ts";
|
|
12
12
|
import { costUSD, defineMetric, durationMs, examScore, passRate, tokens } from "./metrics.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { flag } from "./flag.ts";
|
|
14
14
|
import { formatMetricValue } from "./format.ts";
|
|
15
15
|
import {
|
|
16
16
|
CaseList,
|
|
@@ -465,22 +465,22 @@ describe("MetricScatter.data", () => {
|
|
|
465
465
|
});
|
|
466
466
|
});
|
|
467
467
|
|
|
468
|
-
// ─────────────────────────
|
|
468
|
+
// ───────────────────────── flag():维度与轴 ─────────────────────────
|
|
469
469
|
|
|
470
|
-
describe("
|
|
471
|
-
const
|
|
470
|
+
describe("flag()", () => {
|
|
471
|
+
const withFlags = (id: string, flags: Record<string, unknown> | undefined, outcome: ResultOutcome) =>
|
|
472
472
|
snap({
|
|
473
473
|
experimentId: id,
|
|
474
|
-
results: [res("A", outcome, { experimentId: id, experiment: { id,
|
|
474
|
+
results: [res("A", outcome, { experimentId: id, experiment: { id, flags } })],
|
|
475
475
|
});
|
|
476
476
|
|
|
477
|
-
it("MetricLine.data:x 收
|
|
478
|
-
const s1 =
|
|
479
|
-
const s2 =
|
|
480
|
-
const legacy =
|
|
477
|
+
it("MetricLine.data:x 收 flag、按 experiment 聚合;未声明的作轴 x=null 报数", async () => {
|
|
478
|
+
const s1 = withFlags("ultra/lat-100", { latencyMs: 100, agents: 1 }, "passed");
|
|
479
|
+
const s2 = withFlags("ultra/lat-300", { latencyMs: 300, agents: 1 }, "failed");
|
|
480
|
+
const legacy = withFlags("ultra/legacy", undefined, "passed");
|
|
481
481
|
const data = await MetricLine.data([s1, s2, legacy], {
|
|
482
|
-
x:
|
|
483
|
-
series:
|
|
482
|
+
x: flag("latencyMs", { label: "Simulated latency", unit: "ms" }),
|
|
483
|
+
series: flag("agents", { label: (v) => `${v} agents` }),
|
|
484
484
|
y: passRate,
|
|
485
485
|
});
|
|
486
486
|
expect(data.x).toEqual({ key: "latencyMs", label: "Simulated latency", unit: "ms" });
|
|
@@ -493,18 +493,18 @@ describe("param()", () => {
|
|
|
493
493
|
expect(p100.series).toBe("1 agents");
|
|
494
494
|
expect(p100.y.value).toBe(1);
|
|
495
495
|
|
|
496
|
-
// 未声明
|
|
496
|
+
// 未声明 flag 的 experiment 不猜:作轴 x=null(组件不画、注脚报数),分组归 (unset)
|
|
497
497
|
const legacyRow = data.rows.find((r) => r.key === "ultra/legacy")!;
|
|
498
498
|
expect(legacyRow.x).toBeNull();
|
|
499
499
|
expect(legacyRow.xDisplay).toBe("");
|
|
500
500
|
expect(legacyRow.series).toBe("(unset)");
|
|
501
501
|
});
|
|
502
502
|
|
|
503
|
-
it("
|
|
504
|
-
const s1 =
|
|
505
|
-
const s2 =
|
|
503
|
+
it("flag 当维度用:按声明值分组,label 函数折组名", async () => {
|
|
504
|
+
const s1 = withFlags("exp/a", { agents: 1 }, "passed");
|
|
505
|
+
const s2 = withFlags("exp/b", { agents: 16 }, "failed");
|
|
506
506
|
const data = await MetricTable.data([s1, s2], {
|
|
507
|
-
rows:
|
|
507
|
+
rows: flag("agents", { label: (v) => `${v} agents` }),
|
|
508
508
|
columns: [passRate],
|
|
509
509
|
});
|
|
510
510
|
expect(data.dimension).toBe("agents");
|
package/src/report/report.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { prepareDefaultReportData, runWithDefaultReportData } from "./default-report.tsx";
|
|
19
19
|
|
|
20
20
|
export interface ReportContext {
|
|
21
|
-
/** results.latest() 挑好的选集:现刻水位快照 +
|
|
21
|
+
/** results.latest() 挑好的选集:现刻水位快照 + 结构化挑选警告,同默认报告口径。 */
|
|
22
22
|
selection: Selection;
|
|
23
23
|
/** 默认挑法不合口径时,全量数据自己挑(见 docs/results-lib.md)。 */
|
|
24
24
|
results: Results;
|
package/src/report/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// niceeval/report 的公开类型:指标(Metric)、维度(Dimension /
|
|
1
|
+
// niceeval/report 的公开类型:指标(Metric)、维度(Dimension / flag())与计算函数
|
|
2
2
|
// 产物(即组件的 data props)。数据契约照 docs/reports.md「计算函数与数据契约」;
|
|
3
3
|
// 这些不是持久化格式,没有 format / schemaVersion 信封,兼容性跟随 npm 版本。
|
|
4
4
|
|
|
@@ -66,20 +66,20 @@ export type Dimension =
|
|
|
66
66
|
| { name: string; of: (attempt: AttemptHandle) => string };
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
69
|
+
* flag() 的产物:把 experiment 声明的 flags 当维度(series / rows / columns / points
|
|
70
70
|
* 槽,按声明值分组)或轴(MetricLine 的 x 槽,要求数值并驱动刻度)。
|
|
71
|
-
* 未声明该
|
|
71
|
+
* 未声明该 flag 的 experiment 不猜:分组如实归「(unset)」,作轴不画点、注脚报数。
|
|
72
72
|
*/
|
|
73
|
-
export interface
|
|
74
|
-
readonly kind: "
|
|
73
|
+
export interface FlagRef {
|
|
74
|
+
readonly kind: "flag";
|
|
75
75
|
readonly name: string;
|
|
76
76
|
/** 组标签 / 轴标签;函数形态把声明值折成组名(如 `(v) => \`${v} agents\``)。 */
|
|
77
77
|
readonly label?: string | ((value: string | number | boolean) => string);
|
|
78
78
|
readonly unit?: string;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/** 维度槽的输入:内置/自定义维度,或 experiment 声明的
|
|
82
|
-
export type DimensionInput = Dimension |
|
|
81
|
+
/** 维度槽的输入:内置/自定义维度,或 experiment 声明的 flag。 */
|
|
82
|
+
export type DimensionInput = Dimension | FlagRef;
|
|
83
83
|
|
|
84
84
|
// ───────────────────────── 计算产物(组件 data props)─────────────────────────
|
|
85
85
|
|
|
@@ -174,9 +174,9 @@ export interface ScatterData {
|
|
|
174
174
|
}[];
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
/** MetricLine 的 x 轴:experiment 声明的
|
|
177
|
+
/** MetricLine 的 x 轴:experiment 声明的 flag,数值驱动刻度。 */
|
|
178
178
|
export interface LineAxis {
|
|
179
|
-
/**
|
|
179
|
+
/** flag 名。 */
|
|
180
180
|
key: string;
|
|
181
181
|
label: string;
|
|
182
182
|
unit?: string;
|
|
@@ -184,14 +184,14 @@ export interface LineAxis {
|
|
|
184
184
|
|
|
185
185
|
export interface LineData {
|
|
186
186
|
x: LineAxis;
|
|
187
|
-
/** 系列维度名(
|
|
187
|
+
/** 系列维度名(flag 或普通维度)。 */
|
|
188
188
|
series?: string;
|
|
189
189
|
y: MetricColumn;
|
|
190
190
|
rows: {
|
|
191
191
|
/** 点的键(experiment id):每个点 = 一个 experiment 的聚合。 */
|
|
192
192
|
key: string;
|
|
193
193
|
series?: string;
|
|
194
|
-
/**
|
|
194
|
+
/** flag 声明值;未声明或非数值 → null,点不画、注脚报数。 */
|
|
195
195
|
x: number | null;
|
|
196
196
|
/** 已格式化的 x("300 ms");x 为 null 时为空串。 */
|
|
197
197
|
xDisplay: string;
|
|
@@ -600,7 +600,7 @@ describe("Artifacts reporter(writer 薄壳)", () => {
|
|
|
600
600
|
const fresh: EvalResult = {
|
|
601
601
|
id: "algebra/q1",
|
|
602
602
|
experimentId: "compare/bub",
|
|
603
|
-
experiment: { id: "compare/bub",
|
|
603
|
+
experiment: { id: "compare/bub", flags: { style: "concise" } },
|
|
604
604
|
agent: "bub",
|
|
605
605
|
model: "gpt-5.4",
|
|
606
606
|
outcome: "passed",
|
|
@@ -707,7 +707,7 @@ describe("Artifacts reporter(writer 薄壳)", () => {
|
|
|
707
707
|
"experimentId": "compare/bub",
|
|
708
708
|
"experiment": {
|
|
709
709
|
"id": "compare/bub",
|
|
710
|
-
"
|
|
710
|
+
"flags": {
|
|
711
711
|
"style": "concise"
|
|
712
712
|
}
|
|
713
713
|
},
|
package/src/runner/attempt.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { captureGeneratedFiles, initGitAndCommit } from "./sandbox-prep.ts";
|
|
|
24
24
|
import { createRemoteSandbox, withEvalLocalPaths } from "./remote-sandbox.ts";
|
|
25
25
|
import type {
|
|
26
26
|
AgentContext,
|
|
27
|
+
AgentSetup,
|
|
28
|
+
AgentTeardown,
|
|
27
29
|
Cleanup,
|
|
28
30
|
Config,
|
|
29
31
|
EvalResult,
|
|
@@ -88,6 +90,9 @@ export function runAttemptEffect(
|
|
|
88
90
|
|
|
89
91
|
return Effect.scoped(
|
|
90
92
|
Effect.gen(function* () {
|
|
93
|
+
// run.sandbox ?? config.sandbox 是同一个 SandboxSpec 对象,既用来起沙箱后端,
|
|
94
|
+
// 也是 sandbox.setup / sandbox.teardown 钩子(SandboxSpec.setup()/.teardown() 链式挂的)的来源。
|
|
95
|
+
const sandboxSpec = run.sandbox ?? config.sandbox;
|
|
91
96
|
const sandbox =
|
|
92
97
|
run.agent.kind === "sandbox"
|
|
93
98
|
? yield* sandboxSem.withPermits(1)(
|
|
@@ -96,7 +101,7 @@ export function runAttemptEffect(
|
|
|
96
101
|
// sandboxSem 只覆盖「容器创建」阶段;容器起好后立即释放,后续 npm install / agent 不占位。
|
|
97
102
|
log(t("runner.startSandbox"));
|
|
98
103
|
return yield* createSandbox({
|
|
99
|
-
sandbox:
|
|
104
|
+
sandbox: sandboxSpec,
|
|
100
105
|
timeout: timeoutMs,
|
|
101
106
|
runtime: "node24",
|
|
102
107
|
});
|
|
@@ -161,6 +166,8 @@ export function runAttemptEffect(
|
|
|
161
166
|
return yield* Effect.promise((interruptSignal) =>
|
|
162
167
|
runAttemptBody(a, config, t0, base, {
|
|
163
168
|
sandbox,
|
|
169
|
+
sandboxSetupHooks: sandboxSpec?.setupHooks ?? [],
|
|
170
|
+
sandboxTeardownHooks: sandboxSpec?.teardownHooks ?? [],
|
|
164
171
|
receiver,
|
|
165
172
|
telemetry,
|
|
166
173
|
otel: otelChannel,
|
|
@@ -203,6 +210,10 @@ function causeToError(cause: Cause.Cause<never>): string {
|
|
|
203
210
|
|
|
204
211
|
interface AttemptResources {
|
|
205
212
|
sandbox: Sandbox;
|
|
213
|
+
/** SandboxSpec.setup() 链式挂的钩子,按追加顺序;非沙箱 agent 传空数组(usesSandbox 挡住不会跑)。 */
|
|
214
|
+
sandboxSetupHooks: readonly AgentSetup[];
|
|
215
|
+
/** SandboxSpec.teardown() 链式挂的钩子,按追加顺序保存,执行时逆序。 */
|
|
216
|
+
sandboxTeardownHooks: readonly AgentTeardown[];
|
|
206
217
|
receiver?: TraceReceiver;
|
|
207
218
|
telemetry?: Telemetry;
|
|
208
219
|
/** 非沙箱 tracing agent 的共享 OTLP 通道(run 级池持有,不随 attempt 关)。 */
|
|
@@ -221,14 +232,15 @@ async function runAttemptBody(
|
|
|
221
232
|
res: AttemptResources,
|
|
222
233
|
): Promise<EvalResult> {
|
|
223
234
|
const { evalDef, run, attempt } = a;
|
|
224
|
-
const { sandbox, receiver, telemetry, otel, signal, log } = res;
|
|
235
|
+
const { sandbox, sandboxSetupHooks, sandboxTeardownHooks, receiver, telemetry, otel, signal, log } = res;
|
|
225
236
|
const usesSandbox = run.agent.kind === "sandbox";
|
|
226
237
|
// 整个 attempt 共用一份 agent ctx(sandbox 钩子 / agent setup / tracing configure / teardown 都用它)。
|
|
227
238
|
const attemptCtx: AgentContext = {
|
|
228
239
|
signal,
|
|
229
240
|
model: run.model,
|
|
230
241
|
reasoningEffort: run.reasoningEffort,
|
|
231
|
-
|
|
242
|
+
flags: run.flags,
|
|
243
|
+
experimentId: run.experimentId,
|
|
232
244
|
sandbox,
|
|
233
245
|
session: createAgentSession(),
|
|
234
246
|
telemetry,
|
|
@@ -236,8 +248,20 @@ async function runAttemptBody(
|
|
|
236
248
|
};
|
|
237
249
|
let agentCleanup: Cleanup | void = undefined;
|
|
238
250
|
let agentDidSetup = false;
|
|
251
|
+
// SandboxSpec.setup() 返回的 cleanup 闭包,按调用顺序收集;finally 里 LIFO 跑(见下)。
|
|
252
|
+
const sandboxCleanups: Cleanup[] = [];
|
|
239
253
|
try {
|
|
240
254
|
if (usesSandbox) {
|
|
255
|
+
// 沙箱级生命周期钩子(SandboxSpec.setup):环境预置层,先于 workspace 上传 / git 基线 /
|
|
256
|
+
// eval.setup 跑——改动进 git 基线,不会被误算进 agent 产出的 diff。按追加顺序依次执行;
|
|
257
|
+
// 单个抛错走下面的执行错误路径(与 eval.setup / agent.setup 同一条),已跑过的 cleanup /
|
|
258
|
+
// sandbox.teardown 钩子仍在 finally 里跑(见 catch/finally)。
|
|
259
|
+
if (sandboxSetupHooks.length > 0) log(t("runner.startSandboxSetup"));
|
|
260
|
+
for (const hook of sandboxSetupHooks) {
|
|
261
|
+
const cleanup = await hook(sandbox, attemptCtx);
|
|
262
|
+
if (typeof cleanup === "function") sandboxCleanups.push(cleanup);
|
|
263
|
+
}
|
|
264
|
+
|
|
241
265
|
await initGitAndCommit(sandbox);
|
|
242
266
|
|
|
243
267
|
// eval 级 setup(starter prep:npm install / 装系统依赖等)。命令默认非 root;
|
|
@@ -270,7 +294,8 @@ async function runAttemptBody(
|
|
|
270
294
|
sandbox,
|
|
271
295
|
model: run.model,
|
|
272
296
|
reasoningEffort: run.reasoningEffort,
|
|
273
|
-
|
|
297
|
+
flags: run.flags,
|
|
298
|
+
experimentId: run.experimentId,
|
|
274
299
|
signal,
|
|
275
300
|
log,
|
|
276
301
|
judge,
|
|
@@ -406,7 +431,8 @@ async function runAttemptBody(
|
|
|
406
431
|
};
|
|
407
432
|
} finally {
|
|
408
433
|
// teardown / cleanup 一律在 finally 跑(失败也跑),不改判决,各自兜错(diagnostic)。
|
|
409
|
-
// LIFO
|
|
434
|
+
// LIFO:agent 级(setup 最晚 → cleanup / teardown 先跑)在前,sandbox 级(最早就绪)收尾在后
|
|
435
|
+
//(即 sandbox.setup 返回的 cleanup、sandbox.teardown 钩子最后跑,沙箱销毁前)。
|
|
410
436
|
// 沙箱 stop / 接收器 close 不在这里 —— 由 runAttemptEffect 的 Scope 在本函数返回后回收。
|
|
411
437
|
try {
|
|
412
438
|
if (typeof agentCleanup === "function") await agentCleanup();
|
|
@@ -414,6 +440,27 @@ async function runAttemptBody(
|
|
|
414
440
|
} catch {
|
|
415
441
|
// teardown 失败只是 diagnostic,不影响已出的结果
|
|
416
442
|
}
|
|
443
|
+
if (usesSandbox) {
|
|
444
|
+
// sandbox.setup 返回的 cleanup:LIFO(后 setup 先 cleanup),与 agent 级同构。
|
|
445
|
+
for (let i = sandboxCleanups.length - 1; i >= 0; i--) {
|
|
446
|
+
try {
|
|
447
|
+
await sandboxCleanups[i]();
|
|
448
|
+
} catch {
|
|
449
|
+
// 同上,只作 diagnostic
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// sandbox.teardown 钩子:按追加的逆序执行,沙箱销毁前最后一步。
|
|
453
|
+
if (sandboxTeardownHooks.length > 0) {
|
|
454
|
+
log(t("runner.startSandboxTeardown"));
|
|
455
|
+
for (let i = sandboxTeardownHooks.length - 1; i >= 0; i--) {
|
|
456
|
+
try {
|
|
457
|
+
await sandboxTeardownHooks[i](sandbox, attemptCtx);
|
|
458
|
+
} catch {
|
|
459
|
+
// 同上,只作 diagnostic
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
417
464
|
}
|
|
418
465
|
}
|
|
419
466
|
|
|
@@ -442,7 +489,7 @@ async function collectSources(
|
|
|
442
489
|
function experimentRunInfo(run: AgentRun): EvalResult["experiment"] {
|
|
443
490
|
return {
|
|
444
491
|
id: run.experimentId,
|
|
445
|
-
|
|
492
|
+
flags: run.flags,
|
|
446
493
|
runs: run.runs,
|
|
447
494
|
earlyExit: run.earlyExit,
|
|
448
495
|
sandbox: run.sandbox === undefined ? undefined : sandboxLabel(run.sandbox),
|
|
@@ -38,7 +38,7 @@ export async function computeFingerprint(
|
|
|
38
38
|
experimentId: run.experimentId,
|
|
39
39
|
agent: run.agent.name,
|
|
40
40
|
model: run.model,
|
|
41
|
-
|
|
41
|
+
flags: run.flags,
|
|
42
42
|
sandbox: run.sandbox === undefined ? undefined : sandboxLabel(run.sandbox),
|
|
43
43
|
timeoutMs: run.timeoutMs,
|
|
44
44
|
strict: run.strict,
|
|
@@ -85,7 +85,7 @@ describe("toBraintrustEvent", () => {
|
|
|
85
85
|
model: "gpt-5.2",
|
|
86
86
|
attempt: 1,
|
|
87
87
|
outcome: "failed",
|
|
88
|
-
experiment: { id: "compare/codex",
|
|
88
|
+
experiment: { id: "compare/codex", flags: { tape: true } },
|
|
89
89
|
assertions: [{ name: "compiles", severity: "gate", score: 0, passed: false, detail: "tsc failed" }],
|
|
90
90
|
}),
|
|
91
91
|
);
|
|
@@ -96,7 +96,7 @@ describe("toBraintrustEvent", () => {
|
|
|
96
96
|
outcome: "failed",
|
|
97
97
|
model: "gpt-5.2",
|
|
98
98
|
experiment: "compare/codex",
|
|
99
|
-
|
|
99
|
+
flags: { tape: true },
|
|
100
100
|
failedAssertions: [{ name: "compiles", detail: "tsc failed" }],
|
|
101
101
|
});
|
|
102
102
|
expect(event.id).toBe("compare/codex|algebra/quadratic|codex|gpt-5.2|a1");
|
|
@@ -113,7 +113,7 @@ export function Braintrust(config: BraintrustConfig = {}): Reporter {
|
|
|
113
113
|
* - scores:soft 断言按名字记分,gate 断言记在 `gate:` 前缀下 —— 实验 diff 里
|
|
114
114
|
* gate 回归和 soft 分数回归用同一套机制看。重名断言追加 `#n` 消歧,不静默覆盖。
|
|
115
115
|
* - metrics:start/end(Braintrust 由此算时长)+ token 用量 + 估算成本;缺就不写,不编 0。
|
|
116
|
-
* - metadata:身份维度(agent / model / experiment / attempt /
|
|
116
|
+
* - metadata:身份维度(agent / model / experiment / attempt / flags)+ 失败断言明细。
|
|
117
117
|
*/
|
|
118
118
|
export function toBraintrustEvent(result: EvalResult): BraintrustLogEvent {
|
|
119
119
|
const scores: Record<string, number> = {};
|
|
@@ -150,8 +150,8 @@ export function toBraintrustEvent(result: EvalResult): BraintrustLogEvent {
|
|
|
150
150
|
};
|
|
151
151
|
if (result.model !== undefined) metadata.model = result.model;
|
|
152
152
|
if (result.experimentId !== undefined) metadata.experiment = result.experimentId;
|
|
153
|
-
if (result.experiment?.
|
|
154
|
-
metadata.
|
|
153
|
+
if (result.experiment?.flags && Object.keys(result.experiment.flags).length > 0) {
|
|
154
|
+
metadata.flags = result.experiment.flags;
|
|
155
155
|
}
|
|
156
156
|
if (result.skipReason !== undefined) metadata.skipReason = result.skipReason;
|
|
157
157
|
const failed = result.assertions
|
package/src/runner/run.ts
CHANGED
|
@@ -104,7 +104,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
104
104
|
if (run.experimentId && priorRunKeys.has(`${run.experimentId}|${evalDef.id}`)) continue;
|
|
105
105
|
// key 标识「同一个运行配置下的同一条 eval」,earlyExit 的跳过/abort 只应作用于
|
|
106
106
|
// 同 key 的重试轮。experimentId 必须进 key:两个实验可以同 agent 同 model、只差
|
|
107
|
-
//
|
|
107
|
+
// flags(feature A/B 正是这种形状),漏掉它会让先过的实验把其它实验的同名 eval
|
|
108
108
|
// 整个跳掉——花了钱还丢结果。
|
|
109
109
|
const key = `${run.experimentId ?? ""}|${run.agent.name}|${run.model ?? ""}|${evalDef.id}`;
|
|
110
110
|
attempts.push({
|
package/src/runner/types.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type { TestContext } from "../context/types.ts";
|
|
|
12
12
|
|
|
13
13
|
export interface ExperimentRunInfo {
|
|
14
14
|
id?: string;
|
|
15
|
-
|
|
15
|
+
flags?: Record<string, unknown>;
|
|
16
16
|
runs?: number;
|
|
17
17
|
earlyExit?: boolean;
|
|
18
18
|
sandbox?: string;
|
|
@@ -61,7 +61,7 @@ export interface EvalResult {
|
|
|
61
61
|
/** `summary.json` 的格式标记;把 niceeval 报告和其它工具的同名文件区分开。 */
|
|
62
62
|
export const RESULTS_FORMAT = "niceeval.results";
|
|
63
63
|
/** 结果格式版本,只在破坏兼容读取时递增;读取器只认相同版本,缺失按 1。见 docs/results-format.md。 */
|
|
64
|
-
export const RESULTS_SCHEMA_VERSION =
|
|
64
|
+
export const RESULTS_SCHEMA_VERSION = 3;
|
|
65
65
|
|
|
66
66
|
export interface RunSummary {
|
|
67
67
|
/** 恒为 "niceeval.results";和 schemaVersion、producer 一起构成持久化契约,永不移动或改名。 */
|
|
@@ -95,11 +95,11 @@ export interface RunSummary {
|
|
|
95
95
|
snapshots?: Record<string, { startedAt?: string; knownEvalIds?: string[] }>;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
/** onRunStart 的运行规模:去重后 eval 数 × 配置(
|
|
98
|
+
/** onRunStart 的运行规模:去重后 eval 数 × 配置(agent×model×flags)数 → 总运行(attempt)数。 */
|
|
99
99
|
export interface RunShape {
|
|
100
100
|
/** 去重后实际要跑的 eval 数(= evals.length)。 */
|
|
101
101
|
evals: number;
|
|
102
|
-
/** (agent, model,
|
|
102
|
+
/** (agent, model, flags) 配置组合数;compare 多 agent 时 > 1。 */
|
|
103
103
|
configs: number;
|
|
104
104
|
/** 总 attempt 数(evals × configs × runs);逐行输出与汇总计数都按它。 */
|
|
105
105
|
totalRuns: number;
|
|
@@ -174,8 +174,8 @@ export interface ExperimentDef {
|
|
|
174
174
|
model?: string;
|
|
175
175
|
/** 模型推理努力程度(如 "low"/"medium"/"high",取值由具体模型/adapter 决定);省略=用 agent 原生默认。经 ctx.reasoningEffort 透给 adapter 与 eval。 */
|
|
176
176
|
reasoningEffort?: string;
|
|
177
|
-
/**
|
|
178
|
-
|
|
177
|
+
/** 实验条件(A/B 里的 feature flag),由实验文件声明;经 ctx.flags 透传给 adapter、t.flags 暴露给 eval。 */
|
|
178
|
+
flags?: Record<string, unknown>;
|
|
179
179
|
/** 同一 eval 重复跑几次(结果各计一条 attempt);省略/CLI `--runs` 覆盖时默认 1。 */
|
|
180
180
|
runs?: number;
|
|
181
181
|
/** 一次重复(runs > 1)里某次 attempt 失败后是否跳过剩余重复;省略默认 true(提前退出省钱)。 */
|
|
@@ -267,12 +267,12 @@ export function runWho(run: Pick<AgentRun, "agent" | "model" | "experimentId">):
|
|
|
267
267
|
return run.model ? `${run.agent.name}/${run.model}` : run.agent.name;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
/** 一个 (agent, model,
|
|
270
|
+
/** 一个 (agent, model, flags) 的运行配置 —— 由 CLI / 实验展开。 */
|
|
271
271
|
export interface AgentRun {
|
|
272
272
|
agent: Agent;
|
|
273
273
|
model?: string;
|
|
274
274
|
reasoningEffort?: string;
|
|
275
|
-
|
|
275
|
+
flags: Record<string, unknown>;
|
|
276
276
|
runs: number;
|
|
277
277
|
earlyExit: boolean;
|
|
278
278
|
sandbox?: SandboxOption;
|
package/src/sandbox/types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// sandbox 域类型:Sandbox 接口、后端 spec(可辨识联合)、命令与文件 IO 的形状。
|
|
2
2
|
// 「在哪里跑、如何隔离」的全部契约在这里;后端实现见本目录各文件,分发见 resolve.ts。
|
|
3
3
|
|
|
4
|
+
import type { AgentSetup, AgentTeardown } from "../agents/types.ts";
|
|
5
|
+
|
|
4
6
|
export interface CommandResult {
|
|
5
7
|
stdout: string;
|
|
6
8
|
stderr: string;
|
|
@@ -51,25 +53,64 @@ export type SandboxBackend = "docker" | "vercel" | "e2b";
|
|
|
51
53
|
/** 镜像/模板里的 Node 运行时版本。 */
|
|
52
54
|
export type SandboxRuntime = "node20" | "node24";
|
|
53
55
|
|
|
56
|
+
/**
|
|
57
|
+
* SandboxSpec 四个变体共有的沙箱生命周期钩子挂载点:`.setup(fn)` / `.teardown(fn)` 链式方法,
|
|
58
|
+
* 由 `dockerSandbox()` / `vercelSandbox()` / `e2bSandbox()` / `defineSandbox()` 产出的对象上
|
|
59
|
+
* 直接可调。
|
|
60
|
+
*
|
|
61
|
+
* **语义**:环境预置层,俗称「动态镜像层」——本想直接烘进镜像/快照/模板,但内容要按实验
|
|
62
|
+
* (`ctx.flags` / `ctx.experimentId`)动态变化的东西:装某个实验专属的二进制、预热缓存、
|
|
63
|
+
* 写运行期才知道内容的 hook 文件、按 `ctx.experimentId` 载入/回存跨 attempt 的状态。
|
|
64
|
+
*
|
|
65
|
+
* **与另外两层 setup 的分工**(三者都在同一个沙箱生命周期里,各管一层):
|
|
66
|
+
* · `sandbox.setup`(这里)—— 环境层,不知道具体跑哪个 eval / 接哪个 agent;
|
|
67
|
+
* · `eval.setup` —— 任务层,准备这次 eval 的素材(如 `npm install` 起始项目依赖);
|
|
68
|
+
* · `agent.setup` —— 协议层,接入被测 agent(装 CLI、写鉴权 / 模型配置)。
|
|
69
|
+
*
|
|
70
|
+
* **执行顺序**:沙箱就绪 → `sandbox.setup` 钩子 → workspace 上传 / git 基线 / `eval.setup` →
|
|
71
|
+
* `agent.setup` → `agent.tracing.configure` → 逐轮 `send`。`sandbox.setup` 特意排在 git
|
|
72
|
+
* 基线之前——它的改动会被提交进基线,不会被误算进 agent 产出的 diff。收尾按 LIFO:
|
|
73
|
+
* agent 级 cleanup / `agent.teardown` 先跑,`sandbox.teardown` 钩子最后跑(沙箱销毁前)。
|
|
74
|
+
*
|
|
75
|
+
* **多钩子**:`.setup(a).setup(b)` 按追加顺序依次执行(a 先 b 后);`.teardown(x).teardown(y)`
|
|
76
|
+
* 按追加的**逆序**执行(y 先 x 后)。每次调用都返回一个新 spec(不改变原对象),可继续链式。
|
|
77
|
+
*
|
|
78
|
+
* **失败语义**:`setup` 钩子抛错按执行错误计——与 `eval.setup` / `agent.setup` 抛错走同一条
|
|
79
|
+
* 路径,不新增错误分类;但不阻断该 attempt 已进入的收尾(已跑过的 setup 返回的 cleanup、
|
|
80
|
+
* `teardown` 钩子仍会在 finally 里跑)。`teardown` 钩子报错只作诊断(吞掉 / 记 log),不改变
|
|
81
|
+
* 已产出的结果——与 `agent.teardown` 现状一致。
|
|
82
|
+
*/
|
|
83
|
+
export interface SandboxHooks<Self> {
|
|
84
|
+
/** 已挂载的 setup 钩子,按追加顺序保存(内部读取,一般用不到)。 */
|
|
85
|
+
readonly setupHooks?: readonly AgentSetup[];
|
|
86
|
+
/** 已挂载的 teardown 钩子,按追加顺序保存,执行时逆序(内部读取,一般用不到)。 */
|
|
87
|
+
readonly teardownHooks?: readonly AgentTeardown[];
|
|
88
|
+
/** 追加一个沙箱级 setup 钩子,返回新 spec;详细契约见 {@link SandboxHooks}。 */
|
|
89
|
+
setup(fn: AgentSetup): Self;
|
|
90
|
+
/** 追加一个沙箱级 teardown 钩子,返回新 spec;详细契约见 {@link SandboxHooks}。 */
|
|
91
|
+
teardown(fn: AgentTeardown): Self;
|
|
92
|
+
}
|
|
93
|
+
|
|
54
94
|
/**
|
|
55
95
|
* Sandbox 的「数据结构」定义 —— 与 agent 一样可带参数(见 docs/sandbox.md)。
|
|
56
96
|
* 必须用工厂函数构造(`dockerSandbox()` / `vercelSandbox()` / `e2bSandbox()` / `defineSandbox()`),
|
|
57
97
|
* 放进 config / experiment 的 `sandbox` 字段 —— 字段类型只接受这个数据结构,不接受裸字符串。
|
|
58
98
|
* 各后端的参数互不相同 —— 这是个按 `backend` 区分的可辨识联合(discriminated union)。
|
|
99
|
+
* 四个变体都带 {@link SandboxHooks} 的 `.setup()` / `.teardown()` 链式方法。
|
|
59
100
|
*/
|
|
60
|
-
export interface DockerSandboxSpec {
|
|
101
|
+
export interface DockerSandboxSpec extends SandboxHooks<DockerSandboxSpec> {
|
|
61
102
|
readonly backend: "docker";
|
|
62
103
|
/** 覆盖默认镜像;默认按 runtime 选 `node:*-slim`。预制模板:传烘焙好 agent CLI 的镜像名。 */
|
|
63
104
|
readonly image?: string;
|
|
64
105
|
readonly runtime?: SandboxRuntime;
|
|
65
106
|
}
|
|
66
|
-
export interface VercelSandboxSpec {
|
|
107
|
+
export interface VercelSandboxSpec extends SandboxHooks<VercelSandboxSpec> {
|
|
67
108
|
readonly backend: "vercel";
|
|
68
109
|
/** 从已有快照起 microVM。预制模板:烘焙好 agent CLI 的 snapshotId。 */
|
|
69
110
|
readonly snapshotId?: string;
|
|
70
111
|
readonly runtime?: SandboxRuntime;
|
|
71
112
|
}
|
|
72
|
-
export interface E2BSandboxSpec {
|
|
113
|
+
export interface E2BSandboxSpec extends SandboxHooks<E2BSandboxSpec> {
|
|
73
114
|
readonly backend: "e2b";
|
|
74
115
|
/** e2b 模板名/ID。预制模板:烘焙好 agent CLI 的模板(如 `"niceeval-agents"`)。省略用 e2b 默认 `"base"`。 */
|
|
75
116
|
readonly template?: string;
|
|
@@ -80,7 +121,7 @@ export interface E2BSandboxSpec {
|
|
|
80
121
|
* 用户自定义后端:`create` 直接产出一个 `Sandbox` 实例,不经 resolve.ts 的内置 backend switch。
|
|
81
122
|
* 用 `defineSandbox()` 构造(见 src/define.ts)。`backend` 只用于展示 / 日志,不参与分发。
|
|
82
123
|
*/
|
|
83
|
-
export interface CustomSandboxSpec {
|
|
124
|
+
export interface CustomSandboxSpec extends SandboxHooks<CustomSandboxSpec> {
|
|
84
125
|
readonly backend: string;
|
|
85
126
|
readonly runtime?: SandboxRuntime;
|
|
86
127
|
readonly recommendedConcurrency?: number;
|
package/src/show/compose.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// results.latest() 只挑「每实验最新快照」,带 eval 前缀的局部重跑会产出残缺快照;
|
|
5
5
|
// 榜单承诺「不会因为一次局部重跑变残缺」,所以宿主在实验的全部历史快照上逐 eval
|
|
6
6
|
// 向更早的 run 补齐,再把合成好的选集注入报告槽——内置默认报告与 --report 吃同一份,
|
|
7
|
-
//
|
|
7
|
+
// 默认报告口径 = 宿主注入口径。本文件只消费 niceeval/results 的读取面。
|
|
8
8
|
|
|
9
9
|
import { foldEvalOutcome } from "../shared/outcome.ts";
|
|
10
10
|
import { evalPrefixPredicate } from "../report/aggregate.ts";
|
package/src/view/app/App.tsx
CHANGED
|
@@ -15,7 +15,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs.t
|
|
|
15
15
|
import { RunsView } from "./pages/RunsPage.tsx";
|
|
16
16
|
import { TracesView } from "./pages/TracesPage.tsx";
|
|
17
17
|
|
|
18
|
-
//
|
|
18
|
+
// 首页报告只有一份:不传 --report 时是默认报告(Experiments tab),传了整页替换成用户报告。
|
|
19
19
|
// 证据室(Runs / Traces / AttemptModal)是 view 本体,两种模式下原样保留。
|
|
20
20
|
export const navItems: { id: Tab; label: MessageKey }[] = [
|
|
21
21
|
{ id: "experiments", label: "nav.experiments" },
|
package/src/view/app/i18n.ts
CHANGED
|
@@ -58,7 +58,7 @@ export type MessageKey =
|
|
|
58
58
|
| "detail.rawSample"
|
|
59
59
|
| "detail.rawNote"
|
|
60
60
|
| "config.experiment"
|
|
61
|
-
| "config.
|
|
61
|
+
| "config.flagsNone"
|
|
62
62
|
| "config.default"
|
|
63
63
|
| "config.none"
|
|
64
64
|
| "config.notApplicable"
|
|
@@ -177,7 +177,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
|
|
177
177
|
"detail.rawSample": "Raw sample result",
|
|
178
178
|
"detail.rawNote": "debug JSON, defaults to first error/failure when available",
|
|
179
179
|
"config.experiment": "experiment",
|
|
180
|
-
"config.
|
|
180
|
+
"config.flagsNone": "none",
|
|
181
181
|
"config.default": "default",
|
|
182
182
|
"config.none": "none",
|
|
183
183
|
"config.notApplicable": "n/a",
|
|
@@ -293,7 +293,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
|
|
293
293
|
"detail.rawSample": "原始样例结果",
|
|
294
294
|
"detail.rawNote": "调试 JSON,默认选择第一条错误/失败",
|
|
295
295
|
"config.experiment": "实验",
|
|
296
|
-
"config.
|
|
296
|
+
"config.flagsNone": "无",
|
|
297
297
|
"config.default": "默认",
|
|
298
298
|
"config.none": "无",
|
|
299
299
|
"config.notApplicable": "不适用",
|
package/src/view/app/lib/rows.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { evalLevelStats } from "../../../shared/outcome.ts";
|
|
|
10
10
|
import { displayExperimentName, fallbackExperimentLabel, sumMaybe } from "../../../shared/aggregate.ts";
|
|
11
11
|
import { formatConfigValue } from "./format.ts";
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/** 默认报告格子的列键(= 内置指标的 metric.name,见 src/report/metrics.ts)。 */
|
|
14
14
|
export const CELL_KEYS = {
|
|
15
15
|
passRate: "pass-rate",
|
|
16
16
|
duration: "duration",
|
|
@@ -127,9 +127,9 @@ export function valueFor(row: ViewRow, key: SortKey): string | number | null {
|
|
|
127
127
|
|
|
128
128
|
export function configChips(row: ViewRow, t: T): [string, ReactNode][] {
|
|
129
129
|
const exp = row.experiment || {};
|
|
130
|
-
const
|
|
131
|
-
? Object.entries(exp.
|
|
132
|
-
: t("config.
|
|
130
|
+
const flags = exp.flags && Object.keys(exp.flags).length
|
|
131
|
+
? Object.entries(exp.flags).map(([k, v]) => k + "=" + formatConfigValue(v)).join(", ")
|
|
132
|
+
: t("config.flagsNone");
|
|
133
133
|
return [
|
|
134
134
|
[t("config.experiment"), row.synthetic ? row.label : row.experimentId],
|
|
135
135
|
[t("table.model"), row.model || t("config.default")],
|
|
@@ -138,6 +138,6 @@ export function configChips(row: ViewRow, t: T): [string, ReactNode][] {
|
|
|
138
138
|
["earlyExit", exp.earlyExit === undefined ? t("config.notApplicable") : String(exp.earlyExit)],
|
|
139
139
|
["sandbox", exp.sandbox || t("config.default")],
|
|
140
140
|
["budget", exp.budget === undefined ? t("config.none") : "$" + exp.budget],
|
|
141
|
-
["
|
|
141
|
+
["flags", flags],
|
|
142
142
|
];
|
|
143
143
|
}
|
package/src/view/app/types.ts
CHANGED
|
@@ -48,7 +48,7 @@ export interface ViewRow {
|
|
|
48
48
|
model?: string;
|
|
49
49
|
/** 该快照的 startedAt(ISO)—— 榜单「每行带判决产生的时间」。 */
|
|
50
50
|
lastRunAt: string;
|
|
51
|
-
/**
|
|
51
|
+
/** 默认报告格子(pass-rate / duration / tokens / cost),display 已格式化。 */
|
|
52
52
|
cells: Partial<Record<string, MetricCell>>;
|
|
53
53
|
/** 快照内 attempt 总数。 */
|
|
54
54
|
runs: number;
|