niceeval 0.5.3 → 0.6.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 (344) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +317 -0
  3. package/dist/context/types.d.ts +335 -0
  4. package/dist/i18n/core.d.ts +11 -0
  5. package/dist/i18n/en.d.ts +181 -0
  6. package/dist/i18n/index.d.ts +5 -0
  7. package/dist/i18n/zh-CN.d.ts +183 -0
  8. package/dist/o11y/execution-tree.d.ts +103 -0
  9. package/dist/o11y/otlp/parse.d.ts +3 -0
  10. package/dist/o11y/otlp/receiver.d.ts +21 -0
  11. package/dist/o11y/otlp/select.d.ts +22 -0
  12. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  13. package/dist/o11y/types.d.ts +190 -0
  14. package/dist/report/aggregate.d.ts +57 -0
  15. package/dist/report/aggregate.js +212 -0
  16. package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
  17. package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
  18. package/dist/report/built-ins/index.d.ts +1 -0
  19. package/dist/report/built-ins/index.js +2 -0
  20. package/dist/report/components.d.ts +178 -0
  21. package/dist/report/components.js +132 -0
  22. package/dist/report/compute.d.ts +106 -0
  23. package/dist/report/compute.js +618 -0
  24. package/dist/report/flag.d.ts +6 -0
  25. package/dist/report/flag.js +8 -0
  26. package/dist/report/format.d.ts +32 -0
  27. package/dist/report/format.js +126 -0
  28. package/dist/report/index.d.ts +19 -0
  29. package/dist/report/index.js +30 -0
  30. package/dist/report/load.d.ts +12 -0
  31. package/dist/report/load.js +56 -0
  32. package/dist/report/locale.d.ts +83 -0
  33. package/dist/report/locale.js +160 -0
  34. package/dist/report/metrics.d.ts +21 -0
  35. package/dist/report/metrics.js +115 -0
  36. package/dist/report/primitives.d.ts +64 -0
  37. package/dist/report/primitives.js +113 -0
  38. package/dist/report/react/AttemptList.d.ts +23 -0
  39. package/dist/report/react/AttemptList.js +20 -0
  40. package/dist/report/react/DeltaTable.d.ts +8 -0
  41. package/dist/report/react/DeltaTable.js +24 -0
  42. package/dist/report/react/EvalList.d.ts +10 -0
  43. package/dist/report/react/EvalList.js +0 -0
  44. package/dist/report/react/ExperimentList.d.ts +10 -0
  45. package/dist/report/react/ExperimentList.js +15 -0
  46. package/dist/report/react/GroupSummary.d.ts +8 -0
  47. package/dist/report/react/GroupSummary.js +8 -0
  48. package/dist/report/react/MetricBars.d.ts +10 -0
  49. package/dist/report/react/MetricBars.js +48 -0
  50. package/dist/report/react/MetricLine.d.ts +9 -0
  51. package/dist/report/react/MetricLine.js +68 -0
  52. package/dist/report/react/MetricMatrix.d.ts +10 -0
  53. package/dist/report/react/MetricMatrix.js +23 -0
  54. package/dist/report/react/MetricScatter.d.ts +9 -0
  55. package/dist/report/react/MetricScatter.js +100 -0
  56. package/dist/report/react/MetricTable.d.ts +11 -0
  57. package/dist/report/react/MetricTable.js +26 -0
  58. package/dist/report/react/RunOverview.d.ts +8 -0
  59. package/dist/report/react/RunOverview.js +12 -0
  60. package/dist/report/react/Scoreboard.d.ts +8 -0
  61. package/dist/report/react/Scoreboard.js +26 -0
  62. package/dist/report/react/cell.d.ts +9 -0
  63. package/dist/report/react/cell.js +9 -0
  64. package/dist/report/react/chart-math.d.ts +14 -0
  65. package/dist/report/react/chart-math.js +68 -0
  66. package/dist/report/react/colors.d.ts +14 -0
  67. package/dist/report/react/colors.js +45 -0
  68. package/dist/report/react/fixtures.d.ts +19 -0
  69. package/dist/report/react/fixtures.js +393 -0
  70. package/dist/report/react/format.d.ts +3 -0
  71. package/dist/report/react/format.js +7 -0
  72. package/dist/report/react/index.d.ts +6 -0
  73. package/dist/report/react/index.js +14 -0
  74. package/dist/report/report.d.ts +24 -0
  75. package/dist/report/report.js +49 -0
  76. package/dist/report/text/faces.d.ts +29 -0
  77. package/dist/report/text/faces.js +503 -0
  78. package/dist/report/text/layout.d.ts +75 -0
  79. package/dist/report/text/layout.js +173 -0
  80. package/dist/report/text/plot.d.ts +37 -0
  81. package/dist/report/text/plot.js +81 -0
  82. package/dist/report/text/table.d.ts +8 -0
  83. package/dist/report/text/table.js +86 -0
  84. package/dist/report/tree.d.ts +99 -0
  85. package/dist/report/tree.js +228 -0
  86. package/dist/report/types.d.ts +420 -0
  87. package/dist/report/types.js +4 -0
  88. package/dist/report/web.d.ts +15 -0
  89. package/dist/report/web.js +37 -0
  90. package/dist/results/annotated-source.d.ts +61 -0
  91. package/dist/results/attempt-evidence.d.ts +69 -0
  92. package/dist/results/attempt-source.d.ts +15 -0
  93. package/dist/results/locator.d.ts +75 -0
  94. package/dist/results/locator.js +0 -0
  95. package/dist/results/select.d.ts +45 -0
  96. package/dist/results/select.js +230 -0
  97. package/dist/results/source-hash.d.ts +9 -0
  98. package/dist/results/types.d.ts +204 -0
  99. package/dist/runner/eval-source.d.ts +22 -0
  100. package/dist/runner/fingerprint.d.ts +23 -0
  101. package/dist/runner/types.d.ts +332 -0
  102. package/dist/sandbox/docker-stream.d.ts +24 -0
  103. package/dist/sandbox/docker.d.ts +84 -0
  104. package/dist/sandbox/e2b.d.ts +31 -0
  105. package/dist/sandbox/errors.d.ts +4 -0
  106. package/dist/sandbox/local-files.d.ts +6 -0
  107. package/dist/sandbox/paths.d.ts +4 -0
  108. package/dist/sandbox/registry.d.ts +13 -0
  109. package/dist/sandbox/resolve.d.ts +40 -0
  110. package/dist/sandbox/retry.d.ts +10 -0
  111. package/dist/sandbox/shell.d.ts +11 -0
  112. package/dist/sandbox/source-files.d.ts +13 -0
  113. package/dist/sandbox/types.d.ts +203 -0
  114. package/dist/sandbox/vercel.d.ts +37 -0
  115. package/dist/scoring/types.d.ts +70 -0
  116. package/dist/shared/aggregate.d.ts +29 -0
  117. package/dist/shared/aggregate.js +55 -0
  118. package/dist/shared/types.d.ts +27 -0
  119. package/dist/shared/verdict.d.ts +27 -0
  120. package/dist/shared/verdict.js +34 -0
  121. package/dist/tty-line.d.ts +6 -0
  122. package/dist/types.d.ts +7 -0
  123. package/dist/util.d.ts +22 -0
  124. package/docs-site/zh/concepts/adapter.mdx +9 -4
  125. package/docs-site/zh/concepts/assert.mdx +8 -8
  126. package/docs-site/zh/concepts/evals.mdx +5 -5
  127. package/docs-site/zh/concepts/experiment.mdx +3 -1
  128. package/docs-site/zh/concepts/judge.mdx +9 -9
  129. package/docs-site/zh/concepts/overview.mdx +2 -2
  130. package/docs-site/zh/concepts/tier.mdx +6 -6
  131. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  132. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  133. package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
  134. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  135. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  136. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  137. package/docs-site/zh/guides/experiments.mdx +7 -0
  138. package/docs-site/zh/guides/fixtures.mdx +1 -1
  139. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  140. package/docs-site/zh/guides/publish-report.mdx +4 -4
  141. package/docs-site/zh/guides/report-components.mdx +194 -55
  142. package/docs-site/zh/guides/reporters.mdx +2 -2
  143. package/docs-site/zh/guides/results-data.mdx +35 -34
  144. package/docs-site/zh/guides/runner.mdx +2 -2
  145. package/docs-site/zh/guides/sandbox-agent.mdx +10 -5
  146. package/docs-site/zh/guides/sandbox-providers.mdx +102 -0
  147. package/docs-site/zh/guides/viewing-results.mdx +199 -79
  148. package/docs-site/zh/guides/write-experiment.mdx +23 -1
  149. package/docs-site/zh/guides/write-send.mdx +2 -2
  150. package/docs-site/zh/index.mdx +4 -3
  151. package/docs-site/zh/introduction.mdx +2 -1
  152. package/docs-site/zh/quickstart.mdx +5 -3
  153. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  154. package/docs-site/zh/reference/capabilities.mdx +1 -1
  155. package/docs-site/zh/reference/cli.mdx +36 -13
  156. package/docs-site/zh/reference/define-agent.mdx +24 -12
  157. package/docs-site/zh/reference/define-config.mdx +1 -1
  158. package/docs-site/zh/reference/define-eval.mdx +3 -3
  159. package/docs-site/zh/reference/events.mdx +7 -1
  160. package/docs-site/zh/reference/expect.mdx +3 -3
  161. package/package.json +10 -6
  162. package/src/agents/bub.ts +134 -43
  163. package/src/agents/claude-code.test.ts +152 -0
  164. package/src/agents/claude-code.ts +127 -13
  165. package/src/agents/codex.test.ts +192 -0
  166. package/src/agents/codex.ts +151 -12
  167. package/src/agents/index.ts +11 -3
  168. package/src/agents/manifest.ts +41 -0
  169. package/src/agents/plugin-config.test.ts +94 -0
  170. package/src/agents/shared.ts +33 -2
  171. package/src/agents/skills.test.ts +214 -0
  172. package/src/agents/skills.ts +304 -0
  173. package/src/agents/streaming.ts +5 -5
  174. package/src/agents/types.ts +62 -2
  175. package/src/cli.ts +74 -47
  176. package/src/context/context.test.ts +20 -2
  177. package/src/context/context.ts +3 -0
  178. package/src/context/control-flow.ts +1 -1
  179. package/src/context/session.test.ts +92 -1
  180. package/src/context/session.ts +26 -1
  181. package/src/context/types.ts +4 -4
  182. package/src/define.test.ts +82 -0
  183. package/src/define.ts +72 -11
  184. package/src/expect/index.ts +5 -5
  185. package/src/i18n/en.ts +36 -15
  186. package/src/i18n/zh-CN.ts +39 -21
  187. package/src/index.ts +1 -1
  188. package/src/o11y/execution-tree.test.ts +452 -0
  189. package/src/o11y/execution-tree.ts +367 -0
  190. package/src/o11y/otlp/receiver.ts +1 -1
  191. package/src/o11y/otlp/select.ts +7 -3
  192. package/src/o11y/parsers/claude-code.test.ts +175 -0
  193. package/src/o11y/parsers/claude-code.ts +32 -0
  194. package/src/o11y/types.ts +6 -1
  195. package/src/report/aggregate.ts +26 -20
  196. package/src/report/built-in-user-parity.test.tsx +643 -0
  197. package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
  198. package/src/report/built-ins/index.ts +2 -0
  199. package/src/report/components.tsx +223 -50
  200. package/src/report/compute.ts +358 -53
  201. package/src/report/dual-render.test.tsx +1125 -0
  202. package/src/report/flag.ts +1 -1
  203. package/src/report/format.ts +50 -2
  204. package/src/report/index.ts +48 -14
  205. package/src/report/load.ts +1 -1
  206. package/src/report/locale.ts +206 -0
  207. package/src/report/metrics.ts +39 -15
  208. package/src/report/primitives.tsx +100 -2
  209. package/src/report/react/AttemptList.tsx +115 -0
  210. package/src/report/react/DeltaTable.tsx +9 -6
  211. package/src/report/react/EvalList.tsx +0 -0
  212. package/src/report/react/ExperimentList.tsx +108 -0
  213. package/src/report/react/GroupSummary.tsx +66 -0
  214. package/src/report/react/MetricBars.tsx +24 -13
  215. package/src/report/react/MetricLine.tsx +21 -21
  216. package/src/report/react/MetricMatrix.tsx +8 -4
  217. package/src/report/react/MetricScatter.tsx +136 -96
  218. package/src/report/react/MetricTable.tsx +115 -15
  219. package/src/report/react/RunOverview.tsx +48 -26
  220. package/src/report/react/Scoreboard.tsx +15 -7
  221. package/src/report/react/cell.tsx +18 -9
  222. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  223. package/src/report/react/colors.ts +24 -15
  224. package/src/report/react/enhance.js +129 -0
  225. package/src/report/react/fixtures.ts +163 -34
  226. package/src/report/react/format.ts +1 -1
  227. package/src/report/react/index.tsx +22 -6
  228. package/src/report/react/render.test.tsx +164 -31
  229. package/src/report/react/styles.css +405 -213
  230. package/src/report/report.test.ts +403 -94
  231. package/src/report/report.ts +29 -11
  232. package/src/report/text/faces.ts +303 -95
  233. package/src/report/text/layout.ts +76 -11
  234. package/src/report/text/table.ts +98 -0
  235. package/src/report/tree.ts +103 -20
  236. package/src/report/types.ts +196 -25
  237. package/src/report/web.ts +46 -19
  238. package/src/results/annotated-source.test.ts +146 -0
  239. package/src/results/annotated-source.ts +128 -0
  240. package/src/results/attempt-evidence.test.ts +158 -0
  241. package/src/results/attempt-evidence.ts +158 -0
  242. package/src/results/attempt-source.ts +48 -0
  243. package/src/results/copy.ts +154 -139
  244. package/src/results/format.ts +54 -47
  245. package/src/results/host-equivalence.test.ts +527 -0
  246. package/src/results/index.ts +56 -14
  247. package/src/results/locator.test.ts +198 -0
  248. package/src/results/locator.ts +0 -0
  249. package/src/results/open.ts +392 -175
  250. package/src/results/results.test.ts +943 -494
  251. package/src/results/select.ts +129 -23
  252. package/src/results/skipped-notice.ts +0 -0
  253. package/src/results/source-hash.ts +28 -0
  254. package/src/results/types.ts +98 -62
  255. package/src/results/writer.ts +304 -176
  256. package/src/runner/attempt.test.ts +170 -0
  257. package/src/runner/attempt.ts +97 -15
  258. package/src/runner/discover.test.ts +65 -0
  259. package/src/runner/discover.ts +6 -2
  260. package/src/runner/eval-source.test.ts +121 -0
  261. package/src/runner/eval-source.ts +45 -0
  262. package/src/runner/fingerprint.ts +54 -1
  263. package/src/runner/remote-sandbox.ts +1 -1
  264. package/src/runner/report.test.ts +6 -8
  265. package/src/runner/report.ts +5 -4
  266. package/src/runner/reporters/artifacts.ts +27 -41
  267. package/src/runner/reporters/braintrust.test.ts +3 -3
  268. package/src/runner/reporters/braintrust.ts +1 -1
  269. package/src/runner/reporters/console.ts +6 -6
  270. package/src/runner/reporters/json.ts +4 -4
  271. package/src/runner/reporters/live.test.ts +56 -0
  272. package/src/runner/reporters/live.ts +36 -10
  273. package/src/runner/reporters/quiet.test.ts +66 -0
  274. package/src/runner/reporters/quiet.ts +49 -0
  275. package/src/runner/reporters/shared.ts +5 -5
  276. package/src/runner/reporters/table.ts +17 -17
  277. package/src/runner/run.ts +61 -102
  278. package/src/runner/types.ts +43 -32
  279. package/src/sandbox/checkpoint.ts +1 -1
  280. package/src/sandbox/docker.ts +15 -2
  281. package/src/sandbox/e2b.ts +11 -5
  282. package/src/sandbox/errors.ts +13 -0
  283. package/src/sandbox/index.ts +3 -3
  284. package/src/sandbox/local-files.ts +1 -1
  285. package/src/sandbox/registry.ts +6 -5
  286. package/src/sandbox/resolve.ts +38 -23
  287. package/src/sandbox/retry.test.ts +68 -0
  288. package/src/sandbox/retry.ts +44 -0
  289. package/src/sandbox/types.ts +69 -28
  290. package/src/sandbox/vercel.ts +17 -3
  291. package/src/scoring/collector.ts +3 -3
  292. package/src/scoring/judge.ts +2 -2
  293. package/src/scoring/scoped.ts +13 -1
  294. package/src/scoring/types.ts +7 -7
  295. package/src/scoring/verdict.ts +4 -4
  296. package/src/shared/aggregate.ts +23 -3
  297. package/src/shared/types.ts +2 -2
  298. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  299. package/src/show/compose.ts +12 -132
  300. package/src/show/index.ts +125 -94
  301. package/src/show/render.ts +660 -160
  302. package/src/show/show.test.ts +472 -253
  303. package/src/source-loc.ts +1 -1
  304. package/src/tty-line.ts +32 -0
  305. package/src/types.ts +2 -2
  306. package/src/util.test.ts +21 -1
  307. package/src/util.ts +5 -1
  308. package/src/view/app/App.tsx +33 -198
  309. package/src/view/app/components/AttemptModal.tsx +6 -6
  310. package/src/view/app/components/CodeView.tsx +2 -2
  311. package/src/view/app/components/CopyControls.tsx +32 -36
  312. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  313. package/src/view/app/components/ui/badge.tsx +1 -1
  314. package/src/view/app/components/ui/dialog.tsx +2 -2
  315. package/src/view/app/i18n.ts +40 -31
  316. package/src/view/app/lib/artifact-url.ts +1 -1
  317. package/src/view/app/lib/attempt-route.test.ts +37 -47
  318. package/src/view/app/lib/attempt-route.ts +26 -26
  319. package/src/view/app/lib/rows.ts +9 -112
  320. package/src/view/app/lib/verdict.ts +25 -0
  321. package/src/view/app/main.tsx +9 -12
  322. package/src/view/app/pages/RunsPage.tsx +4 -4
  323. package/src/view/app/pages/TracesPage.tsx +3 -3
  324. package/src/view/app/shared.ts +0 -1
  325. package/src/view/app/types.ts +6 -51
  326. package/src/view/artifact-serving.test.ts +136 -0
  327. package/src/view/client-dist/app.css +1 -1
  328. package/src/view/client-dist/app.js +21 -21
  329. package/src/view/data.test.ts +214 -143
  330. package/src/view/data.ts +166 -120
  331. package/src/view/index.ts +23 -11
  332. package/src/view/server.ts +52 -17
  333. package/src/view/shared/types.ts +22 -31
  334. package/src/view/styles.css +117 -327
  335. package/src/view/view-report.test.ts +146 -98
  336. package/docs-site/zh/guides/sandbox-backends.mdx +0 -75
  337. package/src/report/default-report.tsx +0 -222
  338. package/src/report/dual-face.test.tsx +0 -527
  339. package/src/report/react/CaseList.tsx +0 -70
  340. package/src/view/app/components/CostScoreChart.tsx +0 -147
  341. package/src/view/app/components/ExperimentTable.tsx +0 -284
  342. package/src/view/app/components/GroupSelector.tsx +0 -61
  343. package/src/view/app/components/primitives.tsx +0 -43
  344. package/src/view/app/lib/outcome.ts +0 -82
