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
@@ -12,6 +12,7 @@ import { createFeedbackCoordinator, type FeedbackCoordinator } from "./coordinat
12
12
  import { createFakeFeedbackIO, type FakeFeedbackIO } from "./testing.ts";
13
13
  import { activeFeedbackSinkCount } from "./sink.ts";
14
14
  import { createHumanRenderer, formatElapsed, formatTokenCount, renderDurableLines } from "./human.ts";
15
+ import { t } from "../../i18n/index.ts";
15
16
  import { createInitialRunFeedbackState } from "./reducer.ts";
16
17
  import type { AttemptLocator } from "../../results/locator.ts";
17
18
  import type { AttemptRef, RunCompletion, RunFeedbackPlan, RunSummary } from "../types.ts";
@@ -356,7 +357,7 @@ describe("完成页:失败优先摘要 + locator + show/view 下一步 + 快照
356
357
  expect(text).toContain("Eval: niceeval show @17m2k9p --source");
357
358
  expect(text).toContain("Trace: niceeval show @17m2k9p --execution");
358
359
  expect(text).toContain("Diff: niceeval show @17m2k9p --diff");
359
- expect(text).toContain("Compare: niceeval view compare");
360
+ expect(text).toContain("Compare: niceeval view");
360
361
  expect(text).toContain(".niceeval/compare/bub-e2b/2026-07-13T000000");
361
362
  // 不再是 renderRunReport() 的大表(表格标志性的竖线分隔表头在这里不应出现)。
362
363
  expect(text).not.toMatch(/-\+-/);
@@ -614,3 +615,109 @@ describe("renderDurableLines: 纯函数,不需要经过 coordinator 也能验证
614
615
  expect(lines.join("\n")).toContain("interrupted");
615
616
  });
616
617
  });
