Constrained decoding: making a wrong answer unreachable
Ask a small model to name a command and it will sometimes name a command that does not exist. It will do this confidently, in the right format, with plausible arguments. The usual response is to validate the output and retry.
That works and it is the wrong shape. Validation makes a bad answer rejected; it was still reachable, the model still spent tokens on it, and the retry is another sample from the same distribution that produced it. The better arrangement is to make the bad answer unreachable, so no amount of sampling can produce it.
The problem with checking afterwards
A check after the fact has three properties that get worse as the system grows. It duplicates the list of valid commands, so the checker and the dispatcher can disagree. It runs after the tokens are spent. And it has to decide what to do on failure, which is a policy question nobody wants to answer at three in the morning.
A grammar has none of those. It is consulted during sampling, it is built from the same table the dispatcher uses, and there is no failure case to handle because the failure cannot occur.
A grammar from the live table
The decoder does not choose a command and then spell it. It spells a command, one token at a time, and at every step the grammar computes which tokens could still lead to a valid name and masks the rest before sampling.
position 0 candidates: ls cat write remember run ...
model samples within that set
position 1 prefix "r" -> candidates: remember run
every other token has probability zero
position 2 prefix "re" -> candidates: remember
the rest of the name is now forced
The important word is live. The grammar is built from the applet table as it exists at that moment, not from a list written alongside it. An applet that has been added is spellable the instant it is added; one that has been removed becomes unspellable. There is no second copy to fall out of date, which is the failure mode a hand-maintained validator has by construction.
Prompting makes a wrong name unlikely. It remains in the distribution, so it happens at scale.
Validation makes it rejected. It was still sampled, still cost tokens, and the retry samples the same distribution.
A grammar gives it no path. There is nothing to reject.
Read-only, done properly
This is where the design earns itself, and it is the part worth copying.
The system has a read-only mode: a state in which the model may inspect things but may not change them. The obvious implementation is to let the model choose whatever it likes and refuse the mutating choices. That is a check afterwards, with all three problems above, and one more — the model spends its budget proposing actions that will be refused, and has no way to learn that from inside the episode.
Instead, read-only mode removes the mutating applets from the reachable set before sampling. The grammar is built from a smaller table. The model is not refused, because it never proposes. From inside the decode there is no such thing as a write applet.
The distinction sounds academic until you count the ways a check can be bypassed and the ways a missing grammar production can. There is only one of the second kind: put the applet back in the table.
The bug this caught
A worked example, because it shows the failure the technique prevents and a second one it did not.
The system can compile a successful episode into a reusable skill and
store it under a content-addressed name — something like
/ai/tools/learned-3f2a91c4.ai&xi. Skills are then invoked
through a run applet.
The applet name was always decoded under the grammar, so
run itself was safe. Its argument was free text. Which
meant the model had to spell that hash exactly, from memory, and a skill it
could not spell was a skill it could not use — however good the judges
said it was. The adoption machinery worked perfectly and put tools in a
toolkit nothing could pick up.
The fix is the same idea applied one level down: the argument space is
enumerable, so enumerate it. The grammar for run is built from
the list of stored skills, and the model picks from paths that exist rather
than reproducing a hash.
The honest footnote is that this has not been exercised through a real episode — that needs a model and minutes of wall time. What is checked at boot is narrower: that every choice offered is a full path to a program that exists.
What it does not do
A grammar constrains form. It has nothing to say about whether the choice was any good.
The model can still pick a perfectly spellable, perfectly valid applet that is the wrong one for the task. Constrained decoding cannot help with that, and nothing in this piece should be read as claiming otherwise — which is why the routing accuracy numbers are measured separately, and why the thing that actually decides turned out not to be the decoder at all.
It also does not make a small model good at arithmetic, or at anything else it is bad at. It makes the output well-formed. Those are different claims and conflating them is how benchmark tables become dishonest.
