refactor(benchmarks): use native canary runners and upgrade to agent-eval v2 (#2785)

Signed-off-by: Colton Padden <colton.padden@vercel.com>
This commit is contained in:
Colton Padden
2026-08-31 17:01:28 -04:00
committed by GitHub
parent 014137baab
commit 32d2984676
27 changed files with 597 additions and 1627 deletions
+1
View File
@@ -20,6 +20,7 @@ apps/benchmarks/experiments/
apps/benchmarks/results/ apps/benchmarks/results/
apps/benchmarks/evals/*/PROMPT.md apps/benchmarks/evals/*/PROMPT.md
apps/benchmarks/evals/*/package.json apps/benchmarks/evals/*/package.json
apps/benchmarks/evals/*/.eve-authoring-bootstrap.json
.extension-contracts-cache/ .extension-contracts-cache/
packages/eve/.workflow-vitest/ packages/eve/.workflow-vitest/
packages/eve/.generated/ packages/eve/.generated/
+26 -64
View File
@@ -6,84 +6,46 @@ run in CI or as part of `pnpm test`.
## Run ## Run
The default subject is the current working tree, including uncommitted and untracked files that The default subject is the current `main` canary. The runner resolves that moving alias once to its
Git does not ignore: immutable commit URL, then every model, treatment, and repetition uses that same artifact:
```sh ```sh
pnpm benchmark author-000-imessage pnpm benchmark author-001-weather-tool
pnpm benchmark pnpm benchmark
pnpm benchmark author-000-imessage --runs 3 pnpm benchmark author-001-weather-tool --runs 3
pnpm benchmark author-000-imessage --model kimi-k3 pnpm benchmark author-001-weather-tool --model kimi-k3
pnpm benchmark author-000-imessage --treatment baseline pnpm benchmark author-001-weather-tool --treatment baseline
pnpm benchmark author-000-imessage --dry pnpm benchmark author-001-weather-tool --dry
pnpm benchmark author-000-imessage --verbose pnpm benchmark author-001-weather-tool --verbose
pnpm benchmark author-000-imessage --keep-failures pnpm benchmark author-001-weather-tool --keep-failures
pnpm benchmark author-001-weather-tool --canary main
``` ```
`--keep-failures` keeps a run the runner judged an infrastructure failure — a stalled turn, a `--keep-failures` keeps a run the runner judged an infrastructure failure — a stalled turn, a
sandbox error — as the final result instead of discarding it. Use it while iterating on the sandbox error — as the final result instead of discarding it. Use it while iterating on the
harness, when the failure itself is what you want to read. harness, when the failure itself is what you want to read.
Set `EVE_BENCHMARK_TRACE_PARTS=1` to print every stream part the harness receives with the gap `--canary <ref>` selects another published canary ref. The runner rejects refs without a package
since the previous one. The harness decides a turn is over by reading those parts, so this is what artifact before it starts an eval. Local working trees, unpublished commits, and revision comparisons
to reach for when a turn ends too early or hangs past its closing message. are not supported by the native runner.
Use `--base` to compare a local Git revision with the working tree: The runner uses agent-eval's native Gateway harnesses: OpenCode for other providers, Claude Code
for Anthropic models, and Codex for OpenAI models. Each attempt starts an isolated Vercel Sandbox,
```sh then scaffolds the selected immutable canary with `npx` before the coding agent starts.
pnpm benchmark author-000-imessage --base origin/main --runs 3
```
Pass `--head` to compare two local revisions instead:
```sh
pnpm benchmark author-000-imessage \
--base origin/main \
--head feature-branch \
--runs 3
```
The runner archives each subject locally and uploads it to the sandbox. Revisions and local-only
commits do not need to be pushed. It maintains two persistent snapshot layers: a dependency
snapshot keyed by package-manager inputs, and a subject snapshot keyed by the source tree,
starting point, setup IDs, and bootstrap version. Source-only changes reuse the dependency snapshot
but create a new subject snapshot. For one eval and one run, `--verbose` streams setup phases,
assistant text, tool calls, grading, and build progress.
Local runs use the `guided` treatment by default, which keeps the `AGENTS.md` and aliases generated Local runs use the `guided` treatment by default, which keeps the `AGENTS.md` and aliases generated
by `eve init`. Pass `--treatment baseline` to remove those files before the coding agent starts. by `eve init`. Pass `--treatment baseline` to remove those files before the coding agent starts.
Results are written under `apps/benchmarks/results/`. Each run includes the transcript, Results are written under `apps/benchmarks/results/`. Each run includes the native transcript,
grader output, summary, copied project files, and `project/benchmark/timings.json`. The timing grader output, summary, copied project files, and validation output. Vercel Sandbox and AI Gateway
artifact records the snapshot-cache outcome, source installation and build phases, workspace setup, credentials are required.
each user turn with token and tool-call counts, and grading and validation durations. Use it to
separate sandbox setup time from agent time when comparing runs. Print a compact local report with:
```sh
pnpm benchmark:timings results/current/<timestamp>/<case>/run-1
```
Pass `--json` to print the original timing artifact. Vercel Sandbox and AI Gateway credentials are
required.
To read a whole results directory at once — pass/fail, agent time against setup time, turn and tool
counts, tokens, and any stalled turns:
```sh
node scripts/analyze.mjs results/current/<timestamp>
```
Pass `--docs` to also list, per run, which docs page the agent entered at and every page it went on
to read. That is the fastest way to see whether a documentation change moved agents toward the page
that answers the task or sent them spidering.
## Publish canonical results ## Publish canonical results
Canonical publication compares the `baseline` and `guided` treatments with the same eve revision, Canonical publication compares the `baseline` and `guided` treatments with the same immutable eve
model, harness, cases, and graders. The matrix holds the harness constant at OpenCode and varies canary, model, harness, cases, and graders. The configured harness reflects the provider: OpenCode
only the model, so rows are comparable. A model ID is selected independently from the coding-agent for other providers, Claude Code for Anthropic, and Codex for OpenAI. Publication requires a clean
harness; adding Claude Code, Codex, or Gemini CLI belongs to a separate harness comparison. Publication working tree and defaults to `origin/main`:
requires a clean working tree and defaults to `origin/main`:
```sh ```sh
pnpm benchmark:publish --dry pnpm benchmark:publish --dry
@@ -128,6 +90,6 @@ export default defineAuthoringCase({
}); });
``` ```
Use `simpleProject` for the selected subject's `eve init` output and `emptyProject` for an empty Use `simpleProject` for the selected canary's `eve init` output and `emptyProject` for a project
directory with the subject CLI installed. Put reusable setup under `lib/setups/`. Prefer source the coding agent creates. Put reusable setup under `lib/setups/`. Native runs support one-turn
and event assertions over an LLM judge. cases; the iMessage case remains local-only. Prefer source assertions over an LLM judge.
@@ -2,13 +2,15 @@ import { existsSync, readFileSync } from "node:fs";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
import { subjectDefaultAgentModel } from "./grader.js"; const projectRoot = "wayfinder";
const defaultAgentModel = "openai/gpt-5.6-luna-fast";
test("creates a complete eve project in place", () => { test("creates a complete eve project in place", () => {
expect(existsSync("agent/channels/eve.ts")).toBe(true); expect(existsSync(`${projectRoot}/agent/channels/eve.ts`)).toBe(true);
expect(existsSync("agent/instructions.md")).toBe(true); expect(existsSync(`${projectRoot}/agent/instructions.md`)).toBe(true);
const packageJson = JSON.parse(readFileSync("package.json", "utf8")) as { const packageJson = JSON.parse(readFileSync(`${projectRoot}/package.json`, "utf8")) as {
dependencies?: Record<string, string>; dependencies?: Record<string, string>;
scripts?: Record<string, string>; scripts?: Record<string, string>;
}; };
@@ -17,16 +19,16 @@ test("creates a complete eve project in place", () => {
}); });
test("authors the requested identity without pinning a different model", () => { test("authors the requested identity without pinning a different model", () => {
const instructions = readFileSync("agent/instructions.md", "utf8"); const instructions = readFileSync(`${projectRoot}/agent/instructions.md`, "utf8");
expect(instructions).toMatch(/Wayfinder/i); expect(instructions).toMatch(/Wayfinder/i);
expect(instructions).toMatch(/travel/i); expect(instructions).toMatch(/travel/i);
// `agent/agent.ts` is optional, and omitting it selects the same default the // `agent/agent.ts` is optional, and omitting it selects the same default the
// scaffold pins explicitly. Both shapes satisfy "use the default model"; a // scaffold pins explicitly. Both shapes satisfy "use the default model"; a
// different model id does not. // different model id does not.
if (existsSync("agent/agent.ts")) { if (existsSync(`${projectRoot}/agent/agent.ts`)) {
expect(readFileSync("agent/agent.ts", "utf8")).toContain( expect(readFileSync(`${projectRoot}/agent/agent.ts`, "utf8")).toContain(
`model: "${subjectDefaultAgentModel()}"`, `model: "${defaultAgentModel}"`,
); );
} }
}); });
@@ -4,7 +4,7 @@ export default defineAuthoringCase({
startingPoint: simpleProject, startingPoint: simpleProject,
async interact({ send }) { async interact({ send }) {
await send( await send(
"Put the agent on a cron: every weekday at 9am UTC, run it on a short prompt asking for a status digest. Nothing needs to be delivered anywhere — the run log is fine.", "In this eve project, add an agent schedule under `agent/schedules/`: every weekday at 9am UTC, run the agent on a short prompt asking for a status digest. Nothing needs to be delivered anywhere — the eve run log is fine.",
); );
}, },
}); });
+1 -10
View File
@@ -1,13 +1,6 @@
import type { HarnessV1NetworkSandboxSession } from "@ai-sdk/harness";
import type { HarnessAgentSession } from "@ai-sdk/harness/agent";
import { AUTHORING_EVAL_DIRECTORY } from "./paths.js";
import type { AuthoringTranscriptEntry } from "./protocol.js";
export interface AuthoringSetupContext { export interface AuthoringSetupContext {
readonly sandbox: HarnessV1NetworkSandboxSession;
readonly workspace: string; readonly workspace: string;
readonly artifactsRoot: typeof AUTHORING_EVAL_DIRECTORY; readonly artifactsRoot: string;
run(command: string, workingDirectory?: string): Promise<void>; run(command: string, workingDirectory?: string): Promise<void>;
write(path: string, content: string): Promise<void>; write(path: string, content: string): Promise<void>;
} }
@@ -34,8 +27,6 @@ export interface AuthoringTurn {
} }
export interface AuthoringInteractionContext { export interface AuthoringInteractionContext {
readonly session: HarnessAgentSession;
readonly transcript: ReadonlyArray<AuthoringTranscriptEntry>;
send(prompt: string): Promise<AuthoringTurn>; send(prompt: string): Promise<AuthoringTurn>;
} }
@@ -4,6 +4,7 @@ import { test } from "node:test";
import { import {
findBenchmarkModel, findBenchmarkModel,
findPublishedBenchmarkModel, findPublishedBenchmarkModel,
harnessId,
publishedBenchmark, publishedBenchmark,
publishedBenchmarkModels, publishedBenchmarkModels,
} from "./benchmark-config.ts"; } from "./benchmark-config.ts";
@@ -39,6 +40,9 @@ test("publishes only compatibility-validated models", () => {
assert.equal(findPublishedBenchmarkModel("claude-sonnet-5").harness, "Claude Code"); assert.equal(findPublishedBenchmarkModel("claude-sonnet-5").harness, "Claude Code");
assert.equal(findPublishedBenchmarkModel("claude-opus-5").harness, "Claude Code"); assert.equal(findPublishedBenchmarkModel("claude-opus-5").harness, "Claude Code");
assert.equal(findPublishedBenchmarkModel("kimi-k3").harness, "OpenCode"); assert.equal(findPublishedBenchmarkModel("kimi-k3").harness, "OpenCode");
assert.equal(findPublishedBenchmarkModel("gpt-5-6-sol").harness, "Codex");
assert.equal(findPublishedBenchmarkModel("gpt-5-6-terra").harness, "Codex");
assert.equal(harnessId("Codex"), "codex");
}); });
test("allows candidate probes and rejects unknown models", () => { test("allows candidate probes and rejects unknown models", () => {
+6 -4
View File
@@ -8,7 +8,7 @@ export interface AuthoringBenchmarkModel {
readonly id: string; readonly id: string;
readonly model: string; readonly model: string;
readonly displayName: string; readonly displayName: string;
readonly harness: "OpenCode" | "Claude Code"; readonly harness: "OpenCode" | "Claude Code" | "Codex";
readonly support: AuthoringBenchmarkSupport; readonly support: AuthoringBenchmarkSupport;
} }
@@ -45,14 +45,14 @@ export const benchmarkModels = [
id: "gpt-5-6-sol", id: "gpt-5-6-sol",
model: "openai/gpt-5.6-sol", model: "openai/gpt-5.6-sol",
displayName: "GPT-5.6 Sol", displayName: "GPT-5.6 Sol",
harness: "OpenCode", harness: "Codex",
support: "supported", support: "supported",
}, },
{ {
id: "gpt-5-6-terra", id: "gpt-5-6-terra",
model: "openai/gpt-5.6-terra", model: "openai/gpt-5.6-terra",
displayName: "GPT-5.6 Terra", displayName: "GPT-5.6 Terra",
harness: "OpenCode", harness: "Codex",
support: "supported", support: "supported",
}, },
{ {
@@ -110,7 +110,9 @@ export function publishedExperimentId(
} }
export function harnessId(harness: AuthoringBenchmarkModel["harness"]): string { export function harnessId(harness: AuthoringBenchmarkModel["harness"]): string {
return harness === "Claude Code" ? "claude-code" : "opencode"; if (harness === "Claude Code") return "claude-code";
if (harness === "Codex") return "codex";
return "opencode";
} }
export function parseAuthoringTreatment(value: string): AuthoringTreatment { export function parseAuthoringTreatment(value: string): AuthoringTreatment {
-211
View File
@@ -1,211 +0,0 @@
import type { HarnessV1SandboxProvider } from "@ai-sdk/harness";
import { createVercelSandbox } from "@ai-sdk/sandbox-vercel";
import { Sandbox } from "@vercel/sandbox";
import { SOURCE_ARCHIVE_PATH, SOURCE_ROOT } from "./paths.js";
import type { BenchmarkTimings } from "./timing.js";
const dependencySnapshots = new Map<string, Promise<string>>();
const subjectSnapshots = new Map<string, Promise<string>>();
export function createDependencyCachedSandbox(options: {
readonly archive: Uint8Array;
readonly dependencyArchive: Uint8Array;
readonly dependencyDigest: string;
readonly ports: ReadonlyArray<number>;
readonly env: Readonly<Record<string, string>>;
readonly log: (message: string) => void;
readonly timings: BenchmarkTimings;
}): HarnessV1SandboxProvider {
const sessionProvider = (snapshotId: string) =>
createVercelSandbox({
source: { type: "snapshot", snapshotId },
ports: [...options.ports],
timeout: 15 * 60_000,
env: { ...options.env },
networkPolicy: "allow-all",
});
return {
specificationVersion: "harness-sandbox-v1",
providerId: "eve-benchmark-vercel",
async createSession(request = {}) {
options.log("[setup] preparing dependency cache");
const dependencies = await dependencySnapshot(
options.dependencyArchive,
options.dependencyDigest,
options.log,
options.timings,
);
if (request.identity === undefined || request.onFirstCreate === undefined) {
return sessionProvider(dependencies).createSession(request);
}
const subject = await subjectSnapshot(
dependencies,
options.archive,
request.identity,
options.env,
request.onFirstCreate,
request.abortSignal,
options.timings,
);
return sessionProvider(subject).createSession({
sessionId: request.sessionId,
abortSignal: request.abortSignal,
});
},
async resumeSession(request) {
const dependencies = await dependencySnapshot(
options.dependencyArchive,
options.dependencyDigest,
options.log,
options.timings,
);
const provider = sessionProvider(dependencies);
if (provider.resumeSession === undefined) {
throw new Error("Vercel Sandbox does not support session resume.");
}
return provider.resumeSession(request);
},
};
}
function dependencySnapshot(
archive: Uint8Array,
digest: string,
log: (message: string) => void,
timings: BenchmarkTimings,
): Promise<string> {
const name = `eve-benchmark-dependencies-v5-${digest.slice(0, 24)}`;
let snapshot = dependencySnapshots.get(name);
if (snapshot !== undefined) {
timings.record("dependency-snapshot.memory-cache", 0, "success", { name });
return snapshot;
}
snapshot = createDependencySnapshot(name, archive, log, timings);
dependencySnapshots.set(name, snapshot);
return snapshot;
}
async function createDependencySnapshot(
name: string,
archive: Uint8Array,
log: (message: string) => void,
timings: BenchmarkTimings,
): Promise<string> {
let created = false;
const sandbox = await timings.measure("dependency-snapshot.get-or-create", () =>
Sandbox.getOrCreate({
name,
runtime: "node24",
timeout: 15 * 60_000,
persistent: true,
snapshotExpiration: 0,
networkPolicy: "allow-all",
async onCreate(current) {
created = true;
log("[setup] fetching workspace dependencies");
await current.writeFiles([{ path: SOURCE_ARCHIVE_PATH, content: archive }]);
const command = await current.runCommand("bash", [
"-lc",
`mkdir -p ${SOURCE_ROOT} && tar -xzf ${SOURCE_ARCHIVE_PATH} -C ${SOURCE_ROOT} && npm install --global pnpm@11.15.0 vitest@4.1.10 && cd ${SOURCE_ROOT} && pnpm fetch --frozen-lockfile`,
]);
if (command.exitCode !== 0) {
throw new Error(
`Dependency setup failed (${command.exitCode}):\n${await command.stdout()}\n${await command.stderr()}`,
);
}
},
}),
);
if (!created && sandbox.currentSnapshotId !== undefined) {
timings.record("dependency-snapshot.reused", 0, "success", { name });
return sandbox.currentSnapshotId;
}
return timings.measure("dependency-snapshot.publish", () =>
stopWithSnapshot(sandbox, "Dependency"),
);
}
// Publish bootstrap mutations explicitly; a layered Vercel template can expose
// its inherited snapshot ID before those mutations receive a new snapshot.
function subjectSnapshot(
dependencySnapshotId: string,
archive: Uint8Array,
identity: string,
env: Readonly<Record<string, string>>,
bootstrap: NonNullable<
NonNullable<Parameters<HarnessV1SandboxProvider["createSession"]>[0]>["onFirstCreate"]
>,
abortSignal: AbortSignal | undefined,
timings: BenchmarkTimings,
): Promise<string> {
const name = `eve-benchmark-subject-${identity}`;
let snapshot = subjectSnapshots.get(name);
if (snapshot !== undefined) {
timings.record("subject-snapshot.memory-cache", 0, "success", { name });
return snapshot;
}
snapshot = createSubjectSnapshot(
name,
dependencySnapshotId,
archive,
env,
bootstrap,
abortSignal,
timings,
);
subjectSnapshots.set(name, snapshot);
return snapshot;
}
async function createSubjectSnapshot(
name: string,
dependencySnapshotId: string,
archive: Uint8Array,
env: Readonly<Record<string, string>>,
bootstrap: NonNullable<
NonNullable<Parameters<HarnessV1SandboxProvider["createSession"]>[0]>["onFirstCreate"]
>,
abortSignal: AbortSignal | undefined,
timings: BenchmarkTimings,
): Promise<string> {
let created = false;
const sandbox = await timings.measure("subject-snapshot.get-or-create", () =>
Sandbox.getOrCreate({
name,
source: { type: "snapshot", snapshotId: dependencySnapshotId },
timeout: 15 * 60_000,
env: { ...env },
persistent: true,
snapshotExpiration: 0,
networkPolicy: "allow-all",
signal: abortSignal,
async onCreate(current) {
created = true;
await current.writeFiles([{ path: SOURCE_ARCHIVE_PATH, content: archive }]);
const provider = createVercelSandbox({ sandbox: current });
const session = await provider.createSession({ abortSignal });
await bootstrap(session.restricted(), { abortSignal });
},
}),
);
if (!created && sandbox.currentSnapshotId !== undefined) {
timings.record("subject-snapshot.reused", 0, "success", { name });
return sandbox.currentSnapshotId;
}
return timings.measure("subject-snapshot.publish", () =>
stopWithSnapshot(sandbox, "Subject", abortSignal),
);
}
async function stopWithSnapshot(
sandbox: Sandbox,
label: string,
signal?: AbortSignal,
): Promise<string> {
const stopped = await sandbox.stop(signal === undefined ? undefined : { signal });
const snapshotId = stopped.snapshot?.id ?? sandbox.currentSnapshotId;
if (snapshotId === undefined) throw new Error(`${label} snapshot was not published.`);
return snapshotId;
}
+58 -16
View File
@@ -1,6 +1,8 @@
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { createJiti } from "jiti";
export function fixtureNames(evalsRoot) { export function fixtureNames(evalsRoot) {
return readdirSync(evalsRoot, { withFileTypes: true }) return readdirSync(evalsRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && existsSync(join(evalsRoot, entry.name, "CASE.ts"))) .filter((entry) => entry.isDirectory() && existsSync(join(evalsRoot, entry.name, "CASE.ts")))
@@ -8,10 +10,25 @@ export function fixtureNames(evalsRoot) {
.sort(); .sort();
} }
export function prepareFixtures(evalsRoot, names = fixtureNames(evalsRoot)) { export async function prepareFixtures(evalsRoot, subject, names = fixtureNames(evalsRoot)) {
for (const name of names) { for (const name of names) {
const fixtureRoot = join(evalsRoot, name); const fixtureRoot = join(evalsRoot, name);
writeFileSync(join(fixtureRoot, "PROMPT.md"), ""); const authoringCase = await loadCase(fixtureRoot);
writeFileSync(
join(fixtureRoot, "PROMPT.md"),
`${await promptForCase(authoringCase, fixtureRoot)}\n`,
);
writeFileSync(
join(fixtureRoot, ".eve-authoring-bootstrap.json"),
`${JSON.stringify({
startingPoint: authoringCase.startingPoint.workspace,
projectDirectory: authoringCase.projectDirectory,
revision: subject.revision,
setupIds: [authoringCase.startingPoint.setup, authoringCase.setup]
.filter(Boolean)
.map((setup) => setup.id),
})}\n`,
);
writeFileSync( writeFileSync(
join(fixtureRoot, "package.json"), join(fixtureRoot, "package.json"),
`${JSON.stringify({ name: `eve-authoring-${name}`, private: true, type: "module" }, null, 2)}\n`, `${JSON.stringify({ name: `eve-authoring-${name}`, private: true, type: "module" }, null, 2)}\n`,
@@ -19,29 +36,54 @@ export function prepareFixtures(evalsRoot, names = fixtureNames(evalsRoot)) {
} }
} }
async function loadCase(fixtureRoot) {
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
let authoringCase = await jiti.import(`${fixtureRoot}/CASE.ts`);
while (!isAuthoringCase(authoringCase) && hasDefaultExport(authoringCase)) {
authoringCase = authoringCase.default;
}
if (!isAuthoringCase(authoringCase)) {
throw new Error(`${fixtureRoot}/CASE.ts must export an authoring case as default.`);
}
return authoringCase;
}
async function promptForCase(authoringCase, fixtureRoot) {
const prompts = [];
await authoringCase.interact({
send: async (prompt) => {
prompts.push(prompt);
return { text: "", toolCalls: [] };
},
});
if (prompts.length !== 1) {
throw new Error(
`${fixtureRoot}/CASE.ts is multi-turn and cannot run on a native agent-eval runner.`,
);
}
return prompts[0];
}
function isAuthoringCase(value) {
return typeof value === "object" && value !== null && typeof value.interact === "function";
}
function hasDefaultExport(value) {
return typeof value === "object" && value !== null && "default" in value;
}
export function resetExperiments(experimentsRoot) { export function resetExperiments(experimentsRoot) {
rmSync(experimentsRoot, { recursive: true, force: true }); rmSync(experimentsRoot, { recursive: true, force: true });
mkdirSync(experimentsRoot, { recursive: true }); mkdirSync(experimentsRoot, { recursive: true });
} }
export function writeSubjectArchives(experimentsRoot, subject, name) {
const archiveName = `${name}.source.tar.gz`;
const dependencyArchiveName = `${name}.dependencies.tar.gz`;
writeFileSync(join(experimentsRoot, archiveName), subject.archive);
writeFileSync(join(experimentsRoot, dependencyArchiveName), subject.dependencyArchive);
return { archiveName, dependencyArchiveName };
}
export function writeExperiment(experimentsRoot, name, options) { export function writeExperiment(experimentsRoot, name, options) {
writeFileSync( writeFileSync(
join(experimentsRoot, `${name}.ts`), join(experimentsRoot, `${name}.ts`),
`import { readFileSync } from "node:fs";\n` + `import { authoringExperiment } from "../lib/experiment.js";\n\n` +
`import { authoringExperiment } from "../lib/experiment.js";\n\n` +
`export default authoringExperiment({\n` + `export default authoringExperiment({\n` +
` archive: readFileSync(new URL(${JSON.stringify(`./${options.archiveName}`)}, import.meta.url)),\n` + ` revision: ${JSON.stringify(options.revision)},\n` +
` dependencyArchive: readFileSync(new URL(${JSON.stringify(`./${options.dependencyArchiveName}`)}, import.meta.url)),\n` + ` packageSpec: ${JSON.stringify(options.packageSpec)},\n` +
` digest: ${JSON.stringify(options.digest)},\n` +
` dependencyDigest: ${JSON.stringify(options.dependencyDigest)},\n` +
` runs: ${options.runs},\n` + ` runs: ${options.runs},\n` +
(options.evals === undefined ? "" : ` evals: ${JSON.stringify(options.evals)},\n`) + (options.evals === undefined ? "" : ` evals: ${JSON.stringify(options.evals)},\n`) +
` benchmark: ${JSON.stringify(options.benchmark)},\n` + ` benchmark: ${JSON.stringify(options.benchmark)},\n` +
+17 -20
View File
@@ -9,14 +9,11 @@ import {
prepareFixtures, prepareFixtures,
resetExperiments, resetExperiments,
writeExperiment, writeExperiment,
writeSubjectArchives,
} from "./experiment-files.mjs"; } from "./experiment-files.mjs";
const subject = { const subject = {
archive: Buffer.from("source"), revision: "1234567890abcdef1234567890abcdef12345678",
dependencyArchive: Buffer.from("dependencies"), packageSpec: "https://pkg.eve.dev/1234567890abcdef1234567890abcdef12345678/eve.tgz",
digest: "source-digest",
dependencyDigest: "dependency-digest",
}; };
const benchmark = { const benchmark = {
id: "test", id: "test",
@@ -26,7 +23,7 @@ const benchmark = {
support: "supported", support: "supported",
}; };
test("materializes fixtures and complete experiment inputs", () => { test("materializes fixtures and complete experiment inputs", async () => {
const root = mkdtempSync(join(tmpdir(), "eve-benchmark-experiments-")); const root = mkdtempSync(join(tmpdir(), "eve-benchmark-experiments-"));
const evals = join(root, "evals"); const evals = join(root, "evals");
const experiments = join(root, "experiments"); const experiments = join(root, "experiments");
@@ -36,8 +33,12 @@ test("materializes fixtures and complete experiment inputs", () => {
writeFileSync(join(evals, "not-a-case"), "ignored"); writeFileSync(join(evals, "not-a-case"), "ignored");
assert.deepEqual(fixtureNames(evals), ["author-001-first", "author-002-second"]); assert.deepEqual(fixtureNames(evals), ["author-001-first", "author-002-second"]);
prepareFixtures(evals); await prepareFixtures(evals, subject);
assert.equal(readFileSync(join(evals, "author-001-first", "PROMPT.md"), "utf8"), ""); assert.equal(readFileSync(join(evals, "author-001-first", "PROMPT.md"), "utf8"), "Build it.\n");
assert.deepEqual(
JSON.parse(readFileSync(join(evals, "author-001-first", ".eve-authoring-bootstrap.json"))),
{ startingPoint: "scaffolded", revision: subject.revision, setupIds: [] },
);
assert.deepEqual(JSON.parse(readFileSync(join(evals, "author-001-first", "package.json"))), { assert.deepEqual(JSON.parse(readFileSync(join(evals, "author-001-first", "package.json"))), {
name: "eve-authoring-author-001-first", name: "eve-authoring-author-001-first",
private: true, private: true,
@@ -45,25 +46,18 @@ test("materializes fixtures and complete experiment inputs", () => {
}); });
resetExperiments(experiments); resetExperiments(experiments);
const archives = writeSubjectArchives(experiments, subject, "published-deadbeef");
writeExperiment(experiments, "test-opencode--guided", { writeExperiment(experiments, "test-opencode--guided", {
...archives, revision: subject.revision,
digest: subject.digest, packageSpec: subject.packageSpec,
dependencyDigest: subject.dependencyDigest,
runs: 3, runs: 3,
evals: ["author-001-first"], evals: ["author-001-first"],
benchmark, benchmark,
treatment: "guided", treatment: "guided",
}); });
assert.equal(readFileSync(join(experiments, archives.archiveName), "utf8"), "source");
assert.equal(
readFileSync(join(experiments, archives.dependencyArchiveName), "utf8"),
"dependencies",
);
const experiment = readFileSync(join(experiments, "test-opencode--guided.ts"), "utf8"); const experiment = readFileSync(join(experiments, "test-opencode--guided.ts"), "utf8");
assert.match(experiment, /dependencyArchive: readFileSync/u); assert.match(experiment, /revision: "1234567890abcdef1234567890abcdef12345678"/u);
assert.match(experiment, /published-deadbeef\.dependencies\.tar\.gz/u); assert.match(experiment, /pkg\.eve\.dev\/1234567890abcdef1234567890abcdef12345678\/eve\.tgz/u);
} finally { } finally {
rmSync(root, { recursive: true, force: true }); rmSync(root, { recursive: true, force: true });
} }
@@ -72,6 +66,9 @@ test("materializes fixtures and complete experiment inputs", () => {
function writeCase(evals, name) { function writeCase(evals, name) {
const root = join(evals, name); const root = join(evals, name);
mkdirSync(root, { recursive: true }); mkdirSync(root, { recursive: true });
writeFileSync(join(root, "CASE.ts"), "export default {};\n"); writeFileSync(
join(root, "CASE.ts"),
'export default { startingPoint: { workspace: "scaffolded" }, async interact({ send }) { await send("Build it."); } };\n',
);
assert.ok(existsSync(root)); assert.ok(existsSync(root));
} }
+57
View File
@@ -0,0 +1,57 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { createJiti } from "jiti";
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
const { authoringExperiment } = await jiti.import(
new URL("./experiment.ts", import.meta.url).pathname,
);
const common = {
revision: "1234567890abcdef1234567890abcdef12345678",
packageSpec: "https://pkg.eve.dev/1234567890abcdef1234567890abcdef12345678/eve.tgz",
treatment: "guided",
};
test("uses native Gateway agents and canonical cases", () => {
const config = authoringExperiment({
...common,
benchmark: {
id: "gpt-5-6-sol",
model: "openai/gpt-5.6-sol",
displayName: "GPT-5.6 Sol",
harness: "Codex",
support: "supported",
},
});
assert.equal(config.agent, "vercel-ai-gateway/codex");
assert.equal(config.model, "openai/gpt-5.6-sol");
assert.deepEqual(config.evals, [
"author-001-weather-tool",
"author-002-new-project",
"author-003-openapi-connection",
"author-004-packaged-skill",
"author-005-conditional-approval",
"author-006-custom-channel",
"author-007-digest-schedule",
]);
assert.equal(typeof config.setup, "function");
});
test("strips the Gateway provider prefix for native Claude Code", () => {
const config = authoringExperiment({
...common,
benchmark: {
id: "claude-sonnet-5",
model: "anthropic/claude-sonnet-5",
displayName: "Claude Sonnet 5",
harness: "Claude Code",
support: "supported",
},
});
assert.equal(config.agent, "vercel-ai-gateway/claude-code");
assert.equal(config.model, "claude-sonnet-5");
});
+25 -23
View File
@@ -1,33 +1,28 @@
import type { ExperimentConfig } from "@vercel/agent-eval"; import type { ExperimentConfig } from "@vercel/agent-eval";
import { registerAgent } from "@vercel/agent-eval";
import type { AuthoringBenchmarkModel, AuthoringTreatment } from "./benchmark-config.js"; import {
import { createAuthoringAgent } from "./harness-agent.js"; harnessId,
type AuthoringBenchmarkModel,
type AuthoringTreatment,
publishedBenchmark,
} from "./benchmark-config.js";
import { createNativeAuthoringSetup } from "./native-authoring-setup.js";
export function authoringExperiment(options: { export function authoringExperiment(options: {
readonly archive: Uint8Array; readonly revision: string;
readonly dependencyArchive: Uint8Array; readonly packageSpec: string;
readonly digest: string;
readonly dependencyDigest: string;
readonly runs?: number; readonly runs?: number;
readonly evals?: readonly string[]; readonly evals?: readonly string[];
readonly benchmark: AuthoringBenchmarkModel; readonly benchmark: AuthoringBenchmarkModel;
readonly treatment: AuthoringTreatment; readonly treatment: AuthoringTreatment;
readonly verbose?: boolean; readonly verbose?: boolean;
}): ExperimentConfig { }): ExperimentConfig {
const agent = createAuthoringAgent({
harness: options.benchmark.harness,
model: options.benchmark.model,
archive: options.archive,
dependencyArchive: options.dependencyArchive,
digest: options.digest,
dependencyDigest: options.dependencyDigest,
});
registerAgent(agent);
return { return {
agent: agent.name, agent: `vercel-ai-gateway/${harnessId(options.benchmark.harness)}`,
model: options.benchmark.model, model: nativeModel(options.benchmark),
evals: process.env.EVE_BENCHMARK_EVAL ?? (options.evals ? [...options.evals] : "*"), evals:
process.env.EVE_BENCHMARK_EVAL ??
(options.evals ? [...options.evals] : [...publishedBenchmark.caseIds]),
scripts: ["typecheck", "build"], scripts: ["typecheck", "build"],
runs: options.runs ?? 1, runs: options.runs ?? 1,
earlyExit: false, earlyExit: false,
@@ -36,9 +31,16 @@ export function authoringExperiment(options: {
timeout: Number(process.env.EVE_BENCHMARK_TIMEOUT ?? 900), timeout: Number(process.env.EVE_BENCHMARK_TIMEOUT ?? 900),
sandbox: "vercel", sandbox: "vercel",
copyFiles: "changed", copyFiles: "changed",
agentOptions: { setup: createNativeAuthoringSetup({
agentsMd: options.treatment === "guided", packageSpec: options.packageSpec,
verbose: options.verbose ?? false, revision: options.revision,
}, treatment: options.treatment,
}),
agentOptions: { verbose: options.verbose ?? false },
}; };
} }
function nativeModel(benchmark: AuthoringBenchmarkModel): string {
if (benchmark.harness !== "Claude Code") return benchmark.model;
return benchmark.model.replace(/^anthropic\//u, "");
}
@@ -1,56 +0,0 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { createJiti } from "jiti";
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
const { addUsage } = await jiti.import(new URL("./harness-agent.ts", import.meta.url).pathname);
test("normalizes nested Claude Code token usage", () => {
const usage = {
inputTokens: 0,
outputTokens: 0,
reasoningTokens: 0,
cachedInputTokens: 0,
cacheWriteTokens: 0,
};
addUsage(usage, {
inputTokens: { total: 32_177, noCache: 1, cacheRead: 32_176, cacheWrite: 10_944 },
outputTokens: { total: 327, text: 327 },
});
assert.deepEqual(usage, {
inputTokens: 1,
outputTokens: 327,
reasoningTokens: 0,
cachedInputTokens: 32_176,
cacheWriteTokens: 10_944,
});
});
test("retains flat OpenCode token usage", () => {
const usage = {
inputTokens: 0,
outputTokens: 0,
reasoningTokens: 0,
cachedInputTokens: 0,
cacheWriteTokens: 0,
};
addUsage(usage, {
inputTokens: 100,
outputTokens: 20,
reasoningTokens: 5,
cachedInputTokens: 50,
cacheWriteTokens: 10,
});
assert.deepEqual(usage, {
inputTokens: 100,
outputTokens: 20,
reasoningTokens: 5,
cachedInputTokens: 50,
cacheWriteTokens: 10,
});
});
-766
View File
@@ -1,766 +0,0 @@
import { readFileSync } from "node:fs";
import type { HarnessV1, HarnessV1NetworkSandboxSession } from "@ai-sdk/harness";
import type { HarnessAgentSession } from "@ai-sdk/harness/agent";
import { HarnessAgent } from "@ai-sdk/harness/agent";
import { createClaudeCode } from "@ai-sdk/harness-claude-code";
import { createOpenCode } from "@ai-sdk/harness-opencode";
import type { Agent, AgentRunResult } from "@vercel/agent-eval";
import { z } from "zod";
import type {
AuthoringCase,
AuthoringSetup,
AuthoringSetupContext,
AuthoringTurn,
} from "./authoring-case.js";
import type { AuthoringBenchmarkModel } from "./benchmark-config.js";
import { createDependencyCachedSandbox } from "./dependency-sandbox.js";
import { loadAuthoringCase } from "./load-authoring-case.js";
import {
AGENT_EVAL_DIRECTORY,
AUTHORING_EVAL_DIRECTORY,
AUTHORING_EVAL_DIRECTORY_ENV,
EVE_PACKAGE_PATH,
SOURCE_ARCHIVE_PATH,
SOURCE_ROOT,
WORKSPACE,
} from "./paths.js";
import type { AuthoringTokenUsage, AuthoringTranscriptEntry } from "./protocol.js";
import { BenchmarkTimings } from "./timing.js";
const HARNESS_BRIDGE_PORT = 4172;
const POST_RUN_GRADER_DIRECTORY = ".eve-grader";
const BOOTSTRAP_VERSION = "v9";
// A turn that stops producing output should end the turn, not the eval: the
// remaining turns still run and the graders still see what the agent did.
const TURN_TIMEOUT_SECONDS = Number(process.env.EVE_BENCHMARK_TURN_TIMEOUT ?? 300);
// A turn that has produced no chunk for this long is waiting on the runtime, not
// working. The bound is one slow tool call: the bridge reports a call and its
// result together once the call returns, so an install that takes minutes looks
// from here like silence rather than work in flight.
const TURN_STALL_SECONDS = Number(process.env.EVE_BENCHMARK_TURN_STALL ?? 120);
// The runtime frequently never closes a turn whose last act was the model
// talking, so silence after a closing message has to end the turn on its own.
// Long enough that a pause before the next tool call is not mistaken for one.
const CLOSING_IDLE_MILLIS = 45_000;
// How long to let an aborted turn settle before starting the next one.
const TURN_SETTLE_MILLIS = 15_000;
// The coding agent's own question tool is the one place it can ask the user
// mid-turn. Left unanswered it waits for a human and the runtime stops driving
// the turn, so the harness answers it immediately and points the agent at the
// channel this benchmark can answer on: its reply, which the case's next `send`
// responds to.
type AuthoringHarnessAgent = HarnessAgent<HarnessV1, typeof INTERACTIVE_QUESTION_TOOL>;
const INTERACTIVE_QUESTION_TOOL = {
question: {
description: "Ask the user a question and wait for their answer.",
inputSchema: z.looseObject({}),
execute: async () =>
"The user cannot answer an interactive prompt in this environment. Ask the question in your reply instead and end your turn; the user answers in their next message.",
},
};
export function createAuthoringAgent(subject: {
readonly harness: AuthoringBenchmarkModel["harness"];
readonly model: string;
readonly archive: Uint8Array;
readonly dependencyArchive: Uint8Array;
readonly digest: string;
readonly dependencyDigest: string;
}): Agent {
return {
name: harnessId(subject.harness),
displayName: `${subject.harness} eve authoring harness`,
getApiKeyEnvVar: () => "AI_GATEWAY_API_KEY",
getDefaultModel: () => subject.model,
definition: {
name: harnessId(subject.harness),
displayName: `${subject.harness} eve authoring harness`,
defaultModel: subject.model,
o11yAgentName: harnessId(subject.harness),
runnerPath: "",
getApiKeyEnvVar: () => "AI_GATEWAY_API_KEY",
install: () => [],
configFiles: () => [],
authEnv: () => ({}),
},
async run(fixturePath, options): Promise<AgentRunResult> {
const verbose = options.agentOptions?.verbose === true;
const log = (message: string) => {
if (verbose) console.log(message);
};
const authoringCase = await loadAuthoringCase(fixturePath);
const setups = [authoringCase.startingPoint.setup, authoringCase.setup].filter(
(setup): setup is AuthoringSetup => setup !== undefined,
);
const timings = new BenchmarkTimings();
timings.record("run.context", 0, "success", {
sourceArchiveBytes: subject.archive.length,
dependencyArchiveBytes: subject.dependencyArchive.length,
sourceDigest: subject.digest,
dependencyDigest: subject.dependencyDigest,
startingPoint: authoringCase.startingPoint.id,
setupCount: setups.length,
});
const sandbox = createDependencyCachedSandbox({
archive: subject.archive,
dependencyArchive: subject.dependencyArchive,
dependencyDigest: subject.dependencyDigest,
ports: [HARNESS_BRIDGE_PORT, ...new Set(setups.flatMap((setup) => setup.ports ?? []))],
env: {
EVE_INIT_PACKAGE_SPEC: EVE_PACKAGE_PATH,
[AUTHORING_EVAL_DIRECTORY_ENV]: AUTHORING_EVAL_DIRECTORY,
...Object.assign({}, ...setups.map((setup) => setup.environment ?? {})),
},
log,
timings,
});
const startedAt = Date.now();
const commands: string[] = [];
const transcript: AuthoringTranscriptEntry[] = [];
let session: HarnessAgentSession | undefined;
let activeSandbox: HarnessV1NetworkSandboxSession | undefined;
let workspace: string | undefined;
const agent = new HarnessAgent({
id: "eve-authoring-eval",
harness: createHarness(subject.harness, options.model ?? subject.model),
sandbox,
tools: INTERACTIVE_QUESTION_TOOL,
sandboxConfig: {
workDir: WORKSPACE,
bootstrapHash: bootstrapHash(authoringCase, subject),
onBootstrap: async ({ session: bootstrap, workDir }) => {
const bootstrapSandbox = bootstrap as HarnessV1NetworkSandboxSession;
const context = setupContext(bootstrapSandbox, workDir);
log("[setup] building the selected eve source");
await bootstrapSubject(
bootstrapSandbox,
workDir,
authoringCase.startingPoint.workspace,
subject.archive,
timings,
);
// Case setup runs against the finished starting point. A setup that
// installs a fixture dependency would otherwise create the project's
// `package.json` itself, and `eve init` would then treat the
// workspace as an existing package and skip its own scaffold.
await timings.measure("subject.case-bootstrap", async () => {
for (const setup of setups) await setup.onBootstrap?.(context);
});
},
onSession: async ({ session: current, sessionWorkDir }) => {
activeSandbox = current as HarnessV1NetworkSandboxSession;
workspace = sessionWorkDir;
const context = setupContext(activeSandbox, workspace);
await ensureWorkspace(
context,
authoringCase.startingPoint.workspace,
subject.archive,
timings,
);
await timings.measure("session.setup", async () => {
for (const setup of setups) await setup.onSession?.(context);
if (options.agentOptions?.agentsMd !== true) await installBaselineEveWrapper(context);
});
if (options.agentOptions?.agentsMd !== true) {
await context.run("rm -f AGENTS.md CLAUDE.md GEMINI.md");
}
},
},
permissionMode: "allow-all",
});
try {
session = await timings.measure("session.create", () =>
agent.createSession({ abortSignal: options.signal }),
);
if (activeSandbox === undefined || workspace === undefined) {
throw new Error("HarnessAgent did not initialize its sandbox session.");
}
await authoringCase.interact({
session,
transcript,
send: async (prompt) => {
const turn = transcript.filter((entry) => entry.role === "user").length + 1;
transcript.push({ role: "user", content: prompt });
if (verbose) console.log(`[user] ${prompt}`);
const result = await timings.measure(`agent.turn.${turn}`, () =>
runTurn({
agent,
session: session!,
prompt,
timeout: options.timeout,
verbose,
abortSignal: options.signal,
}),
);
const toolCalls = result.toolCalls;
const usage = result.usage;
transcript.push({ role: "assistant", content: result.text, toolCalls, usage });
const details: Record<string, string | number | boolean> = {
promptCharacters: prompt.length,
responseCharacters: result.text.length,
toolCalls: toolCalls.length,
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
reasoningTokens: usage.reasoningTokens,
};
if (result.stall !== undefined) details.stall = result.stall;
timings.record(
`agent.turn.${turn}.summary`,
0,
result.stall === undefined ? "success" : "failure",
details,
);
if (result.stall !== undefined) log(`[turn ${turn}] ${result.stall}`);
commands.push(...shellCommands(result.toolCalls));
return { text: result.text, toolCalls };
},
});
const projectWorkspace =
authoringCase.projectDirectory === undefined
? workspace
: `${workspace}/${authoringCase.projectDirectory}`;
const context = setupContext(activeSandbox, workspace);
const graderContext = setupContext(activeSandbox, projectWorkspace);
await prepareGraderDirectory(context);
await context.write(
`${AGENT_EVAL_DIRECTORY}/results.json`,
JSON.stringify({ o11y: { shellCommands: commands.map((command) => ({ command })) } }),
);
await context.write(
`${AGENT_EVAL_DIRECTORY}/harness-transcript.json`,
JSON.stringify(transcript),
);
await Promise.all([
graderContext.write(
`${POST_RUN_GRADER_DIRECTORY}/EVAL.test.ts`,
readFileSync(`${fixturePath}/EVAL.ts`, "utf8"),
),
graderContext.write(
`${POST_RUN_GRADER_DIRECTORY}/grader.ts`,
readFileSync(new URL("./grader.ts", import.meta.url), "utf8"),
),
graderContext.write(
`${POST_RUN_GRADER_DIRECTORY}/paths.ts`,
readFileSync(new URL("./paths.ts", import.meta.url), "utf8"),
),
graderContext.write(
`${POST_RUN_GRADER_DIRECTORY}/protocol.ts`,
readFileSync(new URL("./protocol.ts", import.meta.url), "utf8"),
),
]);
log("[grade] running deterministic assertions");
const test = await timings.measure("validation.grader", () =>
resultOf(
activeSandbox!,
`vitest run ${POST_RUN_GRADER_DIRECTORY}/EVAL.test.ts`,
projectWorkspace,
),
);
const scriptsResults = Object.fromEntries(
await Promise.all(
(options.scripts ?? []).map(async (script) => {
log(`[${script}] running`);
const result = await timings.measure(`validation.${script}`, () =>
resultOf(activeSandbox!, `npm run ${shellQuote(script)}`, projectWorkspace),
);
return [
script,
{ success: result.exitCode === 0, output: `${result.stdout}${result.stderr}` },
] as const;
}),
),
);
const scriptsPassed = Object.values(scriptsResults).every((result) => result.success);
timings.record("run.total", Date.now() - startedAt);
await context.write(
`${AGENT_EVAL_DIRECTORY}/timings.json`,
JSON.stringify(timings.entries),
);
logTimingSummary(log, timings);
log(`[result] ${test.exitCode === 0 && scriptsPassed ? "passed" : "failed"}`);
return {
success: test.exitCode === 0 && scriptsPassed,
output: transcript.at(-1)?.content ?? "",
error:
test.exitCode === 0 && scriptsPassed ? undefined : `${test.stdout}\n${test.stderr}`,
duration: Date.now() - startedAt,
testResult: { success: test.exitCode === 0, output: `${test.stdout}${test.stderr}` },
transcript: harnessTranscript(transcript),
scriptsResults,
sandboxId: activeSandbox.id,
generatedFiles: timingArtifact(timings),
};
} catch (error) {
timings.record("run.total", Date.now() - startedAt, "failure");
if (activeSandbox !== undefined && workspace !== undefined) {
await prepareGraderDirectory(setupContext(activeSandbox, workspace)).catch(
() => undefined,
);
await Promise.all([
setupContext(activeSandbox, workspace).write(
`${AGENT_EVAL_DIRECTORY}/harness-transcript.json`,
JSON.stringify(transcript),
),
setupContext(activeSandbox, workspace).write(
`${AGENT_EVAL_DIRECTORY}/timings.json`,
JSON.stringify(timings.entries),
),
]).catch(() => undefined);
}
const result: AgentRunResult = {
success: false,
output: transcript.at(-1)?.content ?? "",
error: error instanceof Error ? error.message : String(error),
duration: Date.now() - startedAt,
transcript: harnessTranscript(transcript),
scriptsResults: {},
generatedFiles: timingArtifact(timings),
};
if (activeSandbox !== undefined) result.sandboxId = activeSandbox.id;
return result;
} finally {
await session?.destroy();
}
},
};
}
interface AuthoringTurnResult extends AuthoringTurn {
readonly usage: AuthoringTokenUsage;
/** Set when the turn did not finish on its own and was cut short. */
readonly stall?: string;
}
// One streaming path for every turn, so a turn that never finishes still yields
// the text and tool calls it produced. The underlying runtime can leave a turn
// open indefinitely after the model's last message, and a turn that consumed the
// whole eval budget used to discard the transcript along with it.
async function runTurn(input: {
readonly agent: AuthoringHarnessAgent;
readonly session: HarnessAgentSession;
readonly prompt: string;
readonly timeout: number;
readonly verbose: boolean;
readonly abortSignal?: AbortSignal;
}): Promise<AuthoringTurnResult> {
const { agent, session, prompt, timeout, verbose } = input;
const steps: string[] = [];
const toolCalls: Array<{ name: string; input: unknown }> = [];
const usage = {
inputTokens: 0,
outputTokens: 0,
reasoningTokens: 0,
cachedInputTokens: 0,
cacheWriteTokens: 0,
};
let current = "";
let lineOpen = false;
const turnTimeout = Math.min(timeout, TURN_TIMEOUT_SECONDS);
let stall: string | undefined;
// The runtime routinely leaves a turn open after the model's last message and
// honors neither the total nor the per-chunk budget the SDK passes down, so
// the harness has to decide when a turn is over. Text the model never follows
// with a tool call is its closing message, so silence after it ends the turn;
// silence anywhere else can still be work in flight.
const controller = new AbortController();
const abort = () => controller.abort();
input.abortSignal?.addEventListener("abort", abort, { once: true });
const deadline = Date.now() + turnTimeout * 1000;
try {
const result = await agent.stream({
session,
prompt,
timeout: { totalMs: turnTimeout * 1000, chunkMs: TURN_STALL_SECONDS * 1000 },
abortSignal: controller.signal,
});
const stream = result.fullStream[Symbol.asyncIterator]();
const tracing = process.env.EVE_BENCHMARK_TRACE_PARTS === "1";
let lastPartAt = Date.now();
let pendingTools = 0;
let stepCalledTool = false;
let turnLooksDone = false;
for (;;) {
const idle = pendingTools > 0 ? Number.POSITIVE_INFINITY : idleBudget(turnLooksDone);
const budget = Math.min(idle, deadline - Date.now());
const next = await withDeadline(stream.next(), budget);
if (next === "expired") {
if (Date.now() >= deadline) stall = `turn exceeded its ${turnTimeout}s budget`;
else if (!turnLooksDone) stall = `turn produced no output for ${TURN_STALL_SECONDS}s`;
await settleStalledTurn(stream, controller, stall ?? "turn ended on its closing message");
break;
}
if (next.done === true) break;
const part = next.value;
if (tracing) {
const now = Date.now();
console.log(`[part] +${((now - lastPartAt) / 1000).toFixed(1)}s ${part.type}`);
lastPartAt = now;
}
if (part.type === "text-delta") {
current += part.text;
// Text the model is not following with a tool call is it signing off.
// The runtime often emits nothing after that closing message, not even
// the step boundary, so the text itself has to be the signal.
turnLooksDone = true;
if (verbose) {
if (!lineOpen) process.stdout.write("[assistant] ");
lineOpen = true;
process.stdout.write(part.text);
}
continue;
}
if (verbose && lineOpen) process.stdout.write("\n");
lineOpen = false;
if (part.type === "tool-call") {
// Only a tool call reopens the turn. The parts that bracket a message
// (`text-start`, `text-end`) are not work, and treating them as work is
// what used to hide a closing message behind the full stall budget.
turnLooksDone = false;
pendingTools += 1;
stepCalledTool = true;
toolCalls.push({ name: part.toolName, input: part.input });
if (verbose) console.log(`[tool] ${formatToolCall(part.toolName, part.input)}`);
} else if (part.type === "tool-result" || part.type === "tool-error") {
pendingTools = Math.max(0, pendingTools - 1);
} else if (part.type === "finish-step") {
if (current.trim().length > 0) steps.push(current.trim());
current = "";
addUsage(usage, (part as { usage?: unknown }).usage);
turnLooksDone = !stepCalledTool;
stepCalledTool = false;
} else if (part.type === "finish") {
replaceUsage(usage, (part as { totalUsage?: unknown }).totalUsage);
turnLooksDone = true;
}
}
} catch (error) {
stall = `turn did not finish: ${error instanceof Error ? error.message : String(error)}`;
} finally {
input.abortSignal?.removeEventListener("abort", abort);
if (verbose && lineOpen) process.stdout.write("\n");
}
if (current.trim().length > 0) steps.push(current.trim());
const turn: AuthoringTurnResult = { text: steps.join("\n\n"), toolCalls, usage };
return stall === undefined ? turn : { ...turn, stall };
}
function idleBudget(turnLooksDone: boolean): number {
return turnLooksDone ? CLOSING_IDLE_MILLIS : TURN_STALL_SECONDS * 1000;
}
// Aborting is what marks the turn finished. Walking away from the stream is not
// enough: the session keeps the turn open and rejects the next prompt as one
// already in progress, which loses every remaining turn of the case. Draining
// afterwards gives that settlement time to land before the next prompt.
async function settleStalledTurn(
stream: AsyncIterator<unknown>,
controller: AbortController,
reason: string,
): Promise<void> {
controller.abort(new Error(reason));
await withDeadline(
(async () => {
try {
while ((await stream.next()).done !== true);
} catch {
// The abort surfaces here as a rejection, which is the settlement.
}
})(),
TURN_SETTLE_MILLIS,
);
}
async function withDeadline<T>(promise: Promise<T>, millis: number): Promise<T | "expired"> {
if (millis <= 0) return "expired";
let timer: NodeJS.Timeout | undefined;
try {
return await Promise.race([
promise,
new Promise<"expired">((resolve) => {
timer = setTimeout(() => resolve("expired"), millis);
}),
]);
} finally {
if (timer !== undefined) clearTimeout(timer);
}
}
type MutableAuthoringTokenUsage = {
-readonly [Key in keyof AuthoringTokenUsage]: AuthoringTokenUsage[Key];
};
export function addUsage(total: MutableAuthoringTokenUsage, value: unknown): void {
if (typeof value !== "object" || value === null) return;
const usage = value as Record<string, unknown>;
const input = usage.inputTokens;
const output = usage.outputTokens;
if (typeof input === "object" && input !== null) {
const tokens = input as Record<string, unknown>;
total.inputTokens += usageNumber(tokens.noCache ?? tokens.total);
total.cachedInputTokens += usageNumber(tokens.cacheRead);
total.cacheWriteTokens += usageNumber(tokens.cacheWrite);
} else {
total.inputTokens += usageNumber(input);
}
if (typeof output === "object" && output !== null) {
total.outputTokens += usageNumber((output as Record<string, unknown>).total);
} else {
total.outputTokens += usageNumber(output);
}
total.reasoningTokens += usageNumber(usage.reasoningTokens);
total.cachedInputTokens += usageNumber(usage.cachedInputTokens);
total.cacheWriteTokens += usageNumber(usage.cacheWriteTokens);
}
function replaceUsage(total: MutableAuthoringTokenUsage, value: unknown): void {
if (typeof value !== "object" || value === null) return;
total.inputTokens = 0;
total.outputTokens = 0;
total.reasoningTokens = 0;
total.cachedInputTokens = 0;
total.cacheWriteTokens = 0;
addUsage(total, value);
}
function usageNumber(value: unknown): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
function createHarness(harness: AuthoringBenchmarkModel["harness"], model: string): HarnessV1 {
if (harness === "Claude Code") {
return createClaudeCode({ auth: { gateway: {} }, model });
}
return createOpenCode({
auth: "ai-gateway",
model: openCodeModel(model),
port: HARNESS_BRIDGE_PORT,
});
}
function harnessId(harness: AuthoringBenchmarkModel["harness"]): string {
return harness === "Claude Code" ? "claude-code" : "opencode";
}
function openCodeModel(model: string): string {
const gatewayModel = model.includes("/") ? model : `anthropic/${model}`;
// OpenCode's Moonshot provider supplies the OpenAI-compatible transport; the
// canonical model ID still makes AI Gateway select the requested provider.
return `moonshotai/${gatewayModel}`;
}
function formatToolCall(name: string, input: unknown): string {
if (typeof input === "object" && input !== null) {
const command = (input as { command?: unknown }).command;
if (typeof command === "string") return command;
}
return `${name} ${JSON.stringify(input)}`;
}
async function bootstrapSubject(
sandbox: HarnessV1NetworkSandboxSession,
workspace: string,
workspaceKind: "scaffolded" | "empty",
archive: Uint8Array,
timings: BenchmarkTimings,
): Promise<void> {
await timings.measure("subject.source-upload", async () => {
await sandbox.writeBinaryFile({ path: SOURCE_ARCHIVE_PATH, content: archive });
});
await timings.measure("subject.source-install", () =>
run(
sandbox,
`rm -rf ${SOURCE_ROOT} && mkdir -p ${SOURCE_ROOT} && tar -xzf ${SOURCE_ARCHIVE_PATH} -C ${SOURCE_ROOT} && pnpm --dir ${SOURCE_ROOT} install --frozen-lockfile --offline`,
),
);
await timings.measure("subject.eve-build", () =>
run(sandbox, `pnpm --dir ${SOURCE_ROOT} --filter eve build`),
);
await timings.measure("subject.eve-pack-and-cli", () =>
run(
sandbox,
`mkdir -p /tmp/eve-package && pnpm --dir ${SOURCE_ROOT}/packages/eve pack --pack-destination /tmp/eve-package && mv $(find /tmp/eve-package -name '*.tgz' -print -quit) ${EVE_PACKAGE_PATH} && ln -sf ${SOURCE_ROOT}/packages/eve/bin/eve.js /usr/local/bin/eve && command -v eve`,
),
);
const workspaceCommands: string[] = [];
if (workspaceKind === "scaffolded") {
workspaceCommands.push(`cd ${shellQuote(workspace)} && AI_AGENT=benchmark eve init .`);
}
// Grader files are created after the agent finishes so an empty starting point remains empty.
workspaceCommands.push("command -v vitest >/dev/null");
await timings.measure("subject.workspace-bootstrap", () =>
run(sandbox, workspaceCommands.join(" && ")),
);
}
function setupContext(
sandbox: HarnessV1NetworkSandboxSession,
workspace: string,
): AuthoringSetupContext {
return {
sandbox,
workspace,
artifactsRoot: AUTHORING_EVAL_DIRECTORY,
run: async (command, workingDirectory = workspace) => {
await run(sandbox, command, workingDirectory);
},
write: async (path, content) => {
await sandbox.writeTextFile({
path: path.startsWith("/") ? path : `${workspace}/${path}`,
content,
});
},
};
}
// Created only after the agent's turns finish: an `empty` starting point has to
// look empty to `eve init .`, which refuses to scaffold into a directory that
// already holds entries it does not recognize.
async function ensureWorkspace(
context: AuthoringSetupContext,
workspaceKind: "scaffolded" | "empty",
archive: Uint8Array,
timings: BenchmarkTimings,
): Promise<void> {
if (workspaceKind === "empty") {
await verifyWorkspace(context, workspaceKind);
return;
}
if (await hasPreparedWorkspace(context)) return;
await timings.measure("session.workspace-recovery", () =>
bootstrapSubject(context.sandbox, context.workspace, workspaceKind, archive, timings),
);
await verifyWorkspace(context, workspaceKind);
}
async function hasPreparedWorkspace(context: AuthoringSetupContext): Promise<boolean> {
const result = await resultOf(
context.sandbox,
`test -f package.json && test -f agent/instructions.md && test -f AGENTS.md && test -f ${EVE_PACKAGE_PATH}`,
context.workspace,
);
return result.exitCode === 0;
}
async function verifyWorkspace(
context: AuthoringSetupContext,
workspaceKind: "scaffolded" | "empty",
): Promise<void> {
const command =
workspaceKind === "empty"
? "test ! -e package.json && test ! -e AGENTS.md && test ! -e agent && test ! -e .eve-grader"
: `for path in package.json agent/instructions.md AGENTS.md ${EVE_PACKAGE_PATH}; do test -f "$path" || { echo "Missing prepared workspace file: $path" >&2; exit 1; }; done`;
await context.run(command);
}
async function prepareGraderDirectory(context: AuthoringSetupContext): Promise<void> {
await context.run(
`mkdir -p ${AGENT_EVAL_DIRECTORY} && printf '{"private":true,"type":"module"}\\n' >${AGENT_EVAL_DIRECTORY}/package.json`,
);
}
async function installBaselineEveWrapper(context: AuthoringSetupContext): Promise<void> {
await context.run(`
cli_path="/usr/local/bin/eve"
test -x "$cli_path"
real_cli="$cli_path.guided"
if [ ! -e "$real_cli" ]; then mv "$cli_path" "$real_cli"; fi
cat >"$cli_path" <<'EOF'
#!/bin/sh
"$0.guided" "$@"
status=$?
rm -f AGENTS.md CLAUDE.md GEMINI.md
exit "$status"
EOF
chmod +x "$cli_path"
`);
}
function timingArtifact(timings: BenchmarkTimings): Record<string, string> {
return { "benchmark/timings.json": `${JSON.stringify(timings.entries, null, 2)}\n` };
}
function logTimingSummary(log: (message: string) => void, timings: BenchmarkTimings): void {
for (const timing of timings.entries) {
log(`[timing] ${timing.phase}: ${timing.durationMs}ms (${timing.outcome})`);
}
}
function harnessTranscript(transcript: ReadonlyArray<AuthoringTranscriptEntry>): string {
return transcript
.map((entry) => {
const message: {
role: AuthoringTranscriptEntry["role"];
content: unknown;
usage?: AuthoringTokenUsage;
} = {
role: entry.role,
content:
entry.role === "assistant"
? [
...(entry.content ? [{ type: "text", text: entry.content }] : []),
...(entry.toolCalls ?? []).map((call) => ({
type: "tool_use",
name: call.name,
input: call.input,
})),
]
: entry.content,
};
if (entry.usage !== undefined) message.usage = entry.usage;
return JSON.stringify({ type: entry.role, message });
})
.join("\n");
}
function shellCommands(toolCalls: ReadonlyArray<{ input: unknown }>): string[] {
return toolCalls.flatMap((call) => {
if (typeof call.input !== "object" || call.input === null) return [];
const command = (call.input as { command?: unknown }).command;
return typeof command === "string" ? [command] : [];
});
}
function bootstrapHash(authoringCase: AuthoringCase, subject: { readonly digest: string }): string {
const setupIds = [authoringCase.startingPoint.setup, authoringCase.setup]
.filter((setup): setup is AuthoringSetup => setup !== undefined)
.map((setup) => setup.id)
.join("-");
return `eve-authoring-${BOOTSTRAP_VERSION}-${subject.digest}-${authoringCase.startingPoint.id}-${setupIds}`;
}
async function resultOf(
sandbox: HarnessV1NetworkSandboxSession,
command: string,
workingDirectory?: string,
) {
const options: { command: string; workingDirectory?: string } = { command };
if (workingDirectory !== undefined) options.workingDirectory = workingDirectory;
return sandbox.run(options);
}
async function run(
sandbox: HarnessV1NetworkSandboxSession,
command: string,
workingDirectory?: string,
): Promise<void> {
const result = await resultOf(sandbox, command, workingDirectory);
if (result.exitCode !== 0) {
throw new Error(`${command} failed (${result.exitCode}):\n${result.stdout}\n${result.stderr}`);
}
}
function shellQuote(value: string): string {
return `'${value.replaceAll("'", `'\\''`)}'`;
}
@@ -0,0 +1,52 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { createJiti } from "jiti";
const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
const { createNativeAuthoringSetup } = await jiti.import(
new URL("./native-authoring-setup.ts", import.meta.url).pathname,
);
test("bootstraps the selected source before a native agent starts", async () => {
const commands = [];
const writes = [];
const setup = createNativeAuthoringSetup({
packageSpec: "https://pkg.eve.dev/commit/eve.tgz",
revision: "commit",
treatment: "baseline",
});
await setup({
async readFile(path) {
assert.equal(path, ".eve-authoring-bootstrap.json");
return JSON.stringify({
startingPoint: "scaffolded",
revision: "commit",
setupIds: ["inventory-openapi-v1"],
});
},
async writeFiles(files) {
writes.push(files);
},
async runCommand(command, args, options) {
commands.push({ command, args, options });
return { stdout: "", stderr: "", exitCode: 0 };
},
getWorkingDirectory: () => "/workspace",
setWorkingDirectory() {},
});
assert.match(commands[0].args[1], /rm -f \.eve-authoring-bootstrap\.json/u);
assert.deepEqual(Object.keys(writes[0]), ["/usr/local/bin/eve"]);
assert.match(writes[0]["/usr/local/bin/eve"], /pkg\.eve\.dev\/commit\/eve\.tgz/u);
assert.match(commands[1].args[1], /chmod \+x \/usr\/local\/bin\/eve/u);
assert.match(
commands[2].args[1],
/AI_AGENT=claude EVE_INIT_PACKAGE_SPEC=.* eve init \. --model openai\/gpt-5\.5/u,
);
assert.match(commands[3].args[1], /mkdir -p agent\/lib/u);
assert.match(commands[4].args[1], /rm -f AGENTS\.md CLAUDE\.md GEMINI\.md/u);
assert.match(commands[5].args[1], /git add \. && git commit --amend --no-edit --quiet/u);
assert.deepEqual(Object.keys(writes[1]), ["/workspace/agent/lib/inventory-openapi.ts"]);
assert.match(writes[1]["/workspace/agent/lib/inventory-openapi.ts"], /getStock/u);
});
@@ -0,0 +1,106 @@
import type { Sandbox } from "@vercel/agent-eval";
import type { AuthoringSetup, AuthoringSetupContext } from "./authoring-case.js";
import { inventoryOpenApiSetup } from "./setups/inventory-openapi.js";
interface FixtureBootstrap {
readonly startingPoint: "scaffolded" | "empty";
readonly projectDirectory?: string;
readonly revision: string;
readonly setupIds: readonly string[];
}
export function createNativeAuthoringSetup(options: {
readonly packageSpec: string;
readonly revision: string;
readonly treatment: "baseline" | "guided";
}) {
return async (sandbox: Sandbox): Promise<void> => {
const bootstrap = JSON.parse(
await sandbox.readFile(".eve-authoring-bootstrap.json"),
) as FixtureBootstrap;
if (bootstrap.revision !== options.revision) {
throw new Error("Native fixture revision does not match the selected subject.");
}
await run(
sandbox,
"rm -f .eve-authoring-bootstrap.json CASE.ts package.json package-lock.json PROMPT.md",
"fixture cleanup",
);
await sandbox.writeFiles({
"/usr/local/bin/eve": `#!/bin/sh\nexec npx --yes --allow-remote=all --package=${shellQuote(options.packageSpec)} eve "$@"\n`,
});
await run(sandbox, "chmod +x /usr/local/bin/eve", "eve canary wrapper");
if (bootstrap.startingPoint === "scaffolded") {
await run(
sandbox,
`AI_AGENT=claude EVE_INIT_PACKAGE_SPEC=${shellQuote(options.packageSpec)} eve init . --model openai/gpt-5.5`,
"workspace bootstrap",
);
}
if (bootstrap.startingPoint === "empty") {
if (bootstrap.projectDirectory === undefined) {
throw new Error("An empty native fixture must declare its project directory.");
}
await sandbox.writeFiles({
"package.json": `${JSON.stringify({
private: true,
scripts: {
typecheck: `npm --prefix ${bootstrap.projectDirectory} run typecheck`,
build: `npm --prefix ${bootstrap.projectDirectory} run build`,
},
})}\n`,
});
}
const context = setupContext(sandbox, bootstrap.projectDirectory);
for (const setup of setupsFor(bootstrap.setupIds)) await setup.onSession?.(context);
if (options.treatment === "baseline") await run(sandbox, "rm -f AGENTS.md CLAUDE.md GEMINI.md");
await run(sandbox, "git add . && git commit --amend --no-edit --quiet");
};
}
function setupsFor(ids: readonly string[]): readonly AuthoringSetup[] {
return ids.flatMap((id) => (id === inventoryOpenApiSetup.id ? [inventoryOpenApiSetup] : []));
}
function setupContext(sandbox: Sandbox, projectDirectory?: string): AuthoringSetupContext {
const workspace =
projectDirectory === undefined
? sandbox.getWorkingDirectory()
: `${sandbox.getWorkingDirectory()}/${projectDirectory}`;
return {
workspace,
artifactsRoot: "/tmp/photon",
run: async (command, workingDirectory = workspace) =>
run(sandbox, command, command, undefined, workingDirectory),
write: async (path, content) =>
sandbox.writeFiles({ [path.startsWith("/") ? path : `${workspace}/${path}`]: content }),
};
}
async function run(
sandbox: Sandbox,
command: string,
label = command,
env?: Record<string, string>,
cwd?: string,
): Promise<void> {
const result = await sandbox.runCommand(
"bash",
["-lc", `timeout 240 bash -lc ${shellQuote(command)}`],
{
env,
cwd,
},
);
if (result.exitCode !== 0) {
throw new Error(`${label} failed (${result.exitCode}):\n${result.stdout}\n${result.stderr}`);
}
}
function shellQuote(value: string): string {
return `'${value.replaceAll("'", `'\\''`)}'`;
}
+35 -95
View File
@@ -1,108 +1,48 @@
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
export function workingTreeSubject(repositoryRoot) { const PACKAGE_HOST = "https://pkg.eve.dev";
return archiveSubject(repositoryRoot, "working tree", "current", (archivePath, environment) => { const IMMUTABLE_PACKAGE_PATH = /^\/([0-9a-f]{40})\/eve\.tgz$/u;
git(repositoryRoot, ["read-tree", "HEAD"], environment);
git(repositoryRoot, ["add", "-A"], environment);
const tree = git(repositoryRoot, ["write-tree"], environment).trim();
git(repositoryRoot, ["archive", "--format=tar.gz", `--output=${archivePath}`, tree]);
return tree;
});
}
export function revisionSubject(repositoryRoot, requestedRevision, label) { /** Resolves a mutable canary ref to the immutable artifact all runs must share. */
const revision = git(repositoryRoot, [ export function canarySubject(ref, label, resolve = resolveCanaryPackageSpec) {
"rev-parse", const packageSpec = resolve(ref);
"--verify", const revision = packageRevision(packageSpec);
`${requestedRevision}^{commit}`, return {
]).trim();
return archiveSubject(
repositoryRoot,
revision.slice(0, 12),
label, label,
(archivePath) => { revision,
const tree = git(repositoryRoot, ["rev-parse", `${revision}^{tree}`]).trim(); description: revision.slice(0, 12),
git(repositoryRoot, ["archive", "--format=tar.gz", `--output=${archivePath}`, revision]); packageSpec,
return tree; };
},
{ revision },
);
} }
function archiveSubject(repositoryRoot, description, label, createArchive, details = {}) { export function resolveCanaryPackageSpec(ref) {
const temporaryDirectory = mkdtempSync(join(tmpdir(), "eve-authoring-")); const requested = `${PACKAGE_HOST}/${encodeURIComponent(ref)}/eve.tgz`;
const archivePath = join(temporaryDirectory, "source.tar.gz"); let resolved;
const indexPath = join(temporaryDirectory, "index");
try { try {
const digest = createArchive(archivePath, { ...process.env, GIT_INDEX_FILE: indexPath }); resolved = execFileSync(
const archive = readFileSync(archivePath); "curl",
return { ["-fsSL", "-o", "/dev/null", "-w", "%{url_effective}", requested],
label, {
description, encoding: "utf8",
archive, },
digest, ).trim();
dependencyArchive: dependencyArchive(archive), } catch {
dependencyDigest: dependencyDigest(archive), throw new Error(
...details, `No eve canary artifact is available for ${JSON.stringify(ref)}. Publish that revision or use a published canary ref such as "main".`,
}; );
} finally {
rmSync(temporaryDirectory, { recursive: true, force: true });
} }
packageRevision(resolved);
return resolved;
} }
function dependencyDigest(archive) { export function packageRevision(packageSpec) {
const hash = createHash("sha256"); const url = new URL(packageSpec);
for (const path of dependencyPaths(archive)) { if (url.origin !== PACKAGE_HOST) {
const content = execFileSync("tar", ["-xOzf", "-", path], { throw new Error(`Eve canary resolved outside ${PACKAGE_HOST}: ${packageSpec}`);
input: archive,
maxBuffer: 10 * 1024 * 1024,
});
hash.update(path).update("\0").update(content).update("\0");
} }
return hash.digest("hex"); const revision = url.pathname.match(IMMUTABLE_PACKAGE_PATH)?.[1];
} if (revision === undefined) {
throw new Error(`Eve canary did not resolve to an immutable revision: ${packageSpec}`);
function dependencyArchive(archive) {
const directory = mkdtempSync(join(tmpdir(), "eve-authoring-dependencies-"));
const archivePath = join(directory, "dependencies.tar.gz");
try {
for (const path of dependencyPaths(archive)) {
const destination = join(directory, "input", path);
mkdirSync(join(destination, ".."), { recursive: true });
writeFileSync(
destination,
execFileSync("tar", ["-xOzf", "-", path], {
input: archive,
maxBuffer: 10 * 1024 * 1024,
}),
);
}
execFileSync("tar", ["-czf", archivePath, "-C", join(directory, "input"), "."]);
return readFileSync(archivePath);
} finally {
rmSync(directory, { recursive: true, force: true });
} }
} return revision;
function dependencyPaths(archive) {
const paths = execFileSync("tar", ["-tzf", "-"], { input: archive, encoding: "utf8" })
.split("\n")
.filter(Boolean);
return paths.filter(
(path) =>
path === ".npmrc" ||
path === "package.json" ||
path === "pnpm-lock.yaml" ||
path === "pnpm-workspace.yaml" ||
/(?:^|\/)package\.json$/u.test(path) ||
path.startsWith("patches/"),
);
}
function git(cwd, args, env = process.env) {
return execFileSync("git", args, { cwd, env, encoding: "utf8" });
} }
+28 -87
View File
@@ -1,96 +1,37 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test"; import { test } from "node:test";
import { revisionSubject, workingTreeSubject } from "./source.mjs"; import { canarySubject, packageRevision } from "./source.mjs";
test("archives the working tree without changing the index", () => { const revision = "1234567890abcdef1234567890abcdef12345678";
const repository = repositoryFixture(); const packageSpec = `https://pkg.eve.dev/${revision}/eve.tgz`;
try {
writeFileSync(join(repository, "tracked.txt"), "changed\n");
writeFileSync(join(repository, "new.txt"), "new\n");
writeFileSync(join(repository, "ignored.txt"), "ignored\n");
rmSync(join(repository, "deleted.txt"));
const statusBefore = git(repository, ["status", "--porcelain"]);
const subject = workingTreeSubject(repository); test("resolves a canary alias once to an immutable subject", () => {
withExtracted(subject.archive, (extracted) => { const calls = [];
assert.equal(readFileSync(join(extracted, "tracked.txt"), "utf8"), "changed\n"); const subject = canarySubject("main", "current", (ref) => {
assert.equal(readFileSync(join(extracted, "new.txt"), "utf8"), "new\n"); calls.push(ref);
assert.equal(existsSync(join(extracted, "ignored.txt")), false); return packageSpec;
assert.equal(existsSync(join(extracted, "deleted.txt")), false); });
});
withExtracted(subject.dependencyArchive, (extracted) => { assert.deepEqual(calls, ["main"]);
assert.equal(readFileSync(join(extracted, "package.json"), "utf8"), '{"private":true}\n'); assert.deepEqual(subject, {
assert.equal( label: "current",
readFileSync(join(extracted, "pnpm-lock.yaml"), "utf8"), revision,
"lockfileVersion: '9.0'\n", description: revision.slice(0, 12),
); packageSpec,
assert.equal(existsSync(join(extracted, "tracked.txt")), false); });
});
assert.equal(git(repository, ["status", "--porcelain"]), statusBefore);
writeFileSync(join(repository, "tracked.txt"), "another source change\n");
const sourceChange = workingTreeSubject(repository);
assert.notEqual(sourceChange.digest, subject.digest);
assert.equal(sourceChange.dependencyDigest, subject.dependencyDigest);
assert.deepEqual(sourceChange.dependencyArchive, subject.dependencyArchive);
writeFileSync(join(repository, "packages.json"), "not a manifest\n");
assert.equal(workingTreeSubject(repository).dependencyDigest, subject.dependencyDigest);
writeFileSync(join(repository, "pnpm-lock.yaml"), "lockfileVersion: '9.1'\n");
assert.notEqual(workingTreeSubject(repository).dependencyDigest, subject.dependencyDigest);
} finally {
rmSync(repository, { recursive: true, force: true });
}
}); });
test("archives a local revision", () => { test("rejects a non-immutable canary URL", () => {
const repository = repositoryFixture(); assert.throws(
try { () => packageRevision("https://pkg.eve.dev/main/eve.tgz"),
writeFileSync(join(repository, "tracked.txt"), "working tree\n"); /did not resolve to an immutable revision/u,
const subject = revisionSubject(repository, "HEAD", "base"); );
assert.equal(subject.label, "base");
withExtracted(subject.archive, (extracted) => {
assert.equal(readFileSync(join(extracted, "tracked.txt"), "utf8"), "committed\n");
});
} finally {
rmSync(repository, { recursive: true, force: true });
}
}); });
function repositoryFixture() { test("rejects a canary URL from another origin", () => {
const repository = mkdtempSync(join(tmpdir(), "eve-authoring-source-test-")); assert.throws(
git(repository, ["init", "--quiet"]); () => packageRevision(`https://example.com/${revision}/eve.tgz`),
git(repository, ["config", "user.name", "Test"]); /resolved outside/u,
git(repository, ["config", "user.email", "test@example.com"]); );
writeFileSync(join(repository, ".gitignore"), "ignored.txt\n"); });
writeFileSync(join(repository, ".npmrc"), "link-workspace-packages=true\n");
writeFileSync(join(repository, "package.json"), '{"private":true}\n');
writeFileSync(join(repository, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
writeFileSync(join(repository, "pnpm-workspace.yaml"), "packages: []\n");
writeFileSync(join(repository, "tracked.txt"), "committed\n");
writeFileSync(join(repository, "deleted.txt"), "delete me\n");
git(repository, ["add", "."]);
git(repository, ["commit", "--quiet", "-m", "fixture"]);
return repository;
}
function withExtracted(archive, assertion) {
const directory = mkdtempSync(join(tmpdir(), "eve-authoring-archive-test-"));
const archivePath = join(directory, "source.tar.gz");
const extracted = join(directory, "extracted");
try {
writeFileSync(archivePath, archive);
execFileSync("mkdir", [extracted]);
execFileSync("tar", ["-xzf", archivePath, "-C", extracted]);
assertion(extracted);
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
function git(cwd, args) {
return execFileSync("git", args, { cwd, encoding: "utf8" });
}
-20
View File
@@ -1,20 +0,0 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { BenchmarkTimings } from "./timing.ts";
test("records successful and failed phases", async () => {
const timings = new BenchmarkTimings();
await timings.measure("success", async () => undefined, { turns: 1 });
await assert.rejects(timings.measure("failure", async () => Promise.reject(new Error("nope"))));
assert.equal(timings.entries.length, 2);
assert.deepEqual(
timings.entries.map(({ phase, outcome, details }) => ({ phase, outcome, details })),
[
{ phase: "success", outcome: "success", details: { turns: 1 } },
{ phase: "failure", outcome: "failure", details: undefined },
],
);
assert.ok(timings.entries.every((entry) => entry.durationMs >= 0));
});
-55
View File
@@ -1,55 +0,0 @@
export interface BenchmarkTiming {
readonly phase: string;
readonly startedAt: string;
readonly durationMs: number;
readonly outcome: "success" | "failure";
readonly details?: Readonly<Record<string, string | number | boolean>>;
}
type TimingDetails = BenchmarkTiming["details"];
export class BenchmarkTimings {
readonly entries: BenchmarkTiming[] = [];
async measure<T>(
phase: string,
operation: () => Promise<T>,
details?: TimingDetails,
): Promise<T> {
const startedAt = new Date().toISOString();
const started = performance.now();
try {
const result = await operation();
this.add(phase, startedAt, performance.now() - started, "success", details);
return result;
} catch (error) {
this.add(phase, startedAt, performance.now() - started, "failure", details);
throw error;
}
}
record(
phase: string,
durationMs: number,
outcome: BenchmarkTiming["outcome"] = "success",
details?: TimingDetails,
): void {
this.add(phase, new Date(Date.now() - durationMs).toISOString(), durationMs, outcome, details);
}
private add(
phase: string,
startedAt: string,
durationMs: number,
outcome: BenchmarkTiming["outcome"],
details: TimingDetails,
): void {
const entry = {
phase,
startedAt,
durationMs: Math.round(durationMs),
outcome,
};
this.entries.push(details === undefined ? entry : { ...entry, details });
}
}
+1 -6
View File
@@ -12,12 +12,7 @@
"benchmark:timings": "node scripts/timings.mjs" "benchmark:timings": "node scripts/timings.mjs"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/harness": "1.0.87", "@vercel/agent-eval": "2.2.1",
"@ai-sdk/harness-claude-code": "1.0.90",
"@ai-sdk/harness-opencode": "1.0.88",
"@ai-sdk/sandbox-vercel": "1.0.87",
"@vercel/agent-eval": "1.5.0",
"@vercel/sandbox": "3.2.0",
"jiti": "2.7.0", "jiti": "2.7.0",
"zod": "catalog:" "zod": "catalog:"
}, },
+8 -20
View File
@@ -12,13 +12,8 @@ import {
publishedBenchmarkModels, publishedBenchmarkModels,
publishedExperimentId, publishedExperimentId,
} from "./lib/benchmark-config.ts"; } from "./lib/benchmark-config.ts";
import { import { prepareFixtures, resetExperiments, writeExperiment } from "./lib/experiment-files.mjs";
prepareFixtures, import { canarySubject } from "./lib/source.mjs";
resetExperiments,
writeExperiment,
writeSubjectArchives,
} from "./lib/experiment-files.mjs";
import { revisionSubject } from "./lib/source.mjs";
const appRoot = dirname(fileURLToPath(import.meta.url)); const appRoot = dirname(fileURLToPath(import.meta.url));
const repositoryRoot = resolve(appRoot, "../.."); const repositoryRoot = resolve(appRoot, "../..");
@@ -51,14 +46,15 @@ if (values["allow-dirty"]) {
); );
} }
const revision = git(["rev-parse", "--verify", `${values.revision}^{commit}`]).trim(); const requestedRevision = git(["rev-parse", "--verify", `${values.revision}^{commit}`]).trim();
const subject = revisionSubject(repositoryRoot, revision, "published"); const subject = canarySubject(requestedRevision, "published");
const revision = subject.revision;
const benchmarks = selectedBenchmarks(values.models); const benchmarks = selectedBenchmarks(values.models);
const experimentNames = benchmarks.flatMap((benchmark) => const experimentNames = benchmarks.flatMap((benchmark) =>
authoringTreatments.map((treatment) => publishedExperimentId(benchmark, treatment)), authoringTreatments.map((treatment) => publishedExperimentId(benchmark, treatment)),
); );
prepareFixtures(evalsRoot); await prepareFixtures(evalsRoot, subject, publishedBenchmark.caseIds);
writeExperiments(subject, revision, benchmarks); writeExperiments(subject, revision, benchmarks);
console.log(`> eve revision: ${revision}`); console.log(`> eve revision: ${revision}`);
@@ -158,19 +154,11 @@ To inspect pending benchmark cells without publishing:
function writeExperiments(subject, revision, benchmarks) { function writeExperiments(subject, revision, benchmarks) {
resetExperiments(experimentsRoot); resetExperiments(experimentsRoot);
const { archiveName, dependencyArchiveName } = writeSubjectArchives(
experimentsRoot,
subject,
`published-${revision.slice(0, 12)}`,
);
for (const benchmark of benchmarks) { for (const benchmark of benchmarks) {
for (const treatment of authoringTreatments) { for (const treatment of authoringTreatments) {
writeExperiment(experimentsRoot, publishedExperimentId(benchmark, treatment), { writeExperiment(experimentsRoot, publishedExperimentId(benchmark, treatment), {
archiveName, revision: subject.revision,
dependencyArchiveName, packageSpec: subject.packageSpec,
digest: subject.digest,
dependencyDigest: subject.dependencyDigest,
runs: publishedBenchmark.runs, runs: publishedBenchmark.runs,
evals: publishedBenchmark.caseIds, evals: publishedBenchmark.caseIds,
benchmark, benchmark,
+20 -38
View File
@@ -6,14 +6,13 @@ import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util"; import { parseArgs } from "node:util";
import { findBenchmarkModel, parseAuthoringTreatment } from "./lib/benchmark-config.ts";
import { import {
prepareFixtures, findBenchmarkModel,
resetExperiments, parseAuthoringTreatment,
writeExperiment, publishedBenchmark,
writeSubjectArchives, } from "./lib/benchmark-config.ts";
} from "./lib/experiment-files.mjs"; import { prepareFixtures, resetExperiments, writeExperiment } from "./lib/experiment-files.mjs";
import { revisionSubject, workingTreeSubject } from "./lib/source.mjs"; import { canarySubject } from "./lib/source.mjs";
const appRoot = dirname(fileURLToPath(import.meta.url)); const appRoot = dirname(fileURLToPath(import.meta.url));
const repositoryRoot = resolve(appRoot, "../.."); const repositoryRoot = resolve(appRoot, "../..");
@@ -23,8 +22,7 @@ const { values, positionals } = parseArgs({
args: process.argv.slice(2), args: process.argv.slice(2),
allowPositionals: true, allowPositionals: true,
options: { options: {
base: { type: "string" }, canary: { type: "string", default: "main" },
head: { type: "string" },
dry: { type: "boolean" }, dry: { type: "boolean" },
runs: { type: "string" }, runs: { type: "string" },
model: { type: "string", default: "claude-sonnet-5" }, model: { type: "string", default: "claude-sonnet-5" },
@@ -44,32 +42,23 @@ if (values.help) {
process.exit(0); process.exit(0);
} }
if (positionals.length > 1) throw new Error("Expected at most one <eval-name>."); if (positionals.length > 1) throw new Error("Expected at most one <eval-name>.");
if (values.head !== undefined && values.base === undefined) {
throw new Error("--head requires --base.");
}
const runs = parseRuns(values.runs); const runs = parseRuns(values.runs);
const selectedEval = positionals[0]; const selectedEval = positionals[0];
const treatment = parseAuthoringTreatment(values.treatment); const treatment = parseAuthoringTreatment(values.treatment);
const benchmark = findBenchmarkModel(values.model); const benchmark = findBenchmarkModel(values.model);
if (values.verbose && (selectedEval === undefined || runs !== 1 || values.base !== undefined)) { if (values.verbose && (selectedEval === undefined || runs !== 1)) {
throw new Error("--verbose requires one eval, one run, and no revision comparison."); throw new Error("--verbose requires one eval and one run.");
} }
if (selectedEval !== undefined && !existsSync(join(evalsRoot, selectedEval, "CASE.ts"))) { if (selectedEval !== undefined && !existsSync(join(evalsRoot, selectedEval, "CASE.ts"))) {
throw new Error(`Unknown eval ${JSON.stringify(selectedEval)}.`); throw new Error(`Unknown eval ${JSON.stringify(selectedEval)}.`);
} }
const workingTree = () => workingTreeSubject(repositoryRoot); const subjects = [canarySubject(values.canary, "current")];
const subjects = await prepareFixtures(
values.base === undefined evalsRoot,
? [workingTree()] subjects[0],
: [ selectedEval === undefined ? publishedBenchmark.caseIds : [selectedEval],
revisionSubject(repositoryRoot, values.base, "base"), );
values.head === undefined
? { ...workingTree(), label: "head" }
: revisionSubject(repositoryRoot, values.head, "head"),
];
prepareFixtures(evalsRoot, selectedEval === undefined ? undefined : [selectedEval]);
mkdirSync(join(appRoot, "results"), { recursive: true }); mkdirSync(join(appRoot, "results"), { recursive: true });
writeExperiments(subjects, runs, benchmark, treatment, values.verbose ?? false); writeExperiments(subjects, runs, benchmark, treatment, values.verbose ?? false);
const executable = join(appRoot, "node_modules/.bin/agent-eval"); const executable = join(appRoot, "node_modules/.bin/agent-eval");
@@ -82,7 +71,8 @@ for (const subject of subjects) console.log(`> ${subject.label}: ${subject.descr
const result = spawnSync(executable, args, { const result = spawnSync(executable, args, {
cwd: appRoot, cwd: appRoot,
stdio: "inherit", stdio: "inherit",
env: { ...process.env, EVE_BENCHMARK_EVAL: selectedEval ?? "*" }, env:
selectedEval === undefined ? process.env : { ...process.env, EVE_BENCHMARK_EVAL: selectedEval },
}); });
if (result.error) throw result.error; if (result.error) throw result.error;
process.exit(result.status ?? 1); process.exit(result.status ?? 1);
@@ -90,16 +80,9 @@ process.exit(result.status ?? 1);
function writeExperiments(subjects, runs, benchmark, treatment, verbose) { function writeExperiments(subjects, runs, benchmark, treatment, verbose) {
resetExperiments(experimentsRoot); resetExperiments(experimentsRoot);
for (const subject of subjects) { for (const subject of subjects) {
const { archiveName, dependencyArchiveName } = writeSubjectArchives(
experimentsRoot,
subject,
subject.label,
);
writeExperiment(experimentsRoot, subject.label, { writeExperiment(experimentsRoot, subject.label, {
archiveName, revision: subject.revision,
dependencyArchiveName, packageSpec: subject.packageSpec,
digest: subject.digest,
dependencyDigest: subject.dependencyDigest,
runs, runs,
benchmark, benchmark,
treatment, treatment,
@@ -118,6 +101,5 @@ function parseRuns(value) {
function usage() { function usage() {
console.log(`Usage: console.log(`Usage:
pnpm benchmark [eval-name] [--model <id>] [--runs N] [--treatment baseline|guided] [--dry] [--verbose] [--keep-failures] pnpm benchmark [eval-name] [--canary main] [--model <id>] [--runs N] [--treatment baseline|guided] [--dry] [--verbose] [--keep-failures]`);
pnpm benchmark [eval-name] --base <revision> [--head <revision>] [--model <id>] [--runs N] [--treatment baseline|guided] [--dry]`);
} }
+71 -11
View File
@@ -86,20 +86,19 @@ export const modelPricing = {
const number = (value) => (typeof value === "number" && Number.isFinite(value) ? value : 0); const number = (value) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
const object = (value) => (value !== null && typeof value === "object" ? value : {}); const object = (value) => (value !== null && typeof value === "object" ? value : {});
export function extractRunUsage(raw) { export function extractRunUsage(raw, harness) {
const usage = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 }; const usage = { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 };
let found = false; let found = false;
for (const line of raw.split("\n")) { for (const line of raw.split("\n")) {
try { try {
const event = JSON.parse(line); const event = JSON.parse(line);
if (event.type !== "assistant") continue; const value = usageForEvent(event, harness);
const value = object(object(event.message).usage); if (value === undefined) continue;
if (Object.keys(value).length === 0) continue; usage.input += value.input;
usage.input += number(value.inputTokens); usage.output += value.output;
usage.output += number(value.outputTokens); usage.reasoning += value.reasoning;
usage.reasoning += number(value.reasoningTokens); usage.cacheRead += value.cacheRead;
usage.cacheRead += number(value.cachedInputTokens); usage.cacheWrite += value.cacheWrite;
usage.cacheWrite += number(value.cacheWriteTokens);
found = true; found = true;
} catch { } catch {
// Ignore malformed transcript lines. // Ignore malformed transcript lines.
@@ -108,6 +107,59 @@ export function extractRunUsage(raw) {
return found && tokenConsumption(usage) > 0 ? usage : null; return found && tokenConsumption(usage) > 0 ? usage : null;
} }
function usageForEvent(event, harness) {
const legacy = object(object(event.message).usage);
if (event.type === "assistant" && "inputTokens" in legacy) {
return {
input: number(legacy.inputTokens),
output: number(legacy.outputTokens),
reasoning: number(legacy.reasoningTokens),
cacheRead: number(legacy.cachedInputTokens),
cacheWrite: number(legacy.cacheWriteTokens),
};
}
if (harness === "OpenCode" && event.type === "step_finish") {
const tokens = object(object(event.part).tokens);
const cache = object(tokens.cache);
return {
input: number(tokens.input),
output: number(tokens.output),
reasoning: number(tokens.reasoning),
cacheRead: number(cache.read),
cacheWrite: number(cache.write),
};
}
if (harness === "Claude Code" && event.type === "assistant") {
const usage = object(object(event.message).usage);
if (Object.keys(usage).length === 0) return undefined;
return {
input: number(usage.input_tokens),
output: number(usage.output_tokens),
reasoning: number(usage.reasoning_tokens),
cacheRead: number(usage.cache_read_input_tokens),
cacheWrite: number(usage.cache_creation_input_tokens),
};
}
if (harness === "Codex" && event.type === "turn.completed") {
const usage = object(event.usage);
const cacheRead = number(usage.cached_input_tokens);
return {
// Codex reports total input tokens. Cache reads are a subset, unlike the
// other runners which report non-cached input separately.
input: Math.max(0, number(usage.input_tokens) - cacheRead),
output: number(usage.output_tokens),
reasoning: number(usage.reasoning_output_tokens ?? usage.reasoning_tokens),
cacheRead,
cacheWrite: number(usage.cache_write_input_tokens ?? usage.cache_creation_input_tokens),
};
}
return undefined;
}
export function tokenConsumption(usage) { export function tokenConsumption(usage) {
return usage.input + usage.output + usage.reasoning; return usage.input + usage.output + usage.reasoning;
} }
@@ -117,10 +169,18 @@ export function countToolInvocations(raw) {
for (const line of raw.split("\n")) { for (const line of raw.split("\n")) {
try { try {
const event = JSON.parse(line); const event = JSON.parse(line);
if (event.type === "tool_use") {
count++;
continue;
}
if (event.type === "item.started" && object(event.item).type === "command_execution") {
count++;
continue;
}
if (event.type !== "assistant") continue; if (event.type !== "assistant") continue;
const content = object(event.message).content; const content = object(event.message).content;
if (!Array.isArray(content)) continue; if (Array.isArray(content))
count += content.filter((part) => object(part).type === "tool_use").length; count += content.filter((part) => object(part).type === "tool_use").length;
} catch { } catch {
// Ignore malformed transcript lines. // Ignore malformed transcript lines.
} }
+60
View File
@@ -32,6 +32,57 @@ test("extracts usage from authoring harness transcripts", () => {
}); });
}); });
test("extracts OpenCode step usage", () => {
const usage = extractRunUsage(
JSON.stringify({
type: "step_finish",
part: {
tokens: { input: 100, output: 20, reasoning: 5, cache: { read: 50, write: 10 } },
},
}),
"OpenCode",
);
assert.deepEqual(usage, { input: 100, output: 20, reasoning: 5, cacheRead: 50, cacheWrite: 10 });
});
test("extracts Claude Code message usage", () => {
const usage = extractRunUsage(
JSON.stringify({
type: "assistant",
message: {
usage: {
input_tokens: 100,
output_tokens: 20,
cache_read_input_tokens: 50,
cache_creation_input_tokens: 10,
},
},
}),
"Claude Code",
);
assert.deepEqual(usage, { input: 100, output: 20, reasoning: 0, cacheRead: 50, cacheWrite: 10 });
});
test("extracts Codex completion usage without double-counting cached input", () => {
const usage = extractRunUsage(
JSON.stringify({
type: "turn.completed",
usage: {
input_tokens: 150,
cached_input_tokens: 50,
cache_write_input_tokens: 10,
output_tokens: 20,
reasoning_output_tokens: 5,
},
}),
"Codex",
);
assert.deepEqual(usage, { input: 100, output: 20, reasoning: 5, cacheRead: 50, cacheWrite: 10 });
});
test("calculates token consumption without double-counting cache details", () => { test("calculates token consumption without double-counting cache details", () => {
assert.equal( assert.equal(
tokenConsumption({ tokenConsumption({
@@ -45,6 +96,15 @@ test("calculates token consumption without double-counting cache details", () =>
); );
}); });
test("counts native OpenCode and Codex tool invocations", () => {
const raw = [
JSON.stringify({ type: "tool_use", part: { tool: "bash" } }),
JSON.stringify({ type: "item.started", item: { type: "command_execution" } }),
].join("\n");
assert.equal(countToolInvocations(raw), 2);
});
test("counts tool invocations in authoring harness transcripts", () => { test("counts tool invocations in authoring harness transcripts", () => {
const raw = [ const raw = [
JSON.stringify({ type: "user", message: { role: "user", content: "Hi" } }), JSON.stringify({ type: "user", message: { role: "user", content: "Hi" } }),
+5 -5
View File
@@ -81,7 +81,7 @@ for (const benchmark of benchmarks) {
caseId, caseId,
status: status ?? "current", status: status ?? "current",
...result, ...result,
...meanRunMetrics(summaryPath, benchmark.model), ...meanRunMetrics(summaryPath, benchmark.model, benchmark.harness),
}); });
} }
} }
@@ -227,8 +227,8 @@ function latestValidResult(experimentId, caseId) {
return undefined; return undefined;
} }
function meanRunMetrics(summaryPath, model) { function meanRunMetrics(summaryPath, model, harness) {
const performanceRuns = runMetrics(summaryPath); const performanceRuns = runMetrics(summaryPath, harness);
const performanceUsage = performanceRuns.flatMap((run) => const performanceUsage = performanceRuns.flatMap((run) =>
run.usage === null ? [] : [run.usage], run.usage === null ? [] : [run.usage],
); );
@@ -248,14 +248,14 @@ function meanRunMetrics(summaryPath, model) {
return result; return result;
} }
function runMetrics(summaryPath) { function runMetrics(summaryPath, harness) {
return readdirSync(dirname(summaryPath), { withFileTypes: true }) return readdirSync(dirname(summaryPath), { withFileTypes: true })
.filter((entry) => entry.isDirectory() && /^run-\d+$/u.test(entry.name)) .filter((entry) => entry.isDirectory() && /^run-\d+$/u.test(entry.name))
.flatMap((entry) => { .flatMap((entry) => {
const transcriptPath = join(dirname(summaryPath), entry.name, "transcript-raw.jsonl"); const transcriptPath = join(dirname(summaryPath), entry.name, "transcript-raw.jsonl");
if (!existsSync(transcriptPath)) return []; if (!existsSync(transcriptPath)) return [];
const raw = readFileSync(transcriptPath, "utf8"); const raw = readFileSync(transcriptPath, "utf8");
return [{ usage: extractRunUsage(raw), toolInvocations: countToolInvocations(raw) }]; return [{ usage: extractRunUsage(raw, harness), toolInvocations: countToolInvocations(raw) }];
}); });
} }
+5 -111
View File
@@ -138,24 +138,9 @@ importers:
apps/benchmarks: apps/benchmarks:
dependencies: dependencies:
'@ai-sdk/harness':
specifier: 1.0.87
version: 1.0.87(ws@8.21.0(bufferutil@4.1.0))(zod@4.5.4)
'@ai-sdk/harness-claude-code':
specifier: 1.0.90
version: 1.0.90(bufferutil@4.1.0)(zod@4.5.4)
'@ai-sdk/harness-opencode':
specifier: 1.0.88
version: 1.0.88(bufferutil@4.1.0)(zod@4.5.4)
'@ai-sdk/sandbox-vercel':
specifier: 1.0.87
version: 1.0.87(ws@8.21.0(bufferutil@4.1.0))(zod@4.5.4)
'@vercel/agent-eval': '@vercel/agent-eval':
specifier: 1.5.0 specifier: 2.2.1
version: 1.5.0(supports-color@10.2.2) version: 2.2.1(supports-color@10.2.2)
'@vercel/sandbox':
specifier: 3.2.0
version: 3.2.0
jiti: jiti:
specifier: 2.7.0 specifier: 2.7.0
version: 2.7.0 version: 2.7.0
@@ -1670,28 +1655,6 @@ packages:
peerDependencies: peerDependencies:
zod: ^3.25.76 || ^4.1.8 zod: ^3.25.76 || ^4.1.8
'@ai-sdk/harness-claude-code@1.0.90':
resolution: {integrity: sha512-SL77y5l3596n6cxuLN4OB19iTkvwg6zJq2KxIV9MAPwAMYEEXHz7YGRXxrAd7GOixIVqcsXEw1HOLN1zcG1L3A==}
engines: {node: '>=22'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/harness-opencode@1.0.88':
resolution: {integrity: sha512-bA22/rBaTbsO/o/x91P78RvLpvgwnCoW6Ie7ATsC7Z6BxGYZpFrS6BGWD1DQL2pN3jy61KWnDLDwy0egg7ypZg==}
engines: {node: '>=22'}
peerDependencies:
zod: ^3.25.76 || ^4.1.8
'@ai-sdk/harness@1.0.87':
resolution: {integrity: sha512-OY+M1B58lLv+fRHB0dh/FJ6lCzmg9HQmTe1z+XS1WTPF8PnTMEr2E/6vNTUoORN7iZMkTyVkszA4DOhod1eFQw==}
engines: {node: '>=22'}
peerDependencies:
ws: ^8.21.0
zod: ^3.25.76 || ^4.1.8
peerDependenciesMeta:
ws:
optional: true
'@ai-sdk/mcp@2.0.29': '@ai-sdk/mcp@2.0.29':
resolution: {integrity: sha512-cK8mYpjKGp7/gmsTYtq/dkdmNlBeWQrwcfsah/W9hN6Vr1yNzunLnAZAqL5tMzumLDBueSz6y+vrb5z8DJ8TMg==} resolution: {integrity: sha512-cK8mYpjKGp7/gmsTYtq/dkdmNlBeWQrwcfsah/W9hN6Vr1yNzunLnAZAqL5tMzumLDBueSz6y+vrb5z8DJ8TMg==}
engines: {node: '>=22'} engines: {node: '>=22'}
@@ -1794,10 +1757,6 @@ packages:
peerDependencies: peerDependencies:
react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1
'@ai-sdk/sandbox-vercel@1.0.87':
resolution: {integrity: sha512-rW8mtwvH5lcoXWPn8hEdh2Kns/2qAQokS9xG/UeqZOgp79ifKJQp8dvWpUSzRNZKgPtqsXNmVFac6J/UnlmO+Q==}
engines: {node: '>=22'}
'@ai-sdk/togetherai@1.0.49': '@ai-sdk/togetherai@1.0.49':
resolution: {integrity: sha512-g4BpEatN7flh3GZ0CN9KvAUX6uLPmIqGSrKKFvAmC3HZdnF940zl+ChXs3atdbtpr6+cwirxM5RACbUzr0uYhA==} resolution: {integrity: sha512-g4BpEatN7flh3GZ0CN9KvAUX6uLPmIqGSrKKFvAmC3HZdnF940zl+ChXs3atdbtpr6+cwirxM5RACbUzr0uYhA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -7846,8 +7805,8 @@ packages:
peerDependencies: peerDependencies:
chat: ^4.0.0 chat: ^4.0.0
'@vercel/agent-eval@1.5.0': '@vercel/agent-eval@2.2.1':
resolution: {integrity: sha512-evbgSkCv6CoQxgIr6dfjh6/3K54NxXoykZN5stXS97kTVm4NuU2y7su1WR71Y55tR4l0xgiUTsVp+BlLVh6zZw==} resolution: {integrity: sha512-TVowf/Q60kw8anu4oAIhb1fc4y8k4jhwjCPwdfNoy9m9W1LHBHZYaIi81QZ+7w2S77hvBi4CAoOnohovjA2Mow==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@vercel/agent-readability@0.6.0': '@vercel/agent-readability@0.6.0':
@@ -15764,18 +15723,6 @@ packages:
utf-8-validate: utf-8-validate:
optional: true optional: true
ws@8.21.0:
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
ws@8.21.3: ws@8.21.3:
resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@@ -16098,44 +16045,6 @@ snapshots:
zod: 4.4.3 zod: 4.4.3
optional: true optional: true
'@ai-sdk/harness-claude-code@1.0.90(bufferutil@4.1.0)(zod@4.5.4)':
dependencies:
'@ai-sdk/harness': 1.0.87(ws@8.21.3(bufferutil@4.1.0))(zod@4.5.4)
'@ai-sdk/provider-utils': 5.0.30(zod@4.5.4)
ws: 8.21.3(bufferutil@4.1.0)
zod: 4.5.4
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@ai-sdk/harness-opencode@1.0.88(bufferutil@4.1.0)(zod@4.5.4)':
dependencies:
'@ai-sdk/harness': 1.0.87(ws@8.21.0(bufferutil@4.1.0))(zod@4.5.4)
'@ai-sdk/provider-utils': 5.0.30(zod@4.5.4)
ws: 8.21.0(bufferutil@4.1.0)
zod: 4.5.4
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@ai-sdk/harness@1.0.87(ws@8.21.0(bufferutil@4.1.0))(zod@4.5.4)':
dependencies:
'@ai-sdk/provider': 4.0.8
'@ai-sdk/provider-utils': 5.0.30(zod@4.5.4)
ai: 7.0.79(zod@4.5.4)
zod: 4.5.4
optionalDependencies:
ws: 8.21.0(bufferutil@4.1.0)
'@ai-sdk/harness@1.0.87(ws@8.21.3(bufferutil@4.1.0))(zod@4.5.4)':
dependencies:
'@ai-sdk/provider': 4.0.8
'@ai-sdk/provider-utils': 5.0.30(zod@4.5.4)
ai: 7.0.79(zod@4.5.4)
zod: 4.5.4
optionalDependencies:
ws: 8.21.3(bufferutil@4.1.0)
'@ai-sdk/mcp@2.0.29(zod@4.5.4)': '@ai-sdk/mcp@2.0.29(zod@4.5.4)':
dependencies: dependencies:
'@ai-sdk/provider': 4.0.7 '@ai-sdk/provider': 4.0.7
@@ -16270,17 +16179,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- zod - zod
'@ai-sdk/sandbox-vercel@1.0.87(ws@8.21.0(bufferutil@4.1.0))(zod@4.5.4)':
dependencies:
'@ai-sdk/harness': 1.0.87(ws@8.21.0(bufferutil@4.1.0))(zod@4.5.4)
'@ai-sdk/provider-utils': 5.0.30(zod@4.5.4)
'@vercel/sandbox': 3.2.0
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
- ws
- zod
'@ai-sdk/togetherai@1.0.49(zod@4.4.3)': '@ai-sdk/togetherai@1.0.49(zod@4.4.3)':
dependencies: dependencies:
'@ai-sdk/openai-compatible': 1.0.46(zod@4.4.3) '@ai-sdk/openai-compatible': 1.0.46(zod@4.4.3)
@@ -22322,7 +22220,7 @@ snapshots:
- supports-color - supports-color
- zod - zod
'@vercel/agent-eval@1.5.0(supports-color@10.2.2)': '@vercel/agent-eval@2.2.1(supports-color@10.2.2)':
dependencies: dependencies:
'@ai-sdk/anthropic': 1.2.12(zod@3.25.76) '@ai-sdk/anthropic': 1.2.12(zod@3.25.76)
'@vercel/sandbox': 1.10.2 '@vercel/sandbox': 1.10.2
@@ -33064,10 +32962,6 @@ snapshots:
optionalDependencies: optionalDependencies:
bufferutil: 4.1.0 bufferutil: 4.1.0
ws@8.21.0(bufferutil@4.1.0):
optionalDependencies:
bufferutil: 4.1.0
ws@8.21.3(bufferutil@4.1.0): ws@8.21.3(bufferutil@4.1.0):
optionalDependencies: optionalDependencies:
bufferutil: 4.1.0 bufferutil: 4.1.0