niceeval 0.5.4 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INDEX.md +55 -0
- package/dist/agents/types.d.ts +321 -0
- package/dist/context/types.d.ts +335 -0
- package/dist/i18n/core.d.ts +11 -0
- package/dist/i18n/en.d.ts +181 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/zh-CN.d.ts +183 -0
- package/dist/o11y/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -0
- package/dist/o11y/otlp/turn-otel.d.ts +43 -0
- package/dist/o11y/types.d.ts +190 -0
- package/dist/report/aggregate.d.ts +57 -0
- package/dist/report/aggregate.js +212 -0
- package/dist/report/built-ins/experiment-comparison.d.ts +1 -0
- package/dist/report/built-ins/experiment-comparison.js +13 -0
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +2 -0
- package/dist/report/components.d.ts +180 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +600 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +26 -0
- package/dist/report/format.js +109 -0
- package/dist/report/index.d.ts +19 -0
- package/dist/report/index.js +30 -0
- package/dist/report/load.d.ts +12 -0
- package/dist/report/load.js +56 -0
- package/dist/report/locale.d.ts +94 -0
- package/dist/report/locale.js +183 -0
- package/dist/report/metrics.d.ts +21 -0
- package/dist/report/metrics.js +115 -0
- package/dist/report/primitives.d.ts +64 -0
- package/dist/report/primitives.js +113 -0
- package/dist/report/react/AttemptList.d.ts +23 -0
- package/dist/report/react/AttemptList.js +19 -0
- package/dist/report/react/DeltaTable.d.ts +8 -0
- package/dist/report/react/DeltaTable.js +24 -0
- package/dist/report/react/EvalList.d.ts +10 -0
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentList.d.ts +11 -0
- package/dist/report/react/ExperimentList.js +67 -0
- package/dist/report/react/GroupSummary.d.ts +8 -0
- package/dist/report/react/GroupSummary.js +8 -0
- package/dist/report/react/MetricBars.d.ts +10 -0
- package/dist/report/react/MetricBars.js +48 -0
- package/dist/report/react/MetricLine.d.ts +9 -0
- package/dist/report/react/MetricLine.js +68 -0
- package/dist/report/react/MetricMatrix.d.ts +10 -0
- package/dist/report/react/MetricMatrix.js +23 -0
- package/dist/report/react/MetricScatter.d.ts +9 -0
- package/dist/report/react/MetricScatter.js +96 -0
- package/dist/report/react/MetricTable.d.ts +11 -0
- package/dist/report/react/MetricTable.js +26 -0
- package/dist/report/react/RunOverview.d.ts +8 -0
- package/dist/report/react/RunOverview.js +12 -0
- package/dist/report/react/Scoreboard.d.ts +8 -0
- package/dist/report/react/Scoreboard.js +26 -0
- package/dist/report/react/cell.d.ts +9 -0
- package/dist/report/react/cell.js +9 -0
- package/dist/report/react/chart-math.d.ts +14 -0
- package/dist/report/react/chart-math.js +68 -0
- package/dist/report/react/colors.d.ts +14 -0
- package/dist/report/react/colors.js +45 -0
- package/dist/report/react/fixtures.d.ts +19 -0
- package/dist/report/react/fixtures.js +398 -0
- package/dist/report/react/format.d.ts +3 -0
- package/dist/report/react/format.js +7 -0
- package/dist/report/react/index.d.ts +6 -0
- package/dist/report/react/index.js +14 -0
- package/dist/report/report.d.ts +24 -0
- package/dist/report/report.js +49 -0
- package/dist/report/text/faces.d.ts +29 -0
- package/dist/report/text/faces.js +541 -0
- package/dist/report/text/layout.d.ts +75 -0
- package/dist/report/text/layout.js +173 -0
- package/dist/report/text/plot.d.ts +37 -0
- package/dist/report/text/plot.js +81 -0
- package/dist/report/text/table.d.ts +8 -0
- package/dist/report/text/table.js +86 -0
- package/dist/report/tree.d.ts +99 -0
- package/dist/report/tree.js +228 -0
- package/dist/report/types.d.ts +408 -0
- package/dist/report/types.js +4 -0
- package/dist/report/web.d.ts +15 -0
- package/dist/report/web.js +37 -0
- package/dist/results/locator.d.ts +75 -0
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +45 -0
- package/dist/results/select.js +230 -0
- package/dist/results/source-hash.d.ts +9 -0
- package/dist/results/types.d.ts +204 -0
- package/dist/runner/eval-source.d.ts +22 -0
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +332 -0
- package/dist/sandbox/docker-stream.d.ts +24 -0
- package/dist/sandbox/docker.d.ts +84 -0
- package/dist/sandbox/e2b.d.ts +31 -0
- package/dist/sandbox/errors.d.ts +4 -0
- package/dist/sandbox/local-files.d.ts +6 -0
- package/dist/sandbox/paths.d.ts +4 -0
- package/dist/sandbox/registry.d.ts +13 -0
- package/dist/sandbox/resolve.d.ts +40 -0
- package/dist/sandbox/retry.d.ts +10 -0
- package/dist/sandbox/shell.d.ts +11 -0
- package/dist/sandbox/source-files.d.ts +13 -0
- package/dist/sandbox/types.d.ts +203 -0
- package/dist/sandbox/vercel.d.ts +37 -0
- package/dist/scoring/types.d.ts +70 -0
- package/dist/shared/aggregate.d.ts +29 -0
- package/dist/shared/aggregate.js +55 -0
- package/dist/shared/types.d.ts +27 -0
- package/dist/shared/verdict.d.ts +27 -0
- package/dist/shared/verdict.js +34 -0
- package/dist/tty-line.d.ts +6 -0
- package/dist/types.d.ts +7 -0
- package/dist/util.d.ts +22 -0
- package/docs-site/zh/concepts/adapter.mdx +7 -7
- package/docs-site/zh/concepts/assert.mdx +19 -18
- package/docs-site/zh/concepts/evals.mdx +12 -11
- package/docs-site/zh/concepts/experiment.mdx +2 -2
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +3 -3
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
- package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +237 -78
- package/docs-site/zh/guides/connect-otel.mdx +3 -3
- package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
- package/docs-site/zh/guides/custom-reports.mdx +196 -36
- package/docs-site/zh/guides/experiments.mdx +7 -1
- package/docs-site/zh/guides/fixtures.mdx +4 -2
- package/docs-site/zh/guides/official-adapters.mdx +31 -9
- package/docs-site/zh/guides/publish-report.mdx +4 -4
- package/docs-site/zh/guides/report-components.mdx +194 -55
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +61 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +3 -2
- package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +12 -12
- package/docs-site/zh/guides/viewing-results.mdx +200 -79
- package/docs-site/zh/guides/write-experiment.mdx +9 -1
- package/docs-site/zh/guides/write-send.mdx +4 -3
- package/docs-site/zh/index.mdx +4 -3
- package/docs-site/zh/introduction.mdx +2 -1
- package/docs-site/zh/quickstart.mdx +5 -3
- package/docs-site/zh/reference/builtin-agents.mdx +38 -13
- package/docs-site/zh/reference/capabilities.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +36 -13
- package/docs-site/zh/reference/define-agent.mdx +15 -13
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/docs-site/zh/reference/events.mdx +9 -3
- package/docs-site/zh/reference/expect.mdx +13 -8
- package/package.json +10 -6
- package/src/agents/bub.ts +129 -42
- package/src/agents/claude-code.test.ts +152 -0
- package/src/agents/claude-code.ts +127 -13
- package/src/agents/codex.test.ts +192 -0
- package/src/agents/codex.ts +151 -12
- package/src/agents/index.ts +9 -3
- package/src/agents/manifest.ts +41 -0
- package/src/agents/plugin-config.test.ts +94 -0
- package/src/agents/shared.ts +33 -2
- package/src/agents/skills.test.ts +214 -0
- package/src/agents/skills.ts +304 -0
- package/src/agents/streaming.ts +5 -5
- package/src/agents/types.ts +59 -3
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +23 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +8 -8
- package/src/define.ts +11 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +33 -15
- package/src/i18n/zh-CN.ts +37 -21
- package/src/index.ts +1 -1
- package/src/o11y/execution-tree.test.ts +452 -0
- package/src/o11y/execution-tree.ts +367 -0
- package/src/o11y/otlp/receiver.ts +1 -1
- package/src/o11y/otlp/select.ts +7 -3
- package/src/o11y/parsers/claude-code.test.ts +175 -0
- package/src/o11y/parsers/claude-code.ts +32 -0
- package/src/o11y/types.ts +6 -1
- package/src/report/aggregate.ts +26 -20
- package/src/report/built-in-user-parity.test.tsx +640 -0
- package/src/report/built-ins/experiment-comparison.tsx +19 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +225 -50
- package/src/report/compute.ts +335 -54
- package/src/report/dual-render.test.tsx +1129 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +35 -2
- package/src/report/index.ts +47 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +229 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +113 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +220 -0
- package/src/report/react/GroupSummary.tsx +66 -0
- package/src/report/react/MetricBars.tsx +24 -13
- package/src/report/react/MetricLine.tsx +21 -21
- package/src/report/react/MetricMatrix.tsx +8 -4
- package/src/report/react/MetricScatter.tsx +126 -96
- package/src/report/react/MetricTable.tsx +115 -15
- package/src/report/react/RunOverview.tsx +48 -26
- package/src/report/react/Scoreboard.tsx +15 -7
- package/src/report/react/cell.tsx +18 -9
- package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
- package/src/report/react/colors.ts +24 -15
- package/src/report/react/enhance.js +168 -0
- package/src/report/react/fixtures.ts +169 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +21 -6
- package/src/report/react/render.test.tsx +180 -31
- package/src/report/react/styles.css +432 -213
- package/src/report/report.test.ts +398 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +344 -96
- package/src/report/text/layout.ts +76 -11
- package/src/report/text/table.ts +98 -0
- package/src/report/tree.ts +103 -20
- package/src/report/types.ts +184 -25
- package/src/report/web.ts +46 -19
- package/src/results/annotated-source.test.ts +146 -0
- package/src/results/annotated-source.ts +128 -0
- package/src/results/attempt-evidence.test.ts +158 -0
- package/src/results/attempt-evidence.ts +158 -0
- package/src/results/attempt-source.ts +48 -0
- package/src/results/copy.ts +154 -139
- package/src/results/format.ts +54 -47
- package/src/results/host-equivalence.test.ts +527 -0
- package/src/results/index.ts +56 -14
- package/src/results/locator.test.ts +198 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +392 -175
- package/src/results/results.test.ts +943 -494
- package/src/results/select.ts +129 -23
- package/src/results/skipped-notice.ts +0 -0
- package/src/results/source-hash.ts +28 -0
- package/src/results/types.ts +98 -62
- package/src/results/writer.ts +304 -176
- package/src/runner/attempt.test.ts +170 -0
- package/src/runner/attempt.ts +48 -13
- package/src/runner/discover.test.ts +65 -0
- package/src/runner/discover.ts +6 -2
- package/src/runner/eval-source.test.ts +121 -0
- package/src/runner/eval-source.ts +45 -0
- package/src/runner/fingerprint.ts +54 -1
- package/src/runner/remote-sandbox.ts +1 -1
- package/src/runner/report.test.ts +6 -8
- package/src/runner/report.ts +5 -4
- package/src/runner/reporters/artifacts.ts +27 -41
- package/src/runner/reporters/braintrust.test.ts +3 -3
- package/src/runner/reporters/braintrust.ts +1 -1
- package/src/runner/reporters/console.ts +6 -6
- package/src/runner/reporters/json.ts +4 -4
- package/src/runner/reporters/live.test.ts +56 -0
- package/src/runner/reporters/live.ts +36 -10
- package/src/runner/reporters/quiet.test.ts +66 -0
- package/src/runner/reporters/quiet.ts +49 -0
- package/src/runner/reporters/shared.ts +5 -5
- package/src/runner/reporters/table.ts +17 -17
- package/src/runner/run.ts +61 -102
- package/src/runner/types.ts +43 -32
- package/src/sandbox/checkpoint.ts +1 -1
- package/src/sandbox/docker.ts +15 -2
- package/src/sandbox/e2b.ts +11 -5
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +3 -3
- package/src/sandbox/local-files.ts +1 -1
- package/src/sandbox/registry.ts +6 -5
- package/src/sandbox/resolve.ts +38 -23
- package/src/sandbox/retry.test.ts +68 -0
- package/src/sandbox/retry.ts +44 -0
- package/src/sandbox/types.ts +24 -24
- package/src/sandbox/vercel.ts +17 -3
- package/src/scoring/collector.ts +3 -3
- package/src/scoring/judge.ts +2 -2
- package/src/scoring/scoped.ts +13 -1
- package/src/scoring/types.ts +7 -7
- package/src/scoring/verdict.ts +4 -4
- package/src/shared/aggregate.ts +23 -3
- package/src/shared/types.ts +2 -2
- package/src/shared/{outcome.ts → verdict.ts} +15 -15
- package/src/show/compose.ts +12 -132
- package/src/show/index.ts +119 -95
- package/src/show/render.ts +462 -159
- package/src/show/show.test.ts +471 -253
- package/src/source-loc.ts +1 -1
- package/src/tty-line.ts +32 -0
- package/src/types.ts +2 -2
- package/src/util.test.ts +21 -1
- package/src/util.ts +5 -1
- package/src/view/app/App.tsx +33 -198
- package/src/view/app/components/AttemptModal.tsx +6 -6
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/CopyControls.tsx +32 -36
- package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
- package/src/view/app/components/ui/badge.tsx +1 -1
- package/src/view/app/components/ui/dialog.tsx +2 -2
- package/src/view/app/i18n.ts +40 -31
- package/src/view/app/lib/artifact-url.ts +1 -1
- package/src/view/app/lib/attempt-route.test.ts +37 -47
- package/src/view/app/lib/attempt-route.ts +26 -26
- package/src/view/app/lib/rows.ts +9 -112
- package/src/view/app/lib/verdict.ts +25 -0
- package/src/view/app/main.tsx +9 -12
- package/src/view/app/pages/RunsPage.tsx +4 -4
- package/src/view/app/pages/TracesPage.tsx +3 -3
- package/src/view/app/shared.ts +0 -1
- package/src/view/app/types.ts +6 -51
- package/src/view/artifact-serving.test.ts +136 -0
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +21 -21
- package/src/view/data.test.ts +214 -143
- package/src/view/data.ts +166 -120
- package/src/view/index.ts +23 -11
- package/src/view/server.ts +52 -17
- package/src/view/shared/types.ts +22 -31
- package/src/view/styles.css +117 -327
- package/src/view/view-report.test.ts +146 -98
- package/src/report/default-report.tsx +0 -222
- package/src/report/dual-face.test.tsx +0 -527
- package/src/report/react/CaseList.tsx +0 -70
- package/src/view/app/components/CostScoreChart.tsx +0 -147
- package/src/view/app/components/ExperimentTable.tsx +0 -284
- package/src/view/app/components/GroupSelector.tsx +0 -61
- package/src/view/app/components/primitives.tsx +0 -43
- package/src/view/app/lib/outcome.ts +0 -82
package/src/results/writer.ts
CHANGED
|
@@ -1,38 +1,47 @@
|
|
|
1
|
-
//
|
|
1
|
+
// createResultsWriter:Results Format 的写入面(定稿见 docs/feature/results/library.md「写:createResultsWriter」)。
|
|
2
2
|
//
|
|
3
3
|
// writer 与 reader 是同一组类型的两半,而且是字面的两半:reader 的 attempt.result 由
|
|
4
|
-
// 「snapshot() 声明的快照级字段(
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// src/runner/reporters/artifacts.ts 是本文件的薄壳。
|
|
4
|
+
// 「snapshot() 声明的快照级字段(experimentId / agent / model / startedAt / experiment)+
|
|
5
|
+
// writeAttempt 第一参」拼成,快照级字段不在 attempt 参数类型里(AttemptEntry 的 Omit),
|
|
6
|
+
// 不存在「谁的值为准」。布局知识(快照目录独占创建、attempt 路径清洗、大字段拆 artifact、
|
|
7
|
+
// has* 回填、空数据不落文件)全在这里;src/runner/reporters/artifacts.ts 是本文件的薄壳。
|
|
8
8
|
|
|
9
9
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
-
import type { EvalResult,
|
|
12
|
-
import type { LocalizedText } from "../types.ts";
|
|
11
|
+
import type { AgentSetupManifest, EvalResult, ExperimentRunInfo, LocalizedText } from "../types.ts";
|
|
13
12
|
import type { DiffData, O11ySummary, SourceArtifact, StreamEvent, TraceSpan } from "../types.ts";
|
|
14
13
|
import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../types.ts";
|
|
15
|
-
import { attemptDirOf } from "./format.ts";
|
|
14
|
+
import { RESULT_FILE, SNAPSHOT_FILE, artifactFileOf, attemptDirOf, experimentDirOf } from "./format.ts";
|
|
15
|
+
import { encodeAttemptLocator } from "./locator.ts";
|
|
16
|
+
import { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
|
|
17
|
+
import type { Producer, SnapshotMeta } from "./types.ts";
|
|
16
18
|
|
|
17
|
-
export interface
|
|
19
|
+
export interface ResultsWriterOptions {
|
|
18
20
|
/** 谁在写这份结果:niceeval 自己,或第三方 harness(name 如实写,别冒充 "niceeval")。 */
|
|
19
|
-
producer:
|
|
21
|
+
producer: Producer;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
/**
|
|
24
|
+
/** 快照级声明:一个 experiment 声明一次,这些字段不塞进每条 attempt。 */
|
|
23
25
|
export interface SnapshotDeclaration {
|
|
24
|
-
|
|
26
|
+
experimentId: string;
|
|
25
27
|
agent: string;
|
|
26
28
|
model?: string;
|
|
27
29
|
/** 必填:身份键与去重以它为锚,官方产出永不缺。 */
|
|
28
30
|
startedAt: string;
|
|
31
|
+
/** 转换历史数据时如实交代收尾时刻;省略则 finish() 用当前时刻。 */
|
|
32
|
+
completedAt?: string;
|
|
33
|
+
/** 实验运行配置(flags / runs / earlyExit / sandbox / timeoutMs / budget),快照内全部 attempt 共享。 */
|
|
34
|
+
experiment?: ExperimentRunInfo;
|
|
29
35
|
/** 该实验已知的 eval 并集(残缺检测的分母);转换只覆盖部分题目时如实交代全集。 */
|
|
30
36
|
knownEvalIds?: string[];
|
|
37
|
+
/** 项目名(来自 config.name),透传给 `niceeval view` 顶部 hero 显示。 */
|
|
38
|
+
name?: LocalizedText;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
/**
|
|
34
42
|
* writeAttempt 的第一参 = attempt 级条目:reader 的 attempt.result 中,快照级字段
|
|
35
|
-
* (agent / model / startedAt /
|
|
43
|
+
* (experimentId / agent / model / startedAt / experiment)与引用字段(artifactBase / has*)
|
|
44
|
+
* 以外的全部;引用字段由 writer 按实际写入的 artifact 回填。
|
|
36
45
|
*/
|
|
37
46
|
export type AttemptEntry = Omit<
|
|
38
47
|
EvalResult,
|
|
@@ -40,219 +49,338 @@ export type AttemptEntry = Omit<
|
|
|
40
49
|
| "model"
|
|
41
50
|
| "startedAt"
|
|
42
51
|
| "experimentId"
|
|
52
|
+
| "experiment"
|
|
43
53
|
| "events"
|
|
44
54
|
| "sources"
|
|
45
55
|
| "o11y"
|
|
46
56
|
| "trace"
|
|
57
|
+
| "agentSetup"
|
|
47
58
|
| "diff"
|
|
48
59
|
| "rawTranscript"
|
|
49
|
-
| "artifactsDir"
|
|
50
60
|
| "artifactBase"
|
|
51
|
-
| "artifactAbsBase"
|
|
52
61
|
| "hasTrace"
|
|
53
62
|
| "hasEvents"
|
|
54
63
|
| "hasSources"
|
|
55
64
|
>;
|
|
56
65
|
|
|
57
|
-
/** writeAttempt 的第二参:reader
|
|
66
|
+
/** writeAttempt 的第二参:reader 懒加载能拿到的那几样 artifact,全部可选;缺哪样读取面就懒加载出 null。 */
|
|
58
67
|
export interface AttemptArtifacts {
|
|
59
68
|
events?: StreamEvent[];
|
|
60
69
|
trace?: TraceSpan[];
|
|
61
70
|
o11y?: O11ySummary;
|
|
71
|
+
/** agent setup 的安装清单(沙箱型 coding agent 装了 Skill / plugin / MCP 才有)。 */
|
|
72
|
+
agentSetup?: AgentSetupManifest;
|
|
62
73
|
diff?: DiffData;
|
|
63
74
|
sources?: SourceArtifact[];
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
export interface SnapshotWriter {
|
|
67
|
-
/**
|
|
78
|
+
/** 本快照的目录(绝对路径)。 */
|
|
79
|
+
readonly dir: string;
|
|
80
|
+
/** 增量落盘一条 attempt:拆 artifact 文件、回填 has* 引用、写 result.json;空数据不落文件。 */
|
|
68
81
|
writeAttempt(entry: AttemptEntry, artifacts?: AttemptArtifacts): Promise<void>;
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
|
|
72
|
-
export interface FinishOverrides {
|
|
73
|
-
name?: LocalizedText;
|
|
74
|
-
agent?: string;
|
|
75
|
-
model?: string;
|
|
76
|
-
startedAt?: string;
|
|
77
|
-
completedAt?: string;
|
|
78
|
-
durationMs?: number;
|
|
79
|
-
usage?: Usage;
|
|
80
|
-
estimatedCostUSD?: number;
|
|
84
|
+
export interface ResultsWriter {
|
|
81
85
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
86
|
+
* 建快照目录(独占创建,撞名换随机后缀重试)+ 立即写 snapshot.json(不含 completedAt)。
|
|
87
|
+
* 同一 writer 内同 experimentId 重复声明 → 返回同一个 SnapshotWriter(懒建语义;
|
|
88
|
+
* knownEvalIds 取并集,completedAt / name 以最后一次声明为准,finish() 时才落盘)。
|
|
84
89
|
*/
|
|
85
|
-
|
|
90
|
+
snapshot(decl: SnapshotDeclaration): Promise<SnapshotWriter>;
|
|
91
|
+
/** 给每个已声明的快照补 completedAt(decl.completedAt ?? 当前时刻)与 name(参数优先,声明兜底)。 */
|
|
92
|
+
finish(opts?: { name?: LocalizedText }): Promise<void>;
|
|
93
|
+
/** @internal runner 薄壳入口:按 EvalResult 的 experimentId 懒建快照并落盘一条 attempt。 */
|
|
94
|
+
writeAttemptFor(result: EvalResult): Promise<void>;
|
|
95
|
+
/** @internal 已创建快照清单(CLI 收尾打印)。 */
|
|
96
|
+
snapshotDirs(): { experimentId: string; dir: string }[];
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
* @internal runner Artifacts 薄壳的增量工件落盘入口:runner 的条目自带 agent / model /
|
|
96
|
-
* experimentId / startedAt(且存在无 experiment 的普通 run),不经 snapshot() 声明。
|
|
97
|
-
*/
|
|
98
|
-
writeAttemptArtifacts(result: EvalResult): Promise<void>;
|
|
99
|
+
interface SnapshotState {
|
|
100
|
+
/** 快照的权威 meta(不含 completedAt;knownEvalIds 随重复声明累加)。 */
|
|
101
|
+
meta: SnapshotMeta;
|
|
102
|
+
dir: string;
|
|
103
|
+
writer: SnapshotWriter;
|
|
104
|
+
declCompletedAt?: string;
|
|
105
|
+
declName?: LocalizedText;
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const decls: SnapshotDeclaration[] = [];
|
|
107
|
-
const entries: EvalResult[] = [];
|
|
108
|
+
/** 同步:不建目录、不碰磁盘。目录创建发生在第一次 snapshot() 调用里。 */
|
|
109
|
+
export function createResultsWriter(root: string, opts: ResultsWriterOptions): ResultsWriter {
|
|
110
|
+
const pending = new Map<string, Promise<SnapshotState>>();
|
|
111
|
+
const created: { experimentId: string; dir: string }[] = [];
|
|
108
112
|
let finished = false;
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
async function buildSnapshot(decl: SnapshotDeclaration): Promise<SnapshotState> {
|
|
115
|
+
const meta: SnapshotMeta = {
|
|
116
|
+
format: RESULTS_FORMAT,
|
|
117
|
+
schemaVersion: RESULTS_SCHEMA_VERSION,
|
|
118
|
+
producer: opts.producer,
|
|
119
|
+
experimentId: decl.experimentId,
|
|
120
|
+
// 运行配置不带 id:身份的家是顶层 experimentId,重复一份只会引出「以谁为准」。
|
|
121
|
+
...(decl.experiment !== undefined ? { experiment: stripInfoId(decl.experiment) } : {}),
|
|
122
|
+
agent: decl.agent,
|
|
123
|
+
...(decl.model !== undefined ? { model: decl.model } : {}),
|
|
124
|
+
startedAt: decl.startedAt,
|
|
125
|
+
...(decl.knownEvalIds?.length ? { knownEvalIds: [...new Set(decl.knownEvalIds)] } : {}),
|
|
126
|
+
...(decl.name !== undefined ? { name: decl.name } : {}),
|
|
127
|
+
};
|
|
128
|
+
const dir = await createSnapshotDir(root, decl.experimentId);
|
|
129
|
+
await writeFile(join(dir, SNAPSHOT_FILE), JSON.stringify(meta, null, 2), "utf-8");
|
|
130
|
+
created.push({ experimentId: decl.experimentId, dir });
|
|
131
|
+
// 快照级源码去重仓库:sha256 → 落盘 Promise,同一快照内并发/重复的 writeAttempt 共享同一次写入
|
|
132
|
+
// (Map 的 has/set 之间没有 await,JS 单线程语义下不会重复起两次写)。
|
|
133
|
+
const sourceStore = new Map<string, Promise<void>>();
|
|
134
|
+
const writer: SnapshotWriter = {
|
|
135
|
+
dir,
|
|
136
|
+
async writeAttempt(entry: AttemptEntry, artifacts?: AttemptArtifacts): Promise<void> {
|
|
137
|
+
await writeAttemptFiles(dir, { experimentId: meta.experimentId, startedAt: meta.startedAt }, entry, artifacts, sourceStore);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
return { meta, dir, writer, declCompletedAt: decl.completedAt, declName: decl.name };
|
|
141
|
+
}
|
|
121
142
|
|
|
122
|
-
|
|
123
|
-
|
|
143
|
+
async function snapshotImpl(decl: SnapshotDeclaration): Promise<SnapshotWriter> {
|
|
144
|
+
if (!decl.experimentId || !decl.agent || !decl.startedAt) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
"writer.snapshot() requires experimentId, agent and startedAt. They are snapshot-level identity: declare them once here instead of on each attempt.",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const existing = pending.get(decl.experimentId);
|
|
150
|
+
const statePromise: Promise<SnapshotState> = existing
|
|
151
|
+
? existing.then((state) => {
|
|
152
|
+
if (decl.knownEvalIds?.length) {
|
|
153
|
+
state.meta.knownEvalIds = [...new Set([...(state.meta.knownEvalIds ?? []), ...decl.knownEvalIds!])];
|
|
154
|
+
}
|
|
155
|
+
if (decl.completedAt !== undefined) state.declCompletedAt = decl.completedAt;
|
|
156
|
+
if (decl.name !== undefined) state.declName = decl.name;
|
|
157
|
+
return state;
|
|
158
|
+
})
|
|
159
|
+
: buildSnapshot(decl);
|
|
160
|
+
pending.set(decl.experimentId, statePromise);
|
|
161
|
+
const state = await statePromise;
|
|
162
|
+
return state.writer;
|
|
163
|
+
}
|
|
124
164
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
165
|
+
async function writeAttemptForImpl(result: EvalResult): Promise<void> {
|
|
166
|
+
if (!result.experimentId) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`writeAttemptFor() requires EvalResult.experimentId (results schemaVersion ${RESULTS_SCHEMA_VERSION} lays out one directory per experiment); eval "${result.id}" has none.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const snap = await snapshotImpl({
|
|
172
|
+
experimentId: result.experimentId,
|
|
173
|
+
agent: result.agent,
|
|
174
|
+
model: result.model,
|
|
175
|
+
// 快照 startedAt 以该实验首条落盘结果的 attempt 时刻为锚(首条 ≈ 实验开跑)。
|
|
176
|
+
startedAt: result.startedAt ?? new Date().toISOString(),
|
|
177
|
+
experiment: result.experiment,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (result.artifactBase) {
|
|
181
|
+
// 携带条目(--resume 合入):本轮没有任何新数据,不写 artifact、不重算 has*,
|
|
182
|
+
// startedAt(身份锚)与 artifactBase 原样保留。locator 同理原样保留(在 ...rest 里,
|
|
183
|
+
// 没被解构掉)、从不重算——`result` 是上一轮 openResults() 读回的记录,原快照的
|
|
184
|
+
// startedAt 已经不在本轮快照里了,重算会用错的 snapshotStartedAt 算出不同的字符串,
|
|
185
|
+
// 让已经发布/引用过的 locator 失效。真缺失(没经过 openResults 的手工构造)时如实留空,
|
|
186
|
+
// 交给读取面按当前身份兜底算(见 open.ts 的 locator 回填),不在这里瞎猜。
|
|
187
|
+
const { agent, model, experimentId, experiment, events, sources, o11y, trace, agentSetup, diff, rawTranscript, ...rest } =
|
|
188
|
+
result;
|
|
189
|
+
void agent;
|
|
190
|
+
void model;
|
|
191
|
+
void experimentId;
|
|
192
|
+
void experiment;
|
|
193
|
+
void events;
|
|
194
|
+
void sources;
|
|
195
|
+
void o11y;
|
|
196
|
+
void trace;
|
|
197
|
+
void agentSetup;
|
|
198
|
+
void diff;
|
|
199
|
+
void rawTranscript;
|
|
200
|
+
const attemptDir = join(snap.dir, attemptDirOf(result));
|
|
201
|
+
await mkdir(attemptDir, { recursive: true });
|
|
202
|
+
await writeFile(join(attemptDir, RESULT_FILE), JSON.stringify(rest, null, 2), "utf-8");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
153
205
|
|
|
154
|
-
|
|
155
|
-
|
|
206
|
+
const {
|
|
207
|
+
agent,
|
|
208
|
+
model,
|
|
209
|
+
startedAt,
|
|
210
|
+
experimentId,
|
|
211
|
+
experiment,
|
|
212
|
+
events,
|
|
213
|
+
sources,
|
|
214
|
+
o11y,
|
|
215
|
+
trace,
|
|
216
|
+
agentSetup,
|
|
217
|
+
diff,
|
|
218
|
+
rawTranscript,
|
|
219
|
+
artifactBase,
|
|
220
|
+
hasTrace,
|
|
221
|
+
hasEvents,
|
|
222
|
+
hasSources,
|
|
223
|
+
...entry
|
|
224
|
+
} = result;
|
|
225
|
+
void agent;
|
|
226
|
+
void model;
|
|
227
|
+
void experimentId;
|
|
228
|
+
void experiment;
|
|
229
|
+
void artifactBase;
|
|
230
|
+
void hasTrace;
|
|
231
|
+
void hasEvents;
|
|
232
|
+
void hasSources;
|
|
233
|
+
// startedAt 是 attempt 级事实(每条各异,view 靠它显示「何时跑的」),原样落盘;
|
|
234
|
+
// 读取面只在记录缺失时才回退快照的 startedAt。
|
|
235
|
+
const record = { ...entry, ...(startedAt !== undefined ? { startedAt } : {}) };
|
|
236
|
+
await snap.writeAttempt(record as AttemptEntry, { events, sources, o11y, trace, agentSetup, diff });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
snapshot: snapshotImpl,
|
|
241
|
+
writeAttemptFor: writeAttemptForImpl,
|
|
242
|
+
snapshotDirs(): { experimentId: string; dir: string }[] {
|
|
243
|
+
return [...created];
|
|
156
244
|
},
|
|
157
245
|
|
|
158
|
-
async finish(
|
|
159
|
-
if (finished) throw new Error(
|
|
246
|
+
async finish(finishOpts?: { name?: LocalizedText }): Promise<void> {
|
|
247
|
+
if (finished) throw new Error("writer.finish() was already called.");
|
|
160
248
|
finished = true;
|
|
249
|
+
const states = await Promise.all([...pending.values()]);
|
|
250
|
+
await Promise.all(
|
|
251
|
+
states.map(async (state) => {
|
|
252
|
+
const completedAt = state.declCompletedAt ?? new Date().toISOString();
|
|
253
|
+
const name = finishOpts?.name ?? state.declName;
|
|
254
|
+
const finalMeta: SnapshotMeta = {
|
|
255
|
+
format: state.meta.format,
|
|
256
|
+
schemaVersion: state.meta.schemaVersion,
|
|
257
|
+
producer: state.meta.producer,
|
|
258
|
+
experimentId: state.meta.experimentId,
|
|
259
|
+
...(state.meta.experiment !== undefined ? { experiment: state.meta.experiment } : {}),
|
|
260
|
+
agent: state.meta.agent,
|
|
261
|
+
...(state.meta.model !== undefined ? { model: state.meta.model } : {}),
|
|
262
|
+
startedAt: state.meta.startedAt,
|
|
263
|
+
completedAt,
|
|
264
|
+
...(state.meta.knownEvalIds?.length ? { knownEvalIds: state.meta.knownEvalIds } : {}),
|
|
265
|
+
...(name !== undefined ? { name } : {}),
|
|
266
|
+
};
|
|
267
|
+
state.meta = finalMeta;
|
|
268
|
+
await writeFile(join(state.dir, SNAPSHOT_FILE), JSON.stringify(finalMeta, null, 2), "utf-8");
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
161
274
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
cost += r.estimatedCostUSD ?? 0;
|
|
173
|
-
duration += r.durationMs ?? 0;
|
|
174
|
-
}
|
|
275
|
+
/** 一条 attempt 的落盘:拆 artifact 文件、算 has*、写 result.json;空数据不落文件。 */
|
|
276
|
+
async function writeAttemptFiles(
|
|
277
|
+
snapDir: string,
|
|
278
|
+
snapshot: { experimentId: string; startedAt: string },
|
|
279
|
+
entry: AttemptEntry,
|
|
280
|
+
artifacts: AttemptArtifacts | undefined,
|
|
281
|
+
sourceStore: Map<string, Promise<void>>,
|
|
282
|
+
): Promise<void> {
|
|
283
|
+
const attemptDir = join(snapDir, attemptDirOf(entry));
|
|
284
|
+
await mkdir(attemptDir, { recursive: true });
|
|
175
285
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
createdAt.toISOString();
|
|
180
|
-
const agent = overrides.agent ?? decls[0]?.agent ?? results[0]?.agent ?? "";
|
|
181
|
-
const model = overrides.model ?? decls[0]?.model;
|
|
182
|
-
const estimatedCostUSD = overrides.estimatedCostUSD ?? (cost || undefined);
|
|
183
|
-
const snapshotsMeta = buildSnapshotsMeta(decls, startedAt);
|
|
286
|
+
const hasEvents = !!(artifacts?.events && artifacts.events.length);
|
|
287
|
+
const hasSources = !!(artifacts?.sources && artifacts.sources.length);
|
|
288
|
+
const hasTrace = !!(artifacts?.trace && artifacts.trace.length);
|
|
184
289
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
};
|
|
290
|
+
const writes: Promise<unknown>[] = [];
|
|
291
|
+
if (hasEvents) writes.push(writeFile(join(attemptDir, "events.json"), JSON.stringify(artifacts!.events), "utf-8"));
|
|
292
|
+
if (hasSources) writes.push(writeSourcesRef(snapDir, attemptDir, artifacts!.sources!, sourceStore));
|
|
293
|
+
if (hasTrace) writes.push(writeFile(join(attemptDir, "trace.json"), JSON.stringify(artifacts!.trace), "utf-8"));
|
|
294
|
+
if (artifacts?.o11y) writes.push(writeFile(join(attemptDir, "o11y.json"), JSON.stringify(artifacts.o11y), "utf-8"));
|
|
295
|
+
if (artifacts?.agentSetup) {
|
|
296
|
+
writes.push(
|
|
297
|
+
writeFile(join(attemptDir, artifactFileOf("agentSetup")), JSON.stringify(artifacts.agentSetup), "utf-8"),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (artifacts?.diff) writes.push(writeFile(join(attemptDir, "diff.json"), JSON.stringify(artifacts.diff), "utf-8"));
|
|
301
|
+
await Promise.all(writes);
|
|
302
|
+
|
|
303
|
+
// locator:caller(如第三方 harness 直接调 SnapshotWriter.writeAttempt)已经带了就尊重,
|
|
304
|
+
// 否则按当前身份元组算一份 —— 这条路径只服务「非携带」的新写入,携带条目走
|
|
305
|
+
// writeAttemptForImpl 的 artifactBase 分支,原样透传 result.locator,从不落到这里重算。
|
|
306
|
+
const locator =
|
|
307
|
+
entry.locator ??
|
|
308
|
+
encodeAttemptLocator({
|
|
309
|
+
experimentId: snapshot.experimentId,
|
|
310
|
+
snapshotStartedAt: snapshot.startedAt,
|
|
311
|
+
evalId: entry.id,
|
|
312
|
+
attempt: entry.attempt,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const record = { ...entry, locator, hasEvents, hasTrace, hasSources };
|
|
316
|
+
await writeFile(join(attemptDir, RESULT_FILE), JSON.stringify(record, null, 2), "utf-8");
|
|
211
317
|
}
|
|
212
318
|
|
|
213
319
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
320
|
+
* sources 是唯一「两层」的 artifact:attempt 目录下只落一份小引用(`{path, sha256}[]`),
|
|
321
|
+
* 真正的源码内容按 sha256 去重存进快照根的 `sources/<sha256>.json`——同一快照内多个 attempt
|
|
322
|
+
* 引用同一份 eval 源码(同文件、同内容)只写一次盘。sourceStore 是这个快照专属的去重登记表
|
|
323
|
+
* (调用方按快照生命周期传入同一个 Map),覆盖并发与重复两种场景。
|
|
217
324
|
*/
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
325
|
+
async function writeSourcesRef(
|
|
326
|
+
snapDir: string,
|
|
327
|
+
attemptDir: string,
|
|
328
|
+
sources: SourceArtifact[],
|
|
329
|
+
sourceStore: Map<string, Promise<void>>,
|
|
330
|
+
): Promise<void> {
|
|
331
|
+
const storeDir = join(snapDir, "sources");
|
|
332
|
+
const refs: { path: string; sha256: string }[] = [];
|
|
333
|
+
for (const src of sources) {
|
|
334
|
+
const sha256 = hashEvalSource(normalizeEvalSource(src.content));
|
|
335
|
+
refs.push({ path: src.path, sha256 });
|
|
336
|
+
if (!sourceStore.has(sha256)) {
|
|
337
|
+
sourceStore.set(
|
|
338
|
+
sha256,
|
|
339
|
+
(async () => {
|
|
340
|
+
await mkdir(storeDir, { recursive: true });
|
|
341
|
+
await writeFile(join(storeDir, `${sha256}.json`), JSON.stringify({ content: src.content }), "utf-8");
|
|
342
|
+
})(),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
await Promise.all(refs.map((r) => sourceStore.get(r.sha256)!));
|
|
347
|
+
await writeFile(join(attemptDir, artifactFileOf("sources")), JSON.stringify(refs), "utf-8");
|
|
229
348
|
}
|
|
230
349
|
|
|
231
|
-
/**
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const existing = meta[decl.experiment];
|
|
245
|
-
if (existing?.knownEvalIds && entry.knownEvalIds) {
|
|
246
|
-
entry.knownEvalIds = [...new Set([...existing.knownEvalIds, ...entry.knownEvalIds])];
|
|
247
|
-
}
|
|
248
|
-
meta[decl.experiment] = { ...existing, ...entry };
|
|
249
|
-
any = true;
|
|
350
|
+
/** 快照目录:独占创建(EEXIST 换随机后缀重试,≤5 次)。 */
|
|
351
|
+
async function createSnapshotDir(root: string, experimentId: string): Promise<string> {
|
|
352
|
+
const parent = join(root, experimentDirOf(experimentId));
|
|
353
|
+
await mkdir(parent, { recursive: true });
|
|
354
|
+
let lastError: unknown;
|
|
355
|
+
for (let i = 0; i < 5; i++) {
|
|
356
|
+
const dir = join(parent, `${safeTimestamp(new Date())}-${randomSuffix()}`);
|
|
357
|
+
try {
|
|
358
|
+
await mkdir(dir);
|
|
359
|
+
return dir;
|
|
360
|
+
} catch (e) {
|
|
361
|
+
if ((e as NodeJS.ErrnoException)?.code !== "EEXIST") throw e;
|
|
362
|
+
lastError = e;
|
|
250
363
|
}
|
|
251
364
|
}
|
|
252
|
-
|
|
365
|
+
throw new Error(`Could not create a unique snapshot directory under "${parent}" after 5 attempts (${String(lastError)}).`);
|
|
253
366
|
}
|
|
254
367
|
|
|
255
|
-
/**
|
|
256
|
-
|
|
368
|
+
/** 运行配置落盘前剥掉 id:experimentId 的家在 snapshot.json 顶层。 */
|
|
369
|
+
function stripInfoId(info: ExperimentRunInfo): ExperimentRunInfo {
|
|
370
|
+
const { id, ...rest } = info;
|
|
371
|
+
void id;
|
|
372
|
+
return rest;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** 快照目录名的时间戳段:Date#toISOString 把 : 与 . 换成 -(与 docs/feature/results/architecture.md 一致)。 */
|
|
376
|
+
function safeTimestamp(d: Date): string {
|
|
257
377
|
return d.toISOString().replace(/[:.]/g, "-");
|
|
258
378
|
}
|
|
379
|
+
|
|
380
|
+
/** 快照目录名的随机后缀:4 位 [a-z0-9]。 */
|
|
381
|
+
function randomSuffix(): string {
|
|
382
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
383
|
+
let out = "";
|
|
384
|
+
for (let i = 0; i < 4; i++) out += chars[Math.floor(Math.random() * chars.length)];
|
|
385
|
+
return out;
|
|
386
|
+
}
|