niceeval 0.8.1 → 0.9.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 (132) hide show
  1. package/INDEX.md +77 -45
  2. package/dist/agents/types.d.ts +28 -6
  3. package/dist/i18n/en.d.ts +2 -0
  4. package/dist/i18n/zh-CN.d.ts +2 -0
  5. package/dist/report/built-in/index.d.ts +3 -2
  6. package/dist/report/built-in/index.js +7 -8
  7. package/dist/report/built-in/standard.d.ts +1 -0
  8. package/dist/report/built-in/standard.js +30 -0
  9. package/dist/report/components.d.ts +69 -2
  10. package/dist/report/components.js +152 -3
  11. package/dist/report/compute.d.ts +28 -1
  12. package/dist/report/compute.js +123 -0
  13. package/dist/report/index.d.ts +4 -4
  14. package/dist/report/index.js +3 -2
  15. package/dist/report/locale.d.ts +39 -1
  16. package/dist/report/locale.js +69 -0
  17. package/dist/report/react/AttemptList.d.ts +3 -1
  18. package/dist/report/react/AttemptList.js +3 -3
  19. package/dist/report/react/CopyFixPrompt.d.ts +12 -0
  20. package/dist/report/react/CopyFixPrompt.js +12 -0
  21. package/dist/report/react/HeroCard.d.ts +13 -0
  22. package/dist/report/react/HeroCard.js +35 -0
  23. package/dist/report/react/PoweredBy.d.ts +5 -0
  24. package/dist/report/react/PoweredBy.js +7 -0
  25. package/dist/report/react/ScopeWarnings.d.ts +12 -0
  26. package/dist/report/react/ScopeWarnings.js +18 -0
  27. package/dist/report/react/TraceWaterfall.d.ts +14 -0
  28. package/dist/report/react/TraceWaterfall.js +22 -0
  29. package/dist/report/react/index.d.ts +6 -1
  30. package/dist/report/react/index.js +6 -0
  31. package/dist/report/report.d.ts +22 -6
  32. package/dist/report/report.js +66 -53
  33. package/dist/report/scope-warnings.d.ts +28 -0
  34. package/dist/report/scope-warnings.js +101 -0
  35. package/dist/report/text/faces.d.ts +19 -1
  36. package/dist/report/text/faces.js +61 -0
  37. package/dist/report/tree.js +6 -1
  38. package/dist/report/types.d.ts +45 -0
  39. package/dist/report/web.d.ts +5 -4
  40. package/dist/report/web.js +7 -22
  41. package/dist/results/select.d.ts +15 -2
  42. package/dist/results/select.js +75 -10
  43. package/dist/results/types.d.ts +26 -0
  44. package/dist/runner/fingerprint.d.ts +3 -3
  45. package/dist/runner/sandbox-selection.d.ts +12 -0
  46. package/dist/runner/types.d.ts +18 -6
  47. package/dist/sandbox/types.d.ts +12 -0
  48. package/docs-site/zh/explanation/evals.mdx +2 -1
  49. package/docs-site/zh/explanation/experiment.mdx +2 -0
  50. package/docs-site/zh/how-to/custom-reports.mdx +22 -6
  51. package/docs-site/zh/how-to/publish-report.mdx +5 -4
  52. package/docs-site/zh/how-to/viewing-results.mdx +3 -3
  53. package/docs-site/zh/how-to/write-experiment.mdx +40 -1
  54. package/docs-site/zh/reference/builtin-agents.mdx +40 -3
  55. package/docs-site/zh/reference/cli.mdx +1 -1
  56. package/docs-site/zh/reference/define-eval.mdx +8 -0
  57. package/docs-site/zh/reference/official-adapters.mdx +10 -5
  58. package/package.json +2 -1
  59. package/src/agents/bub.ts +13 -1
  60. package/src/agents/claude-code.test.ts +43 -1
  61. package/src/agents/claude-code.ts +32 -14
  62. package/src/agents/codex.test.ts +168 -1
  63. package/src/agents/codex.ts +51 -15
  64. package/src/agents/mcp.ts +31 -0
  65. package/src/agents/post-setup.ts +33 -0
  66. package/src/agents/types.ts +28 -7
  67. package/src/cli.ts +8 -6
  68. package/src/define.ts +3 -0
  69. package/src/i18n/en.ts +5 -2
  70. package/src/i18n/zh-CN.ts +5 -1
  71. package/src/index.ts +1 -0
  72. package/src/report/built-in/index.tsx +8 -7
  73. package/src/report/built-in/standard.tsx +59 -0
  74. package/src/report/components.tsx +218 -2
  75. package/src/report/compute.ts +138 -1
  76. package/src/report/dual-render.test.tsx +139 -12
  77. package/src/report/index.ts +20 -0
  78. package/src/report/locale.ts +83 -1
  79. package/src/report/react/AttemptList.tsx +13 -1
  80. package/src/report/react/CopyFixPrompt.tsx +37 -0
  81. package/src/report/react/HeroCard.tsx +59 -0
  82. package/src/report/react/PoweredBy.tsx +20 -0
  83. package/src/report/react/ScopeWarnings.tsx +74 -0
  84. package/src/report/react/TraceWaterfall.tsx +78 -0
  85. package/src/report/react/enhance.js +14 -0
  86. package/src/report/react/index.tsx +11 -0
  87. package/src/report/react/styles.css +187 -7
  88. package/src/report/report.ts +97 -62
  89. package/src/report/scope-warnings.ts +155 -0
  90. package/src/report/site-components.test.tsx +526 -0
  91. package/src/report/text/faces.ts +66 -0
  92. package/src/report/tree.ts +8 -1
  93. package/src/report/types.ts +51 -0
  94. package/src/report/web.ts +7 -40
  95. package/src/results/host-equivalence.test.ts +5 -1
  96. package/src/results/open.ts +5 -4
  97. package/src/results/results.test.ts +78 -1
  98. package/src/results/select.ts +78 -10
  99. package/src/results/types.ts +27 -0
  100. package/src/runner/attempt.ts +10 -9
  101. package/src/runner/discover.test.ts +9 -1
  102. package/src/runner/discover.ts +3 -3
  103. package/src/runner/fingerprint.ts +9 -4
  104. package/src/runner/ledger.test.ts +30 -1
  105. package/src/runner/ledger.ts +26 -4
  106. package/src/runner/run.ts +5 -1
  107. package/src/runner/sandbox-selection.test.ts +131 -0
  108. package/src/runner/sandbox-selection.ts +110 -0
  109. package/src/runner/types.ts +19 -2
  110. package/src/sandbox/types.ts +6 -0
  111. package/src/show/index.ts +17 -10
  112. package/src/show/render.ts +11 -11
  113. package/src/show/report-host.test.ts +32 -15
  114. package/src/show/report-host.ts +5 -4
  115. package/src/show/show.test.ts +140 -3
  116. package/src/view/app/App.test.tsx +78 -17
  117. package/src/view/app/App.tsx +17 -78
  118. package/src/view/app/components/CopyControls.tsx +4 -42
  119. package/src/view/app/i18n.ts +5 -227
  120. package/src/view/app/lib/rows.ts +3 -21
  121. package/src/view/app/shared.ts +1 -3
  122. package/src/view/app/types.ts +2 -2
  123. package/src/view/client-dist/app.css +1 -1
  124. package/src/view/client-dist/app.js +20 -20
  125. package/src/view/data.ts +2 -2
  126. package/src/view/shared/types.ts +11 -6
  127. package/src/view/styles.css +6 -266
  128. package/src/view/view-report.test.ts +59 -22
  129. package/src/view/app/components/LazyArtifact.tsx +0 -51
  130. package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
  131. package/src/view/app/pages/AttemptsPage.tsx +0 -80
  132. package/src/view/app/pages/TracesPage.tsx +0 -35