618
+
619
+ // ───────────────────────── 实验级钩子:运行级行 / 起止行(cases.md「实验级生命周期」) ─────────────────────────
620
+
621
+ describe("TTY dashboard: 实验级钩子的运行级行", () => {
622
+ it("setup started 后 ACTIVE 区出现运行级行,progress 只更新该行 detail,done 后行消失且不写 scrollback", async () => {
623
+ const { fake, coordinator } = setupTty();
624
+ coordinator.start(plan({ totalRuns: 2, evals: 2 }));
625
+ coordinator.experimentHook({ experimentId: "compare/bub-e2b", hook: "setup", status: "started" });
626
+ fake.advance(1000);
627
+ await flush();
628
+ const rowLabel = `${t("feedback.phase.experimentSetup")} · compare/bub-e2b`;
629
+ expect(fake.stderr.writes.join("")).toContain(rowLabel);
630
+
631
+ coordinator.experimentProgress({ experimentId: "compare/bub-e2b", detail: "starting tunnel (2/5)" });
632
+ fake.advance(1000);
633
+ await flush();
634
+ expect(fake.stderr.writes.join("")).toContain("starting tunnel (2/5)");
635
+
636
+ const before = fake.stderr.writes.length;
637
+ coordinator.experimentHook({ experimentId: "compare/bub-e2b", hook: "setup", status: "done", durationMs: 42_000 });
638
+ fake.advance(1000);
639
+ await flush();
640
+ // done 之后的帧不再含运行级行;成功钩子也不追加任何 scrollback 永久行。
641
+ expect(fake.stderr.writes.slice(before).join("")).not.toContain(t("feedback.phase.experimentSetup"));
642
+ expect(fake.stderr.writes.join("")).not.toContain(t("feedback.human.hookDone"));
643
+ await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
644
+ });
645
+
646
+ it("运行级行排在 attempt 行前面", async () => {
647
+ const { fake, coordinator } = setupTty();
648
+ coordinator.start(plan({ totalRuns: 2, evals: 2 }));
649
+ coordinator.emit({ type: "attempt:start", at: 0, identity: ref("memory/a"), who: "bub-e2b", phase: "eval.run" });
650
+ coordinator.experimentHook({ experimentId: "compare/bub-e2b", hook: "setup", status: "started" });
651
+ fake.advance(1000);
652
+ await flush();
653
+ const lastFrame = fake.stderr.writes.filter((w) => w.includes(ESC)).at(-1)!;
654
+ const hookIdx = lastFrame.indexOf(t("feedback.phase.experimentSetup"));
655
+ const attemptIdx = lastFrame.indexOf("memory/a");
656
+ expect(hookIdx).toBeGreaterThan(-1);
657
+ expect(attemptIdx).toBeGreaterThan(-1);
658
+ expect(hookIdx).toBeLessThan(attemptIdx);
659
+ await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
660
+ });
661
+ });
662
+
663
+ describe("非 TTY human: 实验级钩子起止各追加一行", () => {
664
+ it("started / done 各一行 human 文案,done 带耗时;failed 行同形;全程无 ANSI", async () => {
665
+ const { fake, coordinator } = setupPlain();
666
+ coordinator.start(plan({ totalRuns: 2, evals: 2 }));
667
+ coordinator.experimentHook({ experimentId: "compare/bub-e2b", hook: "setup", status: "started" });
668
+ coordinator.experimentHook({ experimentId: "compare/bub-e2b", hook: "setup", status: "done", durationMs: 42_000 });
669
+ coordinator.experimentHook({ experimentId: "compare/codex", hook: "setup", status: "started" });
670
+ coordinator.experimentHook({ experimentId: "compare/codex", hook: "setup", status: "failed", durationMs: 3_000 });
671
+ await flush();
672
+ const all = fake.stderr.writes.join("");
673
+ const label = t("feedback.phase.experimentSetup");
674
+ expect(all).toContain(`${label} · compare/bub-e2b`);
675
+ expect(all).toContain(`${label} ${t("feedback.human.hookDone")} · compare/bub-e2b (42s)`);
676
+ expect(all).toContain(`${label} ${t("feedback.human.hookFailed")} · compare/codex (3s)`);
677
+ expect(all).not.toContain(ESC);
678
+ await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
679
+ });
680
+
681
+ it("实验级 progress 在非 TTY human 下零输出(短命状态没有可覆盖的区域)", async () => {
682
+ const { fake, coordinator } = setupPlain();
683
+ coordinator.start(plan());
684
+ await flush(); // 先让 plan 行落盘,再取零输出基线
685
+ const before = fake.stderr.writes.length;
686
+ coordinator.experimentProgress({ experimentId: "compare/bub-e2b", detail: "starting tunnel" });
687
+ await flush();
688
+ expect(fake.stderr.writes.slice(before).join("")).toBe("");
689
+ await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
690
+ });
691
+ });
692
+
693
+ // ───────────────────────── 运行级瞬时通知(activity)(cases.md「实验级生命周期」) ─────────────────────────
694
+
695
+ describe("运行级瞬时通知(activity): human 追加一行", () => {
696
+ it("TTY:activity 落进 scrollback,dashboard 撤下后重建(通知行不被下一帧覆盖)", async () => {
697
+ const { fake, coordinator } = setupTty();
698
+ coordinator.start(plan());
699
+ coordinator.emit({ type: "attempt:start", at: 0, identity: ref("memory/a"), who: "bub-e2b", phase: "eval.run" });
700
+ fake.advance(1000);
701
+ await flush();
702
+ coordinator.activity("Braintrust experiment: https://example.test/exp/1");
703
+ fake.advance(1000);
704
+ await flush();
705
+ const all = fake.stderr.writes.join("");
706
+ expect(all).toContain("Braintrust experiment: https://example.test/exp/1");
707
+ // 通知之后 dashboard 还在:activity 之后仍有 ANSI 重画帧。
708
+ const activityIdx = fake.stderr.writes.findIndex((w) => w.includes("Braintrust"));
709
+ expect(fake.stderr.writes.slice(activityIdx + 1).some((w) => w.includes(ESC))).toBe(true);
710
+ await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
711
+ });
712
+
713
+ it("非 TTY:activity 追加一行,无 ANSI", async () => {
714
+ const { fake, coordinator } = setupPlain();
715
+ coordinator.start(plan());
716
+ coordinator.activity("prechecking judge config...");
717
+ await flush();
718
+ const all = fake.stderr.writes.join("");
719
+ expect(all).toContain("prechecking judge config...");
720
+ expect(all).not.toContain(ESC);
721
+ await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
722
+ });
723
+ });
@@ -23,7 +23,9 @@ import { assertionSummaryLines } from "../../scoring/display.ts";
23
23
  import { encodeAttemptKey } from "../types.ts";
