niceeval 0.6.1 → 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 +67 -5
- 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 -1
- package/dist/report/built-ins/experiment-comparison.js +116 -10
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +1 -1
- package/dist/report/components.d.ts +8 -2
- package/dist/report/components.js +3 -3
- package/dist/report/compute.d.ts +11 -18
- package/dist/report/compute.js +54 -34
- package/dist/report/flag.d.ts +16 -1
- package/dist/report/flag.js +19 -1
- package/dist/report/format.d.ts +16 -8
- package/dist/report/format.js +27 -12
- package/dist/report/index.d.ts +4 -3
- package/dist/report/index.js +5 -4
- package/dist/report/locale.d.ts +11 -2
- package/dist/report/locale.js +23 -5
- package/dist/report/metrics.d.ts +13 -1
- package/dist/report/metrics.js +65 -14
- package/dist/report/primitives.d.ts +6 -0
- package/dist/report/react/AttemptList.d.ts +2 -2
- package/dist/report/react/AttemptList.js +5 -6
- 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 +2 -1
- package/dist/report/react/ExperimentList.js +8 -10
- package/dist/report/react/MetricScatter.js +5 -11
- 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 +21 -14
- 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 +42 -41
- package/dist/report/text/table.js +36 -5
- package/dist/report/types.d.ts +39 -21
- 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 +22 -4
- package/docs-site/zh/concepts/experiment.mdx +1 -1
- package/docs-site/zh/concepts/overview.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
- 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/official-adapters.mdx +26 -2
- package/docs-site/zh/guides/publish-report.mdx +30 -16
- package/docs-site/zh/guides/report-components.mdx +42 -30
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +17 -9
- package/docs-site/zh/guides/runner.mdx +17 -7
- package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
- package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
- package/docs-site/zh/guides/scoring-guide.mdx +4 -4
- package/docs-site/zh/guides/viewing-results.mdx +79 -36
- package/docs-site/zh/guides/write-experiment.mdx +5 -3
- package/docs-site/zh/guides/write-send.mdx +17 -1
- 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 +33 -7
- package/docs-site/zh/reference/define-agent.mdx +57 -4
- 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/expect.mdx +26 -1
- 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 +68 -5
- 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 +64 -2
- package/src/i18n/zh-CN.ts +65 -3
- 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 +110 -153
- package/src/report/built-ins/experiment-comparison.tsx +173 -13
- package/src/report/built-ins/index.ts +6 -1
- package/src/report/components.tsx +9 -3
- package/src/report/compute.ts +70 -40
- package/src/report/dual-render.test.tsx +194 -67
- package/src/report/flag.ts +30 -2
- package/src/report/format.ts +35 -11
- package/src/report/index.ts +22 -4
- package/src/report/locale.ts +25 -5
- package/src/report/metrics.ts +67 -14
- package/src/report/primitives.tsx +6 -0
- package/src/report/react/AttemptList.tsx +6 -31
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +68 -0
- package/src/report/react/ExperimentList.tsx +15 -9
- package/src/report/react/MetricScatter.tsx +12 -14
- 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 +33 -1
- package/src/report/react/fixtures.ts +24 -17
- package/src/report/react/render.test.tsx +9 -64
- package/src/report/react/styles.css +73 -2
- package/src/report/report.test.ts +306 -98
- package/src/report/report.ts +6 -2
- package/src/report/text/faces.ts +47 -43
- package/src/report/text/table.ts +42 -5
- package/src/report/types.ts +41 -21
- 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 +8 -6
- 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 +21 -1
- package/src/show/render.ts +619 -104
- package/src/show/show.test.ts +235 -19
- 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 +1 -0
- package/src/view/data.ts +11 -1
- package/src/view/index.ts +11 -0
- package/src/view/server.ts +2 -0
- package/src/view/styles.css +3 -0
- package/src/view/view-report.test.ts +6 -5
- 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/cli.ts
CHANGED
|
@@ -5,23 +5,36 @@
|
|
|
5
5
|
// niceeval clean 删除 .niceeval/ 历史运行 artifact
|
|
6
6
|
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
8
9
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
9
10
|
import { existsSync } from "node:fs";
|
|
10
|
-
import { join, relative } from "node:path";
|
|
11
|
+
import { join, relative, resolve as resolvePath } from "node:path";
|
|
11
12
|
import { pathToFileURL } from "node:url";
|
|
12
13
|
import { parseArgs as nodeParseArgs } from "node:util";
|
|
13
14
|
import { discoverEvals, discoverExperiments, makeFilter } from "./runner/discover.ts";
|
|
14
15
|
import { runEvals, type AgentRun } from "./runner/run.ts";
|
|
15
|
-
import { runWho } from "./runner/types.ts";
|
|
16
16
|
import { planCarry } from "./runner/fingerprint.ts";
|
|
17
|
+
import { failureDetailFromResult } from "./runner/feedback/failure.ts";
|
|
17
18
|
import { stopAllSandboxes, liveSandboxCount } from "./sandbox/registry.ts";
|
|
18
19
|
import { evalLevelStats } from "./shared/verdict.ts";
|
|
19
20
|
import { sandboxRecommendedConcurrency } from "./sandbox/resolve.ts";
|
|
20
|
-
import { Console as ConsoleReporter } from "./runner/reporters/console.ts";
|
|
21
|
-
import { Quiet as QuietReporter } from "./runner/reporters/quiet.ts";
|
|
22
21
|
import { Json, JUnit } from "./runner/reporters/json.ts";
|
|
23
|
-
import { Live as LiveReporter, type LiveRow } from "./runner/reporters/live.ts";
|
|
24
22
|
import { Artifacts as ArtifactsReporter } from "./runner/reporters/artifacts.ts";
|
|
23
|
+
import {
|
|
24
|
+
resolveOutputProfile,
|
|
25
|
+
createFeedbackCoordinator,
|
|
26
|
+
createNodeFeedbackIO,
|
|
27
|
+
createHumanRenderer,
|
|
28
|
+
createAgentRenderer,
|
|
29
|
+
createCiRenderer,
|
|
30
|
+
renderAgentPlanEnvelope,
|
|
31
|
+
renderHumanDryPlan,
|
|
32
|
+
renderCiDryPlan,
|
|
33
|
+
computeCiExitCode,
|
|
34
|
+
reportActivity,
|
|
35
|
+
type OutputProfileFlag,
|
|
36
|
+
type AgentPlanRow,
|
|
37
|
+
} from "./runner/feedback/index.ts";
|
|
25
38
|
import {
|
|
26
39
|
buildView,
|
|
27
40
|
startViewServer,
|
|
@@ -38,7 +51,17 @@ import { ReportLoadError } from "../dist/report/load.js";
|
|
|
38
51
|
import { runShow } from "./show/index.ts";
|
|
39
52
|
import { t } from "./i18n/index.ts";
|
|
40
53
|
import { formatThrown, upsertManagedBlock } from "./util.ts";
|
|
41
|
-
import type {
|
|
54
|
+
import type {
|
|
55
|
+
CompletionStatus,
|
|
56
|
+
Config,
|
|
57
|
+
DiscoveredExperiment,
|
|
58
|
+
ReporterError,
|
|
59
|
+
ReporterRegistration,
|
|
60
|
+
RunCompletion,
|
|
61
|
+
RunFeedbackPlan,
|
|
62
|
+
RunFeedbackState,
|
|
63
|
+
RunSummary,
|
|
64
|
+
} from "./types.ts";
|
|
42
65
|
|
|
43
66
|
/**
|
|
44
67
|
* view 的可预期用户错误:版本不同的报告(npx 提示)、位置参数/组合语义错误、
|
|
@@ -60,7 +83,8 @@ interface Flags {
|
|
|
60
83
|
timeout?: number;
|
|
61
84
|
earlyExit?: boolean;
|
|
62
85
|
dry: boolean;
|
|
63
|
-
|
|
86
|
+
/** 反馈 profile,已解析/校验(`auto` 默认值也算已解析——具体环境判定见 `resolveOutputProfile`)。 */
|
|
87
|
+
output: OutputProfileFlag;
|
|
64
88
|
force: boolean;
|
|
65
89
|
strict: boolean;
|
|
66
90
|
budget?: number;
|
|
@@ -78,6 +102,13 @@ interface Flags {
|
|
|
78
102
|
diff: boolean;
|
|
79
103
|
/** --diff=<路径>(必须 = 连写;空格形式会把路径当 eval id 前缀,按文档如此)。 */
|
|
80
104
|
diffPath?: string;
|
|
105
|
+
timing?: "summary" | "full";
|
|
106
|
+
keepSandbox?: "failed" | "all";
|
|
107
|
+
all: boolean;
|
|
108
|
+
allowSensitiveArtifacts: boolean;
|
|
109
|
+
window?: string;
|
|
110
|
+
sandboxPath?: string;
|
|
111
|
+
leaveRunning: boolean;
|
|
81
112
|
history: boolean;
|
|
82
113
|
experiment?: string;
|
|
83
114
|
run?: string;
|
|
@@ -103,6 +134,16 @@ const FLAG_OPTIONS = {
|
|
|
103
134
|
timeout: { type: "string" },
|
|
104
135
|
/** 整次运行的预算上限(美元)。 */
|
|
105
136
|
budget: { type: "string" },
|
|
137
|
+
/** `exp` 命令专用:跑完留下 failed/errored attempt 的沙箱现场(= `--keep-sandbox=failed`);`--keep-sandbox=all` 连 passed 也留。事后用 `niceeval sandbox list/enter/stop` 查看与销毁。 */
|
|
138
|
+
"keep-sandbox": { type: "boolean" },
|
|
139
|
+
/** `sandbox stop` 专用:销毁全部留存沙箱。 */
|
|
140
|
+
all: { type: "boolean" },
|
|
141
|
+
/** `sandbox diff` 专用:只看某个 send 窗口(如 `--window s1/t2`);省略输出全部窗口的串联视图。 */
|
|
142
|
+
window: { type: "string" },
|
|
143
|
+
/** `sandbox diff` 专用:只看某个文件的 patch;省略输出该窗口的全部文件。 */
|
|
144
|
+
path: { type: "string" },
|
|
145
|
+
/** `sandbox enter` 专用:shell 退出后让现场保持运行,不送回休眠。 */
|
|
146
|
+
"leave-running": { type: "boolean" },
|
|
106
147
|
/** 只运行带有该 tag 的 eval(见 `defineEval` 的 `tags`)。 */
|
|
107
148
|
tag: { type: "string" },
|
|
108
149
|
/** 额外写一份 JUnit XML 报告到指定路径,供 CI 消费。 */
|
|
@@ -113,6 +154,8 @@ const FLAG_OPTIONS = {
|
|
|
113
154
|
out: { type: "string" },
|
|
114
155
|
/** `view` 命令专用:指定本地服务器监听端口。 */
|
|
115
156
|
port: { type: "string" },
|
|
157
|
+
/** `view --out` 专用:对非发布根(快照没有 publish:{redaction:"applied"} 标记)导出时的显式确认——静态站会原样携带未消毒的证据文件。 */
|
|
158
|
+
"allow-sensitive-artifacts": { type: "boolean" },
|
|
116
159
|
// show 的证据切面 / 时间轴 / 报告装载(docs-site/zh/guides/viewing-results.mdx)。
|
|
117
160
|
// 证据切面只认 `@<locator>`(或收窄到单个 eval 的前缀)选出的那一个 attempt——不再有
|
|
118
161
|
// 数字 `--attempt`,选哪个 attempt 由 locator 精确指名,不是「先选 eval 再挑第几次」。
|
|
@@ -120,22 +163,24 @@ const FLAG_OPTIONS = {
|
|
|
120
163
|
eval: { type: "boolean" },
|
|
121
164
|
/** `show` 命令专用:该 attempt 的标准执行事件流(消息、thinking、Skill load、工具调用/结果);有 OTel 时同一节点补时间(证据切面)。 */
|
|
122
165
|
execution: { type: "boolean" },
|
|
166
|
+
/** `show` 命令专用:整个 Attempt 的统一时间树;裸 `--timing` 给有界诊断投影,`--timing=full` 逐节点展开全部 runner/已关联 OTel 节点。 */
|
|
167
|
+
timing: { type: "boolean" },
|
|
123
168
|
// --diff 是布尔;--diff=<路径> 在 parseArgs 前预扫成 diffPath(路径必须 = 连写,
|
|
124
169
|
// 空格形式的下一个 token 仍是位置参数 = eval id 前缀,与文档一致)。
|
|
125
170
|
/** `show` 命令专用:sandbox 里的文件改动摘要;`--diff=<文件路径>` 看单个文件的完整改动(路径必须 `=` 连写)。 */
|
|
126
171
|
diff: { type: "boolean" },
|
|
127
172
|
/** `show` 命令专用:跨 run 时间轴,只列真实执行;与 `--report` 互斥。 */
|
|
128
173
|
history: { type: "boolean" },
|
|
129
|
-
/** `show` / `view`
|
|
174
|
+
/** `show` / `view` 命令专用:按路径段前缀收窄 experiment;组名会选中组内全部配置。 */
|
|
130
175
|
experiment: { type: "string" },
|
|
131
176
|
/** `show` / `view` 命令专用:钉死看某一个结果目录(某次快照或 `copySnapshots` 产物)。 */
|
|
132
177
|
run: { type: "string" },
|
|
133
|
-
/** `show` / `view`
|
|
178
|
+
/** `show` / `view` 命令专用:用文件默认导出的 `defineReport(...)` 替换两者共用的默认报告。 */
|
|
134
179
|
report: { type: "string" },
|
|
135
|
-
/** 只打印本次会匹配到的 eval ×
|
|
180
|
+
/** 只打印本次会匹配到的 eval × 运行配置,不实际执行(按下面 `--output` 选中的 profile 给出预览)。 */
|
|
136
181
|
dry: { type: "boolean" },
|
|
137
|
-
/**
|
|
138
|
-
|
|
182
|
+
/** 反馈 profile:`auto`(默认)按环境自动选择,`human` / `agent` / `ci` 强制指定;只改变终端展示,不改变选择、调度、判定、artifact 或退出码。`auto` 依次判定:stderr 是 TTY → human;否则 `CI`(或其它常见 CI 平台环境变量)存在 → ci;否则 → agent。 */
|
|
183
|
+
output: { type: "string" },
|
|
139
184
|
/** 忽略上次运行结果,不跳过已通过的 (experiment, eval) 组合,强制全部重跑。 */
|
|
140
185
|
force: { type: "boolean" },
|
|
141
186
|
/** CI 中推荐使用:让软阈值(`soft`)失败也计入整条 eval 的 verdict。 */
|
|
@@ -162,17 +207,50 @@ function numberFlag(name: string, raw: string | undefined): number | undefined {
|
|
|
162
207
|
return n;
|
|
163
208
|
}
|
|
164
209
|
|
|
210
|
+
const OUTPUT_PROFILE_VALUES = ["auto", "human", "agent", "ci"] as const;
|
|
211
|
+
|
|
212
|
+
/** `--output` 解析仅接受 `auto|human|agent|ci`;非法值直接报清晰用法并退出,不静默回退到某个默认值。 */
|
|
213
|
+
function outputFlag(raw: string | undefined): OutputProfileFlag {
|
|
214
|
+
if (raw === undefined) return "auto";
|
|
215
|
+
if ((OUTPUT_PROFILE_VALUES as readonly string[]).includes(raw)) return raw as OutputProfileFlag;
|
|
216
|
+
process.stderr.write(t("cli.flag.invalidOutput", { value: raw }));
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
165
220
|
function parseArgs(argv: string[]): { command: string; positionals: string[]; flags: Flags } {
|
|
166
221
|
if (argv[0] === "--") argv = argv.slice(1);
|
|
167
222
|
|
|
168
223
|
// --diff=<路径> 预扫:diff 本体是布尔(裸 --diff = 文件级摘要),路径只接受 = 连写。
|
|
169
224
|
let diffPath: string | undefined;
|
|
225
|
+
// --keep-sandbox[=failed|all] 预扫:本体是布尔(裸 = failed 档),档位只接受 = 连写。
|
|
226
|
+
let keepSandboxTier: "failed" | "all" | undefined;
|
|
227
|
+
// --timing[=summary|full] 预扫:node:util 的单个 option 不支持 boolean|string 联合,
|
|
228
|
+
// 所以 mode 在严格 parseArgs 前提取,再把两种形式统一成布尔 --timing。
|
|
229
|
+
let timingMode: "summary" | "full" | undefined;
|
|
170
230
|
argv = argv.map((arg) => {
|
|
171
231
|
if (arg.startsWith("--diff=")) {
|
|
172
232
|
const path = arg.slice("--diff=".length);
|
|
173
233
|
if (path) diffPath = path;
|
|
174
234
|
return "--diff";
|
|
175
235
|
}
|
|
236
|
+
if (arg.startsWith("--keep-sandbox=")) {
|
|
237
|
+
const tier = arg.slice("--keep-sandbox=".length);
|
|
238
|
+
if (tier !== "failed" && tier !== "all") {
|
|
239
|
+
process.stderr.write(`--keep-sandbox only accepts "failed" (default) or "all", got "${tier}".\n`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
keepSandboxTier = tier;
|
|
243
|
+
return "--keep-sandbox";
|
|
244
|
+
}
|
|
245
|
+
if (arg.startsWith("--timing=")) {
|
|
246
|
+
const mode = arg.slice("--timing=".length);
|
|
247
|
+
if (mode !== "summary" && mode !== "full") {
|
|
248
|
+
process.stderr.write(`--timing only accepts "summary" (default) or "full", got "${mode}".\n`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
timingMode = mode;
|
|
252
|
+
return "--timing";
|
|
253
|
+
}
|
|
176
254
|
return arg;
|
|
177
255
|
});
|
|
178
256
|
|
|
@@ -188,7 +266,7 @@ function parseArgs(argv: string[]): { command: string; positionals: string[]; fl
|
|
|
188
266
|
}
|
|
189
267
|
|
|
190
268
|
// 第一个位置参数若是已知命令,则为命令;其余是 eval id 前缀 / view 输入。
|
|
191
|
-
const commands = new Set(["exp", "show", "list", "view", "clean", "init", "watch", "run"]);
|
|
269
|
+
const commands = new Set(["exp", "show", "list", "view", "clean", "init", "watch", "run", "sandbox"]);
|
|
192
270
|
let command = "run";
|
|
193
271
|
let positionals = rawPositionals;
|
|
194
272
|
if (rawPositionals[0] && commands.has(rawPositionals[0])) {
|
|
@@ -209,7 +287,7 @@ function parseArgs(argv: string[]): { command: string; positionals: string[]; fl
|
|
|
209
287
|
out: values.out as string | undefined,
|
|
210
288
|
port: numberFlag("port", values.port as string | undefined),
|
|
211
289
|
dry: values.dry === true,
|
|
212
|
-
|
|
290
|
+
output: outputFlag(values.output as string | undefined),
|
|
213
291
|
force: values.force === true,
|
|
214
292
|
strict: values.strict === true,
|
|
215
293
|
earlyExit: values["no-early-exit"] === true ? false : values["early-exit"] === true ? true : undefined,
|
|
@@ -220,6 +298,13 @@ function parseArgs(argv: string[]): { command: string; positionals: string[]; fl
|
|
|
220
298
|
execution: values.execution === true,
|
|
221
299
|
diff: values.diff === true && diffPath === undefined,
|
|
222
300
|
diffPath,
|
|
301
|
+
timing: values.timing === true ? (timingMode ?? "summary") : undefined,
|
|
302
|
+
keepSandbox: values["keep-sandbox"] === true ? (keepSandboxTier ?? "failed") : undefined,
|
|
303
|
+
all: values.all === true,
|
|
304
|
+
allowSensitiveArtifacts: values["allow-sensitive-artifacts"] === true,
|
|
305
|
+
window: values.window as string | undefined,
|
|
306
|
+
sandboxPath: values.path as string | undefined,
|
|
307
|
+
leaveRunning: values["leave-running"] === true,
|
|
223
308
|
history: values.history === true,
|
|
224
309
|
experiment: values.experiment as string | undefined,
|
|
225
310
|
run: values.run as string | undefined,
|
|
@@ -228,6 +313,31 @@ function parseArgs(argv: string[]): { command: string; positionals: string[]; fl
|
|
|
228
313
|
return { command, positionals, flags };
|
|
229
314
|
}
|
|
230
315
|
|
|
316
|
+
/**
|
|
317
|
+
* exp 只接受两类输入:位置参数选「跑哪些 eval」+ 调度/输出/机器出口 flag 选「对着哪个 agent、
|
|
318
|
+
* 怎么跑」。show / view 专属的证据切面(`--eval`/`--execution`/`--diff`)、时间轴(`--history`)、
|
|
319
|
+
* Selection 收窄(`--experiment`/`--run`)、报告装载(`--report`)、查看器(`--out`/`--port`/`--open`)
|
|
320
|
+
* 不能被 exp 静默忽略(见 docs/feature/experiments/cli.md「用法错误」)。返回第一个被误用的
|
|
321
|
+
* flag 及其归属命令(用于报错),没有误用返回 undefined。
|
|
322
|
+
*/
|
|
323
|
+
function firstViewerOnlyFlag(flags: Flags): { flag: string; command: string } | undefined {
|
|
324
|
+
const SHOW = "show";
|
|
325
|
+
const BOTH = "show / view";
|
|
326
|
+
const VIEW = "view";
|
|
327
|
+
if (flags.eval) return { flag: "--eval", command: SHOW };
|
|
328
|
+
if (flags.execution) return { flag: "--execution", command: SHOW };
|
|
329
|
+
if (flags.timing !== undefined) return { flag: "--timing", command: SHOW };
|
|
330
|
+
if (flags.diff || flags.diffPath !== undefined) return { flag: "--diff", command: SHOW };
|
|
331
|
+
if (flags.history) return { flag: "--history", command: SHOW };
|
|
332
|
+
if (flags.experiment !== undefined) return { flag: "--experiment", command: BOTH };
|
|
333
|
+
if (flags.run !== undefined) return { flag: "--run", command: BOTH };
|
|
334
|
+
if (flags.report !== undefined) return { flag: "--report", command: BOTH };
|
|
335
|
+
if (flags.out !== undefined) return { flag: "--out", command: VIEW };
|
|
336
|
+
if (flags.port !== undefined) return { flag: "--port", command: VIEW };
|
|
337
|
+
if (flags.open !== undefined) return { flag: "--open", command: VIEW };
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
|
|
231
341
|
/** 调度项的环境变量层(标志 > 环境变量 > config > 默认,见 docs/cli.md「配置优先级」)。 */
|
|
232
342
|
function envNumber(name: string): number | undefined {
|
|
233
343
|
const raw = process.env[name]?.trim();
|
|
@@ -357,6 +467,76 @@ function evalsFilterFromExperiment(
|
|
|
357
467
|
return (id) => expFilter(id) && patternFilter(id);
|
|
358
468
|
}
|
|
359
469
|
|
|
470
|
+
/**
|
|
471
|
+
* evals 过滤器的指纹(进 ExperimentRunInfo.evalFilterFingerprint,供「配置没变」判断):
|
|
472
|
+
* 数组按内容、函数按函数体哈希;CLI 追加的位置参数前缀一并计入。不存过滤器本身——
|
|
473
|
+
* 求值结果在 selectedEvalIds(见 runEvals)。
|
|
474
|
+
*/
|
|
475
|
+
function fingerprintEvalsFilter(evals: DiscoveredExperiment["evals"], patterns: string[]): string {
|
|
476
|
+
const basis =
|
|
477
|
+
evals === undefined || evals === "*"
|
|
478
|
+
? "*"
|
|
479
|
+
: Array.isArray(evals)
|
|
480
|
+
? JSON.stringify([...evals].sort())
|
|
481
|
+
: evals.toString();
|
|
482
|
+
return createHash("sha256").update(JSON.stringify({ basis, patterns })).digest("hex").slice(0, 16);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* run 结束后把 coordinator 累计的诊断折成 `RunCompletion`(见 docs/feature/experiments/cli.md
|
|
487
|
+
* 「运行完成状态不只看 verdict 计数」)。只读已经真实发生过的诊断,不额外发明信号:
|
|
488
|
+
* - `"interrupted"` 诊断只在 run.ts 判定为真·中断(Effect exit 真实标记中断,不是「signal 被
|
|
489
|
+
* abort 过」这种更弱的信号)时才会出现,见 `runner/run.ts` 的 `reportInterrupted()` 调用点。
|
|
490
|
+
* - `"budget-exhausted:<experimentId>"` 诊断的 `count` 就是该 experiment 因预算耗尽未派发的
|
|
491
|
+
* attempt 数(见 `runner/feedback/reducer.ts` 对 `budget-exhausted` 事件的注释),跨
|
|
492
|
+
* experiment 求和得到 `unstarted`。
|
|
493
|
+
* - `"reporter-error:<reporter>"` 诊断转成 `ReporterError[]`;`required` 字段来自事件自带的
|
|
494
|
+
* `data.required`,直接反映这个 reporter 注册时的真实 required/best-effort 分类(见上面
|
|
495
|
+
* 构造 `reporters: ReporterRegistration[]` 的地方——artifacts / --json / --junit 恒
|
|
496
|
+
* `required: true`,`config.reporters` 恒 `false`),不是一个统一写死的占位值。
|
|
497
|
+
* - `earlyExitUnstarted` 从反馈状态的 attempt:early-exit 计数派生(减去 fail-fast 的那部分——
|
|
498
|
+
* 那是「未完整覆盖」,进 unstarted,不是「省下的重复验证」)。
|
|
499
|
+
*/
|
|
500
|
+
function assembleRunCompletion(state: RunFeedbackState): RunCompletion {
|
|
501
|
+
let unstarted = 0;
|
|
502
|
+
let failFastSkipped = 0;
|
|
503
|
+
let interrupted = false;
|
|
504
|
+
const reporterErrors: ReporterError[] = [];
|
|
505
|
+
for (const d of state.diagnostics) {
|
|
506
|
+
if (d.key === "interrupted") {
|
|
507
|
+
interrupted = true;
|
|
508
|
+
} else if (d.key.startsWith("budget-exhausted:")) {
|
|
509
|
+
unstarted += d.count;
|
|
510
|
+
} else if (d.key.startsWith("fail-fast:")) {
|
|
511
|
+
// run 级 fail-fast 造成的未派发同样计入 unstarted(结论落 incomplete,见
|
|
512
|
+
// docs/feature/experiments/architecture.md「Completion 与退出」)。
|
|
513
|
+
unstarted += d.count;
|
|
514
|
+
failFastSkipped += d.count;
|
|
515
|
+
} else if (d.key.startsWith("reporter-error:")) {
|
|
516
|
+
// required 决定这条错误是否写进 RunCompletion.reporterErrors 并让 completion 非 complete
|
|
517
|
+
// (见 docs/cli.md「required reporter」);best-effort reporter 的失败只保留为 diagnostic。
|
|
518
|
+
if (d.data?.required !== true) continue;
|
|
519
|
+
reporterErrors.push({
|
|
520
|
+
reporter: typeof d.data?.reporter === "string" ? d.data.reporter : d.key.slice("reporter-error:".length),
|
|
521
|
+
required: true,
|
|
522
|
+
message: d.message,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// 中断造成的未派发(仍在 queued 的 attempt)同样计入 unstarted(见 docs/feature/experiments/
|
|
527
|
+
// architecture.md「Completion 与退出」:budget 耗尽、fail-fast 或中断造成的未派发都不伪装成全绿)。
|
|
528
|
+
if (interrupted) unstarted += state.queued;
|
|
529
|
+
// attempt:early-exit 计数含 fail-fast 的未派发(反馈层同一事件驱动计数守恒);
|
|
530
|
+
// 「省下的重复验证」= 总数减去 fail-fast 那部分。
|
|
531
|
+
const earlyExitUnstarted = Math.max(0, state.earlyExitSkipped - failFastSkipped);
|
|
532
|
+
const status: CompletionStatus = interrupted
|
|
533
|
+
? "interrupted"
|
|
534
|
+
: unstarted > 0 || reporterErrors.length > 0
|
|
535
|
+
? "incomplete"
|
|
536
|
+
: "complete";
|
|
537
|
+
return { status, unstarted, earlyExitUnstarted, reporterErrors };
|
|
538
|
+
}
|
|
539
|
+
|
|
360
540
|
/** package.json 的 version 字段;-v/--version 直接回显这个号。 */
|
|
361
541
|
async function packageVersion(): Promise<string> {
|
|
362
542
|
const raw = await readFile(new URL("../package.json", import.meta.url), "utf-8");
|
|
@@ -394,7 +574,7 @@ async function main(): Promise<void> {
|
|
|
394
574
|
...(flags.report !== undefined ? { report: { path: flags.report, cwd } } : {}),
|
|
395
575
|
};
|
|
396
576
|
if (flags.out) {
|
|
397
|
-
const out = await buildView({ input: viewInput.input, out: flags.out, scan }).catch(exitOnViewUserError);
|
|
577
|
+
const out = await buildView({ input: viewInput.input, out: flags.out, allowSensitiveArtifacts: flags.allowSensitiveArtifacts, scan }).catch(exitOnViewUserError);
|
|
398
578
|
process.stdout.write(t("cli.view.exportedDir", { out }));
|
|
399
579
|
process.exit(0);
|
|
400
580
|
}
|
|
@@ -410,11 +590,26 @@ async function main(): Promise<void> {
|
|
|
410
590
|
await new Promise(() => {});
|
|
411
591
|
}
|
|
412
592
|
|
|
593
|
+
if (command === "sandbox") {
|
|
594
|
+
// sandbox 命令组不读 niceeval.config.ts、不发现 eval:只操作留存注册表与 provider 的
|
|
595
|
+
// detached 能力(见 docs/feature/sandbox/cli.md)。
|
|
596
|
+
const { runSandboxCommand } = await import("./sandbox/cli-commands.ts");
|
|
597
|
+
const code = await runSandboxCommand(cwd, positionals, {
|
|
598
|
+
all: flags.all,
|
|
599
|
+
window: flags.window,
|
|
600
|
+
path: flags.sandboxPath,
|
|
601
|
+
leaveRunning: flags.leaveRunning,
|
|
602
|
+
run: flags.run,
|
|
603
|
+
});
|
|
604
|
+
process.exit(code);
|
|
605
|
+
}
|
|
606
|
+
|
|
413
607
|
if (command === "show") {
|
|
414
608
|
// show 不依赖 niceeval.config.ts:读的是 .niceeval/(或 --run 指定的某个快照目录)的落盘结果。
|
|
415
609
|
const code = await runShow(cwd, positionals, {
|
|
416
610
|
eval: flags.eval,
|
|
417
611
|
execution: flags.execution,
|
|
612
|
+
timing: flags.timing,
|
|
418
613
|
diff: flags.diff,
|
|
419
614
|
diffPath: flags.diffPath,
|
|
420
615
|
history: flags.history,
|
|
@@ -453,15 +648,26 @@ async function main(): Promise<void> {
|
|
|
453
648
|
}
|
|
454
649
|
|
|
455
650
|
const agentRuns: AgentRun[] = [];
|
|
651
|
+
let experimentSelection = t("cli.all");
|
|
652
|
+
let availableExperimentGroups = t("cli.none");
|
|
456
653
|
|
|
457
654
|
if (command === "exp") {
|
|
458
655
|
if (flags.agent || flags.model) {
|
|
459
656
|
process.stderr.write(t("cli.exp.agentModelFlagUnsupported"));
|
|
460
657
|
process.exit(1);
|
|
461
658
|
}
|
|
659
|
+
const viewerFlag = firstViewerOnlyFlag(flags);
|
|
660
|
+
if (viewerFlag) {
|
|
661
|
+
process.stderr.write(t("cli.exp.viewerFlagUnsupported", { flag: viewerFlag.flag, command: viewerFlag.command }));
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
462
664
|
const experiments = await discoverExperiments(cwd);
|
|
463
665
|
const expArg = positionals[0];
|
|
464
666
|
const extraPatterns = positionals.slice(1);
|
|
667
|
+
experimentSelection = positionals.join(" ") || t("cli.all");
|
|
668
|
+
availableExperimentGroups = [...new Set(experiments.map((experiment) => experiment.group || experiment.id))]
|
|
669
|
+
.sort()
|
|
670
|
+
.join(", ") || t("cli.none");
|
|
465
671
|
const selected = expArg
|
|
466
672
|
? experiments.filter((e) => e.group === expArg || e.id === expArg || e.id.startsWith(expArg + "/"))
|
|
467
673
|
: experiments;
|
|
@@ -470,8 +676,21 @@ async function main(): Promise<void> {
|
|
|
470
676
|
arg: expArg ?? t("cli.all"),
|
|
471
677
|
experiments: experiments.map((e) => e.id).join(", ") || t("cli.none"),
|
|
472
678
|
}));
|
|
679
|
+
// show / view 是顶层命令。只有同名 experiment 确实不存在时才纠错,不能抢占合法 id。
|
|
680
|
+
if (expArg === "show" || expArg === "view") {
|
|
681
|
+
process.stderr.write(t("cli.experiment.viewerCommandHint", {
|
|
682
|
+
command: expArg,
|
|
683
|
+
args: extraPatterns.length > 0 ? ` ${extraPatterns.join(" ")}` : "",
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
473
686
|
process.exit(1);
|
|
474
687
|
}
|
|
688
|
+
// 残留提醒:注册表里还有上次留下的沙箱时打一行(不阻塞、不清理)。
|
|
689
|
+
{
|
|
690
|
+
const { keptSandboxReminder } = await import("./sandbox/cli-commands.ts");
|
|
691
|
+
const reminder = await keptSandboxReminder(cwd).catch(() => undefined);
|
|
692
|
+
if (reminder) process.stderr.write(reminder);
|
|
693
|
+
}
|
|
475
694
|
for (const exp of selected) {
|
|
476
695
|
// 一个实验 = 一个配置(单 model)。跨模型对比写多个实验文件,各钉一个 model。
|
|
477
696
|
agentRuns.push({
|
|
@@ -486,6 +705,8 @@ async function main(): Promise<void> {
|
|
|
486
705
|
budget: flags.budget ?? envNumber("NICEEVAL_BUDGET") ?? exp.budget,
|
|
487
706
|
evalFilter: evalsFilterFromExperiment(exp.evals, extraPatterns),
|
|
488
707
|
experimentId: exp.id,
|
|
708
|
+
description: exp.description,
|
|
709
|
+
evalFilterFingerprint: fingerprintEvalsFilter(exp.evals, extraPatterns),
|
|
489
710
|
strict: flags.strict,
|
|
490
711
|
// 实验级并发上限:随 AgentRun 进调度器按实验单独限流(runner 两级信号量),
|
|
491
712
|
// 不再取所有选中实验的最小值钳全局——那会让一个串行实验拖慢整批基线。
|
|
@@ -513,69 +734,119 @@ async function main(): Promise<void> {
|
|
|
513
734
|
process.exit(1);
|
|
514
735
|
}
|
|
515
736
|
|
|
737
|
+
// profile 只改变反馈,不改变选择/调度/判定;显式值覆盖 auto 检测(见 resolveOutputProfile)。
|
|
738
|
+
// --dry 和真正开跑共用同一个已解析 profile。
|
|
739
|
+
const outputProfile = resolveOutputProfile({
|
|
740
|
+
explicit: flags.output,
|
|
741
|
+
isTTY: process.stderr.isTTY === true,
|
|
742
|
+
env: process.env,
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// matchedByRun[i] 对应 agentRuns[i] 匹配到的 eval 集合;--dry 预览与真正开跑时的
|
|
746
|
+
// RunFeedbackPlan(总量、去重 eval 数)共用同一份计算,不重复过滤一遍。
|
|
747
|
+
const matchedByRun = agentRuns.map((run) => evals.filter((e) => run.evalFilter(e.id)));
|
|
748
|
+
const totalRuns = agentRuns.reduce((sum, run, i) => sum + matchedByRun[i]!.length * run.runs, 0);
|
|
749
|
+
const uniqueEvalIds = new Set(matchedByRun.flat().map((e) => e.id));
|
|
750
|
+
|
|
751
|
+
if (totalRuns === 0) {
|
|
752
|
+
process.stderr.write(t("cli.experiment.noEvalsSelected", {
|
|
753
|
+
selection: experimentSelection,
|
|
754
|
+
experiments: availableExperimentGroups,
|
|
755
|
+
}));
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
758
|
+
|
|
516
759
|
if (flags.dry) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
760
|
+
// --dry 只按所选 profile 打印计划,不运行、不落盘 —— 三种 profile 各自的展示逻辑
|
|
761
|
+
// 都在 runner/feedback/{human,agent,ci}.ts 里,这里只负责拼数据、选函数、写流、退出。
|
|
762
|
+
if (outputProfile === "agent") {
|
|
763
|
+
const rows: AgentPlanRow[] = [];
|
|
764
|
+
for (let i = 0; i < agentRuns.length; i++) {
|
|
765
|
+
const run = agentRuns[i]!;
|
|
766
|
+
const label = run.experimentId ?? (run.model ? `${run.agent.name}/${run.model}` : run.agent.name);
|
|
767
|
+
for (const e of matchedByRun[i]!) rows.push({ label, evalId: e.id });
|
|
768
|
+
}
|
|
769
|
+
process.stdout.write(
|
|
770
|
+
renderAgentPlanEnvelope({
|
|
771
|
+
total: totalRuns,
|
|
772
|
+
evals: uniqueEvalIds.size,
|
|
773
|
+
configs: agentRuns.length,
|
|
774
|
+
runs: Math.max(1, ...agentRuns.map((r) => r.runs)),
|
|
775
|
+
rows,
|
|
776
|
+
}) + "\n",
|
|
777
|
+
);
|
|
778
|
+
} else if (outputProfile === "ci") {
|
|
779
|
+
process.stdout.write(
|
|
780
|
+
renderCiDryPlan({
|
|
781
|
+
total: totalRuns,
|
|
782
|
+
evals: uniqueEvalIds.size,
|
|
783
|
+
configs: agentRuns.length,
|
|
784
|
+
rows: agentRuns.map((run, i) => ({
|
|
785
|
+
experimentId: run.experimentId,
|
|
786
|
+
who: run.model ? `${run.agent.name}/${run.model}` : run.agent.name,
|
|
787
|
+
evalIds: matchedByRun[i]!.map((e) => e.id),
|
|
788
|
+
runs: run.runs,
|
|
789
|
+
})),
|
|
790
|
+
}),
|
|
791
|
+
);
|
|
792
|
+
} else {
|
|
793
|
+
process.stdout.write(
|
|
794
|
+
renderHumanDryPlan({
|
|
795
|
+
evals: evals.length,
|
|
796
|
+
configs: agentRuns.length,
|
|
797
|
+
rows: agentRuns.map((run, i) => ({
|
|
798
|
+
who: run.model ? `${run.agent.name}/${run.model}` : run.agent.name,
|
|
799
|
+
experimentSuffix: run.experimentId ? ` (exp ${run.experimentId})` : "",
|
|
800
|
+
evalIds: matchedByRun[i]!.map((e) => e.id),
|
|
801
|
+
runs: run.runs,
|
|
802
|
+
})),
|
|
803
|
+
}),
|
|
804
|
+
);
|
|
527
805
|
}
|
|
528
806
|
process.exit(0);
|
|
529
807
|
}
|
|
530
808
|
|
|
531
|
-
//
|
|
532
|
-
// (
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
// 显示的"携入"和 run.ts 实际调度的"携入"就会对不上。
|
|
809
|
+
// 提前算好携入计划:coordinator 的 plan 事件与 runEvals 内部
|
|
810
|
+
// 实际调度必须共用同一份 planCarry() 判断,否则两边各自算一遍,一旦不一致,dashboard/
|
|
811
|
+
// envelope 展示的"携入"就会和 run.ts 真实调度的"携入"对不上(见 memory 的
|
|
812
|
+
// live-carry-row-shows-waiting-forever)。
|
|
536
813
|
const priorResults = flags.force ? undefined : await loadLatestResultsPerEval(join(cwd, ".niceeval"));
|
|
537
814
|
const carryPlan = priorResults?.length ? await planCarry(evals, agentRuns, priorResults) : undefined;
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
815
|
+
const reusedFailures = (carryPlan?.carriedResults ?? [])
|
|
816
|
+
.map(failureDetailFromResult)
|
|
817
|
+
.filter((failure) => failure !== undefined);
|
|
542
818
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const artifacts = ArtifactsReporter();
|
|
575
|
-
reporters.push( artifacts);
|
|
576
|
-
if (flags.junit) reporters.push(JUnit(flags.junit));
|
|
577
|
-
if (flags.json) reporters.push(Json(flags.json));
|
|
578
|
-
reporters.push(...(config.reporters ?? []));
|
|
819
|
+
// 无全局默认:并发上限由 sandbox provider 的推荐值决定(多个 agentRun 各有 sandbox 时取
|
|
820
|
+
// 最小值,最保守的 provider 决定上限)。同一个值既进 RunFeedbackPlan.shape,也传给 runEvals——
|
|
821
|
+
// 两处必须是同一个数字,dashboard 展示的并发上限不能和真实调度的并发上限对不上。
|
|
822
|
+
const sandboxRecs = agentRuns.map((r) => sandboxRecommendedConcurrency(r.sandbox));
|
|
823
|
+
const sandboxDefaultConcurrency = sandboxRecs.length > 0 ? Math.min(...sandboxRecs) : 10;
|
|
824
|
+
const maxConcurrency =
|
|
825
|
+
flags.maxConcurrency ??
|
|
826
|
+
envNumber("NICEEVAL_MAX_CONCURRENCY") ??
|
|
827
|
+
config.maxConcurrency ??
|
|
828
|
+
sandboxDefaultConcurrency;
|
|
829
|
+
|
|
830
|
+
const plan: RunFeedbackPlan = {
|
|
831
|
+
shape: { evals: uniqueEvalIds.size, configs: agentRuns.length, totalRuns, maxConcurrency },
|
|
832
|
+
reused: carryPlan?.carriedResults.length ?? 0,
|
|
833
|
+
reusedFailures,
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
// 一个 run 内只有一个终端协调者(见 docs/feature/experiments/cli.md「输出流和落盘节奏」):
|
|
837
|
+
// 三种 profile 各自的展示逻辑全部在 renderer 里,这里只按解析出的 profile 选一个构造好、
|
|
838
|
+
// 交给 coordinator。run:start 前(coordinator.start(plan) 之前)的一切都还没有活跃 sink,
|
|
839
|
+
// 出错走 bootstrap stderr;之后所有诊断都经它。
|
|
840
|
+
const io = createNodeFeedbackIO();
|
|
841
|
+
const commandLabel = ["niceeval", command, ...positionals].join(" ").trim();
|
|
842
|
+
const renderer =
|
|
843
|
+
outputProfile === "human"
|
|
844
|
+
? createHumanRenderer({ io, command: commandLabel })
|
|
845
|
+
: outputProfile === "agent"
|
|
846
|
+
? createAgentRenderer({ io })
|
|
847
|
+
: createCiRenderer({ io });
|
|
848
|
+
const coordinator = createFeedbackCoordinator({ profile: outputProfile, renderer, io });
|
|
849
|
+
coordinator.start(plan);
|
|
579
850
|
|
|
580
851
|
// Ctrl+C / kill 的三级响应,核心目标:任何情况下都不留下孤儿沙箱。
|
|
581
852
|
// 1 次:abort controller → runEvals 把它喂给 Effect signal → 各 attempt 的 Scope 跑 release
|
|
@@ -586,28 +857,29 @@ async function main(): Promise<void> {
|
|
|
586
857
|
// 3 次:真不耐烦了,硬退(此时多半已无可清理的)。
|
|
587
858
|
const ctrl = new AbortController();
|
|
588
859
|
let signalCount = 0;
|
|
589
|
-
// 兜底强清 + 退出:只跑一次,带超时(stopAllSandboxes 内每个 stop 各自有超时)
|
|
860
|
+
// 兜底强清 + 退出:只跑一次,带超时(stopAllSandboxes 内每个 stop 各自有超时)。先停 dashboard
|
|
861
|
+
// 的 tick/动态区域(coordinator.stopDynamic()),避免硬退时终端卡在半帧 ANSI 状态。
|
|
590
862
|
let forcing = false;
|
|
591
863
|
const forceCleanupAndExit = (code: number) => {
|
|
592
864
|
if (forcing) return;
|
|
593
865
|
forcing = true;
|
|
594
|
-
void stopAllSandboxes().finally(() => process.exit(code));
|
|
866
|
+
void Promise.all([coordinator.stopDynamic(), stopAllSandboxes()]).finally(() => process.exit(code));
|
|
595
867
|
};
|
|
596
868
|
for (const sig of ["SIGINT", "SIGTERM"] as const) {
|
|
597
869
|
process.on(sig, () => {
|
|
598
870
|
signalCount += 1;
|
|
599
871
|
if (signalCount === 1) {
|
|
600
|
-
|
|
872
|
+
reportActivity(t("cli.interruptCleanup").trimEnd());
|
|
601
873
|
ctrl.abort();
|
|
602
874
|
// 看门狗:graceful 清理 12s 还没让进程自己收口,就强清兜底。
|
|
603
875
|
setTimeout(() => {
|
|
604
876
|
if (liveSandboxCount() > 0) {
|
|
605
|
-
|
|
877
|
+
reportActivity(t("cli.fallbackCleanupTimeout").trimEnd());
|
|
606
878
|
forceCleanupAndExit(130);
|
|
607
879
|
}
|
|
608
880
|
}, 12_000).unref();
|
|
609
881
|
} else if (signalCount === 2) {
|
|
610
|
-
|
|
882
|
+
reportActivity(t("cli.forceCleanupExit").trimEnd());
|
|
611
883
|
forceCleanupAndExit(130);
|
|
612
884
|
} else {
|
|
613
885
|
process.exit(130); // 第三次:硬退
|
|
@@ -615,47 +887,78 @@ async function main(): Promise<void> {
|
|
|
615
887
|
});
|
|
616
888
|
}
|
|
617
889
|
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
sandboxDefaultConcurrency,
|
|
633
|
-
signal: ctrl.signal,
|
|
634
|
-
onProgress,
|
|
635
|
-
priorResults,
|
|
636
|
-
carryPlan,
|
|
890
|
+
// reporter 只剩正交的机器/artifact 出口:human/agent/ci 的展示完全由上面的 coordinator +
|
|
891
|
+
// renderer 负责,不再有 Console/Live/Quiet 这类兼职当 reporter 的展示层(见 docs 的
|
|
892
|
+
// 「CLI 只负责解析 profile、构造 coordinator/reporters、运行和退出」)。每个 reporter 在这里
|
|
893
|
+
// 按来源分类 required/best-effort(见 `ReporterRegistration` 的字段注释):默认落盘的
|
|
894
|
+
// artifacts、显式指定的 --json/--junit 是 agent/CI 读结果的唯一入口,写失败必须让
|
|
895
|
+
// completion/退出码判红;用户 `config.reporters` 只是补充观测,失败只折成一条 diagnostic,
|
|
896
|
+
// 不影响 completion。
|
|
897
|
+
const reporters: ReporterRegistration[] = [];
|
|
898
|
+
const artifacts = ArtifactsReporter();
|
|
899
|
+
reporters.push({ reporter: artifacts, name: "artifacts", required: true });
|
|
900
|
+
if (flags.junit) reporters.push({ reporter: JUnit(flags.junit), name: "junit", required: true, target: flags.junit });
|
|
901
|
+
if (flags.json) reporters.push({ reporter: Json(flags.json), name: "json", required: true, target: flags.json });
|
|
902
|
+
(config.reporters ?? []).forEach((reporter, i) => {
|
|
903
|
+
reporters.push({ reporter, name: `config-reporter-${i}`, required: false });
|
|
637
904
|
});
|
|
638
905
|
|
|
906
|
+
let summary: RunSummary;
|
|
907
|
+
try {
|
|
908
|
+
summary = await runEvals({
|
|
909
|
+
config,
|
|
910
|
+
evals,
|
|
911
|
+
agentRuns,
|
|
912
|
+
reporters,
|
|
913
|
+
maxConcurrency,
|
|
914
|
+
signal: ctrl.signal,
|
|
915
|
+
priorResults,
|
|
916
|
+
carryPlan,
|
|
917
|
+
keepSandbox: flags.keepSandbox,
|
|
918
|
+
niceevalRoot: resolvePath(cwd, ".niceeval"),
|
|
919
|
+
});
|
|
920
|
+
} catch (e) {
|
|
921
|
+
// 真崩溃前先撤下 dashboard,不让半帧 ANSI 状态和下面 main().catch 打印的错误交织。
|
|
922
|
+
await coordinator.stopDynamic();
|
|
923
|
+
throw e;
|
|
924
|
+
}
|
|
925
|
+
|
|
639
926
|
// 正常返回(含被中断后走部分汇总)后再兜一刀:Scope finalizer 没停掉的残留沙箱在这里强清。
|
|
640
927
|
// 跑顺利时登记表已空,是 no-op。
|
|
641
928
|
await stopAllSandboxes();
|
|
642
929
|
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
//
|
|
647
|
-
|
|
930
|
+
// completion 要先算好,--json/--junit 是否"这次真的写出"才有依据(见下)。
|
|
931
|
+
const completion = assembleRunCompletion(coordinator.state);
|
|
932
|
+
|
|
933
|
+
// --json/--junit 是正交机器出口,只在这次运行真的写出对应文件时才把路径交给 coordinator
|
|
934
|
+
// (它转发给 ci renderer 打印独立的 json=/junit= 行,见 docs「CI 怎么用」)。判据是
|
|
935
|
+
// completion.reporterErrors 里有没有这次 required reporter("json"/"junit")的失败记录——
|
|
936
|
+
// 不能用 existsSync 探测磁盘:atomicWriteFile(json.ts)失败时原地保留上一次运行遗留的旧文件,
|
|
937
|
+
// existsSync 只会看到"文件存在"就误判成这次写成功,把上一轮的陈旧内容当成本次结果打印出去。
|
|
938
|
+
const jsonPath = flags.json && !completion.reporterErrors.some((e) => e.reporter === "json") ? flags.json : undefined;
|
|
939
|
+
const junitPath =
|
|
940
|
+
flags.junit && !completion.reporterErrors.some((e) => e.reporter === "junit") ? flags.junit : undefined;
|
|
941
|
+
|
|
942
|
+
// agent 反馈闭环的入口:跑完直接给出每个已创建快照的目录,agent/ci 读 snapshot.json 与各
|
|
943
|
+
// attempt 的 result.json / artifact(events/trace/diff),不必解析人类向的流式输出。相对 cwd
|
|
944
|
+
// 的路径更友好;结果落在 cwd 外时(relative 路径以 .. 开头)原样打印绝对路径。打印本身由
|
|
945
|
+
// renderer 的 "saved" 处理完成,这里只负责把路径交给 coordinator。
|
|
946
|
+
const paths = artifacts.outputDirs().map(({ dir }) => {
|
|
648
947
|
const rel = relative(cwd, dir);
|
|
649
|
-
|
|
650
|
-
}
|
|
948
|
+
return rel && !rel.startsWith("..") ? rel : dir;
|
|
949
|
+
});
|
|
651
950
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
951
|
+
await coordinator.finish({ summary, completion, paths, json: jsonPath, junit: junitPath });
|
|
952
|
+
|
|
953
|
+
// 退出码统一走 CompletionStatus 驱动的语义(interrupted → 130、incomplete/required reporter
|
|
954
|
+
// 失败 → 1),不再只看 verdict 计数;三种 profile 共用同一套退出码,不是 ci 专属。failed/errored
|
|
955
|
+
// 先按 (experiment, eval) 折叠再喂给 computeCiExitCode——它只认 RunSummary 原始字段,不知道
|
|
956
|
+
// 「同一 eval 的重试轮不该重复计红」这条 eval 级判定规则(被 runs+earlyExit 重试吸收的失败,
|
|
957
|
+
// 先挂一次、后来过了,不该把进程判红,否则 CI 判定与 evalLevelStats 报表口径不一致;
|
|
958
|
+
// 见 memory 的 cli-exit-code-attempt-level-not-eval-level)。
|
|
959
|
+
const foldedStats = evalLevelStats(summary.results, (r) => `${r.experimentId ?? ""}|${r.id}`);
|
|
960
|
+
const exitCode = computeCiExitCode({ ...summary, failed: foldedStats.failed, errored: foldedStats.errored }, completion);
|
|
961
|
+
process.exit(exitCode);
|
|
659
962
|
}
|
|
660
963
|
|
|
661
964
|
main().catch(async (e) => {
|