niceeval 0.8.0 → 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 (143) 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 +72 -6
  10. package/dist/report/components.js +159 -10
  11. package/dist/report/compute.d.ts +31 -4
  12. package/dist/report/compute.js +131 -12
  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 -10
  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 -11
  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 +6 -8
  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 -2
  56. package/docs-site/zh/reference/define-eval.mdx +8 -0
  57. package/docs-site/zh/reference/official-adapters.mdx +10 -5
  58. package/docs-site/zh/reference/report-components.mdx +2 -2
  59. package/docs-site/zh/reference/results-data.mdx +2 -4
  60. package/package.json +2 -1
  61. package/src/agents/bub.ts +13 -1
  62. package/src/agents/claude-code.test.ts +43 -1
  63. package/src/agents/claude-code.ts +32 -14
  64. package/src/agents/codex.test.ts +168 -1
  65. package/src/agents/codex.ts +51 -15
  66. package/src/agents/mcp.ts +31 -0
  67. package/src/agents/post-setup.ts +33 -0
  68. package/src/agents/types.ts +28 -7
  69. package/src/cli.ts +9 -11
  70. package/src/context/context.ts +11 -5
  71. package/src/define.ts +3 -0
  72. package/src/i18n/en.ts +5 -2
  73. package/src/i18n/zh-CN.ts +5 -1
  74. package/src/index.ts +1 -0
  75. package/src/report/built-in/index.tsx +8 -7
  76. package/src/report/built-in/standard.tsx +59 -0
  77. package/src/report/components.tsx +231 -17
  78. package/src/report/compute.ts +146 -21
  79. package/src/report/dual-render.test.tsx +139 -12
  80. package/src/report/index.ts +20 -1
  81. package/src/report/locale.ts +83 -1
  82. package/src/report/react/AttemptList.tsx +13 -1
  83. package/src/report/react/CopyFixPrompt.tsx +37 -0
  84. package/src/report/react/HeroCard.tsx +59 -0
  85. package/src/report/react/PoweredBy.tsx +20 -0
  86. package/src/report/react/ScopeWarnings.tsx +74 -0
  87. package/src/report/react/TraceWaterfall.tsx +78 -0
  88. package/src/report/react/enhance.js +14 -0
  89. package/src/report/react/index.tsx +11 -0
  90. package/src/report/react/styles.css +187 -7
  91. package/src/report/report.test.ts +2 -20
  92. package/src/report/report.ts +97 -62
  93. package/src/report/scope-warnings.ts +155 -0
  94. package/src/report/site-components.test.tsx +526 -0
  95. package/src/report/text/faces.ts +66 -0
  96. package/src/report/tree.ts +8 -1
  97. package/src/report/types.ts +51 -11
  98. package/src/report/web.ts +7 -40
  99. package/src/results/copy.ts +15 -78
  100. package/src/results/host-equivalence.test.ts +5 -1
  101. package/src/results/open.ts +5 -4
  102. package/src/results/publish.ts +4 -146
  103. package/src/results/results.test.ts +86 -9
  104. package/src/results/select.ts +78 -10
  105. package/src/results/types.ts +27 -7
  106. package/src/runner/attempt.ts +10 -9
  107. package/src/runner/discover.test.ts +9 -1
  108. package/src/runner/discover.ts +3 -3
  109. package/src/runner/fingerprint.ts +9 -4
  110. package/src/runner/ledger.test.ts +30 -1
  111. package/src/runner/ledger.ts +26 -4
  112. package/src/runner/run.ts +5 -1
  113. package/src/runner/sandbox-selection.test.ts +131 -0
  114. package/src/runner/sandbox-selection.ts +110 -0
  115. package/src/runner/types.ts +19 -2
  116. package/src/sandbox/types.ts +6 -0
  117. package/src/show/index.ts +17 -10
  118. package/src/show/render.ts +11 -11
  119. package/src/show/report-host.test.ts +32 -15
  120. package/src/show/report-host.ts +5 -4
  121. package/src/show/show.test.ts +140 -3
  122. package/src/view/app/App.test.tsx +78 -17
  123. package/src/view/app/App.tsx +17 -78
  124. package/src/view/app/components/CopyControls.tsx +4 -42
  125. package/src/view/app/i18n.ts +5 -227
  126. package/src/view/app/lib/rows.ts +3 -21
  127. package/src/view/app/shared.ts +1 -3
  128. package/src/view/app/types.ts +2 -2
  129. package/src/view/artifact-serving.test.ts +1 -1
  130. package/src/view/client-dist/app.css +1 -1
  131. package/src/view/client-dist/app.js +20 -20
  132. package/src/view/data.ts +2 -13
  133. package/src/view/index.ts +1 -12
  134. package/src/view/server.ts +0 -2
  135. package/src/view/shared/types.ts +11 -6
  136. package/src/view/site-parity.test.ts +1 -1
  137. package/src/view/site.ts +1 -1
  138. package/src/view/styles.css +6 -266
  139. package/src/view/view-report.test.ts +64 -27
  140. package/src/view/app/components/LazyArtifact.tsx +0 -51
  141. package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
  142. package/src/view/app/pages/AttemptsPage.tsx +0 -80
  143. package/src/view/app/pages/TracesPage.tsx +0 -35
