niceeval 0.5.4 → 0.6.1

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 (337) hide show
  1. package/INDEX.md +55 -0
  2. package/dist/agents/types.d.ts +321 -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/otlp/parse.d.ts +3 -0
  9. package/dist/o11y/otlp/receiver.d.ts +21 -0
  10. package/dist/o11y/otlp/turn-otel.d.ts +43 -0
  11. package/dist/o11y/types.d.ts +190 -0
  12. package/dist/report/aggregate.d.ts +57 -0
  13. package/dist/report/aggregate.js +212 -0
  14. package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
  15. package/dist/report/built-ins/experiment-comparison.js +13 -0
  16. package/dist/report/built-ins/index.d.ts +1 -0
  17. package/dist/report/built-ins/index.js +2 -0
  18. package/dist/report/components.d.ts +180 -0
  19. package/dist/report/components.js +132 -0
  20. package/dist/report/compute.d.ts +106 -0
  21. package/dist/report/compute.js +600 -0
  22. package/dist/report/flag.d.ts +6 -0
  23. package/dist/report/flag.js +8 -0
  24. package/dist/report/format.d.ts +26 -0
  25. package/dist/report/format.js +109 -0
  26. package/dist/report/index.d.ts +19 -0
  27. package/dist/report/index.js +30 -0
  28. package/dist/report/load.d.ts +12 -0
  29. package/dist/report/load.js +56 -0
  30. package/dist/report/locale.d.ts +94 -0
  31. package/dist/report/locale.js +183 -0
  32. package/dist/report/metrics.d.ts +21 -0
  33. package/dist/report/metrics.js +115 -0
  34. package/dist/report/primitives.d.ts +64 -0
  35. package/dist/report/primitives.js +113 -0
  36. package/dist/report/react/AttemptList.d.ts +23 -0
  37. package/dist/report/react/AttemptList.js +19 -0
  38. package/dist/report/react/DeltaTable.d.ts +8 -0
  39. package/dist/report/react/DeltaTable.js +24 -0
  40. package/dist/report/react/EvalList.d.ts +10 -0
  41. package/dist/report/react/EvalList.js +0 -0
  42. package/dist/report/react/ExperimentList.d.ts +11 -0
  43. package/dist/report/react/ExperimentList.js +67 -0
  44. package/dist/report/react/GroupSummary.d.ts +8 -0
  45. package/dist/report/react/GroupSummary.js +8 -0
  46. package/dist/report/react/MetricBars.d.ts +10 -0
  47. package/dist/report/react/MetricBars.js +48 -0
  48. package/dist/report/react/MetricLine.d.ts +9 -0
  49. package/dist/report/react/MetricLine.js +68 -0
  50. package/dist/report/react/MetricMatrix.d.ts +10 -0
  51. package/dist/report/react/MetricMatrix.js +23 -0
  52. package/dist/report/react/MetricScatter.d.ts +9 -0
  53. package/dist/report/react/MetricScatter.js +96 -0
  54. package/dist/report/react/MetricTable.d.ts +11 -0
  55. package/dist/report/react/MetricTable.js +26 -0
  56. package/dist/report/react/RunOverview.d.ts +8 -0
  57. package/dist/report/react/RunOverview.js +12 -0
  58. package/dist/report/react/Scoreboard.d.ts +8 -0
  59. package/dist/report/react/Scoreboard.js +26 -0
  60. package/dist/report/react/cell.d.ts +9 -0
  61. package/dist/report/react/cell.js +9 -0
  62. package/dist/report/react/chart-math.d.ts +14 -0
  63. package/dist/report/react/chart-math.js +68 -0
  64. package/dist/report/react/colors.d.ts +14 -0
  65. package/dist/report/react/colors.js +45 -0
  66. package/dist/report/react/fixtures.d.ts +19 -0
  67. package/dist/report/react/fixtures.js +398 -0
  68. package/dist/report/react/format.d.ts +3 -0
  69. package/dist/report/react/format.js +7 -0
  70. package/dist/report/react/index.d.ts +6 -0
  71. package/dist/report/react/index.js +14 -0
  72. package/dist/report/report.d.ts +24 -0
  73. package/dist/report/report.js +49 -0
  74. package/dist/report/text/faces.d.ts +29 -0
  75. package/dist/report/text/faces.js +541 -0
  76. package/dist/report/text/layout.d.ts +75 -0
  77. package/dist/report/text/layout.js +173 -0
  78. package/dist/report/text/plot.d.ts +37 -0
  79. package/dist/report/text/plot.js +81 -0
  80. package/dist/report/text/table.d.ts +8 -0
  81. package/dist/report/text/table.js +86 -0
  82. package/dist/report/tree.d.ts +99 -0
  83. package/dist/report/tree.js +228 -0
  84. package/dist/report/types.d.ts +408 -0
  85. package/dist/report/types.js +4 -0
  86. package/dist/report/web.d.ts +15 -0
  87. package/dist/report/web.js +37 -0
  88. package/dist/results/locator.d.ts +75 -0
  89. package/dist/results/locator.js +0 -0
  90. package/dist/results/select.d.ts +45 -0
  91. package/dist/results/select.js +230 -0
  92. package/dist/results/source-hash.d.ts +9 -0
  93. package/dist/results/types.d.ts +204 -0
  94. package/dist/runner/eval-source.d.ts +22 -0
  95. package/dist/runner/fingerprint.d.ts +23 -0
  96. package/dist/runner/types.d.ts +332 -0
  97. package/dist/sandbox/docker-stream.d.ts +24 -0
  98. package/dist/sandbox/docker.d.ts +84 -0
  99. package/dist/sandbox/e2b.d.ts +31 -0
  100. package/dist/sandbox/errors.d.ts +4 -0
  101. package/dist/sandbox/local-files.d.ts +6 -0
  102. package/dist/sandbox/paths.d.ts +4 -0
  103. package/dist/sandbox/registry.d.ts +13 -0
  104. package/dist/sandbox/resolve.d.ts +40 -0
  105. package/dist/sandbox/retry.d.ts +10 -0
  106. package/dist/sandbox/shell.d.ts +11 -0
  107. package/dist/sandbox/source-files.d.ts +13 -0
  108. package/dist/sandbox/types.d.ts +203 -0
  109. package/dist/sandbox/vercel.d.ts +37 -0
  110. package/dist/scoring/types.d.ts +70 -0
  111. package/dist/shared/aggregate.d.ts +29 -0
  112. package/dist/shared/aggregate.js +55 -0
  113. package/dist/shared/types.d.ts +27 -0
  114. package/dist/shared/verdict.d.ts +27 -0
  115. package/dist/shared/verdict.js +34 -0
  116. package/dist/tty-line.d.ts +6 -0
  117. package/dist/types.d.ts +7 -0
  118. package/dist/util.d.ts +22 -0
  119. package/docs-site/zh/concepts/adapter.mdx +7 -7
  120. package/docs-site/zh/concepts/assert.mdx +19 -18
  121. package/docs-site/zh/concepts/evals.mdx +12 -11
  122. package/docs-site/zh/concepts/experiment.mdx +2 -2
  123. package/docs-site/zh/concepts/judge.mdx +9 -9
  124. package/docs-site/zh/concepts/overview.mdx +3 -3
  125. package/docs-site/zh/concepts/tier.mdx +6 -6
  126. package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
  127. package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
  128. package/docs-site/zh/guides/agent-feedback-loop.mdx +237 -78
  129. package/docs-site/zh/guides/connect-otel.mdx +3 -3
  130. package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
  131. package/docs-site/zh/guides/custom-reports.mdx +196 -36
  132. package/docs-site/zh/guides/experiments.mdx +7 -1
  133. package/docs-site/zh/guides/fixtures.mdx +4 -2
  134. package/docs-site/zh/guides/official-adapters.mdx +31 -9
  135. package/docs-site/zh/guides/publish-report.mdx +4 -4
  136. package/docs-site/zh/guides/report-components.mdx +194 -55
  137. package/docs-site/zh/guides/reporters.mdx +2 -2
  138. package/docs-site/zh/guides/results-data.mdx +61 -34
  139. package/docs-site/zh/guides/runner.mdx +2 -2
  140. package/docs-site/zh/guides/sandbox-agent.mdx +3 -2
  141. package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +12 -12
  142. package/docs-site/zh/guides/viewing-results.mdx +200 -79
  143. package/docs-site/zh/guides/write-experiment.mdx +9 -1
  144. package/docs-site/zh/guides/write-send.mdx +4 -3
  145. package/docs-site/zh/index.mdx +4 -3
  146. package/docs-site/zh/introduction.mdx +2 -1
  147. package/docs-site/zh/quickstart.mdx +5 -3
  148. package/docs-site/zh/reference/builtin-agents.mdx +38 -13
  149. package/docs-site/zh/reference/capabilities.mdx +1 -1
  150. package/docs-site/zh/reference/cli.mdx +36 -13
  151. package/docs-site/zh/reference/define-agent.mdx +15 -13
  152. package/docs-site/zh/reference/define-config.mdx +1 -1
  153. package/docs-site/zh/reference/define-eval.mdx +3 -3
  154. package/docs-site/zh/reference/events.mdx +9 -3
  155. package/docs-site/zh/reference/expect.mdx +13 -8
  156. package/package.json +10 -6
  157. package/src/agents/bub.ts +129 -42
  158. package/src/agents/claude-code.test.ts +152 -0
  159. package/src/agents/claude-code.ts +127 -13
  160. package/src/agents/codex.test.ts +192 -0
  161. package/src/agents/codex.ts +151 -12
  162. package/src/agents/index.ts +9 -3
  163. package/src/agents/manifest.ts +41 -0
  164. package/src/agents/plugin-config.test.ts +94 -0
  165. package/src/agents/shared.ts +33 -2
  166. package/src/agents/skills.test.ts +214 -0
  167. package/src/agents/skills.ts +304 -0
  168. package/src/agents/streaming.ts +5 -5
  169. package/src/agents/types.ts +59 -3
  170. package/src/cli.ts +74 -47
  171. package/src/context/context.test.ts +20 -2
  172. package/src/context/control-flow.ts +1 -1
  173. package/src/context/session.test.ts +92 -1
  174. package/src/context/session.ts +23 -1
  175. package/src/context/types.ts +4 -4
  176. package/src/define.test.ts +8 -8
  177. package/src/define.ts +11 -11
  178. package/src/expect/index.ts +5 -5
  179. package/src/i18n/en.ts +33 -15
  180. package/src/i18n/zh-CN.ts +37 -21
  181. package/src/index.ts +1 -1
  182. package/src/o11y/execution-tree.test.ts +452 -0
  183. package/src/o11y/execution-tree.ts +367 -0
  184. package/src/o11y/otlp/receiver.ts +1 -1
  185. package/src/o11y/otlp/select.ts +7 -3
  186. package/src/o11y/parsers/claude-code.test.ts +175 -0
  187. package/src/o11y/parsers/claude-code.ts +32 -0
  188. package/src/o11y/types.ts +6 -1
  189. package/src/report/aggregate.ts +26 -20
  190. package/src/report/built-in-user-parity.test.tsx +640 -0
  191. package/src/report/built-ins/experiment-comparison.tsx +19 -0
  192. package/src/report/built-ins/index.ts +2 -0
  193. package/src/report/components.tsx +225 -50
  194. package/src/report/compute.ts +335 -54
  195. package/src/report/dual-render.test.tsx +1129 -0
  196. package/src/report/flag.ts +1 -1
  197. package/src/report/format.ts +35 -2
  198. package/src/report/index.ts +47 -14
  199. package/src/report/load.ts +1 -1
  200. package/src/report/locale.ts +229 -0
  201. package/src/report/metrics.ts +39 -15
  202. package/src/report/primitives.tsx +100 -2
  203. package/src/report/react/AttemptList.tsx +113 -0
  204. package/src/report/react/DeltaTable.tsx +9 -6
  205. package/src/report/react/EvalList.tsx +0 -0
  206. package/src/report/react/ExperimentList.tsx +220 -0
  207. package/src/report/react/GroupSummary.tsx +66 -0
  208. package/src/report/react/MetricBars.tsx +24 -13
  209. package/src/report/react/MetricLine.tsx +21 -21
  210. package/src/report/react/MetricMatrix.tsx +8 -4
  211. package/src/report/react/MetricScatter.tsx +126 -96
  212. package/src/report/react/MetricTable.tsx +115 -15
  213. package/src/report/react/RunOverview.tsx +48 -26
  214. package/src/report/react/Scoreboard.tsx +15 -7
  215. package/src/report/react/cell.tsx +18 -9
  216. package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
  217. package/src/report/react/colors.ts +24 -15
  218. package/src/report/react/enhance.js +168 -0
  219. package/src/report/react/fixtures.ts +169 -34
  220. package/src/report/react/format.ts +1 -1
  221. package/src/report/react/index.tsx +21 -6
  222. package/src/report/react/render.test.tsx +180 -31
  223. package/src/report/react/styles.css +432 -213
  224. package/src/report/report.test.ts +398 -94
  225. package/src/report/report.ts +29 -11
  226. package/src/report/text/faces.ts +344 -96
  227. package/src/report/text/layout.ts +76 -11
  228. package/src/report/text/table.ts +98 -0
  229. package/src/report/tree.ts +103 -20
  230. package/src/report/types.ts +184 -25
  231. package/src/report/web.ts +46 -19
  232. package/src/results/annotated-source.test.ts +146 -0
  233. package/src/results/annotated-source.ts +128 -0
  234. package/src/results/attempt-evidence.test.ts +158 -0
  235. package/src/results/attempt-evidence.ts +158 -0
  236. package/src/results/attempt-source.ts +48 -0
  237. package/src/results/copy.ts +154 -139
  238. package/src/results/format.ts +54 -47
  239. package/src/results/host-equivalence.test.ts +527 -0
  240. package/src/results/index.ts +56 -14
  241. package/src/results/locator.test.ts +198 -0
  242. package/src/results/locator.ts +0 -0
  243. package/src/results/open.ts +392 -175
  244. package/src/results/results.test.ts +943 -494
  245. package/src/results/select.ts +129 -23
  246. package/src/results/skipped-notice.ts +0 -0
  247. package/src/results/source-hash.ts +28 -0
  248. package/src/results/types.ts +98 -62
  249. package/src/results/writer.ts +304 -176
  250. package/src/runner/attempt.test.ts +170 -0
  251. package/src/runner/attempt.ts +48 -13
  252. package/src/runner/discover.test.ts +65 -0
  253. package/src/runner/discover.ts +6 -2
  254. package/src/runner/eval-source.test.ts +121 -0
  255. package/src/runner/eval-source.ts +45 -0
  256. package/src/runner/fingerprint.ts +54 -1
  257. package/src/runner/remote-sandbox.ts +1 -1
  258. package/src/runner/report.test.ts +6 -8
  259. package/src/runner/report.ts +5 -4
  260. package/src/runner/reporters/artifacts.ts +27 -41
  261. package/src/runner/reporters/braintrust.test.ts +3 -3
  262. package/src/runner/reporters/braintrust.ts +1 -1
  263. package/src/runner/reporters/console.ts +6 -6
  264. package/src/runner/reporters/json.ts +4 -4
  265. package/src/runner/reporters/live.test.ts +56 -0
  266. package/src/runner/reporters/live.ts +36 -10
  267. package/src/runner/reporters/quiet.test.ts +66 -0
  268. package/src/runner/reporters/quiet.ts +49 -0
  269. package/src/runner/reporters/shared.ts +5 -5
  270. package/src/runner/reporters/table.ts +17 -17
  271. package/src/runner/run.ts +61 -102
  272. package/src/runner/types.ts +43 -32
  273. package/src/sandbox/checkpoint.ts +1 -1
  274. package/src/sandbox/docker.ts +15 -2
  275. package/src/sandbox/e2b.ts +11 -5
  276. package/src/sandbox/errors.ts +13 -0
  277. package/src/sandbox/index.ts +3 -3
  278. package/src/sandbox/local-files.ts +1 -1
  279. package/src/sandbox/registry.ts +6 -5
  280. package/src/sandbox/resolve.ts +38 -23
  281. package/src/sandbox/retry.test.ts +68 -0
  282. package/src/sandbox/retry.ts +44 -0
  283. package/src/sandbox/types.ts +24 -24
  284. package/src/sandbox/vercel.ts +17 -3
  285. package/src/scoring/collector.ts +3 -3
  286. package/src/scoring/judge.ts +2 -2
  287. package/src/scoring/scoped.ts +13 -1
  288. package/src/scoring/types.ts +7 -7
  289. package/src/scoring/verdict.ts +4 -4
  290. package/src/shared/aggregate.ts +23 -3
  291. package/src/shared/types.ts +2 -2
  292. package/src/shared/{outcome.ts → verdict.ts} +15 -15
  293. package/src/show/compose.ts +12 -132
  294. package/src/show/index.ts +119 -95
  295. package/src/show/render.ts +462 -159
  296. package/src/show/show.test.ts +471 -253
  297. package/src/source-loc.ts +1 -1
  298. package/src/tty-line.ts +32 -0
  299. package/src/types.ts +2 -2
  300. package/src/util.test.ts +21 -1
  301. package/src/util.ts +5 -1
  302. package/src/view/app/App.tsx +33 -198
  303. package/src/view/app/components/AttemptModal.tsx +6 -6
  304. package/src/view/app/components/CodeView.tsx +2 -2
  305. package/src/view/app/components/CopyControls.tsx +32 -36
  306. package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
  307. package/src/view/app/components/ui/badge.tsx +1 -1
  308. package/src/view/app/components/ui/dialog.tsx +2 -2
  309. package/src/view/app/i18n.ts +40 -31
  310. package/src/view/app/lib/artifact-url.ts +1 -1
  311. package/src/view/app/lib/attempt-route.test.ts +37 -47
  312. package/src/view/app/lib/attempt-route.ts +26 -26
  313. package/src/view/app/lib/rows.ts +9 -112
  314. package/src/view/app/lib/verdict.ts +25 -0
  315. package/src/view/app/main.tsx +9 -12
  316. package/src/view/app/pages/RunsPage.tsx +4 -4
  317. package/src/view/app/pages/TracesPage.tsx +3 -3
  318. package/src/view/app/shared.ts +0 -1
  319. package/src/view/app/types.ts +6 -51
  320. package/src/view/artifact-serving.test.ts +136 -0
  321. package/src/view/client-dist/app.css +1 -1
  322. package/src/view/client-dist/app.js +21 -21
  323. package/src/view/data.test.ts +214 -143
  324. package/src/view/data.ts +166 -120
  325. package/src/view/index.ts +23 -11
  326. package/src/view/server.ts +52 -17
  327. package/src/view/shared/types.ts +22 -31
  328. package/src/view/styles.css +117 -327
  329. package/src/view/view-report.test.ts +146 -98
  330. package/src/report/default-report.tsx +0 -222
  331. package/src/report/dual-face.test.tsx +0 -527
  332. package/src/report/react/CaseList.tsx +0 -70
  333. package/src/view/app/components/CostScoreChart.tsx +0 -147
  334. package/src/view/app/components/ExperimentTable.tsx +0 -284
  335. package/src/view/app/components/GroupSelector.tsx +0 -61
  336. package/src/view/app/components/primitives.tsx +0 -43
  337. package/src/view/app/lib/outcome.ts +0 -82
