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
@@ -1,11 +1,11 @@
1
- // E2B 沙箱后端:用 e2b SDK 把 E2B microVM 当隔离工作区跑 eval。
1
+ // E2B 沙箱 provider:用 e2b SDK 把 E2B microVM 当隔离工作区跑 eval。
2
2
  // 契约对齐 ../types.ts 的 Sandbox 接口,与 DockerSandbox / VercelSandbox 可互换。
3
3
  //
4
4
  // 鉴权:E2B_API_KEY(team 级,e2b CLI `e2b auth login` 后也写在 ~/.e2b)。
5
5
  // 模板:opts.template 选 e2b 模板名/ID;省略用 e2b 默认 "base"。预制模板(烘焙好
6
6
  // codex/claude-code/bub 的 "niceeval-agents")见 sandbox/e2b/。
7
7
 
8
- import { Sandbox as E2BSdkSandbox, CommandExitError } from "e2b";
8
+ import { Sandbox as E2BSdkSandbox, CommandExitError, RateLimitError } from "e2b";
9
9
  import type {
10
10
  Sandbox,
11
11
  CommandResult,
@@ -14,6 +14,7 @@ import type {
14
14
  SourceFiles,
15
15
  ReadSourceFilesOptions,
16
16
  } from "../types.ts";
17
+ import type { SandboxProvisionErrorKind } from "./errors.ts";
17
18
  import { readSourceFilesByList } from "./source-files.ts";
18
19
  import { collectLocalFiles } from "./local-files.ts";
19
20
  import { shellQuote } from "./shell.ts";
@@ -27,6 +28,11 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 600_000;
27
28
  // 沙箱存活上限(到点 e2b 自动回收)。给足空间跑完 setup + agent + 测试脚本。
28
29
  const SESSION_TIMEOUT_MS = 1_800_000;
29
30
 
31
+ /** e2b 的限流错误是 SDK 原生的 RateLimitError(HTTP 429 映射而来);见 resolve.ts 的 withProvisionRetry。 */
32
+ export function classifyProvisionError(e: unknown): SandboxProvisionErrorKind {
33
+ return e instanceof RateLimitError ? "rate_limit" : "unknown";
34
+ }
35
+
30
36
  export class E2BSandbox implements Sandbox {
31
37
  readonly workdir = E2B_WORKDIR;
32
38
  readonly otlpHost = null;
@@ -63,7 +69,7 @@ export class E2BSandbox implements Sandbox {
63
69
 
64
70
  async runShell(script: string, opts: CommandOptions = {}): Promise<CommandResult> {
65
71
  // e2b commands.run 经 bash 执行 → 支持 && / 管道 / $()。root 用户映射到 { user: "root" },
66
- // 否则用模板默认(非 root)用户 —— 跨后端语义一致(见 types.ts 的 CommandOptions.root)。
72
+ // 否则用模板默认(非 root)用户 —— 跨 provider 语义一致(见 types.ts 的 CommandOptions.root)。
67
73
  try {
68
74
  const res = await this.sbx.commands.run(script, {
69
75
  cwd: resolveSandboxPath(this.workdir, opts.cwd),
@@ -100,7 +106,7 @@ export class E2BSandbox implements Sandbox {
100
106
  }
101
107
 
102
108
  async readSourceFiles(opts: ReadSourceFilesOptions = {}): Promise<SourceFiles> {
103
- // find 列路径 + 逐文件 files.read —— 与 vercel 后端共用同一两阶段模板。
109
+ // find 列路径 + 逐文件 files.read —— 与 vercel provider 共用同一两阶段模板。
104
110
  return readSourceFilesByList({
105
111
  options: opts,
106
112
  runShell: (script) => this.runShell(script),
@@ -109,7 +115,7 @@ export class E2BSandbox implements Sandbox {
109
115
  }
110
116
 
111
117
  // targetDir 已由 paths.ts 的 normalizeSandboxPaths 解析成绝对路径;这里再解析一次
112
- // 只是对直接使用后端实例(未包 normalize)的幂等防御,提到 map 外只算一次。
118
+ // 只是对直接使用 provider 实例(未包 normalize)的幂等防御,提到 map 外只算一次。
113
119
  async writeFiles(files: Record<string, string>, targetDir?: string): Promise<void> {
114
120
  const base = resolveSandboxPath(this.workdir, targetDir);
115
121
  const entries = Object.entries(files).map(([p, data]) => ({ path: resolveSandboxPath(base, p), data }));
@@ -0,0 +1,13 @@
1
+ // 沙箱 provisioning 错误的中性分类:各 provider SDK 的限流错误形状互不相同(e2b 抛
2
+ // RateLimitError,vercel 抛 APIError{ response.status: 429 },docker 是 dockerode 的
3
+ // 普通 Error,message 里带 "toomanyrequests")。resolve.ts 的 createProvider() 据此统一
4
+ // 做退避重试,不需要认识任何 provider 专属的错误类型——分类逻辑留在各 provider 自己的
5
+ // 文件里(见 e2b.ts / vercel.ts / docker.ts 的 classifyProvisionError)。
6
+
7
+ /** 目前只区分"限流,值得退避重试"和"其它,原样抛出"。 */
8
+ export type SandboxProvisionErrorKind = "rate_limit" | "unknown";
9
+
10
+ /** 按 kind 判断是否该重试;模板不存在、凭据缺失等归入 unknown,第一次就抛,重试没有意义。 */
11
+ export function isRetryableProvisionError(kind: SandboxProvisionErrorKind): boolean {
12
+ return kind === "rate_limit";
13
+ }
@@ -1,6 +1,6 @@
1
1
  // niceeval/sandbox 公开导出:「在哪里跑」相关的类型 + 工厂 + 扩展点。
2
- // 具体后端实现类(DockerSandbox / VercelSandbox / E2BSandbox)是内部实现细节,不在此导出——
3
- // 需要自定义后端时用 defineSandbox(),不需要绕开 resolve.ts 直接 new 内置类。
2
+ // 具体 provider 实现类(DockerSandbox / VercelSandbox / E2BSandbox)是内部实现细节,不在此导出——
3
+ // 需要自定义 provider 时用 defineSandbox(),不需要绕开 resolve.ts 直接 new 内置类。
4
4
 
5
5
  export { dockerSandbox, vercelSandbox, e2bSandbox, defineSandbox } from "../define.ts";
6
6
  export { createCheckpoint, restoreCheckpoint } from "./checkpoint.ts";
@@ -12,7 +12,7 @@ export type {
12
12
  SourceFile,
13
13
  SourceFiles,
14
14
  ReadSourceFilesOptions,
15
- SandboxBackend,
15
+ SandboxProvider,
16
16
  SandboxOption,
17
17
  SandboxSpec,
18
18
  SandboxRuntime,
@@ -1,5 +1,5 @@
1
1
  // 本地目录文件采集(uploadDirectory 用)与 readSourceFiles 的默认过滤常量,
2
- // docker / vercel / e2b 三后端共用一份。
2
+ // docker / vercel / e2b 三个 provider 共用一份。
3
3
 
4
4
  import { readdir, readFile, stat } from "node:fs/promises";
5
5
  import { join, relative, sep } from "node:path";
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // 中断(Ctrl+C)时,正常路径靠 Effect 的 Scope finalizer 跑 sb.stop() 停容器。但 finalizer
4
4
  // 这条路不是 100% 可靠:vsb.stop() 这类远端调用可能慢/挂,用户等不及再按一次 Ctrl+C,进程
5
- // 就被硬退了 —— 沙箱成了孤儿(只能等后端 session/TTL 过期)。
5
+ // 就被硬退了 —— 沙箱成了孤儿(只能等 provider session/TTL 过期)。
6
6
  //
7
7
  // 这里维护一份独立于 Effect 的登记表,让 cli 在「二次中断 / graceful 清理超时 / 正常返回后」
8
8
  // 都能直接、带超时地强停所有还活着的沙箱。stop 不再静默吞异常(原 `.catch(() => {})`),失败打到
@@ -10,11 +10,12 @@
10
10
 
11
11
  import type { Sandbox } from "../types.ts";
12
12
  import { t } from "../i18n/index.ts";
13
+ import { writeStderrLine } from "../tty-line.ts";
13
14
 
14
15
  const live = new Set<Sandbox>();
15
16
 
16
17
  // 单个 stop 的默认超时:vsb.stop() 偶发慢/挂,清理不能无限等。到点就放弃、记一笔,
17
- // 让流程继续走到退出 —— 没停掉的沙箱靠后端 session/TTL 兜底过期。
18
+ // 让流程继续走到退出 —— 没停掉的沙箱靠 provider session/TTL 兜底过期。
18
19
  const DEFAULT_STOP_TIMEOUT_MS = 8_000;
19
20
 
20
21
  export function registerSandbox(sb: Sandbox): void {
@@ -26,7 +27,7 @@ export function liveSandboxCount(): number {
26
27
  }
27
28
 
28
29
  /**
29
- * 带超时地停单个沙箱。成功 / 失败 / 超时都从登记表移除(失败的靠后端过期兜底),
30
+ * 带超时地停单个沙箱。成功 / 失败 / 超时都从登记表移除(失败的靠 provider 过期兜底),
30
31
  * 异常打到 stderr 不再静默吞。供 Scope finalizer 与兜底强清共用,避免重复实现 stop 语义。
31
32
  */
32
33
  export async function stopSandbox(sb: Sandbox, timeoutMs = DEFAULT_STOP_TIMEOUT_MS): Promise<void> {
@@ -40,7 +41,7 @@ export async function stopSandbox(sb: Sandbox, timeoutMs = DEFAULT_STOP_TIMEOUT_
40
41
  ]);
41
42
  } catch (e) {
42
43
  const msg = e instanceof Error ? e.message : String(e);
43
- process.stderr.write(t("sandbox.stopFailed", { id: sb.sandboxId, message: msg }));
44
+ writeStderrLine(t("sandbox.stopFailed", { id: sb.sandboxId, message: msg }));
44
45
  } finally {
45
46
  if (timer) clearTimeout(timer);
46
47
  live.delete(sb);
@@ -54,7 +55,7 @@ export async function stopSandbox(sb: Sandbox, timeoutMs = DEFAULT_STOP_TIMEOUT_
54
55
  export async function stopAllSandboxes(timeoutMs = DEFAULT_STOP_TIMEOUT_MS): Promise<number> {
55
56
  const all = [...live];
56
57
  if (all.length === 0) return 0;
57
- process.stderr.write(t("sandbox.forceCleanup", { count: all.length }));
58
+ writeStderrLine(t("sandbox.forceCleanup", { count: all.length }));
58
59
  await Promise.allSettled(all.map((sb) => stopSandbox(sb, timeoutMs)));
59
60
  return all.length;
60
61
  }
@@ -1,18 +1,19 @@
1
- // 沙箱后端解析:把 config.sandbox / experiment.sandbox(工厂函数产出的 spec 数据结构)
2
- // 折叠成一个具体后端 + 参数,并按需创建实例。`sandbox` 没有默认值也没有按名字选的入口——
1
+ // 沙箱 provider 解析:把 config.sandbox / experiment.sandbox(工厂函数产出的 spec 数据结构)
2
+ // 折叠成一个具体 provider + 参数,并按需创建实例。`sandbox` 没有默认值也没有按名字选的入口——
3
3
  // 沙箱型 agent 必须显式给 dockerSandbox() / vercelSandbox() / e2bSandbox() / defineSandbox(),
4
4
  // 省略时 resolveSandbox() 直接抛错,不猜环境、不兜底。
5
- // 后端名的行为分支只允许出现在 sandbox/ 内(见 docs/architecture.md)。
5
+ // provider 名的行为分支只允许出现在 sandbox/ 内(见 docs/architecture.md)。
6
6
 
7
7
  import { Effect } from "effect";
8
8
  import type { CustomSandboxSpec, Sandbox, SandboxOption, SandboxRuntime } from "../types.ts";
9
9
  import { registerSandbox, stopSandbox } from "./registry.ts";
10
10
  import { normalizeSandboxPaths } from "./paths.ts";
11
11
  import { t } from "../i18n/index.ts";
12
+ import { withProvisionRetry, type ProvisionSlot } from "./retry.ts";
12
13
 
13
- /** 归一化后的沙箱描述:确定的后端 + 各后端参数(只有对应后端用得上的会有值)。 */
14
+ /** 归一化后的沙箱描述:确定的 provider + 各 provider 参数(只有对应 provider 用得上的会有值)。 */
14
15
  export interface ResolvedSandbox {
15
- backend: string;
16
+ provider: string;
16
17
  runtime?: SandboxRuntime;
17
18
  /** docker */
18
19
  image?: string;
@@ -20,19 +21,19 @@ export interface ResolvedSandbox {
20
21
  snapshotId?: string;
21
22
  /** e2b */
22
23
  template?: string;
23
- /** 自定义后端(defineSandbox):有它就直接调用,跳过下面的内置 backend switch。 */
24
+ /** 自定义 provider(defineSandbox):有它就直接调用,跳过下面的内置 provider switch。 */
24
25
  create?: CustomSandboxSpec["create"];
25
26
  recommendedConcurrency?: number;
26
27
  }
27
28
 
28
- /** 把 spec 数据结构归一化成 ResolvedSandbox;省略(undefined)直接报错——没有默认后端。 */
29
+ /** 把 spec 数据结构归一化成 ResolvedSandbox;省略(undefined)直接报错——没有默认 provider。 */
29
30
  export function resolveSandbox(opt: SandboxOption | undefined, runtimeDefault?: SandboxRuntime): ResolvedSandbox {
30
31
  if (!opt) throw new Error(t("sandbox.missingSpec"));
31
32
  return { ...opt, runtime: opt.runtime ?? runtimeDefault };
32
33
  }
33
34
 
34
35
  /**
35
- * 各后端的推荐默认并发数。反映的是后端侧约束(daemon 容量、API quota、session 池大小),
36
+ * 各 provider 的推荐默认并发数。反映的是 provider 侧约束(daemon 容量、API quota、session 池大小),
36
37
  * 不是用户侧的 agent API 限速——后者由用户通过 --max-concurrency 或 config.maxConcurrency 设置。
37
38
  * docker:本地 daemon 创建容器有开销,10 是经验上稳健的上限。
38
39
  * e2b:云服务,20 是默认账户并发配额的保守估计。
@@ -41,7 +42,7 @@ export function resolveSandbox(opt: SandboxOption | undefined, runtimeDefault?:
41
42
  export function sandboxRecommendedConcurrency(opt: SandboxOption | undefined): number {
42
43
  if (!opt) return 10;
43
44
  const r = resolveSandbox(opt);
44
- switch (r.backend) {
45
+ switch (r.provider) {
45
46
  case "docker": return 10;
46
47
  case "e2b": return 20;
47
48
  case "vercel": return 1;
@@ -49,28 +50,30 @@ export function sandboxRecommendedConcurrency(opt: SandboxOption | undefined): n
49
50
  }
50
51
  }
51
52
 
52
- /** 报告 / 日志用的简短标签:后端名,带上区分性的参数(镜像 / 快照 / 模板)。 */
53
+ /** 报告 / 日志用的简短标签:provider 名,带上区分性的参数(镜像 / 快照 / 模板)。 */
53
54
  export function sandboxLabel(opt: SandboxOption | undefined): string {
54
55
  const r = resolveSandbox(opt);
55
56
  const detail = r.image ?? r.snapshotId ?? r.template;
56
- return detail ? `${r.backend}:${detail}` : r.backend;
57
+ return detail ? `${r.provider}:${detail}` : r.provider;
57
58
  }
58
59
 
59
60
  /**
60
- * 按解析出的后端 + 参数创建沙箱,并把 stop() 注册为 Scope 回收动作。
61
+ * 按解析出的 provider + 参数创建沙箱,并把 stop() 注册为 Scope 回收动作。
61
62
  * 在 Effect.scoped / Effect.gen 里 yield* 即可;成功/失败/中断都保证 stop。
62
63
  */
63
64
  export function createSandbox(opts: {
64
65
  sandbox?: SandboxOption;
65
66
  timeout?: number;
66
67
  runtime?: SandboxRuntime;
68
+ /** 调用方并发槽位的临时归还/收回,传给 withProvisionRetry 在退避睡眠期间释放(见 retry.ts)。 */
69
+ provisionSlot?: ProvisionSlot;
67
70
  }) {
68
71
  const r = resolveSandbox(opts.sandbox, opts.runtime);
69
72
  return Effect.acquireRelease(
70
73
  Effect.promise<Sandbox>(async () => {
71
74
  // 起好就登记:让 cli 的兜底强清(二次 Ctrl+C / 看门狗超时)能直接停到它,不只靠下面的
72
75
  // release。即便本 fiber 创建后立刻被中断、release 还没来得及跑,登记表也已认得这个沙箱。
73
- const sb = normalizeSandboxPaths(await createBackend(r, opts.timeout));
76
+ const sb = normalizeSandboxPaths(await createProvider(r, opts.timeout, opts.provisionSlot));
74
77
  registerSandbox(sb);
75
78
  return sb;
76
79
  }),
@@ -79,29 +82,41 @@ export function createSandbox(opts: {
79
82
  );
80
83
  }
81
84
 
82
- async function createBackend(r: ResolvedSandbox, timeout?: number): Promise<Sandbox> {
83
- // 自定义后端(defineSandbox):不认后端名,直接调用用户给的 create()。
85
+ async function createProvider(r: ResolvedSandbox, timeout?: number, provisionSlot?: ProvisionSlot): Promise<Sandbox> {
86
+ // 自定义 provider(defineSandbox):不认 provider 名,直接调用用户给的 create()。
84
87
  if (r.create) return r.create({ timeout, runtime: r.runtime });
85
- switch (r.backend) {
88
+ switch (r.provider) {
86
89
  case "docker": {
87
- const { DockerSandbox } = await import("./docker.ts").catch(() => {
90
+ const { DockerSandbox, classifyProvisionError } = await import("./docker.ts").catch(() => {
88
91
  throw new Error(t("sandbox.dependencyMissing.docker"));
89
92
  });
90
- return DockerSandbox.create({ timeout, runtime: r.runtime, image: r.image });
93
+ return withProvisionRetry(
94
+ () => DockerSandbox.create({ timeout, runtime: r.runtime, image: r.image }),
95
+ classifyProvisionError,
96
+ provisionSlot,
97
+ );
91
98
  }
92
99
  case "vercel": {
93
- const { VercelSandbox } = await import("./vercel.ts").catch(() => {
100
+ const { VercelSandbox, classifyProvisionError } = await import("./vercel.ts").catch(() => {
94
101
  throw new Error(t("sandbox.dependencyMissing.vercel"));
95
102
  });
96
- return VercelSandbox.create({ timeout, runtime: r.runtime, snapshotId: r.snapshotId });
103
+ return withProvisionRetry(
104
+ () => VercelSandbox.create({ timeout, runtime: r.runtime, snapshotId: r.snapshotId }),
105
+ classifyProvisionError,
106
+ provisionSlot,
107
+ );
97
108
  }
98
109
  case "e2b": {
99
- const { E2BSandbox } = await import("./e2b.ts").catch(() => {
110
+ const { E2BSandbox, classifyProvisionError } = await import("./e2b.ts").catch(() => {
100
111
  throw new Error(t("sandbox.dependencyMissing.e2b"));
101
112
  });
102
- return E2BSandbox.create({ timeout, runtime: r.runtime, template: r.template });
113
+ return withProvisionRetry(
114
+ () => E2BSandbox.create({ timeout, runtime: r.runtime, template: r.template }),
115
+ classifyProvisionError,
116
+ provisionSlot,
117
+ );
103
118
  }
104
119
  default:
105
- throw new Error(t("sandbox.backendNotImplemented", { backend: r.backend }));
120
+ throw new Error(t("sandbox.providerNotImplemented", { provider: r.provider }));
106
121
  }
107
122
  }
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { withProvisionRetry, type ProvisionSlot } from "./retry.ts";
3
+
4
+ function fakeSlot() {
5
+ const calls: string[] = [];
6
+ const slot: ProvisionSlot = {
7
+ release: async () => {
8
+ calls.push("release");
9
+ },
10
+ reacquire: async () => {
11
+ calls.push("reacquire");
12
+ },
13
+ };
14
+ return { slot, calls };
15
+ }
16
+
17
+ describe("withProvisionRetry", () => {
18
+ it("succeeds without touching the slot when create() succeeds first try", async () => {
19
+ const { slot, calls } = fakeSlot();
20
+ const result = await withProvisionRetry(
21
+ async () => "sandbox",
22
+ () => "unknown",
23
+ slot,
24
+ );
25
+ expect(result).toBe("sandbox");
26
+ expect(calls).toEqual([]);
27
+ });
28
+
29
+ it("throws immediately on a non-retryable error without touching the slot", async () => {
30
+ const { slot, calls } = fakeSlot();
31
+ const err = new Error("bad template");
32
+ await expect(
33
+ withProvisionRetry(
34
+ async () => {
35
+ throw err;
36
+ },
37
+ () => "unknown",
38
+ slot,
39
+ ),
40
+ ).rejects.toBe(err);
41
+ expect(calls).toEqual([]);
42
+ });
43
+
44
+ it("releases the slot before backing off and reacquires it before retrying", async () => {
45
+ vi.useFakeTimers();
46
+ try {
47
+ const { slot, calls } = fakeSlot();
48
+ let attempts = 0;
49
+ const promise = withProvisionRetry(
50
+ async () => {
51
+ attempts += 1;
52
+ if (attempts === 1) throw new Error("rate limited");
53
+ return "sandbox";
54
+ },
55
+ () => "rate_limit",
56
+ slot,
57
+ );
58
+ // 第一次失败后应该先 release,再进入退避睡眠 —— 此时还没到 reacquire。
59
+ await vi.waitFor(() => expect(calls).toEqual(["release"]));
60
+ await vi.runAllTimersAsync();
61
+ await expect(promise).resolves.toBe("sandbox");
62
+ expect(calls).toEqual(["release", "reacquire"]);
63
+ expect(attempts).toBe(2);
64
+ } finally {
65
+ vi.useRealTimers();
66
+ }
67
+ });
68
+ });
@@ -0,0 +1,44 @@
1
+ // provider 无关的 provisioning 退避重试:createProvider()(resolve.ts)对每个内置
2
+ // provider 的 create() 套这一层。只有各 provider 自己的 classifyProvisionError 判为
3
+ // 可重试(目前仅 rate_limit)的错误才会退避重试;其它错误第一次就抛出。
4
+
5
+ import { isRetryableProvisionError, type SandboxProvisionErrorKind } from "./errors.ts";
6
+
7
+ const MAX_ATTEMPTS = 4;
8
+ const BASE_DELAY_MS = 1000;
9
+
10
+ /** 指数退避 + 全抖动(0.5x~1.5x),避免同一批被限流的 create() 同时醒来再次撞限流。 */
11
+ function delayFor(attempt: number): number {
12
+ return BASE_DELAY_MS * 2 ** attempt * (0.5 + Math.random());
13
+ }
14
+
15
+ /**
16
+ * 调用方(resolve.ts)持有的并发槽位的临时归还/收回。不认调用方用的是不是 Effect —— 只要求
17
+ * 两个 async 方法,保持这层 provider 无关。
18
+ */
19
+ export interface ProvisionSlot {
20
+ release(): Promise<void>;
21
+ reacquire(): Promise<void>;
22
+ }
23
+
24
+ export async function withProvisionRetry<T>(
25
+ create: () => Promise<T>,
26
+ classify: (e: unknown) => SandboxProvisionErrorKind,
27
+ slot?: ProvisionSlot,
28
+ ): Promise<T> {
29
+ for (let attempt = 0; ; attempt++) {
30
+ try {
31
+ return await create();
32
+ } catch (e) {
33
+ if (!isRetryableProvisionError(classify(e)) || attempt >= MAX_ATTEMPTS - 1) throw e;
34
+ // 退避期间只是在睡觉,不是在真的创建沙箱:攥着并发槽位陪跑 setTimeout 会让被限流的
35
+ // provider 把整批并发名额拖垮成"看起来卡在个位数并发"——先还名额,睡醒了再排队要回来。
36
+ if (slot) await slot.release();
37
+ try {
38
+ await new Promise((resolve) => setTimeout(resolve, delayFor(attempt)));
39
+ } finally {
40
+ if (slot) await slot.reacquire();
41
+ }
42
+ }
43
+ }
44
+ }
@@ -1,5 +1,5 @@
1
- // sandbox 域类型:Sandbox 接口、后端 spec(可辨识联合)、命令与文件 IO 的形状。
2
- // 「在哪里跑、如何隔离」的全部契约在这里;后端实现见本目录各文件,分发见 resolve.ts。
1
+ // sandbox 域类型:Sandbox 接口、provider spec(可辨识联合)、命令与文件 IO 的形状。
2
+ // 「在哪里跑、如何隔离」的全部契约在这里;provider 实现见本目录各文件,分发见 resolve.ts。
3
3
 
4
4
  import type { AgentSetup, AgentTeardown } from "../agents/types.ts";
5
5
 
@@ -47,8 +47,8 @@ export interface ReadSourceFilesOptions {
47
47
  ignoreFiles?: string[];
48
48
  }
49
49
 
50
- /** 内置后端名;只在 sandbox/resolve.ts 内部做环境探测和 CLI `--sandbox` 解析用,不出现在 `sandbox` 字段的类型里。 */
51
- export type SandboxBackend = "docker" | "vercel" | "e2b";
50
+ /** 内置 provider 名;不出现在 `sandbox` 字段的类型里(spec 用各自的 `provider` 判别字段区分)。 */
51
+ export type SandboxProvider = "docker" | "vercel" | "e2b";
52
52
 
53
53
  /** 镜像/模板里的 Node 运行时版本。 */
54
54
  export type SandboxRuntime = "node20" | "node24";
@@ -92,37 +92,37 @@ export interface SandboxHooks<Self> {
92
92
  }
93
93
 
94
94
  /**
95
- * Sandbox 的「数据结构」定义 —— 与 agent 一样可带参数(见 docs/sandbox.md)。
95
+ * Sandbox 的「数据结构」定义 —— 与 agent 一样可带参数(见 docs/feature/sandbox/library.md)。
96
96
  * 必须用工厂函数构造(`dockerSandbox()` / `vercelSandbox()` / `e2bSandbox()` / `defineSandbox()`),
97
97
  * 放进 config / experiment 的 `sandbox` 字段 —— 字段类型只接受这个数据结构,不接受裸字符串。
98
- * 各后端的参数互不相同 —— 这是个按 `backend` 区分的可辨识联合(discriminated union)。
98
+ * provider 的参数互不相同 —— 这是个按 `provider` 区分的可辨识联合(discriminated union)。
99
99
  * 四个变体都带 {@link SandboxHooks} 的 `.setup()` / `.teardown()` 链式方法。
100
100
  */
101
101
  export interface DockerSandboxSpec extends SandboxHooks<DockerSandboxSpec> {
102
- readonly backend: "docker";
102
+ readonly provider: "docker";
103
103
  /** 覆盖默认镜像;默认按 runtime 选 `node:*-slim`。预制模板:传烘焙好 agent CLI 的镜像名。 */
104
104
  readonly image?: string;
105
105
  readonly runtime?: SandboxRuntime;
106
106
  }
107
107
  export interface VercelSandboxSpec extends SandboxHooks<VercelSandboxSpec> {
108
- readonly backend: "vercel";
108
+ readonly provider: "vercel";
109
109
  /** 从已有快照起 microVM。预制模板:烘焙好 agent CLI 的 snapshotId。 */
110
110
  readonly snapshotId?: string;
111
111
  readonly runtime?: SandboxRuntime;
112
112
  }
113
113
  export interface E2BSandboxSpec extends SandboxHooks<E2BSandboxSpec> {
114
- readonly backend: "e2b";
114
+ readonly provider: "e2b";
115
115
  /** e2b 模板名/ID。预制模板:烘焙好 agent CLI 的模板(如 `"niceeval-agents"`)。省略用 e2b 默认 `"base"`。 */
116
116
  readonly template?: string;
117
117
  /** 仅作记录;e2b 的 node 版本由模板决定,不在创建时选。 */
118
118
  readonly runtime?: SandboxRuntime;
119
119
  }
120
120
  /**
121
- * 用户自定义后端:`create` 直接产出一个 `Sandbox` 实例,不经 resolve.ts 的内置 backend switch。
122
- * 用 `defineSandbox()` 构造(见 src/define.ts)。`backend` 只用于展示 / 日志,不参与分发。
121
+ * 用户自定义 provider:`create` 直接产出一个 `Sandbox` 实例,不经 resolve.ts 的内置 provider switch。
122
+ * 用 `defineSandbox()` 构造(见 src/define.ts)。`provider` 只用于展示 / 日志,不参与分发。
123
123
  */
124
124
  export interface CustomSandboxSpec extends SandboxHooks<CustomSandboxSpec> {
125
- readonly backend: string;
125
+ readonly provider: string;
126
126
  readonly runtime?: SandboxRuntime;
127
127
  readonly recommendedConcurrency?: number;
128
128
  readonly create: (opts: { timeout?: number; runtime?: SandboxRuntime }) => Promise<Sandbox>;
@@ -134,25 +134,25 @@ export type SandboxSpec = DockerSandboxSpec | VercelSandboxSpec | E2BSandboxSpec
134
134
  export type SandboxOption = SandboxSpec;
135
135
 
136
136
  export interface CommandOptions {
137
- /** 追加/覆盖本命令的环境变量(与沙箱默认环境叠加,不清空默认值;各后端会保留自己固定的 `PATH` 等变量,不保证能被这里覆盖)。 */
137
+ /** 追加/覆盖本命令的环境变量(与沙箱默认环境叠加,不清空默认值;各 provider 会保留自己固定的 `PATH` 等变量,不保证能被这里覆盖)。 */
138
138
  env?: Record<string, string>;
139
139
  /** 本命令的工作目录;省略时落到 `Sandbox.workdir`。相对路径按 workdir 解析,绝对路径原样使用。 */
140
140
  cwd?: string;
141
141
  /**
142
142
  * 把本命令的输出也送进沙箱的「原生日志流」(于是 `docker logs` / Docker UI 的 Logs
143
143
  * 标签页能实时看到它)。给 agent 命令(codex exec / bub run / claude)开它,就能在容器
144
- * 日志里看到 agent 的【原始输出】。后端各自实现(docker:tee 到 PID1 tail 的文件;
145
- * 不支持的后端忽略)—— 日志怎么浮现是 backend 的事,adapter 只声明意图。
144
+ * 日志里看到 agent 的【原始输出】。provider 各自实现(docker:tee 到 PID1 tail 的文件;
145
+ * 不支持的 provider 忽略)—— 日志怎么浮现是 provider 的事,adapter 只声明意图。
146
146
  */
147
147
  stream?: boolean;
148
148
  /**
149
149
  * 以 root 跑本命令。默认 `false` —— 命令以沙箱的标准**非 root** 用户跑(agent 的自然环境)。
150
150
  * 给 setup 阶段装系统依赖用(`apt-get install …`、`pip install --break-system-packages …`)。
151
151
  *
152
- * 语义跨后端一致:"本命令以 root 跑,否则以标准非 root 用户跑"。各后端映射到自己的原生机制
152
+ * 语义跨 provider 一致:"本命令以 root 跑,否则以标准非 root 用户跑"。各 provider 映射到自己的原生机制
153
153
  * (docker:`exec --user root`;E2B:`{ user: "root" }`;Vercel:`{ sudo: true }`;Daytona:`{ user }`)。
154
- * 本就全程 root 的后端(如 Modal)视作 no-op;完全无法提权的后端可不支持(抛错)—— 但**默认值与
155
- * 语义保持一致**,不因后端而变。
154
+ * 本就全程 root 的 provider(如 Modal)视作 no-op;完全无法提权的 provider 可不支持(抛错)—— 但**默认值与
155
+ * 语义保持一致**,不因 provider 而变。
156
156
  */
157
157
  root?: boolean;
158
158
  }
@@ -172,7 +172,7 @@ export interface Sandbox {
172
172
  runShell(script: string, opts?: CommandOptions): Promise<CommandResult>;
173
173
  /** 读取沙箱内文件的文本内容(UTF-8)。文件不存在时抛错,不返回空字符串——需要容错请自行 `.catch()`。 */
174
174
  readFile(path: string): Promise<string>;
175
- /** 检查沙箱内路径是否存在。跨后端语义不完全一致:仅保证对普通文件可靠,对目录路径的行为不同后端不保证一致。 */
175
+ /** 检查沙箱内路径是否存在。跨 provider 语义不完全一致:仅保证对普通文件可靠,对目录路径的行为不同 provider 不保证一致。 */
176
176
  fileExists(path: string): Promise<boolean>;
177
177
  /**
178
178
  * 一次 shell 往返读全部源码文件(按扩展名收、按目录/文件名忽略)。
@@ -185,9 +185,9 @@ export interface Sandbox {
185
185
  uploadFiles(files: SandboxFile[], targetDir?: string): Promise<void>;
186
186
  /** 把本地磁盘上的一个目录整体上传进沙箱(递归读取本地文件后按 `uploadFiles` 写入);`opts.ignore` 是排除规则,省略 `targetDir` 落到 workdir。 */
187
187
  uploadDirectory(localDir: string, targetDir?: string, opts?: { ignore?: string[] }): Promise<void>;
188
- /** 销毁沙箱占用的计算资源(容器/microVM)。调用后沙箱不可再用;是否可安全重复调用因后端而异,不要依赖这一点。 */
188
+ /** 销毁沙箱占用的计算资源(容器/microVM)。调用后沙箱不可再用;是否可安全重复调用因 provider 而异,不要依赖这一点。 */
189
189
  stop(): Promise<void>;
190
- /** 本沙箱的稳定标识(各后端原生 ID,如 Docker 容器 ID 前缀);用于跨调用关联同一沙箱的会话状态,也用于日志展示。 */
190
+ /** 本沙箱的稳定标识( provider 原生 ID,如 Docker 容器 ID 前缀);用于跨调用关联同一沙箱的会话状态,也用于日志展示。 */
191
191
  readonly sandboxId: string;
192
192
  /**
193
193
  * 本地 OTLP 接收器的目标 host。
@@ -199,19 +199,19 @@ export interface Sandbox {
199
199
 
200
200
  /**
201
201
  * 可选:把一行写进容器的「主日志」(PID1 在 tail 它)——于是 `docker logs` /
202
- * Docker UI 的 Logs 标签页能实时看到 agent 逐轮活动。docker 后端实现,其它可省略。
202
+ * Docker UI 的 Logs 标签页能实时看到 agent 逐轮活动。docker provider 实现,其它可省略。
203
203
  */
204
204
  appendLog?(line: string): Promise<void>;
205
205
 
206
206
  /**
207
207
  * 从沙箱内任意路径读取文件,返回二进制 Buffer。
208
- * 对应各 backend:Docker getArchive / Vercel readFileToBuffer / e2b files.read(bytes) / …
208
+ * 对应各 provider:Docker getArchive / Vercel readFileToBuffer / e2b files.read(bytes) / …
209
209
  */
210
210
  downloadFile(path: string): Promise<Buffer>;
211
211
 
212
212
  /**
213
213
  * 向沙箱内任意路径写入文件(二进制)。
214
- * 对应各 backend:Docker putArchive / Vercel fs.writeFile(Buffer) / e2b files.write / …
214
+ * 对应各 provider:Docker putArchive / Vercel fs.writeFile(Buffer) / e2b files.write / …
215
215
  */
216
216
  uploadFile(path: string, content: Buffer): Promise<void>;
217
217
  }
@@ -1,7 +1,7 @@
1
- // Vercel Sandbox 后端:用 @vercel/sandbox SDK 把 Vercel microVM 当隔离工作区跑 eval。
1
+ // Vercel Sandbox provider:用 @vercel/sandbox SDK 把 Vercel microVM 当隔离工作区跑 eval。
2
2
  // 契约对齐 ../types.ts 的 Sandbox 接口,与 DockerSandbox 可互换。
3
3
 
4
- import { Sandbox as VSandbox } from "@vercel/sandbox";
4
+ import { Sandbox as VSandbox, APIError } from "@vercel/sandbox";
5
5
  import type {
6
6
  Sandbox,
7
7
  CommandResult,
@@ -14,6 +14,17 @@ import { readSourceFilesByList } from "./source-files.ts";
14
14
  import { collectLocalFiles } from "./local-files.ts";
15
15
  import { resolveSandboxPath } from "./paths.ts";
16
16
  import { t } from "../i18n/index.ts";
17
+ import { beforeExternalTerminalWrite } from "../tty-line.ts";
18
+ import type { SandboxProvisionErrorKind } from "./errors.ts";
19
+
20
+ /**
21
+ * vercel SDK 对单次 fetch 的 429 已有内部重试(见 @vercel/sandbox 的 with-retry.js,
22
+ * 5 次指数退避);这里再分类是为了给 create() 整体重试兜底——耗尽内部重试后仍返回 429
23
+ * 响应的 APIError,或 create() 轮询 session 状态过程里撞到的限流,都会走到这里。
24
+ */
25
+ export function classifyProvisionError(e: unknown): SandboxProvisionErrorKind {
26
+ return e instanceof APIError && e.response.status === 429 ? "rate_limit" : "unknown";
27
+ }
17
28
 
18
29
  // Vercel Sandbox 的默认工作区路径(SDK writeFiles 默认落这里)。
19
30
  const VERCEL_WORKDIR = "/vercel/sandbox";
@@ -111,15 +122,18 @@ export class VercelSandbox implements Sandbox {
111
122
  // 挂起的 stop(最长 15s)不该拖住触发 rotate 的那条命令,还烧新 session 的时长。
112
123
  // 失败只警告不静默(旧的到 session timeout 也会被平台回收)。
113
124
  void withTimeout(oldVsb.stop(), STOP_OLD_SESSION_TIMEOUT_MS).catch((stopErr) => {
125
+ beforeExternalTerminalWrite();
114
126
  console.error(
115
127
  `[VercelSandbox] warning: failed to stop rotated-out session, microVM may leak until session timeout: ${String(stopErr)}`,
116
128
  );
117
129
  });
130
+ beforeExternalTerminalWrite();
118
131
  console.error(t("vercel.rotated", {
119
132
  seconds: Math.round(elapsed / 1000),
120
133
  sessionId: newVsb.currentSession().sessionId,
121
134
  }));
122
135
  } catch (err) {
136
+ beforeExternalTerminalWrite();
123
137
  console.error(t("vercel.rotateFailed", {
124
138
  seconds: Math.round(elapsed / 1000),
125
139
  error: String(err),
@@ -176,7 +190,7 @@ export class VercelSandbox implements Sandbox {
176
190
  }
177
191
 
178
192
  // targetDir 已由 paths.ts 的 normalizeSandboxPaths 解析成绝对路径;这里再解析一次
179
- // 只是对直接使用后端实例(未包 normalize)的幂等防御,提到 map 外只算一次。
193
+ // 只是对直接使用 provider 实例(未包 normalize)的幂等防御,提到 map 外只算一次。
180
194
  async writeFiles(files: Record<string, string>, targetDir?: string): Promise<void> {
181
195
  const base = resolveSandboxPath(this.workdir, targetDir);
182
196
  const entries = Object.entries(files).map(([p, content]) => ({
@@ -1,5 +1,5 @@
1
- // 断言收集器:test 期间记录断言(值级就地、作用域延迟),test 结束后对完整运行
2
- // 结果(ScoringContext)统一 finalize 成 AssertionResult[],再交判决。
1
+ // 断言收集器:test 期间记录断言(值断言就地、作用域断言延迟),test 结束后对完整运行
2
+ // 结果(ScoringContext)统一 finalize 成 AssertionResult[],再交判定。
3
3
 
4
4
  import type { AssertionResult, ScoringContext, Severity, SourceLoc } from "../types.ts";
5
5
  import { captureLoc } from "../source-loc.ts";
@@ -26,7 +26,7 @@ export interface Spec {
26
26
  evaluate(ctx: ScoringContext): number | EvalScore | Promise<number | EvalScore>;
27
27
  }
28
28
 
29
- /** 作者拿到的可链式句柄,改严重级 / 阈值(回头改 spec)。 */
29
+ /** 作者拿到的可链式句柄,改严重度 / 阈值(回头改 spec)。 */
30
30
  export interface RecordHandle {
31
31
  atLeast(threshold: number): RecordHandle;
32
32
  gate(threshold?: number): RecordHandle;
@@ -1,6 +1,6 @@
1
- // LLM-as-judge:用一个与被测 agent 完全分离的评判模型做结构化 autoevals 评分。
1
+ // LLM-as-judge:用一个与被测 agent 完全分离的裁判模型做结构化 autoevals 评分。
2
2
  //
3
- // 评判模型走 OpenAI 兼容的 /chat/completions。base_url + key 解析优先级:
3
+ // 裁判模型走 OpenAI 兼容的 /chat/completions。base_url + key 解析优先级:
4
4
  // judge.baseUrl / judge.apiKeyEnv → NICEEVAL_JUDGE_BASE / CODEX_BASE_URL → OpenAI 官方
5
5
  //
6
6
  // closedQA / factuality / summarizes 直接用 autoevals 库(braintrust)。