niceeval 0.9.0 → 0.10.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 (428) hide show
  1. package/INDEX.md +31 -33
  2. package/dist/agents/types.d.ts +20 -17
  3. package/dist/context/types.d.ts +21 -21
  4. package/dist/i18n/en.d.ts +10 -2
  5. package/dist/i18n/zh-CN.d.ts +15 -7
  6. package/dist/o11y/execution-tree.d.ts +103 -0
  7. package/dist/o11y/otlp/select.d.ts +22 -0
  8. package/dist/report/{react → assets}/colors.d.ts +8 -0
  9. package/dist/report/{react → assets}/colors.js +23 -0
  10. package/dist/report/built-in/index.d.ts +2 -2
  11. package/dist/report/built-in/index.js +2 -2
  12. package/dist/report/built-in/standard.d.ts +8 -1
  13. package/dist/report/built-in/standard.js +17 -7
  14. package/dist/report/components/attempt-detail/AttemptAssertions.d.ts +6 -0
  15. package/dist/report/components/attempt-detail/AttemptAssertions.js +17 -0
  16. package/dist/report/components/attempt-detail/AttemptConversation.d.ts +6 -0
  17. package/dist/report/components/attempt-detail/AttemptConversation.js +34 -0
  18. package/dist/report/components/attempt-detail/AttemptDiagnostics.d.ts +6 -0
  19. package/dist/report/components/attempt-detail/AttemptDiagnostics.js +7 -0
  20. package/dist/report/components/attempt-detail/AttemptDiff.d.ts +6 -0
  21. package/dist/report/components/attempt-detail/AttemptDiff.js +11 -0
  22. package/dist/report/components/attempt-detail/AttemptError.d.ts +6 -0
  23. package/dist/report/components/attempt-detail/AttemptError.js +16 -0
  24. package/dist/report/components/attempt-detail/AttemptFixPrompt.d.ts +6 -0
  25. package/dist/report/components/attempt-detail/AttemptFixPrompt.js +7 -0
  26. package/dist/report/components/attempt-detail/AttemptSource.d.ts +6 -0
  27. package/dist/report/components/attempt-detail/AttemptSource.js +27 -0
  28. package/dist/report/components/attempt-detail/AttemptSummary.d.ts +8 -0
  29. package/dist/report/components/attempt-detail/AttemptSummary.js +20 -0
  30. package/dist/report/components/attempt-detail/AttemptTimeline.d.ts +6 -0
  31. package/dist/report/components/attempt-detail/AttemptTimeline.js +28 -0
  32. package/dist/report/components/attempt-detail/AttemptTrace.d.ts +6 -0
  33. package/dist/report/components/attempt-detail/AttemptTrace.js +29 -0
  34. package/dist/report/components/attempt-detail/AttemptUsage.d.ts +6 -0
  35. package/dist/report/components/attempt-detail/AttemptUsage.js +20 -0
  36. package/dist/report/components/attempt-detail/compute.d.ts +24 -0
  37. package/dist/report/components/attempt-detail/compute.js +254 -0
  38. package/dist/report/components/attempt-detail/faces.d.ts +14 -0
  39. package/dist/report/components/attempt-detail/faces.js +235 -0
  40. package/dist/report/components/attempt-detail/index.d.ts +38 -0
  41. package/dist/report/components/attempt-detail/index.js +527 -0
  42. package/dist/report/{react → components}/cell.d.ts +5 -3
  43. package/dist/report/{react → components}/cell.js +3 -3
  44. package/dist/report/{react → components/entity-lists}/AttemptList.d.ts +8 -4
  45. package/dist/report/{react → components/entity-lists}/AttemptList.js +16 -10
  46. package/dist/report/{react → components/entity-lists}/EvalList.d.ts +3 -3
  47. package/dist/report/{react → components/entity-lists}/EvalList.js +0 -0
  48. package/dist/report/{react → components/entity-lists}/ExperimentList.d.ts +3 -3
  49. package/dist/report/{react → components/entity-lists}/ExperimentList.js +10 -10
  50. package/dist/report/components/entity-lists/compute.d.ts +23 -0
  51. package/dist/report/components/entity-lists/compute.js +171 -0
  52. package/dist/report/components/entity-lists/faces.d.ts +5 -0
  53. package/dist/report/components/entity-lists/faces.js +174 -0
  54. package/dist/report/components/entity-lists/index.d.ts +51 -0
  55. package/dist/report/components/entity-lists/index.js +165 -0
  56. package/dist/report/{react → components}/fixtures.d.ts +2 -2
  57. package/dist/report/{react → components}/fixtures.js +1 -1
  58. package/dist/report/{react → components/metric-views}/DeltaTable.d.ts +2 -2
  59. package/dist/report/{react → components/metric-views}/DeltaTable.js +4 -4
  60. package/dist/report/{react → components/metric-views}/MetricBars.d.ts +3 -3
  61. package/dist/report/{react → components/metric-views}/MetricBars.js +3 -3
  62. package/dist/report/{react → components/metric-views}/MetricLine.d.ts +2 -2
  63. package/dist/report/{react → components/metric-views}/MetricLine.js +8 -5
  64. package/dist/report/{react → components/metric-views}/MetricMatrix.d.ts +3 -3
  65. package/dist/report/{react → components/metric-views}/MetricMatrix.js +4 -4
  66. package/dist/report/components/metric-views/MetricScatter.d.ts +11 -0
  67. package/dist/report/{react → components/metric-views}/MetricScatter.js +26 -23
  68. package/dist/report/{react → components/metric-views}/MetricTable.d.ts +3 -3
  69. package/dist/report/{react → components/metric-views}/MetricTable.js +4 -4
  70. package/dist/report/{react → components/metric-views}/Scoreboard.d.ts +2 -2
  71. package/dist/report/{react → components/metric-views}/Scoreboard.js +3 -3
  72. package/dist/report/components/metric-views/compute.d.ts +89 -0
  73. package/dist/report/{compute.js → components/metric-views/compute.js} +32 -415
  74. package/dist/report/components/metric-views/faces.d.ts +13 -0
  75. package/dist/report/components/metric-views/faces.js +381 -0
  76. package/dist/report/components/metric-views/index.d.ts +50 -0
  77. package/dist/report/components/metric-views/index.js +273 -0
  78. package/dist/report/{text → components/metric-views}/plot.js +1 -1
  79. package/dist/report/components/shared-compute.d.ts +22 -0
  80. package/dist/report/components/shared-compute.js +47 -0
  81. package/dist/report/components/shared-faces.d.ts +9 -0
  82. package/dist/report/components/shared-faces.js +26 -0
  83. package/dist/report/components/shared.d.ts +68 -0
  84. package/dist/report/components/shared.js +125 -0
  85. package/dist/report/{react → components/site-components}/CopyFixPrompt.d.ts +2 -2
  86. package/dist/report/{react → components/site-components}/CopyFixPrompt.js +2 -2
  87. package/dist/report/{react → components/site-components}/HeroCard.d.ts +2 -2
  88. package/dist/report/{react → components/site-components}/HeroCard.js +2 -2
  89. package/dist/report/{react → components/site-components}/ScopeWarnings.d.ts +2 -2
  90. package/dist/report/{react → components/site-components}/ScopeWarnings.js +3 -3
  91. package/dist/report/{react → components/site-components}/TraceWaterfall.d.ts +3 -3
  92. package/dist/report/{react → components/site-components}/TraceWaterfall.js +5 -5
  93. package/dist/report/components/site-components/compute.d.ts +28 -0
  94. package/dist/report/components/site-components/compute.js +132 -0
  95. package/dist/report/components/site-components/faces.d.ts +21 -0
  96. package/dist/report/components/site-components/faces.js +75 -0
  97. package/dist/report/components/site-components/index.d.ts +74 -0
  98. package/dist/report/components/site-components/index.js +220 -0
  99. package/dist/report/{scope-warnings.d.ts → components/site-components/scope-warnings.d.ts} +2 -2
  100. package/dist/report/{scope-warnings.js → components/site-components/scope-warnings.js} +2 -2
  101. package/dist/report/{react → components/summaries}/ScopeSummary.d.ts +2 -2
  102. package/dist/report/{react → components/summaries}/ScopeSummary.js +17 -8
  103. package/dist/report/components/summaries/compute.d.ts +7 -0
  104. package/dist/report/components/summaries/compute.js +51 -0
  105. package/dist/report/components/summaries/faces.d.ts +7 -0
  106. package/dist/report/components/summaries/faces.js +38 -0
  107. package/dist/report/components/summaries/index.d.ts +27 -0
  108. package/dist/report/components/summaries/index.js +78 -0
  109. package/dist/report/definition/grid-layout.d.ts +50 -0
  110. package/dist/report/definition/grid-layout.js +89 -0
  111. package/dist/report/{primitives.d.ts → definition/primitives.d.ts} +34 -4
  112. package/dist/report/{primitives.js → definition/primitives.js} +101 -10
  113. package/dist/report/{report.d.ts → definition/report.d.ts} +36 -65
  114. package/dist/report/{report.js → definition/report.js} +58 -90
  115. package/dist/report/{text/table.d.ts → definition/table-text.d.ts} +2 -2
  116. package/dist/report/{text/table.js → definition/table-text.js} +2 -2
  117. package/dist/report/{tree.d.ts → definition/tree.d.ts} +44 -12
  118. package/dist/report/{tree.js → definition/tree.js} +10 -12
  119. package/dist/report/index.d.ts +33 -18
  120. package/dist/report/index.js +23 -14
  121. package/dist/report/{aggregate.d.ts → model/aggregate.d.ts} +12 -4
  122. package/dist/report/{aggregate.js → model/aggregate.js} +28 -8
  123. package/dist/report/{flag.d.ts → model/flag.d.ts} +14 -1
  124. package/dist/report/{flag.js → model/flag.js} +35 -0
  125. package/dist/report/{format.d.ts → model/format.d.ts} +11 -5
  126. package/dist/report/{format.js → model/format.js} +48 -4
  127. package/dist/report/{locale.d.ts → model/locale.d.ts} +17 -17
  128. package/dist/report/{locale.js → model/locale.js} +31 -33
  129. package/dist/report/{metrics.d.ts → model/metrics.d.ts} +2 -2
  130. package/dist/report/{metrics.js → model/metrics.js} +2 -2
  131. package/dist/report/{types.d.ts → model/types.d.ts} +140 -20
  132. package/dist/report/react/index.d.ts +32 -21
  133. package/dist/report/react/index.js +33 -21
  134. package/dist/report/{load.d.ts → runtime/load.d.ts} +1 -1
  135. package/dist/report/{load.js → runtime/load.js} +1 -1
  136. package/dist/report/runtime/text.d.ts +67 -0
  137. package/dist/report/runtime/text.js +114 -0
  138. package/dist/report/runtime/web.d.ts +35 -0
  139. package/dist/report/runtime/web.js +70 -0
  140. package/dist/results/annotated-source.d.ts +87 -0
  141. package/dist/results/attempt-evidence.d.ts +74 -0
  142. package/dist/results/attempt-source.d.ts +15 -0
  143. package/dist/results/select.d.ts +12 -2
  144. package/dist/results/select.js +27 -5
  145. package/dist/runner/eval-selection.d.ts +26 -0
  146. package/dist/runner/feedback/sink.d.ts +26 -1
  147. package/dist/runner/types.d.ts +149 -23
  148. package/dist/sandbox/docker.d.ts +6 -0
  149. package/dist/sandbox/types.d.ts +23 -21
  150. package/dist/scoring/display.d.ts +7 -2
  151. package/dist/scoring/display.js +42 -4
  152. package/dist/scoring/types.d.ts +3 -3
  153. package/dist/shared/aggregate.d.ts +15 -1
  154. package/dist/shared/aggregate.js +32 -1
  155. package/dist/shared/types.d.ts +6 -1
  156. package/docs-site/images/logo-dark.svg +7 -0
  157. package/docs-site/images/logo.svg +6 -5
  158. package/docs-site/zh/README.md +8 -7
  159. package/docs-site/zh/examples/ai-agent-application.mdx +5 -5
  160. package/docs-site/zh/examples/coding-agent-extensions.mdx +4 -4
  161. package/docs-site/zh/examples/index.mdx +3 -3
  162. package/docs-site/zh/examples/integrations/ai-sdk-v7.mdx +1 -1
  163. package/docs-site/zh/examples/integrations/claude-sdk.mdx +3 -3
  164. package/docs-site/zh/examples/integrations/codex-sdk.mdx +3 -3
  165. package/docs-site/zh/examples/integrations/langgraph.mdx +3 -3
  166. package/docs-site/zh/examples/integrations/pi-sdk.mdx +2 -2
  167. package/docs-site/zh/explanation/adapter.mdx +19 -19
  168. package/docs-site/zh/explanation/assert.mdx +9 -9
  169. package/docs-site/zh/explanation/drive.mdx +4 -4
  170. package/docs-site/zh/explanation/evals.mdx +8 -8
  171. package/docs-site/zh/explanation/experiment.mdx +8 -8
  172. package/docs-site/zh/explanation/hitl.mdx +13 -13
  173. package/docs-site/zh/explanation/judge.mdx +3 -3
  174. package/docs-site/zh/explanation/overview.mdx +7 -7
  175. package/docs-site/zh/explanation/runner.mdx +11 -11
  176. package/docs-site/zh/explanation/tier.mdx +6 -6
  177. package/docs-site/zh/index.mdx +14 -14
  178. package/docs-site/zh/introduction.mdx +13 -16
  179. package/docs-site/zh/reference/builtin-agents.mdx +61 -28
  180. package/docs-site/zh/reference/capabilities.mdx +7 -27
  181. package/docs-site/zh/reference/cli.mdx +35 -32
  182. package/docs-site/zh/reference/define-agent.mdx +32 -31
  183. package/docs-site/zh/reference/define-config.mdx +6 -6
  184. package/docs-site/zh/reference/define-eval.mdx +33 -20
  185. package/docs-site/zh/reference/events.mdx +4 -4
  186. package/docs-site/zh/reference/expect.mdx +3 -3
  187. package/docs-site/zh/reference/official-adapters.mdx +17 -17
  188. package/docs-site/zh/reference/report-components.mdx +193 -121
  189. package/docs-site/zh/reference/results-data.mdx +13 -13
  190. package/docs-site/zh/troubleshooting/debug-sandbox.mdx +11 -11
  191. package/docs-site/zh/troubleshooting/debugging.mdx +43 -19
  192. package/docs-site/zh/{how-to → tutorials}/agent-feedback-loop.mdx +38 -38
  193. package/docs-site/zh/tutorials/agent-onboarding.mdx +100 -0
  194. package/docs-site/zh/tutorials/authoring.mdx +211 -0
  195. package/docs-site/zh/{how-to → tutorials}/ci-integration.mdx +8 -8
  196. package/docs-site/zh/{how-to → tutorials}/connect-otel.mdx +22 -22
  197. package/docs-site/zh/{how-to → tutorials}/connect-your-agent.mdx +58 -63
  198. package/docs-site/zh/tutorials/custom-reports.mdx +453 -0
  199. package/docs-site/zh/{how-to → tutorials}/dataset-fanout.mdx +4 -4
  200. package/docs-site/zh/tutorials/experiments.mdx +103 -0
  201. package/docs-site/zh/{how-to → tutorials}/fixtures.mdx +9 -9
  202. package/docs-site/zh/{how-to → tutorials}/publish-report.mdx +11 -5
  203. package/docs-site/zh/tutorials/quickstart.mdx +18 -18
  204. package/docs-site/zh/{how-to → tutorials}/reporters.mdx +6 -6
  205. package/docs-site/zh/{how-to → tutorials}/sandbox-agent.mdx +6 -6
  206. package/docs-site/zh/{how-to → tutorials}/sandbox-providers.mdx +41 -19
  207. package/docs-site/zh/{how-to → tutorials}/scoring-guide.mdx +4 -4
  208. package/docs-site/zh/{how-to → tutorials}/viewing-results.mdx +59 -50
  209. package/docs-site/zh/tutorials/write-experiment.mdx +355 -0
  210. package/docs-site/zh/{how-to → tutorials}/write-send.mdx +40 -40
  211. package/package.json +8 -4
  212. package/src/agents/bub.ts +21 -8
  213. package/src/agents/claude-code.ts +23 -10
  214. package/src/agents/codex.test.ts +22 -7
  215. package/src/agents/codex.ts +22 -9
  216. package/src/agents/index.ts +2 -2
  217. package/src/agents/openai-compat.ts +1 -1
  218. package/src/agents/post-setup.ts +45 -22
  219. package/src/agents/streaming.ts +1 -1
  220. package/src/agents/types.ts +20 -17
  221. package/src/agents/ui-message-stream.test.ts +10 -0
  222. package/src/agents/ui-message-stream.ts +12 -1
  223. package/src/cli.ts +77 -64
  224. package/src/context/types.ts +21 -21
  225. package/src/define.ts +13 -0
  226. package/src/i18n/en.ts +30 -15
  227. package/src/i18n/zh-CN.ts +30 -15
  228. package/src/index.ts +2 -0
  229. package/src/report/{react → assets}/colors.ts +22 -0
  230. package/src/report/{react → assets}/enhance.js +1 -33
  231. package/src/report/{react → assets}/styles.css +136 -112
  232. package/src/report/built-in/index.tsx +2 -2
  233. package/src/report/built-in/standard.tsx +18 -6
  234. package/src/report/components/attempt-detail/AttemptAssertions.tsx +67 -0
  235. package/src/report/components/attempt-detail/AttemptConversation.tsx +110 -0
  236. package/src/report/components/attempt-detail/AttemptDiagnostics.tsx +38 -0
  237. package/src/report/components/attempt-detail/AttemptDiff.tsx +35 -0
  238. package/src/report/components/attempt-detail/AttemptError.tsx +31 -0
  239. package/src/report/components/attempt-detail/AttemptFixPrompt.tsx +26 -0
  240. package/src/report/components/attempt-detail/AttemptSource.tsx +80 -0
  241. package/src/report/components/attempt-detail/AttemptSummary.tsx +74 -0
  242. package/src/report/components/attempt-detail/AttemptTimeline.tsx +108 -0
  243. package/src/report/components/attempt-detail/AttemptTrace.tsx +63 -0
  244. package/src/report/components/attempt-detail/AttemptUsage.tsx +29 -0
  245. package/src/report/components/attempt-detail/attempt-components.test.tsx +680 -0
  246. package/src/report/components/attempt-detail/compute.ts +287 -0
  247. package/src/report/components/attempt-detail/faces.ts +257 -0
  248. package/src/report/components/attempt-detail/index.tsx +583 -0
  249. package/src/report/components/attempt-detail/validate.test.ts +235 -0
  250. package/src/report/{react → components}/cell.tsx +6 -3
  251. package/src/report/{report.test.ts → components/compute.test.ts} +316 -46
  252. package/src/report/{react → components/entity-lists}/AttemptList.tsx +24 -16
  253. package/src/report/{react → components/entity-lists}/EvalList.tsx +0 -0
  254. package/src/report/{react → components/entity-lists}/ExperimentList.tsx +23 -20
  255. package/src/report/components/entity-lists/compute.ts +196 -0
  256. package/src/report/components/entity-lists/faces.ts +207 -0
  257. package/src/report/components/entity-lists/index.tsx +241 -0
  258. package/src/report/components/entity-lists/validate.test.ts +114 -0
  259. package/src/report/{react → components}/fixtures.ts +2 -2
  260. package/src/report/{react → components/metric-views}/DeltaTable.tsx +5 -5
  261. package/src/report/{react → components/metric-views}/MetricBars.tsx +5 -5
  262. package/src/report/{react → components/metric-views}/MetricLine.tsx +9 -6
  263. package/src/report/{react → components/metric-views}/MetricMatrix.tsx +6 -6
  264. package/src/report/{react → components/metric-views}/MetricScatter.tsx +41 -33
  265. package/src/report/{react → components/metric-views}/MetricTable.tsx +6 -6
  266. package/src/report/{react → components/metric-views}/Scoreboard.tsx +4 -4
  267. package/src/report/{compute.ts → components/metric-views/compute.ts} +51 -477
  268. package/src/report/components/metric-views/faces.ts +423 -0
  269. package/src/report/components/metric-views/index.tsx +352 -0
  270. package/src/report/{text → components/metric-views}/plot.ts +1 -1
  271. package/src/report/components/metric-views/validate.test.ts +211 -0
  272. package/src/report/{react → components}/render.test.tsx +47 -17
  273. package/src/report/components/shared-compute.ts +59 -0
  274. package/src/report/components/shared-faces.ts +30 -0
  275. package/src/report/components/shared.ts +192 -0
  276. package/src/report/{react → components/site-components}/CopyFixPrompt.tsx +3 -3
  277. package/src/report/{react → components/site-components}/HeroCard.tsx +3 -3
  278. package/src/report/{react → components/site-components}/ScopeWarnings.tsx +4 -4
  279. package/src/report/{react → components/site-components}/TraceWaterfall.tsx +13 -10
  280. package/src/report/components/site-components/compute.ts +144 -0
  281. package/src/report/components/site-components/faces.ts +80 -0
  282. package/src/report/components/site-components/index.tsx +265 -0
  283. package/src/report/{scope-warnings.ts → components/site-components/scope-warnings.ts} +3 -3
  284. package/src/report/{site-components.test.tsx → components/site-components/site-components.test.tsx} +23 -21
  285. package/src/report/components/site-components/validate.test.ts +89 -0
  286. package/src/report/{react → components/summaries}/ScopeSummary.tsx +26 -11
  287. package/src/report/components/summaries/compute.ts +56 -0
  288. package/src/report/components/summaries/faces.ts +48 -0
  289. package/src/report/components/summaries/index.tsx +127 -0
  290. package/src/report/components/summaries/validate.test.ts +49 -0
  291. package/src/report/definition/grid-layout.test.ts +124 -0
  292. package/src/report/definition/grid-layout.ts +146 -0
  293. package/src/report/{primitives.tsx → definition/primitives.tsx} +168 -18
  294. package/src/report/{report.ts → definition/report.ts} +98 -165
  295. package/src/report/{shell-head.test.ts → definition/shell-head.test.ts} +2 -2
  296. package/src/report/{text/table.ts → definition/table-text.ts} +4 -4
  297. package/src/report/{tree.ts → definition/tree.ts} +58 -22
  298. package/src/report/index.ts +126 -66
  299. package/src/report/{aggregate.ts → model/aggregate.ts} +31 -9
  300. package/src/report/{flag.ts → model/flag.ts} +39 -1
  301. package/src/report/{format.ts → model/format.ts} +59 -5
  302. package/src/report/{locale.ts → model/locale.ts} +34 -36
  303. package/src/report/{metrics.ts → model/metrics.ts} +3 -3
  304. package/src/report/{types.ts → model/types.ts} +143 -22
  305. package/src/report/react/index.tsx +52 -26
  306. package/src/report/{dual-render.test.tsx → runtime/dual-render.test.tsx} +564 -49
  307. package/src/report/runtime/host.test.ts +44 -0
  308. package/src/report/runtime/host.ts +138 -0
  309. package/src/report/{load.ts → runtime/load.ts} +1 -1
  310. package/src/report/runtime/text.ts +192 -0
  311. package/src/report/runtime/web.ts +106 -0
  312. package/src/results/attempt-evidence.ts +5 -1
  313. package/src/results/host-equivalence.test.ts +54 -3
  314. package/src/results/select.ts +30 -6
  315. package/src/runner/attempt.test.ts +48 -1
  316. package/src/runner/attempt.ts +62 -42
  317. package/src/runner/cleanup-timeout.test.ts +16 -0
  318. package/src/runner/cleanup-timeout.ts +23 -0
  319. package/src/runner/discover.ts +1 -2
  320. package/src/runner/eval-selection.test.ts +187 -0
  321. package/src/runner/eval-selection.ts +109 -0
  322. package/src/runner/experiment-cleanup-registry.test.ts +89 -0
  323. package/src/runner/experiment-cleanup-registry.ts +39 -0
  324. package/src/runner/experiment-labels.test.ts +71 -0
  325. package/src/runner/feedback/agent.test.ts +33 -1
  326. package/src/runner/feedback/agent.ts +12 -16
  327. package/src/runner/feedback/ci.test.ts +35 -1
  328. package/src/runner/feedback/ci.ts +16 -18
  329. package/src/runner/feedback/coordinator.ts +28 -0
  330. package/src/runner/feedback/human.test.ts +108 -1
  331. package/src/runner/feedback/human.ts +71 -21
  332. package/src/runner/feedback/reducer.test.ts +64 -0
  333. package/src/runner/feedback/reducer.ts +26 -0
  334. package/src/runner/feedback/sink.ts +40 -1
  335. package/src/runner/fingerprint.ts +2 -1
  336. package/src/runner/report.test.ts +14 -0
  337. package/src/runner/report.ts +3 -1
  338. package/src/runner/run.test.ts +390 -12
  339. package/src/runner/run.ts +259 -26
  340. package/src/runner/sandbox-selection.test.ts +13 -3
  341. package/src/runner/sandbox-selection.ts +4 -2
  342. package/src/runner/types.ts +158 -23
  343. package/src/sandbox/docker.ts +7 -0
  344. package/src/sandbox/e2b.ts +12 -13
  345. package/src/sandbox/types.ts +23 -21
  346. package/src/scoring/display.test.ts +28 -2
  347. package/src/scoring/display.ts +38 -5
  348. package/src/scoring/types.ts +3 -3
  349. package/src/shared/aggregate.test.ts +52 -0
  350. package/src/shared/aggregate.ts +30 -1
  351. package/src/shared/types.ts +6 -1
  352. package/src/show/command.test.ts +34 -0
  353. package/src/show/command.ts +16 -0
  354. package/src/show/compose.ts +1 -1
  355. package/src/show/index.ts +58 -22
  356. package/src/show/render.ts +16 -240
  357. package/src/show/show.test.ts +128 -35
  358. package/src/view/app/App.test.tsx +8 -7
  359. package/src/view/app/App.tsx +135 -51
  360. package/src/view/app/i18n.ts +7 -147
  361. package/src/view/app/lib/attempt-dialog.test.ts +65 -0
  362. package/src/view/app/lib/attempt-dialog.ts +69 -0
  363. package/src/view/app/main.tsx +0 -1
  364. package/src/view/app/types.ts +3 -86
  365. package/src/view/artifact-serving.test.ts +22 -38
  366. package/src/view/client-dist/app.css +1 -1
  367. package/src/view/client-dist/app.js +15 -22
  368. package/src/view/data.test.ts +48 -36
  369. package/src/view/data.ts +122 -89
  370. package/src/view/index.ts +3 -13
  371. package/src/view/server.ts +36 -6
  372. package/src/view/shared/types.ts +6 -31
  373. package/src/view/site-parity.test.ts +24 -1
  374. package/src/view/site.ts +144 -17
  375. package/src/view/styles.css +18 -806
  376. package/src/view/view-report.test.ts +182 -34
  377. package/dist/report/components.d.ts +0 -179
  378. package/dist/report/components.js +0 -544
  379. package/dist/report/compute.d.ts +0 -139
  380. package/dist/report/react/ExperimentComparison.d.ts +0 -10
  381. package/dist/report/react/ExperimentComparison.js +0 -12
  382. package/dist/report/react/MetricScatter.d.ts +0 -9
  383. package/dist/report/react/format.d.ts +0 -3
  384. package/dist/report/react/format.js +0 -7
  385. package/dist/report/text/faces.d.ts +0 -45
  386. package/dist/report/text/faces.js +0 -671
  387. package/dist/report/web.d.ts +0 -32
  388. package/dist/report/web.js +0 -48
  389. package/docs-site/zh/how-to/authoring.mdx +0 -162
  390. package/docs-site/zh/how-to/custom-reports.mdx +0 -414
  391. package/docs-site/zh/how-to/experiments.mdx +0 -86
  392. package/docs-site/zh/how-to/write-experiment.mdx +0 -164
  393. package/src/report/components.tsx +0 -925
  394. package/src/report/react/ExperimentComparison.tsx +0 -73
  395. package/src/report/react/format.ts +0 -9
  396. package/src/report/text/faces.ts +0 -767
  397. package/src/report/web.ts +0 -77
  398. package/src/show/report-host.test.ts +0 -205
  399. package/src/show/report-host.ts +0 -389
  400. package/src/view/app/components/AttemptModal.tsx +0 -496
  401. package/src/view/app/components/CodeView.test.tsx +0 -142
  402. package/src/view/app/components/CodeView.tsx +0 -310
  403. package/src/view/app/components/CopyControls.tsx +0 -106
  404. package/src/view/app/components/Trace.tsx +0 -100
  405. package/src/view/app/components/Transcript.tsx +0 -157
  406. package/src/view/app/components/ui/badge.tsx +0 -21
  407. package/src/view/app/lib/artifact-url.ts +0 -17
  408. package/src/view/app/lib/attempt-route.test.ts +0 -80
  409. package/src/view/app/lib/attempt-route.ts +0 -52
  410. package/src/view/app/lib/format.ts +0 -70
  411. package/src/view/app/lib/guards.test.ts +0 -108
  412. package/src/view/app/lib/guards.ts +0 -71
  413. package/src/view/app/lib/rows.ts +0 -22
  414. package/src/view/app/lib/transcript-data.tsx +0 -151
  415. package/src/view/app/lib/verdict.ts +0 -23
  416. package/src/view/app/shared.ts +0 -8
  417. /package/dist/report/{react → components/metric-views}/chart-math.d.ts +0 -0
  418. /package/dist/report/{react → components/metric-views}/chart-math.js +0 -0
  419. /package/dist/report/{text → components/metric-views}/plot.d.ts +0 -0
  420. /package/dist/report/{react → components/site-components}/PoweredBy.d.ts +0 -0
  421. /package/dist/report/{react → components/site-components}/PoweredBy.js +0 -0
  422. /package/dist/report/{text/layout.d.ts → model/text-layout.d.ts} +0 -0
  423. /package/dist/report/{text/layout.js → model/text-layout.js} +0 -0
  424. /package/dist/report/{types.js → model/types.js} +0 -0
  425. /package/src/report/{react → components/metric-views}/chart-math.test.ts +0 -0
  426. /package/src/report/{react → components/metric-views}/chart-math.ts +0 -0
  427. /package/src/report/{react → components/site-components}/PoweredBy.tsx +0 -0
  428. /package/src/report/{text/layout.ts → model/text-layout.ts} +0 -0