@@ -0,0 +1,49 @@
1
+ // Quiet 报告器:--quiet 下的最小结果流。进度流照旧(attempt 无 onProgress 时直写 stderr),
2
+ // 这里只补「坏结果」:verdict 为 errored / failed 的结果各写一行 stderr,passed / skipped
3
+ // 静默 —— 保证 --quiet 下起沙箱失败这类执行错不会全程无声、只能事后读 summary.json。
4
+ // 结果流仍走统一的 Reporter 管线,不在 attempt 里散写。
5
+
6
+ import type { EvalResult, Reporter } from "../../types.ts";
7
+ import { t } from "../../i18n/index.ts";
8
+ import { runWho } from "../types.ts";
9
+ import { verdictSymbol } from "./shared.ts";
10
+
11
+ /** error / 断言 detail 的截断上限;比 Console 的 400 更紧,--quiet 只要能定位问题。 */
12
+ const DETAIL_MAX = 200;
13
+
14
+ /** 纯函数:一条结果 → 该写 stderr 的行;passed / skipped 返回 undefined(静默)。 */
15
+ export function quietLine(result: EvalResult): string | undefined {
16
+ if (result.verdict !== "errored" && result.verdict !== "failed") return undefined;
17
+ // [who] 与 attempt 进度行同源(runWho),两条流才能对上同一个运行配置。
18
+ const who = runWho({ agentName: result.agent, model: result.model, experimentId: result.experimentId });
19
+ const verdict = result.verdict === "errored" ? t("report.errored") : t("report.failed");
20
+ const detail =
21
+ result.error !== undefined
22
+ ? `${t("report.error")}: ${truncate(result.error, DETAIL_MAX)}`
23
+ : firstFailedAssertion(result);
24
+ return ` ${verdictSymbol(result.verdict)} ${result.id} ${verdict} [${who}]${detail ? ` ${detail}` : ""}\n`;
25
+ }
26
+
27
+ function firstFailedAssertion(result: EvalResult): string | undefined {
28
+ const a = result.assertions.find((x) => !x.passed);
29
+ if (!a) return undefined;
30
+ const sev = a.severity === "gate" ? t("report.gate") : t("report.soft");
31
+ const thr =
32
+ a.threshold !== undefined
33
+ ? t("report.assertionThreshold", { score: a.score.toFixed(2), threshold: a.threshold })
34
+ : "";
35
+ return `${sev}: ${a.name}${thr}${a.detail ? ` — ${truncate(a.detail, DETAIL_MAX)}` : ""}`;
36
+ }
37
+
38
+ function truncate(s: string, n: number): string {
39
+ return s.length > n ? s.slice(0, n) + "…" : s;
40
+ }
41
+
42
+ export function Quiet(): Reporter {
43
+ return {
44
+ onEvalComplete(result: EvalResult) {
45
+ const line = quietLine(result);
46
+ if (line !== undefined) process.stderr.write(line);
47
+ },
48
+ };
49
+ }
@@ -1,18 +1,18 @@
1
1
  // reporter 共用的展示常量。符号表只此一份 —— console/live/table 各抄一份时,
