niceeval 0.4.5 → 0.5.0

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.
Files changed (175) hide show
  1. package/docs-site/zh/concepts/adapter.mdx +6 -6
  2. package/docs-site/zh/concepts/experiment.mdx +7 -7
  3. package/docs-site/zh/concepts/overview.mdx +1 -1
  4. package/docs-site/zh/concepts/tier.mdx +6 -6
  5. package/docs-site/zh/guides/agent-feedback-loop.mdx +63 -20
  6. package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
  7. package/docs-site/zh/guides/custom-reports.mdx +243 -0
  8. package/docs-site/zh/guides/experiments.mdx +3 -3
  9. package/docs-site/zh/guides/report-components.mdx +252 -0
  10. package/docs-site/zh/guides/reporters.mdx +1 -1
  11. package/docs-site/zh/guides/results-data.mdx +224 -0
  12. package/docs-site/zh/guides/runner.mdx +1 -1
  13. package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
  14. package/docs-site/zh/guides/viewing-results.mdx +147 -77
  15. package/docs-site/zh/guides/write-experiment.mdx +9 -9
  16. package/docs-site/zh/guides/write-send.mdx +6 -6
  17. package/docs-site/zh/index.mdx +1 -1
  18. package/docs-site/zh/introduction.mdx +1 -1
  19. package/docs-site/zh/reference/cli.mdx +10 -2
  20. package/docs-site/zh/reference/define-agent.mdx +5 -5
  21. package/docs-site/zh/reference/define-config.mdx +1 -1
  22. package/docs-site/zh/reference/define-eval.mdx +3 -3
  23. package/package.json +3 -2
  24. package/src/agents/ai-sdk.test.ts +1 -1
  25. package/src/agents/ai-sdk.ts +2 -2
  26. package/src/agents/bub.ts +14 -4
  27. package/src/agents/claude-code.ts +1 -1
  28. package/src/agents/codex.ts +2 -2
  29. package/src/agents/streaming.test.ts +1 -1
  30. package/src/agents/types.ts +4 -4
  31. package/src/agents/ui-message-stream.test.ts +1 -1
  32. package/src/cli.ts +104 -14
  33. package/src/context/context.test.ts +1 -1
  34. package/src/context/context.ts +3 -3
  35. package/src/context/session.ts +2 -2
  36. package/src/context/types.ts +2 -2
  37. package/src/i18n/en.ts +29 -7
  38. package/src/i18n/zh-CN.ts +24 -2
  39. package/src/report/aggregate.ts +79 -50
  40. package/src/report/components.tsx +190 -0
  41. package/src/report/compute.ts +149 -84
  42. package/src/report/default-report.tsx +222 -0
  43. package/src/report/dual-face.test.tsx +527 -0
  44. package/src/report/format.ts +29 -0
  45. package/src/report/index.ts +79 -24
  46. package/src/report/load.ts +67 -0
  47. package/src/report/metrics.ts +2 -1
  48. package/src/report/param.ts +18 -0
  49. package/src/report/primitives.tsx +117 -0
  50. package/src/report/react/CaseList.tsx +2 -2
  51. package/src/report/react/DeltaTable.tsx +2 -2
  52. package/src/report/react/MetricBars.tsx +109 -0
  53. package/src/report/react/MetricLine.tsx +200 -0
  54. package/src/report/react/MetricMatrix.tsx +1 -1
  55. package/src/report/react/MetricScatter.tsx +1 -1
  56. package/src/report/react/MetricTable.tsx +1 -1
  57. package/src/report/react/RunOverview.tsx +4 -4
  58. package/src/report/react/Scoreboard.tsx +3 -3
  59. package/src/report/react/cell.tsx +1 -1
  60. package/src/report/react/fixtures.ts +98 -36
  61. package/src/report/react/format.ts +3 -28
  62. package/src/report/react/index.tsx +41 -24
  63. package/src/report/react/render.test.tsx +8 -8
  64. package/src/report/react/styles.css +77 -0
  65. package/src/report/report.test.ts +248 -128
  66. package/src/report/report.ts +69 -0
  67. package/src/report/text/faces.ts +356 -0
  68. package/src/report/text/layout.ts +125 -0
  69. package/src/report/text/plot.ts +127 -0
  70. package/src/report/tree.ts +220 -0
  71. package/src/report/types.ts +75 -27
  72. package/src/report/web.ts +40 -0
  73. package/src/results/copy.ts +95 -41
  74. package/src/results/format.ts +12 -11
  75. package/src/results/index.ts +34 -17
  76. package/src/results/open.ts +201 -81
  77. package/src/results/results.test.ts +595 -191
  78. package/src/results/select.ts +107 -55
  79. package/src/results/types.ts +137 -45
  80. package/src/results/writer.ts +258 -0
  81. package/src/runner/attempt.ts +3 -3
  82. package/src/runner/fingerprint.ts +1 -1
  83. package/src/runner/reporters/artifacts.ts +38 -78
  84. package/src/runner/reporters/braintrust.test.ts +2 -2
  85. package/src/runner/reporters/braintrust.ts +3 -3
  86. package/src/runner/run.ts +1 -1
  87. package/src/runner/types.ts +18 -12
  88. package/src/show/compose.ts +215 -0
  89. package/src/show/index.ts +263 -0
  90. package/src/show/render.ts +456 -0
  91. package/src/show/show.test.ts +505 -0
  92. package/src/view/app/App.tsx +85 -24
  93. package/src/view/app/components/CostScoreChart.tsx +29 -10
  94. package/src/view/app/components/ExperimentTable.tsx +27 -9
  95. package/src/view/app/components/GroupSelector.tsx +1 -1
  96. package/src/view/app/i18n.ts +18 -3
  97. package/src/view/app/lib/attempt-route.test.ts +9 -9
  98. package/src/view/app/lib/attempt-route.ts +5 -5
  99. package/src/view/app/lib/outcome.ts +1 -3
  100. package/src/view/app/lib/rows.ts +95 -15
  101. package/src/view/app/main.tsx +18 -6
  102. package/src/view/app/pages/RunsPage.tsx +4 -7
  103. package/src/view/app/pages/TracesPage.tsx +3 -8
  104. package/src/view/app/types.ts +50 -6
  105. package/src/view/client-dist/app.css +1 -1
  106. package/src/view/client-dist/app.js +20 -20
  107. package/src/view/data.test.ts +254 -0
  108. package/src/view/data.ts +336 -0
  109. package/src/view/index.ts +64 -10
  110. package/src/view/server.ts +31 -9
  111. package/src/view/shared/types.ts +47 -41
  112. package/src/view/styles.css +5 -0
  113. package/src/view/template.html +1 -0
  114. package/src/view/view-report.test.ts +308 -0
  115. package/docs-site/.mintignore +0 -13
  116. package/docs-site/AGENTS.md +0 -46
  117. package/docs-site/concepts/adapter.mdx +0 -287
  118. package/docs-site/concepts/assert.mdx +0 -243
  119. package/docs-site/concepts/drive.mdx +0 -118
  120. package/docs-site/concepts/evals.mdx +0 -108
  121. package/docs-site/concepts/experiment.mdx +0 -37
  122. package/docs-site/concepts/hitl.mdx +0 -83
  123. package/docs-site/concepts/judge.mdx +0 -129
  124. package/docs-site/concepts/overview.mdx +0 -98
  125. package/docs-site/concepts/tier.mdx +0 -42
  126. package/docs-site/docs-ref/00-index.md +0 -23
  127. package/docs-site/docs-ref/01-page-types.md +0 -43
  128. package/docs-site/docs-ref/03-style-rules.md +0 -45
  129. package/docs-site/docs-ref/04-code-examples.md +0 -37
  130. package/docs-site/docs-ref/05-dx-failure-paths.md +0 -45
  131. package/docs-site/docs-ref/06-checklists.md +0 -53
  132. package/docs-site/docs.json +0 -256
  133. package/docs-site/example/ai-agent-application.mdx +0 -148
  134. package/docs-site/example/claude-code-codex-plugin.mdx +0 -162
  135. package/docs-site/example/claude-code-codex-skill.mdx +0 -147
  136. package/docs-site/example/showcase.mdx +0 -39
  137. package/docs-site/example/tier1-ai-sdk-v7.mdx +0 -136
  138. package/docs-site/example/tier1-claude-sdk.mdx +0 -119
  139. package/docs-site/example/tier1-codex-sdk.mdx +0 -111
  140. package/docs-site/example/tier1-langgraph.mdx +0 -119
  141. package/docs-site/example/tier1-pi-sdk.mdx +0 -119
  142. package/docs-site/favicon.svg +0 -5
  143. package/docs-site/github-diff.css +0 -135
  144. package/docs-site/github-diff.js +0 -11
  145. package/docs-site/guides/authoring.mdx +0 -129
  146. package/docs-site/guides/ci-integration.mdx +0 -97
  147. package/docs-site/guides/connect-otel.mdx +0 -209
  148. package/docs-site/guides/connect-your-agent.mdx +0 -221
  149. package/docs-site/guides/dataset-fanout.mdx +0 -98
  150. package/docs-site/guides/experiments.mdx +0 -71
  151. package/docs-site/guides/fixtures.mdx +0 -147
  152. package/docs-site/guides/reporters.mdx +0 -113
  153. package/docs-site/guides/runner.mdx +0 -79
  154. package/docs-site/guides/sandbox-agent.mdx +0 -138
  155. package/docs-site/guides/sandbox-backends.mdx +0 -64
  156. package/docs-site/guides/scoring-guide.mdx +0 -92
  157. package/docs-site/guides/viewing-results.mdx +0 -195
  158. package/docs-site/guides/write-experiment.mdx +0 -81
  159. package/docs-site/guides/write-send.mdx +0 -347
  160. package/docs-site/index.mdx +0 -181
  161. package/docs-site/introduction.mdx +0 -141
  162. package/docs-site/quickstart.mdx +0 -136
  163. package/docs-site/reference/builtin-agents.mdx +0 -161
  164. package/docs-site/reference/capabilities.mdx +0 -76
  165. package/docs-site/reference/cli.mdx +0 -120
  166. package/docs-site/reference/define-agent.mdx +0 -168
  167. package/docs-site/reference/define-config.mdx +0 -41
  168. package/docs-site/reference/define-eval.mdx +0 -160
  169. package/docs-site/reference/events.mdx +0 -131
  170. package/docs-site/reference/expect.mdx +0 -112
  171. package/docs-site/tracker.js +0 -8
  172. package/src/report/react/data.ts +0 -17
  173. package/src/view/aggregate.ts +0 -103
  174. package/src/view/loader.test.ts +0 -61
  175. package/src/view/loader.ts +0 -257
