niceeval 0.1.1 → 0.1.2
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/README.md +8 -8
- package/README.zh.md +13 -13
- package/docs/README.md +119 -0
- package/docs/adapters/README.md +60 -0
- package/docs/adapters/authoring.md +179 -0
- package/docs/adapters/coding-agent-skills-plugins.md +324 -0
- package/docs/adapters/collection.md +128 -0
- package/docs/adapters/contract.md +263 -0
- package/docs/adapters/reference/agent-eval.md +215 -0
- package/docs/adapters/reference/agent-loop-apis.md +69 -0
- package/docs/adapters/reference/claude-code-otel-telemetry.md +100 -0
- package/docs/adapters/reference/eve-protocol.md +127 -0
- package/docs/adapters/reference/otel-genai.md +107 -0
- package/docs/adapters/reference/otel-instrumentation.md +65 -0
- package/docs/adapters/targets.md +99 -0
- package/docs/architecture.md +140 -0
- package/docs/assertions.md +388 -0
- package/docs/capabilities-by-construction.md +48 -0
- package/docs/cli.md +171 -0
- package/docs/concepts.md +106 -0
- package/docs/e2e-ci.md +295 -0
- package/docs/eval-authoring.md +319 -0
- package/docs/experiments.md +154 -0
- package/docs/getting-started.md +224 -0
- package/docs/multi-agent.md +145 -0
- package/docs/observability.md +333 -0
- package/docs/origin-integration.md +195 -0
- package/docs/references.md +35 -0
- package/docs/results-format.md +228 -0
- package/docs/runner.md +104 -0
- package/docs/sandbox.md +276 -0
- package/docs/scoring.md +119 -0
- package/docs/source-map.md +106 -0
- package/docs/tier-sync.md +177 -0
- package/docs/view.md +157 -0
- package/docs/vision.md +88 -0
- package/package.json +35 -5
- package/src/agents/ai-sdk-otel.ts +0 -0
- package/src/agents/ai-sdk.test.ts +377 -0
- package/src/agents/ai-sdk.ts +577 -0
- package/src/agents/bub.ts +30 -37
- package/src/agents/claude-code.ts +7 -4
- package/src/agents/codex.ts +15 -10
- package/src/agents/index.ts +43 -1
- package/src/agents/sdk-streams.test.ts +145 -0
- package/src/agents/sdk-streams.ts +457 -0
- package/src/agents/shared.ts +77 -39
- package/src/agents/streaming.test.ts +152 -0
- package/src/agents/streaming.ts +195 -0
- package/src/agents/types.ts +218 -0
- package/src/agents/ui-message-stream.test.ts +249 -0
- package/src/agents/ui-message-stream.ts +372 -0
- package/src/cli.ts +124 -77
- package/src/context/context.test.ts +203 -7
- package/src/context/context.ts +158 -49
- package/src/context/session.test.ts +96 -0
- package/src/context/session.ts +119 -9
- package/src/context/types.ts +205 -0
- package/src/define.ts +4 -14
- package/src/expect/index.ts +8 -71
- package/src/i18n/core.ts +28 -0
- package/src/i18n/en.ts +47 -5
- package/src/i18n/index.ts +5 -17
- package/src/i18n/zh-CN.ts +47 -5
- package/src/index.ts +12 -0
- package/src/loaders/index.ts +8 -39
- package/src/o11y/otlp/canonical.ts +7 -6
- package/src/o11y/otlp/mappers/codex.ts +10 -8
- package/src/o11y/otlp/mappers/index.ts +9 -21
- package/src/o11y/otlp/receiver.ts +25 -7
- package/src/o11y/otlp/sandbox-receiver.ts +57 -21
- package/src/o11y/otlp/turn-otel.test.ts +110 -0
- package/src/o11y/otlp/turn-otel.ts +125 -0
- package/src/o11y/parsers/bub.ts +13 -11
- package/src/o11y/parsers/claude-code.ts +27 -51
- package/src/o11y/parsers/codex.ts +29 -46
- package/src/o11y/parsers/index.ts +5 -14
- package/src/o11y/tool-names.test.ts +61 -0
- package/src/o11y/tool-names.ts +74 -0
- package/src/o11y/types.ts +135 -0
- package/src/runner/attempt.ts +456 -0
- package/src/runner/fingerprint.ts +59 -0
- package/src/runner/remote-sandbox.ts +53 -0
- package/src/runner/report.ts +53 -0
- package/src/runner/reporters/artifacts.ts +25 -4
- package/src/runner/reporters/console.ts +2 -8
- package/src/runner/reporters/live.ts +2 -8
- package/src/runner/reporters/shared.ts +15 -0
- package/src/runner/reporters/table.ts +10 -62
- package/src/runner/run.ts +114 -619
- package/src/runner/types.ts +237 -0
- package/src/sandbox/checkpoint.ts +15 -13
- package/src/sandbox/docker-stream.ts +87 -0
- package/src/sandbox/docker.ts +42 -120
- package/src/sandbox/e2b.ts +24 -75
- package/src/sandbox/local-files.ts +33 -0
- package/src/sandbox/paths.test.ts +85 -0
- package/src/sandbox/paths.ts +45 -0
- package/src/sandbox/resolve.ts +10 -34
- package/src/sandbox/shell.ts +17 -0
- package/src/sandbox/source-files.ts +42 -2
- package/src/sandbox/types.ts +158 -0
- package/src/sandbox/vercel.ts +55 -71
- package/src/scoring/collector.ts +3 -2
- package/src/scoring/judge.ts +72 -146
- package/src/scoring/match.ts +79 -0
- package/src/scoring/types.ts +76 -0
- package/src/shared/aggregate.ts +48 -0
- package/src/shared/format.ts +20 -0
- package/src/shared/outcome.ts +48 -0
- package/src/shared/types.ts +37 -0
- package/src/types.ts +20 -911
- package/src/view/aggregate.ts +103 -0
- package/src/view/app/App.tsx +26 -5
- package/src/view/app/components/AttemptModal.tsx +12 -3
- package/src/view/app/components/CodeView.tsx +9 -5
- package/src/view/app/components/CopyControls.tsx +4 -4
- package/src/view/app/components/ExperimentTable.tsx +5 -5
- package/src/view/app/i18n.ts +31 -7
- package/src/view/app/lib/format.ts +17 -20
- package/src/view/app/lib/outcome.ts +4 -16
- package/src/view/app/main.tsx +3 -5
- package/src/view/app/pages/RunsPage.tsx +2 -2
- package/src/view/app/pages/TracesPage.tsx +2 -2
- package/src/view/app/shared.ts +2 -1
- package/src/view/app/types.ts +6 -43
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +18 -18
- package/src/view/index.ts +24 -401
- package/src/view/loader.ts +234 -0
- package/src/view/server.ts +136 -0
- package/src/view/shared/types.ts +64 -0
- package/src/view/styles.css +60 -9
package/src/cli.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { pathToFileURL } from "node:url";
|
|
11
|
+
import { parseArgs as nodeParseArgs } from "node:util";
|
|
11
12
|
import { discoverEvals, discoverExperiments, makeFilter } from "./runner/discover.ts";
|
|
12
13
|
import { runEvals, type AgentRun } from "./runner/run.ts";
|
|
13
14
|
import { stopAllSandboxes, liveSandboxCount } from "./sandbox/registry.ts";
|
|
@@ -16,13 +17,22 @@ import { Console as ConsoleReporter } from "./runner/reporters/console.ts";
|
|
|
16
17
|
import { JUnit } from "./runner/reporters/json.ts";
|
|
17
18
|
import { Live as LiveReporter, type LiveRow } from "./runner/reporters/live.ts";
|
|
18
19
|
import { Artifacts as ArtifactsReporter } from "./runner/reporters/artifacts.ts";
|
|
19
|
-
import { buildView, startViewServer, loadMostRecentResults } from "./view/index.ts";
|
|
20
|
+
import { buildView, startViewServer, loadMostRecentResults, IncompatibleResultsError } from "./view/index.ts";
|
|
20
21
|
import { t } from "./i18n/index.ts";
|
|
22
|
+
import { formatThrown } from "./util.ts";
|
|
21
23
|
import type { Config, DiscoveredExperiment, Reporter } from "./types.ts";
|
|
22
24
|
|
|
25
|
+
/** `niceeval view <summary.json>` 指向版本不同的报告时:打印 npx 提示后退出,不抛堆栈。 */
|
|
26
|
+
function exitOnIncompatibleResults(e: unknown): never {
|
|
27
|
+
if (e instanceof IncompatibleResultsError) {
|
|
28
|
+
process.stderr.write(e.message);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
interface Flags {
|
|
24
35
|
agent?: string;
|
|
25
|
-
sandbox?: string;
|
|
26
36
|
model?: string;
|
|
27
37
|
runs?: number;
|
|
28
38
|
maxConcurrency?: number;
|
|
@@ -38,85 +48,111 @@ interface Flags {
|
|
|
38
48
|
open?: boolean;
|
|
39
49
|
out?: string;
|
|
40
50
|
port?: number;
|
|
51
|
+
help: boolean;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
54
|
+
// 表驱动的 flag 定义(node:util parseArgs)。--no-x 显式声明,不依赖 allowNegative(需 Node 20.14+,
|
|
55
|
+
// engines 是 >=18)。未知 flag 由 strict 模式报清晰错误,不再静默吞掉后面的位置参数。
|
|
56
|
+
const FLAG_OPTIONS = {
|
|
57
|
+
agent: { type: "string" },
|
|
58
|
+
model: { type: "string" },
|
|
59
|
+
runs: { type: "string" },
|
|
60
|
+
"max-concurrency": { type: "string" },
|
|
61
|
+
timeout: { type: "string" },
|
|
62
|
+
budget: { type: "string" },
|
|
63
|
+
tag: { type: "string" },
|
|
64
|
+
junit: { type: "string" },
|
|
65
|
+
out: { type: "string" },
|
|
66
|
+
port: { type: "string" },
|
|
67
|
+
// --sandbox 已移除(sandbox 归 config/experiment);留着解析是为了给出迁移提示而非「未知 flag」。
|
|
68
|
+
sandbox: { type: "string" },
|
|
69
|
+
// --watch / --json:文档曾接受、尚未实现的 flag。容忍(no-op)而不是硬报「未知 flag」,
|
|
70
|
+
// 免得照旧文档写的脚本直接崩;真正实现前不接任何行为。
|
|
71
|
+
watch: { type: "boolean" },
|
|
72
|
+
json: { type: "boolean" },
|
|
73
|
+
dry: { type: "boolean" },
|
|
74
|
+
quiet: { type: "boolean" },
|
|
75
|
+
force: { type: "boolean" },
|
|
76
|
+
strict: { type: "boolean" },
|
|
77
|
+
"early-exit": { type: "boolean" },
|
|
78
|
+
"no-early-exit": { type: "boolean" },
|
|
79
|
+
open: { type: "boolean" },
|
|
80
|
+
"no-open": { type: "boolean" },
|
|
81
|
+
help: { type: "boolean", short: "h" },
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
function numberFlag(name: string, raw: string | undefined): number | undefined {
|
|
85
|
+
if (raw === undefined) return undefined;
|
|
86
|
+
const n = Number(raw);
|
|
87
|
+
if (!Number.isFinite(n)) {
|
|
88
|
+
process.stderr.write(t("cli.flag.invalidNumber", { flag: name, value: raw }));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
return n;
|
|
92
|
+
}
|
|
56
93
|
|
|
57
94
|
function parseArgs(argv: string[]): { command: string; positionals: string[]; flags: Flags } {
|
|
58
95
|
if (argv[0] === "--") argv = argv.slice(1);
|
|
59
|
-
const positionals: string[] = [];
|
|
60
|
-
const flags: Flags = { dry: false, quiet: false, force: false, strict: false };
|
|
61
|
-
let command = "run";
|
|
62
|
-
let i = 0;
|
|
63
96
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
97
|
+
let values: Record<string, string | boolean | undefined>;
|
|
98
|
+
let rawPositionals: string[];
|
|
99
|
+
try {
|
|
100
|
+
const parsed = nodeParseArgs({ args: argv, options: FLAG_OPTIONS, allowPositionals: true, strict: true });
|
|
101
|
+
values = parsed.values as Record<string, string | boolean | undefined>;
|
|
102
|
+
rawPositionals = parsed.positionals;
|
|
103
|
+
} catch (e) {
|
|
104
|
+
process.stderr.write(t("cli.flag.parseError", { message: e instanceof Error ? e.message : String(e) }));
|
|
105
|
+
process.exit(1);
|
|
69
106
|
}
|
|
70
107
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (name === "no-open") {
|
|
84
|
-
flags.open = false;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (name === "open") {
|
|
88
|
-
flags.open = true;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (BOOL_FLAGS.has(name)) {
|
|
92
|
-
if (name === "dry") flags.dry = true;
|
|
93
|
-
else if (name === "quiet") flags.quiet = true;
|
|
94
|
-
else if (name === "force") flags.force = true;
|
|
95
|
-
else if (name === "strict") flags.strict = true;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
const value = argv[++i];
|
|
99
|
-
switch (name) {
|
|
100
|
-
case "agent": flags.agent = value; break;
|
|
101
|
-
case "sandbox": flags.sandbox = value; break;
|
|
102
|
-
case "model": flags.model = value; break;
|
|
103
|
-
case "runs": flags.runs = Number(value); break;
|
|
104
|
-
case "max-concurrency": flags.maxConcurrency = Number(value); break;
|
|
105
|
-
case "timeout": flags.timeout = Number(value); break;
|
|
106
|
-
case "budget": flags.budget = Number(value); break;
|
|
107
|
-
case "tag": flags.tag = value; break;
|
|
108
|
-
case "junit": flags.junit = value; break;
|
|
109
|
-
case "out": flags.out = value; break;
|
|
110
|
-
case "port": flags.port = Number(value); break;
|
|
111
|
-
default: break; // 未知 flag 忽略
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
positionals.push(tok);
|
|
115
|
-
}
|
|
108
|
+
if (values.sandbox !== undefined) {
|
|
109
|
+
process.stderr.write(t("cli.sandboxFlagRemoved"));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 第一个位置参数若是已知命令,则为命令;其余是 eval id 前缀 / view 输入。
|
|
114
|
+
const commands = new Set(["exp", "list", "view", "clean", "init", "watch", "run"]);
|
|
115
|
+
let command = "run";
|
|
116
|
+
let positionals = rawPositionals;
|
|
117
|
+
if (rawPositionals[0] && commands.has(rawPositionals[0])) {
|
|
118
|
+
command = rawPositionals[0];
|
|
119
|
+
positionals = rawPositionals.slice(1);
|
|
116
120
|
}
|
|
121
|
+
|
|
122
|
+
const flags: Flags = {
|
|
123
|
+
agent: values.agent as string | undefined,
|
|
124
|
+
model: values.model as string | undefined,
|
|
125
|
+
runs: numberFlag("runs", values.runs as string | undefined),
|
|
126
|
+
maxConcurrency: numberFlag("max-concurrency", values["max-concurrency"] as string | undefined),
|
|
127
|
+
timeout: numberFlag("timeout", values.timeout as string | undefined),
|
|
128
|
+
budget: numberFlag("budget", values.budget as string | undefined),
|
|
129
|
+
tag: values.tag as string | undefined,
|
|
130
|
+
junit: values.junit as string | undefined,
|
|
131
|
+
out: values.out as string | undefined,
|
|
132
|
+
port: numberFlag("port", values.port as string | undefined),
|
|
133
|
+
dry: values.dry === true,
|
|
134
|
+
quiet: values.quiet === true,
|
|
135
|
+
force: values.force === true,
|
|
136
|
+
strict: values.strict === true,
|
|
137
|
+
earlyExit: values["no-early-exit"] === true ? false : values["early-exit"] === true ? true : undefined,
|
|
138
|
+
open: values["no-open"] === true ? false : values.open === true ? true : undefined,
|
|
139
|
+
help: values.help === true,
|
|
140
|
+
};
|
|
117
141
|
return { command, positionals, flags };
|
|
118
142
|
}
|
|
119
143
|
|
|
144
|
+
/** 调度项的环境变量层(标志 > 环境变量 > config > 默认,见 docs/cli.md「配置优先级」)。 */
|
|
145
|
+
function envNumber(name: string): number | undefined {
|
|
146
|
+
const raw = process.env[name]?.trim();
|
|
147
|
+
if (!raw) return undefined;
|
|
148
|
+
const n = Number(raw);
|
|
149
|
+
if (!Number.isFinite(n)) {
|
|
150
|
+
process.stderr.write(t("cli.envInvalidNumber", { name, value: raw }));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
return n;
|
|
154
|
+
}
|
|
155
|
+
|
|
120
156
|
/** 加载 cwd/.env(不覆盖已有环境变量)。 */
|
|
121
157
|
async function loadDotenv(cwd: string): Promise<void> {
|
|
122
158
|
const path = join(cwd, ".env");
|
|
@@ -203,13 +239,19 @@ async function main(): Promise<void> {
|
|
|
203
239
|
await loadDotenv(cwd);
|
|
204
240
|
const { command, positionals, flags } = parseArgs(process.argv.slice(2));
|
|
205
241
|
|
|
242
|
+
// --help 不需要 config,先于一切命令处理。
|
|
243
|
+
if (flags.help) {
|
|
244
|
+
process.stdout.write(t("cli.help"));
|
|
245
|
+
process.exit(0);
|
|
246
|
+
}
|
|
247
|
+
|
|
206
248
|
if (command === "view") {
|
|
207
249
|
if (flags.out) {
|
|
208
|
-
const out = await buildView({ input: positionals[0], out: flags.out });
|
|
250
|
+
const out = await buildView({ input: positionals[0], out: flags.out }).catch(exitOnIncompatibleResults);
|
|
209
251
|
process.stdout.write(t("cli.view.exported", { out }));
|
|
210
252
|
process.exit(0);
|
|
211
253
|
}
|
|
212
|
-
const server = await startViewServer({ input: positionals[0], port: flags.port });
|
|
254
|
+
const server = await startViewServer({ input: positionals[0], port: flags.port }).catch(exitOnIncompatibleResults);
|
|
213
255
|
process.stdout.write(t("cli.view.url", { url: server.url }));
|
|
214
256
|
if (flags.open !== false) {
|
|
215
257
|
const opened = await openBrowser(server.url);
|
|
@@ -273,11 +315,11 @@ async function main(): Promise<void> {
|
|
|
273
315
|
agent: exp.agent,
|
|
274
316
|
model: exp.model,
|
|
275
317
|
flags: exp.flags ?? {},
|
|
276
|
-
runs: flags.runs ?? exp.runs ?? 1,
|
|
318
|
+
runs: flags.runs ?? envNumber("NICEEVAL_RUNS") ?? exp.runs ?? 1,
|
|
277
319
|
earlyExit: flags.earlyExit ?? exp.earlyExit ?? true,
|
|
278
|
-
sandbox:
|
|
279
|
-
timeoutMs: flags.timeout ?? exp.timeoutMs ?? config.timeoutMs,
|
|
280
|
-
budget: flags.budget ?? exp.budget,
|
|
320
|
+
sandbox: exp.sandbox ?? config.sandbox,
|
|
321
|
+
timeoutMs: flags.timeout ?? envNumber("NICEEVAL_TIMEOUT") ?? exp.timeoutMs ?? config.timeoutMs,
|
|
322
|
+
budget: flags.budget ?? envNumber("NICEEVAL_BUDGET") ?? exp.budget,
|
|
281
323
|
evalFilter: evalsFilterFromExperiment(exp.evals, extraPatterns),
|
|
282
324
|
experimentId: exp.id,
|
|
283
325
|
strict: flags.strict,
|
|
@@ -399,7 +441,12 @@ async function main(): Promise<void> {
|
|
|
399
441
|
evals,
|
|
400
442
|
agentRuns,
|
|
401
443
|
reporters,
|
|
402
|
-
maxConcurrency:
|
|
444
|
+
maxConcurrency:
|
|
445
|
+
flags.maxConcurrency ??
|
|
446
|
+
envNumber("NICEEVAL_MAX_CONCURRENCY") ??
|
|
447
|
+
expMaxConcurrency ??
|
|
448
|
+
config.maxConcurrency ??
|
|
449
|
+
sandboxDefaultConcurrency,
|
|
403
450
|
signal: ctrl.signal,
|
|
404
451
|
onProgress,
|
|
405
452
|
priorResults,
|
|
@@ -414,7 +461,7 @@ async function main(): Promise<void> {
|
|
|
414
461
|
}
|
|
415
462
|
|
|
416
463
|
main().catch(async (e) => {
|
|
417
|
-
process.stderr.write(t("cli.error", { error:
|
|
464
|
+
process.stderr.write(t("cli.error", { error: formatThrown(e) }));
|
|
418
465
|
// 真·崩溃路径也别留孤儿:强清还活着的沙箱(带超时),再退。
|
|
419
466
|
await stopAllSandboxes();
|
|
420
467
|
process.exit(2);
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { createEvalContext } from "./context.ts";
|
|
3
3
|
import { includes } from "../expect/index.ts";
|
|
4
|
-
import type { Agent, AgentContext, Sandbox, StreamEvent, Turn, TurnInput } from "../types.ts";
|
|
4
|
+
import type { Agent, AgentContext, InputRequest, Sandbox, StreamEvent, Turn, TurnInput } from "../types.ts";
|
|
5
5
|
|
|
6
6
|
// 计算工具 + 最终回复"1 + 1 = **2** 哦!😊"——复现截图里的场景:助手回复明明包含 "2",
|
|
7
7
|
// 但 t.check(t.reply, includes("2")) 却失败。
|
|
8
8
|
function calculatorAgent(): Agent {
|
|
9
9
|
return {
|
|
10
10
|
name: "calculator",
|
|
11
|
-
|
|
11
|
+
// 测试注入了真实的 fake sandbox,kind: "sandbox" 让 t.sandbox 过沙箱能力守卫。
|
|
12
|
+
kind: "sandbox",
|
|
12
13
|
async send(_input: TurnInput, ctx: AgentContext): Promise<Turn> {
|
|
13
|
-
ctx.session.
|
|
14
|
+
ctx.session.capture("sess-1");
|
|
14
15
|
const events: StreamEvent[] = [
|
|
15
16
|
{ type: "action.called", callId: "c1", name: "calculate", input: { expr: "1+1" }, tool: undefined },
|
|
16
17
|
{ type: "action.result", callId: "c1", output: { result: 2 }, status: "completed" },
|
|
@@ -21,8 +22,12 @@ function calculatorAgent(): Agent {
|
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
type FakeSandbox = Sandbox & { calls: { uploadDirectory: [string, string | undefined][] } };
|
|
26
|
+
|
|
27
|
+
function fakeSandbox(): FakeSandbox {
|
|
28
|
+
const calls: { uploadDirectory: [string, string | undefined][] } = { uploadDirectory: [] };
|
|
25
29
|
return {
|
|
30
|
+
workdir: "/sandbox/work",
|
|
26
31
|
runCommand: async () => { throw new Error("not implemented"); },
|
|
27
32
|
runShell: async () => { throw new Error("not implemented"); },
|
|
28
33
|
readFile: async () => "",
|
|
@@ -36,23 +41,27 @@ function fakeSandbox(): Sandbox {
|
|
|
36
41
|
}),
|
|
37
42
|
writeFiles: async () => {},
|
|
38
43
|
uploadFiles: async () => {},
|
|
39
|
-
uploadDirectory: async () => {
|
|
44
|
+
uploadDirectory: async (localDir, targetDir) => {
|
|
45
|
+
calls.uploadDirectory.push([localDir, targetDir]);
|
|
46
|
+
},
|
|
40
47
|
stop: async () => {},
|
|
41
48
|
sandboxId: "fake",
|
|
42
49
|
otlpHost: null,
|
|
43
50
|
downloadFile: async () => Buffer.from(""),
|
|
44
51
|
uploadFile: async () => {},
|
|
52
|
+
calls,
|
|
45
53
|
};
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
function makeContext(agent: Agent) {
|
|
56
|
+
function makeContext(agent: Agent, sandbox = fakeSandbox(), evalBaseDir?: string) {
|
|
49
57
|
return createEvalContext({
|
|
50
58
|
agent,
|
|
51
|
-
sandbox
|
|
59
|
+
sandbox,
|
|
52
60
|
flags: {},
|
|
53
61
|
signal: new AbortController().signal,
|
|
54
62
|
log: () => {},
|
|
55
63
|
judge: undefined,
|
|
64
|
+
evalBaseDir,
|
|
56
65
|
});
|
|
57
66
|
}
|
|
58
67
|
|
|
@@ -79,6 +88,25 @@ describe("createEvalContext / TestContext live state", () => {
|
|
|
79
88
|
});
|
|
80
89
|
expect(result.passed).toBe(true);
|
|
81
90
|
expect(result.score).toBe(1);
|
|
91
|
+
expect(result.evidence).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("t.check(...) attaches the actually-checked value as evidence when it fails", async () => {
|
|
95
|
+
const { context, state } = makeContext(calculatorAgent());
|
|
96
|
+
await context.send("1+1=?");
|
|
97
|
+
context.check(context.reply, includes("banana"));
|
|
98
|
+
|
|
99
|
+
const [result] = await state.collector.finalize({
|
|
100
|
+
events: [],
|
|
101
|
+
facts: { toolCalls: [], subagentCalls: [], inputRequests: [], parked: false, messageCount: 0, compactions: 0 },
|
|
102
|
+
diff: state.late.diff,
|
|
103
|
+
scripts: state.late.scripts,
|
|
104
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
105
|
+
status: "completed",
|
|
106
|
+
readFile: async () => undefined,
|
|
107
|
+
});
|
|
108
|
+
expect(result.passed).toBe(false);
|
|
109
|
+
expect(result.evidence).toBe("1 + 1 = **2** 哦!😊");
|
|
82
110
|
});
|
|
83
111
|
|
|
84
112
|
it("t.events reflects the turn's events after send(), not an empty snapshot", async () => {
|
|
@@ -93,4 +121,172 @@ describe("createEvalContext / TestContext live state", () => {
|
|
|
93
121
|
await context.send("1+1=?");
|
|
94
122
|
expect(context.sessionId).toBe("sess-1");
|
|
95
123
|
});
|
|
124
|
+
|
|
125
|
+
it("exposes sandbox workdir to eval authors", () => {
|
|
126
|
+
const { context } = makeContext(calculatorAgent());
|
|
127
|
+
expect(context.sandbox.workdir).toBe("/sandbox/work");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("resolves uploadDirectory local paths relative to the eval file directory", async () => {
|
|
131
|
+
const sandbox = fakeSandbox();
|
|
132
|
+
const { context } = makeContext(calculatorAgent(), sandbox, "/repo/evals/nested");
|
|
133
|
+
|
|
134
|
+
await context.sandbox.uploadDirectory("../fixtures/app", "src");
|
|
135
|
+
|
|
136
|
+
expect(sandbox.calls.uploadDirectory).toEqual([
|
|
137
|
+
["/repo/evals/fixtures/app", "src"],
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// agent 按调用次数依次吐出预设的 Turn 序列,同时记下每次收到的 TurnInput——
|
|
143
|
+
// 用来断言 t.respond()/t.respondAll() 到 adapter 的 input.responses 长什么样。
|
|
144
|
+
function scriptedAgent(turns: Turn[]): Agent & { received: TurnInput[] } {
|
|
145
|
+
const received: TurnInput[] = [];
|
|
146
|
+
let i = 0;
|
|
147
|
+
const agent: Agent = {
|
|
148
|
+
name: "scripted",
|
|
149
|
+
kind: "remote",
|
|
150
|
+
async send(input: TurnInput) {
|
|
151
|
+
received.push(input);
|
|
152
|
+
const turn = turns[Math.min(i, turns.length - 1)] as Turn;
|
|
153
|
+
i++;
|
|
154
|
+
return turn;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
return Object.assign(agent, { received });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function waitingTurn(...requests: InputRequest[]): Turn {
|
|
161
|
+
return {
|
|
162
|
+
status: "waiting",
|
|
163
|
+
events: requests.map((request) => ({ type: "input.requested" as const, request })),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function completedTurn(text = "ok"): Turn {
|
|
168
|
+
return { status: "completed", events: [{ type: "message", role: "assistant", text }] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
describe("t.respond() / t.respondAll(): structured InputResponse", () => {
|
|
172
|
+
it("string arg hitting a pending request's option becomes { requestId, optionId }", async () => {
|
|
173
|
+
const agent = scriptedAgent([
|
|
174
|
+
waitingTurn({ id: "req_1", action: "send_email", options: [{ id: "approve" }, { id: "deny" }] }),
|
|
175
|
+
completedTurn(),
|
|
176
|
+
]);
|
|
177
|
+
const { context } = makeContext(agent);
|
|
178
|
+
await context.send("draft an email");
|
|
179
|
+
await context.respond("approve");
|
|
180
|
+
|
|
181
|
+
expect(agent.received[1]?.text).toBe("approve");
|
|
182
|
+
expect(agent.received[1]?.responses).toEqual([{ requestId: "req_1", optionId: "approve" }]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("string arg that matches no option becomes free-text { requestId, text }", async () => {
|
|
186
|
+
const agent = scriptedAgent([
|
|
187
|
+
waitingTurn({ id: "req_1", action: "send_email", options: [{ id: "approve" }, { id: "deny" }] }),
|
|
188
|
+
completedTurn(),
|
|
189
|
+
]);
|
|
190
|
+
const { context } = makeContext(agent);
|
|
191
|
+
await context.send("draft an email");
|
|
192
|
+
await context.respond("change the recipient to ceo@corp.com");
|
|
193
|
+
|
|
194
|
+
expect(agent.received[1]?.responses).toEqual([
|
|
195
|
+
{ requestId: "req_1", text: "change the recipient to ceo@corp.com" },
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("string arg throws a clear error when multiple requests are pending (needs object form to disambiguate)", async () => {
|
|
200
|
+
const agent = scriptedAgent([
|
|
201
|
+
waitingTurn(
|
|
202
|
+
{ id: "req_1", action: "edit_a", options: [{ id: "approve" }, { id: "deny" }] },
|
|
203
|
+
{ id: "req_2", action: "edit_b", options: [{ id: "approve" }, { id: "deny" }] },
|
|
204
|
+
),
|
|
205
|
+
completedTurn(),
|
|
206
|
+
]);
|
|
207
|
+
const { context } = makeContext(agent);
|
|
208
|
+
await context.send("apply two edits");
|
|
209
|
+
|
|
210
|
+
await expect(context.respond("approve")).rejects.toThrow(/字符串回答无法对位|cannot be matched/);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("object form { request, optionId } disambiguates when multiple requests are pending", async () => {
|
|
214
|
+
const agent = scriptedAgent([
|
|
215
|
+
waitingTurn(
|
|
216
|
+
{ id: "req_1", action: "edit_a", options: [{ id: "approve" }, { id: "deny" }] },
|
|
217
|
+
{ id: "req_2", action: "edit_b", options: [{ id: "approve" }, { id: "deny" }] },
|
|
218
|
+
),
|
|
219
|
+
completedTurn(),
|
|
220
|
+
]);
|
|
221
|
+
const { context } = makeContext(agent);
|
|
222
|
+
await context.send("apply two edits");
|
|
223
|
+
const req2 = context.requireInputRequest({ action: "edit_b" });
|
|
224
|
+
await context.respond({ request: req2, optionId: "deny" });
|
|
225
|
+
|
|
226
|
+
expect(agent.received[1]?.text).toBe("deny");
|
|
227
|
+
expect(agent.received[1]?.responses).toEqual([{ requestId: "req_2", optionId: "deny" }]);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("object form with an optionId not present in the request's options throws instead of silently forwarding", async () => {
|
|
231
|
+
const agent = scriptedAgent([
|
|
232
|
+
waitingTurn({ id: "req_1", action: "send_email", options: [{ id: "approve" }, { id: "deny" }] }),
|
|
233
|
+
completedTurn(),
|
|
234
|
+
]);
|
|
235
|
+
const { context } = makeContext(agent);
|
|
236
|
+
await context.send("draft an email");
|
|
237
|
+
const req = context.requireInputRequest();
|
|
238
|
+
|
|
239
|
+
await expect(context.respond({ request: req, optionId: "yolo" })).rejects.toThrow(/req_1/);
|
|
240
|
+
// 校验先于发送:没有第二次 send 发生。
|
|
241
|
+
expect(agent.received.length).toBe(1);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("respondAll(optionId) answers every pending request and joins input.text with \\n", async () => {
|
|
245
|
+
const agent = scriptedAgent([
|
|
246
|
+
waitingTurn(
|
|
247
|
+
{ id: "req_1", action: "edit_a", options: [{ id: "approve" }, { id: "deny" }] },
|
|
248
|
+
{ id: "req_2", action: "edit_b", options: [{ id: "approve" }, { id: "deny" }] },
|
|
249
|
+
),
|
|
250
|
+
completedTurn(),
|
|
251
|
+
]);
|
|
252
|
+
const { context } = makeContext(agent);
|
|
253
|
+
await context.send("apply two edits");
|
|
254
|
+
await context.respondAll("approve");
|
|
255
|
+
|
|
256
|
+
expect(agent.received[1]?.text).toBe("approve\napprove");
|
|
257
|
+
expect(agent.received[1]?.responses).toEqual([
|
|
258
|
+
{ requestId: "req_1", optionId: "approve" },
|
|
259
|
+
{ requestId: "req_2", optionId: "approve" },
|
|
260
|
+
]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("respondAll(optionId) validates the option against every pending request before sending anything", async () => {
|
|
264
|
+
const agent = scriptedAgent([
|
|
265
|
+
waitingTurn(
|
|
266
|
+
{ id: "req_1", action: "edit_a", options: [{ id: "approve" }, { id: "deny" }] },
|
|
267
|
+
{ id: "req_2", action: "edit_b", options: [{ id: "yes" }, { id: "no" }] },
|
|
268
|
+
),
|
|
269
|
+
completedTurn(),
|
|
270
|
+
]);
|
|
271
|
+
const { context } = makeContext(agent);
|
|
272
|
+
await context.send("apply two edits");
|
|
273
|
+
|
|
274
|
+
await expect(context.respondAll("approve")).rejects.toThrow(/req_2/);
|
|
275
|
+
expect(agent.received.length).toBe(1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("TurnInput.responses reaches the adapter unchanged (not derived/guessed on the adapter side)", async () => {
|
|
279
|
+
const agent = scriptedAgent([
|
|
280
|
+
waitingTurn({ id: "req_1", action: "send_email", options: [{ id: "approve" }, { id: "deny" }] }),
|
|
281
|
+
completedTurn(),
|
|
282
|
+
]);
|
|
283
|
+
const { context } = makeContext(agent);
|
|
284
|
+
await context.send("draft an email");
|
|
285
|
+
const req = context.requireInputRequest();
|
|
286
|
+
await context.respond({ request: req, text: "wait, add a subject line first" });
|
|
287
|
+
|
|
288
|
+
expect(agent.received[1]?.responses).toEqual([
|
|
289
|
+
{ requestId: "req_1", text: "wait, add a subject line first" },
|
|
290
|
+
]);
|
|
291
|
+
});
|
|
96
292
|
});
|