niceeval 0.6.1 → 0.7.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 (378) hide show
  1. package/INDEX.md +23 -23
  2. package/README.zh.md +6 -6
  3. package/dist/agents/types.d.ts +69 -7
  4. package/dist/context/types.d.ts +32 -12
  5. package/dist/i18n/en.d.ts +54 -0
  6. package/dist/i18n/zh-CN.d.ts +57 -3
  7. package/dist/o11y/types.d.ts +16 -2
  8. package/dist/report/aggregate.d.ts +32 -24
  9. package/dist/report/aggregate.js +158 -50
  10. package/dist/report/built-in/index.d.ts +2 -0
  11. package/dist/report/built-in/index.js +8 -0
  12. package/dist/report/components.d.ts +93 -160
  13. package/dist/report/components.js +377 -114
  14. package/dist/report/compute.d.ts +87 -81
  15. package/dist/report/compute.js +597 -417
  16. package/dist/report/flag.d.ts +32 -6
  17. package/dist/report/flag.js +92 -4
  18. package/dist/report/format.d.ts +19 -11
  19. package/dist/report/format.js +30 -13
  20. package/dist/report/index.d.ts +16 -16
  21. package/dist/report/index.js +20 -21
  22. package/dist/report/load.js +3 -2
  23. package/dist/report/locale.d.ts +57 -33
  24. package/dist/report/locale.js +122 -56
  25. package/dist/report/metrics.d.ts +23 -4
  26. package/dist/report/metrics.js +110 -25
  27. package/dist/report/primitives.d.ts +48 -15
  28. package/dist/report/primitives.js +135 -26
  29. package/dist/report/react/AttemptList.d.ts +9 -7
  30. package/dist/report/react/AttemptList.js +17 -10
  31. package/dist/report/react/DeltaTable.js +19 -18
  32. package/dist/report/react/EvalList.d.ts +4 -4
  33. package/dist/report/react/EvalList.js +0 -0
  34. package/dist/report/react/ExperimentComparison.d.ts +10 -0
  35. package/dist/report/react/ExperimentComparison.js +12 -0
  36. package/dist/report/react/ExperimentList.d.ts +4 -3
  37. package/dist/report/react/ExperimentList.js +17 -18
  38. package/dist/report/react/MetricBars.js +5 -4
  39. package/dist/report/react/MetricLine.js +12 -5
  40. package/dist/report/react/MetricMatrix.js +1 -1
  41. package/dist/report/react/MetricScatter.js +59 -28
  42. package/dist/report/react/MetricTable.js +2 -12
  43. package/dist/report/react/ScopeSummary.d.ts +10 -0
  44. package/dist/report/react/ScopeSummary.js +17 -0
  45. package/dist/report/react/Scoreboard.js +6 -6
  46. package/dist/report/react/cell.js +2 -2
  47. package/dist/report/react/chart-math.d.ts +23 -6
  48. package/dist/report/react/chart-math.js +71 -19
  49. package/dist/report/react/fixtures.d.ts +5 -9
  50. package/dist/report/react/fixtures.js +110 -147
  51. package/dist/report/react/index.d.ts +15 -5
  52. package/dist/report/react/index.js +18 -7
  53. package/dist/report/report.d.ts +137 -16
  54. package/dist/report/report.js +259 -28
  55. package/dist/report/text/faces.d.ts +17 -19
  56. package/dist/report/text/faces.js +253 -184
  57. package/dist/report/text/plot.js +1 -1
  58. package/dist/report/text/table.js +38 -7
  59. package/dist/report/tree.d.ts +90 -40
  60. package/dist/report/tree.js +252 -94
  61. package/dist/report/types.d.ts +247 -284
  62. package/dist/report/types.js +4 -3
  63. package/dist/report/web.d.ts +21 -5
  64. package/dist/report/web.js +42 -16
  65. package/dist/results/select.d.ts +38 -16
  66. package/dist/results/select.js +73 -25
  67. package/dist/results/types.d.ts +49 -14
  68. package/dist/runner/feedback/sink.d.ts +110 -0
  69. package/dist/runner/types.d.ts +513 -22
  70. package/dist/sandbox/docker.d.ts +23 -2
  71. package/dist/sandbox/e2b.d.ts +15 -1
  72. package/dist/sandbox/errors.d.ts +30 -3
  73. package/dist/sandbox/io-retry.d.ts +17 -0
  74. package/dist/sandbox/registry.d.ts +2 -0
  75. package/dist/sandbox/resolve.d.ts +18 -5
  76. package/dist/sandbox/retry.d.ts +11 -1
  77. package/dist/sandbox/types.d.ts +39 -5
  78. package/dist/sandbox/vercel.d.ts +7 -1
  79. package/dist/scoring/coverage.d.ts +30 -0
  80. package/dist/scoring/display.d.ts +21 -0
  81. package/dist/scoring/display.js +120 -0
  82. package/dist/scoring/types.d.ts +103 -20
  83. package/dist/shared/aggregate.d.ts +4 -2
  84. package/dist/shared/aggregate.js +8 -7
  85. package/dist/shared/types.d.ts +28 -0
  86. package/dist/tty-line.d.ts +0 -4
  87. package/dist/util.d.ts +23 -0
  88. package/docs-site/zh/README.md +44 -0
  89. package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
  90. package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
  91. package/docs-site/zh/examples/index.mdx +50 -0
  92. package/docs-site/zh/{concepts → explanation}/adapter.mdx +31 -13
  93. package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
  94. package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
  95. package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
  96. package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
  97. package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
  98. package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
  99. package/docs-site/zh/{concepts → explanation}/overview.mdx +11 -11
  100. package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
  101. package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
  102. package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
  103. package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
  104. package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
  105. package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
  106. package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
  107. package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
  108. package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
  109. package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
  110. package/docs-site/zh/how-to/publish-report.mdx +105 -0
  111. package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
  112. package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
  113. package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
  114. package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
  115. package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
  116. package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
  117. package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
  118. package/docs-site/zh/index.mdx +24 -26
  119. package/docs-site/zh/introduction.mdx +8 -8
  120. package/docs-site/zh/reference/builtin-agents.mdx +32 -5
  121. package/docs-site/zh/reference/capabilities.mdx +8 -8
  122. package/docs-site/zh/reference/cli.mdx +40 -12
  123. package/docs-site/zh/reference/define-agent.mdx +58 -5
  124. package/docs-site/zh/reference/define-config.mdx +1 -1
  125. package/docs-site/zh/reference/define-eval.mdx +42 -9
  126. package/docs-site/zh/reference/events.mdx +3 -3
  127. package/docs-site/zh/reference/expect.mdx +26 -1
  128. package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
  129. package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
  130. package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
  131. package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
  132. package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
  133. package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
  134. package/package.json +10 -2
  135. package/src/agents/ai-sdk-otel.test.ts +1 -0
  136. package/src/agents/ai-sdk.test.ts +3 -0
  137. package/src/agents/ai-sdk.ts +3 -0
  138. package/src/agents/bub-install-spec.test.ts +34 -0
  139. package/src/agents/bub-install-spec.ts +32 -0
  140. package/src/agents/bub.ts +31 -32
  141. package/src/agents/claude-code.test.ts +130 -9
  142. package/src/agents/claude-code.ts +76 -4
  143. package/src/agents/codex.test.ts +189 -40
  144. package/src/agents/codex.ts +155 -14
  145. package/src/agents/coding-cli-versions.test.ts +15 -0
  146. package/src/agents/coding-cli-versions.ts +3 -0
  147. package/src/agents/index.ts +13 -2
  148. package/src/agents/langgraph.test.ts +204 -0
  149. package/src/agents/langgraph.ts +495 -0
  150. package/src/agents/marketplace.ts +85 -0
  151. package/src/agents/native-config.test.ts +179 -0
  152. package/src/agents/native-config.ts +267 -0
  153. package/src/agents/openai-compat.test.ts +1 -0
  154. package/src/agents/openai-compat.ts +1 -1
  155. package/src/agents/openclaw.test.ts +31 -0
  156. package/src/agents/openclaw.ts +171 -0
  157. package/src/agents/plugin-config.test.ts +1 -0
  158. package/src/agents/sdk-streams.test.ts +79 -0
  159. package/src/agents/sdk-streams.ts +55 -10
  160. package/src/agents/skills.test.ts +1 -0
  161. package/src/agents/streaming.test.ts +3 -9
  162. package/src/agents/streaming.ts +2 -2
  163. package/src/agents/types.ts +71 -8
  164. package/src/agents/ui-message-stream.test.ts +3 -0
  165. package/src/cli.ts +446 -124
  166. package/src/context/context.test.ts +51 -12
  167. package/src/context/context.ts +162 -30
  168. package/src/context/session.test.ts +2 -1
  169. package/src/context/session.ts +115 -7
  170. package/src/context/types.ts +30 -12
  171. package/src/define.test.ts +13 -8
  172. package/src/define.ts +25 -4
  173. package/src/expect/index.ts +53 -23
  174. package/src/i18n/en.ts +81 -17
  175. package/src/i18n/zh-CN.ts +80 -17
  176. package/src/o11y/cost.test.ts +1 -0
  177. package/src/o11y/execution-tree.test.ts +1 -20
  178. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  179. package/src/o11y/otlp/parse.test.ts +1 -0
  180. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  181. package/src/o11y/parsers/bub.test.ts +1 -0
  182. package/src/o11y/parsers/claude-code.test.ts +1 -34
  183. package/src/o11y/parsers/openclaw.test.ts +154 -0
  184. package/src/o11y/parsers/openclaw.ts +310 -0
  185. package/src/o11y/prices.json +746 -311
  186. package/src/o11y/tool-names.test.ts +1 -0
  187. package/src/o11y/types.ts +16 -2
  188. package/src/report/aggregate.ts +178 -61
  189. package/src/report/built-in/index.tsx +9 -0
  190. package/src/report/components.tsx +625 -279
  191. package/src/report/compute.ts +723 -491
  192. package/src/report/dual-render.test.tsx +741 -1024
  193. package/src/report/flag.ts +104 -12
  194. package/src/report/format.ts +32 -12
  195. package/src/report/index.ts +119 -46
  196. package/src/report/load.ts +3 -2
  197. package/src/report/locale.ts +136 -65
  198. package/src/report/metrics.ts +108 -25
  199. package/src/report/primitives.tsx +196 -45
  200. package/src/report/react/AttemptList.tsx +30 -43
  201. package/src/report/react/DeltaTable.tsx +63 -45
  202. package/src/report/react/EvalList.tsx +0 -0
  203. package/src/report/react/ExperimentComparison.tsx +73 -0
  204. package/src/report/react/ExperimentList.tsx +50 -32
  205. package/src/report/react/MetricBars.tsx +5 -4
  206. package/src/report/react/MetricLine.tsx +13 -8
  207. package/src/report/react/MetricMatrix.tsx +2 -2
  208. package/src/report/react/MetricScatter.tsx +86 -34
  209. package/src/report/react/MetricTable.tsx +4 -76
  210. package/src/report/react/ScopeSummary.tsx +86 -0
  211. package/src/report/react/Scoreboard.tsx +28 -10
  212. package/src/report/react/cell.tsx +2 -2
  213. package/src/report/react/chart-math.test.ts +85 -0
  214. package/src/report/react/chart-math.ts +101 -22
  215. package/src/report/react/enhance.js +89 -5
  216. package/src/report/react/fixtures.ts +114 -154
  217. package/src/report/react/index.tsx +24 -39
  218. package/src/report/react/render.test.tsx +138 -158
  219. package/src/report/react/styles.css +243 -82
  220. package/src/report/report.test.ts +779 -841
  221. package/src/report/report.ts +423 -41
  222. package/src/report/text/faces.ts +290 -193
  223. package/src/report/text/plot.ts +1 -1
  224. package/src/report/text/table.ts +44 -7
  225. package/src/report/tree.ts +362 -104
  226. package/src/report/types.ts +261 -271
  227. package/src/report/web.ts +63 -20
  228. package/src/results/annotated-source.test.ts +62 -9
  229. package/src/results/annotated-source.ts +64 -6
  230. package/src/results/attempt-evidence.test.ts +13 -11
  231. package/src/results/attempt-evidence.ts +20 -13
  232. package/src/results/attempt-source.ts +6 -3
  233. package/src/results/copy.ts +150 -60
  234. package/src/results/host-equivalence.test.ts +34 -20
  235. package/src/results/index.ts +12 -4
  236. package/src/results/locator.test.ts +1 -22
  237. package/src/results/open.ts +15 -5
  238. package/src/results/publish.ts +149 -0
  239. package/src/results/results.test.ts +89 -54
  240. package/src/results/select.ts +104 -34
  241. package/src/results/truncate.ts +90 -0
  242. package/src/results/types.ts +43 -14
  243. package/src/results/writer.ts +31 -13
  244. package/src/runner/attempt.test.ts +138 -7
  245. package/src/runner/attempt.ts +603 -104
  246. package/src/runner/discover.test.ts +47 -0
  247. package/src/runner/discover.ts +36 -2
  248. package/src/runner/eval-source.test.ts +1 -27
  249. package/src/runner/feedback/agent.test.ts +504 -0
  250. package/src/runner/feedback/agent.ts +409 -0
  251. package/src/runner/feedback/ci.test.ts +562 -0
  252. package/src/runner/feedback/ci.ts +401 -0
  253. package/src/runner/feedback/coordinator.test.ts +317 -0
  254. package/src/runner/feedback/coordinator.ts +397 -0
  255. package/src/runner/feedback/failure.ts +40 -0
  256. package/src/runner/feedback/human.test.ts +616 -0
  257. package/src/runner/feedback/human.ts +535 -0
  258. package/src/runner/feedback/index.ts +66 -0
  259. package/src/runner/feedback/io.ts +78 -0
  260. package/src/runner/feedback/profile.test.ts +50 -0
  261. package/src/runner/feedback/profile.ts +58 -0
  262. package/src/runner/feedback/reducer.test.ts +395 -0
  263. package/src/runner/feedback/reducer.ts +260 -0
  264. package/src/runner/feedback/renderer.ts +82 -0
  265. package/src/runner/feedback/sink.ts +203 -0
  266. package/src/runner/feedback/testing.ts +106 -0
  267. package/src/runner/ledger.test.ts +230 -0
  268. package/src/runner/ledger.ts +329 -0
  269. package/src/runner/report.test.ts +128 -3
  270. package/src/runner/report.ts +33 -9
  271. package/src/runner/reporters/artifacts.ts +8 -2
  272. package/src/runner/reporters/braintrust.test.ts +8 -7
  273. package/src/runner/reporters/braintrust.ts +9 -2
  274. package/src/runner/reporters/index.ts +2 -2
  275. package/src/runner/reporters/json.test.ts +162 -0
  276. package/src/runner/reporters/json.ts +35 -8
  277. package/src/runner/reporters/shared.ts +1 -5
  278. package/src/runner/run.test.ts +760 -3
  279. package/src/runner/run.ts +243 -37
  280. package/src/runner/sandbox-prep.ts +3 -42
  281. package/src/runner/timing.ts +158 -0
  282. package/src/runner/types.ts +518 -22
  283. package/src/sandbox/checkpoint.test.ts +55 -0
  284. package/src/sandbox/checkpoint.ts +29 -8
  285. package/src/sandbox/cli-commands.ts +407 -0
  286. package/src/sandbox/docker.ts +115 -16
  287. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  288. package/src/sandbox/e2b-agent-template.ts +94 -0
  289. package/src/sandbox/e2b.ts +74 -9
  290. package/src/sandbox/errors.ts +111 -4
  291. package/src/sandbox/index.ts +2 -0
  292. package/src/sandbox/io-retry.test.ts +58 -0
  293. package/src/sandbox/io-retry.ts +45 -0
  294. package/src/sandbox/keep-registry.test.ts +86 -0
  295. package/src/sandbox/keep-registry.ts +142 -0
  296. package/src/sandbox/keep.ts +178 -0
  297. package/src/sandbox/paths.test.ts +1 -0
  298. package/src/sandbox/paths.ts +19 -8
  299. package/src/sandbox/registry.ts +20 -3
  300. package/src/sandbox/resolve.ts +76 -11
  301. package/src/sandbox/retry.test.ts +70 -0
  302. package/src/sandbox/retry.ts +46 -4
  303. package/src/sandbox/types.ts +44 -6
  304. package/src/sandbox/vercel.ts +43 -20
  305. package/src/scoring/collector.ts +60 -17
  306. package/src/scoring/coverage.ts +95 -0
  307. package/src/scoring/diff.ts +81 -0
  308. package/src/scoring/display.test.ts +121 -0
  309. package/src/scoring/display.ts +133 -0
  310. package/src/scoring/evidence.test.ts +189 -0
  311. package/src/scoring/judge.test.ts +142 -0
  312. package/src/scoring/judge.ts +15 -18
  313. package/src/scoring/scoped.ts +217 -50
  314. package/src/scoring/types.ts +117 -20
  315. package/src/scoring/verdict.ts +16 -4
  316. package/src/shared/aggregate.ts +8 -6
  317. package/src/shared/types.ts +31 -0
  318. package/src/show/compose.ts +50 -67
  319. package/src/show/index.ts +127 -56
  320. package/src/show/render.ts +662 -131
  321. package/src/show/report-host.test.ts +188 -0
  322. package/src/show/report-host.ts +375 -0
  323. package/src/show/show.test.ts +320 -54
  324. package/src/tty-line.ts +8 -26
  325. package/src/util.test.ts +1 -0
  326. package/src/util.ts +41 -0
  327. package/src/view/app/App.test.tsx +69 -0
  328. package/src/view/app/App.tsx +144 -48
  329. package/src/view/app/components/AttemptModal.tsx +423 -11
  330. package/src/view/app/components/CodeView.tsx +41 -14
  331. package/src/view/app/components/CopyControls.tsx +2 -2
  332. package/src/view/app/i18n.ts +37 -17
  333. package/src/view/app/lib/attempt-route.test.ts +1 -0
  334. package/src/view/app/lib/verdict.ts +7 -9
  335. package/src/view/app/main.tsx +13 -8
  336. package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
  337. package/src/view/app/types.ts +4 -1
  338. package/src/view/artifact-serving.test.ts +2 -1
  339. package/src/view/client-dist/app.css +1 -1
  340. package/src/view/client-dist/app.js +17 -17
  341. package/src/view/data.test.ts +10 -3
  342. package/src/view/data.ts +155 -49
  343. package/src/view/index.ts +56 -41
  344. package/src/view/server.ts +37 -15
  345. package/src/view/shared/types.ts +34 -5
  346. package/src/view/styles.css +227 -0
  347. package/src/view/view-report.test.ts +167 -62
  348. package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
  349. package/dist/report/built-ins/experiment-comparison.js +0 -13
  350. package/dist/report/built-ins/index.d.ts +0 -1
  351. package/dist/report/built-ins/index.js +0 -2
  352. package/dist/report/react/GroupSummary.d.ts +0 -8
  353. package/dist/report/react/GroupSummary.js +0 -8
  354. package/dist/report/react/RunOverview.d.ts +0 -8
  355. package/dist/report/react/RunOverview.js +0 -12
  356. package/docs-site/zh/example/ai-agent-application.mdx +0 -152
  357. package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
  358. package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
  359. package/docs-site/zh/example/showcase.mdx +0 -39
  360. package/docs-site/zh/guides/publish-report.mdx +0 -91
  361. package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
  362. package/src/report/built-in-user-parity.test.tsx +0 -640
  363. package/src/report/built-ins/experiment-comparison.tsx +0 -19
  364. package/src/report/built-ins/index.ts +0 -2
  365. package/src/report/react/GroupSummary.tsx +0 -66
  366. package/src/report/react/RunOverview.tsx +0 -109
  367. package/src/runner/reporters/console.ts +0 -70
  368. package/src/runner/reporters/live.test.ts +0 -56
  369. package/src/runner/reporters/live.ts +0 -247
  370. package/src/runner/reporters/quiet.test.ts +0 -66
  371. package/src/runner/reporters/quiet.ts +0 -49
  372. package/src/runner/reporters/table.ts +0 -277
  373. /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
  374. /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
  375. /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
  376. /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
  377. /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
  378. /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