@@ -4,9 +4,14 @@
4
4
  // 选择器必须诚实:残缺、落后、未收尾都被算出来,以结构化 warnings 随 Scope 走 ——
5
5
  // 渲染与否在消费方(message 是渲染好的英文句子,以下一步收尾),但缺口不静默。
6
6
  import { evalPrefixPredicate } from "../shared/aggregate.js";
7
- /** Results.latest() 的实现:每个实验取最新一次快照(= exp.snapshots[0]),生成挑选警告。 */
8
- export function selectLatest(experiments, opts) {
9
- const selected = filterExperiments(experiments, opts?.experiments);
7
+ /**
8
+ * Results.latest() 的实现:每个实验取最新一次快照(= exp.snapshots[0]),生成挑选警告。
9
+ * 收整个 `Results` 而不是裸 `Experiment[]`,是为了同时取 `skipped` / `root` 生成
10
+ * `unreadable-snapshot` 警告(非实验作用域,不受 `opts.experiments` 过滤 —— 那些落盘
11
+ * 本来就没能解析出 experimentId,没有前缀可过滤)。
12
+ */
13
+ export function selectLatest(results, opts) {
14
+ const selected = filterExperiments(results.experiments, opts?.experiments);
10
15
  const snapshots = selected.map((exp) => exp.latest);
11
16
  const warnings = [];
12
17
  // stale 的基准:Scope 中最新的落盘(无阈值,如实触发;要阈值消费方按字段自比)。
@@ -52,6 +57,7 @@ export function selectLatest(experiments, opts) {
52
57
  });
53
58
  }
54
59
  }
60
+ warnings.push(...unreadableSnapshotWarnings(results.skipped, results.root));
55
61
  return makeScope("latest-snapshots", snapshots, warnings);
56
62
  }
57
63
  /** 一个快照的可比性配置投影;pairsByFlag 与 experimentListData 复用同一字段集。 */
@@ -188,8 +194,61 @@ export function selectCurrentResults(results, scope = {}) {
188
194
  });
189
195
  }
190
196
  }
197
+ warnings.push(...unreadableSnapshotWarnings(results.skipped, results.root));
191
198
  return makeScope("current-evals", snapshots, warnings);
192
199
  }
