The agent reaches for the tool it can read
You optimize a tool for what it can do. An agent selects a tool for whether it can predict the result and read the answer when it comes back empty. Those are different properties, and the distance between them is why the more capable tool is so often the one that goes untouched.
I give the coding agents I run a language server. It can jump to a definition, find the references to a symbol, walk a call graph. Most of the time the agent ignores all of it and runs grep.
For a while I read that as the agent settling for less, and I kept meaning to nudge it toward the better tool. Then I looked at what grep actually gives it. grep answers a smaller question, but it answers in a form the agent can use, and that turns out to be the property that decides which tool gets called. The capability I was proud of was never the thing being weighed.
The tool you build for an agent and the tool it reaches for are two different objects. You build the first and optimize it for capability: the more it can do, the more places it can look, the richer its answers. The agent selects the second on an axis you were not designing for. Can it predict what the result will look like before the call, and can it read the result when the result comes back empty. That is not the whole story of tool choice, and I will come back to what else is in it, but it is the half a tool author keeps ignoring while pouring everything into capability.
There is a simpler explanation for the whole pattern, and it deserves a hearing before I lean on mine. grep appears in the model's training more times than anything I could build, and a bespoke tool appears close to never. Maybe the agent reaches for grep because it is familiar, not because it is legible, and I am dressing up a popularity contest as a principle. The honest answer is that the two are tangled, and a single observation cannot pull them apart. But they do come apart in one case, and it is the case worth watching: a brand-new tool the agent has never seen, whose result has a rigid, self-describing shape, set against a well-worn tool whose output is mush. If familiarity were the whole story, the new tool would stay untouched until it got famous. If legibility is doing real work, the new tool earns the reach the first time its result proves readable. I cannot run that experiment cleanly from where I sit, so I hold the causal claim loosely. What I do not have to hold loosely is which half I control. I cannot make my tool more familiar to a model already trained. I can make its result legible. The lever the tool author actually has is the one this argument is about.
An empty grep means one thing
When grep finds nothing, it exits with status 1 and prints nothing. That is the whole event. The empty output has exactly one reading: the pattern is not in the files it searched. When grep instead cannot look, a missing path, a bad regular expression, it says so on stderr and exits 2. So there are two ways to come back empty, and they arrive as two different numbers in the same small place the caller already checks. Silence is reserved for absence. The reason rides home alongside the result, which is exactly what the richer tools fail to do.
That legibility holds only for a caller that reads the exit status; a harness that keeps stdout and drops the status hands itself back the ambiguity grep avoided. The information is there, which is already more than the tools ahead can promise, but it counts only if it survives to the decision.
The richer tool's empty answer does not
Ask a language server to go to a definition and the answer is one location, several, or null. "A response return value of null indicates no result," the specification says. Read the type and you see the gap: null is a shape with no room for why. It is the absence with the reason stripped off.
The spec is not naive about this. It has error codes for the states it can name cleanly: ContentModified when the document shifted under an in-flight request, ServerNotInitialized for a request that arrived before the startup handshake. What it does not do is force the state a coding agent actually collides with, a server that is up, past its handshake, and still building its index, onto any of them. A not-ready server is free to answer a definition query with a plain null, and nothing in the protocol stops it. So the fact that would let an agent tell "there is no definition" from "I was not ready to look" is not guaranteed to be recorded anywhere. Checking for an error object does not rescue it, because the not-ready server never raised one. The messy state in the middle lands on whoever is reading the null, and what they are reading cannot carry its own cause.
Seen that way the agent reaching past the language server is not lazy. It is refusing to build on a signal it cannot read, and reaching for one it can, which is the correct instinct about the signal. It is not always correct about the task: grep cannot tell a symbol from a same-named string in a comment, cannot honor scope, cannot drive a safe rename the way a language server can, and a legible answer to the wrong question is just a confident mistake. The reach is right about what it can trust, not about what the job needs, and a good tool author owes the agent both.
The tools we build for agents have the same hole
The shape shows up one layer out, in the servers we stand up for agents on purpose. A Model Context Protocol tool returns a list of content blocks, and by default those blocks are unstructured, most often a lump of text. The spec added an optional output schema and a structured-content field so a tool can hand back something with a guaranteed shape, but the shape is opt-in, so most tools return text the model has to parse afresh each call. I have built the illegible one myself: a small server that answered correctly every time and in a differently arranged paragraph every time, so that reading the answer back was its own little parsing job.
Failure, oddly, is the part MCP gets closer to right than the language server does. A protocol error covers an unknown tool or a malformed request, and a failure inside the tool comes back as isError: true, riding inside a normal result in the one place the agent is already looking. What MCP leaves open is the other half, the shape. The flag says a call failed; the reason for the failure arrives as the same free text as everything else, a legible signal wrapped around an illegible cause. Capability and legibility are separate investments, and we keep paying for the first while assuming the second comes with it.
The cost that survives deferred loading
It would be easy to file this next to the token tax, and it is a different bill. That one is paid before the call, on the schema every loaded definition parks in the context window. This one is paid after the call, on the result, and it survives every fix for the first. Defer the language server's definition so it costs nothing until the agent asks for it, and the agent still skips it, because the reason it skips it was never the schema in the prompt. It was the answer it would get back.
The token-tax post has a selection argument of its own, that a big enough catalog degrades the model's aim across many tools. That one is about quantity, discrimination among crowded options. This is about the single tool's result contract, which loses even when it is the only tool on the bench. The two do not overlap; a legibly-shaped tool wins whether it has company or not.
This is the third time I have run into an absence with two readings. A pager cannot tell a job that failed from one that was stopped on purpose; a watchdog cannot tell a working agent from a wedged one; a null cannot tell a definition that does not exist from one the server was not ready to find. The settings differ and so do the repairs, but the shape is one shape: an emptiness cannot carry its own cause, so unless something records that cause where the reader will look, the reader is left to infer it, and the two causes infer alike.
Give the empty answer a reason
So the fix for a tool the agent ignores is almost never the one that gets proposed first. The instinct is to make it more capable, or to write it a better description so the model finally understands what it is for. Neither touches the reason it goes unused. A tool earns the reach when its result has a shape the agent can predict and its empty answer carries its own cause, not just "nothing," but which nothing. That is the difference between a bare null and a result that names its own emptiness:
{ "definitions": [], "reason": "not_indexed" }with no_definition, not_indexed, and out_of_scope as distinct values the agent reads in the one place it is already reading, so "there is none" and "I could not look" can never wear the same face. Collapse the reason into the channel that carries the result, the way an exit code rides home beside stdout, and stop making the agent cross-check a second object, or guess, to find out whether the first one meant what it said. That is duller work than shipping a feature, and it is the work that gets the tool called.
The tool you built is measured by what it can do. The tool the agent reaches for is measured by whether the agent can tell what just happened when it called it. Those are not the same tool, and until you build the second one, the first answers in a shape the agent cannot read, while the agent runs grep.