@@ -4,11 +4,13 @@
4
4
  // 省略时 resolveSandbox() 直接抛错,不猜环境、不兜底。
5
5
  // provider 名的行为分支只允许出现在 sandbox/ 内(见 docs/architecture.md)。
6
6
 
7
+ import { createHash, randomUUID } from "node:crypto";
7
8
  import { Effect } from "effect";
8
- import type { CustomSandboxSpec, Sandbox, SandboxOption, SandboxRuntime } from "../types.ts";
9
+ import type { CustomSandboxSpec, JsonValue, Sandbox, SandboxOption, SandboxRuntime, ScopedFeedback } from "../types.ts";
9
10
  import { registerSandbox, stopSandbox } from "./registry.ts";
10
11
  import { normalizeSandboxPaths } from "./paths.ts";
11
12
  import { t } from "../i18n/index.ts";
13
+ import { reportActivity, reportDiagnostic } from "../runner/feedback/sink.ts";
12
14
  import { withProvisionRetry, type ProvisionSlot } from "./retry.ts";
13
15
 
14
16
  /** 归一化后的沙箱描述:确定的 provider + 各 provider 参数(只有对应 provider 用得上的会有值)。 */
@@ -50,6 +52,36 @@ export function sandboxRecommendedConcurrency(opt: SandboxOption | undefined): n
50
52
  }
