niceeval 0.4.6 → 0.5.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/docs-site/zh/concepts/adapter.mdx +6 -6
- package/docs-site/zh/concepts/experiment.mdx +7 -7
- package/docs-site/zh/concepts/overview.mdx +1 -1
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +64 -21
- package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
- package/docs-site/zh/guides/custom-reports.mdx +243 -0
- package/docs-site/zh/guides/experiments.mdx +3 -3
- package/docs-site/zh/guides/report-components.mdx +252 -0
- package/docs-site/zh/guides/reporters.mdx +1 -1
- package/docs-site/zh/guides/results-data.mdx +224 -0
- package/docs-site/zh/guides/runner.mdx +1 -1
- package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
- package/docs-site/zh/guides/viewing-results.mdx +147 -77
- package/docs-site/zh/guides/write-experiment.mdx +9 -9
- package/docs-site/zh/guides/write-send.mdx +6 -6
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/introduction.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +11 -3
- package/docs-site/zh/reference/define-agent.mdx +5 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/package.json +3 -2
- package/src/agents/ai-sdk.test.ts +1 -1
- package/src/agents/ai-sdk.ts +2 -2
- package/src/agents/claude-code.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/streaming.test.ts +1 -1
- package/src/agents/types.ts +4 -4
- package/src/agents/ui-message-stream.test.ts +1 -1
- package/src/cli.ts +120 -18
- package/src/context/context.test.ts +1 -1
- package/src/context/context.ts +3 -3
- package/src/context/session.ts +2 -2
- package/src/context/types.ts +2 -2
- package/src/i18n/en.ts +29 -7
- package/src/i18n/zh-CN.ts +24 -2
- package/src/report/aggregate.ts +79 -50
- package/src/report/components.tsx +190 -0
- package/src/report/compute.ts +149 -84
- package/src/report/default-report.tsx +222 -0
- package/src/report/dual-face.test.tsx +527 -0
- package/src/report/format.ts +29 -0
- package/src/report/index.ts +79 -24
- package/src/report/load.ts +67 -0
- package/src/report/metrics.ts +2 -1
- package/src/report/param.ts +18 -0
- package/src/report/primitives.tsx +117 -0
- package/src/report/react/CaseList.tsx +2 -2
- package/src/report/react/DeltaTable.tsx +2 -2
- package/src/report/react/MetricBars.tsx +109 -0
- package/src/report/react/MetricLine.tsx +200 -0
- package/src/report/react/MetricMatrix.tsx +1 -1
- package/src/report/react/MetricScatter.tsx +1 -1
- package/src/report/react/MetricTable.tsx +1 -1
- package/src/report/react/RunOverview.tsx +4 -4
- package/src/report/react/Scoreboard.tsx +3 -3
- package/src/report/react/cell.tsx +1 -1
- package/src/report/react/fixtures.ts +98 -36
- package/src/report/react/format.ts +3 -28
- package/src/report/react/index.tsx +41 -24
- package/src/report/react/render.test.tsx +8 -8
- package/src/report/react/styles.css +77 -0
- package/src/report/report.test.ts +248 -128
- package/src/report/report.ts +69 -0
- package/src/report/text/faces.ts +356 -0
- package/src/report/text/layout.ts +125 -0
- package/src/report/text/plot.ts +127 -0
- package/src/report/tree.ts +220 -0
- package/src/report/types.ts +75 -27
- package/src/report/web.ts +40 -0
- package/src/results/copy.ts +95 -41
- package/src/results/format.ts +12 -11
- package/src/results/index.ts +34 -17
- package/src/results/open.ts +201 -81
- package/src/results/results.test.ts +595 -191
- package/src/results/select.ts +107 -55
- package/src/results/types.ts +137 -45
- package/src/results/writer.ts +258 -0
- package/src/runner/attempt.ts +3 -3
- package/src/runner/fingerprint.ts +1 -1
- package/src/runner/reporters/artifacts.ts +38 -78
- package/src/runner/reporters/braintrust.test.ts +2 -2
- package/src/runner/reporters/braintrust.ts +3 -3
- package/src/runner/run.ts +1 -1
- package/src/runner/types.ts +18 -12
- package/src/show/compose.ts +215 -0
- package/src/show/index.ts +263 -0
- package/src/show/render.ts +456 -0
- package/src/show/show.test.ts +505 -0
- package/src/view/app/App.tsx +85 -24
- package/src/view/app/components/CostScoreChart.tsx +29 -10
- package/src/view/app/components/ExperimentTable.tsx +27 -9
- package/src/view/app/components/GroupSelector.tsx +1 -1
- package/src/view/app/i18n.ts +18 -3
- package/src/view/app/lib/attempt-route.test.ts +9 -9
- package/src/view/app/lib/attempt-route.ts +5 -5
- package/src/view/app/lib/outcome.ts +1 -3
- package/src/view/app/lib/rows.ts +95 -15
- package/src/view/app/main.tsx +18 -6
- package/src/view/app/pages/RunsPage.tsx +4 -7
- package/src/view/app/pages/TracesPage.tsx +3 -8
- package/src/view/app/types.ts +50 -6
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +20 -20
- package/src/view/data.test.ts +254 -0
- package/src/view/data.ts +336 -0
- package/src/view/index.ts +64 -10
- package/src/view/server.ts +31 -9
- package/src/view/shared/types.ts +47 -41
- package/src/view/styles.css +5 -0
- package/src/view/template.html +1 -0
- package/src/view/view-report.test.ts +308 -0
- package/docs-site/.mintignore +0 -13
- package/docs-site/AGENTS.md +0 -46
- package/docs-site/concepts/adapter.mdx +0 -287
- package/docs-site/concepts/assert.mdx +0 -243
- package/docs-site/concepts/drive.mdx +0 -118
- package/docs-site/concepts/evals.mdx +0 -108
- package/docs-site/concepts/experiment.mdx +0 -37
- package/docs-site/concepts/hitl.mdx +0 -83
- package/docs-site/concepts/judge.mdx +0 -129
- package/docs-site/concepts/overview.mdx +0 -98
- package/docs-site/concepts/tier.mdx +0 -42
- package/docs-site/docs-ref/00-index.md +0 -23
- package/docs-site/docs-ref/01-page-types.md +0 -43
- package/docs-site/docs-ref/03-style-rules.md +0 -45
- package/docs-site/docs-ref/04-code-examples.md +0 -37
- package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
- package/docs-site/docs-ref/06-checklists.md +0 -53
- package/docs-site/docs.json +0 -256
- package/docs-site/example/ai-agent-application.mdx +0 -148
- package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
- package/docs-site/example/claude-code-codex-skill.mdx +0 -147
- package/docs-site/example/showcase.mdx +0 -39
- package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
- package/docs-site/example/tier1-claude-sdk.mdx +0 -119
- package/docs-site/example/tier1-codex-sdk.mdx +0 -111
- package/docs-site/example/tier1-langgraph.mdx +0 -119
- package/docs-site/example/tier1-pi-sdk.mdx +0 -119
- package/docs-site/favicon.svg +0 -5
- package/docs-site/github-diff.css +0 -135
- package/docs-site/github-diff.js +0 -11
- package/docs-site/guides/authoring.mdx +0 -129
- package/docs-site/guides/ci-integration.mdx +0 -97
- package/docs-site/guides/connect-otel.mdx +0 -209
- package/docs-site/guides/connect-your-agent.mdx +0 -221
- package/docs-site/guides/dataset-fanout.mdx +0 -98
- package/docs-site/guides/experiments.mdx +0 -71
- package/docs-site/guides/fixtures.mdx +0 -147
- package/docs-site/guides/reporters.mdx +0 -113
- package/docs-site/guides/runner.mdx +0 -79
- package/docs-site/guides/sandbox-agent.mdx +0 -138
- package/docs-site/guides/sandbox-backends.mdx +0 -64
- package/docs-site/guides/scoring-guide.mdx +0 -92
- package/docs-site/guides/viewing-results.mdx +0 -195
- package/docs-site/guides/write-experiment.mdx +0 -81
- package/docs-site/guides/write-send.mdx +0 -347
- package/docs-site/index.mdx +0 -181
- package/docs-site/introduction.mdx +0 -141
- package/docs-site/quickstart.mdx +0 -136
- package/docs-site/reference/builtin-agents.mdx +0 -161
- package/docs-site/reference/capabilities.mdx +0 -76
- package/docs-site/reference/cli.mdx +0 -120
- package/docs-site/reference/define-agent.mdx +0 -168
- package/docs-site/reference/define-config.mdx +0 -41
- package/docs-site/reference/define-eval.mdx +0 -160
- package/docs-site/reference/events.mdx +0 -131
- package/docs-site/reference/expect.mdx +0 -112
- package/docs-site/tracker.js +0 -8
- package/src/report/react/data.ts +0 -17
- package/src/view/aggregate.ts +0 -103
- package/src/view/loader.test.ts +0 -61
- package/src/view/loader.ts +0 -257
package/src/results/format.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
// Results Format
|
|
1
|
+
// Results Format 的布局与版本知识,规则见 docs/results-format.md。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// view / reporter 改吃这里是后续收编步骤 —— 本实验不动它们的代码。
|
|
3
|
+
// 按 docs/results-lib.md,这份知识只住在本库:写入面(writer.ts / copy.ts)与
|
|
4
|
+
// 读取面(open.ts)共用这里的目录规则与版本判定;src/runner/reporters/artifacts.ts
|
|
5
|
+
// 是 writer 的薄壳,view(src/view/data.ts)经 openResults 消费,不自带布局知识。
|
|
7
6
|
|
|
8
|
-
import type { EvalResult, RunSummary } from "../
|
|
9
|
-
import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../
|
|
7
|
+
import type { EvalResult, RunSummary } from "../types.ts";
|
|
8
|
+
import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../types.ts";
|
|
10
9
|
import type { ArtifactKind } from "./types.ts";
|
|
11
10
|
|
|
12
11
|
/** attempt 工件子目录(相对 run 根):<evalId>/<agent>/<model>[/<experimentId>]/a<attempt>。 */
|
|
@@ -14,8 +13,8 @@ export function attemptDirOf(r: Pick<EvalResult, "id" | "agent" | "model" | "att
|
|
|
14
13
|
const safe = (s: string) => s.replace(/[^\w.@-]/g, "_");
|
|
15
14
|
// evalId 里的 / 保留作目录层级,其余危险字符替换。
|
|
16
15
|
const id = r.id.replace(/[^\w./@-]/g, "_");
|
|
17
|
-
// experiment
|
|
18
|
-
//
|
|
16
|
+
// experiment 段:两个实验可以同 agent 同 model、只差实验参数,少这一段它们的工件会互相
|
|
17
|
+
// 覆盖;experimentId 里的 / 不作层级(整段压成 _),整个实验一格。
|
|
19
18
|
const exp = r.experimentId ? `/${safe(r.experimentId)}` : "";
|
|
20
19
|
return `${id}/${safe(r.agent)}/${safe(r.model ?? "default")}${exp}/a${r.attempt}`;
|
|
21
20
|
}
|
|
@@ -28,7 +27,7 @@ export function artifactFileOf(kind: ArtifactKind): string {
|
|
|
28
27
|
/** summary.json 的版本判定结果;openResults 按它分流 ok / skipped / 静默忽略。 */
|
|
29
28
|
export type SummaryClassification =
|
|
30
29
|
| { kind: "ok"; summary: RunSummary }
|
|
31
|
-
| { kind: "incompatible"; schemaVersion: number;
|
|
30
|
+
| { kind: "incompatible"; schemaVersion: number; producer?: RunSummary["producer"] }
|
|
32
31
|
| { kind: "malformed"; detail: string }
|
|
33
32
|
| { kind: "not-a-report" };
|
|
34
33
|
|
|
@@ -49,7 +48,9 @@ export function classifySummary(raw: unknown): SummaryClassification {
|
|
|
49
48
|
const version = data.schemaVersion ?? 1;
|
|
50
49
|
if (typeof version !== "number") return { kind: "malformed", detail: "schemaVersion is not a number" };
|
|
51
50
|
if (version !== RESULTS_SCHEMA_VERSION) {
|
|
52
|
-
|
|
51
|
+
// skipped 必须带完整 producer(name + version):npx 提示只对 name === "niceeval" 成立,
|
|
52
|
+
// 第三方 harness 的落盘只给裸版本号的话,消费方连做对这个分支的信息都没有。
|
|
53
|
+
return { kind: "incompatible", schemaVersion: version, producer: data.producer };
|
|
53
54
|
}
|
|
54
55
|
if (!Array.isArray(data.results) || typeof data.startedAt !== "string") {
|
|
55
56
|
return { kind: "malformed", detail: "missing results[] or startedAt" };
|
package/src/results/index.ts
CHANGED
|
@@ -1,27 +1,44 @@
|
|
|
1
|
-
// niceeval/results —— 实验结果数据的读写库(
|
|
1
|
+
// niceeval/results —— 实验结果数据的读写库(定稿见 docs/results-lib.md)。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// 读:openResults(实验 → 快照 → eval → attempt 分层、skipped、runDirs、latest() 选集);
|
|
4
|
+
// 写:createRunWriter(快照级声明 + attempt 增量落盘 + summary 收尾);
|
|
5
|
+
// 发布:copySnapshots(格式感知复制 + knownEvalIds 补记);
|
|
6
|
+
// 身份:dedupeAttempts(跨快照聚合前按 (experimentId, evalId, attempt, startedAt) 去重)。
|
|
7
|
+
// 布局知识(路径、清洗、拆分、版本)全宇宙只有这一份实现;
|
|
8
|
+
// src/runner/reporters/artifacts.ts 是写入面的薄壳,view 的读取收编是下一波。
|
|
6
9
|
|
|
7
10
|
export { openResults } from "./open.ts";
|
|
8
|
-
export { dedupeAttempts
|
|
9
|
-
export {
|
|
11
|
+
export { dedupeAttempts } from "./select.ts";
|
|
12
|
+
export { copySnapshots, type CopySnapshotsOptions, type CopySnapshotsResult } from "./copy.ts";
|
|
13
|
+
export {
|
|
14
|
+
createRunWriter,
|
|
15
|
+
type AttemptArtifacts,
|
|
16
|
+
type AttemptEntry,
|
|
17
|
+
type FinishOverrides,
|
|
18
|
+
type RunWriter,
|
|
19
|
+
type RunWriterOptions,
|
|
20
|
+
type SnapshotDeclaration,
|
|
21
|
+
type SnapshotWriter,
|
|
22
|
+
} from "./writer.ts";
|
|
10
23
|
export {
|
|
11
24
|
ARTIFACT_KINDS,
|
|
12
25
|
type ArtifactKind,
|
|
13
26
|
type AttemptHandle,
|
|
14
27
|
type AttemptRef,
|
|
15
|
-
type
|
|
16
|
-
type
|
|
17
|
-
type
|
|
18
|
-
type
|
|
28
|
+
type DedupeWarning,
|
|
29
|
+
type Eval,
|
|
30
|
+
type Experiment,
|
|
31
|
+
type Results,
|
|
32
|
+
type RunDir,
|
|
33
|
+
type Selection,
|
|
34
|
+
type SelectionWarning,
|
|
35
|
+
type SkippedRun,
|
|
36
|
+
type Snapshot,
|
|
19
37
|
} from "./types.ts";
|
|
20
38
|
|
|
21
|
-
// 结果数据类型的家还没搬(
|
|
22
|
-
//
|
|
23
|
-
export { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../
|
|
24
|
-
export type { EvalResult, ExperimentRunInfo, RunSummary } from "../
|
|
25
|
-
export type { O11ySummary, StreamEvent, TraceSpan, Usage } from "../
|
|
26
|
-
export type { DiffData } from "../
|
|
27
|
-
export type { SourceArtifact } from "../shared/types.ts";
|
|
39
|
+
// 结果数据类型的家还没搬(facade 迁移是下一波);先从这里 re-export,
|
|
40
|
+
// 让消费方从一个入口拿全「分层句柄 + 数据类型 + 格式常量」。
|
|
41
|
+
export { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../types.ts";
|
|
42
|
+
export type { EvalResult, ExperimentRunInfo, RunSummary } from "../types.ts";
|
|
43
|
+
export type { O11ySummary, StreamEvent, TraceSpan, Usage } from "../types.ts";
|
|
44
|
+
export type { DiffData, SourceArtifact } from "../types.ts";
|
package/src/results/open.ts
CHANGED
|
@@ -1,75 +1,132 @@
|
|
|
1
|
-
// openResults
|
|
1
|
+
// openResults:扫描结果目录,返回「实验 → 快照 → eval → attempt」的类型化层次
|
|
2
|
+
// (定稿见 docs/results-lib.md「读:openResults」)。
|
|
2
3
|
//
|
|
3
4
|
// 三条铁律:
|
|
4
|
-
// -
|
|
5
|
-
// -
|
|
5
|
+
// - 忠实磁盘:快照与实验归组只切片,不合并、不聚合、不去重;合并/聚合永远发生在消费方。
|
|
6
|
+
// - 读不了的落盘进 skipped(三种原因),不静默丢,也不抛错(单个坏 run 不拖垮整次扫描)。
|
|
6
7
|
// - 重工件全部懒加载:缺失返回 null(存在性判断被方法语义吸收),同一 handle 内记忆化。
|
|
7
8
|
|
|
8
9
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
9
10
|
import { basename, dirname, join, resolve } from "node:path";
|
|
10
|
-
import type { EvalResult } from "../
|
|
11
|
-
import type { O11ySummary, StreamEvent, TraceSpan } from "../
|
|
12
|
-
import type { DiffData } from "../
|
|
13
|
-
import type { SourceArtifact } from "../shared/types.ts";
|
|
11
|
+
import type { EvalResult, RunSummary } from "../types.ts";
|
|
12
|
+
import type { O11ySummary, StreamEvent, TraceSpan } from "../types.ts";
|
|
13
|
+
import type { DiffData, SourceArtifact } from "../types.ts";
|
|
14
14
|
import { artifactFileOf, classifySummary, experimentKeyOf } from "./format.ts";
|
|
15
|
-
import
|
|
15
|
+
import { isNewerRunDir, selectLatest } from "./select.ts";
|
|
16
|
+
import type {
|
|
17
|
+
ArtifactKind,
|
|
18
|
+
AttemptHandle,
|
|
19
|
+
Eval,
|
|
20
|
+
Experiment,
|
|
21
|
+
Results,
|
|
22
|
+
RunDir,
|
|
23
|
+
Selection,
|
|
24
|
+
SkippedRun,
|
|
25
|
+
Snapshot,
|
|
26
|
+
} from "./types.ts";
|
|
27
|
+
import { ARTIFACT_KINDS } from "./types.ts";
|
|
28
|
+
|
|
29
|
+
// copySnapshots 补记 knownEvalIds 需要「复制时刻该实验的 evalIds」,而 Snapshot 上按定稿
|
|
30
|
+
// 不挂 Experiment 反向指针 —— 用模块级 WeakMap 记归属,只供库内部(copy.ts)取用。
|
|
31
|
+
const experimentBySnapshot = new WeakMap<Snapshot, Experiment>();
|
|
32
|
+
|
|
33
|
+
/** 库内部:快照所属的 Experiment(仅对 openResults 产出的快照存在)。 */
|
|
34
|
+
export function experimentOfSnapshot(snapshot: Snapshot): Experiment | undefined {
|
|
35
|
+
return experimentBySnapshot.get(snapshot);
|
|
36
|
+
}
|
|
16
37
|
|
|
17
38
|
/**
|
|
18
39
|
* 打开 `.niceeval/` 根目录、单个 run 目录,或直接指向某个 summary.json 的路径。
|
|
19
|
-
* 目录不存在返回空集合(还没跑过 eval 不是错误)
|
|
40
|
+
* 目录不存在返回空集合(还没跑过 eval 不是错误);任何读不了的落盘进 skipped,不抛错。
|
|
20
41
|
*/
|
|
21
|
-
export async function openResults(dir: string): Promise<
|
|
42
|
+
export async function openResults(dir: string): Promise<Results> {
|
|
22
43
|
const target = resolve(dir);
|
|
23
|
-
const
|
|
44
|
+
const runDirs: RunDir[] = [];
|
|
45
|
+
const skipped: SkippedRun[] = [];
|
|
24
46
|
|
|
25
47
|
let targetStat;
|
|
26
48
|
try {
|
|
27
49
|
targetStat = await stat(target);
|
|
28
50
|
} catch {
|
|
29
|
-
return
|
|
51
|
+
return makeResults([], skipped, runDirs);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const summaryPaths: string[] = [];
|
|
55
|
+
if (targetStat.isFile()) {
|
|
56
|
+
summaryPaths.push(target);
|
|
57
|
+
} else {
|
|
58
|
+
const direct = join(target, "summary.json");
|
|
59
|
+
if (await fileExists(direct)) {
|
|
60
|
+
// target 本身是 run 目录:它的子目录是 attempt 工件目录,不做 incomplete 探测。
|
|
61
|
+
summaryPaths.push(direct, ...(await findSummaryFiles(target, direct)));
|
|
62
|
+
} else {
|
|
63
|
+
// target 是结果根:逐个 immediate child 判定 —— 有 summary 的收 run;
|
|
64
|
+
// 没有 summary 但有 attempt 工件的 = crash 没收尾,进 skipped("incomplete")。
|
|
65
|
+
const entries = await readdir(target, { withFileTypes: true });
|
|
66
|
+
for (const entry of entries.filter((e) => e.isDirectory())) {
|
|
67
|
+
const childDir = join(target, entry.name);
|
|
68
|
+
const found = await findSummaryFiles(childDir);
|
|
69
|
+
if (found.length > 0) {
|
|
70
|
+
summaryPaths.push(...found);
|
|
71
|
+
} else if (await hasArtifactFiles(childDir)) {
|
|
72
|
+
skipped.push({ dir: childDir, reason: "incomplete" });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
30
76
|
}
|
|
31
|
-
const summaryPaths = targetStat.isFile() ? [target] : await findSummaryFiles(target);
|
|
32
77
|
|
|
33
78
|
for (const path of summaryPaths) {
|
|
34
79
|
const outcome = await readRun(path);
|
|
35
|
-
if (outcome.kind === "run")
|
|
36
|
-
else if (outcome.kind === "skipped")
|
|
80
|
+
if (outcome.kind === "run") runDirs.push(outcome.run);
|
|
81
|
+
else if (outcome.kind === "skipped") skipped.push(outcome.entry);
|
|
37
82
|
// not-a-report:无关 JSON,静默忽略。
|
|
38
83
|
}
|
|
39
84
|
|
|
40
85
|
// 最新在前;run 目录名是时间戳,startedAt 同刻时按目录名降序兜底。
|
|
41
|
-
|
|
42
|
-
|
|
86
|
+
runDirs.sort((a, b) => (isNewerRunDir(a, b) ? -1 : 1));
|
|
87
|
+
skipped.sort((a, b) => b.dir.localeCompare(a.dir));
|
|
43
88
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
89
|
+
const snapshots: Snapshot[] = [];
|
|
90
|
+
for (const run of runDirs) snapshots.push(...sliceSnapshots(run));
|
|
91
|
+
return makeResults(buildExperiments(snapshots), skipped, runDirs);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function makeResults(experiments: Experiment[], skipped: SkippedRun[], runDirs: RunDir[]): Results {
|
|
95
|
+
return {
|
|
96
|
+
experiments,
|
|
97
|
+
skipped,
|
|
98
|
+
runDirs,
|
|
99
|
+
latest(opts?: { experiments?: string | string[] }): Selection {
|
|
100
|
+
return selectLatest(experiments, opts);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
48
103
|
}
|
|
49
104
|
|
|
105
|
+
// ───────────────────────── 单个 run 的读取 ─────────────────────────
|
|
106
|
+
|
|
50
107
|
type ReadRunOutcome =
|
|
51
|
-
| { kind: "run"; run:
|
|
52
|
-
| { kind: "skipped"; entry:
|
|
108
|
+
| { kind: "run"; run: RunDir }
|
|
109
|
+
| { kind: "skipped"; entry: SkippedRun }
|
|
53
110
|
| { kind: "not-a-report" };
|
|
54
111
|
|
|
55
112
|
async function readRun(path: string): Promise<ReadRunOutcome> {
|
|
56
113
|
const runDir = dirname(path);
|
|
57
|
-
const
|
|
114
|
+
const skippedEntry = (entry: Omit<SkippedRun, "dir">): ReadRunOutcome => ({
|
|
58
115
|
kind: "skipped",
|
|
59
|
-
entry: { dir: runDir,
|
|
116
|
+
entry: { dir: runDir, ...entry },
|
|
60
117
|
});
|
|
61
118
|
|
|
62
119
|
let text: string;
|
|
63
120
|
try {
|
|
64
121
|
text = await readFile(path, "utf-8");
|
|
65
122
|
} catch (e) {
|
|
66
|
-
return
|
|
123
|
+
return skippedEntry({ reason: "malformed", detail: `cannot read file (${e instanceof Error ? e.message : String(e)})` });
|
|
67
124
|
}
|
|
68
125
|
let raw: unknown;
|
|
69
126
|
try {
|
|
70
127
|
raw = JSON.parse(text);
|
|
71
128
|
} catch {
|
|
72
|
-
return
|
|
129
|
+
return skippedEntry({ reason: "malformed", detail: "invalid JSON" });
|
|
73
130
|
}
|
|
74
131
|
|
|
75
132
|
const classified = classifySummary(raw);
|
|
@@ -77,34 +134,114 @@ async function readRun(path: string): Promise<ReadRunOutcome> {
|
|
|
77
134
|
case "not-a-report":
|
|
78
135
|
return { kind: "not-a-report" };
|
|
79
136
|
case "malformed":
|
|
80
|
-
return
|
|
137
|
+
return skippedEntry({ reason: "malformed", detail: classified.detail });
|
|
81
138
|
case "incompatible":
|
|
82
|
-
return
|
|
139
|
+
return skippedEntry({
|
|
83
140
|
reason: "incompatible-version",
|
|
84
141
|
schemaVersion: classified.schemaVersion,
|
|
85
|
-
|
|
142
|
+
...(classified.producer ? { producer: classified.producer } : {}),
|
|
86
143
|
});
|
|
87
144
|
case "ok": {
|
|
88
|
-
const run:
|
|
89
|
-
run.attempts = classified.summary.results.map((r, i) => makeAttempt(run, r, i));
|
|
145
|
+
const run: RunDir = { dir: runDir, summary: classified.summary, attempts: [] };
|
|
90
146
|
return { kind: "run", run };
|
|
91
147
|
}
|
|
92
148
|
}
|
|
93
149
|
}
|
|
94
150
|
|
|
151
|
+
// ───────────────────────── 快照切片与实验归组 ─────────────────────────
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 把一个 run 按 experiment 身份切成快照,同时填充 run.attempts(按 results[] 下标顺序)。
|
|
155
|
+
* 只切片、不合并、不去重;experimentId 缺失时以 "<agent>/<model>" 合成键,synthetic: true
|
|
156
|
+
* (对应警告由 latest() 生成,归属 Selection)。
|
|
157
|
+
*/
|
|
158
|
+
function sliceSnapshots(run: RunDir): Snapshot[] {
|
|
159
|
+
const summary = run.summary;
|
|
160
|
+
const byExperiment = new Map<string, Snapshot>();
|
|
161
|
+
const evalsByExperiment = new Map<string, Map<string, Eval>>();
|
|
162
|
+
|
|
163
|
+
summary.results.forEach((result, index) => {
|
|
164
|
+
const key = experimentKeyOf(result);
|
|
165
|
+
let snapshot = byExperiment.get(key.id);
|
|
166
|
+
if (!snapshot) {
|
|
167
|
+
const meta = summary.snapshots?.[key.id];
|
|
168
|
+
snapshot = {
|
|
169
|
+
experimentId: key.id,
|
|
170
|
+
startedAt: meta?.startedAt ?? summary.startedAt,
|
|
171
|
+
agent: result.agent,
|
|
172
|
+
...(result.model !== undefined ? { model: result.model } : {}),
|
|
173
|
+
...(summary.producer ? { producer: summary.producer } : {}),
|
|
174
|
+
schemaVersion: summary.schemaVersion ?? 1,
|
|
175
|
+
evals: [],
|
|
176
|
+
attempts: [],
|
|
177
|
+
runDir: run,
|
|
178
|
+
...(key.synthesized ? { synthetic: true } : {}),
|
|
179
|
+
...(meta?.knownEvalIds ? { knownEvalIds: [...meta.knownEvalIds] } : {}),
|
|
180
|
+
};
|
|
181
|
+
byExperiment.set(key.id, snapshot);
|
|
182
|
+
evalsByExperiment.set(key.id, new Map());
|
|
183
|
+
}
|
|
184
|
+
const attempt = makeAttempt(run, key.id, result, index);
|
|
185
|
+
run.attempts.push(attempt);
|
|
186
|
+
const evals = evalsByExperiment.get(key.id)!;
|
|
187
|
+
let ev = evals.get(result.id);
|
|
188
|
+
if (!ev) {
|
|
189
|
+
ev = { id: result.id, attempts: [] };
|
|
190
|
+
evals.set(result.id, ev);
|
|
191
|
+
snapshot.evals.push(ev);
|
|
192
|
+
}
|
|
193
|
+
ev.attempts.push(attempt);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
for (const snapshot of byExperiment.values()) {
|
|
197
|
+
// attempts 平铺 = evals 逐题展开(同题的重试相邻)。
|
|
198
|
+
snapshot.attempts = snapshot.evals.flatMap((ev) => ev.attempts);
|
|
199
|
+
}
|
|
200
|
+
return [...byExperiment.values()];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 同一 experiment id 的历次快照归在一起;实验按 id 字典序,快照最新在前。 */
|
|
204
|
+
function buildExperiments(snapshots: Snapshot[]): Experiment[] {
|
|
205
|
+
const byId = new Map<string, Snapshot[]>();
|
|
206
|
+
for (const snapshot of snapshots) {
|
|
207
|
+
const group = byId.get(snapshot.experimentId);
|
|
208
|
+
if (group) group.push(snapshot);
|
|
209
|
+
else byId.set(snapshot.experimentId, [snapshot]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const experiments: Experiment[] = [];
|
|
213
|
+
for (const [id, group] of byId) {
|
|
214
|
+
group.sort((a, b) => b.startedAt.localeCompare(a.startedAt) || b.runDir.dir.localeCompare(a.runDir.dir));
|
|
215
|
+
// 已知并集 = 本地历史(各快照覆盖的题)∪ 各快照携带的 knownEvalIds ——
|
|
216
|
+
// 不是「优先字段」:把快照复制进已有历史的目录时,本地并集可能更大,优先字段会让分母缩水。
|
|
217
|
+
const ids = new Set<string>();
|
|
218
|
+
for (const snapshot of group) {
|
|
219
|
+
for (const ev of snapshot.evals) ids.add(ev.id);
|
|
220
|
+
for (const known of snapshot.knownEvalIds ?? []) ids.add(known);
|
|
221
|
+
}
|
|
222
|
+
const experiment: Experiment = { id, snapshots: group, latest: group[0], evalIds: [...ids].sort() };
|
|
223
|
+
for (const snapshot of group) experimentBySnapshot.set(snapshot, experiment);
|
|
224
|
+
experiments.push(experiment);
|
|
225
|
+
}
|
|
226
|
+
experiments.sort((a, b) => a.id.localeCompare(b.id));
|
|
227
|
+
return experiments;
|
|
228
|
+
}
|
|
229
|
+
|
|
95
230
|
// ───────────────────────── attempt 懒加载 ─────────────────────────
|
|
96
231
|
|
|
97
|
-
function makeAttempt(run:
|
|
232
|
+
function makeAttempt(run: RunDir, experimentId: string, result: EvalResult, index: number): AttemptHandle {
|
|
98
233
|
// 候选工件目录:本 run 下的 artifactsDir 为主;--resume / 跨实验携带合入的条目,其工件
|
|
99
|
-
//
|
|
234
|
+
// 留在原 run 目录里,summary 里的 artifactBase(相对结果根目录)指向那里,作为回退。
|
|
100
235
|
const candidates: string[] = [];
|
|
101
236
|
if (result.artifactsDir) candidates.push(join(run.dir, result.artifactsDir));
|
|
102
237
|
if (result.artifactBase) candidates.push(join(dirname(run.dir), result.artifactBase));
|
|
103
238
|
|
|
104
239
|
return {
|
|
105
|
-
|
|
106
|
-
|
|
240
|
+
evalId: result.id,
|
|
241
|
+
experimentId,
|
|
107
242
|
result,
|
|
243
|
+
ref: { run: basename(run.dir), result: index },
|
|
244
|
+
runDir: run,
|
|
108
245
|
events: lazyArtifact<StreamEvent[]>(candidates, "events", result.events),
|
|
109
246
|
trace: lazyArtifact<TraceSpan[]>(candidates, "trace", result.trace),
|
|
110
247
|
o11y: lazyArtifact<O11ySummary>(candidates, "o11y", result.o11y),
|
|
@@ -153,55 +290,38 @@ function isMissingFile(e: unknown): boolean {
|
|
|
153
290
|
return code === "ENOENT" || code === "ENOTDIR";
|
|
154
291
|
}
|
|
155
292
|
|
|
156
|
-
// ───────────────────────── 快照切片 ─────────────────────────
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* 把一个 run 按 experiment 身份切成快照;只切片、不合并、不去重。
|
|
160
|
-
* experimentId 缺失时以 "<agent>/<model>" 合成键,并出集合级 warning(每 run 每键一条)。
|
|
161
|
-
*/
|
|
162
|
-
function sliceSnapshots(run: RunHandle, warnings: string[]): SnapshotHandle[] {
|
|
163
|
-
const byExperiment = new Map<string, SnapshotHandle>();
|
|
164
|
-
const evalIdSeen = new Map<string, Set<string>>();
|
|
165
|
-
for (const attempt of run.attempts) {
|
|
166
|
-
const key = experimentKeyOf(attempt.result);
|
|
167
|
-
let snapshot = byExperiment.get(key.id);
|
|
168
|
-
if (!snapshot) {
|
|
169
|
-
if (key.synthesized) {
|
|
170
|
-
warnings.push(
|
|
171
|
-
`warning: run "${run.dir}" has results without experimentId; grouped as "${key.id}" for snapshot identity.`,
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
snapshot = {
|
|
175
|
-
experimentId: key.id,
|
|
176
|
-
run,
|
|
177
|
-
startedAt: run.summary.startedAt,
|
|
178
|
-
agent: attempt.result.agent,
|
|
179
|
-
model: attempt.result.model,
|
|
180
|
-
attempts: [],
|
|
181
|
-
evalIds: [],
|
|
182
|
-
...(key.synthesized ? { synthetic: true } : {}),
|
|
183
|
-
};
|
|
184
|
-
byExperiment.set(key.id, snapshot);
|
|
185
|
-
evalIdSeen.set(key.id, new Set());
|
|
186
|
-
}
|
|
187
|
-
snapshot.attempts.push(attempt);
|
|
188
|
-
const seen = evalIdSeen.get(key.id)!;
|
|
189
|
-
if (!seen.has(attempt.result.id)) {
|
|
190
|
-
seen.add(attempt.result.id);
|
|
191
|
-
snapshot.evalIds.push(attempt.result.id);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return [...byExperiment.values()];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
293
|
// ───────────────────────── 目录扫描 ─────────────────────────
|
|
198
294
|
|
|
199
|
-
/** 递归找出全部 summary.json(
|
|
200
|
-
async function findSummaryFiles(dir: string): Promise<string[]> {
|
|
295
|
+
/** 递归找出全部 summary.json(run 目录嵌套深度不做假设);skip 用于排除已收录的直达文件。 */
|
|
296
|
+
async function findSummaryFiles(dir: string, skip?: string): Promise<string[]> {
|
|
201
297
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
202
|
-
const direct = entries
|
|
298
|
+
const direct = entries
|
|
299
|
+
.filter((e) => e.isFile() && e.name === "summary.json")
|
|
300
|
+
.map((e) => join(dir, e.name))
|
|
301
|
+
.filter((p) => p !== skip);
|
|
203
302
|
const nested = await Promise.all(
|
|
204
|
-
entries.filter((e) => e.isDirectory()).map((e) => findSummaryFiles(join(dir, e.name))),
|
|
303
|
+
entries.filter((e) => e.isDirectory()).map((e) => findSummaryFiles(join(dir, e.name), skip)),
|
|
205
304
|
);
|
|
206
305
|
return [...direct, ...nested.flat()];
|
|
207
306
|
}
|
|
307
|
+
|
|
308
|
+
/** 目录下(递归)是否存在任何 attempt 工件文件 —— incomplete 判定的依据。 */
|
|
309
|
+
async function hasArtifactFiles(dir: string): Promise<boolean> {
|
|
310
|
+
const artifactNames = new Set<string>(ARTIFACT_KINDS.map((kind) => artifactFileOf(kind)));
|
|
311
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
312
|
+
for (const entry of entries) {
|
|
313
|
+
if (entry.isFile() && artifactNames.has(entry.name)) return true;
|
|
314
|
+
}
|
|
315
|
+
for (const entry of entries) {
|
|
316
|
+
if (entry.isDirectory() && (await hasArtifactFiles(join(dir, entry.name)))) return true;
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
322
|
+
try {
|
|
323
|
+
return (await stat(path)).isFile();
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|