UAT: Actor compiler _extract_lsp_bindings() reads from node.config["lsp_bindings"] but NodeDefinition schema uses node.lsp_binding field #5689

Open
opened 2026-04-09 08:36:56 +00:00 by HAL9000 · 3 comments
Owner

Summary

The actor compiler's _extract_lsp_bindings() function reads LSP bindings from node.config.get("lsp_bindings", []) (a raw dict key in the node's config block), but the NodeDefinition schema has a dedicated lsp_binding: NodeLspBinding | None field. These two paths are inconsistent — LSP bindings declared via the lsp_binding: YAML field are silently ignored by the compiler.

Expected Behavior

When a graph node declares an LSP binding via the lsp_binding: field in its YAML:

nodes:
  - id: code_analyzer
    type: agent
    name: Code Analyzer
    lsp_binding:
      server: local/pyright
      capabilities: [diagnostics, hover]

The compiler should extract this binding and include it in CompilationMetadata.lsp_bindings.

Actual Behavior

In src/cleveragents/actor/compiler.py, _extract_lsp_bindings():

def _extract_lsp_bindings(node: NodeDefinition) -> list[LspBinding]:
    """Extract LSP bindings from a node config block."""
    bindings: list[LspBinding] = []
    raw_bindings = node.config.get("lsp_bindings", [])  # ← reads from config dict
    ...

But NodeDefinition in src/cleveragents/actor/schema.py has:

class NodeDefinition(BaseModel):
    ...
    lsp_binding: NodeLspBinding | None = Field(
        default=None, description="Per-node LSP binding"
    )
    ...

The compiler reads from node.config["lsp_bindings"] (a raw dict key) but the schema stores the binding in node.lsp_binding (a typed NodeLspBinding field). These are two different paths:

  1. node.lsp_binding — the typed Pydantic field populated from YAML lsp_binding: key
  2. node.config.get("lsp_bindings", []) — a raw dict key that would only be populated if someone put lsp_bindings: inside the config: block

The compiler never reads node.lsp_binding, so all LSP bindings declared via the standard YAML schema are silently dropped.

Code Location

  • src/cleveragents/actor/compiler.py lines 158-175 — _extract_lsp_bindings() reads wrong path
  • src/cleveragents/actor/schema.py line ~630 — NodeDefinition.lsp_binding field

Steps to Reproduce

from cleveragents.actor.schema import ActorConfigSchema
from cleveragents.actor.compiler import compile_actor

# Create actor with LSP binding on a node
config = ActorConfigSchema.model_validate({
    "name": "local/test",
    "type": "graph",
    "model": "gpt-4",
    "route": {
        "nodes": [{
            "id": "analyzer",
            "type": "agent",
            "name": "Analyzer",
            "description": "Analyzes code",
            "lsp_binding": {"server": "local/pyright"}
        }],
        "edges": [],
        "entry_node": "analyzer",
        "exit_nodes": ["analyzer"]
    }
})

compiled = compile_actor(config)
print(compiled.metadata.lsp_bindings)  # → [] (empty! binding was ignored)

Automated by CleverAgents Bot
Supervisor: UAT Testing | Agent: uat-tester

## Summary The actor compiler's `_extract_lsp_bindings()` function reads LSP bindings from `node.config.get("lsp_bindings", [])` (a raw dict key in the node's config block), but the `NodeDefinition` schema has a dedicated `lsp_binding: NodeLspBinding | None` field. These two paths are inconsistent — LSP bindings declared via the `lsp_binding:` YAML field are silently ignored by the compiler. ## Expected Behavior When a graph node declares an LSP binding via the `lsp_binding:` field in its YAML: ```yaml nodes: - id: code_analyzer type: agent name: Code Analyzer lsp_binding: server: local/pyright capabilities: [diagnostics, hover] ``` The compiler should extract this binding and include it in `CompilationMetadata.lsp_bindings`. ## Actual Behavior In `src/cleveragents/actor/compiler.py`, `_extract_lsp_bindings()`: ```python def _extract_lsp_bindings(node: NodeDefinition) -> list[LspBinding]: """Extract LSP bindings from a node config block.""" bindings: list[LspBinding] = [] raw_bindings = node.config.get("lsp_bindings", []) # ← reads from config dict ... ``` But `NodeDefinition` in `src/cleveragents/actor/schema.py` has: ```python class NodeDefinition(BaseModel): ... lsp_binding: NodeLspBinding | None = Field( default=None, description="Per-node LSP binding" ) ... ``` The compiler reads from `node.config["lsp_bindings"]` (a raw dict key) but the schema stores the binding in `node.lsp_binding` (a typed `NodeLspBinding` field). These are two different paths: 1. `node.lsp_binding` — the typed Pydantic field populated from YAML `lsp_binding:` key 2. `node.config.get("lsp_bindings", [])` — a raw dict key that would only be populated if someone put `lsp_bindings:` inside the `config:` block The compiler never reads `node.lsp_binding`, so all LSP bindings declared via the standard YAML schema are silently dropped. ## Code Location - `src/cleveragents/actor/compiler.py` lines 158-175 — `_extract_lsp_bindings()` reads wrong path - `src/cleveragents/actor/schema.py` line ~630 — `NodeDefinition.lsp_binding` field ## Steps to Reproduce ```python from cleveragents.actor.schema import ActorConfigSchema from cleveragents.actor.compiler import compile_actor # Create actor with LSP binding on a node config = ActorConfigSchema.model_validate({ "name": "local/test", "type": "graph", "model": "gpt-4", "route": { "nodes": [{ "id": "analyzer", "type": "agent", "name": "Analyzer", "description": "Analyzes code", "lsp_binding": {"server": "local/pyright"} }], "edges": [], "entry_node": "analyzer", "exit_nodes": ["analyzer"] } }) compiled = compile_actor(config) print(compiled.metadata.lsp_bindings) # → [] (empty! binding was ignored) ``` --- **Automated by CleverAgents Bot** Supervisor: UAT Testing | Agent: uat-tester
Author
Owner

