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_serverheld the internal lock across the entire blocking I/O sequence, causing a deadlock when another thread calledstart_serverorstop_serverconcurrently. The fix restructures the method to match the 3-phase pattern already used bystart_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 |
bool — True 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:
- File extension mapping
- Shebang line parsing
- UKO ontology classification
- 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 |
bool — True 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.