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.
Files changed (75) hide show
  1. package/docs-site/zh/concepts/adapter.mdx +11 -6
  2. package/docs-site/zh/concepts/experiment.mdx +8 -6
  3. package/docs-site/zh/concepts/overview.mdx +1 -1
  4. package/docs-site/zh/concepts/tier.mdx +6 -6
  5. package/docs-site/zh/guides/ci-integration.mdx +2 -0
  6. package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
  7. package/docs-site/zh/guides/custom-reports.mdx +13 -13
  8. package/docs-site/zh/guides/experiments.mdx +4 -3
  9. package/docs-site/zh/guides/publish-report.mdx +91 -0
  10. package/docs-site/zh/guides/report-components.mdx +4 -4
  11. package/docs-site/zh/guides/reporters.mdx +1 -1
  12. package/docs-site/zh/guides/results-data.mdx +2 -2
  13. package/docs-site/zh/guides/runner.mdx +1 -1
  14. package/docs-site/zh/guides/sandbox-agent.mdx +11 -6
  15. package/docs-site/zh/guides/sandbox-backends.mdx +27 -0
  16. package/docs-site/zh/guides/viewing-results.mdx +5 -21
  17. package/docs-site/zh/guides/write-experiment.mdx +24 -10
  18. package/docs-site/zh/guides/write-send.mdx +6 -6
  19. package/docs-site/zh/index.mdx +1 -1
  20. package/docs-site/zh/introduction.mdx +1 -1
  21. package/docs-site/zh/reference/builtin-agents.mdx +4 -0
  22. package/docs-site/zh/reference/cli.mdx +9 -6
  23. package/docs-site/zh/reference/define-agent.mdx +19 -3
  24. package/docs-site/zh/reference/define-config.mdx +3 -1
  25. package/docs-site/zh/reference/define-eval.mdx +9 -3
  26. package/docs-site/zh/reference/events.mdx +4 -0
  27. package/docs-site/zh/reference/expect.mdx +4 -0
  28. package/package.json +3 -2
  29. package/src/agents/ai-sdk.test.ts +1 -1
  30. package/src/agents/ai-sdk.ts +2 -2
  31. package/src/agents/bub.ts +5 -1
  32. package/src/agents/claude-code.ts +1 -1
  33. package/src/agents/codex.ts +2 -2
  34. package/src/agents/index.ts +2 -0
  35. package/src/agents/streaming.test.ts +1 -1
  36. package/src/agents/types.ts +10 -2
  37. package/src/agents/ui-message-stream.test.ts +1 -1
  38. package/src/cli.ts +38 -14
  39. package/src/context/context.test.ts +1 -1
  40. package/src/context/context.ts +6 -3
  41. package/src/context/session.ts +5 -2
  42. package/src/context/types.ts +2 -2
  43. package/src/define.test.ts +82 -0
  44. package/src/define.ts +68 -7
  45. package/src/i18n/en.ts +4 -2
  46. package/src/i18n/zh-CN.ts +4 -2
  47. package/src/report/aggregate.ts +16 -16
  48. package/src/report/components.tsx +1 -1
  49. package/src/report/compute.ts +7 -7
  50. package/src/report/dual-face.test.tsx +1 -1
  51. package/src/report/{param.ts → flag.ts} +6 -6
  52. package/src/report/index.ts +3 -3
  53. package/src/report/react/MetricLine.tsx +1 -1
  54. package/src/report/react/fixtures.ts +1 -1
  55. package/src/report/report.test.ts +17 -17
  56. package/src/report/report.ts +1 -1
  57. package/src/report/types.ts +11 -11
  58. package/src/results/results.test.ts +2 -2
  59. package/src/runner/attempt.ts +53 -6
  60. package/src/runner/fingerprint.ts +1 -1
  61. package/src/runner/reporters/braintrust.test.ts +2 -2
  62. package/src/runner/reporters/braintrust.ts +3 -3
  63. package/src/runner/run.ts +1 -1
  64. package/src/runner/types.ts +8 -8
  65. package/src/sandbox/types.ts +45 -4
  66. package/src/show/compose.ts +1 -1
  67. package/src/view/app/App.tsx +1 -1
  68. package/src/view/app/css.d.ts +3 -0
  69. package/src/view/app/i18n.ts +3 -3
  70. package/src/view/app/lib/rows.ts +5 -5
  71. package/src/view/app/types.ts +1 -1
  72. package/src/view/client-dist/app.js +2 -2
  73. package/src/view/data.test.ts +26 -2
  74. package/src/view/data.ts +28 -4
  75. package/src/view/shared/types.ts +1 -1
package/src/cli.ts CHANGED
@@ -17,7 +17,7 @@ import { stopAllSandboxes, liveSandboxCount } from "./sandbox/registry.ts";
17
17
  import { evalLevelStats } from "./shared/outcome.ts";
18
18
  import { sandboxRecommendedConcurrency } from "./sandbox/resolve.ts";
19
19
  import { Console as ConsoleReporter } from "./runner/reporters/console.ts";
20
- import { JUnit } from "./runner/reporters/json.ts";
20
+ import { Json, JUnit } from "./runner/reporters/json.ts";
21
21
  import { Live as LiveReporter, type LiveRow } from "./runner/reporters/live.ts";
22
22
  import { Artifacts as ArtifactsReporter } from "./runner/reporters/artifacts.ts";