@@ -0,0 +1,45 @@
1
+ // Eval 源码捕获:discovery 时读一次 eval 定义文件、归一化、算 SHA-256(定稿见
2
+ // docs/concepts.md「标注 Eval 源码」、docs/feature/results/architecture.md「sources.json」)。
3
+ //
4
+ // 目标形态是"发现时捕获,同一快照内相同内容只存一份"——本函数只做捕获这一步(读 + 归一化 +
5
+ // 哈希),不碰去重存储、不写 result.json、不改 discoverEvals()。它的产物形状是为了让后续
6
+ // 集成把 `captureEvalSource(evalDef.sourcePath)` 直接插进 discoverEvals() 的每个文件循环里:
7
+ // path/content 交给 writer 落盘(按 sha256 去重),sha256 也是 buildAnnotatedEvalSource()
8
+ // 重建标注模型时用来核对"这就是同一份源码"的锚。
9
+ //
10
+ // 归一化 + 哈希算法住在 results/source-hash.ts,不在这里重新实现一遍——discovery 侧捕获的
11
+ // 哈希与证据重建侧(annotated-source.ts)重算的哈希必须逐字节一致,唯一的办法是共用同一个
12
+ // 函数。runner 已经单向依赖 results(reporters/artifacts.ts 用 createResultsWriter),这里
13
+ // 反向引用不新增循环。
14
+
15
+ import { readFile } from "node:fs/promises";
16
+ import { relative, sep } from "node:path";
17
+ import { hashEvalSource, normalizeEvalSource } from "../results/source-hash.ts";
18
+
19
+ export interface CapturedEvalSource {
20
+ /** 项目相对路径(正斜杠),与 SourceArtifact.path / SourceLoc.file 同一约定。 */
21
+ path: string;
22
+ /** 归一化后的源码文本(去 BOM、CRLF/CR 统一成 LF)。 */
23
+ content: string;
24
+ /** 归一化文本的 SHA-256 十六进制摘要;同一份内容(哪怕跨平台不同换行符)恒相同。 */
25
+ sha256: string;
26
+ }
27
+
28
+ /**
29
+ * 读一个 eval 定义文件、归一化文本、算哈希。`filePath` 是绝对路径(discoverEvals() 里
30
+ * `DiscoveredEval.sourcePath` 的形状);`opts.root` 决定 `path` 字段相对谁计算,省略时
31
+ * 用 `process.cwd()`(与 src/source-loc.ts 的 captureLoc()、runner/attempt.ts 的
32
+ * collectSources() 同一约定——项目相对路径永远相对进程 cwd,不是相对 discoverEvals() 的
33
+ * `root` 参数,两者通常相同但不保证,显式传参让调用方按需要覆盖,也让单测不依赖真实 cwd)。
34
+ *
35
+ * 读不到文件(已删除 / 权限问题)如实抛错——发现阶段的源码文件应当总是可读,
36
+ * 与 collectSources() 运行后"读不到就跳过"的降级语义不同:那时源码可能已经在沙箱里、
37
+ * 已被清理;这里读的是刚刚 import 成功的同一个文件,读不到是需要暴露的异常。
38
+ */
39
+ export async function captureEvalSource(filePath: string, opts?: { root?: string }): Promise<CapturedEvalSource> {
40
+ const root = opts?.root ?? process.cwd();
41
+ const raw = await readFile(filePath, "utf-8");
42
+ const content = normalizeEvalSource(raw);
43
+ const path = relative(root, filePath).split(sep).join("/");
44
+ return { path, content, sha256: hashEvalSource(content) };
45
+ }
@@ -4,7 +4,7 @@
4
4
  import { createHash } from "node:crypto";
