DOC GK-006 TYPE ENGINEERING LOG SUBJECT VERNIER

Temperature 0
is not
deterministic.

I set out to measure whether upgrading a model changes what an agent does. Before I could measure that, I needed a control: how much do two runs differ when I change nothing at all? I pinned every knob I could find. 6.7% of runs still came out different. The tool is called Vernier, and this is the log of the boring experiment that turned out to be the interesting one.

WHAT CI CHECKED unit mocks lint ALL GREEN · exit 0 WHAT THE AGENT ACTUALLY DID BEFORE lookup_order refund AFTER skipped refund money moves with no lookup
FIG 06.1 · The gap nothing was watching. Every test that mocks the tools passes. The behaviour is the thing that changed.

SEC. 01

The ship gate that isn't

You bump a model id in CI. Tests still pass. Customers still hit a new failure mode.

Here is the concrete case I kept coming back to. A support agent used to call lookup_order and then refund. After a quiet model change it skips the lookup and refunds from memory. Every unit test mocks the tools, so none of them notice. An LLM-as-judge score might still read "fine". The behaviour changed and you shipped it.

The obvious move is to write a tool that diffs agent behaviour before and after a change. That is what I built. But a diff is only meaningful against a floor. If two identical runs already differ, a tool that reports "these two runs differ" is telling you nothing you can act on.

So the first experiment was not the interesting one. It was the control. And the control turned out to be the finding.

RECORD append-only RE-DRIVE shim MODEL · LIVE TO PROVIDER TOOLS · STUBBED FROM THE RECORD ALIGN edit script CLASSIFY D01 to D12 SEVERITY NONE LOW MEDIUM HIGH CRITICAL run = max of fired rules
FIG 06.2 · The split is the whole design. The model goes live to the provider. The tools come back from the recording. That isolates one variable and only one.

SEC. 02

The instrument

Vernier wraps the command you already run. It does not ask you to edit your agent.

$ uv run vern record -- python examples/verify_fixture_agent.py --scenario 01_tools_basic $ uv run vern diff <run_id> --model gpt-4o-mini $ uv run vern gate --corpus testdata/verify --max-severity HIGH vern gate: worst=NONE threshold=HIGH n=10 exit=0

Recording writes an append-only run: event lines plus content addressed blobs. That record is the evidence. Re-driving replays it under a shim, and the shim is where the one interesting decision lives. In vern verify everything is stubbed, model included, which proves the substrate can reproduce a run at all. In vern diff and vern gate the model goes live and only the tools come from the record.

Alignment turns two trajectories into one edit script of matches, inserts, deletes and substitutions. Costs come from a versioned table, not an embedding, so the same inputs produce the same alignment bytes on Linux, macOS and Windows. Then twelve fixed rules read the aligned pair and emit a severity.

There is no model anywhere on that path, and that is the point. A judge that can change its mind tomorrow can give you a signal, but it cannot be your gate. If classification ever needs a coin flip or a network call, Vernier is broken.

KNOBS HELD STILL model · same prompt · byte-eq temperature · 0 seed · pinned batch size · 1 local CPU no contention delta · zero RUNS THAT DIVERGED ANYWAY 6.7% expected: 0%
FIG 06.3 · The control. Nothing changed between the two runs. The bar should have been empty.

SEC. 03

The most boring experiment I could design

Same model. Same prompt, byte for byte. Temperature 0. Seed pinned. Running locally, on a CPU, one request at a time, with nothing else competing for the machine. Every knob held still.

6.7% of runs still came out different.

That number needs its caveat stated before anything else, not buried at the bottom. This is a 4B quantised model on CPU at batch size 1. That is a far more favourable setting than hosted inference under real load, where batching, kernel selection and routing all move underneath you. So treat 6.7% as a floor, not an estimate. Whatever you are actually running is at least this noisy.

SAME REQUEST BODY, REPLAYED 50 TIMES 49 identical 1 different a ~2% rare path, on almost every prompt I checked
FIG 06.4 · Not a coin flip. One dominant answer and one quiet alternative sitting behind it.

SEC. 04

I went looking for my own bug

A number like 6.7% at zero delta usually means you broke something. So I assumed I had, and went hunting. I took every point where two runs diverged, pulled the exact request body, and replayed it 50 times.

The model gave the same answer 49 times and something different once. Roughly a 2% rare path, and it showed up on almost every prompt I looked at.

That shape matters more than the percentage. This is not a model wobbling randomly across a spread of answers. It is one dominant answer with one quiet alternative sitting behind it, waiting. Which means you can run your evaluation suite twenty times, see it pass twenty times, and still be one sample away from the other arm.

STEP 12345678 49 / 50 1 / 50 one token
FIG 06.5 · Divergence compounds. A 2% event at step one is not a 2% event by step eight, because step two is already reading a different world.

SEC. 05

A fork, not a rephrasing

If the rare arm were a reworded sentence, none of this would matter much. It is not.

query, 49 runs: urban-heat research sources query, 1 run : urban-heat definition and causes