23
23
  import {
@@ -60,6 +60,7 @@ interface Flags {
60
60
  budget?: number;
61
61
  tag?: string;
62
62
  junit?: string;
63
+ json?: string;
63
64
  open?: boolean;
64
65
  out?: string;
65
66
  port?: number;
@@ -80,43 +81,69 @@ interface Flags {
80
81
 
81
82
  // 表驱动的 flag 定义(node:util parseArgs)。--no-x 显式声明,不依赖 allowNegative(需 Node 20.14+,
82
83
  // engines 是 >=18)。未知 flag 由 strict 模式报清晰错误,不再静默吞掉后面的位置参数。
84
+ //
85
+ // 每个 flag 的 JSDoc 就是它在 docs-site/zh/reference/cli.mdx flag 表里的说明,由
86
+ // scripts/generate-reference.ts 提取渲染——改 flag 语义时改这里的注释即可,不用碰生成脚本。
87
+ // 负向 flag(no-x)与正向 flag 合并成一行展示,不需要单独写 JSDoc。
83
88
  const FLAG_OPTIONS = {
89
+ /** experiment 运行不支持该 flag。要换 agent,请在 `experiments/` 下新增或复制一个配置文件。 */
84
90
  agent: { type: "string" },
91
+ /** experiment 运行不支持该 flag。要换模型,请新增或复制一个 experiment 文件并修改 `model`。 */
85
92
  model: { type: "string" },
93
+ /** 每个 eval 运行多少次,常用于 pass@N。 */
86
94
  runs: { type: "string" },
95
+ /** 设置同时运行的 eval 数量。 */
87
96
  "max-concurrency": { type: "string" },
97
+ /** 单个 attempt 的超时时间,单位毫秒。 */
88
98
  timeout: { type: "string" },
99
+ /** 整次运行的预算上限(美元)。 */
89
100
  budget: { type: "string" },
101
+ /** 只运行带有该 tag 的 eval(见 `defineEval` 的 `tags`)。 */
90
102
  tag: { type: "string" },
103
+ /** 额外写一份 JUnit XML 报告到指定路径,供 CI 消费。 */
91
104
  junit: { type: "string" },
105
+ /** 额外写一份 JSON 结果(`RunSummary` 原样序列化)到指定路径,供 CI 或下游脚本消费。 */
106
+ json: { type: "string" },
107
+ /** `view` 命令专用:把结果查看器静态导出到指定目录。 */
92
108
  out: { type: "string" },
109
+ /** `view` 命令专用:指定本地服务器监听端口。 */
93
110
  port: { type: "string" },
94
- // --sandbox 已移除(sandbox 归 config/experiment);留着解析是为了给出迁移提示而非「未知 flag」。
95
- sandbox: { type: "string" },
96
- // --watch / --json:文档曾接受、尚未实现的 flag。容忍(no-op)而不是硬报「未知 flag」,
97
- // 免得照旧文档写的脚本直接崩;真正实现前不接任何行为。
98
- watch: { type: "boolean" },
99
- json: { type: "boolean" },
100
111
  // show 的证据切面 / 时间轴 / 报告装载(docs-site/zh/guides/viewing-results.mdx)。
112
+ /** `show` 命令专用:渲染单个 eval 的完整对话与工具调用(证据切面)。 */
101
113
  transcript: { type: "boolean" },
114
+ /** `show` 命令专用:渲染单个 eval 的 trace 瀑布文本版(证据切面)。 */
102
115
  trace: { type: "boolean" },
103
116
  // --diff 是布尔;--diff=<路径> 在 parseArgs 前预扫成 diffPath(路径必须 = 连写,
104
117
  // 空格形式的下一个 token 仍是位置参数 = eval id 前缀,与文档一致)。
118
+ /** `show` 命令专用:sandbox 里的文件改动摘要;`--diff=<文件路径>` 看单个文件的完整改动(路径必须 `=` 连写)。 */
105
119
  diff: { type: "boolean" },
120
+ /** `show` 命令专用:跨 run 时间轴,只列真实执行;与 `--report` 互斥。 */
106
121
  history: { type: "boolean" },
122
+ /** `show` / `view` 命令专用:选集只留该实验。 */
107
123
  experiment: { type: "string" },
124
+ /** `show` 命令专用:指定详情 / 证据切面看第几次 attempt(与展示一致的 1 计序号)。 */
108
125
  attempt: { type: "string" },
126
+ /** `show` / `view` 命令专用:钉死看某一个结果目录(历史 run 或 `copySnapshots` 产物)。 */
109
127
  run: { type: "string" },
128
+ /** `show` / `view` 命令专用:用你的报告文件替换默认报告(文件默认导出 `defineReport(...)`)。 */
110
129
  report: { type: "string" },
130
+ /** 只打印本次会匹配到的 eval × 运行配置,不实际执行。 */
111
131
  dry: { type: "boolean" },
132
+ /** 关闭控制台 / live 进度输出(reporter 仍会写 artifacts)。 */
112
133
  quiet: { type: "boolean" },
134
+ /** 忽略上次运行结果,不跳过已通过的 (experiment, eval) 组合,强制全部重跑。 */
113
135
  force: { type: "boolean" },
136
+ /** CI 中推荐使用:让软阈值(`soft`)失败也计入整条 eval 的 outcome。 */
114
137
  strict: { type: "boolean" },
138
+ /** 某个 eval 的一次 attempt 通过后,停止该 eval 剩余的 attempts。 */
115
139
  "early-exit": { type: "boolean" },
116
140
  "no-early-exit": { type: "boolean" },
141
+ /** `view` 命令专用:启动后自动打开浏览器(默认行为)。 */
117
142
  open: { type: "boolean" },
118
143
  "no-open": { type: "boolean" },
144
+ /** 打印用法说明并退出。 */
119
145
  help: { type: "boolean", short: "h" },
146
+ /** 打印 niceeval 的版本号并退出。 */
120
147
  version: { type: "boolean", short: "v" },
121
148
  } as const;
122
149
 
@@ -155,11 +182,6 @@ function parseArgs(argv: string[]): { command: string; positionals: string[]; fl
155
182
  process.exit(1);
156
183
  }
157
184
 
158
- if (values.sandbox !== undefined) {
159
- process.stderr.write(t("cli.sandboxFlagRemoved"));
160
- process.exit(1);
161
- }
162
-
163
185
  // 第一个位置参数若是已知命令,则为命令;其余是 eval id 前缀 / view 输入。
164
186
  const commands = new Set(["exp", "show", "list", "view", "clean", "init", "watch", "run"]);
165
187
  let command = "run";
@@ -178,6 +200,7 @@ function parseArgs(argv: string[]): { command: string; positionals: string[]; fl
178
200
  budget: numberFlag("budget", values.budget as string | undefined),
179
201
  tag: values.tag as string | undefined,
180
202
  junit: values.junit as string | undefined,
203
+ json: values.json as string | undefined,
181
204
  out: values.out as string | undefined,
182
205
  port: numberFlag("port", values.port as string | undefined),
183
206
  dry: values.dry === true,
@@ -449,7 +472,7 @@ async function main(): Promise<void> {
449
472
  agent: exp.agent,
450
473
  model: exp.model,
451
474
  reasoningEffort: exp.reasoningEffort,
452
- params: exp.params ?? {},
475
+ flags: exp.flags ?? {},
453
476
  runs: flags.runs ?? envNumber("NICEEVAL_RUNS") ?? exp.runs ?? 1,
454
477
  earlyExit: flags.earlyExit ?? exp.earlyExit ?? true,
455
478
  sandbox: exp.sandbox ?? config.sandbox,
@@ -465,7 +488,7 @@ async function main(): Promise<void> {
465
488
  }
466
489
  } else {
467
490
  // 裸 run / `niceeval <eval>` 不再执行。运行配置必须来自 experiments/,
468
- // 这样 agent/model/params/runs/budget 与结果聚合都有可签入的身份。
491
+ // 这样 agent/model/flags/runs/budget 与结果聚合都有可签入的身份。
469
492
  const experiments = await discoverExperiments(cwd);
470
493
  const asExp = experiments.filter((e) =>
471
494
  positionals.some((p) => e.group === p || e.id === p || e.id.startsWith(p + "/")),
@@ -526,6 +549,7 @@ async function main(): Promise<void> {
526
549
  const artifacts = ArtifactsReporter();
527
550
  reporters.push(artifacts);
528
551
  if (flags.junit) reporters.push(JUnit(flags.junit));
552
+ if (flags.json) reporters.push(Json(flags.json));
529
553
  reporters.push(...(config.reporters ?? []));
530
554
 
531
555
  // Ctrl+C / kill 的三级响应,核心目标:任何情况下都不留下孤儿沙箱。
@@ -57,7 +57,7 @@ function makeContext(agent: Agent, sandbox = fakeSandbox(), evalBaseDir?: string
57
57
  return createEvalContext({
58
58
  agent,
59
59
  sandbox,
60
- params: {},
60
+ flags: {},
61
61
  signal: new AbortController().signal,
62
62
  log: () => {},
63
63
  judge: undefined,
@@ -61,7 +61,9 @@ export interface ContextDeps {
61
61
  sandbox: Sandbox;
62
62
  model?: string;
63
63
  reasoningEffort?: string;
64
- params: Record<string, unknown>;
64
+ flags: Record<string, unknown>;
65
+ /** 路径推导出的实验 id(经 send ctx 透给 adapter,见 AgentContext.experimentId)。 */
66
+ experimentId?: string;
65
67
  signal: AbortSignal;
66
68
  log(msg: string): void;
67
69
  judge: JudgeConfig | undefined;
@@ -92,7 +94,8 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
92
94
  sandbox: deps.sandbox,
93
95
  model: deps.model,
94
96
  reasoningEffort: deps.reasoningEffort,
95
- params: deps.params,
97
+ flags: deps.flags,
98
+ experimentId: deps.experimentId,
96
99
  signal: deps.signal,
97
100
  log: deps.log,
98
101
  telemetry: deps.telemetry,
@@ -284,7 +287,7 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
284
287
  signal: deps.signal,
285
288
  model: deps.model,
286
289
  reasoningEffort: deps.reasoningEffort,
287
- params: deps.params,
290
+ flags: deps.flags,
288
291
  log: deps.log,
289
292
  skip: (reason: string) => {
290
293
  if (reason.trim().length === 0) throw new Error(t("context.skipEmpty"));
@@ -88,9 +88,11 @@ export interface SessionDeps {
88
88
  sandbox: Sandbox;
89
89
  model?: string;
90
90
  reasoningEffort?: string;
91
- params: Record<string, unknown>;
91
+ flags: Record<string, unknown>;
92
92
  signal: AbortSignal;
93
93
  log(msg: string): void;
94
+ /** 路径推导出的实验 id(经 send ctx 透给 adapter,见 AgentContext.experimentId)。 */
95
+ experimentId?: string;
94
96
  /** tracing agent 的 OTLP 端点(经 send ctx 透给 adapter,用于注入导出 env)。 */
95
97
  telemetry?: Telemetry;
96
98
  /** 非沙箱 tracing agent 的共享 OTLP 通道(runner 从 run 级池取,经它做逐轮 span 归属)。 */
@@ -138,7 +140,8 @@ export class SessionManager {
138
140
  signal: this.deps.signal,
139
141
  model: this.deps.model,
140
142
  reasoningEffort: this.deps.reasoningEffort,
141
- params: this.deps.params,
143
+ flags: this.deps.flags,
144
+ experimentId: this.deps.experimentId,
142
145
  sandbox: this.deps.sandbox,
143
146
  session,
144
147
  telemetry: this.deps.telemetry,
@@ -274,8 +274,8 @@ export interface TestContext {
274
274
  readonly model?: string;
275
275
  /** 本次 attempt 的推理努力程度(如 "low"/"medium"/"high",取值由 adapter/模型决定)。 */
276
276
  readonly reasoningEffort?: string;
277
- /** 本次 attempt 生效的 params(experiment.params 与 CLI flag 合并后的只读视图)。 */
278
- readonly params: Readonly<Record<string, unknown>>;
277
+ /** 本次 attempt 生效的实验 flags(experiment.flags 的只读视图;实验条件,非命令行开关)。 */
278
+ readonly flags: Readonly<Record<string, unknown>>;
279
279
  /** 打一行调试日志;有 live 进度回调时走该回调,否则落到 stderr,不出现在最终结果里。 */
280
280
  log(msg: string): void;
281
281
  /** 立即中止本 eval 并标记为 skipped(outcome / EvalResult.skipReason),reason 不能为空。 */
@@ -0,0 +1,82 @@
1
+ // SandboxSpec 链式钩子(.setup()/.teardown())的构造期契约:不可变、多次追加按顺序累加。
2
+ // 执行顺序(setup 正序 / teardown 逆序 / LIFO cleanup)是 runner 的事,见
3
+ // test/e2e-sandbox-hooks.test.ts;这里只测 dockerSandbox()/vercelSandbox()/e2bSandbox()/
4
+ // defineSandbox() 这四个工厂产出的 spec 对象本身的构造行为。
5
+ import { describe, expect, it } from "vitest";
6
+ import { dockerSandbox, e2bSandbox, vercelSandbox, defineSandbox } from "./define.ts";
7
+ import type { AgentSetup, AgentTeardown } from "./types.ts";
8
+
9
+ const noopSetup: AgentSetup = () => {};
10
+ const noopTeardown: AgentTeardown = () => {};
11
+
12
+ describe("sandbox factories: .setup()/.teardown() chain", () => {
13
+ it("dockerSandbox() starts with empty hook arrays", () => {
14
+ const spec = dockerSandbox({ image: "custom:latest" });
15
+ expect(spec.backend).toBe("docker");
16
+ expect(spec.image).toBe("custom:latest");
17
+ expect(spec.setupHooks).toEqual([]);
18
+ expect(spec.teardownHooks).toEqual([]);
19
+ });
20
+
21
+ it(".setup() returns a new spec and does not mutate the original", () => {
22
+ const base = dockerSandbox();
23
+ const withSetup = base.setup(noopSetup);
24
+ expect(withSetup).not.toBe(base);
25
+ expect(base.setupHooks).toEqual([]);
26
+ expect(withSetup.setupHooks).toEqual([noopSetup]);
27
+ });
28
+
29
+ it("multiple .setup() calls accumulate in append order", () => {
30
+ const a: AgentSetup = () => {};
31
+ const b: AgentSetup = () => {};
32
+ const spec = dockerSandbox().setup(a).setup(b);
33
+ expect(spec.setupHooks).toEqual([a, b]);
34
+ });
35
+
36
+ it("multiple .teardown() calls accumulate in append order (execution order is the runner's job)", () => {
37
+ const x: AgentTeardown = () => {};
38
+ const y: AgentTeardown = () => {};
39
+ const spec = dockerSandbox().teardown(x).teardown(y);
40
+ expect(spec.teardownHooks).toEqual([x, y]);
41
+ });
42
+
43
+ it("chaining preserves backend-specific fields and stacks across calls", () => {
44
+ const spec = dockerSandbox({ image: "img" }).setup(noopSetup).teardown(noopTeardown).setup(noopSetup);
45
+ expect(spec.backend).toBe("docker");
46
+ expect(spec.image).toBe("img");
47
+ expect(spec.setupHooks).toHaveLength(2);
48
+ expect(spec.teardownHooks).toHaveLength(1);
49
+ });
50
+
51
+ it("vercelSandbox() / e2bSandbox() chain the same way and keep their own fields", () => {
52
+ const vercel = vercelSandbox({ snapshotId: "snap-1" }).setup(noopSetup);
53
+ expect(vercel.backend).toBe("vercel");
54
+ expect(vercel.snapshotId).toBe("snap-1");
55
+ expect(vercel.setupHooks).toEqual([noopSetup]);
56
+
57
+ const e2b = e2bSandbox({ template: "niceeval-agents" }).teardown(noopTeardown);
58
+ expect(e2b.backend).toBe("e2b");
59
+ expect(e2b.template).toBe("niceeval-agents");
60
+ expect(e2b.teardownHooks).toEqual([noopTeardown]);
61
+ });
62
+
63
+ it("defineSandbox() (custom backend) chains too and keeps create()/name", async () => {
64
+ const create = async () => {
65
+ throw new Error("not called in this test");
66
+ };
67
+ const spec = defineSandbox({ name: "my-backend", create }).setup(noopSetup).setup(noopSetup);
68
+ expect(spec.backend).toBe("my-backend");
69
+ expect(spec.create).toBe(create);
70
+ expect(spec.setupHooks).toHaveLength(2);
71
+ });
72
+
73
+ it("each chain call produces an independent object (no shared mutable state)", () => {
74
+ const base = dockerSandbox();
75
+ const other: AgentSetup = () => {};
76
+ const branchA = base.setup(noopSetup);
77
+ const branchB = base.setup(other);
78
+ expect(branchA.setupHooks).toEqual([noopSetup]);
79
+ expect(branchB.setupHooks).toEqual([other]);
80
+ expect(branchA.setupHooks).not.toBe(branchB.setupHooks);
81
+ });
82
+ });
package/src/define.ts CHANGED
@@ -3,6 +3,8 @@
3
3
 
4
4
  import type {
5
5
  Agent,
6
+ AgentSetup,
7
+ AgentTeardown,
6
8
  Config,
7
9
  CustomSandboxSpec,
8
10
  DockerSandboxSpec,
@@ -11,6 +13,7 @@ import type {
11
13
  ExperimentDef,
12
14
  RemoteAgentDef,
13
15
  SandboxAgentDef,
16
+ SandboxHooks,
14
17
  VercelSandboxSpec,
15
18
  } from "./types.ts";
16
19
  import { t } from "./i18n/index.ts";
@@ -71,20 +74,70 @@ export function defineConfig(config: Config): Config {
71
74
  // ───────────────────────── Sandbox 工厂 ─────────────────────────
72
75
  // Sandbox 与 agent 一样用数据结构带参数(见 docs/sandbox.md)。这些工厂只是把
73
76
  // 后端 + 参数包成 spec 对象;真正的行为在 sandbox/<backend>.ts 里,由 resolve.ts 派发。
77
+ //
78
+ // 四个工厂都要挂上 `.setup()` / `.teardown()` 链式方法(见 sandbox/types.ts 的
79
+ // SandboxHooks<Self>):累积的钩子数组随 `HookState` 传递,每次链式调用都重新调
80
+ // `build()` 产出一个新对象,原 spec 不被修改。
81
+
82
+ /** 链式追加中累积的钩子(setup 按追加顺序执行,teardown 执行时逆序,见 SandboxHooks)。 */
83
+ interface HookState {
84
+ readonly setupHooks: readonly AgentSetup[];
85
+ readonly teardownHooks: readonly AgentTeardown[];
86
+ }
87
+
88
+ /** 四个工厂共用:把当前钩子状态包成 `.setup()` / `.teardown()` 方法,调用即用新状态重新 build。 */
89
+ function hookMethods<TSpec>(
90
+ state: HookState,
91
+ rebuild: (state: HookState) => TSpec,
92
+ ): Pick<SandboxHooks<TSpec>, "setup" | "teardown"> {
93
+ return {
94
+ setup: (fn) => rebuild({ setupHooks: [...state.setupHooks, fn], teardownHooks: state.teardownHooks }),
95
+ teardown: (fn) => rebuild({ setupHooks: state.setupHooks, teardownHooks: [...state.teardownHooks, fn] }),
96
+ };
97
+ }
98
+
99
+ const EMPTY_HOOKS: HookState = { setupHooks: [], teardownHooks: [] };
74
100
 
75
101
  /** Docker 沙箱:本地容器。`image` 可覆盖默认 `node:*-slim`(预制模板:烘焙好 agent CLI 的镜像)。 */
76
- export function dockerSandbox(opts: Omit<DockerSandboxSpec, "backend"> = {}): DockerSandboxSpec {
77
- return { backend: "docker", ...opts };
102
+ export function dockerSandbox(
103
+ opts: Omit<DockerSandboxSpec, "backend" | keyof SandboxHooks<unknown>> = {},
104
+ ): DockerSandboxSpec {
105
+ const build = (state: HookState): DockerSandboxSpec => ({
106
+ backend: "docker",
107
+ ...opts,
108
+ setupHooks: state.setupHooks,
109
+ teardownHooks: state.teardownHooks,
110
+ ...hookMethods(state, build),
111
+ });
112
+ return build(EMPTY_HOOKS);
78
113
  }
79
114
 
80
115
  /** Vercel Sandbox:microVM。`snapshotId` 从已有快照起(预制模板:烘焙好 agent CLI 的快照)。 */
81
- export function vercelSandbox(opts: Omit<VercelSandboxSpec, "backend"> = {}): VercelSandboxSpec {
82
- return { backend: "vercel", ...opts };
116
+ export function vercelSandbox(
117
+ opts: Omit<VercelSandboxSpec, "backend" | keyof SandboxHooks<unknown>> = {},
118
+ ): VercelSandboxSpec {
119
+ const build = (state: HookState): VercelSandboxSpec => ({
120
+ backend: "vercel",
121
+ ...opts,
122
+ setupHooks: state.setupHooks,
123
+ teardownHooks: state.teardownHooks,
124
+ ...hookMethods(state, build),
125
+ });
126
+ return build(EMPTY_HOOKS);
83
127
  }
84
128
 
85
129
  /** E2B 沙箱。`template` 选 e2b 模板名/ID(预制模板:如 `"niceeval-agents"`);省略用 e2b 默认 `"base"`。 */
86
- export function e2bSandbox(opts: Omit<E2BSandboxSpec, "backend"> = {}): E2BSandboxSpec {
87
- return { backend: "e2b", ...opts };
130
+ export function e2bSandbox(
131
+ opts: Omit<E2BSandboxSpec, "backend" | keyof SandboxHooks<unknown>> = {},
132
+ ): E2BSandboxSpec {
133
+ const build = (state: HookState): E2BSandboxSpec => ({
134
+ backend: "e2b",
135
+ ...opts,
136
+ setupHooks: state.setupHooks,
137
+ teardownHooks: state.teardownHooks,
138
+ ...hookMethods(state, build),
139
+ });
140
+ return build(EMPTY_HOOKS);
88
141
  }
89
142
 
90
143
  /**
@@ -98,5 +151,13 @@ export function defineSandbox(def: {
98
151
  }): CustomSandboxSpec {
99
152
  if (!def.name) throw new Error(t("define.sandboxNameRequired"));
100
153
  if (typeof def.create !== "function") throw new Error(t("define.sandboxCreateRequired"));
101
- return { backend: def.name, create: def.create, recommendedConcurrency: def.recommendedConcurrency };
154
+ const build = (state: HookState): CustomSandboxSpec => ({
155
+ backend: def.name,
156
+ create: def.create,
157
+ recommendedConcurrency: def.recommendedConcurrency,
158
+ setupHooks: state.setupHooks,
159
+ teardownHooks: state.teardownHooks,
160
+ ...hookMethods(state, build),
161
+ });
162
+ return build(EMPTY_HOOKS);
102
163
  }
package/src/i18n/en.ts CHANGED
@@ -59,7 +59,7 @@ export const en = {
59
59
  "Flags:\n" +
60
60
  " --runs n --max-concurrency n --timeout ms --budget usd --tag t\n" +
61
61
  " --early-exit / --no-early-exit --strict --force --dry --quiet\n" +
62
- " --junit path --out dir --port n --open / --no-open -h, --help -v, --version\n\n" +
62
+ " --junit path --json path --out dir --port n --open / --no-open -h, --help -v, --version\n\n" +
63
63
  "Positional args only select which evals to run (id prefixes); which agent and\n" +
64
64
  "how to run come from experiments/ + flags. Env overrides (flag > env > config):\n" +
65
65
  " NICEEVAL_RUNS NICEEVAL_MAX_CONCURRENCY NICEEVAL_TIMEOUT NICEEVAL_BUDGET\n",
@@ -94,10 +94,10 @@ export const en = {
94
94
  " Docs: node_modules/niceeval/docs-site/zh/guides/write-experiment.mdx\n",
95
95
  "cli.run.experimentRequiredHint": "Hint: \"{{pattern}}\" is an experiment{{kind}}; you probably meant: niceeval exp {{pattern}}\n",
96
96
  "cli.run.experimentRequiredKnown": "Discovered experiments: {{experiments}}\n",
97
- "cli.sandboxFlagRemoved": "`--sandbox` is not a CLI flag. Set `sandbox` in the experiment (or `niceeval.config.ts` as a project-wide fallback) to dockerSandbox() / vercelSandbox() / e2bSandbox() (import from \"niceeval/sandbox\").\n",
98
97
  "cli.unimplemented": "Command \"{{command}}\" is not implemented yet (MVP).\n",
99
98
  "cli.view.exportedDir": "Exported static report site: {{out}} (serve the whole directory with any static host; opening index.html via file:// cannot fetch artifacts)\n",
100
99
  "cli.view.incompatible": "{{dir}}: written by niceeval {{producer}} (schemaVersion {{schemaVersion}}); this CLI reads schemaVersion {{supported}}.\nRun `{{command}}` to view it.\n",
100
+ "cli.view.noResults": "No results found under {{root}}. Run `niceeval exp` first, then `niceeval view`.\n",
101
101
  "cli.view.incompatibleForeign": "{{dir}}: written by {{name}} {{version}} (schemaVersion {{schemaVersion}}); this CLI reads schemaVersion {{supported}}.\nOpen this report with the tool that produced it.\n",
102
102
  "cli.view.url": "niceeval view: {{url}}\n",
103
103
  "context.capabilityMissing":
@@ -186,6 +186,8 @@ export const en = {
186
186
  "runner.startAgentSetup": "agent setup (install CLI / write config)...",
187
187
  "runner.startAgentTracing": "agent tracing (write OTEL export config)...",
188
188
  "runner.startSandbox": "starting sandbox...",
189
+ "runner.startSandboxSetup": "sandbox setup (environment provisioning hooks)...",
190
+ "runner.startSandboxTeardown": "sandbox teardown (environment provisioning hooks)...",
189
191
  "runner.timeout": "attempt timed out ({{timeoutMs}}ms)\nRecent progress:\n{{recentLogs}}",
190
192
  "runner.traceSelected": " -> kept {{count}} semantic spans",
191
193
  "runner.resumeCarry": " · reusing {{carried}} passing results from last run, re-running {{retry}} evals\n",
package/src/i18n/zh-CN.ts CHANGED
@@ -55,7 +55,7 @@ export const zhCN = {
55
55
  "标志:\n" +
56
56
  " --runs n --max-concurrency n --timeout ms --budget usd --tag t\n" +
57
57
  " --early-exit / --no-early-exit --strict --force --dry --quiet\n" +
58
- " --junit path --out dir --port n --open / --no-open -h, --help -v, --version\n\n" +
58
+ " --junit path --json path --out dir --port n --open / --no-open -h, --help -v, --version\n\n" +
59
59
  "位置参数只选「跑哪些 eval」(id 前缀);对着哪个 agent、怎么跑来自 experiments/ 与\n" +
60
60
  "标志。环境变量覆盖(标志 > 环境变量 > config):\n" +
61
61
  " NICEEVAL_RUNS NICEEVAL_MAX_CONCURRENCY NICEEVAL_TIMEOUT NICEEVAL_BUDGET\n",
@@ -91,10 +91,10 @@ export const zhCN = {
91
91
  " 文档:node_modules/niceeval/docs-site/zh/guides/write-experiment.mdx\n",
92
92
  "cli.run.experimentRequiredHint": "提示:\"{{pattern}}\" 是实验{{kind}},你大概想跑:niceeval exp {{pattern}}\n",
93
93
  "cli.run.experimentRequiredKnown": "已发现实验:{{experiments}}\n",
94
- "cli.sandboxFlagRemoved": "`--sandbox` 不是 CLI flag。请在 experiment(或 niceeval.config.ts 做全项目兜底)里把 sandbox 设成 dockerSandbox() / vercelSandbox() / e2bSandbox()(从 \"niceeval/sandbox\" 导入)。\n",
95
94
  "cli.unimplemented": "命令 \"{{command}}\" 暂未实现(MVP)。\n",
96
95
  "cli.view.exportedDir": "已导出静态查看站:{{out}}(整个目录可直接静态托管;本地打开 {{out}}/index.html 需经 http 服务,file:// 下工件 fetch 不可用)\n",
97
96
  "cli.view.incompatible": "{{dir}}: 由 niceeval {{producer}} 写入(schemaVersion {{schemaVersion}}),当前 CLI 只读 schemaVersion {{supported}}。\n运行 `{{command}}` 查看这份报告。\n",
97
+ "cli.view.noResults": "{{root}} 下没有结果。先 `niceeval exp` 跑一轮,再 `niceeval view`。\n",
98
98
  "cli.view.incompatibleForeign": "{{dir}}: 由 {{name}} {{version}} 写入(schemaVersion {{schemaVersion}}),当前 CLI 只读 schemaVersion {{supported}}。\n请用写出它的那个工具查看这份报告。\n",
99
99
  "cli.view.url": "niceeval view: {{url}}\n",
100
100
  "context.capabilityMissing":
@@ -183,6 +183,8 @@ export const zhCN = {
183
183
  "runner.startAgentSetup": "agent setup(装 CLI / 写配置)…",
184
184
  "runner.startAgentTracing": "agent tracing(写 otel 导出配置)…",
185
185
  "runner.startSandbox": "起沙箱…",
186
+ "runner.startSandboxSetup": "sandbox setup(环境预置钩子)…",
187
+ "runner.startSandboxTeardown": "sandbox teardown(环境预置钩子)…",
186
188
  "runner.timeout": "attempt 超时({{timeoutMs}}ms)\n最近进度:\n{{recentLogs}}",
187
189
  "runner.traceSelected": " → 留 {{count}}(按语义)",
188
190
  "runner.resumeCarry": " · 复用上次 {{carried}} 个通过的结果,重跑 {{retry}} 个 eval\n",
@@ -18,15 +18,15 @@ import type {
18
18
  Metric,
19
19
  MetricCell,
20
20
  MetricColumn,
21
- ParamRef,
21
+ FlagRef,
22
22
  } from "./types.ts";
23
23
  import { formatMetricValue } from "./format.ts";
24
24
 
25
25
  // 复合键分隔符:NUL 不会出现在 eval id / experimentId / ISO 时间里,拼接键不会串味
26
26
  const KEY_SEP = "\u0000";
27
27
 
28
- /** param 未声明时的组名:不猜,如实归一组。 */
29
- export const PARAM_UNSET = "(unset)";
28
+ /** flag 未声明时的组名:不猜,如实归一组。 */
29
+ export const FLAG_UNSET = "(unset)";
30
30
 
31
31
  /** 计算函数的第一参:选集(warnings 随行)或手工挑的快照数组(没有挑选过程,自然无警告)。 */
32
32
  export type SnapshotsInput = Selection | Snapshot[];
@@ -105,33 +105,33 @@ export function evalGroupOf(id: string): string {
105
105
  return slash === -1 ? id : id.slice(0, slash);
106
106
  }
107
107
 
108
- function isParamRef(dimension: DimensionInput): dimension is ParamRef {
109
- return typeof dimension === "object" && "kind" in dimension && dimension.kind === "param";
108
+ function isFlagRef(dimension: DimensionInput): dimension is FlagRef {
109
+ return typeof dimension === "object" && "kind" in dimension && dimension.kind === "flag";
110
110
  }
111
111
 
112
- /** experiment 声明的 params(经 runner 原样透传进持久化字段 ExperimentRunInfo.params)。 */
113
- function paramsOf(attempt: AttemptHandle): Record<string, unknown> | undefined {
114
- return attempt.result.experiment?.params;
112
+ /** experiment 声明的 flags(经 runner 原样透传进持久化字段 ExperimentRunInfo.flags)。 */
113
+ function flagsOf(attempt: AttemptHandle): Record<string, unknown> | undefined {
114
+ return attempt.result.experiment?.flags;
115
115
  }
116
116
 
117
- /** param 声明值 → 组标签:label 函数优先,其余 String();未声明 → PARAM_UNSET。 */
118
- export function paramGroupKey(ref: ParamRef, item: Item): string {
119
- const value = paramsOf(item.attempt)?.[ref.name];
120
- if (value === undefined) return PARAM_UNSET;
117
+ /** flag 声明值 → 组标签:label 函数优先,其余 String();未声明 → FLAG_UNSET。 */
118
+ export function flagGroupKey(ref: FlagRef, item: Item): string {
119
+ const value = flagsOf(item.attempt)?.[ref.name];
120
+ if (value === undefined) return FLAG_UNSET;
121
121
  // 持久化字段是 Record<string, unknown>;声明侧的合法值就是这三种标量
122
122
  if (typeof ref.label === "function") return ref.label(value as string | number | boolean);
123
123
  return String(value);
124
124
  }
125
125
 
126
- /** param 作轴:要求数值;未声明或非数值 → null(点不画,注脚报数)。 */
127
- export function paramAxisValue(ref: ParamRef, item: Item): number | null {
128
- const value = paramsOf(item.attempt)?.[ref.name];
126
+ /** flag 作轴:要求数值;未声明或非数值 → null(点不画,注脚报数)。 */
127
+ export function flagAxisValue(ref: FlagRef, item: Item): number | null {
128
+ const value = flagsOf(item.attempt)?.[ref.name];
129
129
  return typeof value === "number" ? value : null;
130
130
  }
131
131
 
132
132
  export function dimensionKey(dimension: DimensionInput, item: Item): string {
133
133
  if (typeof dimension !== "string") {
134
- if (isParamRef(dimension)) return paramGroupKey(dimension, item);
134
+ if (isFlagRef(dimension)) return flagGroupKey(dimension, item);
135
135
  return dimension.of(item.attempt);
136
136
  }
137
137
  const result = item.attempt.result;
@@ -159,7 +159,7 @@ export const MetricScatter = Object.assign(
159
159
  );
160
160
  MetricScatter.displayName = "MetricScatter";
161
161
 
162
- /** 趋势线:x 是 experiment 声明的 param(param()),同系列按 x 排序连线。 */
162
+ /** 趋势线:x 是 experiment 声明的 flag(flag()),同系列按 x 排序连线。 */
163
163
  export const MetricLine = Object.assign(
164
164
  defineComponent<MetricLineProps>({
165
165
  web: (props) => <MetricLineWeb {...props} />,
@@ -19,7 +19,7 @@ import type {
19
19
  Metric,
20
20
  MetricCell,
21
21
  OverviewData,
22
- ParamRef,
22
+ FlagRef,
23
23
  ScatterData,
24
24
  ScoreboardData,
25
25
  TableData,
@@ -39,7 +39,7 @@ import {
39
39
  experimentIdOf,
40
40
  filterItems,
41
41
  groupItems,
42
- paramAxisValue,
42
+ flagAxisValue,
43
43
  resolveInput,
44
44
  snapshotKeyOf,
45
45
  toColumn,
@@ -52,7 +52,7 @@ import { formatMetricValue, formatPlainNumber } from "./format.ts";
52
52
  // ───────────────────────── MetricTable.data ─────────────────────────
53
53
 
54
54
  export interface TableDataOptions<M extends readonly Metric[]> {
55
- /** 行维度(内置 / 自定义 / param())。 */
55
+ /** 行维度(内置 / 自定义 / flag())。 */
56
56
  rows: DimensionInput;
57
57
  /** 每列一个指标;列键 = metric.name 的字面量,拼错编译不过。 */
58
58
  columns: M;
@@ -289,10 +289,10 @@ export async function scatterData(input: SnapshotsInput, opts: ScatterDataOption
289
289
  // ───────────────────────── MetricLine.data ─────────────────────────
290
290
 
291
291
  export interface LineDataOptions {
292
- /** x 轴:experiment 声明的 param(数值),不解析 experiment 命名。 */
293
- x: ParamRef;
292
+ /** x 轴:experiment 声明的 flag(数值),不解析 experiment 命名。 */
293
+ x: FlagRef;
294
294
  y: Metric;
295
- /** 可选:每个系列一条线(param 或普通维度);省略 = 单系列。 */
295
+ /** 可选:每个系列一条线(flag 或普通维度);省略 = 单系列。 */
296
296
  series?: DimensionInput;
297
297
  }
298
298
 
@@ -303,7 +303,7 @@ export async function lineData(input: SnapshotsInput, opts: LineDataOptions): Pr
303
303
  const groups = groupItems(items, "experiment");
304
304
  const rows: LineData["rows"] = [];
305
305
  for (const [key, group] of groups) {
306
- const x = paramAxisValue(opts.x, group[0]); // param 是 experiment 级声明,组内一致
306
+ const x = flagAxisValue(opts.x, group[0]); // flag 是 experiment 级声明,组内一致
307
307
  rows.push({
308
308
  key,
309
309
  series: opts.series ? dimensionKey(opts.series, group[0]) : undefined,
@@ -228,7 +228,7 @@ describe("MetricLine 双面", () => {
228
228
  `);
229
229
  });
230
230
 
231
- it("两面同口径:未声明 param 的点两面都不画、注脚同数", () => {
231
+ it("两面同口径:未声明 flag 的点两面都不画、注脚同数", () => {
232
232
  expect(html).toContain("1 point missing data");
233
233
  expect(term).toContain("1 point missing data");
234
234
  expect(html).not.toContain('data-key="ultra/legacy"'); // 注脚 title 如实列缺数据的点,但不画
@@ -1,18 +1,18 @@
1
- // param():把 experiment 声明的 params 当维度或轴(docs/reports.md「params 与新摆法」)。
1
+ // flag():把 experiment 声明的 flags 当维度或轴(docs/reports.md「flags 与新摆法」)。
2
2
  // 变量来自配置,不来自命名 —— 报告不解析 experiment id 字符串抠变量。
3
3
 
4
- import type { ParamRef } from "./types.ts";
4
+ import type { FlagRef } from "./types.ts";
5
5
 
6
- export function param(
6
+ export function flag(
7
7
  name: string,
8
8
  opts?: {
9
9
  /** 组标签 / 轴标签;函数形态把声明值折成组名(如 `(v) => \`${v} agents\``)。 */
10
10
  label?: string | ((value: string | number | boolean) => string);
11
11
  unit?: string;
12
12
  },
13
- ): ParamRef {
13
+ ): FlagRef {
14
14
  if (typeof name !== "string" || name.length === 0) {
15
- throw new Error("param: name must be a non-empty string (the key declared in the experiment's params).");
15
+ throw new Error("flag: name must be a non-empty string (the key declared in the experiment's flags).");
16
16
  }
17
- return { kind: "param", name, label: opts?.label, unit: opts?.unit };
17
+ return { kind: "flag", name, label: opts?.label, unit: opts?.unit };
18
18
  }
@@ -8,9 +8,9 @@
8
8
  // ./web.ts,只有那一侧 import react-dom。写报告文件的项目要装 react(.tsx 编译产物
9
9
  // import react/jsx-runtime)。
10
10
 
11
- // 指标与 param
11
+ // 指标与 flag
12
12
  export { defineMetric, passRate, examScore, durationMs, tokens, costUSD } from "./metrics.ts";
13
- export { param } from "./param.ts";
13
+ export { flag } from "./flag.ts";
14
14
 
15
15
  // 报告基座与双面组件基座
16
16
  export { defineReport, isReportDefinition, renderReportToText } from "./report.ts";
@@ -84,7 +84,7 @@ export type {
84
84
  MetricCell,
85
85
  MetricColumn,
86
86
  OverviewData,
87
- ParamRef,
87
+ FlagRef,
88
88
  ScatterData,
89
89
  ScoreboardData,
90
90
  SelectionWarning,