24
24
  import type {
25
25
  ActiveAttempt,
26
+ ActiveExperimentHook,
26
27
  AttemptKey,
28
+ ExperimentHookName,
27
29
  LifecyclePhase,
28
30
  DurableFeedbackEvent,
29
31
  RunFeedbackPlan,
@@ -98,6 +100,16 @@ export function renderDurableLines(event: DurableFeedbackEvent, state: RunFeedba
98
100
  // 留存授予单条不即时打印;run 摘要后由 buildSummaryLines 汇总成 Kept sandboxes 块
99
101
  // (见 docs/feature/sandbox/cli.md「run 收尾输出」)。
100
102
  return [];
103
+ case "experiment-hook": {
104
+ // 只服务非 TTY 退化流(TTY dashboard 的 appendDurable 对这个事件直接返回,运行级行
105
+ // 由 state.experimentHooks 驱动,成功钩子不进 scrollback,见 cli.md「实验级钩子的显示」)。
106
+ const label = experimentHookLabel(event.hook);
107
+ const duration = event.durationMs !== undefined ? ` (${formatElapsed(event.durationMs)})` : "";
108
+ if (event.status === "started") return [`${label} · ${event.experimentId}`];
109
+ const statusWord =
110
+ event.status === "done" ? t("feedback.human.hookDone") : t("feedback.human.hookFailed");
111
+ return [`${label} ${statusWord} · ${event.experimentId}${duration}`];
112
+ }
101
113
  case "summary":
102
114
  return buildSummaryLines(event, state);
103
115
  case "saved":
@@ -217,9 +229,9 @@ function buildSummaryLines(event: DurableFeedbackEvent & { type: "summary" }, st
217
229
 
218
230
  function buildSavedLines(event: DurableFeedbackEvent & { type: "saved" }): string[] {
219
231
  const paths = event.paths;
220
- const lines: string[] = [];
221
- const group = deriveResultGroup(paths);
222
- if (group) lines.push(t("feedback.human.compare", { group }));
232
+ // 比较命令直接是 `niceeval view`——它读整个结果根,不需要(也不该被)目录路径收窄成
233
+ // 一个 eval 位置参数(那是选择语义,不是报告分组语义);见 docs/feature/experiments/cli.md。
234
+ const lines: string[] = [t("feedback.human.compare")];
223
235
  if (paths.length === 0) return lines;
224
236
  if (paths.length === 1) {
225
237
  lines.push(`${t("feedback.human.resultsHeader")} ${paths[0]}`);
@@ -233,18 +245,6 @@ function buildSavedLines(event: DurableFeedbackEvent & { type: "saved" }): strin
233
245
  return lines;
234
246
  }
235
247
 
236
- /** 快照路径的共同「组」段(如 `.niceeval/compare/bub-e2b/<snapshot>` → `compare`)。
237
- * 多个 experiment 组混在同一次 invocation 里(不带 group 前缀的 `niceeval exp` 全量运行)时
238
- * 组名不唯一,不猜一个可能就 wrong 的 view 目标,直接省略 Compare 行 —— Results 路径仍然完整。 */
239
- function deriveResultGroup(paths: readonly string[]): string | undefined {
240
- const groups = new Set<string>();
241
- for (const p of paths) {
242
- const seg = p.split("/")[1];
243
- if (seg) groups.add(seg);
244
- }
245
- return groups.size === 1 ? [...groups][0] : undefined;
246
- }
247
-
248
248
  function formatSummaryDetail(durationMs: number, state: RunFeedbackState): string {
249
249
  const parts = [formatElapsed(durationMs)];
250
250
  const fullReuse = state.total > 0 && state.total === state.reused;
@@ -281,6 +281,12 @@ export function formatTokenCount(n: number): string {
281
281
  * 机器面(agent/ci 的 `phase=` 与落盘)保留精确的点分名,收尾段在 Human 侧合并显示为一档。 */
282
282
  function phaseLabel(phase: LifecyclePhase): string {
283
283
  switch (phase) {
284
+ // 实验级两员不会作为 ActiveAttempt.phase 出现(钩子跑的时候没有活跃 attempt),
285
+ // 这里只服务 failure 行的 phase 标注(experiment.setup 失败的合成 errored 结果)。
286
+ case "experiment.setup":
287
+ return t("feedback.phase.experimentSetup");
288
+ case "experiment.teardown":
289
+ return t("feedback.phase.teardown");
284
290
  case "sandbox.queue":
285
291
  return t("feedback.phase.sandboxQueue");
286
292
  case "sandbox.create":
@@ -317,6 +323,13 @@ function phaseLabel(phase: LifecyclePhase): string {
317
323
  }
318
324
  }
319
325
 
326
+ /** 实验级钩子的运行级行标签。`phaseLabel` 把 `experiment.teardown` 与其它收尾段合并成
327
+ * 「cleaning up」一档(那是 attempt failure 行的语境);运行级行没有 attempt 语境,必须
328
+ * 自报家门是哪个实验级钩子,所以这里用独立的两个词。 */
329
+ function experimentHookLabel(hook: ExperimentHookName): string {
330
+ return hook === "setup" ? t("feedback.phase.experimentSetup") : t("feedback.phase.experimentTeardown");
331
+ }
332
+
320
333
  function formatCounts(state: RunFeedbackState): string {
321
334
  const counts = t("feedback.human.counts", {
322
335
  total: state.total,
@@ -365,18 +378,24 @@ function createDashboardRenderer(io: FeedbackIO, command: string): FeedbackRende
365
378
  if (state.total > 0 && state.total === state.reused) return [];
366
379
  const columns = io.stderr.columns;
367
380
  const lines: string[] = [formatCommandLine(command, state.elapsedMs, columns), formatCounts(state)];
368
- if (activeOrder.length === 0) return lines.map((l) => truncateNoWrap(l, columns));
381
+ // 实验级钩子的运行级行排在 attempt 行前面(见 cli.md「实验级钩子的显示」):它解释了
382
+ // 为什么后面的 attempt 还停在 queued。Map 按插入序迭代,天然满足稳定 slot。
383
+ const hookRows = [...state.experimentHooks.values()];
384
+ if (activeOrder.length === 0 && hookRows.length === 0) return lines.map((l) => truncateNoWrap(l, columns));
369
385
 
370
386
  lines.push("", t("feedback.human.active"));
371
387
  const rowBudget = Math.max(0, io.stderr.rows - lines.length - DASHBOARD_ROW_RESERVE);
372
- const total = activeOrder.length;
388
+ const total = hookRows.length + activeOrder.length;
373
389
  // 窄/矮终端先减 active slots(减少行数),而不是先压缩单行内容 ——
374
390
  // 单行内容的截断在 formatActiveRow 里按 columns 单独处理。
375
391
  const showCount = total <= rowBudget ? total : Math.max(0, rowBudget - 1);
376
- for (let i = 0; i < showCount; i++) {
377
- const active = state.active.get(activeOrder[i]!);
378
- if (active) lines.push(formatActiveRow(active, io));
392
+ const rows: string[] = hookRows.map((hookRow) => formatExperimentHookRow(hookRow, io));
393
+ for (const key of activeOrder) {
394
+ if (rows.length >= showCount) break;
395
+ const active = state.active.get(key);
396
+ if (active) rows.push(formatActiveRow(active, io));
379
397
  }
398
+ lines.push(...rows.slice(0, showCount));
380
399
  if (total > showCount) {
381
400
  lines.push(t("feedback.human.moreActive", { count: total - showCount }));
382
401
  }
@@ -412,8 +431,17 @@ function createDashboardRenderer(io: FeedbackIO, command: string): FeedbackRende
412
431
 
413
432
  return {
414
433
  appendDurable(event, state) {
434
+ // 实验级钩子起止在 TTY 下只驱动运行级 active 行(state.experimentHooks 已由 reducer
435
+ // 更新,coordinator 紧接着的 redrawDynamic 会画出来);成功钩子不写 scrollback 永久行
436
+ // (见 cli.md「实验级钩子的显示」)。非 TTY 退化流才逐行追加(见 renderDurableLines)。
437
+ if (event.type === "experiment-hook") return;
415
438
  writeDurable(io, event, state);
416
439
  },
440
+ activity(text) {
441
+ // 运行级瞬时通知(judge 预检、provider 一次性通知……):coordinator 已按
442
+ // clearDynamic → activity → redrawDynamic 包好顺序,这里只管把这一行落进 scrollback。
443
+ io.stderr.write(text.endsWith("\n") ? text : `${text}\n`);
444
+ },
417
445
  clearDynamic() {
418
446
  if (linesDrawn === 0) return; // 幂等:coordinator 收尾时会无条件再调一次
419
447
  let out = `\x1B[${linesDrawn}A`;
@@ -469,6 +497,22 @@ function formatActiveRow(active: ActiveAttempt, io: FeedbackIO): string {
469
497
  return prefix + detail.slice(0, budget);
470
498
  }
471
499
 
500
+ /** 实验级钩子的运行级行:与 attempt 行同一套列宽算法,label 跨过 evalId+who 两列的宽度,
501
+ * elapsed 列因此对齐;detail 来自实验级 `ctx.progress`,没有就只留标签行。 */
502
+ function formatExperimentHookRow(hook: ActiveExperimentHook, io: FeedbackIO): string {
503
+ const columns = io.stderr.columns;
504
+ const elapsed = formatElapsed(io.clock.now() - hook.startedAt).padStart(6);
505
+ const sym = "● ";
506
+ const fixedWidth = sym.length + elapsed.length + 6;
507
+ const detailReserve = Math.min(80, Math.max(0, Math.floor(columns * 0.35)));
508
+ const remaining = Math.max(0, columns - fixedWidth - detailReserve);
509
+ // attempt 行的身份区是 evalCol + 两格间隔 + whoCol(合计 remaining + 2),这里用同一跨度。
510
+ const label = padTrunc(`${experimentHookLabel(hook.hook)} · ${hook.experimentId}`, remaining + 2);
511
+ const prefix = `${sym}${label} ${elapsed} `;
512
+ const budget = Math.max(0, columns - prefix.length);
513
+ return prefix + (hook.detail ?? "").slice(0, budget);
514
+ }
515
+
472
516
  // ───────────────────────── 非 TTY:human 文案的纯追加流 ─────────────────────────
473
517
 
474
518
  function createPlainRenderer(io: FeedbackIO): FeedbackRenderer {
@@ -480,6 +524,12 @@ function createPlainRenderer(io: FeedbackIO): FeedbackRenderer {
480
524
  lastDurableAtMs = event.at;
481
525
  writeDurable(io, event, state);
482
526
  },
527
+ activity(text) {
528
+ // 运行级瞬时通知按永久行追加(非 TTY 没有可覆盖的动态区域),并重置 heartbeat 计时 ——
529
+ // 刚有输出就不需要紧跟一条「还活着」。
530
+ lastDurableAtMs = io.clock.now();
531
+ io.stderr.write(text.endsWith("\n") ? text : `${text}\n`);
532
+ },
483
533
  onTick(event, state) {
484
534
  if (event.at - lastDurableAtMs < NON_TTY_HEARTBEAT_IDLE_MS) return;
485
535
  lastDurableAtMs = event.at;
@@ -487,7 +537,7 @@ function createPlainRenderer(io: FeedbackIO): FeedbackRenderer {
487
537
  `${t("feedback.human.heartbeat", { elapsed: formatElapsed(state.elapsedMs), counts: formatCounts(state) })}\n`,
488
538
  );
489
539
  },
490
- // 没有 clearDynamic/redrawDynamic/activity/onLifecycle:非 TTY 退化流不维护动态区域,
540
+ // 没有 clearDynamic/redrawDynamic/onLifecycle:非 TTY 退化流不维护动态区域,
491
541
  // 不展示 active attempt 的逐次阶段变化,也不逐次输出 provisioning retry/backoff ——
492
542
  // 这些行为由「不实现对应可选钩子」天然满足,不需要在这里写 profile 分支。
493
543
  };
@@ -393,3 +393,67 @@ describe("reduceRunFeedback: 守恒公式", () => {
393
393
  expect(activeAfterPhase?.detail).toBeUndefined();
394
394
  });
395
395
  });
396
+
397
+ describe("reduceRunFeedback: 实验级钩子", () => {
398
+ const plan: RunFeedbackEvent = {
399
+ type: "plan",
400
+ at: 0,
401
+ plan: { shape: { evals: 2, configs: 1, totalRuns: 2, maxConcurrency: 2 }, reused: 0, reusedFailures: [] },
402
+ };
403
+
404
+ it("started 添加运行级行,done 移除;等待 setup 的 attempt 保持 queued(计数不变量不受钩子影响)", () => {
405
+ const afterStart = replay([
406
+ plan,
407
+ { type: "experiment-hook", at: 1, experimentId: "compare/bub-e2b", hook: "setup", status: "started" },
408
+ ]);
409
+ expect(afterStart).toMatchObject({ total: 2, running: 0, queued: 2, completed: 0 });
410
+ expect(afterStart.experimentHooks.get("compare/bub-e2b")).toMatchObject({
411
+ experimentId: "compare/bub-e2b",
412
+ hook: "setup",
413
+ startedAt: 1,
414
+ });
415
+
416
+ const afterDone = replay([
417
+ plan,
418
+ { type: "experiment-hook", at: 1, experimentId: "compare/bub-e2b", hook: "setup", status: "started" },
419
+ { type: "experiment-hook", at: 5, experimentId: "compare/bub-e2b", hook: "setup", status: "done", durationMs: 4 },
420
+ ]);
421
+ expect(afterDone.experimentHooks.size).toBe(0);
422
+ });
423
+
424
+ it("failed 同样移除运行级行(setup 抛错的证据走每条 attempt 的 failure 事件,不留孤儿行)", () => {
425
+ const state = replay([
426
+ plan,
427
+ { type: "experiment-hook", at: 1, experimentId: "compare/codex", hook: "setup", status: "started" },
428
+ { type: "experiment-hook", at: 3, experimentId: "compare/codex", hook: "setup", status: "failed", durationMs: 2 },
429
+ ]);
430
+ expect(state.experimentHooks.size).toBe(0);
431
+ });
432
+
433
+ it("experiment:progress 只覆盖对应行的 detail;没有对应行时静默忽略", () => {
434
+ const state = replay([
435
+ plan,
436
+ { type: "experiment-hook", at: 1, experimentId: "compare/bub-e2b", hook: "setup", status: "started" },
437
+ { type: "experiment:progress", at: 2, experimentId: "compare/bub-e2b", detail: "starting tunnel (1/3)" },
438
+ { type: "experiment:progress", at: 3, experimentId: "compare/bub-e2b", detail: "starting tunnel (2/3)" },
439
+ { type: "experiment:progress", at: 3, experimentId: "compare/unknown", detail: "ignored" },
440
+ ]);
441
+ expect(state.experimentHooks.get("compare/bub-e2b")?.detail).toBe("starting tunnel (2/3)");
442
+ expect(state.experimentHooks.has("compare/unknown")).toBe(false);
443
+ });
444
+
445
+ it("plan 事件清空残留的运行级行(reducer 复用于多次 run 时不带上一次的钩子状态)", () => {
446
+ let state = createInitialRunFeedbackState();
447
+ state = reduceRunFeedback(state, plan);
448
+ state = reduceRunFeedback(state, {
449
+ type: "experiment-hook",
450
+ at: 1,
451
+ experimentId: "compare/bub-e2b",
452
+ hook: "teardown",
453
+ status: "started",
454
+ });
455
+ expect(state.experimentHooks.size).toBe(1);
456
+ state = reduceRunFeedback(state, plan);
457
+ expect(state.experimentHooks.size).toBe(0);
458
+ });
459
+ });
@@ -23,6 +23,7 @@ export function createInitialRunFeedbackState(): RunFeedbackState {
23
23
  earlyExitSkipped: 0,
24
24
  elapsedMs: 0,
25
25
  active: new Map(),
26
+ experimentHooks: new Map(),
26
27
  failures: [],
27
28
  freshFailureCount: 0,
28
29
  diagnostics: [],
@@ -47,6 +48,7 @@ export function reduceRunFeedback(state: RunFeedbackState, event: RunFeedbackEve
47
48
  running: 0,
48
49
  completed: 0,
49
50
  active: new Map(),
51
+ experimentHooks: new Map(),
50
52
  failures: (event.plan.reusedFailures ?? []).map((failure) => ({ ...failure, at: event.at })),
51
53
  freshFailureCount: 0,
52
54
  diagnostics: [],
@@ -120,6 +122,30 @@ export function reduceRunFeedback(state: RunFeedbackState, event: RunFeedbackEve
120
122
  };
121
123
  }
122
124
 
125
+ case "experiment-hook": {
126
+ // 运行级行的增删:started 添加,done/failed 移除(见 cli.md「实验级钩子的显示」)。
127
+ // 不动 running/queued 计数——等待 setup 的 attempt 保持 queued,计数不变量不受钩子影响。
128
+ const experimentHooks = new Map(state.experimentHooks);
129
+ if (event.status === "started") {
130
+ experimentHooks.set(event.experimentId, {
131
+ experimentId: event.experimentId,
132
+ hook: event.hook,
133
+ startedAt: event.at,
134
+ });
135
+ } else {
136
+ experimentHooks.delete(event.experimentId);
137
+ }
138
+ return { ...state, experimentHooks };
139
+ }
140
+
141
+ case "experiment:progress": {
142
+ const existing = state.experimentHooks.get(event.experimentId);
143
+ if (!existing) return state; // 防御:没有对应运行级行时静默忽略,不让 renderer 崩
144
+ const experimentHooks = new Map(state.experimentHooks);
145
+ experimentHooks.set(event.experimentId, { ...existing, detail: event.detail });
146
+ return { ...state, experimentHooks };
147
+ }
148
+
123
149
  case "attempt:early-exit":
124
150
  // 首过即停下已知 verdict 的省略次数:折进 completed(结论已经确定,不再需要派发),
125
151
  // 不产生 failures/diagnostics —— 这不是一次失败或异常,只是省下的重复验证
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { writeStderrLine } from "../../tty-line.ts";
18
18
  import { t } from "../../i18n/index.ts";
19
- import type { AttemptLifecycleEvent, AttemptRef, FailureDetail } from "../types.ts";
19
+ import type { AttemptLifecycleEvent, AttemptRef, ExperimentHookName, FailureDetail } from "../types.ts";
20
20
  import type { Verdict } from "../../scoring/types.ts";
21
21
  import type { JsonValue } from "../../shared/types.ts";
22
22
  import type { AttemptLocator } from "../../results/locator.ts";
@@ -51,6 +51,22 @@ export interface BudgetExhaustedInput {
51
51
  unstarted: number;
52
52
  }
53
53
 
54
+ /** `sink.experimentHook()` 的输入 —— 与 `DurableFeedbackEvent` 的 "experiment-hook" 变体字段
55
+ * 一致,省略 `type`/`at`(由 coordinator 补上)。调用方(run.ts)在钩子真正开始/结束时各调
56
+ * 一次;`durationMs` 只在 done/failed 上给。 */
57
+ export interface ExperimentHookInput {
58
+ experimentId: string;
59
+ hook: ExperimentHookName;
60
+ status: "started" | "done" | "failed";
61
+ durationMs?: number;
62
+ }
63
+
64
+ /** `sink.experimentProgress()` 的输入 —— 实验级 `ctx.progress` 压好的单行文本。 */
65
+ export interface ExperimentProgressInput {
66
+ experimentId: string;
67
+ detail: string;
68
+ }
69
+
54
70
  /** `sink.kept()` 的输入 —— 与 `DurableFeedbackEvent` 的 "kept" 变体字段一致,省略 type/at。 */
55
71
  export interface KeptInput {
56
72
  locator: AttemptLocator;
@@ -76,6 +92,11 @@ export interface FeedbackSink {
76
92
  budgetExhausted(input: BudgetExhaustedInput): void;
77
93
  /** 一次留存授予(--keep-sandbox);见 `KeptInput` 与 docs/feature/sandbox/cli.md。 */
78
94
  kept(input: KeptInput): void;
95
+ /** 实验级钩子(`ExperimentDef.setup` / teardown)的起止(见 `ExperimentHookInput`)。 */
96
+ experimentHook(input: ExperimentHookInput): void;
97
+ /** 实验级 `ctx.progress` 的短命投影:只更新运行级行的 detail。与 `lifecycle` 同级别的
98
+ * 「只服务正在画着的 dashboard」信号,没有活跃 coordinator 时静默丢弃是安全的。 */
99
+ experimentProgress(input: ExperimentProgressInput): void;
79
100
  /** attempt 生命周期事件(queued/start/phase/progress/complete/early-exit),见
80
101
  * `AttemptLifecycleEvent`。只驱动 human dashboard 的 active slot,不落 RunSummary/结果文件,
81
102
  * 所以没有活跃 coordinator 时(见 `reportAttemptLifecycle`)静默丢弃是安全的 —— 这类信息
@@ -155,6 +176,24 @@ export function reportKept(input: KeptInput): void {
155
176
  writeStderrLine(`kept sandbox ${input.sandboxId} (${input.provider}) — ${input.identity.evalId} #${input.identity.attempt} ${input.verdict}\n`);
156
177
  }
157
178
 
179
+ /** 实验级钩子的起止(见 `ExperimentHookInput`)。没有活跃 coordinator 时退回一行 stderr ——
180
+ * 长 setup 的可见性正是这条通道存在的理由,不能像 lifecycle 那样静默丢弃。 */
181
+ export function reportExperimentHook(input: ExperimentHookInput): void {
182
+ const sink = current();
183
+ if (sink) {
184
+ sink.experimentHook(input);
185
+ return;
186
+ }
187
+ const duration = input.durationMs !== undefined ? ` (${Math.round(input.durationMs / 1000)}s)` : "";
188
+ writeStderrLine(`experiment ${input.hook} ${input.status} · ${input.experimentId}${duration}\n`);
189
+ }
190
+
191
+ /** 实验级 `ctx.progress` 的短命投影。与 `reportAttemptLifecycle` 同理:只服务正在画着的
192
+ * dashboard,没有活跃 coordinator 时静默 no-op。 */
193
+ export function reportExperimentProgress(input: ExperimentProgressInput): void {
194
+ current()?.experimentProgress(input);
195
+ }
196
+
158
197
  export function reportBudgetExhausted(input: BudgetExhaustedInput): void {
159
198
  const sink = current();
160
199
  if (sink) {
@@ -7,6 +7,7 @@ import { sandboxRunInfo } from "../sandbox/resolve.ts";
7
7
  import type { DiscoveredEval, EvalResult, SandboxOption } from "../types.ts";
8
8
  import type { AgentRun } from "./types.ts";
9
9
  import { prepareRunSandboxes, sandboxForEval } from "./sandbox-selection.ts";
10
+ import { selectedEvalsForRun } from "./eval-selection.ts";
10
11
 
11
12
  export function cacheKey(run: AgentRun, evalId: string): string {
12
13
  return `${run.experimentId ?? ""}|${evalId}`;
@@ -76,7 +77,7 @@ export async function planCarry(
76
77
  const plannedFingerprints = new Map<string, string>();
77
78
  const jobs: Promise<void>[] = [];
78
79
  for (const run of agentRuns) {
79
- for (const evalDef of evals.filter((e) => run.evalFilter(e.id))) {
80
+ for (const evalDef of selectedEvalsForRun(evals, run)) {
80
81
  jobs.push(
81
82
  computeFingerprint(evalDef, run, sourceCache, configSandbox).then((fp) => {
82
83
  plannedFingerprints.set(cacheKey(run, evalDef.id), fp);
@@ -45,6 +45,18 @@ describe("filterSummary", () => {
45
45
  expect(sub.estimatedCostUSD).toBe(0.1);
46
46
  expect(sub.completedAt).toBe("2026-07-07T00:01:00.000Z");
47
47
  });
48
+
49
+ it("保留原 summary 的 model 字段(docs/engineering/unit-tests/experiments-runner/cases.md「汇总判定与退出码」)", () => {
50
+ const s = { ...summary([result("a/1")]), model: "deepseek-chat" };
51
+ const sub = filterSummary(s, new Set(["a/1"]));
52
+ expect(sub.model).toBe("deepseek-chat");
53
+ });
54
+
55
+ it("未声明 model 时该字段省略,不伪造成空字符串或 undefined 字面量", () => {
56
+ const s = summary([result("a/1")]);
57
+ const sub = filterSummary(s, new Set(["a/1"]));
58
+ expect(sub.model).toBeUndefined();
59
+ });
48
60
  });
49
61
 
50
62
  describe("scopeReporter", () => {
@@ -136,6 +148,8 @@ describe("runReporter / emitReporterEvent · required/best-effort 原样转发,
136
148
  failure() {},
137
149
  budgetExhausted() {},
138
150
  kept() {},
151
+ experimentHook() {},
152
+ experimentProgress() {},
139
153
  lifecycle() {},
140
154
  });
141
155
  return fn(calls).finally(deactivate);
@@ -46,7 +46,7 @@ export async function emitReporterEvent(
46
46
  /** 按 eval id 过滤 RunSummary 并重新计数 —— eval 级 reporter 只看它观测的那部分。 */
47
47
  export function filterSummary(summary: RunSummary, ids: ReadonlySet<string>): RunSummary {
48
48
  const results = summary.results.filter((r) => ids.has(r.id));
49
- const sub = summarize(results, summary.agent, summary.startedAt, summary.durationMs, summary.name);
49
+ const sub = summarize(results, summary.agent, summary.startedAt, summary.durationMs, summary.name, summary.model);
50
50
  // completedAt 用原值(summarize 会重新取 now);name 等其余字段原样保留。
51
51
  return { ...summary, ...sub, completedAt: summary.completedAt };
52
52
  }
@@ -101,6 +101,7 @@ export function summarize(
101
101
  startedAt: string,
102
102
  durationMs: number,
103
103
  name?: LocalizedText,
104
+ model?: string,
104
105
  ): RunSummary {
105
106
  const counts = { passed: 0, failed: 0, skipped: 0, errored: 0 };
106
107
  let inTok = 0;
@@ -115,6 +116,7 @@ export function summarize(
115
116
  return {
116
117
  name,
117
118
  agent,
119
+ model,
118
120
  startedAt,
119
121
  completedAt: new Date().toISOString(),
120
122
  passed: counts.passed,