docs: add LSP API reference, update module index, and add changelog entries for 2026-04-05 merged PRs #3282

Closed
freemo wants to merge 4 commits from docs/add-lsp-api-and-changelog-2026-04-05 into master
3 changed files with 422 additions and 1681 deletions
+40 -1681
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -14,6 +14,7 @@ classes, functions, and exceptions with signatures and usage examples.
| [`cleveragents.skills`](skills.md) | Skill framework — schema, protocol, registry, discovery, and inline executor |
| [`cleveragents.tool`](tool.md) | Tool runtime, lifecycle, registry, router, and container executor |
| [`cleveragents.mcp`](mcp.md) | Model Context Protocol (MCP) adapter, client, registry, and sandbox |
| [`cleveragents.lsp`](lsp.md) | Language Server Protocol (LSP) lifecycle manager, client, runtime, tool adapter, and registry |
| [`cleveragents.resource`](resource.md) | Resource schema, handlers, and type-inheritance system |
| [`cleveragents.config`](config.md) | Application settings, logging, metrics, and security scanning |
| [`cleveragents.tui`](tui.md) | Interactive Terminal UI — app, persona system, input routing, slash commands, session export/import |
+381
View File
@@ -0,0 +1,381 @@
# `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](../adr/ADR-027-language-server-protocol.md) for design rationale.
---
## Quick Start
```python
from cleveragents.lsp import (
LspRegistry,
LspRuntime,
LspServer,
LspToolAdapter,
LspServerConfig,
LspCapability,
LspClient,
LspLifecycleManager,
LspTransport,
StdioTransport,
)
```
---
## Models
**Module:** `cleveragents.lsp.models`
### `LspServerConfig`
```python
@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`
```python
class LspTransport(str, Enum):
STDIO = "stdio"
TCP = "tcp" # reserved — not yet implemented
```
### `LspCapability`
```python
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`
```python
@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.
```python
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.
```python
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`.
```python
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:
```python
[
{
"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.
```python
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.
```python
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`.
```python
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.
```python
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
```python
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.
```python
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:
```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.