GBNF grammar-constrained tool calling is a technique that forces a language model to emit only structurally valid output. A GBNF grammar defines the exact shape allowed, such as a JSON array of tool calls. At each decoding step, the model can sample only tokens the grammar permits, making malformed tool calls impossible.
That single property is what separates a local model that can chat from a local model that can act. An agent has to emit a machine-readable call, take a result, and emit another one, dozens of times in a row. Prompting for that format works on a frontier model and fails on a 4-bit build running on your laptop. Constraining it at the decoder works on both.
- GBNF is llama.cpp’s native grammar format, sent to
llama-serveras agrammarfield in the request body - The grammar masks out every token that would break the structure before the sampler ever sees it, so there is no invalid output to catch and retry
- Quantization degrades format reliability faster than it degrades reasoning, which is exactly why small local models need this and hosted frontier models mostly do not
- Atomic Agent’s grammar roots at an array, not a single object, so a solo step is
[{...}]and a parallel batch costs nothing extra - A grammar guarantees shape, never correctness: the model can still emit a perfectly well-formed call to the wrong tool
What is GBNF?
GBNF stands for GGML BNF. It is a grammar format that llama.cpp supports natively for constrained decoding, and the notation is specified in llama.cpp’s own GBNF documentation. If you have ever seen Backus-Naur Form, the notation used to describe the syntax of programming languages, GBNF will look familiar. It is a BNF-like notation that defines which sequences of tokens are legal.
Think of it as a set of rails. A train can move fast because it physically cannot leave the track. A GBNF grammar is the track. It does not tell the model what to think, but it does decide what the model is even allowed to say next.
For agents, the useful thing to constrain is the output that carries actions: a JSON array of tool calls. If that array is well-formed every single time, the runtime that reads it never chokes on a parse error.
The grammar reaches llama.cpp as a plain string. A client sends it in the grammar field of the completion request body, alongside the prompt and the sampling parameters. There is no separate compilation step to manage and no second model in the loop. The server parses the grammar, builds the state machine, and applies it during sampling.
How does a grammar force valid JSON?
To understand the mechanism, you have to look at how a model actually generates text.
At each decoding step, the model produces a probability distribution over its entire vocabulary, tens of thousands of possible next tokens. Normally, a sampler picks one of the high-probability tokens and appends it. Then it repeats. This is where malformed output comes from: the model is free to pick any token, including one that breaks your JSON, like a stray word where a } should go.
Grammar-constrained decoding changes one thing. Before the sampler picks, the grammar is consulted. Given everything generated so far, the grammar computes the set of tokens that would keep the output valid. Every token outside that set has its probability forced to zero. The model literally cannot sample it.
So if the grammar says the next character must be [ or whitespace, then no matter how confident the model was about emitting the word “Sure,” that token is masked out. It never had a chance. Reliability stops being a matter of prompt luck and becomes a property of the decoder.
Here is a tiny, illustrative GBNF grammar for a JSON array of tool calls:
root ::= "[" ws call (ws "," ws call)* ws "]"call ::= "{" ws "\"name\"" ws ":" ws string ws "," ws "\"arguments\"" ws ":" ws object ws "}"object ::= "{" ws (pair (ws "," ws pair)*)? ws "}"pair ::= string ws ":" ws valuevalue ::= string | number | object | "true" | "false" | "null"string ::= "\"" ([^"\\] | "\\" .)* "\""number ::= "-"? [0-9]+ ("." [0-9]+)?ws ::= [ \t\n]*Read top to bottom, this says: the root must be a [...] array of one or more call objects; each call must have a name string and an arguments object; and so on down to what a valid string or number looks like. There is no production rule that produces prose, an apology, or a trailing comma. Those outputs are simply unreachable.
The important word in that last sentence is unreachable, not rejected. A validator rejects bad output after it exists. A grammar prevents it from existing. That difference is why grammar-constrained decoding has no failure rate to report: there is no path through the state machine that ends in a broken brace.
One consequence surprises people the first time they see it. The model’s probabilities still matter, they just operate inside a smaller room. If the grammar allows three tokens at a given position, the model still chooses among those three according to what it learned, and that choice is where all the useful behavior lives. Constraint does not flatten the distribution, it truncates it. The model is still writing; it simply cannot write outside the lines.
This also means a grammar never fights the model when the model is already right. A well-behaved completion that would have been valid JSON anyway passes through the mask untouched, because every token it wanted was permitted. The constraint only becomes visible at the moments the model was about to go wrong, which is exactly where you want a guarantee to activate.
Why quantization makes this necessary
Frontier models hosted in the cloud are large enough that they usually emit clean JSON when you ask nicely. Small quantized models running on your laptop are a different story, and the reason is worth understanding rather than accepting on faith. If you are still weighing whether to run a local model at all, this is the tradeoff you are accepting in exchange for local execution, and the grammar is what makes it survivable.
Quantization lowers the numeric precision of the weights, typically from 16 bits down to 4 or 5. The model keeps most of its knowledge and most of its reasoning, because those are distributed across many parameters and degrade gracefully. Format adherence is different. Emitting } at exactly the right position is a narrow, low-margin decision: the correct token often wins by a small probability gap over a plausible-looking wrong one. Rounding the weights shrinks those gaps. A decision that was safe at full precision becomes a coin flip near the boundary.
The practical shape of this is a reliability cliff that only shows up at scale. A model that returns valid JSON 99 times out of 100 at full precision might slip to 90 at 4-bit. In a chat window you would barely notice. In an agent loop with thirty tool calls in a task, a 90 percent per-call success rate means the task almost certainly fails somewhere.
The arithmetic is worth sitting with, because it is the entire argument for constraint. Per-step reliability compounds across a task: at 99 percent per call, thirty calls finish clean about three quarters of the time, which is workable. At 90 percent, the same thirty calls finish clean about four percent of the time. The model did not get much worse at reasoning between those two numbers. It got slightly worse at punctuation, and the agent loop multiplied that into failure.
This is also why the problem is so easy to miss during evaluation. Format errors are rare enough per call that a handful of manual tests will not surface them, and they concentrate in exactly the runs that are long enough to matter. Teams routinely conclude a quantized model “cannot do agent work” when what it cannot do is emit forty consecutive pieces of clean JSON unaided.
A local AI agent runs exactly that kind of compressed model on consumer hardware. Left unconstrained, it will occasionally:
- Wrap its JSON in a chatty preamble (“Sure, here’s the tool call:”).
- Hallucinate a tool-call shape that looks plausible but does not match your schema.
- Emit a trailing comma, an unclosed brace, or a smart quote that breaks the parser.
- Drift back into the format it saw most during training rather than the one you asked for.
In an agent, any one of these is fatal. The whole loop depends on the runtime being able to read the model’s output, look up the tool, and run it. One malformed array and the chain falls over.
This is why grammar constraints matter so much for local agents. Quantization keeps the model smart enough to reason after compression; the grammar guarantees the output stays machine-readable while it does. Those two things together are what moved local agents from “toy” to “usable,” and neither works without the other.
Grammar-constrained decoding versus the alternatives
There is more than one way to get structured output. The headline conclusion: grammar-constrained decoding is the only approach on this list that guarantees valid structure without requiring a specific model or a specific vendor, which is precisely the combination a local agent needs.
| Approach | Guarantees valid structure | Needs a specific model | Runtime cost | Works on any local GGUF |
|---|---|---|---|---|
| Prompt and pray | No | No | None | Yes |
| JSON mode | Usually, for JSON only | Yes, model or API must support it | Low | Only where the runtime implements it |
| Function-calling fine-tune | No, improves odds only | Yes, tuned checkpoint required | None at inference | Only that checkpoint |
| Retry and repair loop | No, eventually or never | No | High, pays per failure | Yes |
| GBNF grammar | Yes, by construction | No | Low, paid once at decode | Yes |
A note on each, because the table compresses a lot:
Prompt and pray is asking for JSON in the system prompt and hoping. It is fine on a frontier model handling a handful of calls and fragile on a quantized model handling dozens. There is no mechanism here, only a probability.
JSON mode covers the structured-output modes exposed by hosted providers and some local runtimes. Where it exists it works well, and under the hood it is often the same idea: a JSON Schema gets compiled down to a grammar. The catch is availability. You are depending on the vendor or the runtime to implement it for the model you picked, and for local weights that is not a given.
Function-calling fine-tunes train the format into the weights. This genuinely improves the odds, and it costs nothing at inference. It also locks you to that checkpoint, and it still only shifts a probability. Quantize the fine-tune and you are back to the same cliff, because you compressed the very weights that encoded the format.
Retry and repair loops let the model generate freely, try to parse, and ask again on failure. This works, and it is the most common fallback, but the cost is real: every failure spends a full generation pass, and a stubborn small model can loop several times on one step. It also fails silently in the worst way, by burning your step budget on formatting rather than work.
GBNF grammars move the guarantee into the decoder. Invalid tokens are never sampled, so there is nothing to catch and nothing to retry. You pay the cost once, at decode time, instead of paying it repeatedly in failed attempts.
For a broader look at how different agent runtimes make this choice, and what else separates them, see Atomic Agent vs Hermes vs OpenClaw.
How Atomic Agent uses grammar-constrained tool calling
Atomic Agent’s agent loop is built around this. One model inference equals one step. Each step, the model emits a JSON array of tool calls and stops. That single inference is GBNF grammar-constrained, so the array is always well-formed.
The real grammar lives at grammars/tool-call.gbnf in the repository. Its core shape is a single rule:
tool-call ::= "{" ws "\"tool\"" ws ":" ws tool-name ws "," ws "\"args\"" ws ":" ws object ws "}"Two fields, tool and args, and nothing else is expressible.
Why the root is an array
The design decision worth explaining is that the grammar’s root is an array, not a single object. Every completion starts with [. A single tool call is the model emitting [{...}], and up to 16 calls fit in one completion.
This looks like a detail and is not. If the root allowed both a bare object and an array, the model would have two valid ways to say the same thing, and small models gravitate toward the simpler one. You would get single-object completions almost always, and parallel batching would become a capability the model technically has and never uses. Forcing the array form removes the choice: there is no cheaper shape to fall into, so emitting two calls at once costs the model nothing beyond deciding to do it.
The general principle behind that decision applies to any grammar you write. Every place the grammar offers a choice, the model will resolve it by habit rather than by reasoning about your intent, and habit means whatever shape was most common in training. If you want a behavior to be available, do not merely permit it, remove the alternatives that make skipping it easier. A grammar is a design surface, not just a safety net.
The cap of 16 calls per completion is the other half of the trade. Unbounded batching sounds strictly better and is not: a model that can emit fifty calls in one array will occasionally do so, spraying speculative work before any result comes back to correct it. A ceiling keeps a batch to the size where the calls plausibly are independent, and pushes anything larger into the next step, where the model gets to see results first.
We saw that pay off in a measured run. On a 32 GB M5 running a 4-bit build of Qwen 3.8 27B (UD-Q4_K_XL, 17.9 GB on disk), the agent completed two multi-step tasks with zero malformed tool calls across six steps, and one of those steps correctly emitted a parallel batch of two calls: reading a file and sending images to the vision pipeline at the same time, because neither result depended on the other. Nothing in the prompt asked for that. The full run, with per-step timings, is written up here.
Tool names are in the grammar, not just the prompt
The grammar does not accept an arbitrary string for tool. Tool names are enumerated by family in the grammar itself:
| Family | Examples |
|---|---|
| Browser | navigate, click, type, read_aria, search, tabs, scroll |
| OS | shell.run, fs.read, fs.write, fs.list, fs.grep, git.*, http.request, web.search, clipboard.*, proc.*, notify |
| Skills and discovery | skill lookup and listing tools |
| Memory | memory read and write tools |
| Tasks | task management tools |
| Vision | vision.describe |
| MCP | per-server tools, stitched in dynamically |
| Control | reply, finish |
Enumerating names in the grammar closes a failure mode that pure JSON validity leaves open. A schema that says tool is a string will happily accept "fs.read_file" when the real name is fs.read. The enumeration makes that unreachable in the same way a trailing comma is unreachable.
Tools exposed over the Model Context Protocol are the exception that proves the rule, because a server advertises its tool names at connection time rather than ahead of it. Atomic Agent stitches per-server MCP tools into the grammar dynamically through buildGrammar. A permissive static fallback shape exists so the grammar still parses before the dynamic list is available, and the tool registry rejects unknown names at invoke time. So there is a narrow window where the grammar is looser than the registry, and the registry is the backstop.
What the runtime does with the array
Once the array comes back, the runtime takes over. For each call it:
- Looks the tool up in the tool registry by fully-qualified name, like
os.fs.readorbrowser.navigate. - Validates the arguments.
- Checks whether the tool is dangerous.
- Runs it.
- Compresses the result so the next step’s context stays lean.
Then it calls the model again with the results, and the loop continues. Because the grammar guarantees the array parses, none of these steps ever has to defend against garbage input at the JSON level. You can read more about how the tool layer is structured in the docs.
The proof: GAIA Level 1
Grammar-constrained decoding is not a theoretical nicety. It is the difference between a small local model that can hold a multi-step task together and one that falls over on the third tool call.
We measured it on GAIA Level 1, a 53-task benchmark of real multi-step problems that require tools. Atomic Agent and Hermes ran on the same hardware with the same local model, so the only meaningful difference was what each does with the model’s output.
| Atomic Agent | Hermes | |
|---|---|---|
| Accuracy | 69.8% | 58.5% |
| Tasks solved | 37 of 53 | 31 of 53 |
| Median time per task | ~217 s | ~351 s |
| Peak steps used | 31 | hit the cap |
That is 11.3 percentage points more accurate and roughly 1.6x faster per task, on identical hardware and an identical model.
The speed result is the one that surprises people, because constrained decoding sounds like it should cost time. It does the opposite here. A model that cannot emit a malformed call never burns a step on a retry, and it never spends tokens on preamble before the JSON. Fewer wasted steps means fewer forward passes, and fewer forward passes means less wall-clock time.
The full methodology, including the model, the context window, the step budget, and the per-task breakdown, is published in GAIA-L1-EXPERIMENT.md.
What a grammar guarantees, and what it does not
This is the honest part, and it is important to get right.
A GBNF grammar guarantees:
- The output is valid JSON, or whatever structure you defined.
- The tool-call array parses every time, with no retry loop.
- The model cannot invent a call shape that breaks your parser.
- The
toolvalue is a name that exists, because the names are enumerated. - An entire class of failures, parse errors and hallucinated formats, is removed at the source.
A GBNF grammar does not guarantee:
- That the model picked the right tool.
- That the arguments are semantically correct.
- That the plan makes sense.
- That the model will stop when it should rather than calling one more tool.
In other words, the grammar enforces valid structure, not correct semantics. The model can still emit a perfectly well-formed call to the wrong tool, or pass a valid-but-wrong argument: fs.read on a path that does not exist, browser.click on a selector that matches nothing, a finish before the task is done. Every one of those is grammatically flawless and behaviorally wrong.
Constraining structure does not make the model smarter. It removes the noise so that the model’s actual reasoning is what gets tested, not its JSON luck. That is the whole point: you want the model spending its capacity on what to do, not on remembering to close a brace.
It also changes what a failed run tells you. Before grammars, a broken agent run was ambiguous, because you could not tell a reasoning failure from a formatting failure. After, every failure is a reasoning failure, which is a much more useful thing to debug.
Performance overhead and where it shows up
Grammar-constrained decoding is not free, but the cost is small and lands in a predictable place.
The work happens between the forward pass and the sample. The grammar engine tracks its position in the state machine, computes which tokens are permissible from that position, and masks the rest of the logits. That is bookkeeping over the vocabulary, not another forward pass, so it is measured against a model inference that is already the dominant cost. On a local setup where a single step takes tens of seconds, the masking is not what you notice.
Two things do cost more than the average case:
Grammar compilation happens once when the grammar string is parsed into a state machine. For a grammar the size of a full tool-call definition with every tool family enumerated, this is a startup cost, not a per-token one. It matters if you rebuild the grammar on every request; it does not if you build it when the tool list changes.
Large enumerations widen the mask computation. A grammar that permits any of a hundred tool names has more branches to evaluate at the position where the name begins than one with five. This is why the tool names are grouped into families rather than flattened into one giant alternation.
The comparison that matters is not against unconstrained decoding, it is against the alternative that actually achieves the same reliability. A retry loop pays a full generation pass per failure. At even a 5 percent malformed rate, that is far more expensive than masking logits on every token, and it is unbounded: nothing promises the second attempt parses either.
There is a second saving that is easy to overlook. A constrained model does not spend output tokens on the preamble and the apology and the explanation wrapped around the JSON, because none of that is reachable. Those tokens were never free: each one was a forward pass. Removing them shortens every step, and it keeps the context that carries into the next step smaller, which matters in a loop where every tool result is also competing for that window.
Limits and failure modes
A grammar is a strong guarantee inside a narrow scope. Outside that scope, things it does not cover:
The model can still get stuck against the rails. If the model badly wants to write prose and the grammar only permits JSON, you can get degenerate output: a well-formed call with an empty or nonsense argument, because the model had to emit something and the grammar picked the shape. The output parses. It is still useless. This usually signals a prompt problem, not a grammar problem.
Grammars do not enforce cross-field logic. A grammar can say args is an object. It cannot say “if tool is fs.write then args must contain path and content.” Expressing per-tool argument schemas in the grammar is possible in principle and gets unwieldy fast, which is why argument validation lives in the runtime instead.
A dynamic grammar can lag the registry. MCP tools are stitched in at build time, and before the dynamic list is available the fallback shape is permissive. In that window the model could emit a name the registry does not know. The registry rejects it, so the failure is clean, but it is a rejection rather than an impossibility.
Very long outputs still drift semantically. Nothing about grammar constraint prevents the model from calling the same tool sixteen times in one batch with slightly different arguments, all of them syntactically perfect. Step budgets and result compression exist for that reason.
Grammar support is a runtime feature. GBNF is a llama.cpp capability, so the guarantee is a property of the stack you set up to run offline, not of the weights. Run the same weights through a stack that does not implement it and the guarantee disappears entirely, along with any assumption you built on top of it. This is the most common cause of “the same model works in Atomic Agent and breaks elsewhere.”
Frequently asked questions
Bottom line
If you are running a quantized model as an agent, grammar-constrained decoding is not an optimization, it is the thing that makes the setup viable at all. Everything else on the list either needs a specific model, a specific vendor, or a retry budget.
Key takeaways:
- GBNF is llama.cpp’s native grammar format, passed to the server as a
grammarfield and applied during sampling - At each decoding step the grammar masks every token that would break the structure, so malformed JSON is unreachable rather than rejected
- Quantization degrades format adherence faster than reasoning, which is why small local models need this and hosted frontier models mostly do not
- Atomic Agent roots its grammar at an array and enumerates tool names by family, so parallel batches are free and invented tool names are unreachable
- A grammar guarantees structure, not semantics: the model can still choose the wrong tool, it just cannot emit an unparseable one
Want to run a grammar-constrained local agent yourself? Start with the quickstart.