niceeval 0.6.0 → 0.6.2
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/dist/agents/types.d.ts +72 -6
- package/dist/context/types.d.ts +32 -12
- package/dist/i18n/en.d.ts +54 -0
- package/dist/i18n/zh-CN.d.ts +55 -1
- package/dist/o11y/types.d.ts +16 -2
- package/dist/report/aggregate.d.ts +5 -3
- package/dist/report/aggregate.js +32 -5
- package/dist/report/built-ins/experiment-comparison.d.ts +39 -0
- package/dist/report/built-ins/experiment-comparison.js +119 -0
- package/dist/report/built-ins/index.d.ts +2 -1
- package/dist/report/built-ins/index.js +2 -2
- package/dist/report/components.d.ts +10 -2
- package/dist/report/components.js +3 -3
- package/dist/report/compute.d.ts +11 -18
- package/dist/report/compute.js +68 -66
- package/dist/report/flag.d.ts +16 -1
- package/dist/report/flag.js +19 -1
- package/dist/report/format.d.ts +16 -14
- package/dist/report/format.js +28 -30
- package/dist/report/index.d.ts +5 -4
- package/dist/report/index.js +6 -5
- package/dist/report/locale.d.ts +23 -3
- package/dist/report/locale.js +47 -6
- package/dist/report/metrics.d.ts +13 -1
- package/dist/report/metrics.js +66 -15
- package/dist/report/primitives.d.ts +6 -0
- package/dist/report/react/AttemptList.d.ts +4 -4
- package/dist/report/react/AttemptList.js +8 -10
- package/dist/report/react/EvalList.d.ts +1 -1
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +8 -0
- package/dist/report/react/ExperimentComparison.js +11 -0
- package/dist/report/react/ExperimentList.d.ts +4 -2
- package/dist/report/react/ExperimentList.js +57 -7
- package/dist/report/react/MetricScatter.js +6 -16
- package/dist/report/react/chart-math.d.ts +23 -6
- package/dist/report/react/chart-math.js +71 -19
- package/dist/report/react/fixtures.d.ts +3 -3
- package/dist/report/react/fixtures.js +30 -18
- package/dist/report/react/format.d.ts +1 -1
- package/dist/report/react/format.js +1 -1
- package/dist/report/react/index.d.ts +1 -1
- package/dist/report/report.d.ts +5 -1
- package/dist/report/report.js +6 -2
- package/dist/report/text/faces.d.ts +1 -1
- package/dist/report/text/faces.js +100 -61
- package/dist/report/text/table.js +36 -5
- package/dist/report/types.d.ts +40 -34
- package/dist/results/types.d.ts +11 -0
- package/dist/runner/feedback/sink.d.ts +110 -0
- package/dist/runner/types.d.ts +513 -22
- package/dist/sandbox/docker.d.ts +23 -2
- package/dist/sandbox/e2b.d.ts +15 -1
- package/dist/sandbox/errors.d.ts +30 -3
- package/dist/sandbox/io-retry.d.ts +17 -0
- package/dist/sandbox/registry.d.ts +2 -0
- package/dist/sandbox/resolve.d.ts +18 -5
- package/dist/sandbox/retry.d.ts +11 -1
- package/dist/sandbox/types.d.ts +39 -5
- package/dist/sandbox/vercel.d.ts +7 -1
- package/dist/scoring/coverage.d.ts +30 -0
- package/dist/scoring/display.d.ts +21 -0
- package/dist/scoring/display.js +120 -0
- package/dist/scoring/types.d.ts +103 -20
- package/dist/shared/aggregate.d.ts +1 -0
- package/dist/shared/aggregate.js +3 -3
- package/dist/shared/types.d.ts +28 -0
- package/dist/tty-line.d.ts +0 -4
- package/dist/util.d.ts +23 -0
- package/docs-site/zh/concepts/adapter.mdx +24 -6
- package/docs-site/zh/concepts/assert.mdx +11 -10
- package/docs-site/zh/concepts/evals.mdx +7 -6
- package/docs-site/zh/concepts/experiment.mdx +1 -1
- package/docs-site/zh/concepts/overview.mdx +7 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +35 -31
- package/docs-site/zh/guides/authoring.mdx +33 -0
- package/docs-site/zh/guides/ci-integration.mdx +23 -12
- package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
- package/docs-site/zh/guides/custom-reports.mdx +29 -34
- package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
- package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
- package/docs-site/zh/guides/debugging.mdx +210 -0
- package/docs-site/zh/guides/experiments.mdx +10 -3
- package/docs-site/zh/guides/fixtures.mdx +3 -1
- package/docs-site/zh/guides/official-adapters.mdx +27 -3
- package/docs-site/zh/guides/publish-report.mdx +30 -16
- package/docs-site/zh/guides/report-components.mdx +49 -37
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +42 -8
- package/docs-site/zh/guides/runner.mdx +17 -7
- package/docs-site/zh/guides/sandbox-agent.mdx +57 -7
- package/docs-site/zh/guides/sandbox-providers.mdx +258 -10
- package/docs-site/zh/guides/scoring-guide.mdx +4 -4
- package/docs-site/zh/guides/viewing-results.mdx +85 -41
- package/docs-site/zh/guides/write-experiment.mdx +5 -3
- package/docs-site/zh/guides/write-send.mdx +19 -2
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/reference/builtin-agents.mdx +27 -0
- package/docs-site/zh/reference/capabilities.mdx +2 -2
- package/docs-site/zh/reference/cli.mdx +35 -9
- package/docs-site/zh/reference/define-agent.mdx +60 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +42 -9
- package/docs-site/zh/reference/events.mdx +2 -2
- package/docs-site/zh/reference/expect.mdx +36 -6
- package/package.json +5 -1
- package/src/agents/ai-sdk-otel.test.ts +1 -0
- package/src/agents/ai-sdk.test.ts +3 -0
- package/src/agents/ai-sdk.ts +3 -0
- package/src/agents/bub-install-spec.test.ts +34 -0
- package/src/agents/bub-install-spec.ts +32 -0
- package/src/agents/bub.ts +31 -32
- package/src/agents/claude-code.test.ts +130 -9
- package/src/agents/claude-code.ts +76 -4
- package/src/agents/codex.test.ts +189 -40
- package/src/agents/codex.ts +155 -14
- package/src/agents/coding-cli-versions.test.ts +15 -0
- package/src/agents/coding-cli-versions.ts +3 -0
- package/src/agents/index.ts +11 -0
- package/src/agents/langgraph.test.ts +204 -0
- package/src/agents/langgraph.ts +495 -0
- package/src/agents/marketplace.ts +85 -0
- package/src/agents/native-config.test.ts +179 -0
- package/src/agents/native-config.ts +267 -0
- package/src/agents/openai-compat.test.ts +1 -0
- package/src/agents/openclaw.test.ts +31 -0
- package/src/agents/openclaw.ts +171 -0
- package/src/agents/plugin-config.test.ts +1 -0
- package/src/agents/sdk-streams.test.ts +79 -0
- package/src/agents/sdk-streams.ts +55 -10
- package/src/agents/skills.test.ts +1 -0
- package/src/agents/streaming.test.ts +3 -9
- package/src/agents/types.ts +73 -6
- package/src/agents/ui-message-stream.test.ts +3 -0
- package/src/cli.ts +411 -108
- package/src/context/context.test.ts +51 -12
- package/src/context/context.ts +161 -29
- package/src/context/session.test.ts +1 -0
- package/src/context/session.ts +114 -6
- package/src/context/types.ts +30 -12
- package/src/define.test.ts +13 -8
- package/src/define.ts +25 -4
- package/src/expect/index.ts +53 -23
- package/src/i18n/en.ts +65 -4
- package/src/i18n/zh-CN.ts +66 -4
- package/src/o11y/cost.test.ts +1 -0
- package/src/o11y/execution-tree.test.ts +1 -20
- package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
- package/src/o11y/otlp/parse.test.ts +1 -0
- package/src/o11y/otlp/turn-otel.test.ts +1 -0
- package/src/o11y/parsers/bub.test.ts +1 -0
- package/src/o11y/parsers/claude-code.test.ts +1 -34
- package/src/o11y/parsers/openclaw.test.ts +154 -0
- package/src/o11y/parsers/openclaw.ts +310 -0
- package/src/o11y/prices.json +746 -311
- package/src/o11y/tool-names.test.ts +1 -0
- package/src/o11y/types.ts +16 -2
- package/src/report/aggregate.ts +34 -5
- package/src/report/built-in-user-parity.test.tsx +127 -173
- package/src/report/built-ins/experiment-comparison.tsx +179 -0
- package/src/report/built-ins/index.ts +7 -2
- package/src/report/components.tsx +11 -3
- package/src/report/compute.ts +80 -74
- package/src/report/dual-render.test.tsx +222 -91
- package/src/report/flag.ts +30 -2
- package/src/report/format.ts +36 -27
- package/src/report/index.ts +23 -6
- package/src/report/locale.ts +49 -6
- package/src/report/metrics.ts +68 -15
- package/src/report/primitives.tsx +6 -0
- package/src/report/react/AttemptList.tsx +9 -36
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +68 -0
- package/src/report/react/ExperimentList.tsx +173 -55
- package/src/report/react/MetricScatter.tsx +13 -25
- package/src/report/react/chart-math.test.ts +85 -0
- package/src/report/react/chart-math.ts +101 -22
- package/src/report/react/enhance.js +72 -1
- package/src/report/react/fixtures.ts +34 -21
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +0 -1
- package/src/report/react/render.test.tsx +30 -69
- package/src/report/react/styles.css +112 -14
- package/src/report/report.test.ts +308 -105
- package/src/report/report.ts +6 -2
- package/src/report/text/faces.ts +111 -67
- package/src/report/text/table.ts +42 -5
- package/src/report/types.ts +42 -34
- package/src/results/annotated-source.test.ts +62 -9
- package/src/results/annotated-source.ts +64 -6
- package/src/results/attempt-evidence.test.ts +9 -7
- package/src/results/attempt-evidence.ts +15 -8
- package/src/results/attempt-source.ts +6 -3
- package/src/results/copy.ts +145 -55
- package/src/results/host-equivalence.test.ts +11 -9
- package/src/results/index.ts +2 -0
- package/src/results/locator.test.ts +1 -22
- package/src/results/open.ts +7 -1
- package/src/results/publish.ts +149 -0
- package/src/results/results.test.ts +85 -51
- package/src/results/truncate.ts +90 -0
- package/src/results/types.ts +7 -0
- package/src/results/writer.ts +31 -13
- package/src/runner/attempt.test.ts +138 -7
- package/src/runner/attempt.ts +603 -104
- package/src/runner/discover.test.ts +47 -0
- package/src/runner/discover.ts +36 -2
- package/src/runner/eval-source.test.ts +1 -27
- package/src/runner/feedback/agent.test.ts +504 -0
- package/src/runner/feedback/agent.ts +409 -0
- package/src/runner/feedback/ci.test.ts +562 -0
- package/src/runner/feedback/ci.ts +401 -0
- package/src/runner/feedback/coordinator.test.ts +317 -0
- package/src/runner/feedback/coordinator.ts +397 -0
- package/src/runner/feedback/failure.ts +40 -0
- package/src/runner/feedback/human.test.ts +616 -0
- package/src/runner/feedback/human.ts +535 -0
- package/src/runner/feedback/index.ts +66 -0
- package/src/runner/feedback/io.ts +78 -0
- package/src/runner/feedback/profile.test.ts +50 -0
- package/src/runner/feedback/profile.ts +58 -0
- package/src/runner/feedback/reducer.test.ts +395 -0
- package/src/runner/feedback/reducer.ts +260 -0
- package/src/runner/feedback/renderer.ts +82 -0
- package/src/runner/feedback/sink.ts +203 -0
- package/src/runner/feedback/testing.ts +106 -0
- package/src/runner/ledger.test.ts +230 -0
- package/src/runner/ledger.ts +329 -0
- package/src/runner/report.test.ts +128 -3
- package/src/runner/report.ts +33 -9
- package/src/runner/reporters/artifacts.ts +8 -2
- package/src/runner/reporters/braintrust.test.ts +8 -7
- package/src/runner/reporters/braintrust.ts +9 -2
- package/src/runner/reporters/index.ts +2 -2
- package/src/runner/reporters/json.test.ts +162 -0
- package/src/runner/reporters/json.ts +35 -8
- package/src/runner/reporters/shared.ts +1 -5
- package/src/runner/run.test.ts +760 -3
- package/src/runner/run.ts +242 -36
- package/src/runner/sandbox-prep.ts +3 -42
- package/src/runner/timing.ts +158 -0
- package/src/runner/types.ts +518 -22
- package/src/sandbox/checkpoint.test.ts +55 -0
- package/src/sandbox/checkpoint.ts +29 -8
- package/src/sandbox/cli-commands.ts +407 -0
- package/src/sandbox/docker.ts +115 -16
- package/src/sandbox/e2b-agent-template.test.ts +56 -0
- package/src/sandbox/e2b-agent-template.ts +94 -0
- package/src/sandbox/e2b.ts +74 -9
- package/src/sandbox/errors.ts +111 -4
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/io-retry.test.ts +58 -0
- package/src/sandbox/io-retry.ts +45 -0
- package/src/sandbox/keep-registry.test.ts +86 -0
- package/src/sandbox/keep-registry.ts +142 -0
- package/src/sandbox/keep.ts +178 -0
- package/src/sandbox/paths.test.ts +1 -0
- package/src/sandbox/paths.ts +19 -8
- package/src/sandbox/registry.ts +20 -3
- package/src/sandbox/resolve.ts +76 -11
- package/src/sandbox/retry.test.ts +70 -0
- package/src/sandbox/retry.ts +46 -4
- package/src/sandbox/types.ts +44 -6
- package/src/sandbox/vercel.ts +43 -20
- package/src/scoring/collector.ts +60 -17
- package/src/scoring/coverage.ts +95 -0
- package/src/scoring/diff.ts +81 -0
- package/src/scoring/display.test.ts +121 -0
- package/src/scoring/display.ts +133 -0
- package/src/scoring/evidence.test.ts +189 -0
- package/src/scoring/judge.test.ts +142 -0
- package/src/scoring/judge.ts +15 -18
- package/src/scoring/scoped.ts +217 -50
- package/src/scoring/types.ts +117 -20
- package/src/scoring/verdict.ts +16 -4
- package/src/shared/aggregate.ts +3 -2
- package/src/shared/types.ts +31 -0
- package/src/show/compose.ts +2 -2
- package/src/show/index.ts +29 -16
- package/src/show/render.ts +626 -308
- package/src/show/show.test.ts +251 -36
- package/src/tty-line.ts +8 -26
- package/src/util.test.ts +1 -0
- package/src/util.ts +41 -0
- package/src/view/app/components/AttemptModal.tsx +153 -2
- package/src/view/app/components/CodeView.tsx +32 -11
- package/src/view/app/components/CopyControls.tsx +2 -2
- package/src/view/app/i18n.ts +6 -0
- package/src/view/app/lib/attempt-route.test.ts +1 -0
- package/src/view/app/lib/verdict.ts +7 -9
- package/src/view/artifact-serving.test.ts +2 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +17 -17
- package/src/view/data.test.ts +2 -1
- package/src/view/data.ts +17 -7
- package/src/view/index.ts +12 -1
- package/src/view/server.ts +2 -0
- package/src/view/shared/types.ts +1 -1
- package/src/view/styles.css +3 -0
- package/src/view/view-report.test.ts +11 -10
- package/dist/o11y/execution-tree.d.ts +0 -103
- package/dist/o11y/otlp/select.d.ts +0 -22
- package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +0 -1
- package/dist/report/built-ins/cost-pass-rate-comparison.js +0 -17
- package/dist/results/annotated-source.d.ts +0 -61
- package/dist/results/attempt-evidence.d.ts +0 -69
- package/dist/results/attempt-source.d.ts +0 -15
- package/src/report/built-ins/cost-pass-rate-comparison.tsx +0 -23
- package/src/runner/reporters/console.ts +0 -70
- package/src/runner/reporters/live.test.ts +0 -56
- package/src/runner/reporters/live.ts +0 -247
- package/src/runner/reporters/quiet.test.ts +0 -66
- package/src/runner/reporters/quiet.ts +0 -49
- package/src/runner/reporters/table.ts +0 -277
package/src/runner/attempt.ts
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
import { resolve as resolvePath } from "node:path";
|
|
7
7
|
import { readFile as readSourceFile } from "node:fs/promises";
|
|
8
8
|
import { Effect, Cause, Duration } from "effect";
|
|
9
|
-
import { createSandbox,
|
|
9
|
+
import { createSandbox, resolveSandbox, sandboxRunInfo } from "../sandbox/resolve.ts";
|
|
10
|
+
import { stopSandbox, unregisterSandbox } from "../sandbox/registry.ts";
|
|
11
|
+
import { KEEPABLE_PROVIDERS, nativeEnterCommand, suspendSandbox } from "../sandbox/keep.ts";
|
|
12
|
+
import { keptEntryId, updateKeptEntry, writeKeptEntry } from "../sandbox/keep-registry.ts";
|
|
10
13
|
import { createTraceReceiver, type TraceReceiver } from "../o11y/otlp/receiver.ts";
|
|
11
14
|
import { createInSandboxTraceReceiver } from "../o11y/otlp/sandbox-receiver.ts";
|
|
12
15
|
import type { AgentOtelChannel } from "../o11y/otlp/turn-otel.ts";
|
|
@@ -20,20 +23,24 @@ import { computeVerdict } from "../scoring/verdict.ts";
|
|
|
20
23
|
import { deriveRunFacts, buildO11ySummary } from "../o11y/derive.ts";
|
|
21
24
|
import { estimateCost } from "../o11y/cost.ts";
|
|
22
25
|
import { t } from "../i18n/index.ts";
|
|
23
|
-
import { formatThrown } from "../util.ts";
|
|
24
|
-
import {
|
|
26
|
+
import { describeError, firstLine, formatThrown } from "../util.ts";
|
|
27
|
+
import { createChangeLedger, type ChangeLedger } from "./ledger.ts";
|
|
28
|
+
import { deriveDiffData, emptyDiffData } from "../scoring/diff.ts";
|
|
25
29
|
import { createRemoteSandbox, withEvalLocalPaths } from "./remote-sandbox.ts";
|
|
26
30
|
import type { CapturedEvalSource } from "./eval-source.ts";
|
|
27
31
|
import type {
|
|
28
32
|
AgentContext,
|
|
29
|
-
AgentSetup,
|
|
30
33
|
AgentSetupManifest,
|
|
31
|
-
AgentTeardown,
|
|
32
34
|
Cleanup,
|
|
33
35
|
Config,
|
|
36
|
+
DiagnosticInput,
|
|
37
|
+
DiffArtifact,
|
|
34
38
|
EvalResult,
|
|
35
39
|
JudgeConfig,
|
|
36
40
|
Sandbox,
|
|
41
|
+
SandboxHook,
|
|
42
|
+
SandboxHookContext,
|
|
43
|
+
ScopedFeedback,
|
|
37
44
|
ScoringContext,
|
|
38
45
|
ScriptResult,
|
|
39
46
|
SourceArtifact,
|
|
@@ -41,17 +48,33 @@ import type {
|
|
|
41
48
|
Telemetry,
|
|
42
49
|
TraceSpan,
|
|
43
50
|
} from "../types.ts";
|
|
44
|
-
import {
|
|
45
|
-
import
|
|
51
|
+
import { reportAttemptLifecycle, reportDiagnostic, reportKept } from "./feedback/sink.ts";
|
|
52
|
+
import { encodeAttemptKey, runWho } from "./types.ts";
|
|
53
|
+
import { commandDisplay, commandNode, createTimingRecorder, type TimingRecorder } from "./timing.ts";
|
|
54
|
+
import type {
|
|
55
|
+
AgentRun,
|
|
56
|
+
Attempt,
|
|
57
|
+
AttemptError,
|
|
58
|
+
AttemptRef,
|
|
59
|
+
DiagnosticRecord,
|
|
60
|
+
LifecyclePhase,
|
|
61
|
+
RunOptions,
|
|
62
|
+
} from "./types.ts";
|
|
46
63
|
|
|
47
64
|
export function runAttemptEffect(
|
|
48
65
|
a: Attempt,
|
|
49
66
|
opts: RunOptions,
|
|
50
67
|
sandboxSem: Effect.Semaphore,
|
|
51
68
|
parentSignal?: AbortSignal,
|
|
69
|
+
/** 每次跨入一个新 `LifecyclePhase` 边界时同步回调一次(与下面的 `enterPhase` 同一调用点,见
|
|
70
|
+
* 该函数)。run.ts 用它在本地跟踪「这个 attempt 目前所在的阶段」,好在 attempt 失败/errored
|
|
71
|
+
* 时把 phase 塞进 `reportFailure()`(见 sink.ts 的 `FailureInput.phase`)—— 到那时
|
|
72
|
+
* attempt:complete 已经让 coordinator 把 active map 里的条目删掉,没有别的地方能事后查到。 */
|
|
73
|
+
onPhase?: (phase: LifecyclePhase) => void,
|
|
52
74
|
): Effect.Effect<EvalResult> {
|
|
53
75
|
const config = opts.config;
|
|
54
76
|
const { evalDef, run, attempt } = a;
|
|
77
|
+
const niceevalRoot = opts.niceevalRoot ?? `${process.cwd()}/.niceeval`;
|
|
55
78
|
const t0 = Date.now();
|
|
56
79
|
|
|
57
80
|
const base: EvalResult = {
|
|
@@ -76,19 +99,81 @@ export function runAttemptEffect(
|
|
|
76
99
|
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
77
100
|
const signal = parentSignal ? AbortSignal.any([parentSignal, timeoutSignal]) : timeoutSignal;
|
|
78
101
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
102
|
+
// Attempt 阶段的正式生命周期投影(见 docs/feature/experiments/cli.md「Attempt 阶段」)。
|
|
103
|
+
// run.ts 在这个 attempt 的 body Effect 真正开始跑之前,已经先发出过一次 attempt:start(占位
|
|
104
|
+
// phase,见 run.ts 的 attempt:start emission,和这里的 eval:start 是同一个调用点),所以这里
|
|
105
|
+
// 只需要在每个「实际执行到的」边界调 enterPhase() 覆盖上一个 phase(attempt:phase),不需要
|
|
106
|
+
// 自己区分「第一次」。没有对应 hook/配置的步骤直接不调用,不产生空阶段(如没有 setup 的 agent
|
|
107
|
+
// 跳过 agent-setup)。没有活跃 feedback coordinator 时 reportAttemptLifecycle 静默 no-op,
|
|
108
|
+
// 不产生任何终端输出。
|
|
109
|
+
const identity: AttemptRef = { experimentId: run.experimentId, evalId: evalDef.id, attempt };
|
|
110
|
+
// 最近跨入的正式 phase:errored 结果的 `error.phase` 从它取(见下方 timeout / scope
|
|
111
|
+
// 兜底与 runAttemptBody 的 body catch)。body 与本函数共用同一个 enterPhase 闭包(经 res 传下去),
|
|
112
|
+
// 所以 body 内部的阶段推进也会更新它,不需要 body 再单独维护一份。
|
|
113
|
+
// 阶段计时:live 展示、error.phase、落盘 phases[].name 用同一套 LifecyclePhase 闭集,
|
|
114
|
+
// 一次 enterPhase 同时推进三者(词表全仓只有一套,见 runner/types.ts 的 LifecyclePhase)。
|
|
115
|
+
let lastPhase: LifecyclePhase | undefined;
|
|
116
|
+
const recorder = createTimingRecorder(() => Date.now());
|
|
117
|
+
// adapter send 在飞时,错误/诊断归因到嵌套的 `agent.run`(eval.run 内打开,不单列计时条目)。
|
|
118
|
+
let sendActive = false;
|
|
119
|
+
const enterPhase = (phase: LifecyclePhase) => {
|
|
120
|
+
lastPhase = phase;
|
|
121
|
+
recorder.enter(phase);
|
|
122
|
+
onPhase?.(phase);
|
|
123
|
+
reportAttemptLifecycle({ type: "attempt:phase", at: Date.now(), identity, phase });
|
|
124
|
+
};
|
|
125
|
+
// 本 attempt 累计的诊断(与 verdict 独立):ScopedFeedback.diagnostic 与 teardown 失败都落这里,
|
|
126
|
+
// 收尾时并入结果;dedupeKey 相同的并发诊断折叠成一条并累计 count。
|
|
127
|
+
const diagnostics: DiagnosticRecord[] = [];
|
|
128
|
+
const dedupeIndex = new Map<string, DiagnosticRecord>();
|
|
129
|
+
const recordDiagnostic = (input: DiagnosticInput) => {
|
|
130
|
+
const phase = (sendActive ? "agent.run" : lastPhase) ?? "eval.run";
|
|
131
|
+
if (input.dedupeKey !== undefined) {
|
|
132
|
+
const existing = dedupeIndex.get(input.dedupeKey);
|
|
133
|
+
if (existing) {
|
|
134
|
+
existing.count = (existing.count ?? 1) + 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const record: DiagnosticRecord = {
|
|
139
|
+
code: input.code,
|
|
140
|
+
level: input.level,
|
|
141
|
+
message: input.message,
|
|
142
|
+
phase,
|
|
143
|
+
...(input.data !== undefined ? { data: input.data } : {}),
|
|
144
|
+
};
|
|
145
|
+
if (input.dedupeKey !== undefined) dedupeIndex.set(input.dedupeKey, record);
|
|
146
|
+
diagnostics.push(record);
|
|
147
|
+
// 同时进运行级永久事件流(human 撤下 dashboard 后追加、agent/ci 各追加一条,去重按 key)。
|
|
148
|
+
reportDiagnostic({
|
|
149
|
+
key: input.dedupeKey ?? `${input.code}:${encodeAttemptKey(identity)}`,
|
|
150
|
+
severity: input.level,
|
|
151
|
+
message: input.message,
|
|
152
|
+
identity,
|
|
153
|
+
data: input.data,
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
// 作用域反馈:progress 走 attempt:progress(短命状态,归因由 runner 的当前阶段决定),
|
|
157
|
+
// diagnostic 落 attempt diagnostics + 运行级永久事件。绑定见 docs/feature/experiments/library.md。
|
|
158
|
+
const scopedFeedback: ScopedFeedback = {
|
|
159
|
+
progress: (u) => {
|
|
160
|
+
const suffix = u.current !== undefined && u.total !== undefined ? ` (${u.current}/${u.total})` : "";
|
|
161
|
+
log(`${u.message}${suffix}`);
|
|
162
|
+
},
|
|
163
|
+
diagnostic: recordDiagnostic,
|
|
164
|
+
};
|
|
165
|
+
|
|
82
166
|
// 同时保留最近 20 条进度消息,timeout 时嵌入 error 字段方便定位卡在哪一步。
|
|
83
167
|
const recentLogs: string[] = [];
|
|
84
168
|
const log = (m: string) => {
|
|
85
169
|
recentLogs.push(m);
|
|
86
170
|
if (recentLogs.length > 20) recentLogs.shift();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
171
|
+
// 附着在「当前阶段」上的次要文本(见 ActiveAttempt.detail);attempt:start 早于本函数任何
|
|
172
|
+
// 调用点发出(见上),active map 里一定已经有这个 identity 的条目。这是 log() 唯一的出口 ——
|
|
173
|
+
// 没有裸写 stderr 的兜底分支(那是给已删除的 Live reporter 用的旧接线,见
|
|
174
|
+
// docs/feature/experiments/cli.md「一个 run 内只有一个终端协调者」);由当前活跃的 profile
|
|
175
|
+
// renderer(human/agent/ci)决定这条 detail 要不要、怎么展示。
|
|
176
|
+
reportAttemptLifecycle({ type: "attempt:progress", at: Date.now(), identity, detail: m });
|
|
92
177
|
};
|
|
93
178
|
|
|
94
179
|
return Effect.scoped(
|
|
@@ -96,27 +181,87 @@ export function runAttemptEffect(
|
|
|
96
181
|
// run.sandbox ?? config.sandbox 是同一个 SandboxSpec 对象,既用来起沙箱 provider,
|
|
97
182
|
// 也是 sandbox.setup / sandbox.teardown 钩子(SandboxSpec.setup()/.teardown() 链式挂的)的来源。
|
|
98
183
|
const sandboxSpec = run.sandbox ?? config.sandbox;
|
|
184
|
+
// defineSandbox 自定义 provider 不参与留存(事后命令不执行用户项目代码,新进程无法安全
|
|
185
|
+
// 找回用户对象上的 stopDetached);组合使用在创建沙箱前报清晰错误。
|
|
186
|
+
if (
|
|
187
|
+
run.agent.kind === "sandbox" &&
|
|
188
|
+
opts.keepSandbox !== undefined &&
|
|
189
|
+
resolveSandbox(sandboxSpec).create !== undefined
|
|
190
|
+
) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`--keep-sandbox is not supported with a defineSandbox custom provider ("${resolveSandbox(sandboxSpec).provider}"): the after-the-fact 'niceeval sandbox' commands never load project code, so a detached stop for user-defined sandboxes cannot be recovered safely. Use a built-in provider (docker / e2b / vercel), or drop --keep-sandbox.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
// 留存 disposition:只在本 attempt 内可变,初始 stop;只有留存提交成功才改成 keep
|
|
196
|
+
// (Ctrl+C 中断外层 Scope 时仍是 stop,照常清理)。
|
|
197
|
+
let disposition: "stop" | "keep" = "stop";
|
|
99
198
|
// 退避重试(resolve.ts → retry.ts)期间临时归还这个名额:被限流的 provider 只是在
|
|
100
199
|
// setTimeout 里睡觉,不该攥着 sandboxSem 的槽位陪跑,不然一批 429 能把整体并发拖成个位数。
|
|
101
200
|
const provisionSlot = {
|
|
102
201
|
release: () => Effect.runPromise(sandboxSem.release(1)).then(() => {}),
|
|
103
202
|
reacquire: () => Effect.runPromise(sandboxSem.take(1)).then(() => {}),
|
|
104
203
|
};
|
|
204
|
+
// Scope release(receiver close + provider stop)整段计成 sandbox.stop:先加的 finalizer
|
|
205
|
+
// 后跑(LIFO),所以「先加的」在 release 链末尾打终点戳、「后加的」在 release 开始前打起点戳;
|
|
206
|
+
// 结果封口(附 phases)发生在 Scope release 完成之后(见下方 Effect.map)。
|
|
207
|
+
let releaseStartedAt = 0;
|
|
208
|
+
if (run.agent.kind === "sandbox") {
|
|
209
|
+
yield* Effect.addFinalizer(() =>
|
|
210
|
+
Effect.sync(() => {
|
|
211
|
+
// 留存路径的 phases 以 sandbox.suspend 结尾,没有 sandbox.stop 条目(见 release)。
|
|
212
|
+
if (releaseStartedAt > 0 && disposition !== "keep") {
|
|
213
|
+
recorder.record("sandbox.stop", Date.now() - releaseStartedAt);
|
|
214
|
+
}
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
105
218
|
const sandbox =
|
|
106
219
|
run.agent.kind === "sandbox"
|
|
107
|
-
? yield*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
sandbox
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
220
|
+
? yield* Effect.gen(function* () {
|
|
221
|
+
// ── 沙箱:acquire=起,release=stop(成功 / 失败 / 中断都跑)──
|
|
222
|
+
// sandboxSem 只覆盖「容器创建」阶段;容器起好后立即释放,后续 npm install / agent 不占位。
|
|
223
|
+
enterPhase("sandbox.queue");
|
|
224
|
+
return yield* sandboxSem.withPermits(1)(
|
|
225
|
+
Effect.gen(function* () {
|
|
226
|
+
enterPhase("sandbox.create");
|
|
227
|
+
log(t("runner.startSandbox"));
|
|
228
|
+
return yield* createSandbox({
|
|
229
|
+
sandbox: sandboxSpec,
|
|
230
|
+
provisionSlot,
|
|
231
|
+
timeout: timeoutMs,
|
|
232
|
+
runtime: "node24",
|
|
233
|
+
feedback: scopedFeedback,
|
|
234
|
+
// Scope release 按 disposition 收尾:stop = 销毁(默认);keep = provider
|
|
235
|
+
// suspend(sandbox.suspend 阶段,有界计时),成功把登记项转 dormant,
|
|
236
|
+
// 失败保持 alive 并追加 diagnostic——不销毁、不冒充 dormant。
|
|
237
|
+
release: async (sb) => {
|
|
238
|
+
if (disposition !== "keep") {
|
|
239
|
+
await stopSandbox(sb);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
unregisterSandbox(sb);
|
|
243
|
+
const providerName = resolveSandbox(sandboxSpec).provider;
|
|
244
|
+
const suspendStart = Date.now();
|
|
245
|
+
try {
|
|
246
|
+
await suspendSandbox(sb);
|
|
247
|
+
recorder.record("sandbox.suspend", Date.now() - suspendStart);
|
|
248
|
+
await updateKeptEntry(niceevalRoot, keptEntryId(providerName, sb.sandboxId), {
|
|
249
|
+
state: "dormant",
|
|
250
|
+
}).catch(() => false);
|
|
251
|
+
} catch (e) {
|
|
252
|
+
recorder.record("sandbox.suspend", Date.now() - suspendStart, true);
|
|
253
|
+
recordDiagnostic({
|
|
254
|
+
code: "sandbox-suspend-failed",
|
|
255
|
+
level: "warning",
|
|
256
|
+
message: `sandbox ${sb.sandboxId} kept but suspend failed; the instance is still running: ${e instanceof Error ? e.message : String(e)}`,
|
|
257
|
+
dedupeKey: `sandbox-suspend-failed:${sb.sandboxId}`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
})
|
|
120
265
|
: createRemoteSandbox();
|
|
121
266
|
if (run.agent.kind !== "sandbox") log(t("runner.useRemoteAgent"));
|
|
122
267
|
|
|
@@ -170,10 +315,19 @@ export function runAttemptEffect(
|
|
|
170
315
|
}
|
|
171
316
|
}
|
|
172
317
|
|
|
318
|
+
if (run.agent.kind === "sandbox") {
|
|
319
|
+
// 后加先跑:release 链开始时打起点戳(与上面的终点戳配对,测出整段 sandbox.stop)。
|
|
320
|
+
yield* Effect.addFinalizer(() =>
|
|
321
|
+
Effect.sync(() => {
|
|
322
|
+
releaseStartedAt = Date.now();
|
|
323
|
+
}),
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
173
327
|
// body 是 Promise(adapter 边界)。Effect.promise 给的 AbortSignal 在本 fiber 被中断
|
|
174
328
|
//(用户 Ctrl+C / 下面 timeoutTo 到点)时 abort —— 并进 signal,让真正观察 signal 的
|
|
175
329
|
// adapter / docker 命令随中断一起停,而不只靠 Scope release 兜底。
|
|
176
|
-
|
|
330
|
+
const bodyResult = yield* Effect.promise((interruptSignal) =>
|
|
177
331
|
runAttemptBody(a, config, t0, base, {
|
|
178
332
|
sandbox,
|
|
179
333
|
sandboxSetupHooks: sandboxSpec?.setupHooks ?? [],
|
|
@@ -183,8 +337,73 @@ export function runAttemptEffect(
|
|
|
183
337
|
otel: otelChannel,
|
|
184
338
|
signal: AbortSignal.any([signal, interruptSignal]),
|
|
185
339
|
log,
|
|
340
|
+
enterPhase,
|
|
341
|
+
// send 在飞时归因到嵌套的 agent.run(不切换顶层阶段,见 LifecyclePhase 注释)。
|
|
342
|
+
getPhase: () => (sendActive ? "agent.run" : lastPhase),
|
|
343
|
+
setSendActive: (active) => {
|
|
344
|
+
sendActive = active;
|
|
345
|
+
},
|
|
346
|
+
recorder,
|
|
347
|
+
attemptEpoch: t0,
|
|
348
|
+
feedback: scopedFeedback,
|
|
349
|
+
diagnostics,
|
|
186
350
|
}),
|
|
187
351
|
);
|
|
352
|
+
|
|
353
|
+
// 留存提交:verdict 定稿、其余收尾(teardown 链、diff 采集)已在 body 内完成后,按档位
|
|
354
|
+
// 提交——failed 档留 failed/errored,all 档全部;顺序不可调换:先原子写登记项,写入成功
|
|
355
|
+
// 才把 disposition 改成 keep;写入失败保持 stop、记 diagnostic,`sandbox.kept` 不得为 true。
|
|
356
|
+
const keepMode = opts.keepSandbox;
|
|
357
|
+
if (
|
|
358
|
+
run.agent.kind === "sandbox" &&
|
|
359
|
+
keepMode !== undefined &&
|
|
360
|
+
a.locator !== undefined &&
|
|
361
|
+
(keepMode === "all" || bodyResult.verdict === "failed" || bodyResult.verdict === "errored")
|
|
362
|
+
) {
|
|
363
|
+
const providerName = resolveSandbox(sandboxSpec).provider;
|
|
364
|
+
if (KEEPABLE_PROVIDERS.has(providerName)) {
|
|
365
|
+
try {
|
|
366
|
+
const enter = nativeEnterCommand(providerName, sandbox.sandboxId);
|
|
367
|
+
yield* Effect.promise(() =>
|
|
368
|
+
writeKeptEntry(niceevalRoot, {
|
|
369
|
+
sandboxId: sandbox.sandboxId,
|
|
370
|
+
provider: providerName,
|
|
371
|
+
evalId: evalDef.id,
|
|
372
|
+
attempt,
|
|
373
|
+
...(run.experimentId !== undefined ? { experimentId: run.experimentId } : {}),
|
|
374
|
+
locator: String(a.locator),
|
|
375
|
+
verdict: bodyResult.verdict,
|
|
376
|
+
keptAt: new Date().toISOString(),
|
|
377
|
+
workdir: sandbox.workdir,
|
|
378
|
+
...(enter !== undefined ? { enter } : {}),
|
|
379
|
+
state: "alive",
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
disposition = "keep";
|
|
383
|
+
reportKept({
|
|
384
|
+
locator: a.locator,
|
|
385
|
+
identity,
|
|
386
|
+
who: runWho({ agentName: run.agent.name, model: run.model, experimentId: run.experimentId }),
|
|
387
|
+
verdict: bodyResult.verdict,
|
|
388
|
+
provider: providerName,
|
|
389
|
+
sandboxId: sandbox.sandboxId,
|
|
390
|
+
...(enter !== undefined ? { enter } : {}),
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
...bodyResult,
|
|
394
|
+
sandbox: { provider: providerName, sandboxId: sandbox.sandboxId, kept: true as const },
|
|
395
|
+
};
|
|
396
|
+
} catch (e) {
|
|
397
|
+
recordDiagnostic({
|
|
398
|
+
code: "sandbox-keep-failed",
|
|
399
|
+
level: "warning",
|
|
400
|
+
message: `failed to register kept sandbox ${sandbox.sandboxId}; it will be destroyed normally: ${e instanceof Error ? e.message : String(e)}`,
|
|
401
|
+
dedupeKey: `sandbox-keep-failed:${sandbox.sandboxId}`,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return bodyResult;
|
|
188
407
|
}),
|
|
189
408
|
).pipe(
|
|
190
409
|
// ── attempt 总超时的硬边界(P1)──
|
|
@@ -194,14 +413,21 @@ export function runAttemptEffect(
|
|
|
194
413
|
Effect.timeoutTo({
|
|
195
414
|
duration: Duration.millis(timeoutMs),
|
|
196
415
|
onSuccess: (r: EvalResult) => r,
|
|
197
|
-
onTimeout: (): EvalResult =>
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
416
|
+
onTimeout: (): EvalResult => {
|
|
417
|
+
// 超时:message 是一层原因(首行),recentLogs 明细放进 stack 供 show 展开「卡在哪一步」;
|
|
418
|
+
// operation 取超时那一刻打开的 lifecycle operation。code 稳定为 "timeout"。
|
|
419
|
+
const text = t("runner.timeout", { timeoutMs, recentLogs: recentLogs.map((l) => ` · ${l}`).join("\n") });
|
|
420
|
+
const message = firstLine(text);
|
|
421
|
+
const rest = text.length > message.length ? text.slice(message.length + 1).replace(/\n+$/, "") : "";
|
|
422
|
+
const error: AttemptError = {
|
|
423
|
+
code: "timeout",
|
|
424
|
+
message,
|
|
425
|
+
phase: (sendActive ? "agent.run" : lastPhase) ?? "eval.run",
|
|
426
|
+
...(rest.trim() !== "" ? { stack: rest } : {}),
|
|
427
|
+
};
|
|
428
|
+
recorder.failCurrent();
|
|
429
|
+
return { ...base, durationMs: Date.now() - t0, error };
|
|
430
|
+
},
|
|
205
431
|
}),
|
|
206
432
|
// body 自己已兜了 agent 执行错;这里兜的是资源获取 / Scope 层的意外(起沙箱失败等)。
|
|
207
433
|
// 中断【不】吞:此时 Scope 已跑完 release(容器已停),把中断继续上抛,让 forEach 整体停掉,
|
|
@@ -209,27 +435,63 @@ export function runAttemptEffect(
|
|
|
209
435
|
Effect.catchAllCause((cause) =>
|
|
210
436
|
Cause.isInterrupted(cause)
|
|
211
437
|
? Effect.failCause(cause)
|
|
212
|
-
: Effect.succeed({
|
|
438
|
+
: Effect.succeed({
|
|
439
|
+
...base,
|
|
440
|
+
durationMs: Date.now() - t0,
|
|
441
|
+
error: errorFromThrown(Cause.squash(cause), sendActive ? "agent.run" : lastPhase),
|
|
442
|
+
}),
|
|
213
443
|
),
|
|
444
|
+
// 结果封口在 Scope release 完成之后:sandbox.stop 已由 finalizer 写进 recorder,
|
|
445
|
+
// 这里把完整的阶段计时挂到即将交还的结果上(timeout / scope 兜底分支同样带上)。
|
|
446
|
+
Effect.map((r: EvalResult): EvalResult => {
|
|
447
|
+
const phases = recorder.finalize();
|
|
448
|
+
return phases ? { ...r, phases } : r;
|
|
449
|
+
}),
|
|
214
450
|
);
|
|
215
451
|
}
|
|
216
452
|
|
|
217
|
-
|
|
218
|
-
|
|
453
|
+
/** 把 catch 到的 e(body 里 test()/setup 抛错,或 Scope 层 squash 出来的原始错误)折成
|
|
454
|
+
* `AttemptError`。message/stack/cause 由 `describeError` 拆分;phase 取失败那一刻打开的
|
|
455
|
+
* 生命周期阶段(极早期就挂、还没跨进任何阶段时兜底 `eval.run`——phase 是必填字段,不留空);
|
|
456
|
+
* code 目前只对确定已知的类别赋稳定码,其余走 `"unexpected-error"`——provider 专属的限流码
|
|
457
|
+
* 分类留在各 provider 的 `classifyProvisionError`,没有中性入口能在这里复算,不猜一个可能错的码。 */
|
|
458
|
+
function errorFromThrown(e: unknown, phase: LifecyclePhase | undefined): AttemptError {
|
|
459
|
+
const { message, stack, cause } = describeError(e);
|
|
460
|
+
return {
|
|
461
|
+
code: "unexpected-error",
|
|
462
|
+
message,
|
|
463
|
+
phase: phase ?? "eval.run",
|
|
464
|
+
...(stack ? { stack } : {}),
|
|
465
|
+
...(cause ? { cause } : {}),
|
|
466
|
+
};
|
|
219
467
|
}
|
|
220
468
|
|
|
221
469
|
interface AttemptResources {
|
|
222
470
|
sandbox: Sandbox;
|
|
223
471
|
/** SandboxSpec.setup() 链式挂的钩子,按追加顺序;非沙箱 agent 传空数组(usesSandbox 挡住不会跑)。 */
|
|
224
|
-
sandboxSetupHooks: readonly
|
|
472
|
+
sandboxSetupHooks: readonly SandboxHook[];
|
|
225
473
|
/** SandboxSpec.teardown() 链式挂的钩子,按追加顺序保存,执行时逆序。 */
|
|
226
|
-
sandboxTeardownHooks: readonly
|
|
474
|
+
sandboxTeardownHooks: readonly SandboxHook[];
|
|
227
475
|
receiver?: TraceReceiver;
|
|
228
476
|
telemetry?: Telemetry;
|
|
229
477
|
/** 非沙箱 tracing agent 的共享 OTLP 通道(run 级池持有,不随 attempt 关)。 */
|
|
230
478
|
otel?: AgentOtelChannel;
|
|
231
479
|
signal: AbortSignal;
|
|
232
480
|
log: (m: string) => void;
|
|
481
|
+
/** 进入一个正式 LifecyclePhase 边界(见 runAttemptEffect 顶部的定义)。 */
|
|
482
|
+
enterPhase: (phase: LifecyclePhase) => void;
|
|
483
|
+
/** 读当前最近跨入的 phase(send 在飞时返回嵌套的 `agent.run`):error/diagnostic 归因用。 */
|
|
484
|
+
getPhase: () => LifecyclePhase | undefined;
|
|
485
|
+
/** SessionManager 的 send 在飞通知落点(agent.run 归因)。 */
|
|
486
|
+
setSendActive: (active: boolean) => void;
|
|
487
|
+
/** 阶段计时 recorder(turn/command 时间树挂载点)。 */
|
|
488
|
+
recorder: TimingRecorder;
|
|
489
|
+
/** attempt 墙钟起点(turn 节点的 startOffsetMs 基准)。 */
|
|
490
|
+
attemptEpoch: number;
|
|
491
|
+
/** 作用域反馈句柄(归因随 runner 当前阶段);各生命周期入口共享同一实现。 */
|
|
492
|
+
feedback: ScopedFeedback;
|
|
493
|
+
/** attempt 级诊断累计(runAttemptEffect 持有,含 sandbox.create 期间的诊断)。 */
|
|
494
|
+
diagnostics: DiagnosticRecord[];
|
|
233
495
|
}
|
|
234
496
|
|
|
235
497
|
// attempt 的固定段(上传→基线→setup→驱动 agent→采 diff→脚本→评分→判定)。
|
|
@@ -242,8 +504,29 @@ async function runAttemptBody(
|
|
|
242
504
|
res: AttemptResources,
|
|
243
505
|
): Promise<EvalResult> {
|
|
244
506
|
const { evalDef, run, attempt } = a;
|
|
245
|
-
const {
|
|
507
|
+
const {
|
|
508
|
+
sandbox: rawSandbox,
|
|
509
|
+
sandboxSetupHooks,
|
|
510
|
+
sandboxTeardownHooks,
|
|
511
|
+
receiver,
|
|
512
|
+
telemetry,
|
|
513
|
+
otel,
|
|
514
|
+
signal,
|
|
515
|
+
log,
|
|
516
|
+
enterPhase,
|
|
517
|
+
getPhase,
|
|
518
|
+
setSendActive,
|
|
519
|
+
recorder,
|
|
520
|
+
attemptEpoch,
|
|
521
|
+
feedback,
|
|
522
|
+
diagnostics,
|
|
523
|
+
} = res;
|
|
246
524
|
const usesSandbox = run.agent.kind === "sandbox";
|
|
525
|
+
// 命令时间树:所有经这个包装 sandbox 发出的 runCommand/runShell 都挂成当前阶段(或当前 hook
|
|
526
|
+
// 节点)下的 command 子节点。包装只在最外层公开调用记录一次——provider 内部转调不经过它。
|
|
527
|
+
const sandbox = usesSandbox ? withCommandTiming(rawSandbox, recorder) : rawSandbox;
|
|
528
|
+
// 在两个 return 前赋值,好让 finally 把 diagnostics 挂到即将返回的同一个对象上(见 finally 末尾)。
|
|
529
|
+
let result: EvalResult | undefined;
|
|
247
530
|
// 整个 attempt 共用一份 agent ctx(sandbox 钩子 / agent setup / tracing configure / teardown 都用它)。
|
|
248
531
|
const attemptCtx: AgentContext = {
|
|
249
532
|
signal,
|
|
@@ -254,14 +537,27 @@ async function runAttemptBody(
|
|
|
254
537
|
sandbox,
|
|
255
538
|
session: createAgentSession(),
|
|
256
539
|
telemetry,
|
|
540
|
+
progress: feedback.progress,
|
|
541
|
+
diagnostic: feedback.diagnostic,
|
|
542
|
+
// log 是 progress({ message }) 的别名,不是第二条通道(见 AgentContext.log 注释)。
|
|
257
543
|
log,
|
|
258
544
|
};
|
|
545
|
+
// Sandbox hook / eval.setup 的窄上下文:experimentId + signal + 作用域反馈,不借用完整 AgentContext
|
|
546
|
+
// (hook 拿不到 session / model / telemetry,见 docs/feature/sandbox/library.md)。
|
|
547
|
+
const hookCtx: SandboxHookContext = {
|
|
548
|
+
experimentId: run.experimentId,
|
|
549
|
+
signal,
|
|
550
|
+
progress: feedback.progress,
|
|
551
|
+
diagnostic: feedback.diagnostic,
|
|
552
|
+
};
|
|
259
553
|
let agentCleanup: Cleanup | void = undefined;
|
|
260
554
|
let agentDidSetup = false;
|
|
261
555
|
/** agent.setup 写进沙箱的安装清单(装了 Skill / plugin / MCP 的沙箱型 adapter 才有)。 */
|
|
262
556
|
let agentSetup: AgentSetupManifest | undefined;
|
|
263
557
|
// EvalDef.setup() 返回的 cleanup 闭包;finally 里按 LIFO 跑(见下)。
|
|
264
558
|
let evalCleanup: Cleanup | void = undefined;
|
|
559
|
+
// 变更分类账(仅沙箱型;workspace.baseline 阶段建立)。
|
|
560
|
+
let ledger: ChangeLedger | undefined;
|
|
265
561
|
// SandboxSpec.setup() 返回的 cleanup 闭包,按调用顺序收集;finally 里 LIFO 跑(见下)。
|
|
266
562
|
const sandboxCleanups: Cleanup[] = [];
|
|
267
563
|
try {
|
|
@@ -270,24 +566,50 @@ async function runAttemptBody(
|
|
|
270
566
|
// eval.setup 跑——改动进 git 基线,不会被误算进 agent 产出的 diff。按追加顺序依次执行;
|
|
271
567
|
// 单个抛错走下面的执行错误路径(与 eval.setup / agent.setup 同一条),已跑过的 cleanup /
|
|
272
568
|
// sandbox.teardown 钩子仍在 finally 里跑(见 catch/finally)。
|
|
273
|
-
if (sandboxSetupHooks.length > 0)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
569
|
+
if (sandboxSetupHooks.length > 0) {
|
|
570
|
+
enterPhase("sandbox.setup");
|
|
571
|
+
log(t("runner.startSandboxSetup"));
|
|
572
|
+
}
|
|
573
|
+
for (const [i, hook] of sandboxSetupHooks.entries()) {
|
|
574
|
+
// hook 先建节点,hook 内经 Sandbox.runCommand/runShell 发出的命令挂成它的 command 子节点。
|
|
575
|
+
const hookStart = Date.now();
|
|
576
|
+
const hookNode = recorder.child({
|
|
577
|
+
kind: "hook",
|
|
578
|
+
label: `setup#${i}`,
|
|
579
|
+
startOffsetMs: Math.max(0, hookStart - attemptEpoch),
|
|
580
|
+
durationMs: 0,
|
|
581
|
+
});
|
|
582
|
+
if (hookNode) recorder.pushParent(hookNode);
|
|
583
|
+
try {
|
|
584
|
+
const cleanup = await hook(sandbox, hookCtx);
|
|
585
|
+
if (typeof cleanup === "function") sandboxCleanups.push(cleanup);
|
|
586
|
+
} catch (e) {
|
|
587
|
+
if (hookNode) hookNode.failed = true;
|
|
588
|
+
throw e;
|
|
589
|
+
} finally {
|
|
590
|
+
if (hookNode) {
|
|
591
|
+
hookNode.durationMs = Date.now() - hookStart;
|
|
592
|
+
recorder.popParent();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
277
595
|
}
|
|
278
596
|
|
|
279
|
-
|
|
597
|
+
// 变更分类账锚点:私有 git ledger(git 目录在 workdir 外),排除清单在此冻结。
|
|
598
|
+
enterPhase("workspace.baseline");
|
|
599
|
+
ledger = await createChangeLedger(sandbox, evalDef.diff);
|
|
280
600
|
|
|
281
601
|
// eval 级 setup(starter prep:npm install / 装系统依赖等)。命令默认非 root;
|
|
282
602
|
// setup 里需要 root 的(apt/pip)自己传 { root: true }。
|
|
283
603
|
if (evalDef.setup) {
|
|
604
|
+
enterPhase("eval.setup");
|
|
284
605
|
log(t("runner.evalSetup"));
|
|
285
|
-
evalCleanup = await evalDef.setup(withEvalLocalPaths(sandbox, evalDef.baseDir));
|
|
606
|
+
evalCleanup = await evalDef.setup(withEvalLocalPaths(sandbox, evalDef.baseDir), hookCtx);
|
|
286
607
|
}
|
|
287
608
|
}
|
|
288
609
|
|
|
289
610
|
// agent 自己的 lifecycle:装 CLI、写 config(每个沙箱一次,不在每轮 send 里)。
|
|
290
611
|
if (run.agent.setup) {
|
|
612
|
+
enterPhase("agent.setup");
|
|
291
613
|
log(t("runner.startAgentSetup"));
|
|
292
614
|
agentDidSetup = true;
|
|
293
615
|
agentCleanup = await run.agent.setup(sandbox, attemptCtx);
|
|
@@ -301,11 +623,13 @@ async function runAttemptBody(
|
|
|
301
623
|
// OTLP 导出配置(file-based,如 codex 的 config.toml [otel] 块):与 setup 分开,
|
|
302
624
|
// 在主配置写完后追加。仅当 tracing 开 + 有 endpoint 时调一次(env-based 的不实现 configure)。
|
|
303
625
|
if (telemetry && run.agent.tracing?.configure) {
|
|
626
|
+
enterPhase("telemetry.configure");
|
|
304
627
|
log(t("runner.startAgentTracing"));
|
|
305
628
|
await run.agent.tracing.configure(sandbox, attemptCtx);
|
|
306
629
|
}
|
|
307
630
|
|
|
308
631
|
// 构造 t,跑 test
|
|
632
|
+
enterPhase("eval.run");
|
|
309
633
|
log(t("runner.driveAgent"));
|
|
310
634
|
const judge = resolveJudge(evalDef.judge, config.judge);
|
|
311
635
|
const { context, state } = createEvalContext({
|
|
@@ -321,9 +645,32 @@ async function runAttemptBody(
|
|
|
321
645
|
telemetry,
|
|
322
646
|
otel,
|
|
323
647
|
evalBaseDir: evalDef.baseDir,
|
|
648
|
+
feedback,
|
|
649
|
+
// send 窗口钩子:进入前落 eval 归因、返回后落 agent 归因(见 ledger.ts)。
|
|
650
|
+
ledgerHooks: ledger
|
|
651
|
+
? {
|
|
652
|
+
beforeSend: (label) => ledger!.commitEvalWindow(label),
|
|
653
|
+
afterSend: (label) => ledger!.commitAgentWindow(label),
|
|
654
|
+
}
|
|
655
|
+
: undefined,
|
|
656
|
+
onSendActive: setSendActive,
|
|
657
|
+
// 每次 send 一个 turn 节点:本地单调时钟测得的端到端包络 + session/turn 身份;
|
|
658
|
+
// OTel 接入时再带 traceId,trace.json 的 spans 由消费方按它临时挂到 turn 下。
|
|
659
|
+
onTurn: (info) =>
|
|
660
|
+
recorder.child({
|
|
661
|
+
kind: "turn",
|
|
662
|
+
label: `s${info.sessionIndex}/t${info.turnIndex}`,
|
|
663
|
+
startOffsetMs: Math.max(0, info.startedAt - attemptEpoch),
|
|
664
|
+
durationMs: info.durationMs,
|
|
665
|
+
...(info.failed ? { failed: true as const } : {}),
|
|
666
|
+
sessionIndex: info.sessionIndex,
|
|
667
|
+
turnIndex: info.turnIndex,
|
|
668
|
+
...(info.traceId !== undefined ? { traceId: info.traceId } : {}),
|
|
669
|
+
...(info.traceAttribution !== undefined ? { traceAttribution: info.traceAttribution } : {}),
|
|
670
|
+
}),
|
|
324
671
|
});
|
|
325
672
|
|
|
326
|
-
let error:
|
|
673
|
+
let error: AttemptError | undefined;
|
|
327
674
|
let skipReason: string | undefined;
|
|
328
675
|
try {
|
|
329
676
|
await evalDef.test(context);
|
|
@@ -332,26 +679,53 @@ async function runAttemptBody(
|
|
|
332
679
|
else if (e instanceof EvalRequirementFailed) {
|
|
333
680
|
/* 断言已记录,非执行错误 */
|
|
334
681
|
} else if (e instanceof TurnFailed) {
|
|
335
|
-
|
|
682
|
+
// TurnFailed 是 eval 驱动 agent 时的一层可读失败(message 已是一句话);稳定 code
|
|
683
|
+
// `turn-failed`,不带控制流 stack(那指向 control-flow.ts,对定位无益)。
|
|
684
|
+
error = { code: "turn-failed", message: e.message, phase: getPhase() ?? "eval.run" };
|
|
336
685
|
} else {
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
error =
|
|
686
|
+
// eval 脚本(比如引用了已改名/删掉的 API)抛出的 TypeError:message 是一层原因,完整 stack
|
|
687
|
+
// 单独进 `error.stack`,niceeval show 展开时才看得到 eval 文件的 file:line。
|
|
688
|
+
error = errorFromThrown(e, getPhase());
|
|
340
689
|
}
|
|
341
690
|
}
|
|
342
691
|
|
|
343
692
|
if (skipReason) log(t("runner.skip", { reason: skipReason }));
|
|
344
693
|
|
|
345
|
-
// 采
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
694
|
+
// 采 agent 归因增量(workspace.diff 阶段:从分类账折叠逐窗口 delta)。remote agent 没有 workspace。
|
|
695
|
+
if (!skipReason && usesSandbox) enterPhase("workspace.diff");
|
|
696
|
+
let diffWindows: DiffArtifact = [];
|
|
697
|
+
if (!skipReason && usesSandbox && ledger) {
|
|
698
|
+
const startedAt = Date.now();
|
|
699
|
+
const operation = recorder.child({
|
|
700
|
+
kind: "operation",
|
|
701
|
+
label: "export workspace diff",
|
|
702
|
+
startOffsetMs: Math.max(0, startedAt - attemptEpoch),
|
|
703
|
+
durationMs: 0,
|
|
704
|
+
});
|
|
705
|
+
if (operation) recorder.pushParent(operation);
|
|
706
|
+
try {
|
|
707
|
+
diffWindows = await ledger.exportWindows();
|
|
708
|
+
if (operation) {
|
|
709
|
+
const files = new Set(diffWindows.flatMap((window) => Object.keys(window.changes))).size;
|
|
710
|
+
operation.label = `export workspace diff · ${diffWindows.length} ${diffWindows.length === 1 ? "window" : "windows"} · ${files} ${files === 1 ? "file" : "files"}`;
|
|
711
|
+
}
|
|
712
|
+
} catch (error) {
|
|
713
|
+
if (operation) operation.failed = true;
|
|
714
|
+
throw error;
|
|
715
|
+
} finally {
|
|
716
|
+
if (operation) {
|
|
717
|
+
operation.durationMs = Date.now() - startedAt;
|
|
718
|
+
recorder.popParent();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const diff = deriveDiffData(diffWindows);
|
|
350
723
|
state.late.diff = diff;
|
|
351
724
|
if (!skipReason && usesSandbox) {
|
|
725
|
+
const files = Object.values(diff.files);
|
|
352
726
|
log(t("runner.diffProgress", {
|
|
353
|
-
changed:
|
|
354
|
-
deleted:
|
|
727
|
+
changed: files.filter((f) => f.net !== "deleted").length,
|
|
728
|
+
deleted: files.filter((f) => f.net === "deleted").length,
|
|
355
729
|
}));
|
|
356
730
|
}
|
|
357
731
|
|
|
@@ -369,6 +743,9 @@ async function runAttemptBody(
|
|
|
369
743
|
scripts,
|
|
370
744
|
usage,
|
|
371
745
|
status: state.manager.lastStatus,
|
|
746
|
+
// attempt 级聚合覆盖(各轮最差值);t.* 作用域断言按它折叠,turn/session 作用域在
|
|
747
|
+
// record 时已换成各自的覆盖(见 context.ts 的 recordScoped / makeTurnHandle)。
|
|
748
|
+
coverage: state.manager.coverage,
|
|
372
749
|
readFile: async (path) => {
|
|
373
750
|
try {
|
|
374
751
|
return await sandbox!.readFile(path);
|
|
@@ -377,7 +754,10 @@ async function runAttemptBody(
|
|
|
377
754
|
}
|
|
378
755
|
},
|
|
379
756
|
};
|
|
380
|
-
if (!skipReason)
|
|
757
|
+
if (!skipReason) {
|
|
758
|
+
enterPhase("scoring.evaluate");
|
|
759
|
+
log(t("runner.scoreJudge"));
|
|
760
|
+
}
|
|
381
761
|
const assertions = skipReason ? [] : await state.collector.finalize(scoringContext);
|
|
382
762
|
const verdict = computeVerdict({ error, assertions, skipReason, strict: run.strict });
|
|
383
763
|
|
|
@@ -387,6 +767,7 @@ async function runAttemptBody(
|
|
|
387
767
|
// 再 selectTraceSpans 按 kind 挑出回合/模型/工具,丢掉 "other" 噪声(干净小 trace 整段保留)。
|
|
388
768
|
let trace: TraceSpan[] | undefined;
|
|
389
769
|
if (receiver) {
|
|
770
|
+
enterPhase("telemetry.collect");
|
|
390
771
|
await receiver.settle(250, 1500);
|
|
391
772
|
const spans = receiver.collect();
|
|
392
773
|
if (spans.length) {
|
|
@@ -400,6 +781,7 @@ async function runAttemptBody(
|
|
|
400
781
|
} else if (otel) {
|
|
401
782
|
// 共享通道:receiver 不归本 attempt 关,trace 只取归属到本 attempt 的 span
|
|
402
783
|
//(逐轮攒的 + 按本 attempt traceId sweep 回的迟到批)。
|
|
784
|
+
enterPhase("telemetry.collect");
|
|
403
785
|
const late = await otel.sweep(state.manager.otelTraceIds);
|
|
404
786
|
const spans = [...state.manager.otelSpans, ...late];
|
|
405
787
|
if (spans.length) {
|
|
@@ -410,6 +792,10 @@ async function runAttemptBody(
|
|
|
410
792
|
}
|
|
411
793
|
}
|
|
412
794
|
|
|
795
|
+
// 主链到 telemetry.collect 为止。必须在 Effect Scope release 之前显式封口;否则最后一个
|
|
796
|
+
// 主链 phase 会一直开到 sandbox.stop 完成,既把收尾时间重复算进主链,也会让 phases
|
|
797
|
+
// 主链合计大于 durationMs。Scope finalizer 只负责另记 sandbox.stop / sandbox.suspend。
|
|
798
|
+
recorder.closeCurrent();
|
|
413
799
|
const durationMs = Date.now() - t0;
|
|
414
800
|
const o11y = buildO11ySummary(events, usage, durationMs);
|
|
415
801
|
// 实测成本(网关带回)优先,缺则按 model + 用量查价格表估算(见 o11y/cost.ts)。
|
|
@@ -419,7 +805,7 @@ async function runAttemptBody(
|
|
|
419
805
|
// 收 test 引用到的 eval 源码(按 send / 断言的 loc 去重),供 view 渲染代码视图。
|
|
420
806
|
const sources = await collectSources(events, assertions, evalDef.source);
|
|
421
807
|
|
|
422
|
-
|
|
808
|
+
const value: EvalResult = {
|
|
423
809
|
id: evalDef.id,
|
|
424
810
|
description: evalDef.description,
|
|
425
811
|
experimentId: run.experimentId,
|
|
@@ -441,56 +827,163 @@ async function runAttemptBody(
|
|
|
441
827
|
o11y,
|
|
442
828
|
trace,
|
|
443
829
|
agentSetup,
|
|
444
|
-
diff,
|
|
830
|
+
diff: diffWindows,
|
|
831
|
+
coverage: state.manager.coverage,
|
|
832
|
+
...(usesSandbox
|
|
833
|
+
? {
|
|
834
|
+
sandbox: {
|
|
835
|
+
provider: resolveSandbox(run.sandbox ?? config.sandbox).provider,
|
|
836
|
+
sandboxId: sandbox.sandboxId,
|
|
837
|
+
},
|
|
838
|
+
}
|
|
839
|
+
: {}),
|
|
445
840
|
};
|
|
841
|
+
result = value;
|
|
842
|
+
return value;
|
|
446
843
|
} catch (e) {
|
|
447
|
-
|
|
844
|
+
recorder.failCurrent();
|
|
845
|
+
const value: EvalResult = {
|
|
448
846
|
...base,
|
|
449
847
|
durationMs: Date.now() - t0,
|
|
450
|
-
error:
|
|
848
|
+
error: errorFromThrown(e, getPhase()),
|
|
451
849
|
...(agentSetup !== undefined ? { agentSetup } : {}),
|
|
452
850
|
};
|
|
851
|
+
result = value;
|
|
852
|
+
return value;
|
|
453
853
|
} finally {
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
// 沙箱 stop / 接收器 close 不在这里 —— 由 runAttemptEffect 的 Scope
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
854
|
+
// 收尾段一律在 finally 跑(主链成败都执行),不改判定,各自兜错(diagnostic)、各自计时
|
|
855
|
+
// (不计入 durationMs 口径,见 docs/feature/results/architecture.md)。执行序与 LifecyclePhase
|
|
856
|
+
// 闭集声明一致:eval.teardown → agent.teardown → sandbox.teardown;各段可独立标 failed。
|
|
857
|
+
// 沙箱 stop / 接收器 close 不在这里 —— 由 runAttemptEffect 的 Scope 在本函数返回后回收,
|
|
858
|
+
// 并经 finalizer 计成 sandbox.stop。没有对应 cleanup 的段直接跳过,不产生空阶段。
|
|
859
|
+
const evalCleanupFn = typeof evalCleanup === "function" ? evalCleanup : undefined;
|
|
860
|
+
if (evalCleanupFn) {
|
|
861
|
+
enterPhase("eval.teardown");
|
|
862
|
+
await recorder
|
|
863
|
+
.measureClosing("eval.teardown", async () => {
|
|
864
|
+
try {
|
|
865
|
+
await evalCleanupFn();
|
|
866
|
+
} catch (e) {
|
|
867
|
+
// 收尾失败只是 diagnostic,不改判定 —— 挂到 attempt.diagnostics(见 finally 末尾并入)。
|
|
868
|
+
diagnostics.push(teardownDiagnostic("eval.teardown", e));
|
|
869
|
+
throw e; // 让 measureClosing 把这段标 failed
|
|
870
|
+
}
|
|
871
|
+
})
|
|
872
|
+
.catch(() => {});
|
|
469
873
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
} catch {
|
|
476
|
-
// 同上,只作 diagnostic
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
// sandbox.teardown 钩子:按追加的逆序执行,沙箱销毁前最后一步。
|
|
480
|
-
if (sandboxTeardownHooks.length > 0) {
|
|
481
|
-
log(t("runner.startSandboxTeardown"));
|
|
482
|
-
for (let i = sandboxTeardownHooks.length - 1; i >= 0; i--) {
|
|
874
|
+
const agentCleanupFn = typeof agentCleanup === "function" ? agentCleanup : undefined;
|
|
875
|
+
if (agentCleanupFn !== undefined || (agentDidSetup && run.agent.teardown !== undefined)) {
|
|
876
|
+
enterPhase("agent.teardown");
|
|
877
|
+
await recorder
|
|
878
|
+
.measureClosing("agent.teardown", async () => {
|
|
483
879
|
try {
|
|
484
|
-
await
|
|
485
|
-
|
|
486
|
-
|
|
880
|
+
if (agentCleanupFn) await agentCleanupFn();
|
|
881
|
+
if (agentDidSetup) await run.agent.teardown?.(sandbox, attemptCtx);
|
|
882
|
+
} catch (e) {
|
|
883
|
+
diagnostics.push(teardownDiagnostic("agent.teardown", e));
|
|
884
|
+
throw e;
|
|
487
885
|
}
|
|
488
|
-
}
|
|
489
|
-
|
|
886
|
+
})
|
|
887
|
+
.catch(() => {});
|
|
888
|
+
}
|
|
889
|
+
if (usesSandbox && (sandboxCleanups.length > 0 || sandboxTeardownHooks.length > 0)) {
|
|
890
|
+
enterPhase("sandbox.teardown");
|
|
891
|
+
await recorder
|
|
892
|
+
.measureClosing("sandbox.teardown", async () => {
|
|
893
|
+
const before = diagnostics.length;
|
|
894
|
+
// sandbox.setup 返回的 cleanup:LIFO(后 setup 先 cleanup)。
|
|
895
|
+
for (let i = sandboxCleanups.length - 1; i >= 0; i--) {
|
|
896
|
+
try {
|
|
897
|
+
await sandboxCleanups[i]();
|
|
898
|
+
} catch (e) {
|
|
899
|
+
diagnostics.push(teardownDiagnostic("sandbox.teardown", e));
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
// sandbox.teardown 钩子:按追加的逆序执行,沙箱销毁前最后一步。
|
|
903
|
+
if (sandboxTeardownHooks.length > 0) {
|
|
904
|
+
log(t("runner.startSandboxTeardown"));
|
|
905
|
+
for (let i = sandboxTeardownHooks.length - 1; i >= 0; i--) {
|
|
906
|
+
try {
|
|
907
|
+
const teardownCleanup = await sandboxTeardownHooks[i](sandbox, hookCtx);
|
|
908
|
+
// SandboxHook 类型允许返回 Cleanup(与 setup 同一签名);teardown 返回的
|
|
909
|
+
// cleanup 没有更晚的挂点,立即执行。
|
|
910
|
+
if (typeof teardownCleanup === "function") await teardownCleanup();
|
|
911
|
+
} catch (e) {
|
|
912
|
+
diagnostics.push(teardownDiagnostic("sandbox.teardown", e));
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (diagnostics.length > before) throw new Error("sandbox teardown diagnostics");
|
|
917
|
+
})
|
|
918
|
+
.catch(() => {});
|
|
490
919
|
}
|
|
920
|
+
// finally 在两个 return 求值之后、函数真正交还返回值之前运行;`result` 已经是那个即将被返回的
|
|
921
|
+
// 对象引用,这里往它上面挂 diagnostics,调用方拿到的就是带诊断的同一个对象(标准 try/finally
|
|
922
|
+
// 变异语义)。result 恒已赋值(两个 return 分支都先赋值再 return);极端情况下(finally 之前就
|
|
923
|
+
// 抛了、result 还没赋值)静默跳过,不掩盖原始异常。
|
|
924
|
+
if (diagnostics.length > 0 && result) result.diagnostics = diagnostics;
|
|
491
925
|
}
|
|
492
926
|
}
|
|
493
927
|
|
|
928
|
+
/** 把一次 teardown / cleanup 失败折成一条 `DiagnosticRecord`(warning,不改判定)。message 取一层
|
|
929
|
+
* 摘要(`firstLine(formatThrown)`),完整 stack 不塞进单 attempt 诊断 —— 诊断是「顺带发生的清理
|
|
930
|
+
* 问题」,不是 attempt 的主因(主因在 verdict / error)。稳定 code `teardown-failed`。 */
|
|
931
|
+
function teardownDiagnostic(phase: LifecyclePhase, e: unknown): DiagnosticRecord {
|
|
932
|
+
return {
|
|
933
|
+
code: "teardown-failed",
|
|
934
|
+
level: "warning",
|
|
935
|
+
message: firstLine(formatThrown(e)),
|
|
936
|
+
phase,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* 命令时间树包装:runCommand / runShell 的最外层公开调用各记一个 command 子节点
|
|
942
|
+
* (有界脱敏摘要 + exitCode;env 值与 stdout/stderr 不进入时间树)。Proxy 只拦这两个方法,
|
|
943
|
+
* provider 内部 `this.runCommand(...)` 转调不经过它——不形成重复节点。
|
|
944
|
+
*/
|
|
945
|
+
function withCommandTiming(sandbox: Sandbox, recorder: TimingRecorder): Sandbox {
|
|
946
|
+
const wrap = async <T>(display: string, fn: () => Promise<T>): Promise<T> => {
|
|
947
|
+
const startOffsetMs = recorder.offsetNow();
|
|
948
|
+
const t0 = Date.now();
|
|
949
|
+
try {
|
|
950
|
+
const result = await fn();
|
|
951
|
+
const exitCode = (result as { exitCode?: unknown })?.exitCode;
|
|
952
|
+
recorder.child(
|
|
953
|
+
commandNode({
|
|
954
|
+
display,
|
|
955
|
+
startOffsetMs,
|
|
956
|
+
durationMs: Date.now() - t0,
|
|
957
|
+
...(typeof exitCode === "number" ? { exitCode, failed: exitCode !== 0 } : {}),
|
|
958
|
+
}),
|
|
959
|
+
);
|
|
960
|
+
// CommandResult.command:最外层公开调用恰好是「eval 实际跑了什么」的定义点,摘要
|
|
961
|
+
// 与时间树节点同一份;provider 自己填过就不覆盖。
|
|
962
|
+
if (result !== null && typeof result === "object" && !("command" in result)) {
|
|
963
|
+
return { ...result, command: display } as T;
|
|
964
|
+
}
|
|
965
|
+
return result;
|
|
966
|
+
} catch (e) {
|
|
967
|
+
recorder.child(commandNode({ display, startOffsetMs, durationMs: Date.now() - t0, failed: true }));
|
|
968
|
+
throw e;
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
return new Proxy(sandbox, {
|
|
972
|
+
get(target, prop, receiver) {
|
|
973
|
+
if (prop === "runCommand") {
|
|
974
|
+
return (cmd: string, args?: string[], opts?: unknown) =>
|
|
975
|
+
wrap(commandDisplay(cmd, args), () => (target.runCommand as (...a: unknown[]) => Promise<unknown>)(cmd, args, opts));
|
|
976
|
+
}
|
|
977
|
+
if (prop === "runShell") {
|
|
978
|
+
return (script: string, opts?: unknown) =>
|
|
979
|
+
wrap(commandDisplay(script), () => (target.runShell as (...a: unknown[]) => Promise<unknown>)(script, opts));
|
|
980
|
+
}
|
|
981
|
+
const value = Reflect.get(target, prop, receiver);
|
|
982
|
+
return typeof value === "function" ? (value as (...a: unknown[]) => unknown).bind(target) : value;
|
|
983
|
+
},
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
494
987
|
/**
|
|
495
988
|
* 收集 test 引用到的 eval 源码:从 send(user message)与断言的 loc 去重出文件集。
|
|
496
989
|
* 命中 eval 自己的定义文件(绝大多数情况——send / 断言几乎总在 eval 主体里直接调用)时,
|
|
@@ -521,15 +1014,21 @@ async function collectSources(
|
|
|
521
1014
|
return out;
|
|
522
1015
|
}
|
|
523
1016
|
|
|
1017
|
+
/** 解析后运行配置的穷尽投影(ExperimentRunInfo,见 docs/feature/results/architecture.md):
|
|
1018
|
+
* agent/model 只在快照顶层,这里不复制;sandbox 只经 provider 的公开参数投影落盘。 */
|
|
524
1019
|
function experimentRunInfo(run: AgentRun): EvalResult["experiment"] {
|
|
525
1020
|
return {
|
|
526
|
-
|
|
527
|
-
|
|
1021
|
+
...(run.description !== undefined ? { description: run.description } : {}),
|
|
1022
|
+
...(run.reasoningEffort !== undefined ? { reasoningEffort: run.reasoningEffort } : {}),
|
|
1023
|
+
...(Object.keys(run.flags).length > 0 ? { flags: run.flags } : {}),
|
|
528
1024
|
runs: run.runs,
|
|
529
1025
|
earlyExit: run.earlyExit,
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1026
|
+
...(run.timeoutMs !== undefined ? { timeoutMs: run.timeoutMs } : {}),
|
|
1027
|
+
...(run.budget !== undefined ? { budget: run.budget } : {}),
|
|
1028
|
+
...(run.maxConcurrency !== undefined ? { maxConcurrency: run.maxConcurrency } : {}),
|
|
1029
|
+
selectedEvalIds: run.selectedEvalIds ?? [],
|
|
1030
|
+
...(run.evalFilterFingerprint !== undefined ? { evalFilterFingerprint: run.evalFilterFingerprint } : {}),
|
|
1031
|
+
...(run.sandbox !== undefined ? { sandbox: sandboxRunInfo(run.sandbox) } : {}),
|
|
533
1032
|
};
|
|
534
1033
|
}
|
|
535
1034
|
|