Building a TypeScript LSP Self-Correction Loop with OpenCode — AI That Catches Its Own Type Errors
Anyone who has used AI coding agents in practice eventually hits the same wall. The agent generates code that looks plausible, but running tsc spits out a stream of type errors. So you copy the error messages, paste them into the chat, get back corrected code, run it again, paste more errors... I accepted this workflow as normal at first, until one day I thought, "Could the agent handle this on its own?" That's when I discovered the concept of the LSP-integrated agent.
The idea is simple: take the mechanism that gives you red underlines in real time when you open a TypeScript file in VS Code — the Language Server Protocol (LSP) — and wire it directly into the agent's execution loop. The moment the agent edits a file, the LSP server emits diagnostics, which are automatically injected into the next LLM context, forming a self-correction loop where the model fixes errors on its own. The tool that lets you set up this loop quickly without separate infrastructure is OpenCode, an open-source terminal agent written in Go by the SST team. It is one of the few agents that ships with LSP connectivity built in, bundling more than 30 language servers out of the box.
The importance of this loop can be confirmed with numbers. According to Anthropic's 2026 Agentic Coding Trends Report, on SWE-bench Verified, differences in agent scaffolding design alone — with the same model — can produce a gap of up to 17 problems. The fact that this kind of gap is possible without changing the model is what made me invest time in this one configuration.
Core Concepts
AI Agents and LSP: Why the Connection Matters
The red underlines that appear in real time when you open a TypeScript file in VS Code exist because of LSP. The editor communicates with a separate process called typescript-language-server via JSON-RPC (a lightweight messaging format for editor-server communication), sending file change events and receiving compiler-level diagnostics in return.
The problem is that most existing AI coding agents don't have this feedback channel inside their execution loop. An agent generates text and writes it to a file, but has no immediate way to know whether the result is correct from the type system's perspective. The result is a slow cycle of "run it, check for errors" repeated over and over.
💡 An LSP-integrated agent is an architecture that connects the Language Server Protocol into an AI agent's execution loop, enabling the agent to receive compiler-level diagnostics (type errors, undefined references, unused imports, etc.) in real time immediately after editing a file, and to correct itself accordingly.
How the Self-Correction Loop Works
The loop implemented by OpenCode repeats the following steps until errors are gone:
Edit
↓
Collect LSP diagnostics (150ms debounce + deduplication)
↓
Reconstruct LLM context (inject diagnostics)
↓
Re-edit
↓
No errors? → Done
Errors remain? → Edit again (stop after max N iterations)The 150ms debounce gives the LSP server time to stabilize right after a file is saved. I glossed over this detail at first, but if you skip this wait in a custom pipeline, incomplete analysis results get mixed into the context and quite a few noisy diagnostics sneak in. The maximum iteration limit is also an important part of the loop design — without it, you can end up in an infinite loop where fixing error A produces error B, fixing B brings A back, and so on.
Here's what diagnostic injection into the LLM context actually looks like. The raw diagnostic returned by LSP is in JSON:
{
"uri": "file:///workspace/src/utils.ts",
"diagnostics": [
{
"range": { "start": { "line": 6, "character": 13 } },
"severity": 1,
"code": "TS2339",
"message": "Property 'displayName' does not exist on type 'User'. Did you mean 'name'?"
}
]
}OpenCode converts this into a natural-language form that the agent can easily read and inserts it into the next prompt:
[LSP Diagnostic] src/utils.ts line 7
error TS2339: Property 'displayName' does not exist on type 'User'.
Did you mean 'name'?The model receives this diagnostic as context and corrects user.displayName to user.name in the next edit. Saving the file again triggers LSP re-diagnosis, and if there are no errors the loop terminates.
Major LSP Server Options
| Server | Package | Notes |
|---|---|---|
| typescript-language-server | typescript-language-server |
Official TypeScript/JS LSP server |
| vtsls | @vtsls/language-server |
Based on VS Code TypeScript server, high performance |
| tsgo | (next-gen) | TypeScript Go reimplementation, LSP-compatible (experimental) |
With just lsp: true in your configuration, OpenCode automatically selects and starts the appropriate server based on file extension. TypeScript files get the TypeScript server; .py files get the Python server.
Practical Application
What the Loop Actually Does: Before / After
Before getting into configuration, let's look at what the loop actually does. Suppose you have code with a type error like the one below.
Before — agent-generated code (with type error):
interface User {
id: number;
name: string;
}
function getUserLabel(user: User): string {
return user.displayName; // non-existent property
}LSP detects this state immediately and injects the following diagnostic into the agent context:
[LSP Diagnostic] src/utils.ts line 7
error TS2339: Property 'displayName' does not exist on type 'User'.
Did you mean 'name'?The agent sees this diagnostic and corrects itself in the next edit:
After — one self-correction loop cycle later:
function getUserLabel(user: User): string {
return user.name; // auto-corrected based on LSP diagnostic
}When LSP re-diagnoses and finds no errors, the loop terminates. The key point is that this entire process runs automatically inside the agent without any manual intervention.
Example 1: Auto-Configure with a Single lsp: true Line
This is the fastest way to get started. Create opencode.json at the project root and add just this one line:
{
"$schema": "https://opencode.ai/config.json",
"lsp": true
}This causes OpenCode's 30+ built-in LSP servers to start automatically based on file extension. For a TypeScript project, you don't need to install a separate server or specify a path.
| Setting | Behavior |
|---|---|
"lsp": true |
Automatically activates all built-in servers |
On accessing .ts/.tsx files |
TypeScript LSP server starts automatically |
| No separate install needed | OpenCode manages the required servers automatically |
⚠️ Note: Configuration can fail silently, so it's worth checking the OpenCode execution log for a message like
typescript-language-server started. If you don't see it, move to the explicit configuration in the next example.
Example 2: Fine-Tuning with vtsls
Explicit configuration is useful when you want direct control over import path style or diagnostic strictness. First, install the server globally:
pnpm install -g @vtsls/language-server typescriptThen configure opencode.json as follows:
{
"$schema": "https://opencode.ai/config.json",
"lsp": {
"typescript": {
"command": "vtsls",
"args": ["--stdio"],
"initialization": {
"preferences": {
"importModuleSpecifierPreference": "relative"
}
}
}
}
}initialization is an OpenCode-specific key name, distinct from the LSP standard's initializationOptions. Setting importModuleSpecifierPreference to "relative" makes the agent prefer relative paths over absolute paths when generating imports. You can switch to "non-relative" or "shortest" depending on your team's conventions.
Example 3: Isolated Environment with Docker
When running LSP on untrusted repos or in CI pipelines, using an isolated environment is the safer choice. You can use the blackwell-systems/agent-lsp image:
docker run --rm -i \
-v /your/project:/workspace \
ghcr.io/blackwell-systems/agent-lsp:typescript \
typescript:typescript-language-server,--stdioA quick breakdown of each flag:
-v /your/project:/workspace— mounts the host project directory to/workspaceinside the container--stdio— specifies that the LSP server communicates over standard I/O (stdin/stdout)
This approach avoids installing global packages on the host environment and contains the risk of arbitrary command execution by the LSP server inside the container, offering a security advantage.
💡
agent-lspis a runtime that provides a standardized container environment for AI agents to communicate with LSP servers. It supports 30 languages per the official README.
Pros and Cons
Advantages
| Item | Details |
|---|---|
| Error elimination before compilation | Catches type errors autonomously before running, drastically reducing manual feedback cycles |
| Multi-file refactoring quality | Meaningful quality difference versus agents without LSP feedback during multi-file refactors |
| IDE-level code intelligence | Definition navigation, reference tracking, autocomplete, and other IDE features can be integrated into the agent loop |
| Simple configuration | One line of lsp: true gets 30+ built-in servers running automatically |
| Harness design determines outcomes | Scaffolding differences alone can produce a 17-problem benchmark gap with the same model |
Benchmark figures source: Anthropic 2026 Agentic Coding Trends Report, SWE-bench Verified
The table looks rosy, but there are a few pitfalls you need to know about when applying this in practice.
Drawbacks and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Poor monorepo support | Bug where TypeScript LSP doesn't activate if there's no package.json + node_modules at the root (issue #16335) |
Run OpenCode from within each workspace package directory, or place separate opencode.json files per package |
| Diagnostic surfacing issue | In some configurations, TypeScript diagnostics aren't exposed to the agent context when using general file tools like read/patch (issue #16880) |
Prefer OpenCode's built-in editing tools; track the issue for a fix in an upcoming version |
| Infinite correction loop risk | Possible cycle: fix error A → error B appears → fix B → A recurs | Set an upper bound on loop iterations; review the underlying type design and retry |
| Security risk | Arbitrary shell commands can be injected into the LSP command array in opencode.json |
Always inspect the contents of opencode.json from untrusted repos before opening |
| Context cost | More diagnostics means more LLM context consumed | Filter diagnostics (errors only, exclude warnings) or restrict file scope |
The Most Common Mistakes in Practice
-
Turning on
lsp: trueand never verifying the server is actually running — Check the OpenCode execution log for a message liketypescript-language-server started. Seeing that message means the loop is properly activated. Running the agent while it's silently disabled is no different from the old workflow without LSP feedback. -
Attributing infinite loops solely to the model — When error A and error B cycle back and forth, it's usually a problem with the type design itself. Rather than repeatedly telling the agent to "just fix the errors," if the loop has run 3 or more times, revisiting the underlying type structure is far more efficient.
-
Running OpenCode from the root in a monorepo environment — Due to a known bug, TypeScript LSP goes silent if there's no
node_modulesat the root. Until issue #16335 is resolved, running OpenCode from within each workspace package directory or placing separateopencode.jsonfiles per package works well.
LSAP: The Next Step for Self-Correction Loops
Today, LSP provides atomic operations designed for editors (open file, close file, edit). What agents need is somewhat different — an interface for issuing high-level queries like "find all usages of this function" or "which files are affected if I change this symbol?"
LSAP (Language Server Agent Protocol) is an open protocol under discussion aimed at filling exactly this gap. Its goal is to abstract the operations agents need to understand a codebase — definition exploration, reference tracking, edit impact analysis, and more. As of 2026 it is in active discussion, and an interesting question is how the diagnostic injection approach covered in this article would change under LSAP.
Closing Thoughts
The first time I watched the loop run in person — seeing the agent read a type error, correct itself, receive re-diagnosis, and terminate — was more surprising than I expected. That feeling of oh, this actually works.
Wiring LSP into the agent loop is one of the most reliable ways to raise AI coding quality without changing the model.
Three steps you can take right now:
-
Create
opencode.jsonat the root of your TypeScript project and add{ "lsp": true }. This alone activates the built-in TypeScript LSP server automatically, and the self-correction loop kicks in whenever OpenCode edits a.tsfile. -
Check the OpenCode execution log to confirm
typescript-language-serverstarted successfully. If you see that message, the LSP connection is active. If not, install the server globally withpnpm install -g @vtsls/language-server typescriptand switch to explicit configuration as in Example 2. -
Ask the agent to fix code that intentionally contains type errors. The diagnostic messages being injected into context and the agent correcting itself will appear in the logs. You should be able to watch the error count drop with each loop cycle.
References
- Language Server Protocol Integration | sst/opencode | DeepWiki
- LSP Servers | OpenCode Official Docs
- Config | OpenCode Official Docs
- TypeScript diagnostics are not surfaced to agent/tool context · Issue #16880
- FEATURE: LSP support for TypeScript monorepos · Issue #16335
- Give Your AI Coding Agent Eyes: How LSP Integrations transform Coding Agents
- LSP — The Protocol Your IDE Uses Every Day (And Now Your AI Agent Does Too)
- GitHub - lsp-client/LSAP: Language Server Agent Protocol
- GitHub - blackwell-systems/agent-lsp
- When "Read This File" Means "Run This Code": LSP Configuration in OpenCode - DEV Community
- OpenCode vs Claude Code (2026): Which AI Coding Agent?
- TypeScript Agent Frameworks in 2026: Loop, Runtime, Sandbox | EveryDev.ai
- Mastering OpenCode — Medium
- 2026 Agentic Coding Trends Report — Anthropic