Guides
Runner
How a task, a model and a context become a graded and priced run, with mock models so it works without an API key.
What
The runner lives in packages/bench/src/runner/. runTask({ task, model, context }):
- gives the model a system prompt, the task prompt and a set of tools;
- lets it call the tools until it stops (or hits
maxSteps); - grades every file it wrote and flags edits to protected files;
- with a page checker, typechecks, lints and renders the page (guide 11);
- prices each model call and adds them up.
| File | Job |
|---|---|
workspace.ts | In-memory file system with list_files, read_file, write_file. Seeded with fixture.json only |
prompt.ts | The system prompt: the environment, not the design system |
context.ts | The five context levels and the MCP connections behind them |
check-tool.ts | The check_code of ds-mcp-check-full: design-system graders, tsc and oxlint |
models.ts | Gateway ids for real models; mock/ideal, mock/lazy, mock/rogue, mock/careful |
run.ts | runTask |
results.ts | Writes results/<runId>/<taskId>/, screenshots included, and batch.json |
../cli.ts | bun run bench, with --budget and --no-page |
Why
- In-memory files. Runs are repeatable, can run in parallel, and an agent cannot
damage the repo. The agent sees only
fixture.json, so it cannot read the reference solution or the design-system source: the context level is the only way in. - One system prompt for everyone. It says Next, React 19,
@vektos/ds, Panda and where to write. It says nothing about components or tokens. - Mock models. A mock plays a fixed script, so the loop (tools, MCP, grading, pricing, results) is tested without a key and without cost. Mock token counts are estimated from the request size, tool list included, so a bigger context costs more even in a mock.
- Cost per step.
onStepEndgives the usage of each request; long-context rates depend on one request, not the run (guide 09). - A path must be a path.
write_filerefuses one with control characters, quotes or brackets, longer than 200 characters or with a part longer than 100, and says so to the agent. Beam once sent a page's whole content as its path; the results could not be written and the batch stopped (ch. 68). Writing a result now skips a file the disk refuses instead of stopping. - The feedback is kept. Each step records what the tools that judge the agent's work
answered (
FEEDBACK_TOOLSinrun.ts:check_codeand the spec mode'ssubmit_view), at most 4,000 characters each; the run's page shows it under the call. Other tools' answers (files read, documentation) are left out: large, and they say nothing about the run. - The gateway's charge is kept beside ours. A real run stores what the AI Gateway
billed for each request (
gatewayUsd) and the CLI warns if our price is off by more than 2%. A wrong price table cannot go unnoticed for long. - A budget stops the batch, not the task.
--budget 1stops starting tasks once the spend reaches $1. A task already running is not cut short (--max-stepslimits it), so the total can pass the budget by one task. - MCP by path.
ds-mcpis started asbun packages/ds-mcp/src/cli.ts, not imported: ds-mcp depends on bench forcheck_code, and an import would be a cycle.
Try it
bun run benchA whole sweep on the free models, as in chapter 53 (Storybook must be running for
storybook-mcp; about three hours a model; $0):
for context in none storybook-mcp ds-mcp ds-mcp-check ds-mcp-check-full; do
infisical run -- bun run bench --model reflection/beam-free --context $context \
--label free-rerun --budget 0.05 --no-backup
done
bun run bench:history -- --label free-reruntask page done ds qual steps tokens in/out cost
kyc-form yes yes 0 0 3 3917/1460 $0.0112
statement yes yes 0 0 3 3834/729 $0.0075
...bun run bench --model mock/rogue --task statement # ds 1: panda.config.ts edited
bun run bench --model mock/lazy # page NO: did nothing
cd packages/bench && bun test src/runnerFull options in the user guide.
Extend it
- A context level. Add it to
CONTEXT_LEVELSandopenContextincontext.ts. - A mock behaviour. Add a case to
plan()inmodels.ts: a list of tool calls and a final text. - A real model. Use its gateway id. Add it to
PRICESfirst, or the run refuses to start. - A local model (the provider seam).
--model local/<name>asks an OpenAI-compatible server for<name>:VEKTOS_LOCAL_BASE_URL(Ollama'shttp://localhost:11434/v1by default),VEKTOS_LOCAL_API_KEYif it needs one. It costs nothing and needs no gateway key:bun run bench --model local/qwen2.5-coder:7b --context ds-mcp --task statement --no-save. Another provider is one more branch inresolveModel(models.ts) and its prices inpricesFor. - The output cap.
--max-output-tokens(defaultMAX_OUTPUT_TOKENS, 20,000) is what one model call may write; the batch stores it. - Another model client.
runTask({ languageModel })calls that model instead of the onemodelnames;modelstill picks the price. The payload check uses it to see what the runner sends (guide 19). - Another runner (Claude Code, Codex).
RunResultis the contract; produce the same shape and the rest of the pipeline works.
Known gaps
- Batches before chapter 55 ran without a cap: each provider's default applied (Anthropic's
SDK sent 128,000, the others nothing). Their
max_output_tokensis null, and the run page says so. - Results are files first. The database keeps them as history (guide 12);
results/stays as the readable copy. - Repeats are separate batches. Three runs of a cell are three batches with one label; no variance is computed yet.