niceeval 0.5.2 → 0.5.4
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 +11 -6
- package/docs-site/zh/concepts/experiment.mdx +8 -6
- package/docs-site/zh/concepts/overview.mdx +1 -1
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/guides/ci-integration.mdx +2 -0
- package/docs-site/zh/guides/connect-your-agent.mdx +7 -7
- package/docs-site/zh/guides/custom-reports.mdx +13 -13
- package/docs-site/zh/guides/experiments.mdx +4 -3
- package/docs-site/zh/guides/publish-report.mdx +91 -0
- package/docs-site/zh/guides/report-components.mdx +4 -4
- package/docs-site/zh/guides/reporters.mdx +1 -1
- package/docs-site/zh/guides/results-data.mdx +2 -2
- package/docs-site/zh/guides/runner.mdx +1 -1
- package/docs-site/zh/guides/sandbox-agent.mdx +11 -6
- package/docs-site/zh/guides/sandbox-backends.mdx +27 -0
- package/docs-site/zh/guides/viewing-results.mdx +5 -21
- package/docs-site/zh/guides/write-experiment.mdx +24 -10
- 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/builtin-agents.mdx +4 -0
- package/docs-site/zh/reference/cli.mdx +9 -6
- package/docs-site/zh/reference/define-agent.mdx +19 -3
- package/docs-site/zh/reference/define-config.mdx +3 -1
- package/docs-site/zh/reference/define-eval.mdx +9 -3
- package/docs-site/zh/reference/events.mdx +4 -0
- package/docs-site/zh/reference/expect.mdx +4 -0
- 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/bub.ts +5 -1
- package/src/agents/claude-code.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/index.ts +2 -0
- package/src/agents/streaming.test.ts +1 -1
- package/src/agents/types.ts +10 -2
- package/src/agents/ui-message-stream.test.ts +1 -1
- package/src/cli.ts +38 -14
- package/src/context/context.test.ts +1 -1
- package/src/context/context.ts +6 -3
- package/src/context/session.ts +5 -2
- package/src/context/types.ts +2 -2
- package/src/define.test.ts +82 -0
- package/src/define.ts +68 -7
- package/src/i18n/en.ts +4 -2
- package/src/i18n/zh-CN.ts +4 -2
- package/src/report/aggregate.ts +16 -16
- package/src/report/components.tsx +1 -1
- package/src/report/compute.ts +7 -7
- package/src/report/dual-face.test.tsx +1 -1
- package/src/report/{param.ts → flag.ts} +6 -6
- package/src/report/index.ts +3 -3
- package/src/report/react/MetricLine.tsx +1 -1
- package/src/report/react/fixtures.ts +1 -1
- package/src/report/report.test.ts +17 -17
- package/src/report/report.ts +1 -1
- package/src/report/types.ts +11 -11
- package/src/results/results.test.ts +2 -2
- package/src/runner/attempt.ts +53 -6
- package/src/runner/fingerprint.ts +1 -1
- 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 +8 -8
- package/src/sandbox/types.ts +45 -4
- package/src/show/compose.ts +1 -1
- package/src/view/app/App.tsx +1 -1
- package/src/view/app/css.d.ts +3 -0
- package/src/view/app/i18n.ts +3 -3
- package/src/view/app/lib/rows.ts +5 -5
- package/src/view/app/types.ts +1 -1
- package/src/view/client-dist/app.js +2 -2
- package/src/view/data.test.ts +26 -2
- package/src/view/data.ts +28 -4
- package/src/view/shared/types.ts +1 -1
package/src/cli.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { stopAllSandboxes, liveSandboxCount } from "./sandbox/registry.ts";
|
|
|
17
17
|
import { evalLevelStats } from "./shared/outcome.ts";
|
|
18
18
|
import { sandboxRecommendedConcurrency } from "./sandbox/resolve.ts";
|
|
19
19
|
import { Console as ConsoleReporter } from "./runner/reporters/console.ts";
|
|
20
|
-
import { JUnit } from "./runner/reporters/json.ts";
|
|
20
|
+
import { Json, JUnit } from "./runner/reporters/json.ts";
|
|
21
21
|
import { Live as LiveReporter, type LiveRow } from "./runner/reporters/live.ts";
|
|
22
22
|
import { Artifacts as ArtifactsReporter } from "./runner/reporters/artifacts.ts";
|
|
23
23
|
import {
|
|
@@ -60,6 +60,7 @@ interface Flags {
|
|
|
60
60
|
budget?: number;
|
|
61
61
|
tag?: string;
|
|
62
62
|
junit?: string;
|
|
63
|
+
json?: string;
|
|
63
64
|
open?: boolean;
|
|
64
65
|
out?: string;
|
|
65
66
|
port?: number;
|
|
@@ -80,43 +81,69 @@ interface Flags {
|
|
|
80
81
|
|
|
81
82
|
// 表驱动的 flag 定义(node:util parseArgs)。--no-x 显式声明,不依赖 allowNegative(需 Node 20.14+,
|
|
82
83
|
// engines 是 >=18)。未知 flag 由 strict 模式报清晰错误,不再静默吞掉后面的位置参数。
|
|
84
|
+
//
|
|
85
|
+
// 每个 flag 的 JSDoc 就是它在 docs-site/zh/reference/cli.mdx flag 表里的说明,由
|
|
86
|
+
// scripts/generate-reference.ts 提取渲染——改 flag 语义时改这里的注释即可,不用碰生成脚本。
|
|
87
|
+
// 负向 flag(no-x)与正向 flag 合并成一行展示,不需要单独写 JSDoc。
|
|
83
88
|
const FLAG_OPTIONS = {
|
|
89
|
+
/** experiment 运行不支持该 flag。要换 agent,请在 `experiments/` 下新增或复制一个配置文件。 */
|
|
84
90
|
agent: { type: "string" },
|
|
91
|
+
/** experiment 运行不支持该 flag。要换模型,请新增或复制一个 experiment 文件并修改 `model`。 */
|
|
85
92
|
model: { type: "string" },
|
|
93
|
+
/** 每个 eval 运行多少次,常用于 pass@N。 */
|
|
86
94
|
runs: { type: "string" },
|
|
95
|
+
/** 设置同时运行的 eval 数量。 */
|
|
87
96
|
"max-concurrency": { type: "string" },
|
|
97
|
+
/** 单个 attempt 的超时时间,单位毫秒。 */
|
|
88
98
|
timeout: { type: "string" },
|
|
99
|
+
/** 整次运行的预算上限(美元)。 */
|
|
89
100
|
budget: { type: "string" },
|
|
101
|
+
/** 只运行带有该 tag 的 eval(见 `defineEval` 的 `tags`)。 */
|
|
90
102
|
tag: { type: "string" },
|
|
103
|
+
/** 额外写一份 JUnit XML 报告到指定路径,供 CI 消费。 */
|
|
91
104
|
junit: { type: "string" },
|
|
105
|
+
/** 额外写一份 JSON 结果(`RunSummary` 原样序列化)到指定路径,供 CI 或下游脚本消费。 */
|
|
106
|
+
json: { type: "string" },
|
|
107
|
+
/** `view` 命令专用:把结果查看器静态导出到指定目录。 */
|
|
92
108
|
out: { type: "string" },
|
|
109
|
+
/** `view` 命令专用:指定本地服务器监听端口。 */
|
|
93
110
|
port: { type: "string" },
|
|
94
|
-
// --sandbox 已移除(sandbox 归 config/experiment);留着解析是为了给出迁移提示而非「未知 flag」。
|
|
95
|
-
sandbox: { type: "string" },
|
|
96
|
-
// --watch / --json:文档曾接受、尚未实现的 flag。容忍(no-op)而不是硬报「未知 flag」,
|
|
97
|
-
// 免得照旧文档写的脚本直接崩;真正实现前不接任何行为。
|
|
98
|
-
watch: { type: "boolean" },
|
|
99
|
-
json: { type: "boolean" },
|
|
100
111
|
// show 的证据切面 / 时间轴 / 报告装载(docs-site/zh/guides/viewing-results.mdx)。
|
|
112
|
+
/** `show` 命令专用:渲染单个 eval 的完整对话与工具调用(证据切面)。 */
|
|
101
113
|
transcript: { type: "boolean" },
|
|
114
|
+
/** `show` 命令专用:渲染单个 eval 的 trace 瀑布文本版(证据切面)。 */
|
|
102
115
|
trace: { type: "boolean" },
|
|
103
116
|
// --diff 是布尔;--diff=<路径> 在 parseArgs 前预扫成 diffPath(路径必须 = 连写,
|
|
104
117
|
// 空格形式的下一个 token 仍是位置参数 = eval id 前缀,与文档一致)。
|
|
118
|
+
/** `show` 命令专用:sandbox 里的文件改动摘要;`--diff=<文件路径>` 看单个文件的完整改动(路径必须 `=` 连写)。 */
|
|
105
119
|
diff: { type: "boolean" },
|
|
120
|
+
/** `show` 命令专用:跨 run 时间轴,只列真实执行;与 `--report` 互斥。 */
|
|
106
121
|
history: { type: "boolean" },
|
|
122
|
+
/** `show` / `view` 命令专用:选集只留该实验。 */
|
|
107
123
|
experiment: { type: "string" },
|
|
124
|
+
/** `show` 命令专用:指定详情 / 证据切面看第几次 attempt(与展示一致的 1 计序号)。 */
|
|
108
125
|
attempt: { type: "string" },
|
|
126
|
+
/** `show` / `view` 命令专用:钉死看某一个结果目录(历史 run 或 `copySnapshots` 产物)。 */
|
|
109
127
|
run: { type: "string" },
|
|
128
|
+
/** `show` / `view` 命令专用:用你的报告文件替换默认报告(文件默认导出 `defineReport(...)`)。 */
|
|
110
129
|
report: { type: "string" },
|
|
130
|
+
/** 只打印本次会匹配到的 eval × 运行配置,不实际执行。 */
|
|
111
131
|
dry: { type: "boolean" },
|
|
132
|
+
/** 关闭控制台 / live 进度输出(reporter 仍会写 artifacts)。 */
|
|
112
133
|
quiet: { type: "boolean" },
|
|
134
|
+
/** 忽略上次运行结果,不跳过已通过的 (experiment, eval) 组合,强制全部重跑。 */
|
|
113
135
|
force: { type: "boolean" },
|
|
136
|
+
/** CI 中推荐使用:让软阈值(`soft`)失败也计入整条 eval 的 outcome。 */
|
|
114
137
|
strict: { type: "boolean" },
|
|
138
|
+
/** 某个 eval 的一次 attempt 通过后,停止该 eval 剩余的 attempts。 */
|
|
115
139
|
"early-exit": { type: "boolean" },
|
|
116
140
|
"no-early-exit": { type: "boolean" },
|
|
141
|
+
/** `view` 命令专用:启动后自动打开浏览器(默认行为)。 */
|
|
117
142
|
open: { type: "boolean" },
|
|
118
143
|
"no-open": { type: "boolean" },
|
|
144
|
+
/** 打印用法说明并退出。 */
|
|
119
145
|
help: { type: "boolean", short: "h" },
|
|
146
|
+
/** 打印 niceeval 的版本号并退出。 */
|
|
120
147
|
version: { type: "boolean", short: "v" },
|
|
121
148
|
} as const;
|
|
122
149
|
|
|
@@ -155,11 +182,6 @@ function parseArgs(argv: string[]): { command: string; positionals: string[]; fl
|
|
|
155
182
|
process.exit(1);
|
|
156
183
|
}
|
|
157
184
|
|
|
158
|
-
if (values.sandbox !== undefined) {
|
|
159
|
-
process.stderr.write(t("cli.sandboxFlagRemoved"));
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
185
|
// 第一个位置参数若是已知命令,则为命令;其余是 eval id 前缀 / view 输入。
|
|
164
186
|
const commands = new Set(["exp", "show", "list", "view", "clean", "init", "watch", "run"]);
|
|
165
187
|
let command = "run";
|
|
@@ -178,6 +200,7 @@ function parseArgs(argv: string[]): { command: string; positionals: string[]; fl
|
|
|
178
200
|
budget: numberFlag("budget", values.budget as string | undefined),
|
|
179
201
|
tag: values.tag as string | undefined,
|
|
180
202
|
junit: values.junit as string | undefined,
|
|
203
|
+
json: values.json as string | undefined,
|
|
181
204
|
out: values.out as string | undefined,
|
|
182
205
|
port: numberFlag("port", values.port as string | undefined),
|
|
183
206
|
dry: values.dry === true,
|
|
@@ -449,7 +472,7 @@ async function main(): Promise<void> {
|
|
|
449
472
|
agent: exp.agent,
|
|
450
473
|
model: exp.model,
|
|
451
474
|
reasoningEffort: exp.reasoningEffort,
|
|
452
|
-
|
|
475
|
+
flags: exp.flags ?? {},
|
|
453
476
|
runs: flags.runs ?? envNumber("NICEEVAL_RUNS") ?? exp.runs ?? 1,
|
|
454
477
|
earlyExit: flags.earlyExit ?? exp.earlyExit ?? true,
|
|
455
478
|
sandbox: exp.sandbox ?? config.sandbox,
|
|
@@ -465,7 +488,7 @@ async function main(): Promise<void> {
|
|
|
465
488
|
}
|
|
466
489
|
} else {
|
|
467
490
|
// 裸 run / `niceeval <eval>` 不再执行。运行配置必须来自 experiments/,
|
|
468
|
-
// 这样 agent/model/
|
|
491
|
+
// 这样 agent/model/flags/runs/budget 与结果聚合都有可签入的身份。
|
|
469
492
|
const experiments = await discoverExperiments(cwd);
|
|
470
493
|
const asExp = experiments.filter((e) =>
|
|
471
494
|
positionals.some((p) => e.group === p || e.id === p || e.id.startsWith(p + "/")),
|
|
@@ -526,6 +549,7 @@ async function main(): Promise<void> {
|
|
|
526
549
|
const artifacts = ArtifactsReporter();
|
|
527
550
|
reporters.push(artifacts);
|
|
528
551
|
if (flags.junit) reporters.push(JUnit(flags.junit));
|
|
552
|
+
if (flags.json) reporters.push(Json(flags.json));
|
|
529
553
|
reporters.push(...(config.reporters ?? []));
|
|
530
554
|
|
|
531
555
|
// Ctrl+C / kill 的三级响应,核心目标:任何情况下都不留下孤儿沙箱。
|
package/src/context/context.ts
CHANGED
|
@@ -61,7 +61,9 @@ export interface ContextDeps {
|
|
|
61
61
|
sandbox: Sandbox;
|
|
62
62
|
model?: string;
|
|
63
63
|
reasoningEffort?: string;
|
|
64
|
-
|
|
64
|
+
flags: Record<string, unknown>;
|
|
65
|
+
/** 路径推导出的实验 id(经 send ctx 透给 adapter,见 AgentContext.experimentId)。 */
|
|
66
|
+
experimentId?: string;
|
|
65
67
|
signal: AbortSignal;
|
|
66
68
|
log(msg: string): void;
|
|
67
69
|
judge: JudgeConfig | undefined;
|
|
@@ -92,7 +94,8 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
92
94
|
sandbox: deps.sandbox,
|
|
93
95
|
model: deps.model,
|
|
94
96
|
reasoningEffort: deps.reasoningEffort,
|
|
95
|
-
|
|
97
|
+
flags: deps.flags,
|
|
98
|
+
experimentId: deps.experimentId,
|
|
96
99
|
signal: deps.signal,
|
|
97
100
|
log: deps.log,
|
|
98
101
|
telemetry: deps.telemetry,
|
|
@@ -284,7 +287,7 @@ export function createEvalContext(deps: ContextDeps): { context: TestContext; st
|
|
|
284
287
|
signal: deps.signal,
|
|
285
288
|
model: deps.model,
|
|
286
289
|
reasoningEffort: deps.reasoningEffort,
|
|
287
|
-
|
|
290
|
+
flags: deps.flags,
|
|
288
291
|
log: deps.log,
|
|
289
292
|
skip: (reason: string) => {
|
|
290
293
|
if (reason.trim().length === 0) throw new Error(t("context.skipEmpty"));
|
package/src/context/session.ts
CHANGED
|
@@ -88,9 +88,11 @@ export interface SessionDeps {
|
|
|
88
88
|
sandbox: Sandbox;
|
|
89
89
|
model?: string;
|
|
90
90
|
reasoningEffort?: string;
|
|
91
|
-
|
|
91
|
+
flags: Record<string, unknown>;
|
|
92
92
|
signal: AbortSignal;
|
|
93
93
|
log(msg: string): void;
|
|
94
|
+
/** 路径推导出的实验 id(经 send ctx 透给 adapter,见 AgentContext.experimentId)。 */
|
|
95
|
+
experimentId?: string;
|
|
94
96
|
/** tracing agent 的 OTLP 端点(经 send ctx 透给 adapter,用于注入导出 env)。 */
|
|
95
97
|
telemetry?: Telemetry;
|
|
96
98
|
/** 非沙箱 tracing agent 的共享 OTLP 通道(runner 从 run 级池取,经它做逐轮 span 归属)。 */
|
|
@@ -138,7 +140,8 @@ export class SessionManager {
|
|
|
138
140
|
signal: this.deps.signal,
|
|
139
141
|
model: this.deps.model,
|
|
140
142
|
reasoningEffort: this.deps.reasoningEffort,
|
|
141
|
-
|
|
143
|
+
flags: this.deps.flags,
|
|
144
|
+
experimentId: this.deps.experimentId,
|
|
142
145
|
sandbox: this.deps.sandbox,
|
|
143
146
|
session,
|
|
144
147
|
telemetry: this.deps.telemetry,
|
package/src/context/types.ts
CHANGED
|
@@ -274,8 +274,8 @@ export interface TestContext {
|
|
|
274
274
|
readonly model?: string;
|
|
275
275
|
/** 本次 attempt 的推理努力程度(如 "low"/"medium"/"high",取值由 adapter/模型决定)。 */
|
|
276
276
|
readonly reasoningEffort?: string;
|
|
277
|
-
/** 本次 attempt
|
|
278
|
-
readonly
|
|
277
|
+
/** 本次 attempt 生效的实验 flags(experiment.flags 的只读视图;实验条件,非命令行开关)。 */
|
|
278
|
+
readonly flags: Readonly<Record<string, unknown>>;
|
|
279
279
|
/** 打一行调试日志;有 live 进度回调时走该回调,否则落到 stderr,不出现在最终结果里。 */
|
|
280
280
|
log(msg: string): void;
|
|
281
281
|
/** 立即中止本 eval 并标记为 skipped(outcome / EvalResult.skipReason),reason 不能为空。 */
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// SandboxSpec 链式钩子(.setup()/.teardown())的构造期契约:不可变、多次追加按顺序累加。
|
|
2
|
+
// 执行顺序(setup 正序 / teardown 逆序 / LIFO cleanup)是 runner 的事,见
|
|
3
|
+
// test/e2e-sandbox-hooks.test.ts;这里只测 dockerSandbox()/vercelSandbox()/e2bSandbox()/
|
|
4
|
+
// defineSandbox() 这四个工厂产出的 spec 对象本身的构造行为。
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { dockerSandbox, e2bSandbox, vercelSandbox, defineSandbox } from "./define.ts";
|
|
7
|
+
import type { AgentSetup, AgentTeardown } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
const noopSetup: AgentSetup = () => {};
|
|
10
|
+
const noopTeardown: AgentTeardown = () => {};
|
|
11
|
+
|
|
12
|
+
describe("sandbox factories: .setup()/.teardown() chain", () => {
|
|
13
|
+
it("dockerSandbox() starts with empty hook arrays", () => {
|
|
14
|
+
const spec = dockerSandbox({ image: "custom:latest" });
|
|
15
|
+
expect(spec.backend).toBe("docker");
|
|
16
|
+
expect(spec.image).toBe("custom:latest");
|
|
17
|
+
expect(spec.setupHooks).toEqual([]);
|
|
18
|
+
expect(spec.teardownHooks).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it(".setup() returns a new spec and does not mutate the original", () => {
|
|
22
|
+
const base = dockerSandbox();
|
|
23
|
+
const withSetup = base.setup(noopSetup);
|
|
24
|
+
expect(withSetup).not.toBe(base);
|
|
25
|
+
expect(base.setupHooks).toEqual([]);
|
|
26
|
+
expect(withSetup.setupHooks).toEqual([noopSetup]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("multiple .setup() calls accumulate in append order", () => {
|
|
30
|
+
const a: AgentSetup = () => {};
|
|
31
|
+
const b: AgentSetup = () => {};
|
|
32
|
+
const spec = dockerSandbox().setup(a).setup(b);
|
|
33
|
+
expect(spec.setupHooks).toEqual([a, b]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("multiple .teardown() calls accumulate in append order (execution order is the runner's job)", () => {
|
|
37
|
+
const x: AgentTeardown = () => {};
|
|
38
|
+
const y: AgentTeardown = () => {};
|
|
39
|
+
const spec = dockerSandbox().teardown(x).teardown(y);
|
|
40
|
+
expect(spec.teardownHooks).toEqual([x, y]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("chaining preserves backend-specific fields and stacks across calls", () => {
|
|
44
|
+
const spec = dockerSandbox({ image: "img" }).setup(noopSetup).teardown(noopTeardown).setup(noopSetup);
|
|
45
|
+
expect(spec.backend).toBe("docker");
|
|
46
|
+
expect(spec.image).toBe("img");
|
|
47
|
+
expect(spec.setupHooks).toHaveLength(2);
|
|
48
|
+
expect(spec.teardownHooks).toHaveLength(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("vercelSandbox() / e2bSandbox() chain the same way and keep their own fields", () => {
|
|
52
|
+
const vercel = vercelSandbox({ snapshotId: "snap-1" }).setup(noopSetup);
|
|
53
|
+
expect(vercel.backend).toBe("vercel");
|
|
54
|
+
expect(vercel.snapshotId).toBe("snap-1");
|
|
55
|
+
expect(vercel.setupHooks).toEqual([noopSetup]);
|
|
56
|
+
|
|
57
|
+
const e2b = e2bSandbox({ template: "niceeval-agents" }).teardown(noopTeardown);
|
|
58
|
+
expect(e2b.backend).toBe("e2b");
|
|
59
|
+
expect(e2b.template).toBe("niceeval-agents");
|
|
60
|
+
expect(e2b.teardownHooks).toEqual([noopTeardown]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("defineSandbox() (custom backend) chains too and keeps create()/name", async () => {
|
|
64
|
+
const create = async () => {
|
|
65
|
+
throw new Error("not called in this test");
|
|
66
|
+
};
|
|
67
|
+
const spec = defineSandbox({ name: "my-backend", create }).setup(noopSetup).setup(noopSetup);
|
|
68
|
+
expect(spec.backend).toBe("my-backend");
|
|
69
|
+
expect(spec.create).toBe(create);
|
|
70
|
+
expect(spec.setupHooks).toHaveLength(2);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("each chain call produces an independent object (no shared mutable state)", () => {
|
|
74
|
+
const base = dockerSandbox();
|
|
75
|
+
const other: AgentSetup = () => {};
|
|
76
|
+
const branchA = base.setup(noopSetup);
|
|
77
|
+
const branchB = base.setup(other);
|
|
78
|
+
expect(branchA.setupHooks).toEqual([noopSetup]);
|
|
79
|
+
expect(branchB.setupHooks).toEqual([other]);
|
|
80
|
+
expect(branchA.setupHooks).not.toBe(branchB.setupHooks);
|
|
81
|
+
});
|
|
82
|
+
});
|
package/src/define.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
Agent,
|
|
6
|
+
AgentSetup,
|
|
7
|
+
AgentTeardown,
|
|
6
8
|
Config,
|
|
7
9
|
CustomSandboxSpec,
|
|
8
10
|
DockerSandboxSpec,
|
|
@@ -11,6 +13,7 @@ import type {
|
|
|
11
13
|
ExperimentDef,
|
|
12
14
|
RemoteAgentDef,
|
|
13
15
|
SandboxAgentDef,
|
|
16
|
+
SandboxHooks,
|
|
14
17
|
VercelSandboxSpec,
|
|
15
18
|
} from "./types.ts";
|
|
16
19
|
import { t } from "./i18n/index.ts";
|
|
@@ -71,20 +74,70 @@ export function defineConfig(config: Config): Config {
|
|
|
71
74
|
// ───────────────────────── Sandbox 工厂 ─────────────────────────
|
|
72
75
|
// Sandbox 与 agent 一样用数据结构带参数(见 docs/sandbox.md)。这些工厂只是把
|
|
73
76
|
// 后端 + 参数包成 spec 对象;真正的行为在 sandbox/<backend>.ts 里,由 resolve.ts 派发。
|
|
77
|
+
//
|
|
78
|
+
// 四个工厂都要挂上 `.setup()` / `.teardown()` 链式方法(见 sandbox/types.ts 的
|
|
79
|
+
// SandboxHooks<Self>):累积的钩子数组随 `HookState` 传递,每次链式调用都重新调
|
|
80
|
+
// `build()` 产出一个新对象,原 spec 不被修改。
|
|
81
|
+
|
|
82
|
+
/** 链式追加中累积的钩子(setup 按追加顺序执行,teardown 执行时逆序,见 SandboxHooks)。 */
|
|
83
|
+
interface HookState {
|
|
84
|
+
readonly setupHooks: readonly AgentSetup[];
|
|
85
|
+
readonly teardownHooks: readonly AgentTeardown[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 四个工厂共用:把当前钩子状态包成 `.setup()` / `.teardown()` 方法,调用即用新状态重新 build。 */
|
|
89
|
+
function hookMethods<TSpec>(
|
|
90
|
+
state: HookState,
|
|
91
|
+
rebuild: (state: HookState) => TSpec,
|
|
92
|
+
): Pick<SandboxHooks<TSpec>, "setup" | "teardown"> {
|
|
93
|
+
return {
|
|
94
|
+
setup: (fn) => rebuild({ setupHooks: [...state.setupHooks, fn], teardownHooks: state.teardownHooks }),
|
|
95
|
+
teardown: (fn) => rebuild({ setupHooks: state.setupHooks, teardownHooks: [...state.teardownHooks, fn] }),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const EMPTY_HOOKS: HookState = { setupHooks: [], teardownHooks: [] };
|
|
74
100
|
|
|
75
101
|
/** Docker 沙箱:本地容器。`image` 可覆盖默认 `node:*-slim`(预制模板:烘焙好 agent CLI 的镜像)。 */
|
|
76
|
-
export function dockerSandbox(
|
|
77
|
-
|
|
102
|
+
export function dockerSandbox(
|
|
103
|
+
opts: Omit<DockerSandboxSpec, "backend" | keyof SandboxHooks<unknown>> = {},
|
|
104
|
+
): DockerSandboxSpec {
|
|
105
|
+
const build = (state: HookState): DockerSandboxSpec => ({
|
|
106
|
+
backend: "docker",
|
|
107
|
+
...opts,
|
|
108
|
+
setupHooks: state.setupHooks,
|
|
109
|
+
teardownHooks: state.teardownHooks,
|
|
110
|
+
...hookMethods(state, build),
|
|
111
|
+
});
|
|
112
|
+
return build(EMPTY_HOOKS);
|
|
78
113
|
}
|
|
79
114
|
|
|
80
115
|
/** Vercel Sandbox:microVM。`snapshotId` 从已有快照起(预制模板:烘焙好 agent CLI 的快照)。 */
|
|
81
|
-
export function vercelSandbox(
|
|
82
|
-
|
|
116
|
+
export function vercelSandbox(
|
|
117
|
+
opts: Omit<VercelSandboxSpec, "backend" | keyof SandboxHooks<unknown>> = {},
|
|
118
|
+
): VercelSandboxSpec {
|
|
119
|
+
const build = (state: HookState): VercelSandboxSpec => ({
|
|
120
|
+
backend: "vercel",
|
|
121
|
+
...opts,
|
|
122
|
+
setupHooks: state.setupHooks,
|
|
123
|
+
teardownHooks: state.teardownHooks,
|
|
124
|
+
...hookMethods(state, build),
|
|
125
|
+
});
|
|
126
|
+
return build(EMPTY_HOOKS);
|
|
83
127
|
}
|
|
84
128
|
|
|
85
129
|
/** E2B 沙箱。`template` 选 e2b 模板名/ID(预制模板:如 `"niceeval-agents"`);省略用 e2b 默认 `"base"`。 */
|
|
86
|
-
export function e2bSandbox(
|
|
87
|
-
|
|
130
|
+
export function e2bSandbox(
|
|
131
|
+
opts: Omit<E2BSandboxSpec, "backend" | keyof SandboxHooks<unknown>> = {},
|
|
132
|
+
): E2BSandboxSpec {
|
|
133
|
+
const build = (state: HookState): E2BSandboxSpec => ({
|
|
134
|
+
backend: "e2b",
|
|
135
|
+
...opts,
|
|
136
|
+
setupHooks: state.setupHooks,
|
|
137
|
+
teardownHooks: state.teardownHooks,
|
|
138
|
+
...hookMethods(state, build),
|
|
139
|
+
});
|
|
140
|
+
return build(EMPTY_HOOKS);
|
|
88
141
|
}
|
|
89
142
|
|
|
90
143
|
/**
|
|
@@ -98,5 +151,13 @@ export function defineSandbox(def: {
|
|
|
98
151
|
}): CustomSandboxSpec {
|
|
99
152
|
if (!def.name) throw new Error(t("define.sandboxNameRequired"));
|
|
100
153
|
if (typeof def.create !== "function") throw new Error(t("define.sandboxCreateRequired"));
|
|
101
|
-
|
|
154
|
+
const build = (state: HookState): CustomSandboxSpec => ({
|
|
155
|
+
backend: def.name,
|
|
156
|
+
create: def.create,
|
|
157
|
+
recommendedConcurrency: def.recommendedConcurrency,
|
|
158
|
+
setupHooks: state.setupHooks,
|
|
159
|
+
teardownHooks: state.teardownHooks,
|
|
160
|
+
...hookMethods(state, build),
|
|
161
|
+
});
|
|
162
|
+
return build(EMPTY_HOOKS);
|
|
102
163
|
}
|
package/src/i18n/en.ts
CHANGED
|
@@ -59,7 +59,7 @@ export const en = {
|
|
|
59
59
|
"Flags:\n" +
|
|
60
60
|
" --runs n --max-concurrency n --timeout ms --budget usd --tag t\n" +
|
|
61
61
|
" --early-exit / --no-early-exit --strict --force --dry --quiet\n" +
|
|
62
|
-
" --junit path --out dir --port n --open / --no-open -h, --help -v, --version\n\n" +
|
|
62
|
+
" --junit path --json path --out dir --port n --open / --no-open -h, --help -v, --version\n\n" +
|
|
63
63
|
"Positional args only select which evals to run (id prefixes); which agent and\n" +
|
|
64
64
|
"how to run come from experiments/ + flags. Env overrides (flag > env > config):\n" +
|
|
65
65
|
" NICEEVAL_RUNS NICEEVAL_MAX_CONCURRENCY NICEEVAL_TIMEOUT NICEEVAL_BUDGET\n",
|
|
@@ -94,10 +94,10 @@ export const en = {
|
|
|
94
94
|
" Docs: node_modules/niceeval/docs-site/zh/guides/write-experiment.mdx\n",
|
|
95
95
|
"cli.run.experimentRequiredHint": "Hint: \"{{pattern}}\" is an experiment{{kind}}; you probably meant: niceeval exp {{pattern}}\n",
|
|
96
96
|
"cli.run.experimentRequiredKnown": "Discovered experiments: {{experiments}}\n",
|
|
97
|
-
"cli.sandboxFlagRemoved": "`--sandbox` is not a CLI flag. Set `sandbox` in the experiment (or `niceeval.config.ts` as a project-wide fallback) to dockerSandbox() / vercelSandbox() / e2bSandbox() (import from \"niceeval/sandbox\").\n",
|
|
98
97
|
"cli.unimplemented": "Command \"{{command}}\" is not implemented yet (MVP).\n",
|
|
99
98
|
"cli.view.exportedDir": "Exported static report site: {{out}} (serve the whole directory with any static host; opening index.html via file:// cannot fetch artifacts)\n",
|
|
100
99
|
"cli.view.incompatible": "{{dir}}: written by niceeval {{producer}} (schemaVersion {{schemaVersion}}); this CLI reads schemaVersion {{supported}}.\nRun `{{command}}` to view it.\n",
|
|
100
|
+
"cli.view.noResults": "No results found under {{root}}. Run `niceeval exp` first, then `niceeval view`.\n",
|
|
101
101
|
"cli.view.incompatibleForeign": "{{dir}}: written by {{name}} {{version}} (schemaVersion {{schemaVersion}}); this CLI reads schemaVersion {{supported}}.\nOpen this report with the tool that produced it.\n",
|
|
102
102
|
"cli.view.url": "niceeval view: {{url}}\n",
|
|
103
103
|
"context.capabilityMissing":
|
|
@@ -186,6 +186,8 @@ export const en = {
|
|
|
186
186
|
"runner.startAgentSetup": "agent setup (install CLI / write config)...",
|
|
187
187
|
"runner.startAgentTracing": "agent tracing (write OTEL export config)...",
|
|
188
188
|
"runner.startSandbox": "starting sandbox...",
|
|
189
|
+
"runner.startSandboxSetup": "sandbox setup (environment provisioning hooks)...",
|
|
190
|
+
"runner.startSandboxTeardown": "sandbox teardown (environment provisioning hooks)...",
|
|
189
191
|
"runner.timeout": "attempt timed out ({{timeoutMs}}ms)\nRecent progress:\n{{recentLogs}}",
|
|
190
192
|
"runner.traceSelected": " -> kept {{count}} semantic spans",
|
|
191
193
|
"runner.resumeCarry": " · reusing {{carried}} passing results from last run, re-running {{retry}} evals\n",
|
package/src/i18n/zh-CN.ts
CHANGED
|
@@ -55,7 +55,7 @@ export const zhCN = {
|
|
|
55
55
|
"标志:\n" +
|
|
56
56
|
" --runs n --max-concurrency n --timeout ms --budget usd --tag t\n" +
|
|
57
57
|
" --early-exit / --no-early-exit --strict --force --dry --quiet\n" +
|
|
58
|
-
" --junit path --out dir --port n --open / --no-open -h, --help -v, --version\n\n" +
|
|
58
|
+
" --junit path --json path --out dir --port n --open / --no-open -h, --help -v, --version\n\n" +
|
|
59
59
|
"位置参数只选「跑哪些 eval」(id 前缀);对着哪个 agent、怎么跑来自 experiments/ 与\n" +
|
|
60
60
|
"标志。环境变量覆盖(标志 > 环境变量 > config):\n" +
|
|
61
61
|
" NICEEVAL_RUNS NICEEVAL_MAX_CONCURRENCY NICEEVAL_TIMEOUT NICEEVAL_BUDGET\n",
|
|
@@ -91,10 +91,10 @@ export const zhCN = {
|
|
|
91
91
|
" 文档:node_modules/niceeval/docs-site/zh/guides/write-experiment.mdx\n",
|
|
92
92
|
"cli.run.experimentRequiredHint": "提示:\"{{pattern}}\" 是实验{{kind}},你大概想跑:niceeval exp {{pattern}}\n",
|
|
93
93
|
"cli.run.experimentRequiredKnown": "已发现实验:{{experiments}}\n",
|
|
94
|
-
"cli.sandboxFlagRemoved": "`--sandbox` 不是 CLI flag。请在 experiment(或 niceeval.config.ts 做全项目兜底)里把 sandbox 设成 dockerSandbox() / vercelSandbox() / e2bSandbox()(从 \"niceeval/sandbox\" 导入)。\n",
|
|
95
94
|
"cli.unimplemented": "命令 \"{{command}}\" 暂未实现(MVP)。\n",
|
|
96
95
|
"cli.view.exportedDir": "已导出静态查看站:{{out}}(整个目录可直接静态托管;本地打开 {{out}}/index.html 需经 http 服务,file:// 下工件 fetch 不可用)\n",
|
|
97
96
|
"cli.view.incompatible": "{{dir}}: 由 niceeval {{producer}} 写入(schemaVersion {{schemaVersion}}),当前 CLI 只读 schemaVersion {{supported}}。\n运行 `{{command}}` 查看这份报告。\n",
|
|
97
|
+
"cli.view.noResults": "{{root}} 下没有结果。先 `niceeval exp` 跑一轮,再 `niceeval view`。\n",
|
|
98
98
|
"cli.view.incompatibleForeign": "{{dir}}: 由 {{name}} {{version}} 写入(schemaVersion {{schemaVersion}}),当前 CLI 只读 schemaVersion {{supported}}。\n请用写出它的那个工具查看这份报告。\n",
|
|
99
99
|
"cli.view.url": "niceeval view: {{url}}\n",
|
|
100
100
|
"context.capabilityMissing":
|
|
@@ -183,6 +183,8 @@ export const zhCN = {
|
|
|
183
183
|
"runner.startAgentSetup": "agent setup(装 CLI / 写配置)…",
|
|
184
184
|
"runner.startAgentTracing": "agent tracing(写 otel 导出配置)…",
|
|
185
185
|
"runner.startSandbox": "起沙箱…",
|
|
186
|
+
"runner.startSandboxSetup": "sandbox setup(环境预置钩子)…",
|
|
187
|
+
"runner.startSandboxTeardown": "sandbox teardown(环境预置钩子)…",
|
|
186
188
|
"runner.timeout": "attempt 超时({{timeoutMs}}ms)\n最近进度:\n{{recentLogs}}",
|
|
187
189
|
"runner.traceSelected": " → 留 {{count}}(按语义)",
|
|
188
190
|
"runner.resumeCarry": " · 复用上次 {{carried}} 个通过的结果,重跑 {{retry}} 个 eval\n",
|
package/src/report/aggregate.ts
CHANGED
|
@@ -18,15 +18,15 @@ import type {
|
|
|
18
18
|
Metric,
|
|
19
19
|
MetricCell,
|
|
20
20
|
MetricColumn,
|
|
21
|
-
|
|
21
|
+
FlagRef,
|
|
22
22
|
} from "./types.ts";
|
|
23
23
|
import { formatMetricValue } from "./format.ts";
|
|
24
24
|
|
|
25
25
|
// 复合键分隔符:NUL 不会出现在 eval id / experimentId / ISO 时间里,拼接键不会串味
|
|
26
26
|
const KEY_SEP = "\u0000";
|
|
27
27
|
|
|
28
|
-
/**
|
|
29
|
-
export const
|
|
28
|
+
/** flag 未声明时的组名:不猜,如实归一组。 */
|
|
29
|
+
export const FLAG_UNSET = "(unset)";
|
|
30
30
|
|
|
31
31
|
/** 计算函数的第一参:选集(warnings 随行)或手工挑的快照数组(没有挑选过程,自然无警告)。 */
|
|
32
32
|
export type SnapshotsInput = Selection | Snapshot[];
|
|
@@ -105,33 +105,33 @@ export function evalGroupOf(id: string): string {
|
|
|
105
105
|
return slash === -1 ? id : id.slice(0, slash);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
function
|
|
109
|
-
return typeof dimension === "object" && "kind" in dimension && dimension.kind === "
|
|
108
|
+
function isFlagRef(dimension: DimensionInput): dimension is FlagRef {
|
|
109
|
+
return typeof dimension === "object" && "kind" in dimension && dimension.kind === "flag";
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
/** experiment 声明的
|
|
113
|
-
function
|
|
114
|
-
return attempt.result.experiment?.
|
|
112
|
+
/** experiment 声明的 flags(经 runner 原样透传进持久化字段 ExperimentRunInfo.flags)。 */
|
|
113
|
+
function flagsOf(attempt: AttemptHandle): Record<string, unknown> | undefined {
|
|
114
|
+
return attempt.result.experiment?.flags;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
/**
|
|
118
|
-
export function
|
|
119
|
-
const value =
|
|
120
|
-
if (value === undefined) return
|
|
117
|
+
/** flag 声明值 → 组标签:label 函数优先,其余 String();未声明 → FLAG_UNSET。 */
|
|
118
|
+
export function flagGroupKey(ref: FlagRef, item: Item): string {
|
|
119
|
+
const value = flagsOf(item.attempt)?.[ref.name];
|
|
120
|
+
if (value === undefined) return FLAG_UNSET;
|
|
121
121
|
// 持久化字段是 Record<string, unknown>;声明侧的合法值就是这三种标量
|
|
122
122
|
if (typeof ref.label === "function") return ref.label(value as string | number | boolean);
|
|
123
123
|
return String(value);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
/**
|
|
127
|
-
export function
|
|
128
|
-
const value =
|
|
126
|
+
/** flag 作轴:要求数值;未声明或非数值 → null(点不画,注脚报数)。 */
|
|
127
|
+
export function flagAxisValue(ref: FlagRef, item: Item): number | null {
|
|
128
|
+
const value = flagsOf(item.attempt)?.[ref.name];
|
|
129
129
|
return typeof value === "number" ? value : null;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
export function dimensionKey(dimension: DimensionInput, item: Item): string {
|
|
133
133
|
if (typeof dimension !== "string") {
|
|
134
|
-
if (
|
|
134
|
+
if (isFlagRef(dimension)) return flagGroupKey(dimension, item);
|
|
135
135
|
return dimension.of(item.attempt);
|
|
136
136
|
}
|
|
137
137
|
const result = item.attempt.result;
|
|
@@ -159,7 +159,7 @@ export const MetricScatter = Object.assign(
|
|
|
159
159
|
);
|
|
160
160
|
MetricScatter.displayName = "MetricScatter";
|
|
161
161
|
|
|
162
|
-
/** 趋势线:x 是 experiment 声明的
|
|
162
|
+
/** 趋势线:x 是 experiment 声明的 flag(flag()),同系列按 x 排序连线。 */
|
|
163
163
|
export const MetricLine = Object.assign(
|
|
164
164
|
defineComponent<MetricLineProps>({
|
|
165
165
|
web: (props) => <MetricLineWeb {...props} />,
|
package/src/report/compute.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
Metric,
|
|
20
20
|
MetricCell,
|
|
21
21
|
OverviewData,
|
|
22
|
-
|
|
22
|
+
FlagRef,
|
|
23
23
|
ScatterData,
|
|
24
24
|
ScoreboardData,
|
|
25
25
|
TableData,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
experimentIdOf,
|
|
40
40
|
filterItems,
|
|
41
41
|
groupItems,
|
|
42
|
-
|
|
42
|
+
flagAxisValue,
|
|
43
43
|
resolveInput,
|
|
44
44
|
snapshotKeyOf,
|
|
45
45
|
toColumn,
|
|
@@ -52,7 +52,7 @@ import { formatMetricValue, formatPlainNumber } from "./format.ts";
|
|
|
52
52
|
// ───────────────────────── MetricTable.data ─────────────────────────
|
|
53
53
|
|
|
54
54
|
export interface TableDataOptions<M extends readonly Metric[]> {
|
|
55
|
-
/** 行维度(内置 / 自定义 /
|
|
55
|
+
/** 行维度(内置 / 自定义 / flag())。 */
|
|
56
56
|
rows: DimensionInput;
|
|
57
57
|
/** 每列一个指标;列键 = metric.name 的字面量,拼错编译不过。 */
|
|
58
58
|
columns: M;
|
|
@@ -289,10 +289,10 @@ export async function scatterData(input: SnapshotsInput, opts: ScatterDataOption
|
|
|
289
289
|
// ───────────────────────── MetricLine.data ─────────────────────────
|
|
290
290
|
|
|
291
291
|
export interface LineDataOptions {
|
|
292
|
-
/** x 轴:experiment 声明的
|
|
293
|
-
x:
|
|
292
|
+
/** x 轴:experiment 声明的 flag(数值),不解析 experiment 命名。 */
|
|
293
|
+
x: FlagRef;
|
|
294
294
|
y: Metric;
|
|
295
|
-
/** 可选:每个系列一条线(
|
|
295
|
+
/** 可选:每个系列一条线(flag 或普通维度);省略 = 单系列。 */
|
|
296
296
|
series?: DimensionInput;
|
|
297
297
|
}
|
|
298
298
|
|
|
@@ -303,7 +303,7 @@ export async function lineData(input: SnapshotsInput, opts: LineDataOptions): Pr
|
|
|
303
303
|
const groups = groupItems(items, "experiment");
|
|
304
304
|
const rows: LineData["rows"] = [];
|
|
305
305
|
for (const [key, group] of groups) {
|
|
306
|
-
const x =
|
|
306
|
+
const x = flagAxisValue(opts.x, group[0]); // flag 是 experiment 级声明,组内一致
|
|
307
307
|
rows.push({
|
|
308
308
|
key,
|
|
309
309
|
series: opts.series ? dimensionKey(opts.series, group[0]) : undefined,
|
|
@@ -228,7 +228,7 @@ describe("MetricLine 双面", () => {
|
|
|
228
228
|
`);
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
-
it("两面同口径:未声明
|
|
231
|
+
it("两面同口径:未声明 flag 的点两面都不画、注脚同数", () => {
|
|
232
232
|
expect(html).toContain("1 point missing data");
|
|
233
233
|
expect(term).toContain("1 point missing data");
|
|
234
234
|
expect(html).not.toContain('data-key="ultra/legacy"'); // 注脚 title 如实列缺数据的点,但不画
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
//
|
|
1
|
+
// flag():把 experiment 声明的 flags 当维度或轴(docs/reports.md「flags 与新摆法」)。
|
|
2
2
|
// 变量来自配置,不来自命名 —— 报告不解析 experiment id 字符串抠变量。
|
|
3
3
|
|
|
4
|
-
import type {
|
|
4
|
+
import type { FlagRef } from "./types.ts";
|
|
5
5
|
|
|
6
|
-
export function
|
|
6
|
+
export function flag(
|
|
7
7
|
name: string,
|
|
8
8
|
opts?: {
|
|
9
9
|
/** 组标签 / 轴标签;函数形态把声明值折成组名(如 `(v) => \`${v} agents\``)。 */
|
|
10
10
|
label?: string | ((value: string | number | boolean) => string);
|
|
11
11
|
unit?: string;
|
|
12
12
|
},
|
|
13
|
-
):
|
|
13
|
+
): FlagRef {
|
|
14
14
|
if (typeof name !== "string" || name.length === 0) {
|
|
15
|
-
throw new Error("
|
|
15
|
+
throw new Error("flag: name must be a non-empty string (the key declared in the experiment's flags).");
|
|
16
16
|
}
|
|
17
|
-
return { kind: "
|
|
17
|
+
return { kind: "flag", name, label: opts?.label, unit: opts?.unit };
|
|
18
18
|
}
|
package/src/report/index.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
// ./web.ts,只有那一侧 import react-dom。写报告文件的项目要装 react(.tsx 编译产物
|
|
9
9
|
// import react/jsx-runtime)。
|
|
10
10
|
|
|
11
|
-
// 指标与
|
|
11
|
+
// 指标与 flag
|
|
12
12
|
export { defineMetric, passRate, examScore, durationMs, tokens, costUSD } from "./metrics.ts";
|
|
13
|
-
export {
|
|
13
|
+
export { flag } from "./flag.ts";
|
|
14
14
|
|
|
15
15
|
// 报告基座与双面组件基座
|
|
16
16
|
export { defineReport, isReportDefinition, renderReportToText } from "./report.ts";
|
|
@@ -84,7 +84,7 @@ export type {
|
|
|
84
84
|
MetricCell,
|
|
85
85
|
MetricColumn,
|
|
86
86
|
OverviewData,
|
|
87
|
-
|
|
87
|
+
FlagRef,
|
|
88
88
|
ScatterData,
|
|
89
89
|
ScoreboardData,
|
|
90
90
|
SelectionWarning,
|