28 KiB
adr_number, title, status_history, tier, authors, superseded_by, related_adrs, acceptance
| adr_number | title | status_history | tier | authors | superseded_by | related_adrs | acceptance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 27 | Language Server Protocol (LSP) Integration |
|
4 |
|
null |
|
|
Context
Actors in CleverAgents that perform software development tasks — writing code, refactoring, reviewing, debugging — operate on source code as raw text. Without semantic understanding of the code they manipulate, actors lack the ability to resolve types, navigate symbol definitions, discover references, identify compilation errors, or understand the structural relationships within a codebase. Human developers gain these capabilities through IDE features powered by language servers. Actors need the same language intelligence to work effectively.
Building bespoke code analysis for each programming language would be prohibitively expensive and would perpetually lag behind the state of the art in language tooling. The Language Server Protocol (LSP) is an open standard that decouples language intelligence from any specific editor or tool — it defines a JSON-RPC interface through which any client can request diagnostics, type information, symbol navigation, completions, and more from a language-specific server process. The ecosystem already provides production-quality language servers for virtually every mainstream language (Pyright for Python, typescript-language-server for TypeScript/JavaScript, clangd for C/C++, rust-analyzer for Rust, gopls for Go, etc.).
CleverAgents should leverage this ecosystem directly, giving actors the same rich language intelligence that human developers enjoy — but integrated into the agent's reasoning loop and tool-calling pipeline rather than into an IDE.
Decision Drivers
- Actors performing software development tasks need semantic code understanding (type resolution, symbol navigation, diagnostics) beyond raw text manipulation
- Building bespoke code analysis per language is prohibitively expensive; the LSP ecosystem already provides production-quality servers for virtually every mainstream language
- Language intelligence must integrate into the actor's reasoning loop and tool-calling pipeline, not into an IDE presentation layer
- Actors must be able to work across multiple programming languages without per-language configuration when possible
- LSP capabilities must be available both as explicit tools (on-demand queries) and as automatic context enrichment (injected diagnostics and type information)
Decision
CleverAgents adopts LSP as the standard protocol for attaching language intelligence capabilities to actors and agents. LSP servers are registered in a global LSP Registry (namespaced like all other entities), and bound to specific actor graph nodes via YAML configuration. LSP capabilities are exposed to actors as tools (via an LSPToolAdapter) and as context enrichment (automatic injection of diagnostics and type information into the actor's ACMS context). This integration lives in the Infrastructure layer, not the Presentation layer — LSP serves the AI agents, not human users in editors.
Design
LSP Registry
Language servers are registered as first-class entities in a global LSP Registry, following the same patterns as the Tool Registry, Skill Registry, and Actor Registry.
Each LSP entry is:
- Namespaced using the standard
[[server:]namespace/]nameformat (e.g.,local/pyright,local/clangd,cleverthis/rust-analyzer). - Defined via YAML configuration and registered via
agents lsp add --config <file>. - Independently versioned and managed — language servers can be added, updated, listed, shown, and removed like any other registered entity.
LSP Configuration Schema
name: local/pyright
description: "Pyright language server for Python type checking and intelligence"
command: pyright-langserver
args: ["--stdio"]
transport: stdio # stdio (default) or tcp
languages: # Languages this server covers
- python
capabilities: # LSP capabilities this server provides
- diagnostics # textDocument/publishDiagnostics
- hover # textDocument/hover
- completions # textDocument/completion
- definitions # textDocument/definition
- references # textDocument/references
- rename # textDocument/rename
- code_actions # textDocument/codeAction
- formatting # textDocument/formatting
- signature_help # textDocument/signatureHelp
- document_symbols # textDocument/documentSymbol
- workspace_symbols # workspace/symbol
initialization: # LSP initialization options
python:
pythonPath: "${PYTHON_PATH:/usr/bin/python3}"
venvPath: "${VENV_PATH:}"
analysis:
typeCheckingMode: "basic"
autoSearchPaths: true
workspace_settings: # Per-workspace LSP settings
python.analysis.diagnosticMode: "workspace"
Key configuration fields:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Namespaced identifier (<namespace>/<name>). |
description |
string | No | Human-readable description. |
command |
string | Yes | Executable command to launch the language server. |
args |
list[string] | No | Command-line arguments for the server process. |
transport |
string | No | Communication transport: stdio (default) or tcp. |
languages |
list[string] | Yes | Programming languages this server provides intelligence for. |
capabilities |
list[string] | No | Explicit list of LSP capabilities. When omitted, capabilities are discovered from the server's initialize response. |
initialization |
object | No | LSP initializationOptions passed during the initialize handshake. Supports ${ENV_VAR} interpolation. |
workspace_settings |
object | No | LSP workspace configuration settings sent via workspace/didChangeConfiguration. |
Resource Language Discovery
Resources attached to a project represent codebases, file trees, and repositories that contain source code in one or more programming languages. The system determines what languages a resource contains through a multi-layered discovery process:
- File extension analysis — The most common and fastest method. File extensions (
.py,.ts,.rs,.go,.cpp, etc.) are mapped to language identifiers using a built-in extension-to-language table. - Content analysis — For files without extensions or with ambiguous extensions, content markers are inspected: shebang lines (
#!/usr/bin/env python3), magic comments, file headers, and structural patterns. - UKO classification — The Universal Knowledge Ontology (ACMS) classifies resources at the technology-specific layer (Python, TypeScript, Rust, etc.), providing semantic language identification that persists across sessions.
- Explicit project-level declaration — Projects may explicitly declare the languages they contain via configuration, overriding or supplementing automatic discovery.
Language discovery results are cached per resource and invalidated when the resource content changes (detected via file watchers or resource refresh).
Actor LSP Binding
Actors declare their LSP dependencies in their YAML configuration via the lsp: field. The binding model supports three levels of specificity, from fully explicit to fully automatic:
Explicit Binding (by server name)
The actor lists specific registered LSP servers by their namespaced names:
actors:
code_analyzer:
type: llm
config:
actor: anthropic/claude-3-opus
system_prompt: |
You are a Python code analyst. Use LSP tools to understand
type information and identify issues in the codebase.
skills:
- local/file-ops
lsp:
- local/pyright
- local/ruff-lsp
Language-Based Binding
The actor specifies languages and the runtime resolves the appropriate LSP servers from the registry:
actors:
polyglot_reviewer:
type: llm
config:
actor: openai/gpt-4
system_prompt: |
Review code for correctness and style issues.
lsp:
languages:
- python
- typescript
- rust
At activation, the runtime queries the LSP Registry for servers covering each declared language. If multiple servers cover the same language, the first registered match is used (or the user can disambiguate with explicit names).
Resource-Auto Binding
The actor requests automatic LSP resolution based on the languages present in the resources it is processing:
actors:
universal_developer:
type: llm
config:
actor: anthropic/claude-3-opus
system_prompt: |
You are a software developer. Use LSP capabilities to understand
the codebase you are working in.
lsp:
auto: true
When auto: true is set, the runtime inspects the project's bound resources via language discovery, determines which languages are present, and binds the appropriate LSP servers automatically. This is the most reusable pattern — an actor configured with lsp: { auto: true } works correctly regardless of the language of the project it is assigned to.
Jinja2 Dynamic Binding
For advanced use cases, Jinja2 templates can dynamically resolve LSP bindings at preprocessing time:
actors:
dynamic_analyst:
type: llm
config:
actor: openai/gpt-4
lsp:
{% for lang in context.project_languages %}
- {{ lsp_registry.for_language(lang) | first }}
{% endfor %}
Template variables available during LSP resolution:
| Variable | Type | Description |
|---|---|---|
lsp_registry |
object | The LSP Registry, supporting .for_language(lang) and .all() queries. |
context.project_languages |
list[string] | Languages detected in the current project's resources. |
context.resource_languages |
list[string] | Languages detected in the specific resource being processed. |
Per-Node Binding in Actor Graphs
Different nodes in an actor's graph can have different LSP configurations. This allows fine-grained control — for example, giving a strategy actor read-only diagnostics while giving an execution actor full LSP capabilities:
actors:
strategy_planner:
type: llm
config:
actor: openai/gpt-4
system_prompt: |
Plan the implementation strategy.
lsp:
- local/pyright
lsp_capabilities: # Restrict to read-only capabilities
- diagnostics
- hover
- definitions
- references
code_implementer:
type: llm
config:
actor: anthropic/claude-3-opus
system_prompt: |
Implement code changes according to the strategy.
lsp:
auto: true
lsp_capabilities: all # Full capabilities (default)
routes:
dev_workflow:
type: graph
entry_point: plan
nodes:
- name: plan
type: agent
agent: strategy_planner
- name: implement
type: agent
agent: code_implementer
edges:
- source: plan
target: implement
- source: implement
target: end
The lsp_capabilities field controls which LSP features are exposed to the actor as tools. When omitted or set to all, every capability declared by the bound LSP servers is available.
| Capability | LSP Method | Tool Name | Description |
|---|---|---|---|
diagnostics |
textDocument/publishDiagnostics |
lsp/diagnostics |
Retrieve compilation errors, warnings, and hints for a file |
hover |
textDocument/hover |
lsp/hover |
Get type information and documentation at a position |
completions |
textDocument/completion |
lsp/completions |
Get code completion suggestions at a position |
definitions |
textDocument/definition |
lsp/definition |
Go to the definition of a symbol |
references |
textDocument/references |
lsp/references |
Find all references to a symbol |
rename |
textDocument/rename |
lsp/rename |
Compute a rename refactoring across files |
code_actions |
textDocument/codeAction |
lsp/code-actions |
Get available code actions (quick fixes, refactors) |
formatting |
textDocument/formatting |
lsp/format |
Format a document according to language conventions |
signature_help |
textDocument/signatureHelp |
lsp/signature |
Get function signature information at a call site |
document_symbols |
textDocument/documentSymbol |
lsp/symbols |
List all symbols (classes, functions, variables) in a file |
workspace_symbols |
workspace/symbol |
lsp/workspace-symbols |
Search for symbols across the entire workspace |
LSP Capability Exposure
LSP capabilities reach actors through two complementary mechanisms:
As Tools (via LSPToolAdapter)
The LSPToolAdapter is an Infrastructure-layer adapter (analogous to MCPToolAdapter for MCP) that translates registered LSP server capabilities into CleverAgents tools. When an actor with LSP bindings is activated:
- The runtime resolves the actor's
lsp:configuration to concrete LSP server entries from the LSP Registry. - For each bound server, the
LSPToolAdaptergenerates tool definitions for each exposed capability. - These tools are injected into the actor's available tool surface alongside skill-provided tools.
- The actor can call LSP tools during its reasoning loop (e.g.,
lsp/diagnosticsto check for errors before committing a change,lsp/definitionto understand a symbol before modifying it).
Tool signatures follow a consistent pattern:
lsp/diagnostics(file_path: str) -> list[Diagnostic]
lsp/hover(file_path: str, line: int, column: int) -> HoverResult
lsp/definition(file_path: str, line: int, column: int) -> list[Location]
lsp/references(file_path: str, line: int, column: int) -> list[Location]
lsp/completions(file_path: str, line: int, column: int) -> list[CompletionItem]
lsp/rename(file_path: str, line: int, column: int, new_name: str) -> WorkspaceEdit
lsp/code-actions(file_path: str, start_line: int, start_col: int, end_line: int, end_col: int) -> list[CodeAction]
lsp/format(file_path: str) -> FormattedText
lsp/symbols(file_path: str) -> list[DocumentSymbol]
lsp/workspace-symbols(query: str) -> list[SymbolInformation]
lsp/signature(file_path: str, line: int, column: int) -> SignatureHelp
When multiple LSP servers are bound to an actor (e.g., local/pyright for Python and local/typescript-lsp for TypeScript), the tool adapter routes requests to the appropriate server based on the file's detected language. The tool names remain the same — the routing is transparent to the actor.
As Context Enrichment
In addition to explicit tool calls, LSP servers can automatically enrich the actor's context when code files are loaded into the ACMS hot context:
- Diagnostic injection: When a source file enters the actor's hot context, the LSP server's diagnostics for that file are automatically appended as structured annotations. This means the actor "sees" type errors, unused imports, and other issues without needing to explicitly call
lsp/diagnostics. - Type annotation overlay: Hover information for key symbols (function signatures, class hierarchies, variable types) can be pre-fetched and included as context metadata, giving the actor type-level understanding of the code it is examining.
Context enrichment is controlled by the actor's lsp_context_enrichment configuration:
actors:
enriched_reviewer:
type: llm
config:
actor: openai/gpt-4
lsp:
auto: true
lsp_context_enrichment:
diagnostics: true # Auto-inject diagnostics (default: true)
type_annotations: false # Auto-inject type info (default: false, can be expensive)
max_diagnostics_per_file: 50 # Limit to avoid context bloat
LSP Server Lifecycle
LSP servers are long-lived processes managed by the LSP Runtime in the Infrastructure layer:
-
Startup: When an actor with LSP bindings is activated (either for a plan phase or a direct
agents actor runinvocation), the LSP Runtime starts the required language server processes. Each server is initialized with theinitializeLSP handshake, passinginitializationOptionsfrom the registry entry and workspace root paths mapped from the actor's bound resources. -
Workspace Mapping: LSP workspace roots are mapped to the physical paths of registered resources. When the actor operates within a sandbox (Execute phase), the sandbox's working directory is used as the workspace root, ensuring the language server analyzes the sandboxed code — not the original.
-
File Synchronization: As the actor reads and modifies files (via tools), the LSP Runtime sends
textDocument/didOpen,textDocument/didChange, andtextDocument/didClosenotifications to the language server, keeping the server's view of the codebase synchronized with the actor's mutations. -
Shared Instances: If multiple actors in the same plan or session require the same language server for the same workspace, the LSP Runtime shares a single server instance. Reference counting ensures the server stays alive as long as at least one actor needs it.
-
Shutdown: When the actor deactivates (plan phase completes, session ends, or direct run finishes), the LSP Runtime sends the
shutdownandexitlifecycle messages. Shared instances are shut down only when the last referencing actor deactivates.
LSP in the Plan Lifecycle
LSP integration enhances each phase of the plan lifecycle:
| Phase | LSP Role | Example Use |
|---|---|---|
| Strategize | Read-only intelligence — diagnostics, type information, and symbol navigation inform strategy decisions | Strategy actor calls lsp/diagnostics to assess codebase health, lsp/symbols to understand module structure, lsp/references to gauge impact of proposed changes |
| Execute | Full intelligence — all capabilities available for code generation and modification | Execution actor calls lsp/definition to understand APIs before using them, lsp/diagnostics to verify changes compile, lsp/rename to perform safe refactorings |
| Apply | Validation intelligence — diagnostics confirm the final changeset is clean | Apply-phase validations invoke lsp/diagnostics on all modified files to ensure no regressions before merge |
Comparison with MCP
LSP and MCP are both standard protocols for external capability servers, but they serve different purposes:
| Aspect | MCP (Model Context Protocol) | LSP (Language Server Protocol) |
|---|---|---|
| Purpose | General-purpose tool discovery and invocation | Language-specific code intelligence |
| Scope | Any callable operation (file I/O, API calls, data queries) | Code analysis capabilities (diagnostics, navigation, completions) |
| Registry | Tool Registry (via MCPToolAdapter) |
LSP Registry (via LSPToolAdapter) |
| Binding | Skills compose MCP tools; actors acquire via skill references | Actors bind LSP servers directly via lsp: configuration |
| Tool generation | Tools pre-defined by MCP server, registered at discovery | Tools generated dynamically from LSP server capabilities at activation |
| Lifecycle | Managed by MCP SDK connection lifecycle | Managed by LSP Runtime with workspace mapping and file synchronization |
Both adapters follow the same architectural pattern: a standard protocol server in the Infrastructure layer is bridged into the actor's tool surface through a typed adapter. The key difference is that MCP tools are general-purpose and explicitly authored, while LSP tools are language-specific and automatically derived from the protocol's capability model.
Constraints
- LSP servers must be registered in the LSP Registry before they can be referenced by actors. There is no inline LSP server definition.
- Actor LSP bindings are resolved at actor activation time — unresolvable bindings (missing registry entry, no server for a requested language) produce a clear error.
- LSP tools must not modify resources outside the sandbox boundary during Execute phase. Mutation operations (rename, code actions that apply edits) must route through the sandbox.
- The LSP Runtime must handle language server crashes gracefully — restarting the server and re-synchronizing file state without disrupting the actor's execution.
- LSP servers must operate within the same security boundary as the actor. Server processes run with the same permissions as the CleverAgents process.
- Context enrichment (automatic diagnostic/type injection) must respect the actor's context budget — diagnostic results are truncated if they would exceed the hot context token limit.
Consequences
Positive
- Actors gain the same rich language intelligence that human developers use in IDEs, dramatically improving code quality in generated output.
- The LSP ecosystem provides production-quality language servers for virtually every mainstream language — CleverAgents benefits from years of community investment without building custom analyzers.
- Language-agnostic actor definitions (using
lsp: { auto: true }) work correctly across any project language, maximizing actor reusability. - Per-node LSP binding gives fine-grained control over which actors in a graph receive which capabilities, enabling cost-conscious configurations (e.g., diagnostics-only for strategy, full capabilities for execution).
- The adapter pattern (
LSPToolAdapter) mirrors the establishedMCPToolAdapterpattern, maintaining architectural consistency.
Negative
- Language servers are resource-intensive processes (memory, CPU). Running multiple servers simultaneously (e.g., Pyright + typescript-language-server + clangd for a polyglot project) increases system resource consumption.
- LSP startup time adds latency to actor activation. Large workspaces may take several seconds to index before diagnostics become available.
- File synchronization overhead — every file read/write by the actor triggers LSP notifications, adding I/O overhead proportional to the actor's file activity.
- LSP protocol complexity — the protocol has many capabilities and edge cases. The
LSPToolAdaptermust handle server-specific quirks and partial capability support.
Risks
- Language server stability varies. Some servers may crash under heavy load, produce incorrect diagnostics, or fail to handle large files. The LSP Runtime must be robust to these failure modes.
- LSP diagnostic latency may not keep up with rapid file modifications during Execute phase, leading to stale diagnostics if the actor reads diagnostics immediately after writing a file.
- The
auto: truebinding mode depends on accurate language discovery. Misidentified languages would bind incorrect servers, producing confusing tool results. - Deep Jinja2 templating for LSP resolution may produce configurations that are difficult to debug when binding fails.
Alternatives Considered
Custom language analysis per language — Build bespoke code analysis (AST parsing, type checking) for each supported language. Maximum control but enormous development cost, perpetual maintenance burden, and inevitable lag behind the state of the art in language tooling. The LSP ecosystem already provides what CleverAgents needs. Rejected.
Tree-sitter only — Use Tree-sitter for syntax-level analysis across all languages. Tree-sitter provides fast, incremental parsing and syntax highlighting but lacks semantic understanding (no type checking, no cross-file references, no completion intelligence). Tree-sitter may be used as a complementary lightweight layer for syntax-only tasks, but it cannot replace LSP for semantic intelligence. Rejected as the primary strategy.
LSP as IDE integration (Presentation layer) — Use LSP as a protocol for IDE plugin integration, mapping editor features to plan operations. This conflates two concerns: LSP is designed for language intelligence, not for application-level UI integration. CleverAgents' Presentation-layer clients (CLI, TUI, Web, IDE plugin) all communicate through A2A. Using LSP for IDE integration would create an unnecessary protocol layer between the IDE and A2A when A2A already provides the needed surface. Rejected — the IDE plugin is an embedded TUI communicating through A2A; LSP serves the Infrastructure layer.
LSP as part of the Skill system — Package LSP capabilities as skills rather than as a separate registry and binding mechanism. While architecturally possible, this conflates two distinct concerns: skills organize tools into reusable collections, while LSP binding requires workspace-aware server lifecycle management, file synchronization, and language-based routing that skills are not designed to handle. A dedicated LSP Registry and Runtime provide clearer separation of concerns. Rejected.
Compliance
- LSP conformance tests: Validate that the LSP Runtime correctly implements the LSP initialize/shutdown lifecycle, file synchronization notifications, and capability negotiation for each registered server.
- Tool adapter tests: Verify that
LSPToolAdaptergenerates correct tool definitions from LSP server capabilities and routes tool invocations to the correct server based on file language. - Binding resolution tests: Test all three binding modes (explicit, language-based, resource-auto) including edge cases: missing servers, ambiguous language detection, multiple servers for the same language.
- Lifecycle tests: Verify server startup/shutdown, shared instance reference counting, crash recovery and re-synchronization, and sandbox workspace mapping.
- Context enrichment tests: Confirm that diagnostic injection respects context budgets, handles empty diagnostics, and correctly annotates files in hot context.
- Integration tests: End-to-end tests where an actor with LSP bindings performs code analysis tasks (find errors, navigate definitions, complete code) and produces correct results using LSP tools.