niceeval 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INDEX.md +55 -0
- package/dist/agents/types.d.ts +317 -0
- package/dist/context/types.d.ts +335 -0
- package/dist/i18n/core.d.ts +11 -0
- package/dist/i18n/en.d.ts +181 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/zh-CN.d.ts +183 -0
- package/dist/o11y/execution-tree.d.ts +103 -0
- package/dist/o11y/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -0
- package/dist/o11y/otlp/select.d.ts +22 -0
- package/dist/o11y/otlp/turn-otel.d.ts +43 -0
- package/dist/o11y/types.d.ts +190 -0
- package/dist/report/aggregate.d.ts +57 -0
- package/dist/report/aggregate.js +212 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +2 -0
- package/dist/report/components.d.ts +178 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +618 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +32 -0
- package/dist/report/format.js +126 -0
- package/dist/report/index.d.ts +19 -0
- package/dist/report/index.js +30 -0
- package/dist/report/load.d.ts +12 -0
- package/dist/report/load.js +56 -0
- package/dist/report/locale.d.ts +83 -0
- package/dist/report/locale.js +160 -0
- package/dist/report/metrics.d.ts +21 -0
- package/dist/report/metrics.js +115 -0
- package/dist/report/primitives.d.ts +64 -0
- package/dist/report/primitives.js +113 -0
- package/dist/report/react/AttemptList.d.ts +23 -0
- package/dist/report/react/AttemptList.js +20 -0
- package/dist/report/react/DeltaTable.d.ts +8 -0
- package/dist/report/react/DeltaTable.js +24 -0
- package/dist/report/react/EvalList.d.ts +10 -0
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentList.d.ts +10 -0
- package/dist/report/react/ExperimentList.js +15 -0
- package/dist/report/react/GroupSummary.d.ts +8 -0
- package/dist/report/react/GroupSummary.js +8 -0
- package/dist/report/react/MetricBars.d.ts +10 -0
- package/dist/report/react/MetricBars.js +48 -0
- package/dist/report/react/MetricLine.d.ts +9 -0
- package/dist/report/react/MetricLine.js +68 -0
- package/dist/report/react/MetricMatrix.d.ts +10 -0
- package/dist/report/react/MetricMatrix.js +23 -0
- package/dist/report/react/MetricScatter.d.ts +9 -0
- package/dist/report/react/MetricScatter.js +100 -0
- package/dist/report/react/MetricTable.d.ts +11 -0
- package/dist/report/react/MetricTable.js +26 -0
- package/dist/report/react/RunOverview.d.ts +8 -0
- package/dist/report/react/RunOverview.js +12 -0
- package/dist/report/react/Scoreboard.d.ts +8 -0
- package/dist/report/react/Scoreboard.js +26 -0
- package/dist/report/react/cell.d.ts +9 -0
- package/dist/report/react/cell.js +9 -0
- package/dist/report/react/chart-math.d.ts +14 -0
- package/dist/report/react/chart-math.js +68 -0
- package/dist/report/react/colors.d.ts +14 -0
- package/dist/report/react/colors.js +45 -0
- package/dist/report/react/fixtures.d.ts +19 -0
- package/dist/report/react/fixtures.js +393 -0
- package/dist/report/react/format.d.ts +3 -0
- package/dist/report/react/format.js +7 -0
- package/dist/report/react/index.d.ts +6 -0
- package/dist/report/react/index.js +14 -0
- package/dist/report/report.d.ts +24 -0
- package/dist/report/report.js +49 -0
- package/dist/report/text/faces.d.ts +29 -0
- package/dist/report/text/faces.js +503 -0
- package/dist/report/text/layout.d.ts +75 -0
- package/dist/report/text/layout.js +173 -0
- package/dist/report/text/plot.d.ts +37 -0
- package/dist/report/text/plot.js +81 -0
- package/dist/report/text/table.d.ts +8 -0
- package/dist/report/text/table.js +86 -0
- package/dist/report/tree.d.ts +99 -0
- package/dist/report/tree.js +228 -0
- package/dist/report/types.d.ts +420 -0
- package/dist/report/types.js +4 -0
- package/dist/report/web.d.ts +15 -0
- package/dist/report/web.js +37 -0
- package/dist/results/annotated-source.d.ts +61 -0
- package/dist/results/attempt-evidence.d.ts +69 -0
- package/dist/results/attempt-source.d.ts +15 -0
- package/dist/results/locator.d.ts +75 -0
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +45 -0
- package/dist/results/select.js +230 -0
- package/dist/results/source-hash.d.ts +9 -0
- package/dist/results/types.d.ts +204 -0
- package/dist/runner/eval-source.d.ts +22 -0
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +332 -0
- package/dist/sandbox/docker-stream.d.ts +24 -0
- package/dist/sandbox/docker.d.ts +84 -0
- package/dist/sandbox/e2b.d.ts +31 -0
- package/dist/sandbox/errors.d.ts +4 -0
- package/dist/sandbox/local-files.d.ts +6 -0
- package/dist/sandbox/paths.d.ts +4 -0
- package/dist/sandbox/registry.d.ts +13 -0
- package/dist/sandbox/resolve.d.ts +40 -0
- package/dist/sandbox/retry.d.ts +10 -0
- package/dist/sandbox/shell.d.ts +11 -0
- package/dist/sandbox/source-files.d.ts +13 -0
- package/dist/sandbox/types.d.ts +203 -0
- package/dist/sandbox/vercel.d.ts +37 -0
- package/dist/scoring/types.d.ts +70 -0
- package/dist/shared/aggregate.d.ts +29 -0
- package/dist/shared/aggregate.js +55 -0
- package/dist/shared/types.d.ts +27 -0
- package/dist/shared/verdict.d.ts +27 -0
- package/dist/shared/verdict.js +34 -0
- package/dist/tty-line.d.ts +6 -0
- package/dist/types.d.ts +7 -0
- package/dist/util.d.ts +22 -0
- package/docs-site/zh/concepts/adapter.mdx +5 -5
- package/docs-site/zh/concepts/assert.mdx +8 -8
- package/docs-site/zh/concepts/evals.mdx +5 -5
- package/docs-site/zh/concepts/experiment.mdx +2 -2
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +2 -2
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
- package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
- package/docs-site/zh/guides/connect-otel.mdx +3 -3
- package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
- package/docs-site/zh/guides/custom-reports.mdx +196 -36
- package/docs-site/zh/guides/experiments.mdx +7 -1
- package/docs-site/zh/guides/fixtures.mdx +1 -1
- package/docs-site/zh/guides/official-adapters.mdx +31 -9
- package/docs-site/zh/guides/publish-report.mdx +3 -3
- package/docs-site/zh/guides/report-components.mdx +194 -55
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +35 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
- package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +11 -11
- package/docs-site/zh/guides/viewing-results.mdx +199 -79
- package/docs-site/zh/guides/write-experiment.mdx +9 -1
- package/docs-site/zh/guides/write-send.mdx +2 -2
- package/docs-site/zh/index.mdx +4 -3
- package/docs-site/zh/introduction.mdx +2 -1
- package/docs-site/zh/quickstart.mdx +5 -3
- package/docs-site/zh/reference/builtin-agents.mdx +38 -13
- package/docs-site/zh/reference/capabilities.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +36 -13
- package/docs-site/zh/reference/define-agent.mdx +12 -12
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/docs-site/zh/reference/events.mdx +7 -1
- package/docs-site/zh/reference/expect.mdx +3 -3
- package/package.json +10 -6
- package/src/agents/bub.ts +129 -42
- package/src/agents/claude-code.test.ts +152 -0
- package/src/agents/claude-code.ts +127 -13
- package/src/agents/codex.test.ts +192 -0
- package/src/agents/codex.ts +151 -12
- package/src/agents/index.ts +9 -3
- package/src/agents/manifest.ts +41 -0
- package/src/agents/plugin-config.test.ts +94 -0
- package/src/agents/shared.ts +33 -2
- package/src/agents/skills.test.ts +214 -0
- package/src/agents/skills.ts +304 -0
- package/src/agents/streaming.ts +5 -5
- package/src/agents/types.ts +54 -2
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +23 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +8 -8
- package/src/define.ts +11 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +34 -15
- package/src/i18n/zh-CN.ts +37 -21
- package/src/index.ts +1 -1
- package/src/o11y/execution-tree.test.ts +452 -0
- package/src/o11y/execution-tree.ts +367 -0
- package/src/o11y/otlp/receiver.ts +1 -1
- package/src/o11y/otlp/select.ts +7 -3
- package/src/o11y/parsers/claude-code.test.ts +175 -0
- package/src/o11y/parsers/claude-code.ts +32 -0
- package/src/o11y/types.ts +6 -1
- package/src/report/aggregate.ts +26 -20
- package/src/report/built-in-user-parity.test.tsx +643 -0
- package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +223 -50
- package/src/report/compute.ts +358 -53
- package/src/report/dual-render.test.tsx +1125 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +50 -2
- package/src/report/index.ts +48 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +206 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +115 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +108 -0
- package/src/report/react/GroupSummary.tsx +66 -0
- package/src/report/react/MetricBars.tsx +24 -13
- package/src/report/react/MetricLine.tsx +21 -21
- package/src/report/react/MetricMatrix.tsx +8 -4
- package/src/report/react/MetricScatter.tsx +136 -96
- package/src/report/react/MetricTable.tsx +115 -15
- package/src/report/react/RunOverview.tsx +48 -26
- package/src/report/react/Scoreboard.tsx +15 -7
- package/src/report/react/cell.tsx +18 -9
- package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
- package/src/report/react/colors.ts +24 -15
- package/src/report/react/enhance.js +129 -0
- package/src/report/react/fixtures.ts +163 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +22 -6
- package/src/report/react/render.test.tsx +164 -31
- package/src/report/react/styles.css +405 -213
- package/src/report/report.test.ts +403 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +303 -95
- package/src/report/text/layout.ts +76 -11
- package/src/report/text/table.ts +98 -0
- package/src/report/tree.ts +103 -20
- package/src/report/types.ts +196 -25
- package/src/report/web.ts +46 -19
- package/src/results/annotated-source.test.ts +146 -0
- package/src/results/annotated-source.ts +128 -0
- package/src/results/attempt-evidence.test.ts +158 -0
- package/src/results/attempt-evidence.ts +158 -0
- package/src/results/attempt-source.ts +48 -0
- package/src/results/copy.ts +154 -139
- package/src/results/format.ts +54 -47
- package/src/results/host-equivalence.test.ts +527 -0
- package/src/results/index.ts +56 -14
- package/src/results/locator.test.ts +198 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +392 -175
- package/src/results/results.test.ts +943 -494
- package/src/results/select.ts +129 -23
- package/src/results/skipped-notice.ts +0 -0
- package/src/results/source-hash.ts +28 -0
- package/src/results/types.ts +98 -62
- package/src/results/writer.ts +304 -176
- package/src/runner/attempt.test.ts +170 -0
- package/src/runner/attempt.ts +48 -13
- package/src/runner/discover.test.ts +65 -0
- package/src/runner/discover.ts +6 -2
- package/src/runner/eval-source.test.ts +121 -0
- package/src/runner/eval-source.ts +45 -0
- package/src/runner/fingerprint.ts +54 -1
- package/src/runner/remote-sandbox.ts +1 -1
- package/src/runner/report.test.ts +6 -8
- package/src/runner/report.ts +5 -4
- package/src/runner/reporters/artifacts.ts +27 -41
- package/src/runner/reporters/braintrust.test.ts +3 -3
- package/src/runner/reporters/braintrust.ts +1 -1
- package/src/runner/reporters/console.ts +6 -6
- package/src/runner/reporters/json.ts +4 -4
- package/src/runner/reporters/live.test.ts +56 -0
- package/src/runner/reporters/live.ts +36 -10
- package/src/runner/reporters/quiet.test.ts +66 -0
- package/src/runner/reporters/quiet.ts +49 -0
- package/src/runner/reporters/shared.ts +5 -5
- package/src/runner/reporters/table.ts +17 -17
- package/src/runner/run.ts +61 -102
- package/src/runner/types.ts +43 -32
- package/src/sandbox/checkpoint.ts +1 -1
- package/src/sandbox/docker.ts +15 -2
- package/src/sandbox/e2b.ts +11 -5
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +3 -3
- package/src/sandbox/local-files.ts +1 -1
- package/src/sandbox/registry.ts +6 -5
- package/src/sandbox/resolve.ts +38 -23
- package/src/sandbox/retry.test.ts +68 -0
- package/src/sandbox/retry.ts +44 -0
- package/src/sandbox/types.ts +24 -24
- package/src/sandbox/vercel.ts +17 -3
- package/src/scoring/collector.ts +3 -3
- package/src/scoring/judge.ts +2 -2
- package/src/scoring/scoped.ts +13 -1
- package/src/scoring/types.ts +7 -7
- package/src/scoring/verdict.ts +4 -4
- package/src/shared/aggregate.ts +23 -3
- package/src/shared/types.ts +2 -2
- package/src/shared/{outcome.ts → verdict.ts} +15 -15
- package/src/show/compose.ts +12 -132
- package/src/show/index.ts +125 -94
- package/src/show/render.ts +660 -160
- package/src/show/show.test.ts +472 -253
- package/src/source-loc.ts +1 -1
- package/src/tty-line.ts +32 -0
- package/src/types.ts +2 -2
- package/src/util.test.ts +21 -1
- package/src/util.ts +5 -1
- package/src/view/app/App.tsx +33 -198
- package/src/view/app/components/AttemptModal.tsx +6 -6
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/CopyControls.tsx +32 -36
- package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
- package/src/view/app/components/ui/badge.tsx +1 -1
- package/src/view/app/components/ui/dialog.tsx +2 -2
- package/src/view/app/i18n.ts +40 -31
- package/src/view/app/lib/artifact-url.ts +1 -1
- package/src/view/app/lib/attempt-route.test.ts +37 -47
- package/src/view/app/lib/attempt-route.ts +26 -26
- package/src/view/app/lib/rows.ts +9 -112
- package/src/view/app/lib/verdict.ts +25 -0
- package/src/view/app/main.tsx +9 -12
- package/src/view/app/pages/RunsPage.tsx +4 -4
- package/src/view/app/pages/TracesPage.tsx +3 -3
- package/src/view/app/shared.ts +0 -1
- package/src/view/app/types.ts +6 -51
- package/src/view/artifact-serving.test.ts +136 -0
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +21 -21
- package/src/view/data.test.ts +214 -143
- package/src/view/data.ts +166 -120
- package/src/view/index.ts +23 -11
- package/src/view/server.ts +52 -17
- package/src/view/shared/types.ts +22 -31
- package/src/view/styles.css +117 -327
- package/src/view/view-report.test.ts +146 -98
- package/src/report/default-report.tsx +0 -222
- package/src/report/dual-face.test.tsx +0 -527
- package/src/report/react/CaseList.tsx +0 -70
- package/src/view/app/components/CostScoreChart.tsx +0 -147
- package/src/view/app/components/ExperimentTable.tsx +0 -284
- package/src/view/app/components/GroupSelector.tsx +0 -61
- package/src/view/app/components/primitives.tsx +0 -43
- package/src/view/app/lib/outcome.ts +0 -82
package/src/sandbox/e2b.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// E2B
|
|
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)用户 ——
|
|
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
|
-
//
|
|
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
|
+
}
|
package/src/sandbox/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// niceeval/sandbox 公开导出:「在哪里跑」相关的类型 + 工厂 + 扩展点。
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
-
|
|
15
|
+
SandboxProvider,
|
|
16
16
|
SandboxOption,
|
|
17
17
|
SandboxSpec,
|
|
18
18
|
SandboxRuntime,
|
package/src/sandbox/registry.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 中断(Ctrl+C)时,正常路径靠 Effect 的 Scope finalizer 跑 sb.stop() 停容器。但 finalizer
|
|
4
4
|
// 这条路不是 100% 可靠:vsb.stop() 这类远端调用可能慢/挂,用户等不及再按一次 Ctrl+C,进程
|
|
5
|
-
// 就被硬退了 —— 沙箱成了孤儿(
|
|
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
|
-
// 让流程继续走到退出 ——
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/sandbox/resolve.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// 沙箱 provider 解析:把 config.sandbox / experiment.sandbox(工厂函数产出的 spec 数据结构)
|
|
2
|
+
// 折叠成一个具体 provider + 参数,并按需创建实例。`sandbox` 没有默认值也没有按名字选的入口——
|
|
3
3
|
// 沙箱型 agent 必须显式给 dockerSandbox() / vercelSandbox() / e2bSandbox() / defineSandbox(),
|
|
4
4
|
// 省略时 resolveSandbox() 直接抛错,不猜环境、不兜底。
|
|
5
|
-
//
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
57
|
+
return detail ? `${r.provider}:${detail}` : r.provider;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
|
-
*
|
|
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
|
|
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
|
|
83
|
-
//
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
+
}
|
package/src/sandbox/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// sandbox 域类型:Sandbox
|
|
2
|
-
//
|
|
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
|
-
/**
|
|
51
|
-
export type
|
|
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
|
-
*
|
|
98
|
+
* 各 provider 的参数互不相同 —— 这是个按 `provider` 区分的可辨识联合(discriminated union)。
|
|
99
99
|
* 四个变体都带 {@link SandboxHooks} 的 `.setup()` / `.teardown()` 链式方法。
|
|
100
100
|
*/
|
|
101
101
|
export interface DockerSandboxSpec extends SandboxHooks<DockerSandboxSpec> {
|
|
102
|
-
readonly
|
|
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
|
|
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
|
|
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
|
-
*
|
|
122
|
-
* 用 `defineSandbox()` 构造(见 src/define.ts)。`
|
|
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
|
|
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
|
-
/** 追加/覆盖本命令的环境变量(
|
|
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
|
|
145
|
-
*
|
|
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
|
-
*
|
|
152
|
+
* 语义跨 provider 一致:"本命令以 root 跑,否则以标准非 root 用户跑"。各 provider 映射到自己的原生机制
|
|
153
153
|
* (docker:`exec --user root`;E2B:`{ user: "root" }`;Vercel:`{ sudo: true }`;Daytona:`{ user }`)。
|
|
154
|
-
* 本就全程 root
|
|
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
|
-
/** 本沙箱的稳定标识(
|
|
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
|
-
* 对应各
|
|
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
|
-
* 对应各
|
|
214
|
+
* 对应各 provider:Docker putArchive / Vercel fs.writeFile(Buffer) / e2b files.write / …
|
|
215
215
|
*/
|
|
216
216
|
uploadFile(path: string, content: Buffer): Promise<void>;
|
|
217
217
|
}
|
package/src/sandbox/vercel.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// Vercel Sandbox
|
|
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
|
-
//
|
|
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]) => ({
|
package/src/scoring/collector.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// 断言收集器: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
|
-
/**
|
|
29
|
+
/** 作者拿到的可链式句柄,改严重度 / 阈值(回头改 spec)。 */
|
|
30
30
|
export interface RecordHandle {
|
|
31
31
|
atLeast(threshold: number): RecordHandle;
|
|
32
32
|
gate(threshold?: number): RecordHandle;
|
package/src/scoring/judge.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// LLM-as-judge:用一个与被测 agent
|
|
1
|
+
// LLM-as-judge:用一个与被测 agent 完全分离的裁判模型做结构化 autoevals 评分。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
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)。
|