Tooling & Integration

LSP and Agents: Two Different Models of Understanding Code

David Guzenburg/ / 8 min read

Both appear to understand your codebase. One has a resolved type graph, the other has whatever it read five minutes ago — which is why only one of them invents functions.

LSPlanguage serverstoolingtoken budget

Two systems that look similar and are not

An editor with a language server and an editor with a coding agent both appear to "understand your code". They arrive at that understanding by completely different routes, and the difference explains a lot of otherwise puzzling behaviour — why an agent confidently references a function that does not exist, and why a language server never does.

Language serverCoding agent
Model of the codeParsed AST, resolved symbols, type graphText in a context window
CoverageThe whole project, indexedWhatever it has read this session
CorrectnessDeterministic; correct or it errorsProbabilistic; plausible or not
Cost of a queryMicroseconds, localTokens and latency, remote
Failure modeSays it does not knowInvents something reasonable

The last row is the one that matters operationally. A language server asked for a definition that does not exist returns nothing. An agent asked the same question will frequently produce a confident, well-formed answer about a function it has inferred from naming conventions.

What the language server actually knows

LSP servers build a semantic model: parse to an AST, resolve imports, compute types, index every symbol and reference. Queries against that model are lookups, and the answers are ground truth about the code as written.

That is why "go to definition" never guesses. There is no guessing available — either the symbol resolves or it does not.

The limitation is that the model is entirely syntactic and semantic. A language server can tell you every caller of a function. It cannot tell you whether calling it from the billing module is a layering violation, because that is not a fact about the type graph.

What the agent knows

An agent has no persistent index. It has a context window containing files it has opened this session, plus statistical knowledge of code in general from training.

That combination is why agents are strong at things language servers cannot touch — explaining intent, proposing a refactor, writing a test for behaviour rather than shape — and unreliable at things language servers are exact about, like whether a symbol exists.

The practical rule

Facts about the code as written: trust the language server. Judgements about what the code should be: use the agent. Trouble arrives when an agent is asked for a fact it can only guess at.

Giving the agent access to the index

The useful architecture is not choosing between them. It is letting the agent query the language server, so that questions with exact answers get exact answers.

find_definition(symbol)      → exact file and line, or "not found"
find_references(symbol)      → every call site, complete
get_type(expression)         → resolved type
list_symbols(file)           → everything defined, with signatures
get_diagnostics(file)        → current errors and warnings

Three things improve at once. Answers become correct rather than plausible. Token cost drops sharply — a symbol lookup returns one line instead of requiring the agent to read five files. And "not found" becomes a real signal the agent can act on, rather than a gap it fills by inventing.

The token argument on its own

Even setting correctness aside, the economics favour the index.

QuestionWithout indexWith index
Where is process_payment defined?Grep, then read 2–3 files: ~4,000 tokensOne line: ~30 tokens
Who calls it?Grep, read every match: ~10,000 tokensA list: ~200 tokens
What type does this return?Read the function and its imports: ~2,000One line: ~20 tokens

Those savings compound across a session, and they buy something better than speed: the tokens go back into the budget for code the agent actually needs to reason about, rather than code it read merely to answer a lookup.

Why grep is not a substitute

Agents without an index fall back on text search, and it is worth being precise about why that is worse rather than merely slower.

QueryGrep returnsIndex returns
process_payment Definition, calls, a comment, a string literal, a test name, a similarly-named method on an unrelated class The definition; separately, the resolved call sites
Callers of an interface method Nothing useful — callers use the interface name, not the impl Every implementation and every caller
A dynamically dispatched call NothingResolved through the type graph

Row two is where text search fails outright rather than noisily. In any codebase using interfaces, dependency injection or polymorphism, the relationship between a call site and the code that runs is not textual. An agent relying on grep in that codebase is not getting an incomplete answer; it is getting a confidently wrong one, and it has no way to know.

The noise in row one matters too, in a way that is easy to miss: every false match the agent reads costs context budget. A grep returning forty hits across twelve files can consume more of the window than the actual task.

Keeping the index healthy

If the language server is the ground truth an agent converges against, a broken language server is worse than none — it returns wrong answers confidently instead of no answers honestly.

Four things silently degrade it, and all are common:

This produces a genuinely counterintuitive piece of advice: if agent output in a repository is unexpectedly poor, check that your editor's language server is working before you change anything about your prompts or context file. A significant fraction of "the agent is bad at this codebase" turns out to be an unresolved import path.

Where the two genuinely combine

The strongest pattern is a loop that alternates between them:

  1. Agent proposes a change — judgement, which it is good at.
  2. Language server reports diagnostics — facts, which it is exact about.
  3. Agent reads the errors and revises.
  4. Repeat until diagnostics are clean.

This is why agents integrated with a working type checker produce noticeably better output than those without. The type checker is a source of ground truth the agent can iterate against, and it converts a one-shot guess into a converging process.

It also explains a common frustration. An agent working in a codebase where the language server is misconfigured — missing dependencies, unresolved imports, a broken environment — loses its ground truth and falls back on plausibility. Fixing your editor's language server setup measurably improves agent output, which is not an obvious connection until you see the mechanism.

Takeaway

A language server holds a resolved model of the code and fails by saying it does not know. An agent holds text and fails by inventing. Give the agent tools backed by the index so that factual questions get factual answers — and keep your language server healthy, because it is the ground truth the agent converges against.

Keep reading
Tooling & Integration

How Deep Is Your LSP Integration? Three Probes That Tell You

Four levels of language-server integration, three five-minute probes that reveal which one you have, and how to reach query access yourself when the tool does not provide it.

Tooling & Integration

Building a Local MCP Server: Exposing What the Filesystem Cannot Answer

A working TypeScript MCP server over stdio, why the tool description decides whether it is ever called, returning errors the model can read, and the read-only role to create before it runs.

Tooling & Integration

Giving Claude a Second Opinion: Connecting Gemini Through a Local MCP Server

A working walkthrough for wiring another model into Claude Code as a tool: the server code, the registration command, the flag ordering that breaks it, and why the context parameter is the part people get wrong.

Tooling & Integration

Context Caching and Reuse: What Survives Between Sessions

Three unrelated mechanisms people call caching, how to order prompts so prefix caching works, and the distinction between caching understanding and caching state.

← Orchestrating Parallel Agent Sessions Without Corrupting Your Repo  ·  Normalising Agent Commits: The Only Surviving Record of Intent →

All tooling & integration articles  ·  Every article