@@ -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` 时渲染的就是默认报告,你的报告和它是同级实现。
@@ -20,12 +20,9 @@ const results = await openResults(".niceeval");
20
20
  await rm(output, { recursive: true, force: true });
21
21
  await copySnapshots(results.latest(), output, {
22
22
  artifacts: ["sources", "events", "trace", "o11y", "agentSetup"],
23
- redact: (text) => text.replaceAll(/sk-[A-Za-z0-9]+/g, "[redacted]"),
24
23
  });
25
24
  ```
26
25
 
27
- `redact` 是必填项:给一个函数改写待发布文件里的自由文本,或者确认这批数据可以原文公开、显式传 `redact: false`。要发布的站点谁都能翻到 prompt 和工具输出,这个选择必须写在脚本里。
28
-
29
26
  运行 `npx tsx scripts/publish-results.ts`,然后提交 `report-data/`。`copySnapshots` 在创建目录前检查所有待发布文件;任何文件超过 50 MiB 时整体失败并列出路径、大小和处理建议,不会留下半份目录。`diff` 缺省不发布;需要 diff 时显式加进 `artifacts`,它也受同一个预算约束。历史版本留下的超大 events / trace 不会被悄悄改写,预检会要求你排除这类证据或用当前版本重跑。
30
27
 
31
28
  ## 构建命令就是导出命令
@@ -38,7 +35,7 @@ npx niceeval view --results report-data --out site
38
35
 
39
36
  ## 发布自定义报告
40
37
 
41
- 不传 `--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` 看到的一模一样:
42
39
 
43
40
  ```bash
44
41
  npx niceeval view --results report-data --report reports/exam.tsx --out site
@@ -48,12 +45,14 @@ npx niceeval view --results report-data --report reports/exam.tsx --out site
48
45
 
49
46
  ## 接站点分析与第三方脚本
50
47
 
51
- 发布出去的站想挂 Google Analytics、埋点或评论组件,在报告文件的 `head` 字段里声明标签。厂商文档里的 snippet 逐字段照抄成对象就行——以 GA4 为例,官方给的两段 `<script>` 写成两个条目:
48
+ 发布出去的站想挂 Google Analytics、埋点或评论组件,在报告文件的 `head` 字段里声明标签。厂商文档里的 snippet 逐字段照抄成对象就行——以 GA4 为例,官方给的两段 `<script>` 写成两个条目。页面内容不用重写:默认报告以 `standard` 为名从 `niceeval/report/built-in` 导出,`extends` 它就是原样的默认站点加上你的标题和脚本:
52
49
 
53
50
  ```tsx
54
- import { ExperimentComparison, defineReport } from "niceeval/report";
51
+ import { defineReport } from "niceeval/report";
52
+ import { standard } from "niceeval/report/built-in";
55
53
 