51
53
  }
52
54
 
55
+ /**
56
+ * ExperimentRunInfo.sandbox 的投影:provider 名 + 公开参数(镜像/快照/模板/runtime)+ 配置指纹。
57
+ * 参数只经这个投影落盘——token、凭据路径永不进来;defineSandbox 自定义 provider 未实现
58
+ * `publicConfig()` 时只落 provider 名(见 docs/feature/results/architecture.md)。
59
+ */
60
+ export function sandboxRunInfo(
61
+ opt: SandboxOption | undefined,
62
+ ): { provider: string; params?: Record<string, JsonValue>; fingerprint?: string } | undefined {
63
+ if (!opt) return undefined;
64
+ const r = resolveSandbox(opt);
65
+ let params: Record<string, JsonValue> | undefined;
66
+ if (r.create) {
67
+ // 自定义 provider:只有显式实现了 publicConfig() 投影才落参数。
68
+ params = (opt as CustomSandboxSpec).publicConfig?.();
69
+ } else {
70
+ const p: Record<string, JsonValue> = {};
71
+ if (r.image !== undefined) p.image = r.image;
72
+ if (r.snapshotId !== undefined) p.snapshotId = r.snapshotId;
73
+ if (r.template !== undefined) p.template = r.template;
74
+ if (r.runtime !== undefined) p.runtime = r.runtime;
75
+ params = Object.keys(p).length > 0 ? p : undefined;
76
+ }
77
+ if (params === undefined) return { provider: r.provider };
78
+ const fingerprint = createHash("sha256")
79
+ .update(JSON.stringify({ provider: r.provider, params }))
80
+ .digest("hex")
81
+ .slice(0, 16);
82
+ return { provider: r.provider, params, fingerprint };
83
+ }
84
+
53
85
  /** 报告 / 日志用的简短标签:provider 名,带上区分性的参数(镜像 / 快照 / 模板)。 */
