Compare commits

...

3 Commits

3 changed files with 229 additions and 0 deletions
+2
View File
@@ -11,6 +11,8 @@ classes, functions, and exceptions with signatures and usage examples.
| [`cleveragents.core`](core.md) | Exception hierarchy, error handling, retry patterns, async cleanup |
| [`cleveragents.a2a`](a2a.md) | Agent-to-Agent (A2A) protocol facade, transport, models, and events |
| [`cleveragents.actor`](actor.md) | Actor registry, configuration, loader, and compiler |
| [`cleveragents.providers`](providers.md) | AI provider registry — discovery, selection, capability metadata, and LLM factory |
| [`cleveragents.lsp`](lsp.md) | Language Server Protocol integration — lifecycle manager, client, transport, and tool adapter |
| [`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 |
+226
View File
@@ -0,0 +1,226 @@
# `cleveragents.lsp` — Language Server Protocol Integration
The `lsp` package manages Language Server Protocol (LSP) server processes,
provides a protocol client, and exposes LSP-backed code intelligence as
tools available to actors.
See [ADR-027](../adr/ADR-027-language-server-protocol.md) for design rationale.
---
## `LspLifecycleManager`
**Module:** `cleveragents.lsp.lifecycle`
Thread-safe manager for running LSP server instances with reference counting.
Multiple actors can share a single server process; the process is only stopped
when the last reference is released.
```python
from cleveragents.lsp.lifecycle import LspLifecycleManager
manager = LspLifecycleManager()
```
### Concurrency model
`LspLifecycleManager` uses a **3-phase lock pattern** to avoid holding
`self._lock` during blocking I/O (process spawn, LSP handshake — up to 60 s):
1. **Phase 1 (short lock):** Read/mutate shared state, snapshot needed fields.
2. **Phase 2 (no lock):** Perform blocking I/O (start process, initialize).
3. **Phase 3 (short lock):** Commit new state; handle races.
> **Fix (v3.7.0+, PR #3165):** `restart_server()` previously held the lock
> during blocking I/O, causing a deadlock when another thread called
> `stop_server()` concurrently. The method now releases the lock before
> stopping the old transport and starting the new one.
### Methods
#### `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.
- Otherwise, a new process is spawned and the LSP handshake is performed
outside the lock.
```python
from cleveragents.lsp.models import LspServerConfig
config = LspServerConfig(
name="lsp/pyright",
command="pyright-langserver",
args=["--stdio"],
)
client = manager.start_server(config, workspace_path="/my/project")
```
#### `stop_server(name) -> None`
Release a reference to a named server. When the reference count reaches
zero the server is shut down gracefully.
Raises `LspServerNotFoundError` if no server with `name` is running.
#### `restart_server(name) -> LspClient`
Restart a crashed or unresponsive server. Uses the 3-phase lock pattern
to avoid holding the lock during blocking I/O.
Returns a new `LspClient` for the restarted server.
#### `get_client(name) -> LspClient`
Return the `LspClient` for a running server.
Raises `LspServerNotFoundError` if the server is not running.
#### `health_check(name) -> bool`
Return `True` if the named server process is alive.
#### `stop_all() -> None`
Shut down all running servers. Called during application cleanup.
#### `list_running() -> list[dict]`
Return status info for all running servers:
```python
[
{
"name": "lsp/pyright",
"workspace": "/my/project",
"alive": True,
"ref_count": 2,
"initialized": True,
}
]
```
---
## `LspClient`
**Module:** `cleveragents.lsp.client`
Protocol client that speaks LSP over a `StdioTransport`.
```python
from cleveragents.lsp.client import LspClient
client = LspClient(transport, server_name="lsp/pyright")
client.initialize(workspace_path="/my/project")
```
Key attributes:
| Attribute | Type | Description |
|-----------|------|-------------|
| `is_initialized` | `bool` | `True` after a successful `initialize` handshake |
| `server_name` | `str` | Namespaced server name |
Key methods:
| Method | Description |
|--------|-------------|
| `initialize(workspace_path)` | Perform LSP `initialize` + `initialized` handshake |
| `shutdown()` | Send `shutdown` + `exit` notifications |
| `text_document_completion(uri, position)` | Request completions |
| `text_document_hover(uri, position)` | Request hover info |
| `text_document_definition(uri, position)` | Go-to-definition |
| `text_document_references(uri, position)` | Find all references |
| `text_document_diagnostics(uri)` | Pull diagnostics |
---
## `StdioTransport`
**Module:** `cleveragents.lsp.transport`
Manages the subprocess and JSON-RPC framing over stdio.
```python
from cleveragents.lsp.transport import StdioTransport
transport = StdioTransport(
command="pyright-langserver",
args=["--stdio"],
env=None,
cwd="/my/project",
)
transport.start()
# ... use transport ...
transport.stop()
```
| Property | Type | Description |
|----------|------|-------------|
| `is_alive` | `bool` | `True` if the subprocess is running |
---
## `LspServerConfig`
**Module:** `cleveragents.lsp.models`
Pydantic model describing an LSP server.
```python
from cleveragents.lsp.models import LspServerConfig
config = LspServerConfig(
name="lsp/pyright", # namespaced identifier
command="pyright-langserver",
args=["--stdio"],
env={"PYTHONPATH": "/my/project/src"},
)
```
| Field | Type | Description |
|-------|------|-------------|
| `name` | `str` | Namespaced server name (e.g. `lsp/pyright`) |
| `command` | `str` | Executable to run |
| `args` | `list[str]` | Command-line arguments |
| `env` | `dict[str, str] \| None` | Extra environment variables |
---
## `LspToolAdapter`
**Module:** `cleveragents.lsp.tool_adapter`
Bridges LSP capabilities into the `ToolRegistry` so actors can invoke
code intelligence operations as tools.
```python
from cleveragents.lsp.tool_adapter import LspToolAdapter
adapter = LspToolAdapter(client=client, server_name="lsp/pyright")
tools = adapter.get_tools() # list[ToolSpec]
```
Tools exposed per server:
| Tool name | LSP method |
|-----------|-----------|
| `lsp/completion` | `textDocument/completion` |
| `lsp/hover` | `textDocument/hover` |
| `lsp/definition` | `textDocument/definition` |
| `lsp/references` | `textDocument/references` |
| `lsp/diagnostics` | `textDocument/publishDiagnostics` |
---
## Error Types
| Exception | Description |
|-----------|-------------|
| `LspError` | Base LSP exception |
| `LspServerNotFoundError` | Named server is not registered in the lifecycle manager |
| `LspInitializationError` | LSP handshake failed |
| `LspTransportError` | Subprocess I/O error |
+1
View File
@@ -0,0 +1 @@
placeholder