56
54
  export default defineReport({
55
+ extends: standard,
57
56
  title: "Memory Evals",
58
57
  head: [
59
58
  { tag: "script", attrs: { async: true, src: "https://www.googletagmanager.com/gtag/js?id=G-XXXX" } },
@@ -67,7 +66,6 @@ export default defineReport({
67
66
  `,
68
67
  },
69
68
  ],
70
- content: <ExperimentComparison />,
71
69
  });
72
70
  ```
73
71
 
@@ -139,4 +137,4 @@ jobs:
139
137
 
140
138
  ## 发布的是选中的证据
141
139
 
142
- 整站导出会带上发布结果根里选中的 transcript、源码快照和 trace。新写入的超大 events / trace 字符串可能带结构化截断标记。`copySnapshots` 只在你给了消毒函数时改写自由文本(`redact` 是必填项,见上),其余只做选择和整文件大小预检;传 `redact: false` 的目录再导出时还要加 `--allow-sensitive-artifacts` 确认一次。发布到公网前确认结果里没有密钥或敏感数据。
140
+ 整站导出会带上发布结果根里选中的 transcript、源码快照和 trace。新写入的超大 events / trace 字符串可能带结构化截断标记。`copySnapshots` 只做选择和整文件大小预检,不改写任何内容。发布的站点谁都能翻到 prompt 和工具输出,发布到公网前确认结果内容适合公开——NiceEval 在记录时就不把环境变量值和命令输出写进结果文件,transcript 里是 Agent 自己的输入输出和你的 eval 任务本身。
@@ -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)
@@ -88,7 +88,6 @@ npx niceeval exp compare-models weather
88
88
  | `--json` | string | 额外写一份 JSON 结果(`RunSummary` 原样序列化)到指定路径,供 CI 或下游脚本消费。 |
89
89
  | `--out` | string | `view` 命令专用:把结果查看器静态导出到指定目录。 |
90
90
  | `--port` | number | `view` 命令专用:指定本地服务器监听端口。 |
91
- | `--allow-sensitive-artifacts` | boolean | `view --out` 专用:对非发布根(快照没有 publish:&#123;redaction:"applied"&#125; 标记)导出时的显式确认——静态站会原样携带未消毒的证据文件。 |
92
91
  | `--source` | boolean | `show` 命令专用:该 attempt 运行时保存的 Eval 源码,gate/soft 断言标回源码行(证据切面)。 |
93
92
  | `--execution` | boolean | `show` 命令专用:该 attempt 的标准执行事件流(消息、thinking、Skill load、工具调用/结果);有 OTel 时同一节点补时间(证据切面)。 |
94
93
  | `--timing` | boolean | `show` 命令专用:整个 Attempt 的统一时间树;裸 `--timing` 给有界诊断投影,`--timing=full` 逐节点展开全部 runner/已关联 OTel 节点。 |
@@ -98,7 +97,7 @@ npx niceeval exp compare-models weather
98
97
  | `--results` | string | `show` / `view` / `sandbox enter\|list\|stop` 共用:结果根目录(`.niceeval` 之外的另一个根,如 `copySnapshots` 产出的发布根)。 |
99
98
  | `--snapshot` | string | `view` 命令专用:只打开这一份快照文件(`snapshot.json`);文件不可读时命令失败(扫描模式只跳过)。 |
100
99
  | `--report` | string | `show` / `view` 命令专用:用文件默认导出的 `defineReport(...)` 替换两者共用的默认报告。 |
101
- | `--page` | string | `show` / `view` 命令专用:选择多页报告的页(页 id 见 `show --report` 的页索引);`view` 里定初始页。 |
100
+ | `--page` | string | `show` / `view` 命令专用:选择报告的初始页;`show` 渲染该页并在尾部附其余页索引,`view` 以它作初始路由。未命中的页 id 按用法错误退出并列出可用页 id。 |
102
101
  | `--dry` | boolean | 只打印本次会匹配到的 eval × 运行配置,不实际执行(按下面 `--output` 选中的 profile 给出预览)。 |
103
102
  | `--output` | string | 反馈 profile:`auto`(默认)按环境自动选择,`human` / `agent` / `ci` 强制指定;只改变终端展示,不改变选择、调度、判定、artifact 或退出码。`auto` 依次判定:stderr 是 TTY → human;否则 `CI`(或其它常见 CI 平台环境变量)存在 → ci;否则 → agent。 |
104
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
@@ -15,10 +15,11 @@ description: "NiceEval 内置的 Sandbox 和非 Sandbox 适配器分别是什么
15
15
  ### claude-code
16
16
 
17
17
  - **鉴权**:`ANTHROPIC_API_KEY`(工厂参数 `apiKey` 可覆盖),可选 `ANTHROPIC_BASE_URL`(工厂参数 `baseUrl`)。
18
- - **装 MCP server**:`mcpServers` 配置项,`setup` 阶段写进沙箱里用户级的 `~/.claude.json`(顶层 `mcpServers` 字段)。
18
+ - **装 MCP server**:`mcpServers` 配置项,`setup` 阶段写进沙箱里用户级的 `~/.claude.json`(顶层 `mcpServers` 字段)。两种形态按字段区分:本地 stdio 进程写 `command`(可带 `args` / `env`);远程 Streamable HTTP 端点写 `url`(可带 `headers`,逐字进请求头,常用于 `Authorization`),写成 `{ "type": "http", "url": …, "headers": … }` 条目。`url` 要沙箱内可达:服务跑在你自己机器上时,先用 cloudflared / tailscale 这类隧道暴露成公网地址。
19
19
  - **装 Skill**:`skills: SkillSpec[]`——本地 Skill(`{ kind: "local", path }`,从项目根读文件或目录)或 Repo Skill(`{ kind: "repo", source, ref, skills }`,可钉 commit/tag、可只启用多 Skill 仓库里的一部分)。装进沙箱的 project 级 `.claude/skills/<name>/`,claude CLI 原生发现(原生 `Skill` 工具调用被 adapter 归一为 `skill.loaded` 事件,不重复记成工具调用;用 `t.loadedSkill()` 断言,不是 `t.calledTool("Skill", ...)`)。
20
20
  - **装原生 Plugin**:`plugins: ClaudeCodePluginSpec[]`,每一项声明 Marketplace 连接(`name` / `source` / 可选 `ref`)和其中的 Plugin 名。这个类型只属于 claude-code,传不进 codex。
21
21
  - **官方配置文件**:`settingsFile` 是运行 NiceEval 的机器上的本地项目路径,不是 Sandbox 内路径;它指向一份完整的 Claude Code `settings.json`。路径相对项目根,只允许普通相对路径或 `./` 前缀;`..`、绝对路径、`~` 和解析后逃出项目根的符号链接都会报错。Adapter 从本地读取后上传文件,原样替换 Sandbox 中原本为空的用户级 `~/.claude/settings.json`;不继承宿主机配置,也不 deep merge 或重新序列化。`model` 和 `env` 归 Experiment 和 Adapter 管,出现在文件里会在 `setup` 阶段报错并点名冲突键。Secret 走环境变量,别写进配置文件。
22
+ - **安装后脚本**:`postSetup: SandboxHook[]`,在写 settings、挂 MCP、装 Skill 与 Plugin 全部完成后,按数组顺序在沙箱里跑你的钩子函数。典型用途是运行插件自带的 setup 脚本(比如它要往全局配置里登记 hook)——这类脚本必须等安装产物就位才能跑。钩子抛错算基础设施错误(Attempt 记 errored),不算 agent 答题失败。
22
23
  - **tracing**:claude CLI 的 beta 原生遥测(`CLAUDE_CODE_ENHANCED_TELEMETRY_BETA`),span 只有结构和计时,细节见 [OTel 接入](/zh/how-to/connect-otel)。
23
24
 
24
25
  例如,用 `configs/claude-code/no-web.json` 关闭内置联网检索:
@@ -60,15 +61,16 @@ export default defineExperiment({
60
61
  ### codex
61
62
 
62
63
  - **鉴权**:`CODEX_API_KEY`(工厂参数 `apiKey` 可覆盖,不是 `OPENAI_API_KEY`),可选 `CODEX_BASE_URL`(工厂参数 `baseUrl`)接 OpenAI 兼容代理。
63
- - **装 MCP server**:`mcpServers` 配置项,`setup` 阶段追加进 `~/.codex/config.toml` 的 `[mcp_servers.<name>]` 段。
64
+ - **装 MCP server**:`mcpServers` 配置项,`setup` 阶段追加进 `~/.codex/config.toml` 的 `[mcp_servers.<name>]` 段。两种形态按字段区分:本地 stdio 进程写 `command`(可带 `args` / `env`);远程 Streamable HTTP 端点写 `url`(可带 `headers`,写成 `[mcp_servers.<name>.http_headers]` 子表)。`url` 要沙箱内可达:服务跑在你自己机器上时,先用 cloudflared / tailscale 这类隧道暴露成公网地址。
64
65
 
65
66
  <Warning>
66
67
  是复数 `mcp_servers`:单数 `[mcp_server.x]` 会被 codex CLI 静默忽略,MCP 压根挂不上,且不会报错——自查用 `codex mcp list`。
67
68
  </Warning>
68
69
 
69
70
  - **装 Skill**:`skills: SkillSpec[]`,与 claude-code 同一个类型。装进 `.agents/skills/<name>/`,并把发现指引写进 AGENTS.md——codex 没有 claude-code 那种原生 Skill 工具,只把文件装进去它不会主动去读;断言"用没用到"看它是否真的执行过读那个文件的 shell 命令,没有工具调用可以直接认。
70
- - **装原生 Plugin**:`plugins: CodexPluginSpec[]`,声明 Marketplace 连接(`name` / `source` / 可选 `ref`)和其中的 Plugin 名。这个类型只属于 codex,传不进 claude-code。
71
+ - **装原生 Plugin**:`plugins: CodexPluginSpec[]`,声明 Marketplace 连接(`name` / `source` / 可选 `ref` / 可选 `sparse`)和其中的 Plugin 名。`sparse` 是路径数组(如 `[".agents", "plugins/repo-map"]`),每项让 `codex plugin marketplace add` 带一个 `--sparse <path>`,大仓库只拉插件所需路径,装出来的内容不变。这个类型只属于 codex,传不进 claude-code。
71
72
  - **官方配置文件**:`configFile` 是运行 NiceEval 的机器上的本地项目路径,不是 Sandbox 内路径;它指向一份完整的 Codex `config.toml`。路径相对项目根,只允许普通相对路径或 `./` 前缀;`..`、绝对路径、`~` 和解析后逃出项目根的符号链接都会报错。Adapter 从本地读取后上传文件,原样替换 Sandbox 中原本为空的用户级 `~/.codex/config.toml`;不继承宿主机配置,也不拼接、deep merge 或解析后重写。`model`、`model_provider`、`model_providers`、`model_reasoning_effort`、`mcp_servers`、`otel` 归 Experiment 和 Adapter 管,出现在文件里会在 `setup` 阶段报错并点名冲突键。Secret 走环境变量,别写进配置文件。
73
+ - **安装后脚本**:`postSetup: SandboxHook[]`,语义与 claude-code 相同:全部安装步骤完成后按序在沙箱里跑你的钩子函数,适合运行插件自带的 setup 脚本。脚本往 codex 全局配置登记的 hook 不需要交互式信任确认——运行时 `codex exec` 已绕过 hook 信任门槛,hook 直接生效。
72
74
  - **tracing**:内置,通过 `config.toml` 的 `[otel.trace_exporter.otlp-http]` 段配置,协议 `http/json`。
73
75
 
74
76
  例如,用 `configs/codex/no-web.toml` 关闭内置联网检索:
@@ -87,6 +89,7 @@ export default defineExperiment({
87
89
  agent: codexAgent({
88
90
  mcpServers: [
89
91
  { name: "browser", command: "npx", args: ["-y", "@anthropic/mcp-browser"] },
92
+ { name: "team-memory", url: "https://mem.example.com/mcp/", headers: { Authorization: `Bearer ${process.env.MEM_API_KEY}` } },
90
93
  ],
91
94
  skills: [{ kind: "repo", source: "Effect-TS/skills", ref: "8f3c1a2", skills: ["effect"] }],
92
95
  plugins: [
@@ -109,6 +112,7 @@ export default defineExperiment({
109
112
  - **装插件**:`pythonPlugins: PythonPluginSpec[]`(`{ package }`:PyPI 包、版本约束或 git URL),`setup` 阶段进 `uv tool install … --with <package>`。这个类型只属于 bub;package 集合进安装 checkpoint key,插件不同的两个变体不会复用同一份安装缓存。
110
113
  - **预制 Bub**:NiceEval 的 E2B 配方会把 Bub、OTel 插件和 Python 插件集合算成安装指纹。Adapter 只复用指纹完全一致的环境;仅在 PATH 里放一个 `bub` 不足以证明兼容。构建入口见 [沙箱 provider · 从官方基线继续构建以提速](/zh/how-to/sandbox-providers#从官方基线继续构建以提速)。
111
114
  - bub 没有 `mcpServers`——MCP 只属于支持它的 Adapter,Config 上压根没有这个字段。
115
+ - **安装后脚本**:`postSetup: SandboxHook[]`,语义与另外两个 Adapter 相同:全部安装步骤完成后按序在沙箱里跑你的钩子函数。
112
116
  - **安装方式**:走 `uv tool install`(PyPI 包,不是 npm 包),首次安装会建 checkpoint 缓存加速后续沙箱。
113
117
  - **tracing**:内置,通过环境变量注入,协议 `http/protobuf`。
114
118
 
@@ -132,15 +136,16 @@ export default defineExperiment({
132
136
  | | claude-code | codex | bub |
133
137
  |---|---|---|---|
134
138
  | 鉴权环境变量 | `ANTHROPIC_API_KEY` | `CODEX_API_KEY` | `BUB_API_KEY` + `BUB_API_BASE` |
135
- | MCP server | ✅ `mcpServers` | ✅ `mcpServers` | ❌ |
139
+ | MCP server | ✅ `mcpServers`(stdio + HTTP) | ✅ `mcpServers`(stdio + HTTP) | ❌ |
136
140
  | Skill | ✅ `skills: SkillSpec[]`(原生发现) | ✅ `skills: SkillSpec[]`(+ 发现指引) | ✅ `skills: SkillSpec[]`(+ 发现指引) |
137
141
  | 原生 Plugin | ✅ `plugins: ClaudeCodePluginSpec[]` | ✅ `plugins: CodexPluginSpec[]` | ❌ |
138
142
  | 官方配置文件 | ✅ `settingsFile`(完整 settings.json) | ✅ `configFile`(完整 config.toml) | ❌ |
139
143
  | Python Plugin | ❌ | ❌ | ✅ `pythonPlugins: PythonPluginSpec[]` |
144
+ | 安装后脚本 | ✅ `postSetup: SandboxHook[]` | ✅ `postSetup: SandboxHook[]` | ✅ `postSetup: SandboxHook[]` |
140
145
  | tracing | ✅(beta,仅结构与计时) | ✅ | ✅ |
141
146
  | 安装方式 | npm 全局包 | npm 全局包 | `uv tool install`(PyPI) |
142
147
 
143
- 装了什么有据可查:Adapter 在 `setup` 收尾把安装清单写进沙箱的 `__niceeval__/agent-setup.json`,运行器把它存成 Attempt Artifact `agent-setup.json`(库里读 `attempt.agentSetup()`)。清单只记来源、ref、Skill/Plugin 名、解析出的版本,以及官方配置文件的项目相对路径和 SHA-256;不保存配置正文、API Key 或环境变量值。
148
+ 装了什么有据可查:Adapter 在 `setup` 收尾把安装清单写进沙箱的 `__niceeval__/agent-setup.json`,运行器把它存成 Attempt Artifact `agent-setup.json`(库里读 `attempt.agentSetup()`)。清单只记来源、ref、Skill/Plugin 名、解析出的版本,以及官方配置文件的项目相对路径和 SHA-256;不保存配置正文、API Key 或环境变量值。MCP server 同理只记非敏感字段:stdio 形态记 `name` / `command` / `args` 不记 `env`,HTTP 形态记 `name` / `url` 不记 `headers`。
144
149
 
145
150
  ## 非 Sandbox 适配器
146
151
 
@@ -211,7 +211,7 @@ inspect: niceeval show @<id> [--eval|--execution|--diff]
211
211
  每项固定代表一个 Attempt,显示 experiment、Eval、Attempt 序号、判定、耗时、成本、失败断言、结构化 error 的一层摘要、Judge 评语和证据链接。diagnostics、cause 和 stack 留给 locator 下钻详情,避免比较列表被基础设施日志撑开。它既能列失败证据,也能列通过样本,不把 verdict 过滤写死在组件名里。
212
212
 
213
213
  ```tsx
214
- const attempts = await AttemptList.data(selection, { redact });
214
+ const attempts = await AttemptList.data(selection);
215
215
 
216
216
  <AttemptList
217
217
  items={attempts.filter((item) => item.verdict === "failed" || item.verdict === "errored")}
@@ -234,7 +234,7 @@ inspect: niceeval show @<id> [--eval|--execution|--diff]
234
234
  (3 more not shown · showing 20 of 23)
235
235
  ```
236
236
 
237
- 要在页面显示前遮蔽 error message/cause/stack、diagnostic message/data、断言 detail 或 Judge 评语,把 `redact` 交给 `.data()`;稳定 code、lifecycle operation、experiment、Eval 和 locator 不改。它只影响这份组件数据,管不到发布目录里的 artifact 文件——发布数据集的消毒用 [`copySnapshots` 的 `redact` 选项](/zh/reference/results-data)。要展示哪些 Attempt,过滤返回的 `AttemptListItem[]`。`limit` 也由报告作者在数组上用 `.slice(0, 20)` 表达,截断时把原始数量交给组件的 `total`,组件据此显示“还有 n 项未展示”,不静默截断。
237
+ 要展示哪些 Attempt,过滤返回的 `AttemptListItem[]`。`limit` 也由报告作者在数组上用 `.slice(0, 20)` 表达,截断时把原始数量交给组件的 `total`,组件据此显示“还有 n 项未展示”,不静默截断。
238
238
 
239
239
  ## 指标表(`MetricTable`)
240
240
 
@@ -235,9 +235,7 @@ import { openResults, copySnapshots } from "niceeval/results";
235
235
  const results = await openResults(".niceeval");
236
236
  await copySnapshots(results.latest(), "site-data/run", {
237
237
  artifacts: ["sources", "events", "trace", "o11y"], // diff 不截断,缺省也不带;
238
- redact: (text) => text.replaceAll(/sk-[A-Za-z0-9]+/g, "[redacted]"),
239
- }); // redact 必填:函数消毒,或 false 显式声明原文发布;
240
- // 每个待发布文件还会经过 50 MiB 预检;
238
+ }); // 每个待发布文件还会经过 50 MiB 预检;
241
239
  // o11y 只有几 KB,报告用到 turns 这类
242
240
  // 读 o11y 的指标就把它带上,不然渲染成「—」
243
241
  ```
@@ -246,7 +244,7 @@ await copySnapshots(results.latest(), "site-data/run", {
246
244
 
247
245
  复制开始前,NiceEval 会规划全部目标文件并检查序列化后的大小。任一文件超过固定的 50 MiB,整次复制在创建目标目录前失败,错误会列出路径、实际大小和处理建议。你可以从 `artifacts` 排除那类证据;如果是旧版本留下的超大 events / trace,用当前版本重跑后再发布。这个检查既覆盖没有逐值截断的源码 / diff,也覆盖单值都正常但累计过大的 JSON,避免直到 `git push` 才撞上 Git host 的单文件限制。
248
246
 
249
- 大小预检只决定整次复制成功或失败,不会从一个超大文件中间删内容。消毒不是可选项——`copySnapshots` 要求显式传 `redact`:给一个函数就改写复制出来的所有文件里的自由文本(events、trace、源码、diff、运行摘要都在内;id、事件类型这类标识字段不动),确定这批数据可以原文公开就传 `redact: false`,两个都不传会直接报错。注意报告积木 `AttemptList.data` 的 `redact` 只影响页面上显示的数据,管不到发布目录里的 artifact 文件;发布场景一律在 `copySnapshots` 这一步消毒。唯一随行补记的是挑选时的**覆盖事实**:`partial-coverage` 警告的分母是实验的历史并集,而发布目录没有历史——所以每个复制出的快照带上 `knownEvalIds`(复制时刻该实验已知的 eval 并集),reader 端把它并进 `exp.evalIds` 的计算(取本地历史与快照携带值的并集)。发布目录上重新 `openResults().latest()`,残缺警告被同一套机制重新算出来,不靠发布者转述。复制出的目录就是标准结果目录,`niceeval view --results <目录>` 直接能看;要让报告站随 push 自动更新,workflow 见[通过 CI 发布报告](/zh/how-to/publish-report)。
247
+ 大小预检只决定整次复制成功或失败,不会从一个超大文件中间删内容。复制忠实于源:artifact 按原字节复制,不重新序列化、不改写。唯一随行补记的是挑选时的**覆盖事实**:`partial-coverage` 警告的分母是实验的历史并集,而发布目录没有历史——所以每个复制出的快照带上 `knownEvalIds`(复制时刻该实验已知的 eval 并集),reader 端把它并进 `exp.evalIds` 的计算(取本地历史与快照携带值的并集)。发布目录上重新 `openResults().latest()`,残缺警告被同一套机制重新算出来,不靠发布者转述。复制出的目录就是标准结果目录,`niceeval view --results <目录>` 直接能看;要让报告站随 push 自动更新,workflow 见[通过 CI 发布报告](/zh/how-to/publish-report)。
250
248
 
251
249
  ## 分层速览
252
250
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niceeval",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Agent-native eval tool — eval agents, services, functions, and coding-agent fixtures",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -172,6 +172,7 @@
172
172
  "tiers:check": "node scripts/sync-tiers.mjs check",
173
173
  "view:build": "vite build --config src/view/app/vite.config.ts",
174
174
  "build:report": "tsc -p tsconfig.report-build.json && node scripts/prune-report-dist.mjs",
175
+ "build:index": "tsx scripts/generate-reference.ts --bundled-index",
175
176
  "site:dev": "cd site && node scripts/dev.mjs",
176
177
  "site:build": "cd site && next build",
177
178
  "docs:dev": "node scripts/docs-dev.mjs",
package/src/agents/bub.ts CHANGED
@@ -11,7 +11,8 @@ import {
11
11
  import { writeAgentSetupManifest } from "./manifest.ts";
12
12
  import { createCheckpoint, restoreCheckpoint } from "../sandbox/checkpoint.ts";
13
13
  import { mapBubSpans } from "../o11y/otlp/mappers/bub.ts";
14
- import type { Agent, AgentContext, AgentSetupManifest, Sandbox, SkillSpec } from "../types.ts";
14
+ import { runPostSetupHooks } from "./post-setup.ts";
15
+ import type { Agent, AgentContext, AgentSetupManifest, Sandbox, SandboxHook, SkillSpec } from "../types.ts";
15
16
  import { createHash, randomUUID } from "node:crypto";
16
17
  import { readFile, writeFile, mkdir } from "node:fs/promises";
17
18
  import { homedir } from "node:os";
@@ -61,6 +62,13 @@ export interface BubConfig {
61
62
  * 安装 checkpoint(否则第二个变体会静默拿到第一个变体的环境)。
62
63
  */
63
64
  pythonPlugins?: PythonPluginSpec[];
65
+ /**
66
+ * 安装后按数组顺序运行的用户钩子(复用 SandboxHook 的窄上下文):在装 bub、装 Skills /
67
+ * Python package、写 manifest 全部完成后执行。钩子返回的 cleanup 按 LIFO 与 teardown 一起
68
+ * 收尾;抛错按基础设施错误计(attempt errored)。
69
+ * 见 docs/feature/adapters/library/coding-agent-extensions.md「安装后运行脚本」。
70
+ */
71
+ postSetup?: SandboxHook[];
64
72
  }
65
73
 
66
74
  const UV = "$HOME/.local/bin/uv";
@@ -278,6 +286,10 @@ export function bubAgent(config?: BubConfig): Agent {
278
286
  if (manifest.skills.length || manifest.pythonPlugins?.length) {
279
287
  await writeAgentSetupManifest(sb, manifest);
280
288
  }
289
+
290
+ // 安装后钩子(postSetup):排在 manifest 之后——manifest 审计 Adapter 自身的安装事实,
291
+ // 钩子失败不该丢掉这份证据。返回的合成 cleanup 交给 runner,与 teardown 一起 LIFO 收尾。
292
+ return await runPostSetupHooks(sb, ctx, config?.postSetup);
281
293
  },
282
294
 
283
295
  async send(input, ctx) {