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,66 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { quietLine } from "./quiet.ts";
3
+ import type { EvalResult } from "../../types.ts";
4
+
5
+ function baseResult(overrides: Partial<EvalResult> = {}): EvalResult {
6
+ return {
7
+ id: "algebra/quadratic",
8
+ agent: "codex",
9
+ verdict: "passed",
10
+ attempt: 0,
11
+ durationMs: 42_000,
12
+ assertions: [],
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ describe("quietLine", () => {
18
+ it("passed / skipped 静默(返回 undefined)", () => {
19
+ expect(quietLine(baseResult())).toBeUndefined();
20
+ expect(quietLine(baseResult({ verdict: "skipped", skipReason: "no api key" }))).toBeUndefined();
21
+ });
22
+
23
+ it("errored 带 eval id、[who] 与截断后的 error", () => {
24
+ const line = quietLine(
25
+ baseResult({
26
+ verdict: "errored",
27
+ model: "gpt-5",
28
+ error: "sandbox create failed: e2b timeout after 3.9s",
29
+ }),
30
+ );
31
+ expect(line).toContain("algebra/quadratic");
32
+ expect(line).toContain("[codex/gpt-5]");
33
+ expect(line).toContain("errored");
34
+ expect(line).toContain("e2b timeout after 3.9s");
35
+ });
36
+
37
+ it("[who] 与进度行同源:有 experimentId 时用其 basename", () => {
38
+ const line = quietLine(
39
+ baseResult({ verdict: "errored", experimentId: "compare/xxx--agents-md", error: "boom" }),
40
+ );
41
+ expect(line).toContain("[xxx--agents-md]");
42
+ });
43
+
44
+ it("failed 无 error 时取首个失败断言(severity、阈值、detail)", () => {
45
+ const line = quietLine(
46
+ baseResult({
47
+ verdict: "failed",
48
+ assertions: [
49
+ { name: "compiles", severity: "gate", score: 1, passed: true },
50
+ { name: "closedQA", severity: "gate", score: 0.2, passed: false, threshold: 0.7, detail: "wrong city" },
51
+ ],
52
+ }),
53
+ );
54
+ expect(line).toContain("failed");
55
+ expect(line).toContain("closedQA");
56
+ expect(line).toContain("0.20");
57
+ expect(line).toContain("wrong city");
58
+ expect(line).not.toContain("compiles");
59
+ });
60
+
61
+ it("超长 error 截断到 200 字符并加省略号", () => {
62
+ const line = quietLine(baseResult({ verdict: "errored", error: "x".repeat(500) }))!;
63
+ expect(line).toContain("…");
64
+ expect(line.length).toBeLessThan(300);
65
+ });
66
+ });
@@ -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
  },