Files
cleveragents-core/docs/adr/ADR-027-language-server-protocol.md
freemo d5b122d4a3
CI / lint (push) Successful in 14s
CI / quality (push) Successful in 21s
CI / security (push) Successful in 36s
CI / build (push) Successful in 36s
CI / typecheck (push) Successful in 39s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 2m57s
CI / integration_tests (push) Successful in 3m23s
CI / docker (push) Successful in 53s
CI / coverage (push) Successful in 5m58s
CI / benchmark-publish (push) Successful in 19m27s
Docs: Updated to A2A and integrating rest standard
2026-03-11 13:19:55 -04:00

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
2026-02-20
Proposed
Jeffrey Phillips Freeman
2026-02-20
Accepted
Jeffrey Phillips Freeman
4
Jeffrey Phillips Freeman
null
number title relationship
1 Layered Architecture The LSP Runtime resides in the Infrastructure layer; LSP tools are exposed to actors through the Domain layer's tool abstractions
number title relationship
8 Resource System Resources provide the workspace roots for LSP servers and the language discovery inputs for auto-binding
number title relationship
10 Actor and Agent Architecture Actors declare LSP bindings in their YAML configuration; LSP tools participate as capabilities in actor graph nodes
number title relationship
11 Tool System LSP capabilities are exposed as tools through the `LSPToolAdapter`, following the same adapter pattern as MCP
number title relationship
12 Skill System LSP tools complement skill-provided tools in the actor's merged tool surface
number title relationship
14 Context Management (ACMS) LSP context enrichment injects diagnostics and type information into the ACMS hot context
number title relationship
29 Model Context Protocol (MCP) Adoption MCP and LSP follow analogous adapter patterns; both bridge standard protocol servers into the tool system
number title relationship
31 Actor Abstraction Definition Actors acquire language intelligence through LSP bindings alongside tool capabilities through skills
number title relationship
32 Jinja2 YAML Template Preprocessing Jinja2 templates enable dynamic LSP binding resolution in actor YAML files
number title relationship
40 LSP Resource Types ADR-027 defines CleverAgents as an LSP server; ADR-040 defines external LSP servers as resources in the DAG
votes_for votes_against abstentions
voter comment
Jeffrey Phillips Freeman <Jeffrey.Freeman@CleverThis.com> Giving actors the same language intelligence that human developers use in IDEs is a force multiplier for code quality — and leveraging the LSP ecosystem avoids reinventing mature tooling

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/]name format (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:

  1. 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.
  2. 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.
  3. 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.
  4. 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:

  1. The runtime resolves the actor's lsp: configuration to concrete LSP server entries from the LSP Registry.
  2. For each bound server, the LSPToolAdapter generates tool definitions for each exposed capability.
  3. These tools are injected into the actor's available tool surface alongside skill-provided tools.
  4. The actor can call LSP tools during its reasoning loop (e.g., lsp/diagnostics to check for errors before committing a change, lsp/definition to 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:

  1. Startup: When an actor with LSP bindings is activated (either for a plan phase or a direct agents actor run invocation), the LSP Runtime starts the required language server processes. Each server is initialized with the initialize LSP handshake, passing initializationOptions from the registry entry and workspace root paths mapped from the actor's bound resources.

  2. 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.

  3. File Synchronization: As the actor reads and modifies files (via tools), the LSP Runtime sends textDocument/didOpen, textDocument/didChange, and textDocument/didClose notifications to the language server, keeping the server's view of the codebase synchronized with the actor's mutations.

  4. 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.

  5. Shutdown: When the actor deactivates (plan phase completes, session ends, or direct run finishes), the LSP Runtime sends the shutdown and exit lifecycle 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 established MCPToolAdapter pattern, 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 LSPToolAdapter must 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: true binding 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 LSPToolAdapter generates 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.