- Write two cases: a coder case checked by real Go tests, and a chat case graded by an LLM judge. The code goes inline in the suite or stays in real files that the suite reads with
!file. - Run them and save the result as a baseline.
- Lay out one folder per change you want to test, each holding a copy of the suite with that one change.
- Test a configuration change that looks dangerous but turns out to only cost more.
- Test a policy change that really breaks the coder, and watch the run fail with exit code 3.
- Compare the three runs, offline, and wire the gate into CI.
Evals spend real tokens.
chatcli eval validate and chatcli eval list cost nothing; use them while you write a suite, and put a ceiling on every run with --max-cost.1. Write the suite
Createevals-demo/demo.yaml. The coder case gives the agent a Go module whose Reverse swaps bytes, which breaks accents, CJK characters and emoji; the tests already cover those and fail. The chat case asks for a short explanation in Portuguese.
evals-demo/demo.yaml
Prefer real files? Read them with !file
To keep the module as ordinary files you edit in your IDE, put them in a directory next to the suite and tag each files: entry with !file. The suite then holds no code:
- Paths resolve against the suite file, like
fixture:, and!filekeeps the source’s permission bits, so a script you run insetup:stays executable. - For the whole directory, use
fixture: testdata/strutilinstead: it copies the tree in one line.fixture:must name a directory; pointing it at a file fails with a message that says so and suggests!file. - They mix. The fixture is copied first, then
files:are written over it, thensetup:runs. Keep the module in a fixture and replace one file per case, inline or with!file. - Name the directory
testdata/.go build,go test ./...andgo vetskip it, so the broken module stays out of the repository that hosts the suite.
chatcli eval validate reads every !file while it checks the suite, so a wrong path, a directory or a misspelled tag (!fiel) shows up before you spend anything. Files on disk or inline lists every message, and Workspace patterns shows seven ways to combine fixture, files and setup, plus how to check a workspace without calling a model.
2. Check it for free
3. Run it and save a baseline
Use a judge that is a different model from the candidate: a model grading itself shares its own blind spots, and the report flags such a run asself_judged. Two trials per case show whether a pass is consistent.
- Each trial and each case line shows the candidate’s cost plus the judge’s. The chat trials cost about 0.00014 per answer. The summary line splits the two (
cost $0.0157 + judge $0.0065). 2/2 trialswith the defaultpass_policy: allmeans pass^k: every trial passed. Two out of three would show the case as flaky, and it would fail.run1.jsonis your baseline. Keep it.
4. Read the report
The JSON report holds every check of every trial, the judge’s reasoning, the tools called and the final answer. A fewjq lines answer most questions:
tests pass and tests untouched ran on their own in the sandbox.
5. Lay out the baseline and its variants
To test a change, run the same suite with that one change against the baseline. Keep each variant in its own folder, next to the suite it changes:- One change per variant. When a variant regresses, you know which change did it. Two changes in one variant leave you guessing.
- The same suite
nameand the same caseids in every copy. The comparison matches cases by<suite>/<id>. Rename either one andcomparereports the case as removed and a new one added, instead of comparing them. - Variants in subfolders stay out of normal runs.
chatcli eval run evals-demo/loads only the*.yamlfiles directly in that folder; subfolders are not scanned. The baseline run never picks up a variant by accident, and each variant runs only when you name its file. - One report per run, numbered. Every variant is compared against the same
run1.json.
diff that nothing else moved:
chatcli eval validate badcfg/demo.yaml checks the copy for free before you spend anything on it.
6. Test a change that only costs more
Suppose someone lowers the output-token ceiling to save money. Put the change in the suite’senv:, not on your command line:
name and the case ids unchanged, so the comparison matches case by case, and run against the baseline:
0. The coder loop notices a reply cut off at the token limit, raises the limit and continues, so a low ceiling does not break it. The change still shows up in the report, though, as extra turns:
turns, output_tokens and cost_usd between reports to see it.
7. Test a change that breaks the coder
Now suppose someone tightens the coder policy so that it can no longer change files. A case’spolicy: installs rules for that case only, merged over your global policy:
write, patch and multipatch are the subcommands that edit files. exec is denied too: without that, the agent could still edit through the shell.
- Only the affected case fails. The chat case uses no tools and still passes.
-
The score says how much it got worse. Four of the six checks still pass (the tests are untouched,
@coderwas called, turns and cost stayed in budget), so the trial scores 0.67, not 0. The verdict is still a failure, because a trial passes only when every check does. - Exit code 3 means a regression against the baseline. It is the code that fails a CI job.
-
The agent’s behavior under the block is visible too. Its final answer did not claim a fix and did not keep trying other ways around the rule:
Nobody can answer the agent’s question in an eval, so the case fails. That is the point: the policy change is what broke it.
8. Compare the three runs
Side by side, the three reports tell three different stories:
The turn counts come from the reports (
jq '.cases[].trials[] | .turns' runN.json). Neither the pass rate nor the exit code shows the cost of the second change; the turns do.
Offline, without calling a model
compare reruns the comparison from two saved reports, without calling a model, with the same gates and exit codes as run:
9. Gate pull requests in CI
Commit the suite and a baseline produced onmain, then run the suite against that baseline on every pull request. A GitHub Actions job:
.github/workflows/evals.yml
--max-costcaps every run: once candidate and judge spending reach it, no new trials start and the rest are reported as skipped.--markdownwrites the summary, the delta against the baseline and the failed checks, ready for the job summary or a pull request comment.- Refresh the baseline on purpose, after a change you accept, by rerunning on
mainwith--out evals/baseline.json.
Pitfalls
Related
Evals
Every field, check, flag and exit code.
Coder Security
The policy rules the
policy: field of a case uses.