@@ -0,0 +1,258 @@
1
+ // createRunWriter:Results Format 的写入面(定稿见 docs/results-lib.md「写:createRunWriter」)。
2
+ //
3
+ // writer 与 reader 是同一组类型的两半,而且是字面的两半:reader 的 attempt.result 由
4
+ // 「snapshot() 声明的快照级字段(experiment / agent / model / startedAt)+ writeAttempt 第一参」
5
+ // 拼成,快照级字段不在 attempt 参数类型里(AttemptEntry 的 Omit),不存在「谁的值为准」。
6
+ // 布局知识(时间戳目录、attempt 路径清洗、大字段拆工件、has* 回填、空数据不落文件)全在这里;
7
+ // src/runner/reporters/artifacts.ts 是本文件的薄壳。
8
+
9
+ import { mkdir, writeFile } from "node:fs/promises";
10
+ import { join } from "node:path";
11
+ import type { EvalResult, RunSummary, Usage } from "../types.ts";
12
+ import type { LocalizedText } from "../types.ts";
13
+ import type { DiffData, O11ySummary, SourceArtifact, StreamEvent, TraceSpan } from "../types.ts";
14
+ import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION } from "../types.ts";
15
+ import { attemptDirOf } from "./format.ts";
16
+
17
+ export interface RunWriterOptions {
18
+ /** 谁在写这份结果:niceeval 自己,或第三方 harness(name 如实写,别冒充 "niceeval")。 */
19
+ producer: NonNullable<RunSummary["producer"]>;
20
+ }
21
+
22
+ /** 快照级元数据的家:一个 experiment 开一个;这些字段不塞进每条 attempt。 */
23
+ export interface SnapshotDeclaration {
24
+ experiment: string;
25
+ agent: string;
26
+ model?: string;
27
+ /** 必填:身份键与去重以它为锚,官方产出永不缺。 */
28
+ startedAt: string;
29
+ /** 该实验已知的 eval 并集(残缺检测的分母);转换只覆盖部分题目时如实交代全集。 */
30
+ knownEvalIds?: string[];
31
+ }
32
+
33
+ /**
34
+ * writeAttempt 的第一参 = attempt 级条目:reader 的 attempt.result 中,快照级字段
35
+ * (agent / model / startedAt / experimentId)以外的全部;工件引用字段由 writer 回填。
36
+ */
37
+ export type AttemptEntry = Omit<
38
+ EvalResult,
39
+ | "agent"
40
+ | "model"
41
+ | "startedAt"
42
+ | "experimentId"
43
+ | "events"
44
+ | "sources"
45
+ | "o11y"
46
+ | "trace"
47
+ | "diff"
48
+ | "rawTranscript"
49
+ | "artifactsDir"
50
+ | "artifactBase"
51
+ | "artifactAbsBase"
52
+ | "hasTrace"
53
+ | "hasEvents"
54
+ | "hasSources"
55
+ >;
56
+
57
+ /** writeAttempt 的第二参:reader 懒加载能拿到的那几样工件,全部可选;缺哪样读取面就懒加载出 null。 */
58
+ export interface AttemptArtifacts {
59
+ events?: StreamEvent[];
60
+ trace?: TraceSpan[];
61
+ o11y?: O11ySummary;
62
+ diff?: DiffData;
63
+ sources?: SourceArtifact[];
64
+ }
65
+
66
+ export interface SnapshotWriter {
67
+ /** 增量落盘一条 attempt:拆工件文件、算 artifactsDir(含路径清洗)、回填 has* 引用;空数据不落文件。 */
68
+ writeAttempt(entry: AttemptEntry, artifacts?: AttemptArtifacts): Promise<void>;
69
+ }
70
+
71
+ /** finish() 从已写 attempt 推导 summary;个别推不出的字段走这里覆盖,不让调用方手拼整份 summary。 */
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;
81
+ /**
82
+ * @internal runner 专用:携带条目(--resume 合入)与最终排序只有调度器知道,Artifacts 薄壳
83
+ * 从这里交权威 results(可含内联大字段,writer 统一瘦身)。第三方转换不需要它。
84
+ */
85
+ results?: EvalResult[];
86
+ }
87
+
88
+ export interface RunWriter {
89
+ /** 本次 run 的输出目录(root 下的时间戳目录,: 与 . 已替换)。 */
90
+ readonly dir: string;
91
+ snapshot(decl: SnapshotDeclaration): SnapshotWriter;
92
+ /** 写出 summary.json 收尾;没走到这里的目录读取面归入 skipped("incomplete")。 */
93
+ finish(overrides?: FinishOverrides): Promise<RunSummary>;
94
+ /**
95
+ * @internal runner Artifacts 薄壳的增量工件落盘入口:runner 的条目自带 agent / model /
96
+ * experimentId / startedAt(且存在无 experiment 的普通 run),不经 snapshot() 声明。
97
+ */
98
+ writeAttemptArtifacts(result: EvalResult): Promise<void>;
99
+ }
100
+
101
+ export async function createRunWriter(root: string, opts: RunWriterOptions): Promise<RunWriter> {
102
+ const createdAt = new Date();
103
+ const dir = join(root, safeTimestamp(createdAt));
104
+ await mkdir(dir, { recursive: true });
105
+
106
+ const decls: SnapshotDeclaration[] = [];
107
+ const entries: EvalResult[] = [];
108
+ let finished = false;
109
+
110
+ const writeArtifacts = async (target: EvalResult | AttemptArtifacts, dirFor: EvalResult): Promise<void> => {
111
+ const attemptDest = join(dir, attemptDirOf(dirFor));
112
+ await mkdir(attemptDest, { recursive: true });
113
+ const writes: Promise<unknown>[] = [];
114
+ if (target.events?.length) writes.push(writeFile(join(attemptDest, "events.json"), JSON.stringify(target.events), "utf-8"));
115
+ if (target.sources?.length) writes.push(writeFile(join(attemptDest, "sources.json"), JSON.stringify(target.sources), "utf-8"));
116
+ if (target.trace?.length) writes.push(writeFile(join(attemptDest, "trace.json"), JSON.stringify(target.trace), "utf-8"));
117
+ if (target.o11y) writes.push(writeFile(join(attemptDest, "o11y.json"), JSON.stringify(target.o11y), "utf-8"));
118
+ if (target.diff) writes.push(writeFile(join(attemptDest, "diff.json"), JSON.stringify(target.diff), "utf-8"));
119
+ await Promise.all(writes);
120
+ };
121
+
122
+ return {
123
+ dir,
124
+
125
+ snapshot(decl: SnapshotDeclaration): SnapshotWriter {
126
+ if (!decl.experiment || !decl.agent || !decl.startedAt) {
127
+ throw new Error("writer.snapshot() requires experiment, agent and startedAt. They are snapshot-level identity: declare them once here instead of on each attempt.");
128
+ }
129
+ decls.push(decl);
130
+ return {
131
+ async writeAttempt(entry: AttemptEntry, artifacts?: AttemptArtifacts): Promise<void> {
132
+ // 快照级字段注入用「缺才补」:参数类型上它们不存在(Omit),运行时保守处理,
133
+ // 不覆盖调用方带来的值,也不打乱既有键序。
134
+ const full = { ...entry } as EvalResult;
135
+ full.experimentId ??= decl.experiment;
136
+ full.agent ??= decl.agent;
137
+ if (full.model === undefined && decl.model !== undefined) full.model = decl.model;
138
+ full.startedAt ??= decl.startedAt;
139
+ await writeArtifacts(artifacts ?? {}, full);
140
+ entries.push(
141
+ slimEntry({
142
+ ...full,
143
+ events: artifacts?.events,
144
+ sources: artifacts?.sources,
145
+ trace: artifacts?.trace,
146
+ o11y: artifacts?.o11y,
147
+ diff: artifacts?.diff,
148
+ }),
149
+ );
150
+ },
151
+ };
152
+ },
153
+
154
+ async writeAttemptArtifacts(result: EvalResult): Promise<void> {
155
+ await writeArtifacts(result, result);
156
+ },
157
+
158
+ async finish(overrides: FinishOverrides = {}): Promise<RunSummary> {
159
+ if (finished) throw new Error(`finish() already called for run directory ${dir}.`);
160
+ finished = true;
161
+
162
+ const results = overrides.results ? overrides.results.map(slimEntry) : entries;
163
+ const counts = { passed: 0, failed: 0, skipped: 0, errored: 0 };
164
+ let inTok = 0;
165
+ let outTok = 0;
166
+ let cost = 0;
167
+ let duration = 0;
168
+ for (const r of results) {
169
+ counts[r.outcome] += 1;
170
+ inTok += r.usage?.inputTokens ?? 0;
171
+ outTok += r.usage?.outputTokens ?? 0;
172
+ cost += r.estimatedCostUSD ?? 0;
173
+ duration += r.durationMs ?? 0;
174
+ }
175
+
176
+ const startedAt =
177
+ overrides.startedAt ??
178
+ decls.map((d) => d.startedAt).sort()[0] ??
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);
184
+
185
+ // 键序是持久化契约的一部分(与 runner 直写时代逐字节一致):format / schemaVersion /
186
+ // producer 在最前,results 与 outputDir 收尾,可选键缺席时不留 undefined 占位。
187
+ const summary: RunSummary = {
188
+ format: RESULTS_FORMAT,
189
+ schemaVersion: RESULTS_SCHEMA_VERSION,
190
+ producer: opts.producer,
191
+ ...(overrides.name !== undefined ? { name: overrides.name } : {}),
192
+ agent,
193
+ ...(model !== undefined ? { model } : {}),
194
+ startedAt,
195
+ completedAt: overrides.completedAt ?? new Date().toISOString(),
196
+ passed: counts.passed,
197
+ failed: counts.failed,
198
+ skipped: counts.skipped,
199
+ errored: counts.errored,
200
+ durationMs: overrides.durationMs ?? duration,
201
+ usage: overrides.usage ?? { inputTokens: inTok, outputTokens: outTok },
202
+ ...(estimatedCostUSD !== undefined ? { estimatedCostUSD } : {}),
203
+ results,
204
+ outputDir: dir,
205
+ ...(snapshotsMeta ? { snapshots: snapshotsMeta } : {}),
206
+ };
207
+ await writeFile(join(dir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
208
+ return summary;
209
+ },
210
+ };
211
+ }
212
+
213
+ /**
214
+ * 瘦身条目:去掉内联大字段,回填 artifactsDir 与 has* 引用。
215
+ * 携带条目(--resume 合入,rest 上带着 artifactBase 指向原 run 的工件目录)原样保留:
216
+ * 本轮没有任何新数据,不能重新推导出 false 的 has*,更不能编出一个本轮没写过文件的 artifactsDir。
217
+ */
218
+ export function slimEntry(r: EvalResult): EvalResult {
219
+ const { events, sources, o11y, trace, diff, rawTranscript, ...rest } = r;
220
+ void rawTranscript;
221
+ if (rest.artifactBase) return rest;
222
+ return {
223
+ ...rest,
224
+ artifactsDir: attemptDirOf(r),
225
+ hasTrace: !!(trace && trace.length),
226
+ hasEvents: !!(events && events.length),
227
+ hasSources: !!(sources && sources.length),
228
+ };
229
+ }
230
+
231
+ /** 快照级元数据落盘:startedAt 只在与顶层不同(一 run 多快照、时刻不同)时记;空 meta 不落字段。 */
232
+ function buildSnapshotsMeta(
233
+ decls: SnapshotDeclaration[],
234
+ summaryStartedAt: string,
235
+ ): RunSummary["snapshots"] | undefined {
236
+ const meta: NonNullable<RunSummary["snapshots"]> = {};
237
+ let any = false;
238
+ for (const decl of decls) {
239
+ const entry: { startedAt?: string; knownEvalIds?: string[] } = {};
240
+ if (decl.startedAt !== summaryStartedAt) entry.startedAt = decl.startedAt;
241
+ if (decl.knownEvalIds?.length) entry.knownEvalIds = [...decl.knownEvalIds];
242
+ if (Object.keys(entry).length > 0) {
243
+ // 同一 experiment 声明多次时后声明覆盖(knownEvalIds 取并集,覆盖事实只会变大不会缩水)。
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;
250
+ }
251
+ }
252
+ return any ? meta : undefined;
253
+ }
254
+
255
+ /** run 目录名:Date#toISOString 把 : 与 . 换成 -(与 docs/results-format.md 一致)。 */
256
+ export function safeTimestamp(d: Date): string {
257
+ return d.toISOString().replace(/[:.]/g, "-");
258
+ }
@@ -228,7 +228,7 @@ async function runAttemptBody(
228
228
  signal,
229
229
  model: run.model,
230
230
  reasoningEffort: run.reasoningEffort,
231
- flags: run.flags,
231
+ params: run.params,
232
232
  sandbox,
233
233
  session: createAgentSession(),
234
234
  telemetry,
@@ -270,7 +270,7 @@ async function runAttemptBody(
270
270
  sandbox,
271
271
  model: run.model,
272
272
  reasoningEffort: run.reasoningEffort,
273
- flags: run.flags,
273
+ params: run.params,
274
274
  signal,
275
275
  log,
276
276
  judge,
@@ -442,7 +442,7 @@ async function collectSources(
442
442
  function experimentRunInfo(run: AgentRun): EvalResult["experiment"] {
443
443
  return {
444
444
  id: run.experimentId,
445
- flags: run.flags,
445
+ params: run.params,
446
446
  runs: run.runs,
447
447
  earlyExit: run.earlyExit,
448
448
  sandbox: run.sandbox === undefined ? undefined : sandboxLabel(run.sandbox),
@@ -38,7 +38,7 @@ export async function computeFingerprint(
38
38
  experimentId: run.experimentId,
39
39
  agent: run.agent.name,
40
40
  model: run.model,
41
- flags: run.flags,
41
+ params: run.params,
42
42
  sandbox: run.sandbox === undefined ? undefined : sandboxLabel(run.sandbox),
43
43
  timeoutMs: run.timeoutMs,
44
44
  strict: run.strict,
@@ -1,15 +1,13 @@
1
1
  // 默认本地工件报告器:给 `niceeval view` 提供稳定的离线输入。
2
2
  //
3
- // 布局( eval-attempt 一个文件夹,重数据分文件;summary.json 只留榜单元数据):
4
- // .niceeval/<run>/
5
- // summary.json # run 元数据 + 各 result 的判决/usage/断言/引用(瘦身)
6
- // <evalId>/<agent>/<model>/a<attempt>/
7
- // events.json sources.json trace.json o11y.json diff.json
8
- // view 读 summary.json 渲染榜单,展开某条 trace 时再按需 fetch 它的 trace.json。
3
+ // 本文件是 niceeval/results 写入面(createRunWriter)的薄壳:订阅 reporter 事件转手调 writer,
4
+ // 自己不持有任何布局知识(时间戳目录、attempt 路径、大字段拆分、瘦身、版本元数据都在库内)。
5
+ // 落盘格式见 docs/results-format.md;每 eval-attempt 一个文件夹,重数据分文件,
6
+ // summary.json 只留榜单元数据,view 展开某条 trace 时再按需 fetch 它的 trace.json。
9
7
 
10
- import { mkdir, readFile, writeFile } from "node:fs/promises";
11
- import { join } from "node:path";
12
- import { RESULTS_FORMAT, RESULTS_SCHEMA_VERSION, type EvalResult, type Reporter, type RunSummary } from "../../types.ts";
8
+ import { readFile } from "node:fs/promises";
9
+ import type { Reporter } from "../../types.ts";
10
+ import { createRunWriter, type RunWriter } from "../../results/writer.ts";
13
11
 
14
12
  /** niceeval 自身的 npm 版本,写进 producer.version;版本不匹配时读取器靠它拼 npx 提示。 */
15
13
  let producerVersionPromise: Promise<string | undefined> | undefined;
@@ -20,91 +18,53 @@ function producerVersion(): Promise<string | undefined> {
20
18
  return producerVersionPromise;
21
19
  }
22
20
 
23
- /** 一个 attempt 的工件子目录(相对 run 根):<evalId>/<agent>/<model>[/<experiment>]/a<attempt>。 */
24
- function attemptDir(r: EvalResult): string {
25
- const safe = (s: string) => s.replace(/[^\w.@-]/g, "_");
26
- // evalId 里的 / 保留作目录层级,其余危险字符替换。
27
- const id = r.id.replace(/[^\w./@-]/g, "_");
28
- // experiment 段:两个实验可以同 agent 同 model、只差 flags(feature A/B),没有这一段
29
- // 它们的工件会互相覆盖。experimentId 里的 / 不作层级(压成 _),整个实验一格。
30
- const exp = r.experimentId ? `/${safe(r.experimentId)}` : "";
31
- return `${id}/${safe(r.agent)}/${safe(r.model ?? "default")}${exp}/a${r.attempt}`;
32
- }
33
-
34
- /** summary.json 用的瘦身结果:去掉大数组(events/trace/o11y/diff/sources),换成目录引用 + 存在标记。 */
35
- function slimResult(r: EvalResult): EvalResult {
36
- const { events, sources, o11y, trace, diff, rawTranscript, ...rest } = r;
37
- void events;
38
- void sources;
39
- void o11y;
40
- void trace;
41
- void diff;
42
- void rawTranscript;
43
- // 携带结果(跨实验复用上次 pass,见 run.ts 的 carriedResults):本轮没有任何新数据,
44
- // rest 上已经带着 artifactBase 指向旧 run 的产物目录,hasSources/hasEvents/hasTrace
45
- // 也是从旧 summary 带过来的真值——不能因为"这轮没数据"就重新推导成 false / 编出一个
46
- // 这轮压根没写过文件的新 artifactsDir(会让 artifactBase 在下次 withViewRefs 时被覆盖)。
47
- if (rest.artifactBase) return rest;
48
- return {
49
- ...rest,
50
- artifactsDir: attemptDir(r),
51
- hasTrace: !!(trace && trace.length),
52
- hasEvents: !!(events && events.length),
53
- hasSources: !!(sources && sources.length),
54
- };
55
- }
56
-
57
21
  /** Artifacts 报告器额外暴露输出目录:CLI 在 run 结束时打出 summary.json 路径给 agent 直读。 */
58
22
  export type ArtifactsReporter = Reporter & { outputDir(): string };
59
23
 
60
24
  export function Artifacts(root = ".niceeval"): ArtifactsReporter {
61
- let outputDir = "";
62
- const ensureDir = async (): Promise<void> => {
63
- if (!outputDir) outputDir = join(root, safeTimestamp(new Date()));
64
- await mkdir(outputDir, { recursive: true });
25
+ let writer: Promise<RunWriter> | undefined;
26
+ let dir = "";
27
+ const ensureWriter = (): Promise<RunWriter> => {
28
+ writer ??= (async () => {
29
+ const w = await createRunWriter(root, {
30
+ producer: { name: "niceeval", version: await producerVersion() },
31
+ });
32
+ dir = w.dir;
33
+ return w;
34
+ })();
35
+ return writer;
65
36
  };
66
37
 
67
38
  return {
68
- outputDir: () => outputDir,
39
+ outputDir: () => dir,
69
40
 
70
41
  async onRunStart() {
71
- outputDir = join(root, safeTimestamp(new Date()));
72
- await mkdir(outputDir, { recursive: true });
42
+ // 每次 run 开一个新的时间戳目录(同一个 reporter 实例可能被复用)
43
+ writer = undefined;
44
+ await ensureWriter();
73
45
  },
74
46
 
75
47
  // 每条结果一出来就把它的重数据落到自己的文件夹(增量、互不影响)。
48
+ // runner 的条目自带 agent / model / experimentId / startedAt(且存在无 experiment 的
49
+ // 普通 run),不经 snapshot() 声明,走 writer 的内部增量入口。
76
50
  async onEvalComplete(result) {
77
- await ensureDir();
78
- const dir = join(outputDir, attemptDir(result));
79
- await mkdir(dir, { recursive: true });
80
- const writes: Promise<unknown>[] = [];
81
- if (result.events?.length)
82
- writes.push(writeFile(join(dir, "events.json"), JSON.stringify(result.events), "utf-8"));
83
- if (result.sources?.length)
84
- writes.push(writeFile(join(dir, "sources.json"), JSON.stringify(result.sources), "utf-8"));
85
- if (result.trace?.length)
86
- writes.push(writeFile(join(dir, "trace.json"), JSON.stringify(result.trace), "utf-8"));
87
- if (result.o11y) writes.push(writeFile(join(dir, "o11y.json"), JSON.stringify(result.o11y), "utf-8"));
88
- if (result.diff) writes.push(writeFile(join(dir, "diff.json"), JSON.stringify(result.diff), "utf-8"));
89
- await Promise.all(writes);
51
+ await (await ensureWriter()).writeAttemptArtifacts(result);
90
52
  },
91
53
 
92
- // run 结束写瘦身 summary.json(版本元数据 + 榜单要的字段 + 工件引用)
54
+ // run 结束写瘦身 summary.json:携带条目(--resume 合入)与最终排序只有调度器知道,
55
+ // 权威 results 经 overrides 交给 writer,瘦身与版本元数据注入都在库内发生。
93
56
  async onRunComplete(summary) {
94
- await ensureDir();
95
- const slim: RunSummary = {
96
- format: RESULTS_FORMAT,
97
- schemaVersion: RESULTS_SCHEMA_VERSION,
98
- producer: { name: "niceeval", version: await producerVersion() },
99
- ...summary,
100
- outputDir,
101
- results: summary.results.map(slimResult),
102
- };
103
- await writeFile(join(outputDir, "summary.json"), JSON.stringify(slim, null, 2), "utf-8");
57
+ await (await ensureWriter()).finish({
58
+ ...(summary.name !== undefined ? { name: summary.name } : {}),
59
+ agent: summary.agent,
60
+ ...(summary.model !== undefined ? { model: summary.model } : {}),
61
+ startedAt: summary.startedAt,
62
+ completedAt: summary.completedAt,
63
+ durationMs: summary.durationMs,
64
+ ...(summary.usage !== undefined ? { usage: summary.usage } : {}),
65
+ ...(summary.estimatedCostUSD !== undefined ? { estimatedCostUSD: summary.estimatedCostUSD } : {}),
66
+ results: summary.results,
67
+ });
104
68
  },
105
69
  };
106
70
  }
107
-
108
- function safeTimestamp(d: Date): string {
109
- return d.toISOString().replace(/[:.]/g, "-");
110
- }
@@ -85,7 +85,7 @@ describe("toBraintrustEvent", () => {
85
85
  model: "gpt-5.2",
86
86
  attempt: 1,
87
87
  outcome: "failed",
88
- experiment: { id: "compare/codex", flags: { tape: true } },
88
+ experiment: { id: "compare/codex", params: { tape: true } },
89
89
  assertions: [{ name: "compiles", severity: "gate", score: 0, passed: false, detail: "tsc failed" }],
90
90
  }),
91
91
  );
@@ -96,7 +96,7 @@ describe("toBraintrustEvent", () => {
96
96
  outcome: "failed",
97
97
  model: "gpt-5.2",
98
98
  experiment: "compare/codex",
99
- flags: { tape: true },
99
+ params: { tape: true },
100
100
  failedAssertions: [{ name: "compiles", detail: "tsc failed" }],
101
101
  });
102
102
  expect(event.id).toBe("compare/codex|algebra/quadratic|codex|gpt-5.2|a1");
@@ -113,7 +113,7 @@ export function Braintrust(config: BraintrustConfig = {}): Reporter {
113
113
  * - scores:soft 断言按名字记分,gate 断言记在 `gate:` 前缀下 —— 实验 diff 里
114
114
  * gate 回归和 soft 分数回归用同一套机制看。重名断言追加 `#n` 消歧,不静默覆盖。
115
115
  * - metrics:start/end(Braintrust 由此算时长)+ token 用量 + 估算成本;缺就不写,不编 0。
116
- * - metadata:身份维度(agent / model / experiment / attempt / flags)+ 失败断言明细。
116
+ * - metadata:身份维度(agent / model / experiment / attempt / params)+ 失败断言明细。
117
117
  */
118
118
  export function toBraintrustEvent(result: EvalResult): BraintrustLogEvent {
119
119
  const scores: Record<string, number> = {};
@@ -150,8 +150,8 @@ export function toBraintrustEvent(result: EvalResult): BraintrustLogEvent {
150
150
  };
151
151
  if (result.model !== undefined) metadata.model = result.model;
152
152
  if (result.experimentId !== undefined) metadata.experiment = result.experimentId;
153
- if (result.experiment?.flags && Object.keys(result.experiment.flags).length > 0) {
154
- metadata.flags = result.experiment.flags;
153
+ if (result.experiment?.params && Object.keys(result.experiment.params).length > 0) {
154
+ metadata.params = result.experiment.params;
155
155
  }
156
156
  if (result.skipReason !== undefined) metadata.skipReason = result.skipReason;
157
157
  const failed = result.assertions
package/src/runner/run.ts CHANGED
@@ -104,7 +104,7 @@ export async function runEvals(opts: RunOptions): Promise<RunSummary> {
104
104
  if (run.experimentId && priorRunKeys.has(`${run.experimentId}|${evalDef.id}`)) continue;
105
105
  // key 标识「同一个运行配置下的同一条 eval」,earlyExit 的跳过/abort 只应作用于
106
106
  // 同 key 的重试轮。experimentId 必须进 key:两个实验可以同 agent 同 model、只差
107
- // flags(feature A/B 正是这种形状),漏掉它会让先过的实验把其它实验的同名 eval
107
+ // params(feature A/B 正是这种形状),漏掉它会让先过的实验把其它实验的同名 eval
108
108
  // 整个跳掉——花了钱还丢结果。
109
109
  const key = `${run.experimentId ?? ""}|${run.agent.name}|${run.model ?? ""}|${evalDef.id}`;
110
110
  attempts.push({
@@ -12,7 +12,7 @@ import type { TestContext } from "../context/types.ts";
12
12
 
13
13
  export interface ExperimentRunInfo {
14
14
  id?: string;
15
- flags?: Record<string, unknown>;
15
+ params?: Record<string, unknown>;
16
16
  runs?: number;
17
17
  earlyExit?: boolean;
18
18
  sandbox?: string;
@@ -49,9 +49,9 @@ export interface EvalResult {
49
49
  // ── 拆分工件的引用(Artifacts 报告器写 summary.json 时填;view 按需懒加载)──
50
50
  /** 本 attempt 工件目录(相对 run 根),下有 events/trace/o11y/diff.json。 */
51
51
  artifactsDir?: string;
52
- /** view 拼好的工件目录(相对 view 输入根,供前端 fetch);loadSummaries 注入。 */
52
+ /** 工件目录(相对结果根,供前端 fetch 与懒加载回退);view loader 注入,--resume 携带条目落盘时保留。 */
53
53
  artifactBase?: string;
54
- /** 工件目录的绝对路径;loadSummaries 注入,供复制/展示用。 */
54
+ /** 工件目录的绝对路径;历史字段,当前读取面不再注入。 */
55
55
  artifactAbsBase?: string;
56
56
  hasTrace?: boolean;
57
57
  hasEvents?: boolean;
@@ -61,15 +61,16 @@ export interface EvalResult {
61
61
  /** `summary.json` 的格式标记;把 niceeval 报告和其它工具的同名文件区分开。 */
62
62
  export const RESULTS_FORMAT = "niceeval.results";
63
63
  /** 结果格式版本,只在破坏兼容读取时递增;读取器只认相同版本,缺失按 1。见 docs/results-format.md。 */
64
- export const RESULTS_SCHEMA_VERSION = 1;
64
+ export const RESULTS_SCHEMA_VERSION = 2;
65
65
 
66
66
  export interface RunSummary {
67
67
  /** 恒为 "niceeval.results";和 schemaVersion、producer 一起构成持久化契约,永不移动或改名。 */
68
68
  format?: typeof RESULTS_FORMAT;
69
69
  /** 结果格式版本;与读取器不同即视为不兼容,提示用 producer.version 对应的 niceeval 查看。 */
70
70
  schemaVersion?: number;
71
- /** 写这份报告的 niceeval;version 唯一用途是拼 `npx niceeval@<version> view` 提示。 */
72
- producer?: { name: "niceeval"; version?: string; commit?: string };
71
+ /** 写这份报告的工具:niceeval 自己,或经 niceeval/results 写入面转换的第三方 harness。
72
+ * name === "niceeval" version 用于拼 `npx niceeval@<version> view` 提示;其它 name 如实报出。 */
73
+ producer?: { name: string; version?: string; commit?: string };
73
74
  /** 项目名(来自 config.name),透传给 `niceeval view` 顶部 hero 显示。 */
74
75
  name?: LocalizedText;
75
76
  agent: string;
@@ -87,13 +88,18 @@ export interface RunSummary {
87
88
  estimatedCostUSD?: number;
88
89
  results: EvalResult[];
89
90
  outputDir?: string;
91
+ /** 快照级元数据,按 experiment 键(缺 experimentId 时为 "<agent>/<model>" 合成键)存放:
92
+ * startedAt 只在与顶层 startedAt 不同(一个 run 装多份不同时刻的快照)时需要;
93
+ * knownEvalIds 是写入时刻该实验已知的 eval 并集(copySnapshots 自动补记,writer.snapshot 可声明)。
94
+ * 可选新增字段,不递增 schemaVersion(docs/results-format.md 版本规则)。 */
95
+ snapshots?: Record<string, { startedAt?: string; knownEvalIds?: string[] }>;
90
96
  }
91
97
 
92
- /** onRunStart 的运行规模:去重后 eval 数 × 配置(agent×model×flags)数 → 总运行(attempt)数。 */
98
+ /** onRunStart 的运行规模:去重后 eval 数 × 配置(agent×model×params)数 → 总运行(attempt)数。 */
93
99
  export interface RunShape {
94
100
  /** 去重后实际要跑的 eval 数(= evals.length)。 */
95
101
  evals: number;
96
- /** (agent, model, flags) 配置组合数;compare 多 agent 时 > 1。 */
102
+ /** (agent, model, params) 配置组合数;compare 多 agent 时 > 1。 */
97
103
  configs: number;
98
104
  /** 总 attempt 数(evals × configs × runs);逐行输出与汇总计数都按它。 */
99
105
  totalRuns: number;
@@ -168,8 +174,8 @@ export interface ExperimentDef {
168
174
  model?: string;
169
175
  /** 模型推理努力程度(如 "low"/"medium"/"high",取值由具体模型/adapter 决定);省略=用 agent 原生默认。经 ctx.reasoningEffort 透给 adapter 与 eval。 */
170
176
  reasoningEffort?: string;
171
- /** 传给每次 attempt 的 flags,经 t.flags 暴露给 eval;与 CLI flag 合并(CLI 优先)。 */
172
- flags?: Record<string, unknown>;
177
+ /** 传给每次 attempt 的 params,经 t.params 暴露给 eval;与 CLI flag 合并(CLI 优先)。 */
178
+ params?: Record<string, unknown>;
173
179
  /** 同一 eval 重复跑几次(结果各计一条 attempt);省略/CLI `--runs` 覆盖时默认 1。 */
174
180
  runs?: number;
175
181
  /** 一次重复(runs > 1)里某次 attempt 失败后是否跳过剩余重复;省略默认 true(提前退出省钱)。 */
@@ -261,12 +267,12 @@ export function runWho(run: Pick<AgentRun, "agent" | "model" | "experimentId">):
261
267
  return run.model ? `${run.agent.name}/${run.model}` : run.agent.name;
262
268
  }
263
269
 
264
- /** 一个 (agent, model, flags) 的运行配置 —— 由 CLI / 实验展开。 */
270
+ /** 一个 (agent, model, params) 的运行配置 —— 由 CLI / 实验展开。 */
265
271
  export interface AgentRun {
266
272
  agent: Agent;
267
273
  model?: string;
268
274
  reasoningEffort?: string;
269
- flags: Record<string, unknown>;
275
+ params: Record<string, unknown>;
270
276
  runs: number;
271
277
  earlyExit: boolean;
272
278
  sandbox?: SandboxOption;