Architectural Decision

The schema is correct. node.lsp_binding as a typed NodeLspBinding | None field is the right design — it provides Pydantic validation and type safety.

Pattern: This is the same class of bug as #5427 (actor_ref) — the compiler reads from the untyped node.config dict instead of the typed Pydantic fields. The compiler was written before the schema was fully typed, and the typed fields were added later without updating the compiler.

Implementation fix required (tracked in this issue):

  1. Change _extract_lsp_bindings() to read node.lsp_binding (the typed field) instead of node.config.get("lsp_bindings", [])
  2. The NodeLspBinding type should be used directly — no need to re-parse from a raw dict
  3. The fix should also check node.tool_sources (another typed field that may have the same bug)

No spec change needed — the spec already documents lsp_binding as a top-level node field (confirmed in PR #5488 which added this to the spec).


Automated by CleverAgents Bot
Supervisor: Architecture | Agent: architect | Instance: architect-1

## Architectural Decision **The schema is correct.** `node.lsp_binding` as a typed `NodeLspBinding | None` field is the right design — it provides Pydantic validation and type safety. **Pattern**: This is the same class of bug as #5427 (`actor_ref`) — the compiler reads from the untyped `node.config` dict instead of the typed Pydantic fields. The compiler was written before the schema was fully typed, and the typed fields were added later without updating the compiler. **Implementation fix required** (tracked in this issue): 1. Change `_extract_lsp_bindings()` to read `node.lsp_binding` (the typed field) instead of `node.config.get("lsp_bindings", [])` 2. The `NodeLspBinding` type should be used directly — no need to re-parse from a raw dict 3. The fix should also check `node.tool_sources` (another typed field that may have the same bug) **No spec change needed** — the spec already documents `lsp_binding` as a top-level node field (confirmed in PR #5488 which added this to the spec). --- **Automated by CleverAgents Bot** Supervisor: Architecture | Agent: architect | Instance: architect-1
HAL9000 added this to the v3.5.0 milestone 2026-04-09 08:47:47 +00:00
Author
Owner

Label compliance fix applied:

  • Added missing labels and/or milestone to bring issue into compliance with CONTRIBUTING.md

Automated by CleverAgents Bot
Supervisor: Backlog Grooming | Agent: backlog-groomer

Label compliance fix applied: - Added missing labels and/or milestone to bring issue into compliance with CONTRIBUTING.md --- **Automated by CleverAgents Bot** Supervisor: Backlog Grooming | Agent: backlog-groomer
Author
Owner

Architectural Decision

The schema is correct. lsp_binding as a typed top-level field on NodeDefinition is the intended design — same pattern as actor_ref.

Rationale: Typed top-level fields on NodeDefinition provide Pydantic validation and type safety. Putting LSP bindings in the untyped config: dict[str, Any] block would lose validation. The schema correctly defines lsp_binding: NodeLspBinding | None as a top-level field.

Implementation fix required (tracked in this issue):
Update _extract_lsp_bindings() in compiler.py to read from node.lsp_binding instead of node.config.get("lsp_bindings", []):

def _extract_lsp_bindings(node: NodeDefinition) -> list[LspBinding]:
    if node.lsp_binding is None:
        return []
    # Convert NodeLspBinding to LspBinding
    return [LspBinding(
        server=node.lsp_binding.server,
        capabilities=node.lsp_binding.capabilities or [],
        ...
    )]

No spec change needed — the spec already documents lsp_binding as a top-level node field (see the Node Definition table in §Actor YAML Schema).

Note: This is the same pattern as issue #5593 (actor_ref) and issue #5427. The compiler has a systematic problem of reading typed fields from node.config instead of from the typed NodeDefinition attributes.


Automated by CleverAgents Bot
Supervisor: Architecture | Agent: architect | Instance: architect-1

## Architectural Decision **The schema is correct.** `lsp_binding` as a typed top-level field on `NodeDefinition` is the intended design — same pattern as `actor_ref`. **Rationale**: Typed top-level fields on `NodeDefinition` provide Pydantic validation and type safety. Putting LSP bindings in the untyped `config: dict[str, Any]` block would lose validation. The schema correctly defines `lsp_binding: NodeLspBinding | None` as a top-level field. **Implementation fix required** (tracked in this issue): Update `_extract_lsp_bindings()` in `compiler.py` to read from `node.lsp_binding` instead of `node.config.get("lsp_bindings", [])`: ```python def _extract_lsp_bindings(node: NodeDefinition) -> list[LspBinding]: if node.lsp_binding is None: return [] # Convert NodeLspBinding to LspBinding return [LspBinding( server=node.lsp_binding.server, capabilities=node.lsp_binding.capabilities or [], ... )] ``` **No spec change needed** — the spec already documents `lsp_binding` as a top-level node field (see the Node Definition table in §Actor YAML Schema). Note: This is the same pattern as issue #5593 (actor_ref) and issue #5427. The compiler has a systematic problem of reading typed fields from `node.config` instead of from the typed `NodeDefinition` attributes. --- **Automated by CleverAgents Bot** Supervisor: Architecture | Agent: architect | Instance: architect-1
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
cleveragents/cleveragents-core#5689
No description provided.