2
2
  // 改一个符号要同步三处且不报错。
3
3
 
4
- import type { ResultOutcome } from "../../types.ts";
4
+ import type { Verdict } from "../../types.ts";
5
5
 
6
- export const OUTCOME_SYM: Record<ResultOutcome, string> = {
6
+ export const VERDICT_SYM: Record<Verdict, string> = {
7
7
  passed: "✓",
8
8
  failed: "✗",
9
9
  errored: "!",
10
10
  skipped: "○",
11
11
  };
12
12
 
13
- export function outcomeSymbol(outcome: string): string {
14
- return OUTCOME_SYM[outcome as ResultOutcome] ?? "?";
13
+ export function verdictSymbol(verdict: string): string {
14
+ return VERDICT_SYM[verdict as Verdict] ?? "?";
15
15
  }
16
16
 
17
- /** live 表格里「还没抢到并发名额」的行:和转圈的 SPINNER、完成后的 OUTCOME_SYM 三态区分开。 */
17
+ /** live 表格里「还没抢到并发名额」的行:和转圈的 SPINNER、完成后的 VERDICT_SYM 三态区分开。 */
18
18
  export const WAITING_SYM = "·";
@@ -1,11 +1,11 @@
1
1
  import type { EvalResult, RunSummary, Usage } from "../../types.ts";
2
2
  import { t } from "../../i18n/index.ts";
3
- import { outcomeSymbol } from "./shared.ts";
4
- import { foldEvalOutcome, evalLevelStats as sharedEvalLevelStats } from "../../shared/outcome.ts";
3
+ import { verdictSymbol } from "./shared.ts";
4
+ import { foldEvalVerdict, evalLevelStats as sharedEvalLevelStats } from "../../shared/verdict.ts";
5
5
  // 展示口径(耗时/成本/百分比)与聚合小工具(标签/求和/排序)与 view 共用一份实现;
6
6
  // console.ts 经这里 re-export formatDuration。
7
7
  import { formatCost, formatDuration, formatPercent } from "../../shared/format.ts";
8
- import { OUTCOME_ORDER, avg, displayExperimentName, fallbackExperimentLabel, sumMaybe, totalTokens } from "../../shared/aggregate.ts";
8
+ import { VERDICT_ORDER, avg, displayExperimentName, fallbackExperimentLabel, sumMaybe, totalTokens } from "../../shared/aggregate.ts";
9
9
 
10
10
  export { formatCost, formatDuration } from "../../shared/format.ts";
11
11
 
@@ -29,7 +29,7 @@ interface ExperimentRow {
29
29
 
30
30
  interface EvalRow {
31
31
  id: string;
32
- outcome: EvalResult["outcome"];
32
+ verdict: EvalResult["verdict"];
33
33
  reason: string;
34
34
  durationMs: number;
35
35
  tokens: number;
@@ -77,7 +77,7 @@ export function renderRunReport(summary: RunSummary): string {
77
77
  t("report.table.runs"),
78
78
  ],
79
79
  aggregateEvalRows(row.results).map((evalRow) => [
80
- formatOutcome(evalRow.outcome),
80
+ formatVerdict(evalRow.verdict),
81
81
  evalRow.id,
82
82
  evalRow.reason,
83
83
  formatDuration(evalRow.durationMs),
@@ -114,14 +114,14 @@ export function formatTokens(n: number): string {
114
114
  return String(n);
115
115
  }
116
116
 
117
- export function formatOutcome(outcome: string): string {
118
- const sym = outcomeSymbol(outcome);
119
- switch (outcome) {
117
+ export function formatVerdict(verdict: string): string {
118
+ const sym = verdictSymbol(verdict);
119
+ switch (verdict) {
120
120
  case "passed": return `${sym} ${t("report.passed")}`;
121
121
  case "failed": return `${sym} ${t("report.failed")}`;
122
122
  case "errored": return `${sym} ${t("report.errored")}`;
123
123
  case "skipped": return `${sym} ${t("report.skipped")}`;
124
- default: return `${sym} ${outcome}`;
124
+ default: return `${sym} ${verdict}`;
125
125
  }
126
126
  }
127
127
 
@@ -162,26 +162,26 @@ function aggregateEvalRows(results: EvalResult[]): EvalRow[] {
162
162
  for (const result of results) byEval.set(result.id, [...(byEval.get(result.id) ?? []), result]);
163
163
  return [...byEval.entries()]
164
164
  .map(([id, evalResults]) => {
165
- const outcome = foldEvalOutcome(evalResults);
165
+ const verdict = foldEvalVerdict(evalResults);
166
166
  const representative =
167
- evalResults.find((r) => r.outcome === outcome) ??
168
- evalResults.find((r) => r.outcome !== "passed") ??
167
+ evalResults.find((r) => r.verdict === verdict) ??
168
+ evalResults.find((r) => r.verdict !== "passed") ??
169
169
  evalResults[0]!;
170
170
  return {
171
171
  id,
172
- outcome,
173
- reason: outcome === "passed" ? t("report.passed") : reasonFor(representative),
172
+ verdict,
173
+ reason: verdict === "passed" ? t("report.passed") : reasonFor(representative),
174
174
  durationMs: avg(evalResults.map((r) => r.durationMs)),
175
175
  tokens: totalTokens(evalResults.map((r) => r.usage)),
176
176
  cost: sumMaybe(evalResults.map((r) => r.estimatedCostUSD)),
177
- passedAttempts: evalResults.filter((r) => r.outcome === "passed").length,
177
+ passedAttempts: evalResults.filter((r) => r.verdict === "passed").length,
178
178
  attempts: evalResults.length,
179
179
  };
180
180
  })
181
- .sort((a, b) => OUTCOME_ORDER[a.outcome] - OUTCOME_ORDER[b.outcome] || a.id.localeCompare(b.id));
181
+ .sort((a, b) => VERDICT_ORDER[a.verdict] - VERDICT_ORDER[b.verdict] || a.id.localeCompare(b.id));
182
182
  }
183
183
 
184
- // 折叠 / 计票口径与 view 同一份实现(src/shared/outcome.ts),CLI 表格和网页榜单永不打架。
184
+ // 折叠 / 计票口径与 view 同一份实现(src/shared/verdict.ts),CLI 表格和网页榜单永不打架。
185
185
  function evalLevelStats(results: EvalResult[]) {
186
186
  return sharedEvalLevelStats(results, (r) => r.id);
187
187
  }
package/src/runner/run.ts CHANGED
@@ -3,13 +3,14 @@
3
3
  // reporter 编排 / 汇总在 report.ts,Sandbox 适配器在 remote-sandbox.ts。
4
4
 
5
5
  import { readFile } from "node:fs/promises";
6
- import { Effect, Cause, Duration, Exit } from "effect";
6
+ import { Effect, Cause, Exit } from "effect";
7
7
  import { probeJudge } from "../scoring/judge.ts";
8
8
  import { t } from "../i18n/index.ts";
9
- import { cacheKey, computeFingerprint } from "./fingerprint.ts";
9
+ import { cacheKey, planCarry } from "./fingerprint.ts";
10
10
  import { OtelReceiverPool } from "../o11y/otlp/turn-otel.ts";
11
11
  import { runAttemptEffect } from "./attempt.ts";
12
12
  import { runReporter, emitReporterEvent, scopeReporter, summarize } from "./report.ts";
13
+ import { writeStderrLine } from "../tty-line.ts";
13
14
  import type { Agent, EvalResult, JudgeConfig, Reporter, RunShape, RunSummary } from "../types.ts";
14
15
  import type { AgentRun, Attempt, RunOptions } from "./types.ts";
15
16
 
@@ -54,42 +55,14 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
54
55
  return p;
55
56
  };
56
57
 
57
- const plannedFingerprints = new Map<string, string>();
58
- {
59
- const jobs: Promise<void>[] = [];
60
- for (const run of opts.agentRuns) {
61
- for (const evalDef of opts.evals.filter((e) => run.evalFilter(e.id))) {
62
- jobs.push(
63
- computeFingerprint(evalDef, run, sourceCache).then((fp) => {
64
- plannedFingerprints.set(cacheKey(run, evalDef.id), fp);
65
- }),
66
- );
67
- }
68
- }
69
- await Promise.all(jobs);
70
- }
71
-
72
- // 跨实验结果复用:只有上次 passed 且 fingerprint 匹配的 (experimentId, evalId) 组合直接携入。
73
- // 失败/错误/跳过/fingerprint 不匹配都会重跑。--force 跳过此逻辑。
74
- const priorRunKeys = new Set<string>();
75
- const carriedResults: EvalResult[] = [];
76
- if (opts.priorResults?.length) {
77
- for (const r of opts.priorResults) {
78
- if (!r.experimentId) continue;
79
- const key = `${r.experimentId}|${r.id}`;
80
- if (r.outcome === "passed" && r.fingerprint !== undefined && r.fingerprint === plannedFingerprints.get(key)) {
81
- priorRunKeys.add(key);
82
- }
83
- }
84
- for (const r of opts.priorResults) {
85
- if (!r.experimentId || !priorRunKeys.has(`${r.experimentId}|${r.id}`)) continue;
86
- // artifactsDir 是相对"本次 run 自己目录"的路径,新 summary 换了目录就会失效,必须清掉。
87
- // artifactBase 不一样:它已经是 loadLatestResultsPerEval(经 withViewRefs)拼好的、
88
- // 相对稳定的 .niceeval 根的路径,指向旧 run 的产物目录依然可解析,原样带过来
89
- // ——不然 view 就再也找不到这条携带结果的源码/转录/trace 了。
90
- carriedResults.push({ ...r, artifactsDir: undefined });
91
- }
92
- }
58
+ // 跨实验结果复用:上次 passed failed 且 fingerprint 匹配的 (experimentId, evalId) 组合
59
+ // 直接携入 —— 两者都是"跑完了、判定确定"的终态,没有理由重花一次 agent/sandbox 成本去
60
+ // 复现同一个已知结果。errored 是框架/环境层面的不确定失败(超时、沙箱挂了、judge 探测失败
61
+ // 等),判定本身不可信,必须重跑。跳过/fingerprint 不匹配同样重跑。--force 跳过此逻辑
62
+ // (cli.ts --force 时不传 priorResults,也不算 carryPlan)
63
+ // carryPlan 优先用调用方(cli.ts,为了 live 表格)已经算好的那份,不重算一遍。
64
+ const { plannedFingerprints, priorRunKeys, carriedResults } =
65
+ opts.carryPlan ?? (await planCarry(opts.evals, opts.agentRuns, opts.priorResults));
93
66
 
94
67
  // 展开 attempts
95
68
  // 外层按「round」(run index)迭代,内层按 eval 迭代:同一 key 的第 i+1 次 attempt 排在
@@ -207,17 +180,16 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
207
180
  // 真的没过)才代表 agent 行为的样本,值得跑满 runs 去测通过率。earlyExit 开时两者都提前收尾。
208
181
  const erroredKeys = new Set<string>();
209
182
 
210
- // budget 护栏带「在飞预扣」:实测成本只有 attempt 完成后才知道,若只检查已完成花费,
211
- // maxConcurrency attempt 会在任何成本回写前全部起飞,实际花费能冲到 budget 的十几倍。
212
- // 口径:还没有任何【带成本】的完成样本时,同一 budgetKey 只放一个 attempt 在飞(探单次成本);
213
- // 有样本后,用平均实测成本给每个在飞 attempt 预扣,预计总额到顶就等,已花到顶就停。
214
- // costSamples 只数报了成本的 attempt —— 不报成本的完成(agent 无用量、模型不在价格表、
215
- // 早期 errored)不能算 0 元样本,否则均值被拉成 0,护栏彻底失效。连续多次完成都拿不到
216
- // 成本时,说明这个 agent budget 根本不可执行:警告一次然后放行,而不是永远串行装样子。
183
+ // budget 护栏:只按「已完成 attempt 的实测花费」判断,不做预测性节流。之前的实现会按
184
+ // 「平均成本 × 在飞数」预扣,快到顶就让还没起飞的 attempt 排队等——这在探测阶段(还没有任何
185
+ // 成本样本时)等价于把同一 budgetKey 的并发摁到一个很小的数,且完全没有文档承诺过这个副作用
186
+ // (`docs-site/zh/guides/write-experiment.mdx` `budget` 的描述只有一句「这一格配置的预算
187
+ // 上限」)。新语义:已完成 attempt 的花费加总一旦到顶,就不再放新 attempt 起飞(已经在飞的
188
+ // 照常跑完,不会被中途打断);到顶之前不做任何预测性限流,并发完全由 globalSem / runSem 决定。
189
+ // 代价是「已花 + 在飞未结算」的总花费可能短暂超出 budget——这是有意识的取舍:budget 是防止
190
+ // 无限烧钱的安全网,不是精确计费闸,不应该反过来限制吞吐。
217
191
  interface BudgetState {
218
192
  spent: number;
219
- inflight: number;
220
- costSamples: number;
221
193
  completedNoCost: number;
222
194
  unenforceableWarned: boolean;
223
195
  }
@@ -225,7 +197,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
225
197
  const budgetState = (key: string): BudgetState => {
226
198
  let s = budgetStates.get(key);
227
199
  if (!s) {
228
- s = { spent: 0, inflight: 0, costSamples: 0, completedNoCost: 0, unenforceableWarned: false };
200
+ s = { spent: 0, completedNoCost: 0, unenforceableWarned: false };
229
201
  budgetStates.set(key, s);
230
202
  }
231
203
  return s;
@@ -236,7 +208,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
236
208
  // 并发 worker 交错写 → 用一个 permit=1 的信号量串起来(替代原先手搓的 reportQueue 链)。
237
209
  const reportMutex = Effect.runSync(Effect.makeSemaphore(1));
238
210
  // 沙箱启动单独限流:与 agent 并发(maxConcurrency)解耦,防高并发下 daemon/API 过载。
239
- // 未显式指定时跟 maxConcurrency 走——各 backend 的推荐值已在 cli 层写进 maxConcurrency 默认值。
211
+ // 未显式指定时跟 maxConcurrency 走——各 provider 的推荐值已在 cli 层写进 maxConcurrency 默认值。
240
212
  const sandboxSem = Effect.runSync(Effect.makeSemaphore(opts.maxConcurrency));
241
213
 
242
214
  // 两级并发闸:全局(opts.maxConcurrency)+ 实验级(AgentRun.maxConcurrency,可选)。
@@ -286,10 +258,15 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
286
258
  Effect.forEach(
287
259
  attempts,
288
260
  (a) => {
289
- const body = Effect.gen(function* () {
290
- // 早停:同 key 已通过,或已 errored(重跑只会重复同一个框架错误)且开了 earlyExit
291
- // → 跳过未启动的 attempt。检查必须在拿到 permit 之后(fiber unbounded 一次性
292
- // 全建的,建时结果还没出来);跳过路径短暂占一个 permit,可忽略。
261
+ const budgetKey = a.run.experimentId ?? a.run.agent.name;
262
+
263
+ // preflight:「要不要开始跑」的许可判断(首过即停 + budget 上限检查),不持有
264
+ // globalSem——两类判断都是即时返回,不该占着全局并发槽位做无谓等待。runSem(实验自己的
265
+ // maxConcurrency)例外:它是实验私有资源,preflight 占着不影响别的实验,且和 mempal
266
+ // 那类「必须串行」的语义一致,所以仍然把 preflight 包在 runSem 里面(见下方)。
267
+ const preflight = Effect.gen(function* () {
268
+ // 首过即停:同 key 已通过,或已 errored(重跑只会重复同一个框架错误)且开了 earlyExit
269
+ // → 跳过未启动的 attempt。
293
270
  if (a.run.earlyExit && (passedKeys.has(a.key) || erroredKeys.has(a.key))) {
294
271
  yield* reportMutex.withPermits(1)(
295
272
  Effect.promise(() =>
@@ -300,49 +277,32 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
300
277
  }),
301
278
  ),
302
279
  );
303
- return;
280
+ return false;
304
281
  }
305
282
 
306
283
  const budget = a.run.budget;
307
- const budgetKey = a.run.experimentId ?? a.run.agent.name;
308
284
  if (budget !== undefined) {
309
- // 预扣循环:预计花费(已花 + 在飞×均值)到顶就等在飞的结算,已花到顶就整段停。
310
- // 注意等待的 fiber 仍占一个并发位(简单可靠;混跑「带 budget + 不带 budget」
311
- // 的多实验时会牺牲些吞吐 —— budget 本就是拿速度换花费上限的模式)
312
- for (;;) {
313
- const s = budgetState(budgetKey);
314
- if (s.spent >= budget) {
315
- if (!budgetReported.has(budgetKey)) {
316
- budgetReported.add(budgetKey);
317
- yield* reportMutex.withPermits(1)(
318
- Effect.promise(() =>
319
- emitReporterEvent(reporters, { type: "run:budgetExceeded", budget, spent: s.spent }),
320
- ),
321
- );
322
- }
323
- return;
324
- }
325
- if (s.costSamples === 0 && s.completedNoCost >= 3 && !s.unenforceableWarned) {
326
- // 连续几次完成都拿不到成本:budget 对这个 agent 不可执行,说清楚再放行。
327
- s.unenforceableWarned = true;
328
- process.stderr.write(t("runner.budgetUnenforceable", { budgetKey }));
329
- }
330
- if (s.costSamples === 0 && s.unenforceableWarned) {
331
- s.inflight += 1;
332
- break;
333
- }
334
- const avg = s.costSamples > 0 ? s.spent / s.costSamples : undefined;
335
- const projected =
336
- avg === undefined ? (s.inflight > 0 ? Number.POSITIVE_INFINITY : 0) : s.spent + s.inflight * avg;
337
- if (projected < budget) {
338
- s.inflight += 1;
339
- break;
285
+ // 只看已完成 attempt 的实测花费(见上方 BudgetState 注释),到顶就跳过新 attempt,
286
+ // 没到顶就立即放行——不等待、不做预测性节流。
287
+ const s = budgetState(budgetKey);
288
+ if (s.spent >= budget) {
289
+ if (!budgetReported.has(budgetKey)) {
290
+ budgetReported.add(budgetKey);
291
+ yield* reportMutex.withPermits(1)(
292
+ Effect.promise(() =>
293
+ emitReporterEvent(reporters, { type: "run:budgetExceeded", budget, spent: s.spent }),
294
+ ),
295
+ );
340
296
  }
341
- yield* Effect.sleep(Duration.millis(200));
297
+ return false;
342
298
  }
343
299
  }
300
+ return true;
301
+ });
344
302
 
345
- // 合并全局信号与本 eval 的早停信号:任一 abort → 本 attempt 的信号 abort
303
+ // body:preflight 放行之后才跑,只有这一段真正占用全局并发槽位(globalSem)
304
+ const body = Effect.gen(function* () {
305
+ // 合并全局信号与本 eval 的首过即停信号:任一 abort → 本 attempt 的信号 abort。
346
306
  const evalAc = evalAbortControllers.get(a.key);
347
307
  const attemptSignal =
348
308
  evalAc && opts.signal
@@ -361,34 +321,29 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
361
321
  }),
362
322
  ),
363
323
  );
364
- const result = yield* runAttemptEffect(a, opts, sandboxSem, attemptSignal).pipe(
365
- Effect.ensuring(
366
- Effect.sync(() => {
367
- if (a.run.budget !== undefined) {
368
- const s = budgetState(budgetKey);
369
- s.inflight = Math.max(0, s.inflight - 1);
370
- }
371
- }),
372
- ),
373
- );
324
+ const result = yield* runAttemptEffect(a, opts, sandboxSem, attemptSignal);
374
325
  if (a.run.budget !== undefined) {
375
326
  const s = budgetState(budgetKey);
376
327
  if (result.estimatedCostUSD !== undefined) {
377
328
  s.spent += result.estimatedCostUSD;
378
- s.costSamples += 1;
379
329
  } else {
380
330
  s.completedNoCost += 1;
331
+ if (s.spent === 0 && s.completedNoCost >= 3 && !s.unenforceableWarned) {
332
+ // 连续几次完成都拿不到成本:budget 对这个 agent 不可执行,说清楚一次。
333
+ s.unenforceableWarned = true;
334
+ writeStderrLine(t("runner.budgetUnenforceable", { budgetKey }));
335
+ }
381
336
  }
382
337
  }
383
338
 
384
- if (result.outcome === "passed") {
339
+ if (result.verdict === "passed") {
385
340
  passedKeys.add(a.key);
386
341
  evalAc?.abort(); // 让同 key 并发 attempt 尽早退出
387
342
  } else if (a.run.earlyExit && (passedKeys.has(a.key) || erroredKeys.has(a.key))) {
388
343
  // 并发情况:同 key 另一个 attempt 已通过/已 errored 后本 attempt 才完成
389
344
  // (被 abort 后产出 errored),不计入结果。
390
345
  return;
391
- } else if (result.outcome === "errored") {
346
+ } else if (result.verdict === "errored") {
392
347
  erroredKeys.add(a.key);
393
348
  evalAc?.abort(); // 框架层面的错误会确定性重复,让同 key 剩余 attempt 尽早退出
394
349
  }
@@ -413,7 +368,11 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
413
368
  Effect.promise(() => emitReporterEvent(reporters, { type: "eval:complete", result })),
414
369
  );
415
370
  });
416
- const gated = globalSem.withPermits(1)(body);
371
+ const gated = Effect.gen(function* () {
372
+ const proceed = yield* preflight;
373
+ if (!proceed) return;
374
+ yield* globalSem.withPermits(1)(body);
375
+ });
417
376
  const runSem = runSems.get(a.run);
418
377
  return runSem ? runSem.withPermits(1)(gated) : gated;
419
378
  },
@@ -3,10 +3,11 @@
3
3
 
4
4
  import type { Cleanup, LocalizedText, SourceArtifact } from "../shared/types.ts";
5
5
  import type { O11ySummary, StreamEvent, TraceSpan, Usage } from "../o11y/types.ts";
6
- import type { Agent } from "../agents/types.ts";
6
+ import type { Agent, AgentSetupManifest } from "../agents/types.ts";
7
7
  import type { Sandbox, SandboxOption } from "../sandbox/types.ts";
8
- import type { AssertionResult, DiffData, JudgeConfig, ResultOutcome } from "../scoring/types.ts";
8
+ import type { AssertionResult, DiffData, JudgeConfig, Verdict } from "../scoring/types.ts";
9
9
  import type { TestContext } from "../context/types.ts";
10
+ import type { CapturedEvalSource } from "./eval-source.ts";
10
11
 
11
12
  // ───────────────────────── 结果 / 报告 ─────────────────────────
12
13
 
@@ -27,11 +28,19 @@ export interface EvalResult {
27
28
  experiment?: ExperimentRunInfo;
28
29
  agent: string;
29
30
  model?: string;
30
- outcome: ResultOutcome;
31
+ verdict: Verdict;
31
32
  fingerprint?: string;
32
33
  attempt: number;
33
34
  /** 本 attempt 开始的墙钟时刻(ISO);view 按 eval 粒度展示「何时跑的」。 */
34
35
  startedAt?: string;
36
+ /**
37
+ * 不透明的 Attempt 定位符(`@` 前缀短确定性编码,见 `src/results/locator.ts` 的 AttemptLocator),
38
+ * 由 {experimentId, 快照 startedAt, evalId, attempt} 身份元组派生。非携带条目由 writer 在落盘时
39
+ * 算出;携带条目(`--resume` 合入)原样复制上一轮的值,从不重算——原快照的 startedAt 已经不在
40
+ * 当前快照里,重算会算出不同的字符串,详见 writer.ts 对携带分支的说明。省略时读取面按当前
41
+ * 已知身份兜底算出(第三方 harness 未实现 locator 时的降级,不保证跨 --resume 稳定)。
42
+ */
43
+ locator?: string;
35
44
  durationMs: number;
36
45
  assertions: AssertionResult[];
37
46
  usage?: Usage;
@@ -44,33 +53,29 @@ export interface EvalResult {
44
53
  o11y?: O11ySummary;
45
54
  /** agent 经 OpenTelemetry 导出的运行追踪(有 tracing 能力且收到 span 时)。 */
46
55
  trace?: TraceSpan[];
56
+ /** 本 attempt 的 agent setup 实际装了什么(Skill / native plugin / MCP / Python plugin);不参与评分。 */
57
+ agentSetup?: AgentSetupManifest;
47
58
  diff?: DiffData;
48
59
  rawTranscript?: string;
49
- // ── 拆分工件的引用(Artifacts 报告器写 summary.json 时填;view 按需懒加载)──
50
- /** 本 attempt 工件目录(相对 run 根),下有 events/trace/o11y/diff.json。 */
51
- artifactsDir?: string;
52
- /** 工件目录(相对结果根,供前端 fetch 与懒加载回退);view 的 loader 注入,--resume 携带条目落盘时保留。 */
60
+ /** 携带条目(--resume 合入)专用:artifact 目录(相对结果根目录),指向原快照里的落盘。 */
53
61
  artifactBase?: string;
54
- /** 工件目录的绝对路径;历史字段,当前读取面不再注入。 */
55
- artifactAbsBase?: string;
56
62
  hasTrace?: boolean;
57
63
  hasEvents?: boolean;
58
64
  hasSources?: boolean;
59
65
  }
60
66
 
61
- /** `summary.json` 的格式标记;把 niceeval 报告和其它工具的同名文件区分开。 */
67
+ /** `snapshot.json` 的格式标记;把 niceeval 报告和其它工具的同名文件区分开。 */
62
68
  export const RESULTS_FORMAT = "niceeval.results";
63
- /** 结果格式版本,只在破坏兼容读取时递增;读取器只认相同版本,缺失按 1。见 docs/results-format.md。 */
64
- export const RESULTS_SCHEMA_VERSION = 3;
69
+ /**
70
+ * 结果格式版本,只在破坏兼容读取时递增;读取器只认相同版本。见 docs/feature/results/architecture.md。
71
+ * `5`(见 memory 的 attempt-locator-and-source-dedup 条目)= result.json 新增 `locator` 字段;
72
+ * `sources.json` 从逐 attempt 内联全量内容改为「attempt 级引用 + 快照级 `sources/<sha256>.json`
73
+ * 去重仓库」,`AttemptHandle.sources()` 的公开返回形状不变(仍是 `SourceArtifact[] | null`)。
74
+ */
75
+ export const RESULTS_SCHEMA_VERSION = 5;
65
76
 
77
+ /** 一次运行的纯运行时内存聚合(reporter 契约用);落盘格式契约在 niceeval/results 的 SnapshotMeta / AttemptRecord,见 docs/feature/results/architecture.md。 */
66
78
  export interface RunSummary {
67
- /** 恒为 "niceeval.results";和 schemaVersion、producer 一起构成持久化契约,永不移动或改名。 */
68
- format?: typeof RESULTS_FORMAT;
69
- /** 结果格式版本;与读取器不同即视为不兼容,提示用 producer.version 对应的 niceeval 查看。 */
70
- schemaVersion?: number;
71
- /** 写这份报告的工具:niceeval 自己,或经 niceeval/results 写入面转换的第三方 harness。
72
- * name === "niceeval" 时 version 用于拼 `npx niceeval@<version> view` 提示;其它 name 如实报出。 */
73
- producer?: { name: string; version?: string; commit?: string };
74
79
  /** 项目名(来自 config.name),透传给 `niceeval view` 顶部 hero 显示。 */
75
80
  name?: LocalizedText;
76
81
  agent: string;
@@ -87,12 +92,6 @@ export interface RunSummary {
87
92
  usage?: Usage;
88
93
  estimatedCostUSD?: number;
89
94
  results: EvalResult[];
90
- outputDir?: string;
91
- /** 快照级元数据,按 experiment 键(缺 experimentId 时为 "<agent>/<model>" 合成键)存放:
92
- * startedAt 只在与顶层 startedAt 不同(一个 run 装多份不同时刻的快照)时需要;
93
- * knownEvalIds 是写入时刻该实验已知的 eval 并集(copySnapshots 自动补记,writer.snapshot 可声明)。
94
- * 可选新增字段,不递增 schemaVersion(docs/results-format.md 版本规则)。 */
95
- snapshots?: Record<string, { startedAt?: string; knownEvalIds?: string[] }>;
96
95
  }
97
96
 
98
97
  /** onRunStart 的运行规模:去重后 eval 数 × 配置(agent×model×flags)数 → 总运行(attempt)数。 */
@@ -144,7 +143,7 @@ export interface EvalDef {
144
143
  /**
145
144
  * eval 级预置:拿到沙箱(已上传 workspace + git 基线 + 装好依赖前)。
146
145
  * 默认命令以非 root 跑(agent 的自然环境);装系统依赖时给 `runCommand` 传 `{ root: true }`
147
- * (如 `runCommand("apt-get", ["install", …], { root: true })`),跨后端语义一致。
146
+ * (如 `runCommand("apt-get", ["install", …], { root: true })`),跨 provider 语义一致。
148
147
  */
149
148
  setup?: (sandbox: Sandbox) => Promise<void | Cleanup> | void | Cleanup;
150
149
  /** eval 主体:拿到 TestContext,驱动对话 / 沙箱操作并就地断言。 */
@@ -158,6 +157,11 @@ export interface DiscoveredEval extends EvalDef {
158
157
  baseDir: string;
159
158
  /** 定义文件绝对路径,用于内容指纹缓存。 */
160
159
  sourcePath: string;
160
+ /**
161
+ * discovery 时捕获的规范化源码(归一化文本 + 项目相对路径 + SHA-256),见 `eval-source.ts`。
162
+ * 同一文件里多个 eval(数组默认导出)共享同一份引用——哈希与内容天然相同,不重复读盘。
163
+ */
164
+ source: CapturedEvalSource;
161
165
  }
162
166
 
163
167
  export interface ExperimentDef {
@@ -196,7 +200,7 @@ export interface ExperimentDef {
196
200
  * 本实验自己的并发上限:调度器只对这个实验的 attempt 限流,同批其它实验不受影响,
197
201
  * 仍按全局并发(CLI / env / config / 沙箱默认)跑。用于串行化有共享状态的实验
198
202
  * (如跨 eval 累积记忆:`maxConcurrency: 1` 保证 attempt 按 eval 顺序一个个跑),
199
- * 或给撞后端限额的实验单独降速。
203
+ * 或给撞 provider 限额的实验单独降速。
200
204
  */
201
205
  maxConcurrency?: number;
202
206
  }
@@ -212,7 +216,7 @@ export interface Config {
212
216
  * 可传字符串,或按 locale 提供多语言(如 `{ en: "...", "zh-CN": "..." }`),随 view 语言切换。
213
217
  */
214
218
  name?: LocalizedText;
215
- /** 项目级默认沙箱后端(docker / vercel / e2b / custom);experiment / CLI flag 可覆盖。 */
219
+ /** 项目级默认沙箱 provider(docker / vercel / e2b / custom);experiment 可覆盖。 */
216
220
  sandbox?: SandboxOption;
217
221
  /** 上传进沙箱的工作区根目录,省略则用项目根;eval 的 sandbox 视图从这里起步。 */
218
222
  workspace?: string;
@@ -262,9 +266,9 @@ export interface PriceOverride {
262
266
  * 能区分同 agent 同 model 的实验变体,如 xxx 与 xxx--agents-md;与汇总表口径一致);
263
267
  * 无 experiment 时退回 agent/model。live display 以它作行聚合 key,两处必须同源。
264
268
  */
265
- export function runWho(run: Pick<AgentRun, "agent" | "model" | "experimentId">): string {
269
+ export function runWho(run: { agentName: string; model?: string; experimentId?: string }): string {
266
270
  if (run.experimentId) return run.experimentId.split("/").pop()!;
267
- return run.model ? `${run.agent.name}/${run.model}` : run.agent.name;
271
+ return run.model ? `${run.agentName}/${run.model}` : run.agentName;
268
272
  }
269
273
 
270
274
  /** 一个 (agent, model, flags) 的运行配置 —— 由 CLI / 实验展开。 */
@@ -295,8 +299,15 @@ export interface RunOptions {
295
299
  signal?: AbortSignal;
296
300
  /** TTY live display 的进度回调;设置后 attempt 的 log 消息路由到它而不是 stderr。 */
297
301
  onProgress?: (evalId: string, who: string, msg: string) => void;
298
- /** 上次运行的结果。outcome === "passed" 的 (experimentId, evalId) 组合跳过重跑,结果直接合入本次汇总。 */
302
+ /** 上次运行的结果。verdict passed/failed 的 (experimentId, evalId) 组合跳过重跑,结果直接合入本次汇总。 */
299
303
  priorResults?: EvalResult[];
304
+ /**
305
+ * 预算好的携入计划(见 fingerprint.ts 的 planCarry)。cli.ts 为了让 live 表格提前知道
306
+ * 哪些行会被携入,必须在构建 liveRows 之前算一次;传进来后 runEvals 直接复用,不重算
307
+ * 一遍(否则两处各自算一次,不仅重复 I/O,还留下"两边判断可能不一致"的隐患)。
308
+ * 省略时 runEvals 自己算(测试直调等场景)。
309
+ */
310
+ carryPlan?: import("./fingerprint.ts").CarryPlan;
300
311
  /**
301
312
  * 非沙箱 tracing agent 的 run 级共享 OTLP 接收池(runEvals 创建并回收;
302
313
  * 每个 agent 一个 receiver,attempt 之间共享 —— 被测应用是长驻进程,端点不能随 attempt 换)。
@@ -309,7 +320,7 @@ export interface Attempt {
309
320
  evalDef: DiscoveredEval;
310
321
  run: AgentRun;
311
322
  attempt: number;
312
- /** agent+model+evalId,用于早停。 */
323
+ /** agent+model+evalId,用于首过即停。 */
313
324
  key: string;
314
325
  fingerprint: string;
315
326
  }
@@ -8,7 +8,7 @@
8
8
  // restore: uploadFile → /tmp/__fe_rs_<uuid>.tar.gz → tar xzf -C /
9
9
  //
10
10
  // tar / binary file I/O 在所有 Linux sandbox(Docker、Vercel、e2b、Modal…)里都支持,
11
- // 这段代码对任意 backend 的 Sandbox 实现无改动即可使用。
11
+ // 这段代码对任意 provider 的 Sandbox 实现无改动即可使用。
12
12
 
13
13
  import { randomUUID } from "node:crypto";
14
14
  import type { Sandbox } from "../types.ts";
@@ -1,4 +1,4 @@
1
- // Docker 沙箱后端:用 dockerode 把容器当隔离工作区跑 eval。
1
+ // Docker 沙箱 provider:用 dockerode 把容器当隔离工作区跑 eval。
2
2
  // 改编自 agent-eval 的 docker-sandbox.ts,签名对齐 ../types.ts 的 Sandbox 契约
3
3
  //(runShell/runCommand 的 opts 一律是选项对象,不再用位置参数)。
4
4
 
@@ -24,6 +24,17 @@ import { buildFindScript, shellQuote } from "./shell.ts";
24
24
  import { createExecDemuxer, extractFileFromTar, packFilesToTar, readableToBuffer } from "./docker-stream.ts";
25
25
  import { resolveSandboxPath } from "./paths.ts";
26
26
  import { t } from "../i18n/index.ts";
27
+ import { beforeExternalTerminalWrite } from "../tty-line.ts";
28
+ import type { SandboxProvisionErrorKind } from "./errors.ts";
29
+
30
+ /**
31
+ * dockerode 对镜像拉取限流没有专门的错误类型;Docker Hub 429 体现在错误 message 里
32
+ * (如 "toomanyrequests: You have reached your pull rate limit")。
33
+ */
34
+ export function classifyProvisionError(e: unknown): SandboxProvisionErrorKind {
35
+ const msg = e instanceof Error ? e.message : String(e);
36
+ return /toomanyrequests|rate limit exceeded|429/i.test(msg) ? "rate_limit" : "unknown";
37
+ }
27
38
 
28
39
  // 行首哨兵:源码文件几乎不可能出现这一串,用来切分单次 shell 输出里的多份文件。
29
40
  const SOURCE_FILE_MARKER = "::FE-SRC-7b3f9c::";
@@ -167,8 +178,10 @@ export class DockerSandbox implements Sandbox {
167
178
  await image.inspect();
168
179
  } catch {
169
180
  // 镜像不存在,拉取。
181
+ beforeExternalTerminalWrite();
170
182
  console.log(t("docker.imagePullStart", { image: imageName }));
171
183
  await this.pullImage(imageName);
184
+ beforeExternalTerminalWrite();
172
185
  console.log(t("docker.imagePullDone", { image: imageName }));
173
186
  }
174
187
  }
@@ -240,7 +253,7 @@ export class DockerSandbox implements Sandbox {
240
253
  });
241
254
  }
242
255
 
243
- /** 以 root 跑命令(后端内部用:容器初始化、属主收敛)。 */
256
+ /** 以 root 跑命令(provider 内部用:容器初始化、属主收敛)。 */
244
257
  private async runCommandAsRoot(
245
258
  cmd: string,
246
259
  args: string[] = [],