LSP and Agents: Two Different Models of Understanding Code
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.
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 server | Coding agent | |
|---|---|---|
| Model of the code | Parsed AST, resolved symbols, type graph | Text in a context window |
| Coverage | The whole project, indexed | Whatever it has read this session |
| Correctness | Deterministic; correct or it errors | Probabilistic; plausible or not |
| Cost of a query | Microseconds, local | Tokens and latency, remote |
| Failure mode | Says it does not know | Invents 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.
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.
| Question | Without index | With index |
|---|---|---|
Where is process_payment defined? | Grep, then read 2–3 files: ~4,000 tokens | One line: ~30 tokens |
| Who calls it? | Grep, read every match: ~10,000 tokens | A list: ~200 tokens |
| What type does this return? | Read the function and its imports: ~2,000 | One 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.
| Query | Grep returns | Index 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 | Nothing | Resolved 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:
- Uninstalled dependencies. Imports do not resolve, so half the symbol graph is missing. Everything downstream degrades to guessing.
- Wrong interpreter or SDK. Types resolve against the wrong standard library, producing answers that are subtly wrong rather than absent.
- Generated code not generated. Protobuf or ORM stubs that exist in CI but not locally leave a hole exactly where the data model lives.
- Index not finished. On a large repository the first minutes after opening return incomplete results with no indication they are incomplete.
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:
- Agent proposes a change — judgement, which it is good at.
- Language server reports diagnostics — facts, which it is exact about.
- Agent reads the errors and revises.
- 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.
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.