200
+ /**
201
+ * `results.skipped` 里每一条不可读落盘 → 一条 `unreadable-snapshot` ScopeWarning。
202
+ * 非实验作用域(没有 experimentId 字段):`latest()` / `current()` 都原样带上全部
203
+ * `skipped` 条目,不受 `opts.experiments` 前缀过滤影响(那些落盘本来就没能解析出
204
+ * experimentId,没有前缀可比);`makeScope().filter()` 按「非实验作用域的警告保留」
205
+ * 规则自动放行,不需要额外分支。
206
+ */
207
+ function unreadableSnapshotWarnings(skipped, root) {
208
+ return skipped.map((s) => {
209
+ switch (s.reason) {
210
+ case "incompatible-version": {
211
+ const producer = s.producer;
212
+ const schemaText = s.schemaVersion !== undefined ? ` (schemaVersion ${s.schemaVersion})` : "";
213
+ if (producer?.name === "niceeval" && producer.version) {
214
+ const command = `npx niceeval@${producer.version} show --results ${root}`;
215
+ return {
216
+ kind: "unreadable-snapshot",
217
+ dir: s.dir,
218
+ reason: s.reason,
219
+ message: `snapshot at "${s.dir}" was written by niceeval ${producer.version}${schemaText} and cannot be read by this version; run \`${command}\` to open it`,
220
+ command,
221
+ };
222
+ }
223
+ const writtenBy = producer?.name
224
+ ? `${producer.name}${producer.version ? ` ${producer.version}` : ""}`
225
+ : "an incompatible tool version";
226
+ return {
227
+ kind: "unreadable-snapshot",
228
+ dir: s.dir,
229
+ reason: s.reason,
230
+ message: `snapshot at "${s.dir}" was written by ${writtenBy}${schemaText} and cannot be read by this version; open it with the tool version that produced it`,
231
+ };
232
+ }
233
+ case "malformed": {
234
+ const detail = s.detail ? ` (${s.detail})` : "";
235
+ return {
236
+ kind: "unreadable-snapshot",
237
+ dir: s.dir,
238
+ reason: s.reason,
239
+ message: `snapshot at "${s.dir}" is malformed${detail} and was skipped; inspect snapshot.json in that directory for corrupted JSON or a missing required field`,
240
+ };
241
+ }
242
+ case "incomplete":
243
+ return {
244
+ kind: "unreadable-snapshot",
245
+ dir: s.dir,
246
+ reason: s.reason,
247
+ message: `snapshot at "${s.dir}" has attempt data but no snapshot.json (likely interrupted before metadata was written) and was skipped; inspect ${s.dir} — completed attempts remain on disk for manual review`,
248
+ };
249
+ }
250
+ });
251
+ }
193
252
  /**
194
253
  * Scope 构造:attempts 按口径物化(快照 attempts 的平铺);filter 只删不换 —— 快照删减,
195
254
  * attempts 随之同步修剪,warnings 修剪规则是「experimentId 不在幸存快照中的丢弃,
@@ -261,18 +320,24 @@ export function filterExperiments(experiments, filter) {
261
320
  const prefixes = (Array.isArray(filter) ? filter : [filter]).map((p) => p.replace(/\/+$/, ""));
262
321
  return experiments.filter((exp) => prefixes.some((p) => exp.id === p || exp.id.startsWith(p + "/")));
263
322
  }
264
- /** stale 警告的人话时距:选粒度最大的单位,四舍五入。 */
265
- function humanizeGap(fromIso, toIso) {
323
+ /**
324
+ * stale 警告的人话时距:选粒度最大的单位,四舍五入。结构化形态是单源——message 的英文时距
325
+ * 与 ScopeWarnings 徽标的本地化时距都从这里出,阈值不写两份。
326
+ */
327
+ export function gapParts(fromIso, toIso) {
266
328
  const ms = Math.max(0, Date.parse(toIso) - Date.parse(fromIso));
267
329
  const seconds = Math.round(ms / 1000);
268
330
  if (seconds < 90)
269
- return `${seconds} second${seconds === 1 ? "" : "s"}`;
331
+ return { n: seconds, unit: "second" };
270
332
  const minutes = Math.round(seconds / 60);
271
333
  if (minutes < 90)
272
- return `${minutes} minute${minutes === 1 ? "" : "s"}`;
334
+ return { n: minutes, unit: "minute" };
273
335
  const hours = Math.round(minutes / 60);
274
336
  if (hours < 36)
275
- return `${hours} hour${hours === 1 ? "" : "s"}`;
276
- const days = Math.round(hours / 24);
277
- return `${days} day${days === 1 ? "" : "s"}`;
337
+ return { n: hours, unit: "hour" };
338
+ return { n: Math.round(hours / 24), unit: "day" };
339
+ }
340
+ function humanizeGap(fromIso, toIso) {
341
+ const { n, unit } = gapParts(fromIso, toIso);
342
+ return `${n} ${unit}${n === 1 ? "" : "s"}`;
278
343
  }
@@ -142,6 +142,12 @@ export interface SkippedDir {
142
142
  }
143
143
  /** openResults 的返回:experiments 分层;skipped 不静默丢。 */
144
144
  export interface Results {
145
+ /**
146
+ * 结果根目录的绝对路径(`openResults()` 入参解析后的原样值,不论传入的是结果根、
147
+ * 实验目录、快照目录还是某个 snapshot.json)。`unreadable-snapshot` 警告拼版本化
148
+ * `command`(`npx niceeval@<version> show --results <root>`)时取它。
149
+ */
150
+ root: string;
145
151
  /** 每个实验一项,挂着自己的全部历史(id 字典序)。 */
146
152
  experiments: Experiment[];
147
153
  skipped: SkippedDir[];
@@ -218,6 +224,26 @@ export type ScopeWarning = {
218
224
  message: string;
219
225
  /** 一条可复制即跑的推进命令:`niceeval exp <experimentId>`。 */
220
226
  command: string;
227
+ } | {
228
+ /**
229
+ * 扫描结果根遇到的不可读快照:schema 不兼容、JSON 损坏 / 必需字段错误(malformed)、
230
+ * attempt 已写入但缺 `snapshot.json`(incomplete)。该快照被跳过,不挡其余结果
231
+ * (非 niceeval JSON 静默忽略,不产生这个 kind)。非实验作用域(没有 experimentId
232
+ * 字段) —— `Scope.filter()` 修剪时恒保留。
233
+ */
234
+ kind: "unreadable-snapshot";
235
+ /** 该快照目录的绝对路径。 */
236
+ dir: string;
237
+ /** 与 `SkippedDir.reason` 同一取值集,原样透传。 */
238
+ reason: "incompatible-version" | "malformed" | "incomplete";
239
+ message: string;
240
+ /**
241
+ * 只有 reason 为 `incompatible-version` 且能确定是 niceeval 自己产出(`producer.name
242
+ * === "niceeval"` 且带 `producer.version`)时给出:`npx niceeval@<version> show --results
243
+ * <root>`。第三方 producer、版本信息缺失,或 reason 为 malformed / incomplete 时省略——
244
+ * 这些情况没有单条命令能解决,message 改给定位动作。
245
+ */
246
+ command?: string;
221
247
  };
222
248
  /** dedupeAttempts 的警告:身份键缺 startedAt,宁可不去重也不误删。 */
223
249
  export interface DedupeWarning {
@@ -1,11 +1,11 @@
1
- import type { DiscoveredEval, EvalResult } from "../types.ts";
1
+ import type { DiscoveredEval, EvalResult, SandboxOption } from "../types.ts";
2
2
  import type { AgentRun } from "./types.ts";
3
3
  export declare function cacheKey(run: AgentRun, evalId: string): string;
4
4
  /**
5
5
  * @param sourceCache 按 sourcePath 缓存文件内容:一个矩阵(实验 × eval)会对同一批源文件
6
6
  * 反复算指纹,不带缓存会在任何 attempt 起跑前做 E×N 次重复文件读。
7
7
  */
8
- export declare function computeFingerprint(evalDef: DiscoveredEval, run: AgentRun, sourceCache?: Map<string, Promise<string>>): Promise<string>;
8
+ export declare function computeFingerprint(evalDef: DiscoveredEval, run: AgentRun, sourceCache?: Map<string, Promise<string>>, configSandbox?: SandboxOption): Promise<string>;
9
9
  export interface CarryPlan {
10
10
  /** `cacheKey(run, evalId)` → 本次规划出的指纹,供调用方按同一口径判断"这条要不要携入"。 */
11
11
  plannedFingerprints: Map<string, string>;
@@ -20,4 +20,4 @@ export interface CarryPlan {
20
20
  * 不一致,live 表格就会显示"还在等名额",而 run.ts 其实已经把它筛掉、根本不会调度这个 attempt
21
21
  * (见 memory 的 live-carry-row-shows-waiting-forever)。
22
22
  */
23
- export declare function planCarry(evals: DiscoveredEval[], agentRuns: AgentRun[], priorResults: EvalResult[] | undefined): Promise<CarryPlan>;
23
+ export declare function planCarry(evals: DiscoveredEval[], agentRuns: AgentRun[], priorResults: EvalResult[] | undefined, configSandbox?: SandboxOption): Promise<CarryPlan>;
@@ -0,0 +1,12 @@
1
+ import type { DiscoveredEval, SandboxOption, SandboxRunInfo } from "../types.ts";
2
+ import type { AgentRun } from "./types.ts";
3
+ /** 该 eval 实际起步的 spec:未声明 environment 用基础 spec;声明了则查表派生并缓存。 */
4
+ export declare function sandboxForEval(run: AgentRun, evalDef: DiscoveredEval, fallback?: SandboxOption): SandboxOption | undefined;
5
+ /** 在 dry-run / carry / concurrency / attempt 展开之前一次性查表;全部缺项一次穷举,不等到花费发生后才出现。 */
6
+ export declare function prepareRunSandboxes(evals: DiscoveredEval[], runs: AgentRun[], fallback?: SandboxOption): void;
7
+ /** ExperimentRunInfo 的 sandbox 投影:顶层恒为基础 spec;sandboxByEval 只含声明了 environment 的选中 eval。 */
8
+ export declare function sandboxProjection(run: AgentRun, fallback?: SandboxOption): {
9
+ sandbox?: SandboxRunInfo;
10
+ sandboxByEval?: Record<string, SandboxRunInfo>;
11
+ };
12
+ export declare function resolvedSandboxRecommendedConcurrency(evals: DiscoveredEval[], runs: AgentRun[], fallback?: SandboxOption): number;
@@ -25,11 +25,14 @@ export interface ExperimentRunInfo {
25
25
  /** evals 过滤器的指纹(数组内容 / 函数体哈希),供「配置没变」判断;与 selectedEvalIds 一起取代原过滤器。 */
26
26
  evalFilterFingerprint?: string;
27
27
  /** provider 名、provider 的公开参数投影与配置 fingerprint;参数只经投影落盘,token/凭据永不进来。 */
28
- sandbox?: {
29
- provider: string;
30
- params?: Record<string, JsonValue>;
31
- fingerprint?: string;
32
- };
28
+ sandbox?: SandboxRunInfo;
29
+ /** spec 携带 environments 表时:声明了 environment 的选中 eval 各自解析到的产物投影,按 eval id 留审计映射;其余 eval 以 `sandbox` 为准。 */
30
+ sandboxByEval?: Record<string, SandboxRunInfo>;
31
+ }
32
+ export interface SandboxRunInfo {
33
+ provider: string;
34
+ params?: Record<string, JsonValue>;
35
+ fingerprint?: string;
33
36
  }
34
37
  /**
35
38
  * 一次 attempt 的生命周期词表——**全仓唯一一套**(见 docs/feature/results/architecture.md
@@ -309,6 +312,8 @@ export interface EvalDef {
309
312
  description?: string;
310
313
  /** 标签,供 CLI `--tag` 过滤和 view 分类;与 id 前缀过滤是两套独立的筛选维度。 */
311
314
  tags?: string[];
315
+ /** 这条 eval 需要的环境 profile id(provider-neutral,如 `"python-3.9-astropy-4.2"`);由 sandbox spec 的 `environments` 表翻译成该 provider 的预制产物。 */
316
+ environment?: string;
312
317
  /** 覆盖项目级 Config.judge,只对这一个 eval 生效(如换个更贵的评审模型)。 */
313
318
  judge?: JudgeConfig;
314
319
  /** 覆盖 / 追加项目级 Config.reporters,只对这一个 eval 生效。 */
@@ -377,7 +382,10 @@ export interface ExperimentDef {
377
382
  evals?: "*" | string[] | ((id: string) => boolean);
378
383
  /** 覆盖项目级 / CLI 的单次 attempt 超时(毫秒),只对这个实验生效。 */
379
384
  timeoutMs?: number;
380
- /** 覆盖项目级 Config.sandbox,只对这个实验生效。 */
385
+ /**
386
+ * 覆盖项目级 Config.sandbox,只对这个实验生效。固定 SandboxSpec 对全部选中 eval 复用;
387
+ * spec 可携带 `environments` 表,按 eval 的 `environment` profile 换预制产物。
388
+ */
381
389
  sandbox?: SandboxOption;
382
390
  /**
383
391
  * 本实验的花费上限(USD)。调度器按「已完成 attempt 的实测花费」累计,到顶后跳过这个实验
@@ -474,6 +482,8 @@ export interface AgentRun {
474
482
  runs: number;
475
483
  earlyExit: boolean;
476
484
  sandbox?: SandboxOption;
485
+ /** environments 查表的规划期缓存(只含声明了 environment 的 selected eval);每条只派生一次。 */
486
+ resolvedSandboxes?: Map<string, SandboxOption>;
477
487
  timeoutMs?: number;
478
488
  budget?: number;
479
489
  evalFilter: (id: string) => boolean;
@@ -534,6 +544,8 @@ export interface Attempt {
534
544
  /** agent+model+evalId,用于首过即停。 */
535
545
  key: string;
536
546
  fingerprint: string;
547
+ /** 规划期按 eval 的 environment 查表派生的具体 spec;attempt 生命周期不再重新查表。 */
548
+ sandboxSpec?: SandboxOption;
537
549
  /**
538
550
  * 构造 fresh attempt plan 时即算好的 Attempt 定位符(不是完成后写回):由 invocation 的
539
551
  * snapshotStartedAt 与 attempt 身份派生,贯穿执行、留存登记与落盘——登记项、run 收尾反馈与
@@ -109,18 +109,30 @@ export interface DockerSandboxSpec extends SandboxHooks<DockerSandboxSpec> {
109
109
  readonly provider: "docker";
110
110
  /** 覆盖默认镜像;默认按 runtime 选 `node:*-slim`。预制模板:传烘焙好 agent CLI 的镜像名。 */
111
111
  readonly image?: string;
112
+ /** 按 eval 的 `environment` profile 覆盖预制镜像:键为 profile id,值为该 profile 起步的镜像。未声明 environment 的 eval 用 `image`。 */
113
+ readonly environments?: Readonly<Record<string, {
114
+ readonly image: string;
115
+ }>>;
112
116
  readonly runtime?: SandboxRuntime;
113
117
  }
114
118
  export interface VercelSandboxSpec extends SandboxHooks<VercelSandboxSpec> {
115
119
  readonly provider: "vercel";
116
120
  /** 从已有快照起 microVM。预制模板:烘焙好 agent CLI 的 snapshotId。 */
117
121
  readonly snapshotId?: string;
122
+ /** 按 eval 的 `environment` profile 覆盖预制快照:键为 profile id,值为该 profile 起步的 snapshotId。未声明 environment 的 eval 用 `snapshotId`。 */
123
+ readonly environments?: Readonly<Record<string, {
124
+ readonly snapshotId: string;
125
+ }>>;
118
126
  readonly runtime?: SandboxRuntime;
119
127
  }
120
128
  export interface E2BSandboxSpec extends SandboxHooks<E2BSandboxSpec> {
121
129
  readonly provider: "e2b";
122
130
  /** e2b 模板名/ID。预制模板:烘焙好 agent CLI 的模板(如 `"niceeval-agents"`)。省略用 e2b 默认 `"base"`。 */
123
131
  readonly template?: string;
132
+ /** 按 eval 的 `environment` profile 覆盖预制模板:键为 profile id,值为该 profile 起步的模板。未声明 environment 的 eval 用 `template`。 */
133
+ readonly environments?: Readonly<Record<string, {
134
+ readonly template: string;
135
+ }>>;
124
136
  /** 仅作记录;e2b 的 node 版本由模板决定,不在创建时选。 */
125
137
  readonly runtime?: SandboxRuntime;
126
138
  }
@@ -28,6 +28,7 @@ export default defineEval({
28
28
  | 字段 | 说明 |
29
29
  |---|---|
30
30
  | `description` | 给人看的描述,出现在报告里 |
31
+ | `environment` | 可选的环境需求 profile,由 sandbox spec 的 `environments` 表映射到具体预制环境 |
31
32
  | `test(t)` | 交互和断言逻辑 |
32
33
 
33
34
  eval 本身不声明用哪个 Agent——它默认保持 agent-neutral,同一个 eval 可以在不同 experiment 下跑不同 Agent。选哪个 Agent 是 experiment 的字段,不由 CLI 临时覆盖。
@@ -45,7 +46,7 @@ npx niceeval exp local weather
45
46
  npx niceeval exp local weather/brooklyn
46
47
  ```
47
48
 
48
- 这种方式让 ID 稳定、可读,并自然跟目录结构保持一致。
49
+ 这种方式让 ID 稳定、可读,并自然跟目录结构保持一致。位置参数采用裸字符串前缀:`terminal-swe-bench` 也会命中 `terminal-swe-bench-astropy-1`,不要求后一个字符必须是 `/`。
49
50
 
50
51
  ## 生命周期
51
52
 
@@ -27,6 +27,8 @@ export default defineExperiment({
27
27
 
28
28
  Experiment 是纯配置数据,没有 `setup` / `teardown` 这类生命周期字段。要按实验准备环境(装二进制、预热、跨 attempt 存取状态),挂在 `sandbox` 字段的 spec 上——`dockerSandbox()` 等工厂返回的对象可以链 `.setup()` / `.teardown()`,见 [沙箱 provider · 环境钩子](/zh/how-to/sandbox-providers#环境钩子)。
29
29
 
30
+ 同一实验里的 eval 需要不同预制环境时,eval 只声明 provider-neutral 的 `environment` profile;sandbox spec 的 `environments` 表再把 profile 映射到 Docker image、E2B template 或 Vercel sandbox snapshot。这样任务需求留在 eval,provider 产物留在 spec,一个实验仍覆盖全部 eval、对比不拆分。写法见[写实验 · 让不同 eval 使用不同预制环境](/zh/how-to/write-experiment#让不同-eval-使用不同预制环境)。
31
+
30
32
  ## 矩阵对比
31
33
 
32
34
  要比较的每个变体写一个 experiment 文件:两个模型就是两个文件,只差 `model` 一行;prompt A/B 就是只差一个参数。同一批 eval 在多个 experiment 下各跑一遍,pass rate、成本、延迟就有了可比的横截面——`niceeval view` 里叠着看。适合比什么、结果怎么读,见[实验矩阵](/zh/how-to/experiments)。
@@ -8,12 +8,28 @@ description: "一份报告就是一个报告文件:官方宿主打开结果、
8
8
 
9
9
  一份报告就是一个报告文件。你不用打开结果目录、不用写渲染代码、不用起自己的应用:`niceeval show` / `niceeval view` 本体就是宿主——替你打开结果、把数据注入进来,你用官方组件和 `Row` / `Col` 摆版面,写完把文件路径递给 `--report`,终端和网页两扇门就都认它——官方的证据深链、`--results` 换根、静态导出,自定义报告全部原样享有。
10
10
 
11
- ## show / view 的默认报告也是一份报告定义
11
+ ## show / view 的默认报告也是一份报告文件
12
12
 
13
- `niceeval show` / `view` 不传 `--report` 时渲染的默认报告不是私有实现,而是 `niceeval/report` 公开导出的一个值:
13
+ `niceeval show` / `view` 不传 `--report` 时渲染的默认报告不是私有实现,而是包里自带的一个普通报告文件:三个页面(报告、Attempts、追踪),每页由 `niceeval/report` 公开导出的组件搭成——页首的标题区(`Hero`)、选择警告(`ScopeWarnings`),首页的比较组件 `ExperimentComparison`,Attempts 页的 Attempt 列表(`AttemptList`),追踪页的追踪瀑布(`TraceWaterfall`)。
14
+
15
+ 这份默认报告本身以 `standard` 为名从 `niceeval/report/built-in` 导出。只想在默认报告上加站点标题、GitHub 链接或统计脚本时,用 `extends` 在它上面叠自己的外壳,页面内容一行不用写;NiceEval 升级带来的页面改进也会自动跟过来:
16
+
17
+ ```tsx
18
+ // reports/branded.tsx —— 默认报告整站 + 自己的标题和链接
19
+ import { defineReport } from "niceeval/report";
20
+ import { standard } from "niceeval/report/built-in";
21
+
22
+ export default defineReport({
23
+ extends: standard,
24
+ title: "Memory Evals",
25
+ links: [{ label: "GitHub", href: "https://github.com/you/repo" }],
26
+ });
27
+ ```
28
+
29
+ `niceeval/report/built-in` 是内置报告的集合,每份一个名字;今天只有 `standard`,以后新增的内置报告也从这里按名字导入。想改页面内容本身,用同一批公开组件自己搭——你的报告文件能逐字写出同样的页面,也能只留自己要的部分:
14
30
 
15
31
  ```ts
16
- import { ExperimentComparison } from "niceeval/report";
32
+ import { ExperimentComparison, Hero, ScopeWarnings } from "niceeval/report";
17
33
  ```
18
34
 
19
35
  `ExperimentComparison` 先按 experiment id 的父目录切成可比组,再为每组分别计算成本 × 端到端成功率散点图和实验明细表。`compare/bub` 与 `compare/codex` 可以同图,`dev-e2b/bub` 必须在另一个组;顶层 experiment 各自成为单例组。网页持有全部组并一次聚焦一组;终端命中多组时只列索引和单组查看命令,命中单组时才展开详情。两面都不会生成跨组总榜。
@@ -63,13 +79,13 @@ niceeval view --report reports/exam.tsx # 网页:同一棵树走网页面
63
79
 
64
80
  报告文件里有两种数据形态。实体列表的 `.data(selection)` 返回普通数组:`ExperimentListItem[]`、`EvalListItem[]`、`AttemptListItem[]`。报告作者用 JavaScript `.filter()` / `.slice()` 决定展示哪些实体,再把数组作为 `items` 传给列表;组件不藏另一套过滤 DSL。指标图形和汇总组件收算好的 `data`;其中 `MetricScatter` 也提供 `selection` 简写,由宿主在渲染前计算。计算后的组件只渲染传入数据,不碰结果目录。
65
81
 
66
- 挑选提醒(覆盖不全、快照过期、有没跑完的运行)不用你在报告里专门摆一个组件来显示:不管你的报告摆了哪些组件,`niceeval show` 和 `niceeval view` 都会把 `selection.warnings` 显示在报告正文上方,不会因为你没摆某一块就静默丢掉。
82
+ 挑选提醒(覆盖不全、快照过期、有没跑完的运行、读不出来的快照)由选择警告(`ScopeWarnings`)组件显示:提醒按实验归组,组头列出实验名、问题标签和一条可复制的重跑命令,每条提醒的原文收在组内可展开的折叠块里(提醒少时默认展开),读不出来的快照单独归成一组。默认报告每一页都摆了它;自己的报告想显示提醒,就在页首摆一个 `<ScopeWarnings />`——不摆就不显示,数字是否附带完整性提醒由你对读者负责。
67
83
 
68
84
  命令行的范围先作用在挑选上,报告拿到的就是收窄到这个范围后的 `selection`:位置参数的 eval id 前缀收窄 Selection 覆盖的 eval(覆盖提醒的分母同样收窄到范围内),`--results` 把结果根换成指定目录,`--experiment` 让 Selection 只留该实验。`--history` 与 `--report` 互斥——趋势在报告里用 `exp.snapshots` 自己摆;证据切面(`--eval` / `--execution` / `--diff`)只看证据,不渲染报告。
69
85
 
70
86
  页面里的每个组件都是**双面**的:网页面是 React 渲染,终端面是字符渲染,两面吃同一份算好的数据。实体列表按 experiment → Eval → Attempt 展示事实;指标表、矩阵、条形图、成绩单、散点图、趋势图和差异表展示聚合值。完整清单见[报告组件](/zh/reference/report-components)。网页面的实体、格子和点深链到 Attempt 详情,终端面印出对应的 `niceeval show <eval id>` 下钻命令。
71
87
 
72
- 默认报告没有特权:它就是上面导出的 `ExperimentComparison`。你的报告和它同级;需要同样的“按目录分组、组内比较”摆法就直接引用它,不需要就不摆。自己直接组合 `MetricScatter` / `ExperimentList` 时,通用组件只消费你传入的数据,不会自动分组;把跨组 Selection 传进去就表示你明确要做跨组分析。
88
+ 默认报告没有特权:它的三个页面全部由公开组件搭成,你的报告和它同级。需要同样的“按目录分组、组内比较”摆法就直接写 `<ExperimentComparison />`,需要 Attempts 页就写带过滤的 `<AttemptList />`,不需要就不摆——包括页首标题区和 `Powered by NiceEval` 品牌行:它们分别是 `Hero` 和 `PoweredBy` 组件,用了就带、不用就没有,组件本身不提供开关。自己直接组合 `MetricScatter` / `ExperimentList` 时,通用组件只消费你传入的数据,不会自动分组;把跨组数据传进去就表示你明确要做跨组分析。
73
89
 
74
90
  ## 排版:Row 与 Col 在两个面都成立
75
91
 
@@ -395,4 +411,4 @@ niceeval view --report reports/exam.tsx --out site
395
411
 
396
412
  ## 界线:内置命令不长配置
397
413
 
398
- 一次只渲染一份报告,`--report` 收显式文件路径——没有 `reports/` 目录自动发现、没有插件注册表、没有配置文件。自定义指标和自定义组件都住在你的报告文件里,随文件一起递入,宿主不为它们长任何注册面。不传 `--report` 时渲染的就是内置的 `ExperimentComparison`,你的报告和它是同级实现。
414
+ 一次只渲染一份报告,`--report` 收显式文件路径——没有 `reports/` 目录自动发现、没有插件注册表、没有配置文件。自定义指标和自定义组件都住在你的报告文件里,随文件一起递入,宿主不为它们长任何注册面。不传 `--report` 时渲染的就是默认报告,你的报告和它是同级实现。
@@ -35,7 +35,7 @@ npx niceeval view --results report-data --out site
35
35
 
36
36
  ## 发布自定义报告
37
37
 
38
- 不传 `--report` 时,发布出来的首页是默认报告。想让首页换成自己的报告,把 [`defineReport` 报告文件](/zh/how-to/custom-reports)传给 `--report` 就行——attempt 证据页(transcript、trace、代码视图)仍在同一个站里,报告里的每个数字点进去就是对应证据,和本地 `view --report` 看到的一模一样:
38
+ 不传 `--report` 时,发布出来的站点就是默认报告的三个页面(报告、Attempts、追踪)。想换成自己的页面,把 [`defineReport` 报告文件](/zh/how-to/custom-reports)传给 `--report` 就行——attempt 详情(transcript、trace、代码视图)仍在同一个站里,报告里的每个数字点进去就是对应证据,和本地 `view --report` 看到的一模一样:
39
39
 
40
40
  ```bash
41
41
  npx niceeval view --results report-data --report reports/exam.tsx --out site
@@ -45,12 +45,14 @@ npx niceeval view --results report-data --report reports/exam.tsx --out site
45
45
 
46
46
  ## 接站点分析与第三方脚本
47
47
 
48
- 发布出去的站想挂 Google Analytics、埋点或评论组件,在报告文件的 `head` 字段里声明标签。厂商文档里的 snippet 逐字段照抄成对象就行——以 GA4 为例,官方给的两段 `<script>` 写成两个条目:
48
+ 发布出去的站想挂 Google Analytics、埋点或评论组件,在报告文件的 `head` 字段里声明标签。厂商文档里的 snippet 逐字段照抄成对象就行——以 GA4 为例,官方给的两段 `<script>` 写成两个条目。页面内容不用重写:默认报告以 `standard` 为名从 `niceeval/report/built-in` 导出,`extends` 它就是原样的默认站点加上你的标题和脚本:
49
49
 
50
50
  ```tsx
51
- import { ExperimentComparison, defineReport } from "niceeval/report";
51
+ import { defineReport } from "niceeval/report";
52
+ import { standard } from "niceeval/report/built-in";
52
53
 
53
54
  export default defineReport({
55
+ extends: standard,
54
56
  title: "Memory Evals",
55
57
  head: [
56
58
  { tag: "script", attrs: { async: true, src: "https://www.googletagmanager.com/gtag/js?id=G-XXXX" } },
@@ -64,7 +66,6 @@ export default defineReport({
64
66
  `,
65
67
  },
66
68
  ],
67
- content: <ExperimentComparison />,
68
69
  });
69
70
  ```
70
71
 
@@ -323,7 +323,7 @@ compare/codex-gpt-5.4 · 5 runs · passed 2/5
323
323
 
324
324
  两次 run 的精确对比(这次修复具体翻转了哪些 eval)不做成 flag:用 `DeltaTable` 积木写一份报告递给 `--report`,几行就是一份自定义对比报告,终端和网页两扇门都认——内置命令只管固定摆法,自定义口径见[自定义报告](/zh/how-to/custom-reports)。
325
325
 
326
- ## `niceeval view`:网页证据室
326
+ ## `niceeval view`:在网页看证据
327
327
 
328
328
  ```bash
329
329
  npx niceeval view
@@ -335,9 +335,9 @@ npx niceeval view
335
335
  失败后立刻运行 `npx niceeval view`,可以直接打开刚刚那次运行的 artifacts。
336
336
  </Tip>
337
337
 
338
- `view` 的首页是一份报告:不传 `--report` 时完整加载当前结果并显示全部可比组索引,选中一组后只显示该组的成本 × 端到端成功率散点图与实验表。切组只改变页面状态,不重新读取或计算;不同组不会混进同一张图或榜单。浏览器禁用 JS 时,每组作为独立的 `<details>` 完整可读;启用 JS 后一次聚焦一组。传了 `--report` 就换成你自己的报告(与 `show --report` 吃同一个文件),见[自定义报告](/zh/how-to/custom-reports)。证据部分(Attempt 弹窗、transcript、trace 瀑布、run 列表)始终保留。
338
+ `view` 的首页是一份报告:不传 `--report` 时完整加载当前结果并显示全部可比组索引,选中一组后只显示该组的成本 × 端到端成功率散点图与实验表。切组只改变页面状态,不重新读取或计算;不同组不会混进同一张图或榜单。浏览器禁用 JS 时,每组作为独立的 `<details>` 完整可读;启用 JS 后一次聚焦一组。传了 `--report` 就换成你自己的报告(与 `show --report` 吃同一个文件),页面与导航完全由报告文件决定,见[自定义报告](/zh/how-to/custom-reports)Attempt 详情(弹窗里的 transcript、时间树、tracediff)始终可用——它是查看器自身的能力,报告里的每个数字点进去都是对应证据;默认报告另有 Attempts 列表页和追踪瀑布页,自己的报告要同款页面就摆同名组件。
339
339
 
340
- 网页版多几样浏览操作:切换可比组、点表头就地排序、在当前组的过滤框里筛行、点开一个 experiment 行看它每道题的判定与原因、悬停散点看数值——这些只影响眼前的视图,不改判定口径,刷新即恢复。Attempt 弹窗里有与 `show --timing` 同源的统一时间树:Sandbox 启动、setup hook 及其 shell、agent 安装命令、每轮 send 与可关联的 OTel model/tool、评分与收尾都能逐层展开。报告上方有 **Copy fix prompt** 按钮,把全部失败打包成可直接粘给 coding agent 的修复 prompt(attempt 弹窗里有单条版)。报告文案有中英两份,随界面语言切换。
340
+ 网页版多几样浏览操作:切换可比组、点表头就地排序、在当前组的过滤框里筛行、点开一个 experiment 行看它每道题的判定与原因、悬停散点看数值——这些只影响眼前的视图,不改判定口径,刷新即恢复。Attempt 弹窗里有与 `show --timing` 同源的统一时间树:Sandbox 启动、setup hook 及其 shell、agent 安装命令、每轮 send 与可关联的 OTel model/tool、评分与收尾都能逐层展开。默认报告页里有 **Copy fix prompt** 按钮(`CopyFixPrompt` 组件,自己的报告可以直接摆同款),把全部失败打包成可直接粘给 coding agent 的修复 prompt(attempt 弹窗里有单条版)。报告文案有中英两份,随界面语言切换。
341
341
 
342
342
  ## 导出与静态托管
343
343
 
@@ -104,7 +104,7 @@ npx niceeval exp prompt-variants/concise
104
104
  | `timeoutMs` | 单个 attempt 的超时 |
105
105
  | `budget` | 这一格配置的预算上限 |
106
106
  | `maxConcurrency` | 这一格配置的并发上限 |
107
- | `sandbox` | sandbox agent 使用的 provider,如 `dockerSandbox()` / `e2bSandbox()`;spec 上可以链 `.setup()` / `.teardown()` 挂按实验变化的环境钩子 |
107
+ | `sandbox` | sandbox agent 使用的固定 spec;spec 可带 `environments` 表按 eval 的环境 profile 换预制产物,也可以链 `.setup()` / `.teardown()` 挂环境钩子 |
108
108
 
109
109
  Experiment 本身是纯配置数据,没有 `setup` / `teardown` 这类字段。要在跑 agent 前按实验准备环境(装二进制、预热、跨 attempt 载入和回存状态),挂在 `sandbox` 字段的 spec 上:
110
110
 
@@ -122,4 +122,43 @@ export default defineExperiment({
122
122
 
123
123
  钩子的执行时机、多钩子顺序和失败语义见 [沙箱 provider · 环境钩子](/zh/how-to/sandbox-providers#环境钩子)。
124
124
 
125
+ ## 让不同 eval 使用不同预制环境
126
+
127
+ 一批真实任务可能需要不同版本的运行时和依赖。eval 用 `environment` 声明一个与 provider 无关的 profile ID;sandbox spec 的 `environments` 表再把它映射到具体模板或快照:
128
+
129
+ ```ts
130
+ // evals/astropy-2021.eval.ts
131
+ export default defineEval({
132
+ environment: "python-3.9-astropy-4.2",
133
+ async test(t) {
134
+ // 驱动任务并验证结果
135
+ },
136
+ });
137
+ ```
138
+
139
+ ```ts
140
+ // experiments/shared.ts —— 一个 provider 一张表,所有实验共用
141
+ import { e2bSandbox } from "niceeval/sandbox";
142
+
143
+ export const e2b = e2bSandbox({
144
+ template: "codex-default", // 未声明 environment 的 eval 用它
145
+ environments: {
146
+ "python-3.9-astropy-4.2": { template: "codex-python39" },
147
+ },
148
+ });
149
+ ```
150
+
151
+ ```ts
152
+ // experiments/e2b.ts —— 实验保持一行 diff,覆盖全部 eval
153
+ import { defineExperiment } from "niceeval";
154
+ import { e2b } from "./shared";
155
+
156
+ export default defineExperiment({
157
+ agent: codexAgent(),
158
+ sandbox: e2b,
159
+ });
160
+ ```
161
+
162
+ `environment` 是非空的稳定字符串,不是包版本约束。`environments` 表的值就是该 provider 预制产物字段的覆盖(Docker 的 `image`、E2B 的 `template`、Vercel 的 `snapshotId`)。NiceEval 在启动任何 Sandbox 前对所有选中的 eval 完成查表;某条 eval 声明的 profile 缺表项会在启动时一次性报出全部缺项。remote Agent 不创建 Sandbox,不参与查表。因为映射是随 spec 复用的数据,同一个实验能覆盖全部 eval——分数和对比表不会因为环境不同被拆成多个实验。
163
+
125
164
  跨配置比较的设计建议见[实验矩阵](/zh/how-to/experiments)。adapter 如何消费 `ctx.model` 和 `ctx.flags` 见[Adapter](/zh/explanation/adapter)。
@@ -41,7 +41,7 @@ description: "NiceEval 内置的 claude-code、codex、bub 适配器分别做到
41
41
 
42
42
  ### codex
43
43
 
44
- - 连接方式:沙箱里跑 `codex exec --json`(续接时是 `codex exec resume <id> --json`),stdout JSONL 当 transcript
44
+ - 连接方式:沙箱里跑 `codex exec --json`(续接时是 `codex exec resume <id> --json`),stdout JSONL 当 transcript。命令带 `--dangerously-bypass-approvals-and-sandbox` 与 `--dangerously-bypass-hook-trust`:沙箱里没人能回答 codex 的交互确认,插件或 `postSetup` 装好的 hook 因此不需要交互授信就能生效。
45
45
  - 鉴权:`CODEX_API_KEY`(不是 `OPENAI_API_KEY`),可选 `CODEX_BASE_URL` 接 OpenAI 兼容代理;配置项见下方 `CodexConfig`。
46
46
  - `tracing` 通过 `~/.codex/config.toml` 的 `[otel.trace_exporter.otlp-http]` 段配置,协议 `http/json`。
47
47
 
@@ -93,7 +93,8 @@ mcpServers?: McpServer[];
93
93
  ```
94
94
 
95
95
  额外 MCP server(每个沙箱 setup 时写进用户级 ~/.claude.json)。
96
- 示例:&#123; name: "browser", command: "npx", args: ["-y", "@anthropic/mcp-browser"] &#125;
96
+ stdio 形态写 command(可带 args / env);Streamable HTTP 形态写 url(可带 headers,
97
+ 逐字进请求头),落成 &#123; "type": "http", "url": …, "headers": … &#125; 条目。
97
98
 
98
99
  #### `skills`
99
100
 
@@ -125,6 +126,18 @@ niceeval 的项目根(含 `niceeval.config.ts` 的目录)解析,不是 Sandbox
125
126
  (不继承宿主机配置、不拼接、不重新序列化);保留键 `model` 与 `env` 出现在文件里
126
127
  setup 报错。manifest 只记项目相对路径与字节 SHA-256,不落正文。
127
128
 
129
+ #### `postSetup`
130
+
131
+ ```ts
132
+ postSetup?: SandboxHook[];
133
+ ```
134
+
135
+ 安装后按数组顺序运行的用户钩子(复用 SandboxHook 的窄上下文):在写 settings、挂 MCP、
136
+ 装 Skills / Plugin、写 manifest 全部完成后执行,适合跑插件自带的 setup 脚本这类
137
+ 「安装产物就位后才能跑」的过程动作。钩子返回的 cleanup 按 LIFO 与 teardown 一起收尾;
138
+ 抛错按基础设施错误计(attempt errored)。
139
+ 见 docs/feature/adapters/library/coding-agent-extensions.md「安装后运行脚本」。
140
+
128
141
  ### `CodexConfig`
129
142
 
130
143
  #### `apiKey`
@@ -150,7 +163,8 @@ mcpServers?: McpServer[];
150
163
  ```
151
164
 
152
165
  额外 MCP server(每个沙箱 setup 时追加进 ~/.codex/config.toml)。
153
- 格式对应 codex config.toml 的 [mcp_servers.&lt;name&gt;] 表。
166
+ stdio 形态(command/args/env)写 [mcp_servers.&lt;name&gt;] 的 command 行;
167
+ Streamable HTTP 形态(url/headers)写 url 行,headers 进 [mcp_servers.&lt;name&gt;.http_headers] 子表。
154
168
 
155
169
  #### `skills`
156
170
 
@@ -184,6 +198,18 @@ niceeval 的项目根(含 `niceeval.config.ts` 的目录)解析,不是 Sandbox
184
198
  `model_reasoning_effort`、`mcp_servers`、`otel` 出现在文件里 setup 报错。manifest 只记
185
199
  项目相对路径与字节 SHA-256,不落正文。
186
200
 
201
+ #### `postSetup`
202
+
203
+ ```ts
204
+ postSetup?: SandboxHook[];
205
+ ```
206
+
207
+ 安装后按数组顺序运行的用户钩子(复用 SandboxHook 的窄上下文):在写主配置、挂 MCP、
208
+ 装 Skills / Plugin、写 manifest 全部完成后执行,适合跑插件自带的 setup 脚本这类
209
+ 「安装产物就位后才能跑」的过程动作。钩子返回的 cleanup 按 LIFO 与 teardown 一起收尾;
210
+ 抛错按基础设施错误计(attempt errored)。
211
+ 见 docs/feature/adapters/library/coding-agent-extensions.md「安装后运行脚本」。
212
+
187
213
  ### `BubConfig`
188
214
 
189
215
  #### `apiKey`
@@ -221,6 +247,17 @@ pythonPlugins?: PythonPluginSpec[];
221
247
  规范化后的 package 列表进安装 checkpoint key:plugin 集合不同的两个 agent 变体不会复用同一个
222
248
  安装 checkpoint(否则第二个变体会静默拿到第一个变体的环境)。
223
249
 
250
+ #### `postSetup`
251
+
252
+ ```ts
253
+ postSetup?: SandboxHook[];
254
+ ```
255
+
256
+ 安装后按数组顺序运行的用户钩子(复用 SandboxHook 的窄上下文):在装 bub、装 Skills /
257
+ Python package、写 manifest 全部完成后执行。钩子返回的 cleanup 按 LIFO 与 teardown 一起
258
+ 收尾;抛错按基础设施错误计(attempt errored)。
259
+ 见 docs/feature/adapters/library/coding-agent-extensions.md「安装后运行脚本」。
260
+
224
261
  {/* GENERATED:END builtin-agent-config */}
225
262
 
226
263
  ## `uiMessageStreamAgent`:接 AI SDK 应用的内建无侵入 adapter(含 HITL)
@@ -97,7 +97,7 @@ npx niceeval exp compare-models weather
97
97
  | `--results` | string | `show` / `view` / `sandbox enter\|list\|stop` 共用:结果根目录(`.niceeval` 之外的另一个根,如 `copySnapshots` 产出的发布根)。 |
98
98
  | `--snapshot` | string | `view` 命令专用:只打开这一份快照文件(`snapshot.json`);文件不可读时命令失败(扫描模式只跳过)。 |
99
99
  | `--report` | string | `show` / `view` 命令专用:用文件默认导出的 `defineReport(...)` 替换两者共用的默认报告。 |
100
- | `--page` | string | `show` / `view` 命令专用:选择多页报告的页(页 id 见 `show --report` 的页索引);`view` 里定初始页。 |
100
+ | `--page` | string | `show` / `view` 命令专用:选择报告的初始页;`show` 渲染该页并在尾部附其余页索引,`view` 以它作初始路由。未命中的页 id 按用法错误退出并列出可用页 id。 |
101
101
  | `--dry` | boolean | 只打印本次会匹配到的 eval × 运行配置,不实际执行(按下面 `--output` 选中的 profile 给出预览)。 |
102
102
  | `--output` | string | 反馈 profile:`auto`(默认)按环境自动选择,`human` / `agent` / `ci` 强制指定;只改变终端展示,不改变选择、调度、判定、artifact 或退出码。`auto` 依次判定:stderr 是 TTY → human;否则 `CI`(或其它常见 CI 平台环境变量)存在 → ci;否则 → agent。 |
103
103
  | `--force` | boolean | 忽略上次运行结果,不跳过已通过的 (experiment, eval) 组合,强制全部重跑。 |
@@ -52,6 +52,14 @@ tags?: string[];
52
52
 
53
53
  标签,供 CLI `--tag` 过滤和 view 分类;与 id 前缀过滤是两套独立的筛选维度。
54
54
 
55
+ #### `environment`
56
+
57
+ ```ts
58
+ environment?: string;
59
+ ```
60
+
61
+ 这条 eval 需要的环境 profile id(provider-neutral,如 `"python-3.9-astropy-4.2"`);由 sandbox spec 的 `environments` 表翻译成该 provider 的预制产物。
62
+
55
63
  #### `judge`
56
64
 
57
65
  ```ts