5
5
  import { readFile } from "node:fs/promises";
6
6
  import { sandboxLabel } from "../sandbox/resolve.ts";
7
- import type { DiscoveredEval } from "../types.ts";
7
+ import type { DiscoveredEval, EvalResult } from "../types.ts";
8
8
  import type { AgentRun } from "./types.ts";
9
9
 
10
10
  export function cacheKey(run: AgentRun, evalId: string): string {
@@ -47,6 +47,59 @@ export async function computeFingerprint(
47
47
  return createHash("sha256").update(stableJson(payload)).digest("hex");
48
48
  }
49
49
 
50
+ export interface CarryPlan {
51
+ /** `cacheKey(run, evalId)` → 本次规划出的指纹,供调用方按同一口径判断"这条要不要携入"。 */
52
+ plannedFingerprints: Map<string, string>;
53
+ /** 命中携入条件(passed/failed 终态 + 指纹匹配)的 `${experimentId}|${evalId}` 集合。 */
54
+ priorRunKeys: Set<string>;
55
+ /** priorRunKeys 对应的完整结果对象,供 run.ts 直接并入 summary、cli.ts 直接取 verdict 展示。 */
56
+ carriedResults: EvalResult[];
57
+ }
58
+
59
+ /**
60
+ * 算出这一批 (agentRun × eval) 的指纹,并据此从 priorResults 里筛出可以携入(跳过重跑)的结果。
61
+ * run.ts 与 cli.ts(live 表格构建)必须共用这同一份计算 —— 否则两边一旦对"哪些携入"的判断
62
+ * 不一致,live 表格就会显示"还在等名额",而 run.ts 其实已经把它筛掉、根本不会调度这个 attempt
63
+ * (见 memory 的 live-carry-row-shows-waiting-forever)。
64
+ */
65
+ export async function planCarry(
66
+ evals: DiscoveredEval[],
67
+ agentRuns: AgentRun[],
68
+ priorResults: EvalResult[] | undefined,
69
+ ): Promise<CarryPlan> {
70
+ const sourceCache = new Map<string, Promise<string>>();
71
+ const plannedFingerprints = new Map<string, string>();
72
+ const jobs: Promise<void>[] = [];
73
+ for (const run of agentRuns) {
74
+ for (const evalDef of evals.filter((e) => run.evalFilter(e.id))) {
75
+ jobs.push(
76
+ computeFingerprint(evalDef, run, sourceCache).then((fp) => {
77
+ plannedFingerprints.set(cacheKey(run, evalDef.id), fp);
78
+ }),
79
+ );
80
+ }
81
+ }
82
+ await Promise.all(jobs);
83
+
84
+ const priorRunKeys = new Set<string>();
85
+ const carriedResults: EvalResult[] = [];
86
+ if (priorResults?.length) {
87
+ for (const r of priorResults) {
88
+ if (!r.experimentId) continue;
89
+ const key = `${r.experimentId}|${r.id}`;
90
+ const isTerminalVerdict = r.verdict === "passed" || r.verdict === "failed";
91
+ if (isTerminalVerdict && r.fingerprint !== undefined && r.fingerprint === plannedFingerprints.get(key)) {
92
+ priorRunKeys.add(key);
93
+ }
94
+ }
95
+ for (const r of priorResults) {
96
+ if (!r.experimentId || !priorRunKeys.has(`${r.experimentId}|${r.id}`)) continue;
97
+ carriedResults.push(r);
98
+ }
99
+ }
100
+ return { plannedFingerprints, priorRunKeys, carriedResults };
101
+ }
102
+
50
103
  /** 键序稳定的 JSON 序列化(对象键排序),保证同一 payload 永远同一指纹。 */
51
104
  function stableJson(value: unknown): string {
52
105
  if (value === null || typeof value !== "object") return JSON.stringify(value);
@@ -46,7 +46,7 @@ export function withEvalLocalPaths(sandbox: Sandbox, baseDir: string): Sandbox {
46
46
  target.uploadDirectory(resolveLocalPath(baseDir, localDir), targetDir, opts);
47
47
  }
48
48
  const value = Reflect.get(target, prop, receiver);
49
- // 方法要 bind 回原对象:后端实现(class 实例)里的 this 不能指向 Proxy。
49
+ // 方法要 bind 回原对象:provider 实现(class 实例)里的 this 不能指向 Proxy。
50
50
  return typeof value === "function" ? value.bind(target) : value;
51
51
  },
52
52
  });