@@ -16,7 +16,7 @@ import type {
16
16
  Snapshot,
17
17
  } from "./types.ts";
18
18
  import type { ExperimentRunInfo, JsonValue } from "../types.ts";
19
- import { evalPrefixPredicate } from "../shared/aggregate.ts";
19
+ import { evalPrefixPredicate, matchExperimentSelector } from "../shared/aggregate.ts";
20
20
 
21
21
  /**
22
22
  * Results.latest() 的实现:每个实验取最新一次快照(= exp.snapshots[0]),生成挑选警告。
@@ -81,7 +81,7 @@ export function selectLatest(
81
81
 
82
82
  /** selectCurrentResults 的范围输入:experiment id 前缀与 eval id 前缀,都可缺省。 */
83
83
  export interface ResultScope {
84
- /** experiment id 前缀(--experiment),分段匹配语义同 filterExperiments。 */
84
+ /** experiment id 前缀(--exp),分段匹配语义同 filterExperiments。 */
85
85
  experiment?: string | string[];
86
86
  /** eval id 前缀(位置参数),收窄 Scope 覆盖的 eval;覆盖警告分母同步收窄到范围内。 */
87
87
  patterns?: string[];
@@ -136,6 +136,15 @@ export function deepEqualJson(a: unknown, b: unknown): boolean {
136
136
  return false;
137
137
  }
138
138
 
139
+ /**
140
+ * 一个快照实际选中的 eval id 全集:优先读落盘的 `experiment.selectedEvalIds`(niceeval 自己的
141
+ * 写入面必有此字段);第三方 harness 未实现该字段时退化为该快照实际写出的 `evals`,不把整份
142
+ * 来源排除在外(见 docs/feature/results/architecture.md「selectedEvalIds」)。
143
+ */
144
+ export function selectedEvalIdsOf(snapshot: Snapshot): readonly string[] {
145
+ return snapshot.experiment?.selectedEvalIds ?? snapshot.evals.map((ev) => ev.id);
146
+ }
147
+
139
148
  /**
140
149
  * 两个宿主(show / view)共用的现刻水位选择器:每个 experiment × eval 取「包含该 eval 的
141
150
  * 最新快照」里的全部 attempt,跨 run 合成。results.latest() 只挑「每实验最新快照」,带 eval
@@ -166,8 +175,12 @@ export function selectCurrentResults(results: Results, scope: ResultScope = {}):
166
175
  const taken = new Map<string, { ev: Eval; snapshot: Snapshot }>();
167
176
  for (const snapshot of exp.snapshots) {
168
177
  if (!deepEqualJson(comparabilityConfigOf(snapshot), baseline)) continue;
178
+ // 一个来源快照只贡献它自己选中的 eval——不在其 selectedEvalIds(或第三方退化后的实际
179
+ // evals)内的历史 attempt 不进入合成结果,即使它恰好出现在 snapshot.evals 里。第三方
180
+ // 无该字段时 selectedEvalIdsOf 退化为快照实际 evals,这里的过滤天然是 no-op。
181
+ const selectedIds = new Set(selectedEvalIdsOf(snapshot));
169
182
  for (const ev of snapshot.evals) {
170
- if (!match(ev.id) || taken.has(ev.id)) continue;
183
+ if (!selectedIds.has(ev.id) || !match(ev.id) || taken.has(ev.id)) continue;
171
184
  taken.set(ev.id, { ev, snapshot });
172
185
  }
173
186
  }
@@ -184,12 +197,17 @@ export function selectCurrentResults(results: Results, scope: ResultScope = {}):
184
197
  }
185
198
  const evals = picks.map((p) => p.ev);
186
199
  const base = exp.latest;
200
+ // 合成快照的 selectedEvalIds 重建为最终 picks 的有序 id 列表——不是照抄某一来源快照
201
+ // (通常是最新快照)的局部选择。base.experiment 缺失(第三方无 ExperimentRunInfo)时不
202
+ // 伪造一份不完整投影,继续靠 report 侧 fallback(见 selectedEvalIdsOf)。
203
+ const experiment =
204
+ base.experiment !== undefined ? { ...base.experiment, selectedEvalIds: evals.map((ev) => ev.id) } : undefined;
187
205
  snapshots.push({
188
206
  experimentId: exp.id,
189
207
  startedAt,
190
208
  agent: base.agent,
191
209
  ...(base.model !== undefined ? { model: base.model } : {}),
192
- ...(base.experiment !== undefined ? { experiment: base.experiment } : {}),
210
+ ...(experiment !== undefined ? { experiment } : {}),
193
211
  ...(base.name !== undefined ? { name: base.name } : {}),
194
212
  producer: base.producer,
195
213
  schemaVersion: base.schemaVersion,
@@ -368,12 +386,18 @@ export function isNewerSnapshot(a: Snapshot, b: Snapshot): boolean {
368
386
  return a.dir.localeCompare(b.dir) > 0;
369
387
  }
370
388
 
371
- /** experiment id 分段前缀过滤(--experiment / latest({ experiments }) 同一语义);包内使用,不进公共 barrel。 */
389
+ /**
390
+ * experiment 选择器过滤(--exp / latest({ experiments }) 同一语义,与 `niceeval exp` 位置参数
391
+ * 共用 matchExperimentSelector,见 docs/feature/experiments/cli.md「实验选择器怎样解析」);
392
+ * 包内使用,不进公共 barrel。
393
+ */
372
394
  export function filterExperiments(experiments: Experiment[], filter?: string | string[]): Experiment[] {
373
395
  if (filter === undefined) return experiments;
374
396
  // 允许 "compare/" 这种带尾斜杠的写法,与 "compare" 等价;分段匹配不误配 "compare2"。
375
397
  const prefixes = (Array.isArray(filter) ? filter : [filter]).map((p) => p.replace(/\/+$/, ""));
376
- return experiments.filter((exp) => prefixes.some((p) => exp.id === p || exp.id.startsWith(p + "/")));
398
+ const ids = experiments.map((exp) => exp.id);
399
+ const matched = new Set(prefixes.flatMap((p) => matchExperimentSelector(ids, p)));
400
+ return experiments.filter((exp) => matched.has(exp.id));
377
401
  }
378
402
 
379
403
  /**
@@ -100,7 +100,7 @@ async function runOnce(
100
100
  // 自定义 provider:create() 直接返回内存 fake,绕开真实沙箱 provider。
101
101
  sandbox: defineSandbox({ name: "fake-provider", create: async () => asSandbox(box) }),
102
102
  timeoutMs: 5_000,
103
- evalFilter: () => true,
103
+ selectedEvalIds: [evalDef.id],
104
104
  };
105
105
  const attempt: Attempt = { evalDef, run, attempt: 0, key: "fake/eval", fingerprint: "" };
106
106
  const config: Config = {};
@@ -275,6 +275,53 @@ describe("runAttemptEffect · onPhase 回调随 enterPhase 同步触发", () =>
275
275
  });
276
276
  });
277
277
 
278
+ // eval.teardown 的触发条件是「eval.setup 时点走到过」,不是「setup 声明且成功」(成对触发规则,
279
+ // 见 docs/runner.md「环境预置不进运行器,但按顺序调它」)。时点在 attempt.ts 里于调用
280
+ // evalDef.setup 之前就置位,所以 setup 抛错、乃至压根没声明 setup,都不豁免 teardown。
281
+ describe("runAttemptEffect · eval.teardown 的触发规则", () => {
282
+ it("eval.setup 抛错时,eval.teardown 仍被调用(半初始化现场同样要扫尾)", async () => {
283
+ const agent = defineSandboxAgent({
284
+ name: "fake-agent-eval-setup-throws",
285
+ send: async () => ({ events: [], status: "completed" }),
286
+ });
287
+ let teardownCalls = 0;
288
+ const box = new FakeSandbox();
289
+ const result = await runOnce(agent, box, {
290
+ evalDefOverrides: {
291
+ setup: async () => {
292
+ throw new Error("boom-from-eval-setup");
293
+ },
294
+ teardown: async () => {
295
+ teardownCalls += 1;
296
+ },
297
+ },
298
+ });
299
+
300
+ expect(result.error?.message).toContain("boom-from-eval-setup");
301
+ expect(result.error?.phase).toBe("eval.setup");
302
+ expect(teardownCalls).toBe(1);
303
+ });
304
+
305
+ it("未声明 eval.setup 时,eval.teardown 依然触发(时点走到不依赖 setup 是否声明)", async () => {
306
+ const agent = defineSandboxAgent({
307
+ name: "fake-agent-no-eval-setup",
308
+ send: async () => ({ events: [], status: "completed" }),
309
+ });
310
+ let teardownCalls = 0;
311
+ const box = new FakeSandbox();
312
+ const result = await runOnce(agent, box, {
313
+ evalDefOverrides: {
314
+ teardown: async () => {
315
+ teardownCalls += 1;
316
+ },
317
+ },
318
+ });
319
+
320
+ expect(result.error).toBeUndefined();
321
+ expect(teardownCalls).toBe(1);
322
+ });
323
+ });
324
+
278
325
  describe("runAttemptEffect · 主链与 Scope 收尾的计时边界", () => {
279
326
  it("sandbox.stop 只计入收尾,主链 phase 合计不超过 durationMs", async () => {
280
327
  const agent = defineSandboxAgent({
@@ -8,6 +8,7 @@ import { readFile as readSourceFile } from "node:fs/promises";
8
8
  import { Effect, Cause, Duration } from "effect";
9
9
  import { createSandbox, resolveSandbox } from "../sandbox/resolve.ts";
10
10
  import { stopSandbox, unregisterSandbox } from "../sandbox/registry.ts";
11
+ import { withCleanupTimeout } from "./cleanup-timeout.ts";
11
12
  import { KEEPABLE_PROVIDERS, nativeEnterCommand, suspendSandbox } from "../sandbox/keep.ts";
12
13
  import { keptEntryId, updateKeptEntry, writeKeptEntry } from "../sandbox/keep-registry.ts";
13
14
  import { createTraceReceiver, type TraceReceiver } from "../o11y/otlp/receiver.ts";
@@ -31,7 +32,6 @@ import type { CapturedEvalSource } from "./eval-source.ts";
31
32
  import type {
32
33
  AgentContext,
33
34
  AgentSetupManifest,
34
- Cleanup,
35
35
  Config,
36
36
  DiagnosticInput,
37
37
  DiffArtifact,
@@ -456,7 +456,7 @@ export function runAttemptEffect(
456
456
  * 生命周期阶段(极早期就挂、还没跨进任何阶段时兜底 `eval.run`——phase 是必填字段,不留空);
457
457
  * code 目前只对确定已知的类别赋稳定码,其余走 `"unexpected-error"`——provider 专属的限流码
458
458
  * 分类留在各 provider 的 `classifyProvisionError`,没有中性入口能在这里复算,不猜一个可能错的码。 */
459
- function errorFromThrown(e: unknown, phase: LifecyclePhase | undefined): AttemptError {
459
+ export function errorFromThrown(e: unknown, phase: LifecyclePhase | undefined): AttemptError {
460
460
  const { message, stack, cause } = describeError(e);
461
461
  return {
462
462
  code: "unexpected-error",
@@ -551,22 +551,21 @@ async function runAttemptBody(
551
551
  progress: feedback.progress,
552
552
  diagnostic: feedback.diagnostic,
553
553
  };
554
- let agentCleanup: Cleanup | void = undefined;
555
554
  let agentDidSetup = false;
555
+ /** agent.setup 时点已走到(未声明 setup 也置位)——agent.teardown 的触发条件(成对触发规则)。 */
556
+ let agentSetupReached = false;
556
557
  /** agent.setup 写进沙箱的安装清单(装了 Skill / plugin / MCP 的沙箱型 adapter 才有)。 */
557
558
  let agentSetup: AgentSetupManifest | undefined;
558
- // EvalDef.setup() 返回的 cleanup 闭包;finally 里按 LIFO 跑(见下)。
559
- let evalCleanup: Cleanup | void = undefined;
559
+ /** eval.setup 时点已走到(分类账锚点之后;未声明 setup 也置位)——eval.teardown 的触发条件。 */
560
+ let evalSetupReached = false;
560
561
  // 变更分类账(仅沙箱型;workspace.baseline 阶段建立)。
561
562
  let ledger: ChangeLedger | undefined;
562
- // SandboxSpec.setup() 返回的 cleanup 闭包,按调用顺序收集;finally 里 LIFO 跑(见下)。
563
- const sandboxCleanups: Cleanup[] = [];
564
563
  try {
565
564
  if (usesSandbox) {
566
565
  // 沙箱级生命周期钩子(SandboxSpec.setup):环境预置层,先于 workspace 上传 / git 基线 /
567
566
  // eval.setup 跑——改动进 git 基线,不会被误算进 agent 产出的 diff。按追加顺序依次执行;
568
- // 单个抛错走下面的执行错误路径(与 eval.setup / agent.setup 同一条),已跑过的 cleanup /
569
- // sandbox.teardown 钩子仍在 finally 里跑(见 catch/finally)
567
+ // 单个抛错走下面的执行错误路径(与 eval.setup / agent.setup 同一条),后续 setup 不再执行,
568
+ // sandbox.teardown 钩子链仍在 finally 里完整跑(见 catch/finally)——半初始化的沙箱同样要扫尾。
570
569
  if (sandboxSetupHooks.length > 0) {
571
570
  enterPhase("sandbox.setup");
572
571
  log(t("runner.startSandboxSetup"));
@@ -582,8 +581,17 @@ async function runAttemptBody(
582
581
  });
583
582
  if (hookNode) recorder.pushParent(hookNode);
584
583
  try {
585
- const cleanup = await hook(sandbox, hookCtx);
586
- if (typeof cleanup === "function") sandboxCleanups.push(cleanup);
584
+ // 迁移护栏:tsx 用户没有类型检查,旧式「setup 返回 cleanup」会被静默忽略——命中即报
585
+ // 清晰错误(attempt errored),指向成对 teardown 写法。下方 eval.setup / agent.setup 同。
586
+ const returned = (await hook(sandbox, hookCtx)) as unknown;
587
+ if (typeof returned === "function") {
588
+ throw new Error(
589
+ t("runner.setupReturnedCleanup", {
590
+ layer: `SandboxSpec.setup() hook #${i}`,
591
+ hint: "SandboxSpec.teardown(fn)",
592
+ }).trimEnd(),
593
+ );
594
+ }
587
595
  } catch (e) {
588
596
  if (hookNode) hookNode.failed = true;
589
597
  throw e;
@@ -600,20 +608,41 @@ async function runAttemptBody(
600
608
  ledger = await createChangeLedger(sandbox, evalDef.diff);
601
609
 
602
610
  // eval 级 setup(starter prep:npm install / 装系统依赖等)。命令默认非 root;
603
- // setup 里需要 root 的(apt/pip)自己传 { root: true }
611
+ // setup 里需要 root 的(apt/pip)自己传 { root: true }。时点在此走到——
612
+ // teardown 的触发条件是时点,不是「setup 声明且成功」(成对触发规则,见
613
+ // docs/runner.md「环境预置不进运行器,但按顺序调它」)。
614
+ evalSetupReached = true;
604
615
  if (evalDef.setup) {
605
616
  enterPhase("eval.setup");
606
617
  log(t("runner.evalSetup"));
607
- evalCleanup = await evalDef.setup(withEvalLocalPaths(sandbox, evalDef.baseDir), hookCtx);
618
+ const returned = (await evalDef.setup(withEvalLocalPaths(sandbox, evalDef.baseDir), hookCtx)) as unknown;
619
+ if (typeof returned === "function") {
620
+ throw new Error(
621
+ t("runner.setupReturnedCleanup", {
622
+ layer: `EvalDef.setup (${evalDef.id})`,
623
+ hint: "EvalDef.teardown",
624
+ }).trimEnd(),
625
+ );
626
+ }
608
627
  }
609
628
  }
610
629
 
611
630
  // agent 自己的 lifecycle:装 CLI、写 config(每个沙箱一次,不在每轮 send 里)。
631
+ // 时点在此走到(未声明 setup 也算)——agent.teardown 据此触发。
632
+ agentSetupReached = true;
612
633
  if (run.agent.setup) {
613
634
  enterPhase("agent.setup");
614
635
  log(t("runner.startAgentSetup"));
615
636
  agentDidSetup = true;
616
- agentCleanup = await run.agent.setup(sandbox, attemptCtx);
637
+ const returned = (await run.agent.setup(sandbox, attemptCtx)) as unknown;
638
+ if (typeof returned === "function") {
639
+ throw new Error(
640
+ t("runner.setupReturnedCleanup", {
641
+ layer: `Agent.setup (${run.agent.name})`,
642
+ hint: "Agent.teardown",
643
+ }).trimEnd(),
644
+ );
645
+ }
617
646
  }
618
647
 
619
648
  // 安装 manifest:adapter 在 setup 收尾写进沙箱的固定路径,核心只把它抬成 attempt artifact
@@ -856,14 +885,17 @@ async function runAttemptBody(
856
885
  // (不计入 durationMs 口径,见 docs/feature/results/architecture.md)。执行序与 LifecyclePhase
857
886
  // 闭集声明一致:eval.teardown → agent.teardown → sandbox.teardown;各段可独立标 failed。
858
887
  // 沙箱 stop / 接收器 close 不在这里 —— 由 runAttemptEffect 的 Scope 在本函数返回后回收,
859
- // 并经 finalizer 计成 sandbox.stop。没有对应 cleanup 的段直接跳过,不产生空阶段。
860
- const evalCleanupFn = typeof evalCleanup === "function" ? evalCleanup : undefined;
861
- if (evalCleanupFn) {
888
+ // 并经 finalizer 计成 sandbox.stop。触发规则统一是「同层 setup 时点走到过」(setup / test
889
+ // 抛错不豁免,见 docs/runner.md「环境预置不进运行器,但按顺序调它」);没有对应 teardown
890
+ // 的段直接跳过,不产生空阶段。
891
+ if (evalSetupReached && evalDef.teardown) {
862
892
  enterPhase("eval.teardown");
863
893
  await recorder
864
894
  .measureClosing("eval.teardown", async () => {
865
895
  try {
866
- await evalCleanupFn();
896
+ // 收尾可调用体一律有界(docs/cli.md「中断:三级响应」的有界性前提):挂起的 teardown
897
+ // 到点按本段失败语义收束,后续段照常执行,收尾链不能无限拖住退出。下同。
898
+ await withCleanupTimeout(() => evalDef.teardown!(withEvalLocalPaths(sandbox, evalDef.baseDir), hookCtx));
867
899
  } catch (e) {
868
900
  // 收尾失败只是 diagnostic,不改判定 —— 挂到 attempt.diagnostics(见 finally 末尾并入)。
869
901
  diagnostics.push(teardownDiagnostic("eval.teardown", e));
@@ -872,14 +904,12 @@ async function runAttemptBody(
872
904
  })
873
905
  .catch(() => {});
874
906
  }
875
- const agentCleanupFn = typeof agentCleanup === "function" ? agentCleanup : undefined;
876
- if (agentCleanupFn !== undefined || (agentDidSetup && run.agent.teardown !== undefined)) {
907
+ if (agentSetupReached && run.agent.teardown) {
877
908
  enterPhase("agent.teardown");
878
909
  await recorder
879
910
  .measureClosing("agent.teardown", async () => {
880
911
  try {
881
- if (agentCleanupFn) await agentCleanupFn();
882
- if (agentDidSetup) await run.agent.teardown?.(sandbox, attemptCtx);
912
+ await withCleanupTimeout(() => run.agent.teardown!(sandbox, attemptCtx));
883
913
  } catch (e) {
884
914
  diagnostics.push(teardownDiagnostic("agent.teardown", e));
885
915
  throw e;
@@ -887,33 +917,22 @@ async function runAttemptBody(
887
917
  })
888
918
  .catch(() => {});
889
919
  }
890
- if (usesSandbox && (sandboxCleanups.length > 0 || sandboxTeardownHooks.length > 0)) {
920
+ if (usesSandbox && sandboxTeardownHooks.length > 0) {
891
921
  enterPhase("sandbox.teardown");
892
922
  await recorder
893
923
  .measureClosing("sandbox.teardown", async () => {
894
924
  const before = diagnostics.length;
895
- // sandbox.setup 返回的 cleanup:LIFO(后 setup 先 cleanup)。
896
- for (let i = sandboxCleanups.length - 1; i >= 0; i--) {
925
+ // sandbox.teardown 钩子:按追加的逆序执行(LIFO),沙箱销毁前最后一步。setup 链中途
926
+ // 抛错不影响本链完整走完——半初始化的沙箱同样要扫尾,钩子自己对未初始化状态防御。
927
+ // 逐钩子有界:一个挂起的钩子不阻塞链上其余钩子拿到执行机会。
928
+ log(t("runner.startSandboxTeardown"));
929
+ for (let i = sandboxTeardownHooks.length - 1; i >= 0; i--) {
897
930
  try {
898
- await sandboxCleanups[i]();
931
+ await withCleanupTimeout(() => sandboxTeardownHooks[i](sandbox, hookCtx));
899
932
  } catch (e) {
900
933
  diagnostics.push(teardownDiagnostic("sandbox.teardown", e));
901
934
  }
902
935
  }
903
- // sandbox.teardown 钩子:按追加的逆序执行,沙箱销毁前最后一步。
904
- if (sandboxTeardownHooks.length > 0) {
905
- log(t("runner.startSandboxTeardown"));
906
- for (let i = sandboxTeardownHooks.length - 1; i >= 0; i--) {
907
- try {
908
- const teardownCleanup = await sandboxTeardownHooks[i](sandbox, hookCtx);
909
- // SandboxHook 类型允许返回 Cleanup(与 setup 同一签名);teardown 返回的
910
- // cleanup 没有更晚的挂点,立即执行。
911
- if (typeof teardownCleanup === "function") await teardownCleanup();
912
- } catch (e) {
913
- diagnostics.push(teardownDiagnostic("sandbox.teardown", e));
914
- }
915
- }
916
- }
917
936
  if (diagnostics.length > before) throw new Error("sandbox teardown diagnostics");
918
937
  })
919
938
  .catch(() => {});
@@ -1017,17 +1036,18 @@ async function collectSources(
1017
1036
 
1018
1037
  /** 解析后运行配置的穷尽投影(ExperimentRunInfo,见 docs/feature/results/architecture.md):
1019
1038
  * agent/model 只在快照顶层,这里不复制;sandbox 只经 provider 的公开参数投影落盘。 */
1020
- function experimentRunInfo(run: AgentRun, configSandbox?: Config["sandbox"]): EvalResult["experiment"] {
1039
+ export function experimentRunInfo(run: AgentRun, configSandbox?: Config["sandbox"]): EvalResult["experiment"] {
1021
1040
  return {
1022
1041
  ...(run.description !== undefined ? { description: run.description } : {}),
1023
1042
  ...(run.reasoningEffort !== undefined ? { reasoningEffort: run.reasoningEffort } : {}),
1024
1043
  ...(Object.keys(run.flags).length > 0 ? { flags: run.flags } : {}),
1044
+ ...(run.labels !== undefined && Object.keys(run.labels).length > 0 ? { labels: run.labels } : {}),
1025
1045
  runs: run.runs,
1026
1046
  earlyExit: run.earlyExit,
1027
1047
  ...(run.timeoutMs !== undefined ? { timeoutMs: run.timeoutMs } : {}),
1028
1048
  ...(run.budget !== undefined ? { budget: run.budget } : {}),
1029
1049
  ...(run.maxConcurrency !== undefined ? { maxConcurrency: run.maxConcurrency } : {}),
1030
- selectedEvalIds: run.selectedEvalIds ?? [],
1050
+ selectedEvalIds: [...run.selectedEvalIds],
1031
1051
  ...(run.evalFilterFingerprint !== undefined ? { evalFilterFingerprint: run.evalFilterFingerprint } : {}),
1032
1052
  ...sandboxProjection(run, configSandbox),
1033
1053
  };
@@ -0,0 +1,16 @@
1
+ // cases: docs/engineering/unit-tests/experiments-runner/cases.md
2
+ // (「cleanup 执行有界」行;sandbox/cases.md 的「收尾链每个可调用体各有清理超时」行同样由本文件证明机制)
3
+ // bug: memory/force-exit-skips-experiment-teardown.md
4
+ import { describe, expect, it } from "vitest";
5
+ import { withCleanupTimeout } from "./cleanup-timeout.ts";
6
+
7
+ describe("withCleanupTimeout", () => {
8
+ it("挂起的可调用体到点抛超时错误(消息含超时毫秒数)", async () => {
9
+ await expect(withCleanupTimeout(() => new Promise<never>(() => {}), 20)).rejects.toThrow(/20ms/);
10
+ });
11
+
12
+ it("按时完成的可调用体原样透传返回值;抛错原样透传,不被包装", async () => {
13
+ await expect(withCleanupTimeout(async () => "ok", 1_000)).resolves.toBe("ok");
14
+ await expect(withCleanupTimeout(() => Promise.reject(new Error("boom")), 1_000)).rejects.toThrow("boom");
15
+ });
16
+ });
@@ -0,0 +1,23 @@
1
+ // 收尾可调用体的有界执行:eval/agent/sandbox 各段的 cleanup、钩子与实验级 cleanup 共用。
2
+ // 有界性是「强清 = 加速收尾」设计的前提(docs/cli.md「中断:三级响应」):任何一个挂起的收尾
3
+ // 都不能无限拖住退出,到点按该段自己的失败语义收束(teardown-failed / experiment-teardown-failed
4
+ // 诊断),超时后遗留的 promise 悬空,随进程退出消亡。
5
+
6
+ import { t } from "../i18n/index.ts";
7
+
8
+ /** 单个收尾可调用体的清理超时(docs 声明值,provider stop 另有自己的 8s 超时)。 */
9
+ export const CLEANUP_TIMEOUT_MS = 30_000;
10
+
11
+ export async function withCleanupTimeout<T>(fn: () => Promise<T> | T, timeoutMs = CLEANUP_TIMEOUT_MS): Promise<T> {
12
+ let timer: ReturnType<typeof setTimeout> | undefined;
13
+ try {
14
+ return await Promise.race([
15
+ Promise.resolve().then(fn),
16
+ new Promise<never>((_, reject) => {
17
+ timer = setTimeout(() => reject(new Error(t("runner.cleanupTimeout", { timeoutMs }))), timeoutMs);
18
+ }),
19
+ ]);
20
+ } finally {
21
+ if (timer) clearTimeout(timer);
22
+ }
23
+ }
@@ -103,8 +103,7 @@ export async function discoverExperiments(root: string): Promise<DiscoveredExper
103
103
  .replace(/\.experiment$/, "")
104
104
  .split(sep)
105
105
  .join("/");
106
- const group = id.includes("/") ? id.split("/")[0]! : "";
107
- out.push({ ...def, id, group });
106
+ out.push({ ...def, id });
108
107
  }
109
108
  return out;
110
109
  }
@@ -0,0 +1,187 @@
1
+ // cases: docs/engineering/unit-tests/experiments-runner/cases.md
2
+ // 纯选题边界:EvalDescriptor 投影 + resolveExperimentEvals 求值(定稿见
3
+ // docs/feature/eval/library.md「EvalDescriptor」、docs/feature/experiments/library.md「evals」)。
4
+
5
+ import { describe, expect, it } from "vitest";
6
+ import { evalDescriptorOf, resolveExperimentEvals, selectedEvalsForRun } from "./eval-selection.ts";
7
+ import type { DiscoveredEval, EvalDescriptor } from "./types.ts";
8
+
9
+ const source = { path: "evals/fake.eval.ts", content: "export default { test() {} };\n", sha256: "fake" };
10
+
11
+ function makeEval(id: string, overrides: Partial<DiscoveredEval> = {}): DiscoveredEval {
12
+ return {
13
+ id,
14
+ baseDir: "/project/evals",
15
+ sourcePath: `/project/evals/${id}.eval.ts`,
16
+ source,
17
+ test() {},
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ describe("evalDescriptorOf", () => {
23
+ it("显式白名单投影:不暴露 sourcePath / baseDir / test 等内部字段", () => {
24
+ const evalDef = makeEval("coding/fix-button", {
25
+ description: "fix the button",
26
+ tags: ["coding", "frontend"],
27
+ environment: "node-22",
28
+ metadata: { owner: "team-a" },
29
+ });
30
+ const descriptor = evalDescriptorOf(evalDef);
31
+ expect(descriptor).toEqual({
32
+ id: "coding/fix-button",
33
+ description: "fix the button",
34
+ tags: ["coding", "frontend"],
35
+ environment: "node-22",
36
+ metadata: { owner: "team-a" },
37
+ });
38
+ expect(descriptor).not.toHaveProperty("sourcePath");
39
+ expect(descriptor).not.toHaveProperty("baseDir");
40
+ expect(descriptor).not.toHaveProperty("source");
41
+ expect(descriptor).not.toHaveProperty("test");
42
+ });
43
+
44
+ it("未声明 tags 得到只读空数组;冻结,mutation 不影响下一次读取", () => {
45
+ const evalDef = makeEval("bare/eval");
46
+ const d1 = evalDescriptorOf(evalDef);
47
+ expect(d1.tags).toEqual([]);
48
+ expect(Object.isFrozen(d1)).toBe(true);
49
+ expect(Object.isFrozen(d1.tags)).toBe(true);
50
+
51
+ // 每次投影都是新对象,一次调用拿到的引用被 mutate(TS 层本应拒绝,这里模拟无类型 JS)
52
+ // 不会影响下一次对同一 evalDef 的投影。
53
+ const mutable = d1 as unknown as { tags: string[] };
54
+ expect(() => mutable.tags.push("x")).toThrow(); // 冻结数组:push 直接抛错
55
+ const d2 = evalDescriptorOf(evalDef);
56
+ expect(d2.tags).toEqual([]);
57
+ });
58
+ });
59
+
60
+ describe("resolveExperimentEvals", () => {
61
+ const codingFixButton = makeEval("coding/fix-button", { tags: ["coding", "frontend"], environment: "node-22" });
62
+ const researchGpu = makeEval("research/gpu-literature", { tags: ["research"], environment: "gpu" });
63
+ const evals = [codingFixButton, researchGpu];
64
+
65
+ it("谓词同时可读 id / tags / environment / metadata,只命中匹配的 eval", () => {
66
+ const { selectedEvalIds } = resolveExperimentEvals({
67
+ experimentId: "exp/coding-only",
68
+ selector: (e) => e.id.startsWith("coding/") && e.tags.includes("coding") && e.environment !== "gpu",
69
+ cliPatterns: [],
70
+ evals,
71
+ });
72
+ expect(selectedEvalIds).toEqual(["coding/fix-button"]);
73
+ });
74
+
75
+ it('"*" 或省略 selector 全选;数组按裸前缀;CLI patterns 与 selector 取交集', () => {
76
+ expect(
77
+ resolveExperimentEvals({ experimentId: "e", selector: "*", cliPatterns: [], evals }).selectedEvalIds,
78
+ ).toEqual(["coding/fix-button", "research/gpu-literature"]);
79
+ expect(
80
+ resolveExperimentEvals({ experimentId: "e", selector: undefined, cliPatterns: [], evals }).selectedEvalIds,
81
+ ).toEqual(["coding/fix-button", "research/gpu-literature"]);
82
+ expect(
83
+ resolveExperimentEvals({ experimentId: "e", selector: ["coding/"], cliPatterns: [], evals }).selectedEvalIds,
84
+ ).toEqual(["coding/fix-button"]);
85
+ // selector 选中两条,CLI 追加前缀再收窄到一条:两层是交集。
86
+ expect(
87
+ resolveExperimentEvals({ experimentId: "e", selector: "*", cliPatterns: ["research/"], evals }).selectedEvalIds,
88
+ ).toEqual(["research/gpu-literature"]);
89
+ });
90
+
91
+ it("零命中时 selectedEvalIds 为空(不报错)", () => {
92
+ const { selectedEvals, selectedEvalIds } = resolveExperimentEvals({
93
+ experimentId: "e",
94
+ selector: () => false,
95
+ cliPatterns: [],
96
+ evals,
97
+ });
98
+ expect(selectedEvals).toEqual([]);
99
+ expect(selectedEvalIds).toEqual([]);
100
+ });
101
+
102
+ it("返回顺序 = discovery 稳定顺序,不随谓词命中顺序改变;id 去重", () => {
103
+ const a = makeEval("z/a");
104
+ const b = makeEval("a/b");
105
+ const { selectedEvalIds } = resolveExperimentEvals({
106
+ experimentId: "e",
107
+ selector: "*",
108
+ cliPatterns: [],
109
+ evals: [a, b],
110
+ });
111
+ expect(selectedEvalIds).toEqual(["z/a", "a/b"]); // discovery 顺序,不是字典序
112
+ });
113
+
114
+ it("每个候选 eval 只求值谓词恰好一次", () => {
115
+ let calls = 0;
116
+ resolveExperimentEvals({
117
+ experimentId: "e",
118
+ selector: () => {
119
+ calls += 1;
120
+ return true;
121
+ },
122
+ cliPatterns: [],
123
+ evals,
124
+ });
125
+ expect(calls).toBe(evals.length);
126
+ });
127
+
128
+ it("谓词返回非 boolean:报错携带 experiment id 与 eval id", () => {
129
+ expect(() =>
130
+ resolveExperimentEvals({
131
+ experimentId: "exp/bad-return",
132
+ selector: () => "yes" as unknown as boolean,
133
+ cliPatterns: [],
134
+ evals: [codingFixButton],
135
+ }),
136
+ ).toThrow(/exp\/bad-return.*coding\/fix-button/s);
137
+ });
138
+
139
+ it("谓词返回 Promise:不按 truthy 接受,报错说明必须同步", () => {
140
+ expect(() =>
141
+ resolveExperimentEvals({
142
+ experimentId: "exp/async",
143
+ selector: () => Promise.resolve(true) as unknown as boolean,
144
+ cliPatterns: [],
145
+ evals: [codingFixButton],
146
+ }),
147
+ ).toThrow(/Promise/);
148
+ });
149
+
150
+ it("谓词抛错:错误携带 experiment id + eval id,并保留原 cause", () => {
151
+ const original = new Error("boom");
152
+ let thrown: unknown;
153
+ try {
154
+ resolveExperimentEvals({
155
+ experimentId: "exp/throws",
156
+ selector: (): boolean => {
157
+ throw original;
158
+ },
159
+ cliPatterns: [],
160
+ evals: [codingFixButton],
161
+ });
162
+ } catch (e) {
163
+ thrown = e;
164
+ }
165
+ expect(thrown).toBeInstanceOf(Error);
166
+ expect((thrown as Error).message).toMatch(/exp\/throws/);
167
+ expect((thrown as Error).message).toMatch(/coding\/fix-button/);
168
+ expect((thrown as Error).cause).toBe(original);
169
+ });
170
+ });
171
+
172
+ describe("selectedEvalsForRun", () => {
173
+ it("按已解析的 selectedEvalIds 取 eval,保持 discovery 顺序", () => {
174
+ const a = makeEval("a");
175
+ const b = makeEval("b");
176
+ const c = makeEval("c");
177
+ const picked = selectedEvalsForRun([a, b, c], { selectedEvalIds: ["c", "a"] });
178
+ expect(picked.map((e) => e.id)).toEqual(["a", "c"]); // 顺序随 all,不随 selectedEvalIds 声明序
179
+ });
180
+ });
181
+
182
+ describe("EvalDescriptor 类型可从公开入口导入", () => {
183
+ it("类型层守卫:占位断言(真正的推断检查在 pnpm typecheck)", () => {
184
+ const descriptor: EvalDescriptor = { id: "x", tags: [] };
185
+ expect(descriptor.id).toBe("x");
186
+ });
187
+ });