Different search. Different documents. A different context window for every step that follows. The agent is not saying the same thing differently, it is deciding something different and then acting on it.

A 2% event at step one is not a 2% event by step eight. Step two is already reading a different world.

That is the arithmetic that turns a rare token into 6.7% of whole trajectories. It is also why measuring divergence at the token level tells you almost nothing useful. You have to measure the trajectory.

A-01A-02A-03A-04A-05 A-06 tool args chosen from a fixed set free text DIVERGENCE BY AGENT SHAPE 100% of it, from one shape
FIG 06.6 · The actionable part. Six agents. The five that pick from fixed options never moved.

SEC. 06

Where the variance actually lives

Here is the part I did not expect, and the part I think is worth more than the headline number.

The divergence was not spread evenly across the six agents in the corpus. All of it came from one: the agent where the model writes free text into tool arguments. The agents that pick from a fixed set of options were completely stable across every run.

So this is not "models are random". It is much more specific than that, and much more actionable. Give the model a blank field and you have variance. Give it an enum and you have a decision you can test. If you are designing an agent you intend to gate in CI, that is a schema choice you can make on day one, for free, before you have any of this tooling.

I would not have found that by measuring one agent harder. I found it by measuring six shapes and noticing that five of them sat still.

V0 RESULT vs PRE-REGISTERED BAR bar: 20% 100% RETRACTED cause: dead hostname · candidate agent died before calling any tool · classifier correctly said an empty run differs from a real one
FIG 06.7 · A perfect score against your own bar should scare you. The write-up is still in the repo, marked do-not-cite.

SEC. 07

The version I got wrong

My first version of this study reported 100% divergence against a threshold I had pre-registered at 20%. For about an hour that felt like a discovery.

It was a broken test harness pointing at a dead hostname. The candidate agent died before it called a single tool. The classifier then did exactly its job and reported that an empty run differs from a real one, on every single fixture, with total confidence.

Every part of the system worked correctly. The result was still garbage. That is the failure mode worth being afraid of, because nothing errors.

The lesson I actually took from it is smaller and more useful than "test your harness". It is this: a result that clears your pre-registered bar by five times is not a triumph, it is an alarm. I had written the 20% threshold down in advance precisely so I would have something honest to be measured against, and then I nearly let a 100% flatter me past it.

RESULTS-v0-RETRACTED.md is still in the repository, at the top level, marked do-not-cite. It stays there. Deleting it would make the repo look better and the record worse.

STOP CONDITIONS, WRITTEN BEFORE THE CODE KT-1 model-version change moves behaviour in <20% of runs UNEVAL KT-2 classifier below 85% against hand labels UNEVAL KT-3 full-stub re-drive must match 100% of actions PASS KT-3b live-diff with no override must return NONE PASS KT-4 nobody puts vern gate in CI unprompted NOT DUE
FIG 06.8 · Two of these are still open. The board is public and it is not softened.

SEC. 08

What broke, and what I would change

The harness was wrong before the result was right. Covered above. Cost me a write-up and a retraction, and it is the single most useful thing in the repository now.

One run replayed 50 out of 50 identical and still diverged in the experiment. This is my favourite detail in the whole study. The recording itself had captured the rare arm. My baseline was the 1-in-50. Which means a recorded run is a sample, not the truth, and every tool in this space inherits that problem, mine included. If I rebuilt the study I would record each baseline three times and keep the modal trajectory rather than the first one.

I have not answered the original question. KT-1 and KT-2 both read UNEVALUATED and that is not modesty, it is the actual state. Testing whether a real model upgrade changes agent behaviour needs paid API access I have not spent yet. The harness is built, the corpus exists, the runner is written. It is roughly twenty dollars of tokens and a free afternoon. Until it runs, the headline claim of the tool is unproven and I would rather say so than let a good control experiment stand in for it.

Tools are stubbed on re-drive, so this does not test your tools. Live-diff asks the new model what to do and feeds it recorded tool results. That isolates the model variable cleanly, and it means Vernier is silent on whether your live integrations still work. Two different jobs. I would rather do one of them honestly.

Providers are nondeterministic and I cannot fix that from here. Treat live-diff as evidence, not as a mathematical identity proof. When you need a hard reproduce check, use vern verify with everything stubbed.

ClaimStatusEvidence
The substrate can reproduce a recorded run exactlyPASSKT-3, 10 runs, 8+ steps
Live-diff with no change returns NONEPASSKT-3b
Zero-delta noise floor is 6.7% of runsMEASURED4B quantised, CPU, batch 1
A model upgrade changes agent behaviourUNEVALUATEDneeds paid API access
The classifier agrees with hand labelsUNEVALUATEDground truth not yet labelled

Everything is public, including the raw replay data and the retraction, so you can check my arithmetic instead of trusting it. If the twenty dollars is pocket change to you, the repo has everything you need to run KT-1 yourself. I would rather the number existed than that I made it up.

DOC GK-006 · Vernier · Rev 2026.08.12 Next log: GK-007 · Cairn →