@@ -6,7 +6,7 @@ function result(id: string, overrides: Partial<EvalResult> = {}): EvalResult {
6
6
  return {
7
7
  id,
8
8
  agent: "codex",
9
- outcome: "passed",
9
+ verdict: "passed",
10
10
  attempt: 0,
11
11
  durationMs: 1000,
12
12
  assertions: [],
@@ -19,21 +19,20 @@ function summary(results: EvalResult[]): RunSummary {
19
19
  agent: "codex",
20
20
  startedAt: "2026-07-07T00:00:00.000Z",
21
21
  completedAt: "2026-07-07T00:01:00.000Z",
22
- passed: results.filter((r) => r.outcome === "passed").length,
23
- failed: results.filter((r) => r.outcome === "failed").length,
22
+ passed: results.filter((r) => r.verdict === "passed").length,
23
+ failed: results.filter((r) => r.verdict === "failed").length,
24
24
  skipped: 0,
25
25
  errored: 0,
26
26
  durationMs: 60_000,
27
27
  results,
28
- outputDir: ".niceeval/x",
29
28
  };
30
29
  }
31
30
 
32
31
  describe("filterSummary", () => {
33
- it("按 eval id 过滤结果并重新计数,保留原 completedAt / outputDir", () => {
32
+ it("按 eval id 过滤结果并重新计数,保留原 completedAt", () => {
34
33
  const s = summary([
35
34
  result("a/1", { usage: { inputTokens: 10, outputTokens: 5 }, estimatedCostUSD: 0.1 }),
36
- result("a/1", { outcome: "failed", attempt: 1 }),
35
+ result("a/1", { verdict: "failed", attempt: 1 }),
37
36
  result("b/1", { usage: { inputTokens: 100, outputTokens: 50 }, estimatedCostUSD: 1 }),
38
37
  ]);
39
38
  const sub = filterSummary(s, new Set(["a/1"]));
@@ -43,7 +42,6 @@ describe("filterSummary", () => {
43
42
  expect(sub.usage).toEqual({ inputTokens: 10, outputTokens: 5 });
44
43
  expect(sub.estimatedCostUSD).toBe(0.1);
45
44
  expect(sub.completedAt).toBe("2026-07-07T00:01:00.000Z");
46
- expect(sub.outputDir).toBe(".niceeval/x");
47
45
  });
48
46
  });
49
47
 
@@ -81,7 +79,7 @@ describe("scopeReporter", () => {
81
79
  it("onRunComplete 收到重新计数的子集汇总", async () => {
82
80
  const { calls, reporter } = recordingReporter();
83
81
  const scoped = scopeReporter(reporter, new Set(["a/1"]));
84
- await scoped.onRunComplete?.(summary([result("a/1"), result("b/1", { outcome: "failed" })]));
82
+ await scoped.onRunComplete?.(summary([result("a/1"), result("b/1", { verdict: "failed" })]));
85
83
  const got = calls[0]?.args[0] as RunSummary;
86
84
  expect(got.results.map((r) => r.id)).toEqual(["a/1"]);
87
85
  expect(got.passed).toBe(1);
@@ -4,13 +4,14 @@
4
4
  import type { EvalResult, LocalizedText, Reporter, ReporterEvent, RunShape, RunSummary } from "../types.ts";
5
5
  import { t } from "../i18n/index.ts";
6
6
  import { formatThrown } from "../util.ts";
7
+ import { writeStderrLine } from "../tty-line.ts";
7
8
 
8
9
  /** reporter 调用的统一兜错。返回 void,永不 reject(供 Promise.all 安全聚合)。 */
9
10
  export async function runReporter(stage: string, fn: () => unknown): Promise<void> {
10
11
  try {
11
12
  await fn();
12
13
  } catch (e) {
13
- process.stderr.write(t("runner.reporterDiagnostic", { stage, message: formatThrown(e) }));
14
+ writeStderrLine(t("runner.reporterDiagnostic", { stage, message: formatThrown(e) }));
14
15
  }
15
16
  }
16
17
 
@@ -22,7 +23,7 @@ export async function emitReporterEvent(reporters: readonly Reporter[], event: R
22
23
  export function filterSummary(summary: RunSummary, ids: ReadonlySet<string>): RunSummary {
23
24
  const results = summary.results.filter((r) => ids.has(r.id));
24
25
  const sub = summarize(results, summary.agent, summary.startedAt, summary.durationMs, summary.name);
25
- // completedAt 用原值(summarize 会重新取 now);format / producer / outputDir 等元数据原样保留。
26
+ // completedAt 用原值(summarize 会重新取 now);name 等其余字段原样保留。
26
27
  return { ...summary, ...sub, completedAt: summary.completedAt };
27
28
  }
28
29
 
@@ -69,7 +70,7 @@ export function scopeReporter(r: Reporter, ids: ReadonlySet<string>, shape?: Run
69
70
  return scoped;
70
71
  }
71
72
 
72
- /** 全局汇总:outcome 计数 + token / cost 折叠。按 attempt 计(eval 级折叠见 shared/outcome.ts)。 */
73
+ /** 全局汇总:verdict 计数 + token / cost 折叠。按 attempt 计(eval 级折叠见 shared/verdict.ts)。 */
73
74
  export function summarize(
74
75
  results: EvalResult[],
75
76
  agent: string,
@@ -82,7 +83,7 @@ export function summarize(
82
83
  let outTok = 0;
83
84
  let cost = 0;
84
85
  for (const r of results) {
85
- counts[r.outcome] += 1;
86
+ counts[r.verdict] += 1;
86
87
  inTok += r.usage?.inputTokens ?? 0;
87
88
  outTok += r.usage?.outputTokens ?? 0;
88
89
  cost += r.estimatedCostUSD ?? 0;
@@ -1,13 +1,14 @@
1
- // 默认本地工件报告器:给 `niceeval view` 提供稳定的离线输入。
1
+ // 默认本地 artifact 报告器:给 `niceeval view` 提供稳定的离线输入。
2
2
  //
3
- // 本文件是 niceeval/results 写入面(createRunWriter)的薄壳:订阅 reporter 事件转手调 writer,
4
- // 自己不持有任何布局知识(时间戳目录、attempt 路径、大字段拆分、瘦身、版本元数据都在库内)。
5
- // 落盘格式见 docs/results-format.md;每 eval-attempt 一个文件夹,重数据分文件,
6
- // summary.json 只留榜单元数据,view 展开某条 trace 时再按需 fetch 它的 trace.json。
3
+ // 本文件是 niceeval/results 写入面(createResultsWriter)的薄壳:订阅 reporter 事件按
4
+ // experimentId 路由转手调 writer,自己不持有任何布局知识(实验目录、快照目录、attempt
5
+ // 路径清洗、大字段拆分、瘦身、版本元数据全在库内)。落盘格式见 docs/feature/results/architecture.md:
6
+ // 每个 experiment 一个实验目录,目录下按时间戳开快照,快照内每 eval-attempt 一个文件夹,
7
+ // 重数据分文件,snapshot.json 只留快照元数据,view 展开某条 trace 时再按需 fetch 它的 trace.json。
7
8
 
8
9
  import { readFile } from "node:fs/promises";
9
10
  import type { Reporter } from "../../types.ts";
10
- import { createRunWriter, type RunWriter } from "../../results/writer.ts";
11
+ import { createResultsWriter, type ResultsWriter } from "../../results/writer.ts";
11
12
 
12
13
  /** niceeval 自身的 npm 版本,写进 producer.version;版本不匹配时读取器靠它拼 npx 提示。 */
13
14
  let producerVersionPromise: Promise<string | undefined> | undefined;
@@ -18,53 +19,38 @@ function producerVersion(): Promise<string | undefined> {
18
19
  return producerVersionPromise;
19
20
  }
20
21
 
21
- /** Artifacts 报告器额外暴露输出目录:CLI 在 run 结束时打出 summary.json 路径给 agent 直读。 */
22
- export type ArtifactsReporter = Reporter & { outputDir(): string };
22
+ /** Artifacts 报告器额外暴露已创建的快照目录清单:CLI 在 run 结束时逐条打出给 agent 直读。 */
23
+ export type ArtifactsReporter = Reporter & { outputDirs(): { experimentId: string; dir: string }[] };
23
24
 
24
25
  export function Artifacts(root = ".niceeval"): ArtifactsReporter {
25
- let writer: Promise<RunWriter> | undefined;
26
- let dir = "";
27
- const ensureWriter = (): Promise<RunWriter> => {
28
- writer ??= (async () => {
29
- const w = await createRunWriter(root, {
30
- producer: { name: "niceeval", version: await producerVersion() },
31
- });
32
- dir = w.dir;
33
- return w;
34
- })();
35
- return writer;
36
- };
26
+ let writer: ResultsWriter | undefined;
37
27
 
38
28
  return {
39
- outputDir: () => dir,
29
+ outputDirs: () => writer?.snapshotDirs() ?? [],
40
30
 
41
31
  async onRunStart() {
42
- // 每次 run 开一个新的时间戳目录(同一个 reporter 实例可能被复用)
43
- writer = undefined;
44
- await ensureWriter();
32
+ // 每次 run 换一个新 writer(同一个 reporter 实例可能被复用):writer 内部按
33
+ // experimentId 懒建各自的快照目录,这里只重置引用。
34
+ writer = createResultsWriter(root, {
35
+ producer: { name: "niceeval", version: await producerVersion() },
36
+ });
45
37
  },
46
38
 
47
- // 每条结果一出来就把它的重数据落到自己的文件夹(增量、互不影响)。
48
- // runner 的条目自带 agent / model / experimentId / startedAt(且存在无 experiment
49
- // 普通 run),不经 snapshot() 声明,走 writer 的内部增量入口。
39
+ // 每条结果一出来就按它的 experimentId 路由落盘(增量、互不影响)。fresh 条目在这里
40
+ // 一次写成;--resume 携带合入的条目( artifactBase)不经这里,onRunComplete 补写。
50
41
  async onEvalComplete(result) {
51
- await (await ensureWriter()).writeAttemptArtifacts(result);
42
+ await writer?.writeAttemptFor(result);
52
43
  },
53
44
 
54
- // run 结束写瘦身 summary.json:携带条目(--resume 合入)与最终排序只有调度器知道,
55
- // 权威 results overrides 交给 writer,瘦身与版本元数据注入都在库内发生。
45
+ // run 结束:先把携带条目(--resume 合入,summary.results 里带 artifactBase 的那些)
46
+ // 逐条落盘——它们没有触发过 onEvalComplete,只会原样写 result.json,不写 artifact
47
+ // (artifact 仍在原快照里,靠 artifactBase 懒加载回退)。再给每个快照补 completedAt。
56
48
  async onRunComplete(summary) {
57
- await (await ensureWriter()).finish({
58
- ...(summary.name !== undefined ? { name: summary.name } : {}),
59
- agent: summary.agent,
60
- ...(summary.model !== undefined ? { model: summary.model } : {}),
61
- startedAt: summary.startedAt,
62
- completedAt: summary.completedAt,
63
- durationMs: summary.durationMs,
64
- ...(summary.usage !== undefined ? { usage: summary.usage } : {}),
65
- ...(summary.estimatedCostUSD !== undefined ? { estimatedCostUSD: summary.estimatedCostUSD } : {}),
66
- results: summary.results,
67
- });
49
+ if (!writer) return;
50
+ for (const result of summary.results) {
51
+ if (result.artifactBase !== undefined) await writer.writeAttemptFor(result);
52
+ }
53
+ await writer.finish({ name: summary.name });
68
54
  },
69
55
  };
70
56
  }
@@ -6,7 +6,7 @@ function baseResult(overrides: Partial<EvalResult> = {}): EvalResult {
6
6
  return {
7
7
  id: "algebra/quadratic",
8
8
  agent: "codex",
9
- outcome: "passed",
9
+ verdict: "passed",
10
10
  attempt: 0,
11
11
  durationMs: 42_000,
12
12
  assertions: [],
@@ -84,7 +84,7 @@ describe("toBraintrustEvent", () => {
84
84
  experimentId: "compare/codex",
85
85
  model: "gpt-5.2",
86
86
  attempt: 1,
87
- outcome: "failed",
87
+ verdict: "failed",
88
88
  experiment: { id: "compare/codex", flags: { tape: true } },
89
89
  assertions: [{ name: "compiles", severity: "gate", score: 0, passed: false, detail: "tsc failed" }],
90
90
  }),
@@ -93,7 +93,7 @@ describe("toBraintrustEvent", () => {
93
93
  eval: "algebra/quadratic",
94
94
  agent: "codex",
95
95
  attempt: 1,
96
- outcome: "failed",
96
+ verdict: "failed",
97
97
  model: "gpt-5.2",
98
98
  experiment: "compare/codex",
99
99
  flags: { tape: true },
@@ -146,7 +146,7 @@ export function toBraintrustEvent(result: EvalResult): BraintrustLogEvent {
146
146
  eval: result.id,
147
147
  agent: result.agent,
148
148
  attempt: result.attempt,
149
- outcome: result.outcome,
149
+ verdict: result.verdict,
150
150
  };
151
151
  if (result.model !== undefined) metadata.model = result.model;
152
152
  if (result.experimentId !== undefined) metadata.experiment = result.experimentId;
@@ -3,7 +3,7 @@
3
3
  import type { EvalResult, Reporter, RunSummary } from "../../types.ts";
4
4
  import { t } from "../../i18n/index.ts";
5
5
  import { formatCost, formatDuration, formatTokens, renderRunReport } from "./table.ts";
6
- import { outcomeSymbol } from "./shared.ts";
6
+ import { verdictSymbol } from "./shared.ts";
7
7
 
8
8
  export function Console(): Reporter {
9
9
  return {
@@ -18,14 +18,14 @@ export function Console(): Reporter {
18
18
  process.stdout.write(t("report.runStart", { count: n, extra, concurrency: shape?.maxConcurrency ?? "?" }));
19
19
  },
20
20
  onEvalComplete(result: EvalResult) {
21
- const sym = outcomeSymbol(result.outcome);
21
+ const sym = verdictSymbol(result.verdict);
22
22
  const tok = (result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0);
23
23
  // requests > 0 但 tokens = 0 → agent 跑了但不上报用量(如 bub);显示 — 而非误导性的 0
24
24
  const tokStr = tok > 0 ? `${formatTokens(tok)} tok` : (result.usage?.requests ?? 0) > 0 ? `— tok` : `0 tok`;
25
25
  const cost = result.estimatedCostUSD !== undefined ? ` ${formatCost(result.estimatedCostUSD)}` : "";
26
26
  const who = result.model ? `${result.agent}/${result.model}` : result.agent;
27
27
  const meta = `(${formatDuration(result.durationMs)} ${tokStr}${cost})`;
28
- const label = result.outcome === "passed" ? "" : ` ${formatOutcome(result.outcome)}`;
28
+ const label = result.verdict === "passed" ? "" : ` ${formatVerdict(result.verdict)}`;
29
29
  process.stdout.write(` ${sym} ${result.id}${label} [${who}] ${meta}\n`);
30
30
 
31
31
  if (result.skipReason) {
@@ -55,13 +55,13 @@ export function Console(): Reporter {
55
55
  };
56
56
  }
57
57
 
58
- function formatOutcome(outcome: string): string {
59
- switch (outcome) {
58
+ function formatVerdict(verdict: string): string {
59
+ switch (verdict) {
60
60
  case "passed": return t("report.passed");
61
61
  case "failed": return t("report.failed");
62
62
  case "errored": return t("report.errored");
63
63
  case "skipped": return t("report.skipped");
64
- default: return outcome;
64
+ default: return verdict;
65
65
  }
66
66
  }
67
67
 
@@ -1,4 +1,4 @@
1
- // JSON / JUnit 报告器:把运行结果落成机器可读工件,接 CI 或下游 dashboard。
1
+ // JSON / JUnit 报告器:把运行结果落成机器可读 artifact,接 CI 或下游 dashboard。
2
2
 
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
4
  import { dirname } from "node:path";
@@ -21,14 +21,14 @@ export function JUnit(path: string): Reporter {
21
21
  .map((r) => {
22
22
  const name = xmlAttr(`${r.id} [${r.agent}${r.model ? "/" + r.model : ""}]`);
23
23
  const time = (r.durationMs / 1000).toFixed(3);
24
- if (r.outcome === "errored") {
24
+ if (r.verdict === "errored") {
25
25
  return ` <testcase name="${name}" time="${time}"><error message="${xmlAttr(r.error ?? "execution error")}"/></testcase>`;
26
26
  }
27
- if (r.outcome === "failed") {
27
+ if (r.verdict === "failed") {
28
28
  const msg = xmlAttr(r.assertions.filter((a) => !a.passed).map((a) => a.name).join("; "));
29
29
  return ` <testcase name="${name}" time="${time}"><failure message="${msg}"/></testcase>`;
30
30
  }
31
- if (r.outcome === "skipped") {
31
+ if (r.verdict === "skipped") {
32
32
  return ` <testcase name="${name}" time="${time}"><skipped message="${xmlAttr(r.skipReason ?? "")}"/></testcase>`;
33
33
  }
34
34
  return ` <testcase name="${name}" time="${time}"/>`;
@@ -0,0 +1,56 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { Live, type LiveRow } from "./live.ts";
3
+
4
+ function withMockTty<T>(fn: () => T): { writes: string[]; result: T } {
5
+ const writes: string[] = [];
6
+ vi.spyOn(process.stderr, "write").mockImplementation((chunk: unknown) => {
7
+ writes.push(String(chunk));
8
+ return true;
9
+ });
10
+ Object.defineProperty(process.stderr, "isTTY", { value: true, configurable: true });
11
+ Object.defineProperty(process.stderr, "columns", { value: 120, configurable: true });
12
+ Object.defineProperty(process.stderr, "rows", { value: 40, configurable: true });
13
+ return { writes, result: fn() };
14
+ }
15
+
16
+ describe("Live carried rows", () => {
17
+ afterEach(() => {
18
+ vi.restoreAllMocks();
19
+ });
20
+
21
+ it("renders a carried row as already-done from the first frame, not waiting for a slot", () => {
22
+ const rows: LiveRow[] = [
23
+ { evalId: "memory/carried-fail", who: "codex-e2b", total: 1, carriedVerdict: "failed" },
24
+ { evalId: "memory/fresh", who: "codex-e2b", total: 1 },
25
+ ];
26
+ const live = Live(rows, 2);
27
+
28
+ const { writes } = withMockTty(() => {
29
+ live.onRunStart?.([], { name: "codex", kind: "sandbox" } as never, {
30
+ evals: 2,
31
+ configs: 1,
32
+ totalRuns: 1,
33
+ maxConcurrency: 5,
34
+ });
35
+ });
36
+ vi.spyOn(process.stdout, "write").mockImplementation(() => true);
37
+ live.onRunComplete?.({
38
+ startedAt: "",
39
+ finishedAt: "",
40
+ durationMs: 0,
41
+ results: [],
42
+ totalRuns: 1,
43
+ } as never);
44
+
45
+ const out = writes.join("");
46
+ const carriedLine = out.split("\n").find((l) => l.includes("memory/carried-fail"));
47
+ const freshLine = out.split("\n").find((l) => l.includes("memory/fresh"));
48
+
49
+ expect(carriedLine).toBeDefined();
50
+ expect(carriedLine).toContain("✗");
51
+ expect(carriedLine).not.toContain("waiting");
52
+
53
+ expect(freshLine).toBeDefined();
54
+ expect(freshLine).toContain("waiting");
55
+ });
56
+ });
@@ -5,7 +5,9 @@
5
5
  import type { Reporter, ReporterEvent, RunShape, RunSummary } from "../../types.ts";
6
6
  import { t } from "../../i18n/index.ts";
7
7
  import { renderRunReport } from "./table.ts";
8
- import { outcomeSymbol, WAITING_SYM } from "./shared.ts";
8
+ import { verdictSymbol, WAITING_SYM } from "./shared.ts";
9
+ import { runWho } from "../types.ts";
10
+ import { onBeforeExternalTerminalWrite } from "../../tty-line.ts";
9
11
 
10
12
  const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
11
13
 
@@ -31,6 +33,13 @@ export interface LiveRow {
31
33
  who: string;
32
34
  /** 该 (evalId, who) 对的总 attempt 数(含 earlyExit 估算值)。 */
33
35
  total: number;
36
+ /**
37
+ * 设置时表示这一行从第一帧起就是携入(carry)的结果,不会真的调度 attempt——直接按这个
38
+ * verdict 渲染成已完成,不经过 waiting → spinner 的过程。没有它,carry 掉的行会卡在
39
+ * "waiting for a slot"直到进程退出:它永远等不到 eval:start,因为 run.ts 压根不会为
40
+ * 它派发 attempt。
41
+ */
42
+ carriedVerdict?: string;
34
43
  }
35
44
 
36
45
  export interface LiveReporter extends Reporter {
@@ -41,7 +50,7 @@ export interface LiveReporter extends Reporter {
41
50
  interface RowState extends LiveRow {
42
51
  completed: number;
43
52
  lastMsg: string;
44
- dominantOutcome: string | undefined;
53
+ dominantVerdict: string | undefined;
45
54
  /** true 一旦拿到并发名额、attempt effect 真正开始跑;之前是排队等待,不该转圈误导。 */
46
55
  started: boolean;
47
56
  }
@@ -49,12 +58,21 @@ interface RowState extends LiveRow {
49
58
  export function Live(rows: LiveRow[], totalAttempts: number): LiveReporter {
50
59
  const stateMap = new Map<string, RowState>();
51
60
  const keyOrder: string[] = [];
61
+ let totalCompleted = 0;
52
62
 
53
63
  for (const r of rows) {
54
64
  const k = `${r.evalId}|${r.who}`;
55
65
  if (!stateMap.has(k)) {
56
- stateMap.set(k, { ...r, completed: 0, lastMsg: "", dominantOutcome: undefined, started: false });
66
+ const carried = r.carriedVerdict !== undefined;
67
+ stateMap.set(k, {
68
+ ...r,
69
+ completed: carried ? r.total : 0,
70
+ lastMsg: "",
71
+ dominantVerdict: r.carriedVerdict,
72
+ started: carried,
73
+ });
57
74
  keyOrder.push(k);
75
+ if (carried) totalCompleted += r.total;
58
76
  } else {
59
77
  // 同一 (evalId, who) 可能在多个 agentRun 里出现(不应发生,但做防御)
60
78
  stateMap.get(k)!.total += r.total;
@@ -62,17 +80,17 @@ export function Live(rows: LiveRow[], totalAttempts: number): LiveReporter {
62
80
  }
63
81
 
64
82
  let spinFrame = 0;
65
- let totalCompleted = 0;
66
83
  let drawnLines = 0; // 上次 draw() 写了多少行,用于 \x1B[nA 回跳
67
84
  let intervalId: ReturnType<typeof setInterval> | undefined;
68
85
  let shape: RunShape | undefined;
86
+ let unsubscribeExternalWrite: (() => void) | undefined;
69
87
 
70
88
  const cols = () => process.stderr.columns || 100;
71
89
 
72
90
  function renderRow(state: RowState, frame: number): string {
73
91
  const done = state.completed >= state.total;
74
92
  const sym = done
75
- ? outcomeSymbol(state.dominantOutcome ?? "")
93
+ ? verdictSymbol(state.dominantVerdict ?? "")
76
94
  : state.started
77
95
  ? SPINNER[frame % SPINNER.length]
78
96
  : WAITING_SYM;
@@ -176,13 +194,17 @@ export function Live(rows: LiveRow[], totalAttempts: number): LiveReporter {
176
194
 
177
195
  onEvent(event: ReporterEvent) {
178
196
  if (event.type !== "eval:start") return;
179
- const who = event.model ? `${event.agent.name}/${event.model}` : event.agent.name;
197
+ const who = runWho({ agentName: event.agent.name, model: event.model, experimentId: event.experimentId });
180
198
  const state = stateMap.get(`${event.eval.id}|${who}`);
181
199
  if (state) state.started = true;
182
200
  },
183
201
 
184
202
  onRunStart(_evals, _agent, s) {
185
203
  shape = s;
204
+ // sandbox teardown 失败、budget 不可执行这类独立诊断行可能绕开 progress()/onEvalComplete
205
+ // 直接落地(见 tty-line.ts)。订阅后先把已画的表格清掉、drawnLines 归零,下一帧从新起点
206
+ // 重画,不然回跳量和实际光标错位,越滚越多。
207
+ unsubscribeExternalWrite = onBeforeExternalTerminalWrite(() => clearDisplay());
186
208
  // 初始渲染:让用户看到行表
187
209
  draw(0);
188
210
  intervalId = setInterval(() => {
@@ -192,14 +214,14 @@ export function Live(rows: LiveRow[], totalAttempts: number): LiveReporter {
192
214
  },
193
215
 
194
216
  onEvalComplete(result) {
195
- const who = result.model ? `${result.agent}/${result.model}` : result.agent;
217
+ const who = runWho({ agentName: result.agent, model: result.model, experimentId: result.experimentId });
196
218
  const state = stateMap.get(`${result.id}|${who}`);
197
219
  if (state) {
198
220
  state.completed += 1;
199
221
  totalCompleted += 1;
200
- const prev = state.dominantOutcome;
201
- if (!prev || (prev === "passed" && result.outcome !== "passed")) {
202
- state.dominantOutcome = result.outcome;
222
+ const prev = state.dominantVerdict;
223
+ if (!prev || (prev === "passed" && result.verdict !== "passed")) {
224
+ state.dominantVerdict = result.verdict;
203
225
  }
204
226
  } else {
205
227
  totalCompleted += 1;
@@ -211,6 +233,10 @@ export function Live(rows: LiveRow[], totalAttempts: number): LiveReporter {
211
233
  clearInterval(intervalId);
212
234
  intervalId = undefined;
213
235
  }
236
+ if (unsubscribeExternalWrite) {
237
+ unsubscribeExternalWrite();
238
+ unsubscribeExternalWrite = undefined;
239
+ }
214
240
  // 最后刷一帧(所有行都 done)
215
241
  draw(spinFrame);
216
242
  clearDisplay();