Your First Benchmark
We are going to measure something concrete: given a genuine bug and a failing test suite, which model fixes it — and does it touch anything it should not have?
axon bench init parser-bench
cd parser-bench
That scaffolds the whole structure. Four things to fill in.
1. Build the world
workspace/ is what the agent will see. Put a small project in it with a real defect:
// workspace/src/parser.ts
export function parseLine(line: string): { key: string; value: string } {
const [key, value] = line.split("=")
return { key, value }
}
And the suite that catches it:
// workspace/tests/parser.test.ts
import { expect, it } from "bun:test"
import { parseLine } from "../src/parser"
it("parses a simple pair", () => {
expect(parseLine("a=1")).toEqual({ key: "a", value: "1" })
})
it("keeps '=' inside the value", () => {
expect(parseLine("url=http://x/?a=1")).toEqual({ key: "url", value: "http://x/?a=1" })
})
it("trims surrounding whitespace", () => {
expect(parseLine(" a = 1 ")).toEqual({ key: "a", value: "1" })
})
One passes, two fail. split("=") drops everything after the second separator and nothing
trims. A real bug with a real test suite proving it — which is what makes the measurement
mean something.
2. Declare what you are measuring
type Schema = {
/** Has the agent resolved the bug? */
resolved: boolean
/** Files edited beyond the target. @objective minimize */
collateral: number
}
Two measurements. resolved needs no objective — more fixed bugs being better is obvious.
collateral does, because fewer touched files being better is not something a chart can
infer.
3. Declare what varies
import { OpenRouter } from "@arcforge/engines"
export default defineBench<Schema>({
description: "Can the agent fix a real bug without collateral damage?",
workspace: { source: "./workspace", retain: "failed" },
matrix: {
model: [
OpenRouter({ model: "anthropic/claude-sonnet-4.6" }),
OpenRouter({ model: "openai/gpt-5" }),
],
},
trials: 3,
async setup() {
await Bun.$`bun install`.cwd(bench.workspace)
},
})
Two models, three trials: six runs. setup() installs the workspace's dependencies after
the copy and before the agent boots, so the test suite is runnable when the agent arrives.
Add the engines package so the import resolves:
bun add @arcforge/engines
4. Write the scenario
// tests/fix-bug.bench.ts
it("resolves the failing test", async ({ workspace }) => {
expect(workspace).toHaveFile("src/parser.ts")
const { axon } = await Axon()
await axon.request("Two tests in tests/parser.test.ts are failing. Fix src/parser.ts so they pass.")
observe("resolved", await workspace.tests.pass())
observe("collateral", (await workspace.changed()).filter(f => f !== "src/parser.ts").length)
await bench.attach("diff", await workspace.diff())
})
The expect guards the experiment — if src/parser.ts is missing, the world is wrong and
this trial measures nothing. The two observe calls record what happened, whatever it was.
The attach keeps the actual patch, because when a cell scores badly the diff is what tells
you why.
Run it
axon bench prepare
axon bench run
Benchmark run 01JQZX8K2M4N6P8R
4 passed, 2 failed, 12 observations
Twelve observations from six runs: two measurements each. The full record:
axon bench result 01JQZX8K2M4N6P8R
Every trial, its physics, its session, its observations, and the manifest pinning exactly which model produced which number.
What you did not have to write
Duration, token counts, cost, engine calls, tool calls, error counts. Which trials completed and which faulted, and why. The full session for every run. Coverage — whether every measurement you declared actually fired.
None of that is in the config or the test. It falls out of events the runtime already emits, which means it is there for every benchmark ever written.
Where to go next
Cut the cost of iterating to zero with Testing with Mock — the same six-run flow, no API keys, no spend.
Then Publishing, so somebody else can run this against their agent.