54
86
  export function sandboxLabel(opt: SandboxOption | undefined): string {
55
87
  const r = resolveSandbox(opt);
@@ -61,59 +93,92 @@ export function sandboxLabel(opt: SandboxOption | undefined): string {
61
93
  * 按解析出的 provider + 参数创建沙箱,并把 stop() 注册为 Scope 回收动作。
62
94
  * 在 Effect.scoped / Effect.gen 里 yield* 即可;成功/失败/中断都保证 stop。
63
95
  */
96
+ /** 没有 runner 绑定 feedback 时(测试直调等)的兜底:退回全局 sink,行为与旧接线一致。 */
97
+ function fallbackFeedback(): ScopedFeedback {
98
+ return {
99
+ progress: (u) =>
100
+ reportActivity(u.current !== undefined && u.total !== undefined ? `${u.message} (${u.current}/${u.total})` : u.message),
101
+ diagnostic: (d) =>
102
+ reportDiagnostic({ key: d.dedupeKey ?? d.code, severity: d.level, message: d.message, data: d.data }),
103
+ };
104
+ }
105
+
64
106
  export function createSandbox(opts: {
65
107
  sandbox?: SandboxOption;
66
108
  timeout?: number;
67
109
  runtime?: SandboxRuntime;
68
110
  /** 调用方并发槽位的临时归还/收回,传给 withProvisionRetry 在退避睡眠期间释放(见 retry.ts)。 */
69
111
  provisionSlot?: ProvisionSlot;
112
+ /** runner 绑定到 `sandbox.create` 阶段的反馈句柄;provider 的进度/诊断都走它。 */
113
+ feedback?: ScopedFeedback;
114
+ /**
115
+ * release 覆写(留存路径用):Scope 关闭时按调用方的 disposition 决定 stop 还是 suspend。
116
+ * 省略 = 恒 stopSandbox(默认销毁)。
117
+ */
118
+ release?: (sb: Sandbox) => Promise<void>;
70
119
  }) {
71
120
  const r = resolveSandbox(opts.sandbox, opts.runtime);
121
+ const feedback = opts.feedback ?? fallbackFeedback();
72
122
  return Effect.acquireRelease(
73
123
  Effect.promise<Sandbox>(async () => {
74
124
  // 起好就登记:让 cli 的兜底强清(二次 Ctrl+C / 看门狗超时)能直接停到它,不只靠下面的
75
125
  // release。即便本 fiber 创建后立刻被中断、release 还没来得及跑,登记表也已认得这个沙箱。
76
- const sb = normalizeSandboxPaths(await createProvider(r, opts.timeout, opts.provisionSlot));
126
+ const sb = normalizeSandboxPaths(await createProvider(r, feedback, opts.timeout, opts.provisionSlot));
77
127
  registerSandbox(sb);
78
128
  return sb;
79
129
  }),
80
130
  // release:成功 / 失败 / 中断都跑。带超时 + 失败不静默(stopSandbox 内做),并把它移出登记表。
81
- (sb) => Effect.promise(() => stopSandbox(sb)),
131
+ (sb) => Effect.promise(() => (opts.release ? opts.release(sb) : stopSandbox(sb))),
82
132
  );
83
133
  }
84
134
 
85
- async function createProvider(r: ResolvedSandbox, timeout?: number, provisionSlot?: ProvisionSlot): Promise<Sandbox> {
86
- // 自定义 provider(defineSandbox):不认 provider 名,直接调用用户给的 create()。
87
- if (r.create) return r.create({ timeout, runtime: r.runtime });
135
+ async function createProvider(
136
+ r: ResolvedSandbox,
137
+ feedback: ScopedFeedback,
138
+ timeout?: number,
139
+ provisionSlot?: ProvisionSlot,
140
+ ): Promise<Sandbox> {
141
+ // 自定义 provider(defineSandbox):不认 provider 名,直接调用用户给的 create();
142
+ // feedback 已绑定到 sandbox.create 阶段(见 docs/feature/sandbox/library.md)。
143
+ if (r.create) return r.create({ timeout, runtime: r.runtime, feedback });
88
144
  switch (r.provider) {
89
145
  case "docker": {
90
- const { DockerSandbox, classifyProvisionError } = await import("./docker.ts").catch(() => {
146
+ const { DockerSandbox, classifyProvisionError, reconcileProvision } = await import("./docker.ts").catch(() => {
91
147
  throw new Error(t("sandbox.dependencyMissing.docker"));
92
148
  });
149
+ // 一次性 provision token:歧义类失败重试前按它对账(销毁可能已创建的实例再重建)。
150
+ const token = randomUUID();
93
151
  return withProvisionRetry(
94
- () => DockerSandbox.create({ timeout, runtime: r.runtime, image: r.image }),
152
+ () => DockerSandbox.create({ timeout, runtime: r.runtime, image: r.image, feedback, provisionToken: token }),
95
153
  classifyProvisionError,
96
154
  provisionSlot,
155
+ feedback,
156
+ () => reconcileProvision(token),
97
157
  );
98
158
  }
99
159
  case "vercel": {
100
160
  const { VercelSandbox, classifyProvisionError } = await import("./vercel.ts").catch(() => {
101
161
  throw new Error(t("sandbox.dependencyMissing.vercel"));
102
162
  });
163
+ // vercel SDK 没有按元数据检索实例的通道:不传 reconcile,歧义类第一次抛出。
103
164
  return withProvisionRetry(
104
- () => VercelSandbox.create({ timeout, runtime: r.runtime, snapshotId: r.snapshotId }),
165
+ () => VercelSandbox.create({ timeout, runtime: r.runtime, snapshotId: r.snapshotId, feedback }),
105
166
  classifyProvisionError,
106
167
  provisionSlot,
168
+ feedback,
107
169
  );
108
170
  }
109
171
  case "e2b": {
110
- const { E2BSandbox, classifyProvisionError } = await import("./e2b.ts").catch(() => {
172
+ const { E2BSandbox, classifyProvisionError, reconcileProvision } = await import("./e2b.ts").catch(() => {
111
173
  throw new Error(t("sandbox.dependencyMissing.e2b"));
112
174
  });
175
+ const token = randomUUID();
113
176
  return withProvisionRetry(
114
- () => E2BSandbox.create({ timeout, runtime: r.runtime, template: r.template }),
177
+ () => E2BSandbox.create({ timeout, runtime: r.runtime, template: r.template, provisionToken: token }),
115
178
  classifyProvisionError,
116
179
  provisionSlot,
180
+ feedback,
181
+ () => reconcileProvision(token),
117
182
  );
118
183
  }
119
184
  default:
@@ -1,3 +1,4 @@
1
+ // cases: docs/engineering/unit-tests/sandbox/cases.md
1
2
  import { describe, expect, it, vi } from "vitest";
2
3
  import { withProvisionRetry, type ProvisionSlot } from "./retry.ts";
3
4
 
@@ -41,6 +42,75 @@ describe("withProvisionRetry", () => {
41
42
  expect(calls).toEqual([]);
42
43
  });
43
44
 
45
+ it("throws an ambiguous error on the first attempt when no reconcile channel exists", async () => {
46
+ const err = new Error("fetch failed");
47
+ let attempts = 0;
48
+ await expect(
49
+ withProvisionRetry(
50
+ async () => {
51
+ attempts += 1;
52
+ throw err;
53
+ },
54
+ () => "ambiguous",
55
+ ),
56
+ ).rejects.toBe(err);
57
+ expect(attempts).toBe(1);
58
+ });
59
+
60
+ it("reconciles before every retry, including rejected-class errors like rate_limit", async () => {
61
+ vi.useFakeTimers();
62
+ try {
63
+ const order: string[] = [];
64
+ let attempts = 0;
65
+ const promise = withProvisionRetry(
66
+ async () => {
67
+ attempts += 1;
68
+ order.push(`create#${attempts}`);
69
+ if (attempts === 1) throw new Error("rate limited");
70
+ return "sandbox";
71
+ },
72
+ () => "rate_limit",
73
+ undefined,
74
+ undefined,
75
+ async () => {
76
+ order.push("reconcile");
77
+ },
78
+ );
79
+ await vi.runAllTimersAsync();
80
+ await expect(promise).resolves.toBe("sandbox");
81
+ // create() 闭包跨多个请求,429 可能来自实例已创建之后的初始化请求——拒绝类也必须先对账。
82
+ expect(order).toEqual(["create#1", "reconcile", "create#2"]);
83
+ } finally {
84
+ vi.useRealTimers();
85
+ }
86
+ });
87
+
88
+ it("aborts the retry and rethrows the original error when reconcile fails", async () => {
89
+ vi.useFakeTimers();
90
+ try {
91
+ const createErr = new Error("rate limited");
92
+ let attempts = 0;
93
+ const promise = withProvisionRetry(
94
+ async () => {
95
+ attempts += 1;
96
+ throw createErr;
97
+ },
98
+ () => "rate_limit",
99
+ undefined,
100
+ undefined,
101
+ async () => {
102
+ throw new Error("list also rate limited");
103
+ },
104
+ );
105
+ const assertion = expect(promise).rejects.toBe(createErr);
106
+ await vi.runAllTimersAsync();
107
+ await assertion;
108
+ expect(attempts).toBe(1);
109
+ } finally {
110
+ vi.useRealTimers();
111
+ }
112
+ });
113
+
44
114
  it("releases the slot before backing off and reacquires it before retrying", async () => {
45
115
  vi.useFakeTimers();
46
116
  try {
@@ -1,8 +1,13 @@
1
1
  // provider 无关的 provisioning 退避重试:createProvider()(resolve.ts)对每个内置
2
2
  // provider 的 create() 套这一层。只有各 provider 自己的 classifyProvisionError 判为
3
- // 可重试(目前仅 rate_limit)的错误才会退避重试;其它错误第一次就抛出。
3
+ // 可重试的错误才会退避重试;其它错误第一次就抛出。防泄漏的两道防线
4
+ // (见 docs/feature/sandbox/architecture.md「Provisioning 失败与重试」)中,这里承担
5
+ // 「重试前对账」;kill-on-failure 在各 provider 的 create() 内部。
4
6
 
5
- import { isRetryableProvisionError, type SandboxProvisionErrorKind } from "./errors.ts";
7
+ import { isRejectedProvisionError, isRetryableProvisionError, type SandboxProvisionErrorKind } from "./errors.ts";
8
+ import { t } from "../i18n/index.ts";
9
+ import { reportActivity, reportDiagnostic } from "../runner/feedback/sink.ts";
10
+ import type { ScopedFeedback } from "../types.ts";
6
11
 
7
12
  const MAX_ATTEMPTS = 4;
8
13
  const BASE_DELAY_MS = 1000;
@@ -25,20 +30,57 @@ export async function withProvisionRetry<T>(
25
30
  create: () => Promise<T>,
26
31
  classify: (e: unknown) => SandboxProvisionErrorKind,
27
32
  slot?: ProvisionSlot,
33
+ feedback?: ScopedFeedback,
34
+ /**
35
+ * 对账钩子:按 provision token 检索远端、销毁可能已创建的实例。提供时**每次重试前都执行**,
36
+ * 不区分拒绝类还是歧义类——被重试的 create() 闭包跨多个请求,一个 429 可能来自实例已创建
37
+ * 之后的初始化请求,分类不保证远端没有实例。对账排在退避睡眠之后(紧跟失败发出的检索大概率
38
+ * 同样被限流);对账抛错即放弃重试、抛回原始 create 错误。没有对账通道的 provider 不传——
39
+ * 歧义类第一次抛出:宁可判死一个 attempt,不留一台计费的无主实例
40
+ * (见 docs/feature/sandbox/architecture.md)。
41
+ */
42
+ reconcile?: () => Promise<void>,
28
43
  ): Promise<T> {
29
44
  for (let attempt = 0; ; attempt++) {
30
45
  try {
31
46
  return await create();
32
47
  } catch (e) {
33
- if (!isRetryableProvisionError(classify(e)) || attempt >= MAX_ATTEMPTS - 1) throw e;
48
+ const kind = classify(e);
49
+ if (!isRetryableProvisionError(kind) || attempt >= MAX_ATTEMPTS - 1) throw e;
50
+ // 歧义类:远端可能已有实例,没有对账通道就不能重试,第一次抛出。
51
+ if (!isRejectedProvisionError(kind) && !reconcile) throw e;
34
52
  // 退避期间只是在睡觉,不是在真的创建沙箱:攥着并发槽位陪跑 setTimeout 会让被限流的
35
53
  // provider 把整批并发名额拖垮成"看起来卡在个位数并发"——先还名额,睡醒了再排队要回来。
36
54
  if (slot) await slot.release();
55
+ // 「activity」而非「diagnostic」—— 这是正常的退避进度,不是需要去重/永久留痕的
56
+ // warning(与 docker.ts 的镜像拉取进度、vercel.ts 的 session rotate 通知同一个理由,
57
+ // 见 sink.ts 的 reportActivity 说明)。让 human dashboard 的 active slot 在整个退避
58
+ // 窗口里有可见更新,而不是冻结到重试成功或耗尽为止。
59
+ const delayMs = delayFor(attempt);
60
+ const message = t("sandbox.provisionRetry", { delayMs: Math.round(delayMs), attempt: attempt + 1, maxAttempts: MAX_ATTEMPTS }).trimEnd();
61
+ if (feedback) feedback.progress({ message });
62
+ else reportActivity(message);
37
63
  try {
38
- await new Promise((resolve) => setTimeout(resolve, delayFor(attempt)));
64
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
39
65
  } finally {
40
66
  if (slot) await slot.reacquire();
41
67
  }
68
+ if (reconcile) {
69
+ try {
70
+ await reconcile();
71
+ } catch (reconcileError) {
72
+ // 对账是重试的硬前置:查不到账就重试与盲重试无异。放弃重试,留 diagnostic,
73
+ // 抛回原始 create 错误(对账失败不掩盖它)。
74
+ const diagnostic = {
75
+ code: "sandbox-provision-reconcile-failed",
76
+ level: "warning" as const,
77
+ message: t("sandbox.provisionReconcileFailed", { error: String(reconcileError) }).trimEnd(),
78
+ };
79
+ if (feedback) feedback.diagnostic(diagnostic);
80
+ else reportDiagnostic({ key: diagnostic.code, severity: diagnostic.level, message: diagnostic.message });
81
+ throw e;
82
+ }
83
+ }
42
84
  }
43
85
  }
44
86
  }
@@ -1,12 +1,18 @@
1
1
  // sandbox 域类型:Sandbox 接口、provider spec(可辨识联合)、命令与文件 IO 的形状。
2
2
  // 「在哪里跑、如何隔离」的全部契约在这里;provider 实现见本目录各文件,分发见 resolve.ts。
3
3
 
4
- import type { AgentSetup, AgentTeardown } from "../agents/types.ts";
4
+ import type { Cleanup, ScopedFeedback } from "../shared/types.ts";
5
5
 
6
6
  export interface CommandResult {
7
7
  stdout: string;
8
8
  stderr: string;
9
9
  exitCode: number;
10
+ /**
11
+ * 这次执行的命令摘要(有界、已脱敏,与时间树 command 节点同一份文案)。由执行面在最外层
12
+ * 公开调用处附加;直接从 provider 拿到的裸结果可能没有。断言失败时用作 evidence
13
+ * (`commandSucceeded()` 的「命令行本身」),消费方按可选字段读。
14
+ */
15
+ command?: string;
10
16
  }
11
17
 
12
18
  export interface SandboxFile {
@@ -82,15 +88,33 @@ export type SandboxRuntime = "node20" | "node24";
82
88
  */
83
89
  export interface SandboxHooks<Self> {
84
90
  /** 已挂载的 setup 钩子,按追加顺序保存(内部读取,一般用不到)。 */
85
- readonly setupHooks?: readonly AgentSetup[];
91
+ readonly setupHooks?: readonly SandboxHook[];
86
92
  /** 已挂载的 teardown 钩子,按追加顺序保存,执行时逆序(内部读取,一般用不到)。 */
87
- readonly teardownHooks?: readonly AgentTeardown[];
93
+ readonly teardownHooks?: readonly SandboxHook[];
88
94
  /** 追加一个沙箱级 setup 钩子,返回新 spec;详细契约见 {@link SandboxHooks}。 */
89
- setup(fn: AgentSetup): Self;
95
+ setup(fn: SandboxHook): Self;
90
96
  /** 追加一个沙箱级 teardown 钩子,返回新 spec;详细契约见 {@link SandboxHooks}。 */
91
- teardown(fn: AgentTeardown): Self;
97
+ teardown(fn: SandboxHook): Self;
92
98
  }
93
99
 
100
+ /**
101
+ * Sandbox hook 的窄上下文:只有 `experimentId`、`signal` 与作用域绑定的 `progress/diagnostic`,
102
+ * 不借用包含 session / model / telemetry 的完整 `AgentContext`
103
+ * (见 docs/feature/sandbox/library.md「环境层生命周期钩子」)。
104
+ */
105
+ export interface SandboxHookContext extends ScopedFeedback {
106
+ /** 路径推导出的实验 id;不经 experiment 跑时是 undefined。跨 attempt 状态按它分区。 */
107
+ readonly experimentId?: string;
108
+ /** 本次 attempt 的中止信号。 */
109
+ readonly signal: AbortSignal;
110
+ }
111
+
112
+ /** 沙箱级生命周期钩子(`.setup()` / `.teardown()` 链式挂载);`setup` 可返回 cleanup 闭包。 */
113
+ export type SandboxHook = (
114
+ sandbox: Sandbox,
115
+ ctx: SandboxHookContext,
116
+ ) => void | Cleanup | Promise<void | Cleanup>;
117
+
94
118
  /**
95
119
  * Sandbox 的「数据结构」定义 —— 与 agent 一样可带参数(见 docs/feature/sandbox/library.md)。
96
120
  * 必须用工厂函数构造(`dockerSandbox()` / `vercelSandbox()` / `e2bSandbox()` / `defineSandbox()`),
@@ -125,7 +149,13 @@ export interface CustomSandboxSpec extends SandboxHooks<CustomSandboxSpec> {
125
149
  readonly provider: string;
126
150
  readonly runtime?: SandboxRuntime;
127
151
  readonly recommendedConcurrency?: number;
128
- readonly create: (opts: { timeout?: number; runtime?: SandboxRuntime }) => Promise<Sandbox>;
152
+ /** `feedback` 绑定到 `sandbox.create` 阶段:分配实例 / 拉镜像 / 恢复 snapshot 的进度与诊断走它。 */
153
+ readonly create: (opts: { timeout?: number; runtime?: SandboxRuntime; feedback: ScopedFeedback }) => Promise<Sandbox>;
154
+ /**
155
+ * 「哪些参数可发布」的投影,进结果快照的 ExperimentRunInfo.sandbox.params;
156
+ * 未实现时只落 provider 名。token、凭据路径永远不该出现在返回值里。
157
+ */
158
+ readonly publicConfig?: () => Record<string, import("../shared/types.ts").JsonValue>;
129
159
  }
130
160
 
131
161
  export type SandboxSpec = DockerSandboxSpec | VercelSandboxSpec | E2BSandboxSpec | CustomSandboxSpec;
@@ -145,6 +175,14 @@ export interface CommandOptions {
145
175
  * 不支持的 provider 忽略)—— 日志怎么浮现是 provider 的事,adapter 只声明意图。
146
176
  */
147
177
  stream?: boolean;
178
+ /**
179
+ * 命令 stdout 每到一块就调用一次。回调只用于运行中的短命反馈;完整 stdout 仍会原样
180
+ * 出现在返回的 `CommandResult` 里。provider 不支持真流时,至少会在命令结束后按完整
181
+ * stdout 调用一次,不能静默丢掉。
182
+ */
183
+ onStdout?: (chunk: string) => void | Promise<void>;
184
+ /** `onStdout` 的 stderr 对应物;完整 stderr 仍保留在 `CommandResult`。 */
185
+ onStderr?: (chunk: string) => void | Promise<void>;
148
186
  /**
149
187
  * 以 root 跑本命令。默认 `false` —— 命令以沙箱的标准**非 root** 用户跑(agent 的自然环境)。
150
188
  * 给 setup 阶段装系统依赖用(`apt-get install …`、`pip install --break-system-packages …`)。
@@ -14,8 +14,8 @@ 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";
17
+ import { reportActivity, reportDiagnostic } from "../runner/feedback/sink.ts";
18
+ import { classifyProvisionErrorFallback, type SandboxProvisionErrorKind } from "./errors.ts";
19
19
 
20
20
  /**
21
21
  * vercel SDK 对单次 fetch 的 429 已有内部重试(见 @vercel/sandbox 的 with-retry.js,
@@ -23,7 +23,9 @@ import type { SandboxProvisionErrorKind } from "./errors.ts";
23
23
  * 响应的 APIError,或 create() 轮询 session 状态过程里撞到的限流,都会走到这里。
24
24
  */
25
25
  export function classifyProvisionError(e: unknown): SandboxProvisionErrorKind {
26
- return e instanceof APIError && e.response.status === 429 ? "rate_limit" : "unknown";
26
+ if (e instanceof APIError && e.response.status === 429) return "rate_limit";
27
+ // SDK 没有按元数据检索实例的通道:歧义类不重试、第一次抛出(见 retry.ts 的 reconcile 语义)。
28
+ return classifyProvisionErrorFallback(e);
27
29
  }
28
30
 
29
31
  // Vercel Sandbox 的默认工作区路径(SDK writeFiles 默认落这里)。
@@ -70,7 +72,7 @@ export class VercelSandbox implements Sandbox {
70
72
  }
71
73
 
72
74
  static async create(
73
- opts: { timeout?: number; runtime?: "node20" | "node24"; snapshotId?: string } = {},
75
+ opts: { timeout?: number; runtime?: "node20" | "node24"; snapshotId?: string; feedback?: import("../types.ts").ScopedFeedback } = {},
74
76
  ): Promise<VercelSandbox> {
75
77
  // Vercel 支持 node22/node24/node26/python3.13;node20 回退到 node22。
76
78
  const runtime = opts.runtime === "node20" ? "node22" : (opts.runtime ?? "node24");
@@ -122,22 +124,29 @@ export class VercelSandbox implements Sandbox {
122
124
  // 挂起的 stop(最长 15s)不该拖住触发 rotate 的那条命令,还烧新 session 的时长。
123
125
  // 失败只警告不静默(旧的到 session timeout 也会被平台回收)。
124
126
  void withTimeout(oldVsb.stop(), STOP_OLD_SESSION_TIMEOUT_MS).catch((stopErr) => {
125
- beforeExternalTerminalWrite();
126
- console.error(
127
- `[VercelSandbox] warning: failed to stop rotated-out session, microVM may leak until session timeout: ${String(stopErr)}`,
128
- );
127
+ reportDiagnostic({
128
+ key: "vercel-stop-rotated-session-failed",
129
+ severity: "warning",
130
+ message: `[VercelSandbox] warning: failed to stop rotated-out session, microVM may leak until session timeout: ${String(stopErr)}`,
131
+ data: { sandboxId: this.sandboxId, error: String(stopErr) },
132
+ });
129
133
  });
130
- beforeExternalTerminalWrite();
131
- console.error(t("vercel.rotated", {
132
- seconds: Math.round(elapsed / 1000),
133
- sessionId: newVsb.currentSession().sessionId,
134
- }));
134
+ reportActivity(
135
+ t("vercel.rotated", {
136
+ seconds: Math.round(elapsed / 1000),
137
+ sessionId: newVsb.currentSession().sessionId,
138
+ }).trimEnd(),
139
+ );
135
140
  } catch (err) {
136
- beforeExternalTerminalWrite();
137
- console.error(t("vercel.rotateFailed", {
138
- seconds: Math.round(elapsed / 1000),
139
- error: String(err),
140
- }));
141
+ reportDiagnostic({
142
+ key: "vercel-rotate-failed",
143
+ severity: "warning",
144
+ message: t("vercel.rotateFailed", {
145
+ seconds: Math.round(elapsed / 1000),
146
+ error: String(err),
147
+ }).trimEnd(),
148
+ data: { sandboxId: this.sandboxId, error: String(err) },
149
+ });
141
150
  }
142
151
  }
143
152
 
@@ -152,9 +161,15 @@ export class VercelSandbox implements Sandbox {
152
161
  // 显式设 per-command timeout 防止长跑命令(npm build/install)被流截断。
153
162
  timeoutMs: this.commandTimeoutMs,
154
163
  });
164
+ const stdout = await finished.stdout();
165
+ const stderr = await finished.stderr();
166
+ // Vercel SDK 的命令 API 只在结束后给完整输出;仍兑现 CommandOptions 回调的
167
+ // 「至少一次」语义,让 adapter 不必按 provider 分叉。
168
+ if (stdout) await opts.onStdout?.(stdout);
169
+ if (stderr) await opts.onStderr?.(stderr);
155
170
  return {
156
- stdout: await finished.stdout(),
157
- stderr: await finished.stderr(),
171
+ stdout,
172
+ stderr,
158
173
  exitCode: finished.exitCode,
159
174
  };
160
175
  }
@@ -220,6 +235,14 @@ export class VercelSandbox implements Sandbox {
220
235
  await this.vsb.stop();
221
236
  }
222
237
 
238
+ /**
239
+ * 留存休眠(suspend):vercel `stop`——sandbox 默认持久,stop 保存文件系统,之后经
240
+ * `Sandbox.get` 恢复(SDK 原生能力);内存态不保留,唤醒后进程要重新启动。
241
+ */
242
+ async suspend(): Promise<void> {
243
+ await this.vsb.stop();
244
+ }
245
+
223
246
  async downloadFile(path: string): Promise<Buffer> {
224
247
  const absPath = resolveSandboxPath(this.workdir, path);
225
248
  const buf = await this.vsb.readFileToBuffer({ path: absPath });
@@ -9,27 +9,50 @@ import { formatThrown } from "../util.ts";
9
9
  export interface EvalScore {
10
10
  score: number;
11
11
  detail?: string;
12
- /** 这条分数是看着什么材料算出来的(judge 收到的输入,或 t.check 失败时实际被检查的值);供 view 展开排查「为什么是这个分」。 */
12
+ /** 期望条件的有界文本预览( `contains "Brooklyn"`),失败时供 show/view 直接展示。 */
13
+ expected?: string;
14
+ /** 实际值的有界文本预览(被检查值 / 作用域内实际调用清单)。 */
15
+ received?: string;
16
+ /** 这条分数是看着什么材料算出来的(judge 收到的输入等);供 view 展开排查「为什么是这个分」。 */
13
17
  evidence?: string;
14
18
  }
15
19
 
16
- /** 一条尚未评估的断言。evaluate finalize 时拿到完整运行结果再算分 [0,1]。 */
20
+ /** evaluate 返回它表示「这条断言评不了」:证据通道不完整 / judge 未解析到模型等。 */
21
+ export interface EvalUnavailable {
22
+ unavailable: true;
23
+ /** 机器可读原因,如 "judge-model-unresolved"、"coverage:actions=partial"。 */
24
+ reason: string;
25
+ }
26
+
27
+ /** 构造 EvalUnavailable 的便捷工厂(scoped / judge 断言用)。 */
28
+ export function unavailable(reason: string): EvalUnavailable {
29
+ return { unavailable: true, reason };
30
+ }
31
+
32
+ function isUnavailable(v: unknown): v is EvalUnavailable {
33
+ return typeof v === "object" && v !== null && (v as EvalUnavailable).unavailable === true;
34
+ }
35
+
36
+ /** 一条尚未评估的断言。evaluate 在 finalize 时拿到完整运行结果再算分 [0,1](或报告评不了)。 */
17
37
  export interface Spec {
18
38
  name: string;
19
39
  severity: Severity;
20
40
  threshold?: number;
41
+ /** 作者用 .optional() 显式允许该断言证据缺席;unavailable 只保留在记录里,不影响判定。 */
42
+ optional?: true;
21
43
  detail?: string;
22
- /** 所属分组(t.group 标题,可嵌套用 › 连接)。纯组织用,不影响打分。 */
23
- group?: string;
44
+ /** 所属分组路径(外层在前的 t.group 标题数组)。纯组织用,不影响打分。 */
45
+ groupPath?: string[];
24
46
  /** 断言在 eval 源码里的调用点(record 时栈回溯抠出)。 */
25
47
  loc?: SourceLoc;
26
- evaluate(ctx: ScoringContext): number | EvalScore | Promise<number | EvalScore>;
48
+ evaluate(ctx: ScoringContext): number | EvalScore | EvalUnavailable | Promise<number | EvalScore | EvalUnavailable>;
27
49
  }
28
50
 
29
- /** 作者拿到的可链式句柄,改严重度 / 阈值(回头改 spec)。 */
51
+ /** 作者拿到的可链式句柄,改严重度 / 阈值 / optional(回头改 spec)。 */
30
52
  export interface RecordHandle {
31
53
  atLeast(threshold: number): RecordHandle;
32
54
  gate(threshold?: number): RecordHandle;
55
+ optional(): RecordHandle;
33
56
  }
34
57
 
35
58
  export class AssertionCollector {
@@ -40,7 +63,7 @@ export class AssertionCollector {
40
63
  return this.specs.length > 0;
41
64
  }
42
65
 
43
- /** t.group(title, fn) 期间入栈;栈内 record 的断言都打上当前(嵌套则 › 连接)分组标题。 */
66
+ /** t.group(title, fn) 期间入栈;栈内 record 的断言都打上当前分组路径(嵌套时外层在前) */
44
67
  async withGroup<T>(title: string, fn: () => Promise<T> | T): Promise<T> {
45
68
  this.groupStack.push(title);
46
69
  try {
@@ -51,8 +74,8 @@ export class AssertionCollector {
51
74
  }
52
75
 
53
76
  record(spec: Spec): RecordHandle {
54
- if (spec.group === undefined && this.groupStack.length > 0) {
55
- spec.group = this.groupStack.join(" › ");
77
+ if (spec.groupPath === undefined && this.groupStack.length > 0) {
78
+ spec.groupPath = this.groupStack.slice();
56
79
  }
57
80
  if (spec.loc === undefined) spec.loc = captureLoc();
58
81
  this.specs.push(spec);
@@ -67,6 +90,10 @@ export class AssertionCollector {
67
90
  spec.threshold = threshold;
68
91
  return handle;
69
92
  },
93
+ optional() {
94
+ spec.optional = true;
95
+ return handle;
96
+ },
70
97
  };
71
98
  return handle;
72
99
  }
@@ -74,16 +101,32 @@ export class AssertionCollector {
74
101
  async finalize(ctx: ScoringContext): Promise<AssertionResult[]> {
75
102
  const out: AssertionResult[] = [];
76
103
  for (const spec of this.specs) {
104
+ const base = {
105
+ name: spec.name,
106
+ severity: spec.severity,
107
+ ...(spec.optional ? { optional: true as const } : {}),
108
+ ...(spec.detail !== undefined ? { detail: spec.detail } : {}),
109
+ ...(spec.groupPath !== undefined ? { groupPath: spec.groupPath } : {}),
110
+ ...(spec.loc !== undefined ? { loc: spec.loc } : {}),
111
+ };
77
112
  let score = 0;
78
113
  let detail = spec.detail;
114
+ let expected: string | undefined;
115
+ let received: string | undefined;
79
116
  let evidence: string | undefined;
80
117
  try {
81
118
  const raw = await spec.evaluate(ctx);
119
+ if (isUnavailable(raw)) {
120
+ out.push({ ...base, outcome: "unavailable", reason: raw.reason });
121
+ continue;
122
+ }
82
123
  if (typeof raw === "number") {
83
124
  score = raw;
84
125
  } else {
85
126
  score = raw.score;
86
127
  if (raw.detail) detail = detail ? `${detail}; ${raw.detail}` : raw.detail;
128
+ expected = raw.expected;
129
+ received = raw.received;
87
130
  evidence = raw.evidence;
88
131
  }
89
132
  } catch (e) {
@@ -92,16 +135,16 @@ export class AssertionCollector {
92
135
  error: formatThrown(e),
93
136
  })}`;
94
137
  }
138
+ const passed = computePassed(spec.severity, spec.threshold, score);
95
139
  out.push({
96
- name: spec.name,
97
- severity: spec.severity,
98
- threshold: spec.threshold,
140
+ ...base,
141
+ ...(detail !== undefined ? { detail } : {}),
142
+ outcome: passed ? "passed" : "failed",
99
143
  score,
100
- passed: computePassed(spec.severity, spec.threshold, score),
101
- detail,
102
- evidence,
103
- group: spec.group,
104
- loc: spec.loc,
144
+ ...(spec.threshold !== undefined ? { threshold: spec.threshold } : {}),
145
+ ...(expected !== undefined ? { expected } : {}),
146
+ ...(received !== undefined ? { received } : {}),
147
+ ...(evidence !== undefined ? { evidence } : {}),
105
148
  });
106
149
  }
107
150
  return out;