niceeval 0.4.0 → 0.4.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/cli.md +2 -0
- package/package.json +1 -1
- package/src/cli.ts +2 -2
- package/src/runner/run.ts +1 -1
- package/src/view/index.ts +1 -1
- package/src/view/loader.test.ts +61 -0
- package/src/view/loader.ts +21 -5
package/docs/cli.md
CHANGED
|
@@ -61,6 +61,8 @@ niceeval exp compare --force # 忽略指纹缓存,全量重跑
|
|
|
61
61
|
niceeval exp compare --tag <tag> # 按单个标签过滤
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
不带 `--force` 时,续跑携带的基线是跨全部历史 run、每 `(experimentId, evalId)` 取最新一份(`src/view/loader.ts` 的 `loadLatestResultsPerEval`):上次 passed 且 fingerprint 匹配的直接携入新 summary,只真跑失败/errored/缺失的。所以「补齐一组实验」就是重跑 `niceeval exp <组>` 本身。注意带 eval-id 位置参数时,summary 只含本次计划内的 eval——补跑别用位置参数,否则产出部分快照(见 memory 的 rerun-with-eval-filter-partial-snapshot)。
|
|
65
|
+
|
|
64
66
|
## 评分与退出
|
|
65
67
|
|
|
66
68
|
```sh
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { Console as ConsoleReporter } from "./runner/reporters/console.ts";
|
|
|
17
17
|
import { JUnit } from "./runner/reporters/json.ts";
|
|
18
18
|
import { Live as LiveReporter, type LiveRow } from "./runner/reporters/live.ts";
|
|
19
19
|
import { Artifacts as ArtifactsReporter } from "./runner/reporters/artifacts.ts";
|
|
20
|
-
import { buildView, startViewServer,
|
|
20
|
+
import { buildView, startViewServer, loadLatestResultsPerEval, IncompatibleResultsError } from "./view/index.ts";
|
|
21
21
|
import { t } from "./i18n/index.ts";
|
|
22
22
|
import { formatThrown } from "./util.ts";
|
|
23
23
|
import type { Config, DiscoveredExperiment, Reporter } from "./types.ts";
|
|
@@ -435,7 +435,7 @@ async function main(): Promise<void> {
|
|
|
435
435
|
const sandboxRecs = agentRuns.map((r) => sandboxRecommendedConcurrency(r.sandbox));
|
|
436
436
|
const sandboxDefaultConcurrency = sandboxRecs.length > 0 ? Math.min(...sandboxRecs) : 10;
|
|
437
437
|
|
|
438
|
-
const priorResults = flags.force ? undefined : await
|
|
438
|
+
const priorResults = flags.force ? undefined : await loadLatestResultsPerEval(join(cwd, ".niceeval"));
|
|
439
439
|
|
|
440
440
|
const summary = await runEvals({
|
|
441
441
|
config,
|
package/src/runner/run.ts
CHANGED
|
@@ -70,7 +70,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
|
|
|
70
70
|
for (const r of opts.priorResults) {
|
|
71
71
|
if (!r.experimentId || !priorRunKeys.has(`${r.experimentId}|${r.id}`)) continue;
|
|
72
72
|
// artifactsDir 是相对"本次 run 自己目录"的路径,新 summary 换了目录就会失效,必须清掉。
|
|
73
|
-
// artifactBase 不一样:它已经是
|
|
73
|
+
// artifactBase 不一样:它已经是 loadLatestResultsPerEval(经 withViewRefs)拼好的、
|
|
74
74
|
// 相对稳定的 .niceeval 根的路径,指向旧 run 的产物目录依然可解析,原样带过来
|
|
75
75
|
// ——不然 view 就再也找不到这条携带结果的源码/转录/trace 了。
|
|
76
76
|
carriedResults.push({ ...r, artifactsDir: undefined });
|
package/src/view/index.ts
CHANGED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// loadLatestResultsPerEval:续跑携带基线必须是跨历史「每 (experimentId, evalId) 最新一份」,
|
|
2
|
+
// 而不是最近一个 run —— 否则部分补跑 run 会把基线清空,`exp <组>` 补齐失效。
|
|
3
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
7
|
+
import { loadLatestResultsPerEval } from "./loader.ts";
|
|
8
|
+
import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../runner/types.ts";
|
|
9
|
+
|
|
10
|
+
let root: string;
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
if (root) await rm(root, { recursive: true, force: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
async function writeRun(dir: string, startedAt: string, results: object[]): Promise<void> {
|
|
17
|
+
await mkdir(join(root, dir), { recursive: true });
|
|
18
|
+
await writeFile(
|
|
19
|
+
join(root, dir, "summary.json"),
|
|
20
|
+
JSON.stringify({ format: RESULTS_FORMAT, schemaVersion: RESULTS_SCHEMA_VERSION, startedAt, results }),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function result(experimentId: string, id: string, outcome: string, extra: object = {}): object {
|
|
25
|
+
return { experimentId, id, agent: "a", outcome, ...extra };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("loadLatestResultsPerEval", () => {
|
|
29
|
+
it("部分补跑 run 只遮蔽它跑过的 eval,其它 eval 仍取自更早的全量 run", async () => {
|
|
30
|
+
root = await mkdtemp(join(tmpdir(), "niceeval-loader-"));
|
|
31
|
+
await writeRun("2026-01-01T00-00-00", "2026-01-01T00:00:00.000Z", [
|
|
32
|
+
result("exp/a", "e1", "passed"),
|
|
33
|
+
result("exp/a", "e2", "errored"),
|
|
34
|
+
result("exp/b", "e1", "passed"),
|
|
35
|
+
]);
|
|
36
|
+
// 部分补跑:只重跑了 exp/a 的 e2
|
|
37
|
+
await writeRun("2026-01-02T00-00-00", "2026-01-02T00:00:00.000Z", [result("exp/a", "e2", "passed")]);
|
|
38
|
+
|
|
39
|
+
const results = (await loadLatestResultsPerEval(root)) as Array<{ experimentId: string; id: string; outcome: string }>;
|
|
40
|
+
const byKey = new Map(results.map((r) => [`${r.experimentId}|${r.id}`, r.outcome]));
|
|
41
|
+
expect(byKey.get("exp/a|e1")).toBe("passed"); // 来自旧全量 run,没被部分 run 冲掉
|
|
42
|
+
expect(byKey.get("exp/a|e2")).toBe("passed"); // 来自补跑 run(最新)
|
|
43
|
+
expect(byKey.get("exp/b|e1")).toBe("passed");
|
|
44
|
+
expect(results).toHaveLength(3);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("同 (experiment, eval) 多 attempt 整批取自含它的最新 run,不跨 run 混装", async () => {
|
|
48
|
+
root = await mkdtemp(join(tmpdir(), "niceeval-loader-"));
|
|
49
|
+
await writeRun("2026-01-01T00-00-00", "2026-01-01T00:00:00.000Z", [
|
|
50
|
+
result("exp/a", "e1", "passed", { fingerprint: "old" }),
|
|
51
|
+
]);
|
|
52
|
+
await writeRun("2026-01-02T00-00-00", "2026-01-02T00:00:00.000Z", [
|
|
53
|
+
result("exp/a", "e1", "failed", { fingerprint: "new" }),
|
|
54
|
+
result("exp/a", "e1", "passed", { fingerprint: "new" }),
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const results = (await loadLatestResultsPerEval(root)) as Array<{ fingerprint: string }>;
|
|
58
|
+
expect(results).toHaveLength(2);
|
|
59
|
+
expect(results.every((r) => r.fingerprint === "new")).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
package/src/view/loader.ts
CHANGED
|
@@ -87,11 +87,27 @@ export class NotAReportError extends Error {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
/**
|
|
91
|
+
* 读跨历史「每 (experimentId, evalId) 最新一份」的 EvalResult,供续跑携带已通过结果。
|
|
92
|
+
* 只看最近一个 run 不行:部分补跑(位置参数只跑几道题)会把携带基线换成那个部分 run,
|
|
93
|
+
* 之后重跑任何实验都携带不到东西,`exp <组>` 的「补齐缺失」语义随之失效。
|
|
94
|
+
* 同一 (experimentId, evalId) 的多个 attempt 整批取自含它的最新 run,不跨 run 混装。
|
|
95
|
+
*/
|
|
96
|
+
export async function loadLatestResultsPerEval(root = ".niceeval"): Promise<EvalResult[]> {
|
|
97
|
+
const { loaded } = await loadSummaries(root); // 已按 startedAt 降序
|
|
98
|
+
const claimed = new Set<string>();
|
|
99
|
+
const results: EvalResult[] = [];
|
|
100
|
+
for (const { summary } of loaded) {
|
|
101
|
+
const takenThisRun = new Set<string>();
|
|
102
|
+
for (const r of summary.results ?? []) {
|
|
103
|
+
const key = `${r.experimentId ?? `${r.agent}|${r.model ?? ""}`}|${r.id}`;
|
|
104
|
+
if (claimed.has(key)) continue;
|
|
105
|
+
takenThisRun.add(key);
|
|
106
|
+
results.push(r);
|
|
107
|
+
}
|
|
108
|
+
for (const key of takenThisRun) claimed.add(key);
|
|
109
|
+
}
|
|
110
|
+
return results;
|
|
95
111
|
}
|
|
96
112
|
|
|
97
113
|
/** 服务/解析工件的根目录:输入是目录就用它,是文件就用其所在目录。 */
|