niceeval 0.8.1 → 0.9.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 +77 -45
- package/dist/agents/types.d.ts +28 -6
- package/dist/i18n/en.d.ts +2 -0
- package/dist/i18n/zh-CN.d.ts +3 -1
- package/dist/report/built-in/index.d.ts +3 -2
- package/dist/report/built-in/index.js +7 -8
- package/dist/report/built-in/standard.d.ts +1 -0
- package/dist/report/built-in/standard.js +30 -0
- package/dist/report/components.d.ts +69 -2
- package/dist/report/components.js +152 -3
- package/dist/report/compute.d.ts +28 -1
- package/dist/report/compute.js +123 -0
- package/dist/report/index.d.ts +4 -4
- package/dist/report/index.js +3 -2
- package/dist/report/locale.d.ts +39 -1
- package/dist/report/locale.js +69 -0
- package/dist/report/react/AttemptList.d.ts +3 -1
- package/dist/report/react/AttemptList.js +3 -3
- package/dist/report/react/CopyFixPrompt.d.ts +12 -0
- package/dist/report/react/CopyFixPrompt.js +12 -0
- package/dist/report/react/HeroCard.d.ts +13 -0
- package/dist/report/react/HeroCard.js +35 -0
- package/dist/report/react/PoweredBy.d.ts +5 -0
- package/dist/report/react/PoweredBy.js +7 -0
- package/dist/report/react/ScopeWarnings.d.ts +12 -0
- package/dist/report/react/ScopeWarnings.js +18 -0
- package/dist/report/react/TraceWaterfall.d.ts +14 -0
- package/dist/report/react/TraceWaterfall.js +22 -0
- package/dist/report/react/index.d.ts +6 -1
- package/dist/report/react/index.js +6 -0
- package/dist/report/report.d.ts +22 -6
- package/dist/report/report.js +68 -55
- package/dist/report/scope-warnings.d.ts +28 -0
- package/dist/report/scope-warnings.js +101 -0
- package/dist/report/text/faces.d.ts +19 -1
- package/dist/report/text/faces.js +61 -0
- package/dist/report/tree.d.ts +1 -1
- package/dist/report/tree.js +7 -2
- package/dist/report/types.d.ts +45 -0
- package/dist/report/web.d.ts +5 -4
- package/dist/report/web.js +7 -22
- package/dist/results/select.d.ts +17 -4
- package/dist/results/select.js +76 -11
- package/dist/results/types.d.ts +26 -0
- package/dist/runner/fingerprint.d.ts +3 -3
- package/dist/runner/sandbox-selection.d.ts +12 -0
- package/dist/runner/types.d.ts +18 -6
- package/dist/sandbox/types.d.ts +12 -0
- package/dist/shared/aggregate.d.ts +1 -1
- package/dist/shared/aggregate.js +1 -1
- package/docs-site/zh/explanation/evals.mdx +2 -1
- package/docs-site/zh/explanation/experiment.mdx +2 -0
- package/docs-site/zh/how-to/custom-reports.mdx +23 -7
- package/docs-site/zh/how-to/experiments.mdx +2 -2
- package/docs-site/zh/how-to/publish-report.mdx +11 -4
- package/docs-site/zh/how-to/viewing-results.mdx +17 -8
- package/docs-site/zh/how-to/write-experiment.mdx +40 -1
- package/docs-site/zh/reference/builtin-agents.mdx +40 -3
- package/docs-site/zh/reference/cli.mdx +3 -3
- package/docs-site/zh/reference/define-eval.mdx +8 -0
- package/docs-site/zh/reference/official-adapters.mdx +10 -5
- package/docs-site/zh/troubleshooting/debugging.mdx +3 -3
- package/package.json +2 -1
- package/src/agents/bub.ts +13 -1
- package/src/agents/claude-code.test.ts +43 -1
- package/src/agents/claude-code.ts +32 -14
- package/src/agents/codex.test.ts +168 -1
- package/src/agents/codex.ts +51 -15
- package/src/agents/mcp.ts +31 -0
- package/src/agents/post-setup.ts +33 -0
- package/src/agents/types.ts +28 -7
- package/src/cli.ts +15 -14
- package/src/define.ts +3 -0
- package/src/i18n/en.ts +8 -5
- package/src/i18n/zh-CN.ts +8 -4
- package/src/index.ts +1 -0
- package/src/report/built-in/index.tsx +8 -7
- package/src/report/built-in/standard.tsx +59 -0
- package/src/report/components.tsx +218 -2
- package/src/report/compute.ts +138 -1
- package/src/report/dual-render.test.tsx +141 -14
- package/src/report/index.ts +20 -0
- package/src/report/locale.ts +83 -1
- package/src/report/react/AttemptList.tsx +13 -1
- package/src/report/react/CopyFixPrompt.tsx +37 -0
- package/src/report/react/HeroCard.tsx +59 -0
- package/src/report/react/PoweredBy.tsx +20 -0
- package/src/report/react/ScopeWarnings.tsx +74 -0
- package/src/report/react/TraceWaterfall.tsx +78 -0
- package/src/report/react/enhance.js +14 -0
- package/src/report/react/index.tsx +11 -0
- package/src/report/react/styles.css +193 -7
- package/src/report/report.ts +99 -64
- package/src/report/scope-warnings.ts +155 -0
- package/src/report/site-components.test.tsx +526 -0
- package/src/report/text/faces.ts +66 -0
- package/src/report/tree.ts +10 -3
- package/src/report/types.ts +51 -0
- package/src/report/web.ts +7 -40
- package/src/results/host-equivalence.test.ts +6 -2
- package/src/results/open.ts +5 -4
- package/src/results/results.test.ts +78 -1
- package/src/results/select.ts +80 -12
- package/src/results/types.ts +27 -0
- package/src/runner/attempt.ts +10 -9
- package/src/runner/discover.test.ts +9 -1
- package/src/runner/discover.ts +3 -3
- package/src/runner/fingerprint.ts +9 -4
- package/src/runner/ledger.test.ts +30 -1
- package/src/runner/ledger.ts +26 -4
- package/src/runner/run.ts +5 -1
- package/src/runner/sandbox-selection.test.ts +131 -0
- package/src/runner/sandbox-selection.ts +110 -0
- package/src/runner/types.ts +19 -2
- package/src/sandbox/types.ts +6 -0
- package/src/shared/aggregate.ts +1 -1
- package/src/show/index.ts +19 -12
- package/src/show/render.ts +12 -12
- package/src/show/report-host.test.ts +34 -17
- package/src/show/report-host.ts +6 -5
- package/src/show/show.test.ts +141 -4
- package/src/view/app/App.test.tsx +79 -17
- package/src/view/app/App.tsx +25 -74
- package/src/view/app/components/CopyControls.tsx +4 -42
- package/src/view/app/i18n.ts +5 -227
- package/src/view/app/lib/rows.ts +3 -21
- package/src/view/app/shared.ts +1 -3
- package/src/view/app/types.ts +2 -2
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +20 -20
- package/src/view/data.ts +21 -10
- package/src/view/index.ts +2 -12
- package/src/view/server.ts +4 -4
- package/src/view/shared/types.ts +11 -6
- package/src/view/styles.css +9 -252
- package/src/view/view-report.test.ts +99 -33
- package/src/view/app/components/LazyArtifact.tsx +0 -51
- package/src/view/app/components/SkippedRunsBanner.tsx +0 -140
- package/src/view/app/pages/AttemptsPage.tsx +0 -80
- package/src/view/app/pages/TracesPage.tsx +0 -35
package/src/report/types.ts
CHANGED
|
@@ -299,6 +299,57 @@ export interface ScopeSummaryData {
|
|
|
299
299
|
totalCostUSD: MetricCell;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
// ───────────────────────── 站点组件(Hero / CopyFixPrompt / TraceWaterfall)─────────────────────────
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* `HeroCard` 的数据(docs/feature/reports/library/site-components.md):站点标题区的
|
|
306
|
+
* 运行 meta——最后运行时间与快照合成来源。标题不在 data 里,它是站点声明与 Scope 的合成物,
|
|
307
|
+
* 经 `HeroCardProps.title` 传入。
|
|
308
|
+
*/
|
|
309
|
+
export interface HeroData {
|
|
310
|
+
/** Scope 中最新快照的开始时间;空 Scope 为 null,不编造当前时间。 */
|
|
311
|
+
latestStartedAt: string | null;
|
|
312
|
+
/** 贡献当前水位的快照数;大于 1 时 web 面标注「由 N 次运行合成」。 */
|
|
313
|
+
snapshots: number;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* `CopyFixPrompt` 的数据:resolve 期算好的修复 prompt 全文与参与的失败数
|
|
318
|
+
* (docs/feature/reports/library/site-components.md)。
|
|
319
|
+
*/
|
|
320
|
+
export interface CopyFixPromptData {
|
|
321
|
+
/** 修复 prompt 全文;失败逐条含 eval id、主失败摘要与 attempt 下钻命令。 */
|
|
322
|
+
prompt: string;
|
|
323
|
+
/** 参与 prompt 的失败 attempt 数(verdict 为 failed / errored)。 */
|
|
324
|
+
failures: number;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** `TraceWaterfall` 一行里的一个顶层 span 摘要(canonical OTel 字段归一后的形态)。 */
|
|
328
|
+
export interface TraceSpanSummary {
|
|
329
|
+
name: string;
|
|
330
|
+
/** 归一后的语义角色;turn 归入 agent,未识别落 other。 */
|
|
331
|
+
kind: "agent" | "model" | "tool" | "other";
|
|
332
|
+
/** 相对该 attempt trace 起点的偏移(毫秒)。 */
|
|
333
|
+
startOffsetMs: number;
|
|
334
|
+
durationMs: number;
|
|
335
|
+
/** span status 为 error 时 true(web 面失败标记的来源)。 */
|
|
336
|
+
failed: boolean;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* `TraceWaterfall` 一行 = 一次 attempt 的执行时间瀑布摘要。只画被测 agent 的原始 span
|
|
341
|
+
* (trace.json);runner 生命周期节点(`result.phases`)不进瀑布,组合视图归 attempt 详情。
|
|
342
|
+
*/
|
|
343
|
+
export interface TraceWaterfallRow {
|
|
344
|
+
experimentId: string;
|
|
345
|
+
evalId: string;
|
|
346
|
+
locator: AttemptLocator;
|
|
347
|
+
/** trace.json 缺失或为空时 null;行照常出现,证据位置如实显示缺失,不猜值。 */
|
|
348
|
+
durationMs: number | null;
|
|
349
|
+
/** 顶层 span 摘要,按 startOffsetMs 升序。 */
|
|
350
|
+
spans: readonly TraceSpanSummary[];
|
|
351
|
+
}
|
|
352
|
+
|
|
302
353
|
/** 一个可比组的数据;三个子块都只消费本组快照,不能含其它父目录的引用。 */
|
|
303
354
|
export interface ExperimentComparisonGroupData {
|
|
304
355
|
/** experiment id 的完整父路径;根目录 experiment 使用完整 id。 */
|
package/src/report/web.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// HTML 烘进查看器的报告槽。只有这一侧真正 import react-dom(import 边界即运行时边界),
|
|
3
3
|
// 所以本文件不从 niceeval/report 的入口 re-export —— 宿主与测试按源路径 import。
|
|
4
4
|
|
|
5
|
-
import * as React from "react";
|
|
6
5
|
import type { ReactNode } from "react";
|
|
7
6
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
8
7
|
import type { AttemptLocator } from "../results/locator.ts";
|
|
@@ -26,39 +25,11 @@ export interface StaticHtmlOptions {
|
|
|
26
25
|
locale?: ReportLocale;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
/**
|
|
30
|
-
* 挑选警告的 HTML 形态:宿主级前置块(`.nre nre-report-warnings` 外壳内一个
|
|
31
|
-
* `ul.nre-warnings` + `li.nre-warning[data-kind]`,复用 styles.css 已有样式)。
|
|
32
|
-
* 带 `command` 的警告把命令渲染为可复制块(`.nre-warning-command`);无 command 的
|
|
33
|
-
* 只显示 message,不硬造动作。经 renderToStaticMarkup 走 React,文本自动转义。
|
|
34
|
-
*/
|
|
35
|
-
function renderScopeWarningsHtml(scope: Scope): string {
|
|
36
|
-
return renderToStaticMarkup(
|
|
37
|
-
React.createElement(
|
|
38
|
-
"div",
|
|
39
|
-
{ className: "nre nre-report-warnings" },
|
|
40
|
-
React.createElement(
|
|
41
|
-
"ul",
|
|
42
|
-
{ className: "nre-warnings" },
|
|
43
|
-
scope.warnings.map((w, i) =>
|
|
44
|
-
React.createElement(
|
|
45
|
-
"li",
|
|
46
|
-
{ key: i, className: "nre-warning", "data-kind": w.kind },
|
|
47
|
-
w.message,
|
|
48
|
-
"command" in w && w.command
|
|
49
|
-
? React.createElement("code", { className: "nre-warning-command", "data-nre-copy": w.command }, w.command)
|
|
50
|
-
: null,
|
|
51
|
-
),
|
|
52
|
-
),
|
|
53
|
-
),
|
|
54
|
-
),
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
28
|
/**
|
|
59
29
|
* web 宿主的装载语义:选页 → resolve(组合展开 + spec 取数,唯一的 await 边界)→
|
|
60
|
-
* 树校验(与 text 宿主同一遍)→ 静态渲染 web
|
|
61
|
-
*
|
|
30
|
+
* 树校验(与 text 宿主同一遍)→ 静态渲染 web 面。宿主不在报告树外另设警告通道——
|
|
31
|
+
* 挑选警告的呈现件是 `ScopeWarnings` 组件,内建报告每页都放它,自定义报告放不放是
|
|
32
|
+
* 作者义务(docs/feature/reports/architecture.md「Scope 是计算入口」)。
|
|
62
33
|
*/
|
|
63
34
|
export async function renderReportToStaticHtml(
|
|
64
35
|
definition: ReportDefinition,
|
|
@@ -78,15 +49,13 @@ export async function renderReportToStaticHtml(
|
|
|
78
49
|
attemptHref: options?.attemptHref ?? ((locator) => `#/attempt/${locator}`),
|
|
79
50
|
locale: options?.locale ?? DEFAULT_REPORT_LOCALE,
|
|
80
51
|
};
|
|
81
|
-
|
|
82
|
-
const warnings = ctx.scope.warnings.length > 0 ? renderScopeWarningsHtml(ctx.scope) : "";
|
|
83
|
-
return warnings + body;
|
|
52
|
+
return runWithWebContext(webCtx, () => renderToStaticMarkup(resolved as ReactNode));
|
|
84
53
|
}
|
|
85
54
|
|
|
86
55
|
/**
|
|
87
56
|
* 渲染一页报告树的 web 面(宿主逐页调用;页选择归宿主):resolve → validate → 静态渲染。
|
|
88
|
-
*
|
|
89
|
-
*
|
|
57
|
+
* 挑选警告由页内的 `ScopeWarnings` 组件呈现,宿主不前置任何树外块。
|
|
58
|
+
* ctx.report 是宿主规范化后的声明。
|
|
90
59
|
*/
|
|
91
60
|
export async function renderReportTreeToStaticHtml(
|
|
92
61
|
tree: import("./tree.ts").ReportNode,
|
|
@@ -104,7 +73,5 @@ export async function renderReportTreeToStaticHtml(
|
|
|
104
73
|
attemptHref: options?.attemptHref ?? ((locator) => `#/attempt/${locator}`),
|
|
105
74
|
locale: options?.locale ?? DEFAULT_REPORT_LOCALE,
|
|
106
75
|
};
|
|
107
|
-
|
|
108
|
-
const warnings = ctx.scope.warnings.length > 0 ? renderScopeWarningsHtml(ctx.scope) : "";
|
|
109
|
-
return warnings + body;
|
|
76
|
+
return runWithWebContext(webCtx, () => renderToStaticMarkup(resolved as ReactNode));
|
|
110
77
|
}
|
|
@@ -126,7 +126,8 @@ interface NormExperiment {
|
|
|
126
126
|
type NormWarning =
|
|
127
127
|
| { kind: "partial-coverage"; experimentId: string; covered: number; total: number }
|
|
128
128
|
| { kind: "stale-snapshot"; experimentId: string; startedAt: string; latestStartedAt: string }
|
|
129
|
-
| { kind: "unfinished-snapshot"; experimentId: string; startedAt: string }
|
|
129
|
+
| { kind: "unfinished-snapshot"; experimentId: string; startedAt: string }
|
|
130
|
+
| { kind: "unreadable-snapshot"; reason: string };
|
|
130
131
|
interface NormSelection {
|
|
131
132
|
warnings: NormWarning[];
|
|
132
133
|
experiments: NormExperiment[];
|
|
@@ -141,6 +142,9 @@ function normalizeWarning(w: ScopeWarning): NormWarning {
|
|
|
141
142
|
case "unfinished-snapshot":
|
|
142
143
|
// dir 是宿主机绝对路径,归一化掉;身份靠 experimentId + startedAt。
|
|
143
144
|
return { kind: w.kind, experimentId: w.experimentId, startedAt: w.startedAt };
|
|
145
|
+
case "unreadable-snapshot":
|
|
146
|
+
// dir 是宿主机绝对路径,归一化掉;这个 kind 本就非实验作用域,没有 experimentId 可比。
|
|
147
|
+
return { kind: w.kind, reason: w.reason };
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
|
|
@@ -325,7 +329,7 @@ describe("selectCurrentResults · 现刻水位结构化身份", () => {
|
|
|
325
329
|
expect(algebra.warnings).toEqual([]);
|
|
326
330
|
});
|
|
327
331
|
|
|
328
|
-
it("场景8 --
|
|
332
|
+
it("场景8 --exp 分段前缀过滤:只留匹配段,不误配同前缀实验", async () => {
|
|
329
333
|
const root = await makeRoot();
|
|
330
334
|
await writeSnapshot(root, "2026-07-01T08-00-00-000Z", { experimentId: "compare/bub", startedAt: "2026-07-01T08:00:00.000Z" }, [res("q1", "passed")]);
|
|
331
335
|
await writeSnapshot(root, "2026-07-01T09-00-00-000Z", { experimentId: "compare/codex", agent: "codex", startedAt: "2026-07-01T09:00:00.000Z" }, [res("q1", "passed")]);
|
package/src/results/open.ts
CHANGED
|
@@ -150,7 +150,7 @@ export async function openResults(dir: string): Promise<Results> {
|
|
|
150
150
|
try {
|
|
151
151
|
targetStat = await stat(target);
|
|
152
152
|
} catch {
|
|
153
|
-
return makeResults([], []);
|
|
153
|
+
return makeResults([], [], target);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
if (targetStat.isFile()) {
|
|
@@ -165,17 +165,18 @@ export async function openResults(dir: string): Promise<Results> {
|
|
|
165
165
|
// 建 locator 索引:必须在全部快照扫完、Experiment 归组完成之后,返回 Results 之前——
|
|
166
166
|
// 撞车(LocatorCollisionError)在这里抛,不静默吞、不拖到消费方第一次 resolveLocator() 才发现。
|
|
167
167
|
const locatorIndex = buildAttemptLocatorIndex(experiments);
|
|
168
|
-
const results = makeResults(experiments, state.skipped);
|
|
168
|
+
const results = makeResults(experiments, state.skipped, target);
|
|
169
169
|
locatorIndexByResults.set(results, locatorIndex);
|
|
170
170
|
return results;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
function makeResults(experiments: Experiment[], skipped: SkippedDir[]): Results {
|
|
173
|
+
function makeResults(experiments: Experiment[], skipped: SkippedDir[], root: string): Results {
|
|
174
174
|
const results: Results = {
|
|
175
|
+
root,
|
|
175
176
|
experiments,
|
|
176
177
|
skipped,
|
|
177
178
|
latest(opts?: { experiments?: string | string[] }): Scope {
|
|
178
|
-
return selectLatest(
|
|
179
|
+
return selectLatest(results, opts);
|
|
179
180
|
},
|
|
180
181
|
current(opts?: { experiments?: string | string[] }): Scope {
|
|
181
182
|
return selectCurrentResults(results, { experiment: opts?.experiments });
|
|
@@ -375,13 +375,89 @@ describe("results.latest() · Selection", () => {
|
|
|
375
375
|
|
|
376
376
|
const filtered = latest.filter((s) => s.experimentId !== "mid/b");
|
|
377
377
|
expect(filtered.snapshots.map((s) => s.experimentId)).toEqual(["mid/a"]);
|
|
378
|
-
expect(filtered.warnings.map((w) => w.experimentId)).toEqual(["mid/a"]);
|
|
378
|
+
expect(filtered.warnings.map((w) => ("experimentId" in w ? w.experimentId : undefined))).toEqual(["mid/a"]);
|
|
379
379
|
// 原 Selection 不被改动。
|
|
380
380
|
expect(latest.snapshots).toHaveLength(2);
|
|
381
381
|
expect(latest.warnings).toHaveLength(2);
|
|
382
382
|
});
|
|
383
383
|
});
|
|
384
384
|
|
|
385
|
+
// ───────────────────────── unreadable-snapshot 警告 ─────────────────────────
|
|
386
|
+
|
|
387
|
+
describe("results.latest() / results.current() · unreadable-snapshot", () => {
|
|
388
|
+
it("malformed 快照产生 warning 且其余快照照常计入;无关 JSON 不产生 warning", async () => {
|
|
389
|
+
const root = await makeRoot();
|
|
390
|
+
const okDir = await writeSnapshot(root, "ok-exp", "2026-07-04T08-00-00-000Z-oooo", meta({ experimentId: "ok", agent: "bub", startedAt: "2026-07-04T08:00:00.000Z", completedAt: "2026-07-04T08:10:00.000Z" }));
|
|
391
|
+
await writeResultFile(okDir, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
392
|
+
|
|
393
|
+
const badDir = join(root, "bad-exp", "2026-07-02T08-00-00-000Z-zzzz");
|
|
394
|
+
await mkdir(badDir, { recursive: true });
|
|
395
|
+
await writeFile(join(badDir, "snapshot.json"), "not json {", "utf-8");
|
|
396
|
+
|
|
397
|
+
const alienDir = join(root, "alien-exp", "2026-07-06T08-00-00-000Z-alien");
|
|
398
|
+
await mkdir(alienDir, { recursive: true });
|
|
399
|
+
await writeFile(join(alienDir, "summary.json"), JSON.stringify({ hello: 1 }), "utf-8");
|
|
400
|
+
|
|
401
|
+
const results = await openResults(root);
|
|
402
|
+
const latest = results.latest();
|
|
403
|
+
// 其余快照照常计入,不被坏落盘拖垮。
|
|
404
|
+
expect(latest.snapshots.map((s) => s.experimentId)).toEqual(["ok"]);
|
|
405
|
+
|
|
406
|
+
const unreadable = latest.warnings.filter((w) => w.kind === "unreadable-snapshot");
|
|
407
|
+
expect(unreadable).toHaveLength(1); // 无关 JSON(alien)不产生 warning
|
|
408
|
+
expect(unreadable[0]).toMatchObject({ kind: "unreadable-snapshot", dir: badDir, reason: "malformed" });
|
|
409
|
+
expect(unreadable[0].message).toContain(badDir);
|
|
410
|
+
expect(unreadable[0].message).toMatch(/inspect .*snapshot\.json/);
|
|
411
|
+
expect((unreadable[0] as { command?: string }).command).toBeUndefined();
|
|
412
|
+
|
|
413
|
+
// results.current() 同一份事实(show / view 报告槽走的是它)。
|
|
414
|
+
const current = results.current();
|
|
415
|
+
expect(current.warnings.filter((w) => w.kind === "unreadable-snapshot")).toHaveLength(1);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("incompatible(schemaVersion 不兼容,niceeval producer)的 warning 带版本化 command", async () => {
|
|
419
|
+
const root = await makeRoot();
|
|
420
|
+
const oldDir = join(root, "old-exp", "2026-06-01T08-00-00-000Z");
|
|
421
|
+
await mkdir(oldDir, { recursive: true });
|
|
422
|
+
await writeFile(
|
|
423
|
+
join(oldDir, "snapshot.json"),
|
|
424
|
+
JSON.stringify({
|
|
425
|
+
format: RESULTS_FORMAT,
|
|
426
|
+
schemaVersion: RESULTS_SCHEMA_VERSION - 1,
|
|
427
|
+
producer: { name: "niceeval", version: "0.4.6" },
|
|
428
|
+
experimentId: "old",
|
|
429
|
+
agent: "bub",
|
|
430
|
+
startedAt: "2026-06-01T08:00:00.000Z",
|
|
431
|
+
}),
|
|
432
|
+
"utf-8",
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const latest = (await openResults(root)).latest();
|
|
436
|
+
const warn = latest.warnings.find((w) => w.kind === "unreadable-snapshot")!;
|
|
437
|
+
expect(warn).toMatchObject({ kind: "unreadable-snapshot", dir: oldDir, reason: "incompatible-version" });
|
|
438
|
+
expect((warn as { command?: string }).command).toBe(`npx niceeval@0.4.6 show --results ${root}`);
|
|
439
|
+
expect(warn.message).toContain("npx niceeval@0.4.6 show --results");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("非实验作用域:Selection.filter 收窄后 unreadable-snapshot warning 仍在", async () => {
|
|
443
|
+
const root = await makeRoot();
|
|
444
|
+
const aDir = await writeSnapshot(root, "mid_a", "s1", meta({ experimentId: "mid/a", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z", completedAt: "2026-07-01T08:10:00.000Z" }));
|
|
445
|
+
await writeResultFile(aDir, "q1/a1", record({ id: "q1", attempt: 1 }));
|
|
446
|
+
|
|
447
|
+
const badDir = join(root, "bad-exp", "2026-07-02T08-00-00-000Z-zzzz");
|
|
448
|
+
await mkdir(badDir, { recursive: true });
|
|
449
|
+
await writeFile(join(badDir, "snapshot.json"), "not json {", "utf-8");
|
|
450
|
+
|
|
451
|
+
const latest = (await openResults(root)).latest();
|
|
452
|
+
expect(latest.warnings.filter((w) => w.kind === "unreadable-snapshot")).toHaveLength(1);
|
|
453
|
+
|
|
454
|
+
// filter 只删快照,不删非实验作用域的警告 —— 即便过滤条件与该 warning 无关的实验都不复存在。
|
|
455
|
+
const filtered = latest.filter((s) => s.experimentId === "mid/a");
|
|
456
|
+
expect(filtered.snapshots).toHaveLength(1);
|
|
457
|
+
expect(filtered.warnings.filter((w) => w.kind === "unreadable-snapshot")).toHaveLength(1);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
385
461
|
// ───────────────────────── 身份键去重 ─────────────────────────
|
|
386
462
|
|
|
387
463
|
function fakeSnapshot(over: { experimentId: string; startedAt: string; dir: string }): Snapshot {
|
|
@@ -974,6 +1050,7 @@ describe("AttemptLocator · 落盘 / 读取 / 携带 / 撞车", () => {
|
|
|
974
1050
|
snapshot.attempts = [attempt];
|
|
975
1051
|
snapshot.evals = [{ id: "q1", attempts: [attempt] }];
|
|
976
1052
|
const handMadeResults: Results = {
|
|
1053
|
+
root: "/tmp/e",
|
|
977
1054
|
experiments: [{ id: "e", snapshots: [snapshot], latest: snapshot, evalIds: ["q1"] }],
|
|
978
1055
|
skipped: [],
|
|
979
1056
|
// filter() 本测试不调用,用不到,给个占位实现即可满足 Scope 接口。
|
package/src/results/select.ts
CHANGED
|
@@ -12,17 +12,23 @@ import type {
|
|
|
12
12
|
Results,
|
|
13
13
|
Scope,
|
|
14
14
|
ScopeWarning,
|
|
15
|
+
SkippedDir,
|
|
15
16
|
Snapshot,
|
|
16
17
|
} from "./types.ts";
|
|
17
18
|
import type { ExperimentRunInfo, JsonValue } from "../types.ts";
|
|
18
19
|
import { evalPrefixPredicate } from "../shared/aggregate.ts";
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Results.latest() 的实现:每个实验取最新一次快照(= exp.snapshots[0]),生成挑选警告。
|
|
23
|
+
* 收整个 `Results` 而不是裸 `Experiment[]`,是为了同时取 `skipped` / `root` 生成
|
|
24
|
+
* `unreadable-snapshot` 警告(非实验作用域,不受 `opts.experiments` 过滤 —— 那些落盘
|
|
25
|
+
* 本来就没能解析出 experimentId,没有前缀可过滤)。
|
|
26
|
+
*/
|
|
21
27
|
export function selectLatest(
|
|
22
|
-
|
|
28
|
+
results: Pick<Results, "experiments" | "skipped" | "root">,
|
|
23
29
|
opts?: { experiments?: string | string[] },
|
|
24
30
|
): Scope {
|
|
25
|
-
const selected = filterExperiments(experiments, opts?.experiments);
|
|
31
|
+
const selected = filterExperiments(results.experiments, opts?.experiments);
|
|
26
32
|
const snapshots = selected.map((exp) => exp.latest);
|
|
27
33
|
const warnings: ScopeWarning[] = [];
|
|
28
34
|
|
|
@@ -69,12 +75,13 @@ export function selectLatest(
|
|
|
69
75
|
});
|
|
70
76
|
}
|
|
71
77
|
}
|
|
78
|
+
warnings.push(...unreadableSnapshotWarnings(results.skipped, results.root));
|
|
72
79
|
return makeScope("latest-snapshots", snapshots, warnings);
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
/** selectCurrentResults 的范围输入:experiment id 前缀与 eval id 前缀,都可缺省。 */
|
|
76
83
|
export interface ResultScope {
|
|
77
|
-
/** experiment id 前缀(--
|
|
84
|
+
/** experiment id 前缀(--exp),分段匹配语义同 filterExperiments。 */
|
|
78
85
|
experiment?: string | string[];
|
|
79
86
|
/** eval id 前缀(位置参数),收窄 Scope 覆盖的 eval;覆盖警告分母同步收窄到范围内。 */
|
|
80
87
|
patterns?: string[];
|
|
@@ -235,9 +242,63 @@ export function selectCurrentResults(results: Results, scope: ResultScope = {}):
|
|
|
235
242
|
}
|
|
236
243
|
}
|
|
237
244
|
|
|
245
|
+
warnings.push(...unreadableSnapshotWarnings(results.skipped, results.root));
|
|
238
246
|
return makeScope("current-evals", snapshots, warnings);
|
|
239
247
|
}
|
|
240
248
|
|
|
249
|
+
/**
|
|
250
|
+
* `results.skipped` 里每一条不可读落盘 → 一条 `unreadable-snapshot` ScopeWarning。
|
|
251
|
+
* 非实验作用域(没有 experimentId 字段):`latest()` / `current()` 都原样带上全部
|
|
252
|
+
* `skipped` 条目,不受 `opts.experiments` 前缀过滤影响(那些落盘本来就没能解析出
|
|
253
|
+
* experimentId,没有前缀可比);`makeScope().filter()` 按「非实验作用域的警告保留」
|
|
254
|
+
* 规则自动放行,不需要额外分支。
|
|
255
|
+
*/
|
|
256
|
+
function unreadableSnapshotWarnings(skipped: readonly SkippedDir[], root: string): ScopeWarning[] {
|
|
257
|
+
return skipped.map((s): ScopeWarning => {
|
|
258
|
+
switch (s.reason) {
|
|
259
|
+
case "incompatible-version": {
|
|
260
|
+
const producer = s.producer;
|
|
261
|
+
const schemaText = s.schemaVersion !== undefined ? ` (schemaVersion ${s.schemaVersion})` : "";
|
|
262
|
+
if (producer?.name === "niceeval" && producer.version) {
|
|
263
|
+
const command = `npx niceeval@${producer.version} show --results ${root}`;
|
|
264
|
+
return {
|
|
265
|
+
kind: "unreadable-snapshot",
|
|
266
|
+
dir: s.dir,
|
|
267
|
+
reason: s.reason,
|
|
268
|
+
message: `snapshot at "${s.dir}" was written by niceeval ${producer.version}${schemaText} and cannot be read by this version; run \`${command}\` to open it`,
|
|
269
|
+
command,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const writtenBy = producer?.name
|
|
273
|
+
? `${producer.name}${producer.version ? ` ${producer.version}` : ""}`
|
|
274
|
+
: "an incompatible tool version";
|
|
275
|
+
return {
|
|
276
|
+
kind: "unreadable-snapshot",
|
|
277
|
+
dir: s.dir,
|
|
278
|
+
reason: s.reason,
|
|
279
|
+
message: `snapshot at "${s.dir}" was written by ${writtenBy}${schemaText} and cannot be read by this version; open it with the tool version that produced it`,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
case "malformed": {
|
|
283
|
+
const detail = s.detail ? ` (${s.detail})` : "";
|
|
284
|
+
return {
|
|
285
|
+
kind: "unreadable-snapshot",
|
|
286
|
+
dir: s.dir,
|
|
287
|
+
reason: s.reason,
|
|
288
|
+
message: `snapshot at "${s.dir}" is malformed${detail} and was skipped; inspect snapshot.json in that directory for corrupted JSON or a missing required field`,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
case "incomplete":
|
|
292
|
+
return {
|
|
293
|
+
kind: "unreadable-snapshot",
|
|
294
|
+
dir: s.dir,
|
|
295
|
+
reason: s.reason,
|
|
296
|
+
message: `snapshot at "${s.dir}" has attempt data but no snapshot.json (likely interrupted before metadata was written) and was skipped; inspect ${s.dir} — completed attempts remain on disk for manual review`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
241
302
|
/**
|
|
242
303
|
* Scope 构造:attempts 按口径物化(快照 attempts 的平铺);filter 只删不换 —— 快照删减,
|
|
243
304
|
* attempts 随之同步修剪,warnings 修剪规则是「experimentId 不在幸存快照中的丢弃,
|
|
@@ -307,7 +368,7 @@ export function isNewerSnapshot(a: Snapshot, b: Snapshot): boolean {
|
|
|
307
368
|
return a.dir.localeCompare(b.dir) > 0;
|
|
308
369
|
}
|
|
309
370
|
|
|
310
|
-
/** experiment id 分段前缀过滤(--
|
|
371
|
+
/** experiment id 分段前缀过滤(--exp / latest({ experiments }) 同一语义);包内使用,不进公共 barrel。 */
|
|
311
372
|
export function filterExperiments(experiments: Experiment[], filter?: string | string[]): Experiment[] {
|
|
312
373
|
if (filter === undefined) return experiments;
|
|
313
374
|
// 允许 "compare/" 这种带尾斜杠的写法,与 "compare" 等价;分段匹配不误配 "compare2"。
|
|
@@ -315,15 +376,22 @@ export function filterExperiments(experiments: Experiment[], filter?: string | s
|
|
|
315
376
|
return experiments.filter((exp) => prefixes.some((p) => exp.id === p || exp.id.startsWith(p + "/")));
|
|
316
377
|
}
|
|
317
378
|
|
|
318
|
-
/**
|
|
319
|
-
|
|
379
|
+
/**
|
|
380
|
+
* stale 警告的人话时距:选粒度最大的单位,四舍五入。结构化形态是单源——message 的英文时距
|
|
381
|
+
* 与 ScopeWarnings 徽标的本地化时距都从这里出,阈值不写两份。
|
|
382
|
+
*/
|
|
383
|
+
export function gapParts(fromIso: string, toIso: string): { n: number; unit: "second" | "minute" | "hour" | "day" } {
|
|
320
384
|
const ms = Math.max(0, Date.parse(toIso) - Date.parse(fromIso));
|
|
321
385
|
const seconds = Math.round(ms / 1000);
|
|
322
|
-
if (seconds < 90) return
|
|
386
|
+
if (seconds < 90) return { n: seconds, unit: "second" };
|
|
323
387
|
const minutes = Math.round(seconds / 60);
|
|
324
|
-
if (minutes < 90) return
|
|
388
|
+
if (minutes < 90) return { n: minutes, unit: "minute" };
|
|
325
389
|
const hours = Math.round(minutes / 60);
|
|
326
|
-
if (hours < 36) return
|
|
327
|
-
|
|
328
|
-
|
|
390
|
+
if (hours < 36) return { n: hours, unit: "hour" };
|
|
391
|
+
return { n: Math.round(hours / 24), unit: "day" };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function humanizeGap(fromIso: string, toIso: string): string {
|
|
395
|
+
const { n, unit } = gapParts(fromIso, toIso);
|
|
396
|
+
return `${n} ${unit}${n === 1 ? "" : "s"}`;
|
|
329
397
|
}
|
package/src/results/types.ts
CHANGED
|
@@ -160,6 +160,12 @@ export interface SkippedDir {
|
|
|
160
160
|
|
|
161
161
|
/** openResults 的返回:experiments 分层;skipped 不静默丢。 */
|
|
162
162
|
export interface Results {
|
|
163
|
+
/**
|
|
164
|
+
* 结果根目录的绝对路径(`openResults()` 入参解析后的原样值,不论传入的是结果根、
|
|
165
|
+
* 实验目录、快照目录还是某个 snapshot.json)。`unreadable-snapshot` 警告拼版本化
|
|
166
|
+
* `command`(`npx niceeval@<version> show --results <root>`)时取它。
|
|
167
|
+
*/
|
|
168
|
+
root: string;
|
|
163
169
|
/** 每个实验一项,挂着自己的全部历史(id 字典序)。 */
|
|
164
170
|
experiments: Experiment[];
|
|
165
171
|
skipped: SkippedDir[];
|
|
@@ -237,6 +243,27 @@ export type ScopeWarning =
|
|
|
237
243
|
message: string;
|
|
238
244
|
/** 一条可复制即跑的推进命令:`niceeval exp <experimentId>`。 */
|
|
239
245
|
command: string;
|
|
246
|
+
}
|
|
247
|
+
| {
|
|
248
|
+
/**
|
|
249
|
+
* 扫描结果根遇到的不可读快照:schema 不兼容、JSON 损坏 / 必需字段错误(malformed)、
|
|
250
|
+
* attempt 已写入但缺 `snapshot.json`(incomplete)。该快照被跳过,不挡其余结果
|
|
251
|
+
* (非 niceeval JSON 静默忽略,不产生这个 kind)。非实验作用域(没有 experimentId
|
|
252
|
+
* 字段) —— `Scope.filter()` 修剪时恒保留。
|
|
253
|
+
*/
|
|
254
|
+
kind: "unreadable-snapshot";
|
|
255
|
+
/** 该快照目录的绝对路径。 */
|
|
256
|
+
dir: string;
|
|
257
|
+
/** 与 `SkippedDir.reason` 同一取值集,原样透传。 */
|
|
258
|
+
reason: "incompatible-version" | "malformed" | "incomplete";
|
|
259
|
+
message: string;
|
|
260
|
+
/**
|
|
261
|
+
* 只有 reason 为 `incompatible-version` 且能确定是 niceeval 自己产出(`producer.name
|
|
262
|
+
* === "niceeval"` 且带 `producer.version`)时给出:`npx niceeval@<version> show --results
|
|
263
|
+
* <root>`。第三方 producer、版本信息缺失,或 reason 为 malformed / incomplete 时省略——
|
|
264
|
+
* 这些情况没有单条命令能解决,message 改给定位动作。
|
|
265
|
+
*/
|
|
266
|
+
command?: string;
|
|
240
267
|
};
|
|
241
268
|
|
|
242
269
|
/** dedupeAttempts 的警告:身份键缺 startedAt,宁可不去重也不误删。 */
|
package/src/runner/attempt.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { resolve as resolvePath } from "node:path";
|
|
7
7
|
import { readFile as readSourceFile } from "node:fs/promises";
|
|
8
8
|
import { Effect, Cause, Duration } from "effect";
|
|
9
|
-
import { createSandbox, resolveSandbox
|
|
9
|
+
import { createSandbox, resolveSandbox } from "../sandbox/resolve.ts";
|
|
10
10
|
import { stopSandbox, unregisterSandbox } from "../sandbox/registry.ts";
|
|
11
11
|
import { KEEPABLE_PROVIDERS, nativeEnterCommand, suspendSandbox } from "../sandbox/keep.ts";
|
|
12
12
|
import { keptEntryId, updateKeptEntry, writeKeptEntry } from "../sandbox/keep-registry.ts";
|
|
@@ -51,6 +51,7 @@ import type {
|
|
|
51
51
|
import { reportAttemptLifecycle, reportDiagnostic, reportKept } from "./feedback/sink.ts";
|
|
52
52
|
import { encodeAttemptKey, runWho } from "./types.ts";
|
|
53
53
|
import { commandDisplay, commandNode, createTimingRecorder, type TimingRecorder } from "./timing.ts";
|
|
54
|
+
import { sandboxForEval, sandboxProjection } from "./sandbox-selection.ts";
|
|
54
55
|
import type {
|
|
55
56
|
AgentRun,
|
|
56
57
|
Attempt,
|
|
@@ -81,7 +82,7 @@ export function runAttemptEffect(
|
|
|
81
82
|
id: evalDef.id,
|
|
82
83
|
description: evalDef.description,
|
|
83
84
|
experimentId: run.experimentId,
|
|
84
|
-
experiment: experimentRunInfo(run),
|
|
85
|
+
experiment: experimentRunInfo(run, config.sandbox),
|
|
85
86
|
agent: run.agent.name,
|
|
86
87
|
model: run.model,
|
|
87
88
|
verdict: "errored",
|
|
@@ -178,9 +179,9 @@ export function runAttemptEffect(
|
|
|
178
179
|
|
|
179
180
|
return Effect.scoped(
|
|
180
181
|
Effect.gen(function* () {
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
const sandboxSpec =
|
|
182
|
+
// 规划期按当前 eval 解析出的同一个 SandboxSpec,既用来起 provider,也作为
|
|
183
|
+
// sandbox.setup / sandbox.teardown 钩子(SandboxSpec.setup()/.teardown() 链式挂的)来源。
|
|
184
|
+
const sandboxSpec = a.sandboxSpec ?? sandboxForEval(run, evalDef, config.sandbox);
|
|
184
185
|
// defineSandbox 自定义 provider 不参与留存(事后命令不执行用户项目代码,新进程无法安全
|
|
185
186
|
// 找回用户对象上的 stopDetached);组合使用在创建沙箱前报清晰错误。
|
|
186
187
|
if (
|
|
@@ -809,7 +810,7 @@ async function runAttemptBody(
|
|
|
809
810
|
id: evalDef.id,
|
|
810
811
|
description: evalDef.description,
|
|
811
812
|
experimentId: run.experimentId,
|
|
812
|
-
experiment: experimentRunInfo(run),
|
|
813
|
+
experiment: experimentRunInfo(run, config.sandbox),
|
|
813
814
|
agent: run.agent.name,
|
|
814
815
|
model: run.model,
|
|
815
816
|
verdict,
|
|
@@ -832,7 +833,7 @@ async function runAttemptBody(
|
|
|
832
833
|
...(usesSandbox
|
|
833
834
|
? {
|
|
834
835
|
sandbox: {
|
|
835
|
-
provider: resolveSandbox(
|
|
836
|
+
provider: resolveSandbox(a.sandboxSpec ?? sandboxForEval(run, evalDef, config.sandbox)).provider,
|
|
836
837
|
sandboxId: sandbox.sandboxId,
|
|
837
838
|
},
|
|
838
839
|
}
|
|
@@ -1016,7 +1017,7 @@ async function collectSources(
|
|
|
1016
1017
|
|
|
1017
1018
|
/** 解析后运行配置的穷尽投影(ExperimentRunInfo,见 docs/feature/results/architecture.md):
|
|
1018
1019
|
* agent/model 只在快照顶层,这里不复制;sandbox 只经 provider 的公开参数投影落盘。 */
|
|
1019
|
-
function experimentRunInfo(run: AgentRun): EvalResult["experiment"] {
|
|
1020
|
+
function experimentRunInfo(run: AgentRun, configSandbox?: Config["sandbox"]): EvalResult["experiment"] {
|
|
1020
1021
|
return {
|
|
1021
1022
|
...(run.description !== undefined ? { description: run.description } : {}),
|
|
1022
1023
|
...(run.reasoningEffort !== undefined ? { reasoningEffort: run.reasoningEffort } : {}),
|
|
@@ -1028,7 +1029,7 @@ function experimentRunInfo(run: AgentRun): EvalResult["experiment"] {
|
|
|
1028
1029
|
...(run.maxConcurrency !== undefined ? { maxConcurrency: run.maxConcurrency } : {}),
|
|
1029
1030
|
selectedEvalIds: run.selectedEvalIds ?? [],
|
|
1030
1031
|
...(run.evalFilterFingerprint !== undefined ? { evalFilterFingerprint: run.evalFilterFingerprint } : {}),
|
|
1031
|
-
...(run
|
|
1032
|
+
...sandboxProjection(run, configSandbox),
|
|
1032
1033
|
};
|
|
1033
1034
|
}
|
|
1034
1035
|
|
|
@@ -6,7 +6,7 @@ import { describe, expect, it, afterEach } from "vitest";
|
|
|
6
6
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { discoverEvals } from "./discover.ts";
|
|
9
|
+
import { discoverEvals, makeFilter } from "./discover.ts";
|
|
10
10
|
import { captureEvalSource } from "./eval-source.ts";
|
|
11
11
|
|
|
12
12
|
const roots: string[] = [];
|
|
@@ -20,6 +20,14 @@ afterEach(async () => {
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
describe("discoverEvals · 源码捕获", () => {
|
|
23
|
+
// bug: memory/exp-eval-prefix-segment-drift.md
|
|
24
|
+
it("eval 位置参数按裸字面前缀命中 sibling,不要求路径段边界", () => {
|
|
25
|
+
const filter = makeFilter(["memory/terminal-swe-bench"]);
|
|
26
|
+
expect(filter("memory/terminal-swe-bench-astropy-1")).toBe(true);
|
|
27
|
+
expect(filter("memory/terminal-swe-bench-astropy-2")).toBe(true);
|
|
28
|
+
expect(filter("memory/other")).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
23
31
|
it("单个默认导出:source 与 captureEvalSource() 直接调出来的一致(路径/内容/哈希)", async () => {
|
|
24
32
|
const root = await makeRoot();
|
|
25
33
|
await mkdir(join(root, "evals"), { recursive: true });
|
package/src/runner/discover.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { dirname, join, relative, sep } from "node:path";
|
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { pad4 } from "../util.ts";
|
|
8
8
|
import { captureEvalSource } from "./eval-source.ts";
|
|
9
|
+
import { evalPrefixPredicate } from "../shared/aggregate.ts";
|
|
9
10
|
import type { DiscoveredEval, DiscoveredExperiment, EvalDef, ExperimentDef } from "../types.ts";
|
|
10
11
|
|
|
11
12
|
const SKIP_DIRS = new Set(["node_modules", ".git", ".niceeval", "dist", ".next"]);
|
|
@@ -108,8 +109,7 @@ export async function discoverExperiments(root: string): Promise<DiscoveredExper
|
|
|
108
109
|
return out;
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
/** id
|
|
112
|
+
/** eval id 的裸字面前缀过滤;exp / show / view 共用 shared helper,避免路径段语义漂移。 */
|
|
112
113
|
export function makeFilter(patterns: string[]): (id: string) => boolean {
|
|
113
|
-
|
|
114
|
-
return (id) => patterns.some((p) => id === p || id.startsWith(p + "/"));
|
|
114
|
+
return evalPrefixPredicate(patterns.length > 0 ? patterns : undefined);
|
|
115
115
|
}
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
|
-
import {
|
|
7
|
-
import type { DiscoveredEval, EvalResult } from "../types.ts";
|
|
6
|
+
import { sandboxRunInfo } from "../sandbox/resolve.ts";
|
|
7
|
+
import type { DiscoveredEval, EvalResult, SandboxOption } from "../types.ts";
|
|
8
8
|
import type { AgentRun } from "./types.ts";
|
|
9
|
+
import { prepareRunSandboxes, sandboxForEval } from "./sandbox-selection.ts";
|
|
9
10
|
|
|
10
11
|
export function cacheKey(run: AgentRun, evalId: string): string {
|
|
11
12
|
return `${run.experimentId ?? ""}|${evalId}`;
|
|
@@ -19,6 +20,7 @@ export async function computeFingerprint(
|
|
|
19
20
|
evalDef: DiscoveredEval,
|
|
20
21
|
run: AgentRun,
|
|
21
22
|
sourceCache?: Map<string, Promise<string>>,
|
|
23
|
+
configSandbox?: SandboxOption,
|
|
22
24
|
): Promise<string> {
|
|
23
25
|
let sourcePromise = sourceCache?.get(evalDef.sourcePath);
|
|
24
26
|
if (!sourcePromise) {
|
|
@@ -31,6 +33,7 @@ export async function computeFingerprint(
|
|
|
31
33
|
eval: {
|
|
32
34
|
id: evalDef.id,
|
|
33
35
|
tags: evalDef.tags ?? [],
|
|
36
|
+
environment: evalDef.environment,
|
|
34
37
|
metadata: evalDef.metadata ?? {},
|
|
35
38
|
timeoutMs: evalDef.timeoutMs,
|
|
36
39
|
},
|
|
@@ -39,7 +42,7 @@ export async function computeFingerprint(
|
|
|
39
42
|
agent: run.agent.name,
|
|
40
43
|
model: run.model,
|
|
41
44
|
flags: run.flags,
|
|
42
|
-
sandbox: run
|
|
45
|
+
sandbox: sandboxRunInfo(sandboxForEval(run, evalDef, configSandbox)),
|
|
43
46
|
timeoutMs: run.timeoutMs,
|
|
44
47
|
strict: run.strict,
|
|
45
48
|
},
|
|
@@ -66,14 +69,16 @@ export async function planCarry(
|
|
|
66
69
|
evals: DiscoveredEval[],
|
|
67
70
|
agentRuns: AgentRun[],
|
|
68
71
|
priorResults: EvalResult[] | undefined,
|
|
72
|
+
configSandbox?: SandboxOption,
|
|
69
73
|
): Promise<CarryPlan> {
|
|
74
|
+
prepareRunSandboxes(evals, agentRuns, configSandbox);
|
|
70
75
|
const sourceCache = new Map<string, Promise<string>>();
|
|
71
76
|
const plannedFingerprints = new Map<string, string>();
|
|
72
77
|
const jobs: Promise<void>[] = [];
|
|
73
78
|
for (const run of agentRuns) {
|
|
74
79
|
for (const evalDef of evals.filter((e) => run.evalFilter(e.id))) {
|
|
75
80
|
jobs.push(
|
|
76
|
-
computeFingerprint(evalDef, run, sourceCache).then((fp) => {
|
|
81
|
+
computeFingerprint(evalDef, run, sourceCache, configSandbox).then((fp) => {
|
|
77
82
|
plannedFingerprints.set(cacheKey(run, evalDef.id), fp);
|
|
78
83
|
}),
|
|
79
84
|
);
|