Files
cleveragents-core/docs/api/lsp.md
T

11 KiB

cleveragents.lsp — Language Server Protocol Integration

The lsp package manages Language Server Protocol (LSP) server processes and provides code intelligence tools (diagnostics, completions, hover, definitions, symbols) to actors.

See ADR-027 for design rationale.


Quick Start

from cleveragents.lsp import (
    LspRegistry,
    LspRuntime,
    LspServer,
    LspToolAdapter,
    LspServerConfig,
    LspCapability,
    LspClient,
    LspLifecycleManager,
    LspTransport,
    StdioTransport,
)

Models

Module: cleveragents.lsp.models

LspServerConfig

@dataclass
class LspServerConfig:
    name: str                          # namespaced server name, e.g. "lsp/pyright"
    command: str                       # executable to launch
    args: list[str] = field(default_factory=list)
    env: dict[str, str] = field(default_factory=dict)
    description: str = ""              # max 1000 chars
    transport: LspTransport = LspTransport.STDIO
    initialization: dict[str, Any] = field(default_factory=dict)
    workspace_settings: dict[str, Any] = field(default_factory=dict)

Configuration for a single LSP server instance. transport defaults to LspTransport.STDIO; TCP transport is reserved for future use. initialization is forwarded as initializationOptions in the LSP initialize handshake; workspace_settings is sent via workspace/didChangeConfiguration.

LspTransport

class LspTransport(str, Enum):
    STDIO = "stdio"
    TCP   = "tcp"    # reserved — not yet implemented

LspCapability

class LspCapability(str, Enum):
    DIAGNOSTICS       = "diagnostics"
    COMPLETIONS       = "completions"
    HOVER             = "hover"
    DEFINITIONS       = "definitions"
    SIGNATURE_HELP    = "signature_help"
    DOCUMENT_SYMBOLS  = "document_symbols"
    WORKSPACE_SYMBOLS = "workspace_symbols"
    FORMATTING        = "formatting"
    RENAME            = "rename"
    REFERENCES        = "references"
    CODE_ACTIONS      = "code_actions"

All 11 capabilities are advertised during initialize(). Tool adapter dispatches each capability to the corresponding LspRuntime method.

LspBinding

@dataclass
class LspBinding:
    server: str                        # server name
    capabilities: list[LspCapability]

Per-node LSP binding declared in actor YAML. Controls which server each actor node uses and which capabilities it exposes.


LspLifecycleManager

Module: cleveragents.lsp.lifecycle

Thread-safe manager for running LSP server instances with reference counting. Multiple actors can share one server; the process is only stopped when the last reference is released.

from cleveragents.lsp import LspLifecycleManager, LspServerConfig

manager = LspLifecycleManager()

start_server(config, workspace_path) -> LspClient

Start or acquire a reference to an LSP server.

If a server with the same name is already running for the same workspace, its reference count is incremented and the existing client is returned. Uses a 3-phase lock pattern to avoid holding the internal lock during blocking I/O (server startup can take up to 60 seconds):

  • Phase 1 (short lock): check for an existing live server.
  • Phase 2 (no lock): spawn the process and perform the LSP handshake.
  • Phase 3 (short lock): commit the new server into shared state, handling the race where another thread started the same server concurrently.
client = manager.start_server(config, workspace_path="/workspace/myproject")

Raises: LspError if the server process cannot be started.

stop_server(name) -> None

Release a reference. When the count reaches zero the server is shut down gracefully (shutdown + exit LSP messages, then process termination).

Raises: LspServerNotFoundError if no server with name is running.

restart_server(name) -> LspClient

Restart a crashed or unresponsive server. Uses the same 3-phase lock pattern as start_server — the lock is released before blocking I/O so other threads are never blocked during the restart. The old entry is removed from shared state in Phase 1 so concurrent callers see the server as absent during the restart window.

Note (v3.7.0+, fixed PR #3165): Prior to this fix, restart_server held the internal lock across the entire blocking I/O sequence, causing a deadlock when another thread called start_server or stop_server concurrently. The fix restructures the method to match the 3-phase pattern already used by start_server.

new_client = manager.restart_server("lsp/pyright")

Raises: LspServerNotFoundError if the server was never started.

get_client(name) -> LspClient

Return the LspClient for a running server without changing the reference count.

Raises: LspServerNotFoundError if no server with name is running.

health_check(name) -> bool

Return True if the server process is alive.

stop_all() -> None

Shut down all running servers. Called during application cleanup.

list_running() -> list[dict[str, Any]]

Return status info for all running servers:

[
    {
        "name": "lsp/pyright",
        "workspace": "/workspace/myproject",
        "alive": True,
        "ref_count": 2,
        "initialized": True,
    },
    ...
]

LspClient

Module: cleveragents.lsp.client

Low-level LSP protocol client. Wraps a StdioTransport and implements the JSON-RPC request/notification lifecycle.

from cleveragents.lsp import LspClient, StdioTransport

transport = StdioTransport(command="pyright-langserver", args=["--stdio"])
transport.start()
client = LspClient(transport, server_name="lsp/pyright")
client.initialize(workspace_path="/workspace/myproject")
Method Description
initialize(workspace_path) Perform the LSP initialize / initialized handshake
shutdown() Send shutdown + exit and close the transport
get_diagnostics(file_path) Return list[dict] of LSP diagnostic objects
get_completions(file_path, line, character) Return completion items
get_hover(file_path, line, character) Return hover result dict
get_definitions(file_path, line, character) Return location(s)
get_signature_help(file_path, line, character) Return signature help
get_document_symbols(file_path) Return document symbol list
get_workspace_symbols(query) Return workspace symbol list
is_initialized boolTrue after a successful handshake

All position arguments use 1-based line/column numbers; the client converts to 0-based internally before sending LSP requests.


LspRuntime

Module: cleveragents.lsp.runtime

High-level runtime used by LspToolAdapter. Wraps LspLifecycleManager and adds input validation, file reading, language detection, and coordinate conversion.

from cleveragents.lsp import LspRuntime

runtime = LspRuntime(lifecycle_manager=manager)
diagnostics = runtime.get_diagnostics("lsp/pyright", "/workspace/myproject/main.py")

When no runtime is supplied to LspToolAdapter, tool handlers fall back to raising LspNotAvailableError.


LspToolAdapter

Module: cleveragents.lsp.tool_adapter

Registers LSP capabilities as tools in ToolRegistry.

from cleveragents.lsp import LspToolAdapter

adapter = LspToolAdapter(runtime=runtime)
adapter.register_tools(tool_registry, bindings=[binding])

Each LspCapability maps to a tool with a JSON Schema spec:

Capability Tool name Key parameters
DIAGNOSTICS lsp_diagnostics file_path
COMPLETIONS lsp_completions file_path, line, character
HOVER lsp_hover file_path, line, character
DEFINITIONS lsp_definitions file_path, line, character
SIGNATURE_HELP lsp_signature_help file_path, line, character
DOCUMENT_SYMBOLS lsp_document_symbols file_path
WORKSPACE_SYMBOLS lsp_workspace_symbols query
FORMATTING lsp_formatting file_path
RENAME lsp_rename file_path, line, character, new_name
REFERENCES lsp_references file_path, line, character
CODE_ACTIONS lsp_code_actions file_path, line, character

LspRegistry

Module: cleveragents.lsp.registry

Stores LspServerConfig objects indexed by name. Used by the DI container to resolve server configs at runtime.

from cleveragents.lsp import LspRegistry

registry = LspRegistry()
registry.register(config)
server_config = registry.get("lsp/pyright")
all_configs = registry.list_all()

LspServer

Module: cleveragents.lsp.server

Domain model representing a registered LSP server entry (name, config, status). Distinct from the internal _ManagedServer lifecycle state holder.


LanguageDiscovery

Module: cleveragents.lsp.discovery

4-layer language detection for a given file path:

  1. File extension mapping
  2. Shebang line parsing
  3. UKO ontology classification
  4. Project config heuristics
from cleveragents.lsp import LanguageDiscovery

discovery = LanguageDiscovery()
language = discovery.detect("/workspace/myproject/main.py")  # -> "python"

StdioTransport

Module: cleveragents.lsp.transport

Manages the LSP server subprocess over stdin/stdout JSON-RPC.

from cleveragents.lsp import StdioTransport

transport = StdioTransport(
    command="pyright-langserver",
    args=["--stdio"],
    env={},
    cwd="/workspace/myproject",
)
transport.start()
# ... use transport ...
transport.stop()
Property/Method Description
is_alive boolTrue if the subprocess is running
start() Spawn the subprocess
stop() Terminate the subprocess gracefully

Error Types

Module: cleveragents.lsp.errors

Exception Description
LspError Base LSP exception
LspNotAvailableError LSP runtime not configured for this actor node
LspServerNotFoundError No server with the given name is running

Actor YAML Integration

LSP bindings are declared per-node in actor YAML:

name: local/python-dev
entry_node: analyst
nodes:
  analyst:
    model: gpt-4o
    tool_sources: [builtin]
    lsp_bindings:
      - server: lsp/pyright
        capabilities: [diagnostics, completions, hover, definitions]

The ActorCompiler resolves bindings at compile time and wires the LspToolAdapter into the node's tool registry.