From fa49e06c894a1b01e9bdbe54c0cc903dfb546c56 Mon Sep 17 00:00:00 2001 From: HAL9000 Date: Mon, 27 Apr 2026 11:50:10 +0000 Subject: [PATCH 1/3] fix(actor): read lsp_binding from NodeDefinition field instead of config dict in compiler The _extract_lsp_bindings() function now reads from the dedicated typed node.lsp_binding (NodeLspBinding) field and converts it to LspBinding records. Also maintains backward compatibility by checking the config dict for legacy lsp_bindings configuration. Fixes the issue where per-node LSP bindings configured via the lsp_binding: YAML key were being silently ignored during actor graph compilation. ISSUES CLOSED: #1432 --- src/cleveragents/actor/compiler.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/cleveragents/actor/compiler.py b/src/cleveragents/actor/compiler.py index 7c381552d..bcdedc1dd 100644 --- a/src/cleveragents/actor/compiler.py +++ b/src/cleveragents/actor/compiler.py @@ -156,11 +156,36 @@ def _map_edge(edge: EdgeDefinition) -> lg_nodes.Edge: def _extract_lsp_bindings(node: NodeDefinition) -> list[LspBinding]: - """Extract LSP bindings from a node config block.""" + """Extract LSP bindings from a node's dedicated lsp_binding field. + + Reads from the typed ``node.lsp_binding`` (NodeLspBinding) field and + converts to LspBinding records. Also checks config dict for backward + compatibility with legacy ``lsp_bindings`` config key. + + Args: + node: The node definition to extract bindings from. + + Returns: + List of LspBinding records for this node. + """ bindings: list[LspBinding] = [] + + # Primary path: read from the dedicated typed field (NodeLspBinding) + if node.lsp_binding is not None: + bindings.append( + LspBinding( + node_name=node.id, + lsp_server_name=node.lsp_binding.server or "", + languages=node.lsp_binding.languages or [], + auto_detect=node.lsp_binding.auto if node.lsp_binding.auto is not None else True, + ) + ) + + # Fallback path: check config dict for backward compatibility raw_bindings = node.config.get("lsp_bindings", []) if not isinstance(raw_bindings, list): return bindings + for entry in raw_bindings: if not isinstance(entry, dict): continue @@ -175,6 +200,7 @@ def _extract_lsp_bindings(node: NodeDefinition) -> list[LspBinding]: auto_detect=entry.get("auto_detect", True), ) ) + return bindings -- 2.52.0 From ddc9d98b7ef470271fdff0fae1d2362cca5a521a Mon Sep 17 00:00:00 2001 From: controller-ci-rerun Date: Fri, 29 May 2026 22:46:05 -0400 Subject: [PATCH 2/3] chore: re-trigger CI [controller] -- 2.52.0 From 02e51b92986346fbb27ca2467f0f77b92cd07543 Mon Sep 17 00:00:00 2001 From: CleverThis Date: Sat, 30 May 2026 00:00:48 -0400 Subject: [PATCH 3/3] fix(actor): fix lint errors and add regression tests for lsp_binding field (#1432) - Remove trailing whitespace from blank lines in _extract_lsp_bindings() (W293 at lines 160, 164, 167, 172, 183, 188, 203) - Simplify over-long line 180 (E501): auto_detect=node.lsp_binding.auto - Add Behave scenario verifying NodeLspBinding typed field populates CompilationMetadata.lsp_bindings (features/actor_lsp_binding_field.feature) - Add corresponding @given step using NodeLspBinding in actor_compiler_steps.py - Add Robot Framework regression test case and lsp-binding-field helper command confirming node.lsp_binding is read by compile_actor() - Update CHANGELOG.md and CONTRIBUTORS.md ISSUES CLOSED: #1432 --- CHANGELOG.md | 6 +++ CONTRIBUTORS.md | 5 +++ features/actor_lsp_binding_field.feature | 22 +++++++++++ features/steps/actor_compiler_steps.py | 22 +++++++++++ robot/actor_compiler.robot | 13 +++++++ robot/helper_actor_compiler.py | 49 ++++++++++++++++++++++++ src/cleveragents/actor/compiler.py | 16 ++++---- 7 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 features/actor_lsp_binding_field.feature diff --git a/CHANGELOG.md b/CHANGELOG.md index 8235ae7b8..3872d17a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ Changed `wf10_batch.robot` to be less likely to create files, and `plan_generation_graph.robot` to give more test answers. ## [Unreleased] +- **Fix actor compiler to read LSP bindings from typed `lsp_binding` field** (#1488): Fixed + `_extract_lsp_bindings()` in `actor/compiler.py` to read from `node.lsp_binding` (the typed + `NodeLspBinding` Pydantic field on `NodeDefinition`) as the primary path, with + backward-compatible fallback to the legacy `lsp_bindings` config dict key. Per-node LSP + bindings specified via the `lsp_binding:` YAML key are no longer silently dropped. Includes + Behave BDD and Robot Framework regression tests (#1432). - **TDD regression tests for automation_profile DI bypass** (#1031): Added BDD scenarios and Robot Framework integration tests verifying that ``_get_service()`` in ``automation_profile.py`` resolves ``AutomationProfileService`` through the DI container diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index b9dc98d50..d121f9b9e 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -48,4 +48,9 @@ Below are some of the specific details of various contributions. * HAL 9000 has contributed the DecisionService wiring for PlanExecutor strategize persistence fix (#10813): added decision_service to the PlanExecutor constructor and wired it from the CLI dependency-injection container in `_get_plan_executor()`, plus implemented `_persist_strategy_decisions()` to persist strategy decisions as domain `Decision` objects. * HAL 9000 has contributed the A2A module rename standardization BDD tests (PR #10583 / issue #8615): comprehensive Behave test suite validating that all 22 A2A symbols are properly exported from `cleveragents.a2a`, no legacy ACP references remain in the module source, and documentation uses correct A2A naming conventions — fixing inline imports, unused behave symbols, cross-scenario context dependencies, and missing type annotations. * HAL 9000 has contributed the `ActorSelectionOverlay._render` → `_refresh_display` rename fix (PR #11176 / issue #11039, Epic #8174): renamed `_render()` method to `_refresh_display()` to avoid shadowing Textual's `Widget._render()`, fixing a crash in textual >=1.0 where `get_content_height()` would receive `None` and raise `AttributeError: 'NoneType' object has no attribute 'get_height'`. +* HAL 9000 has contributed the actor compiler LSP binding fix (PR #1488 / issue #1432): fixed + `_extract_lsp_bindings()` to read `node.lsp_binding` (the typed `NodeLspBinding` field on + `NodeDefinition`) as the primary extraction path instead of only checking the untyped config + dict, so per-node LSP bindings specified via `lsp_binding:` YAML key are no longer silently + dropped. Includes Behave BDD and Robot Framework regression tests. * HAL 9000 has contributed the config-actor combined-format support fix (PR #11232 / issue #11189): added ``_detect_nested_config_actor()``, ``_flatten_config_actor()``, and handling in ``ActorConfiguration.from_blob()`` to transparently flatten the nested ``config.actor`` block from both compact-string and nested-dict forms so v3 detection, schema validation, and canonicalisation see flat data — eliminating the ``"provider is required"`` crash. diff --git a/features/actor_lsp_binding_field.feature b/features/actor_lsp_binding_field.feature new file mode 100644 index 000000000..b8010e486 --- /dev/null +++ b/features/actor_lsp_binding_field.feature @@ -0,0 +1,22 @@ +Feature: Actor compiler reads LSP bindings from NodeDefinition.lsp_binding typed field + As a CleverAgents developer + I want the actor compiler to read LSP bindings from the typed lsp_binding field + So that per-node LSP bindings are not silently dropped when specified via lsp_binding: YAML key + + Background: + Given the actor compiler is available + + @tdd_issue @tdd_issue_1432 + Scenario: Node with lsp_binding typed field populates CompilationMetadata.lsp_bindings + Given a GRAPH actor config with a node using the lsp_binding typed field + When I compile the actor config + Then the compilation should succeed + And the compilation metadata should have 1 LSP binding + And the LSP binding should reference server "local/pyright" + + @tdd_issue @tdd_issue_1432 + Scenario: Node without lsp_binding field produces empty lsp_bindings in metadata + Given a GRAPH actor config with a single node + When I compile the actor config + Then the compilation should succeed + And the compilation metadata should have 0 LSP bindings diff --git a/features/steps/actor_compiler_steps.py b/features/steps/actor_compiler_steps.py index 10c09f0af..ca68fd8db 100644 --- a/features/steps/actor_compiler_steps.py +++ b/features/steps/actor_compiler_steps.py @@ -23,6 +23,7 @@ from cleveragents.actor.schema import ( ActorType, EdgeDefinition, NodeDefinition, + NodeLspBinding, NodeType, RouteDefinition, ) @@ -133,6 +134,27 @@ def step_given_lsp_bindings(context: Context) -> None: ) +@given("a GRAPH actor config with a node using the lsp_binding typed field") +def step_given_lsp_typed_field(context: Context) -> None: + nodes = [ + NodeDefinition( + id="coder", + type=NodeType.AGENT, + name="Coder", + description="Writes code with LSP support", + config={"agent": "coder_agent"}, + lsp_binding=NodeLspBinding( + server="local/pyright", + languages=["python"], + auto=False, + ), + ), + ] + context.actor_config = _build_graph_config( + "workflows/lsp_typed", nodes, [], "coder", ["coder"] + ) + + @given("a GRAPH actor config with a conditional routing node") def step_given_conditional(context: Context) -> None: nodes = [ diff --git a/robot/actor_compiler.robot b/robot/actor_compiler.robot index 7605a6485..f9537a610 100644 --- a/robot/actor_compiler.robot +++ b/robot/actor_compiler.robot @@ -54,3 +54,16 @@ Detect Cross-Actor Subgraph Cycle Via actor_ref Field Log ${result.stderr} Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} actor-compiler-cycle-detected + +Extract LSP Binding From Typed lsp_binding Field + [Documentation] Verify that a node with a typed lsp_binding field populates + ... CompilationMetadata.lsp_bindings — regression test for issue #1432: + ... _extract_lsp_bindings() was reading only node.config.get("lsp_bindings") + ... instead of the NodeLspBinding typed field on NodeDefinition. + [Tags] slow + ${result}= Run Process ${PYTHON} ${HELPER} lsp-binding-field cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} actor-compiler-lsp-binding-ok + Should Contain ${result.stdout} lsp-server: local/pyright diff --git a/robot/helper_actor_compiler.py b/robot/helper_actor_compiler.py index fad35e8c1..6e847abb4 100644 --- a/robot/helper_actor_compiler.py +++ b/robot/helper_actor_compiler.py @@ -142,6 +142,55 @@ def main() -> int: print(f"actor-compiler-unexpected-error: {exc}") return 1 + if command == "lsp-binding-field": + # Regression test for issue #1432: _extract_lsp_bindings() must read + # from node.lsp_binding (NodeLspBinding typed field), not only config dict. + from cleveragents.actor.schema import ( + ActorType, + NodeDefinition, + NodeLspBinding, + NodeType, + RouteDefinition, + ) + + node = NodeDefinition( + id="coder", + type=NodeType.AGENT, + name="Coder", + description="Code writing node with typed LSP binding", + config={"agent": "coder_agent"}, + lsp_binding=NodeLspBinding( + server="local/pyright", + languages=["python"], + auto=False, + ), + ) + route = RouteDefinition( + nodes=[node], + edges=[], + entry_node="coder", + exit_nodes=["coder"], + ) + config = ActorConfigSchema( + name="workflows/lsp_typed", + type=ActorType.GRAPH, + description="Test actor with typed lsp_binding field", + model="gpt-4", + route=route, + ) + try: + compiled = compile_actor(config) + bindings = compiled.metadata.lsp_bindings + if len(bindings) == 1 and bindings[0].lsp_server_name == "local/pyright": + print("actor-compiler-lsp-binding-ok") + print(f"lsp-server: {bindings[0].lsp_server_name}") + return 0 + print(f"actor-compiler-lsp-binding-fail: got {bindings}") + return 1 + except Exception as exc: + print(f"actor-compiler-fail: {exc}") + return 1 + print(f"Unknown command: {command}") return 1 diff --git a/src/cleveragents/actor/compiler.py b/src/cleveragents/actor/compiler.py index bcdedc1dd..e4f90ecb0 100644 --- a/src/cleveragents/actor/compiler.py +++ b/src/cleveragents/actor/compiler.py @@ -157,19 +157,19 @@ def _map_edge(edge: EdgeDefinition) -> lg_nodes.Edge: def _extract_lsp_bindings(node: NodeDefinition) -> list[LspBinding]: """Extract LSP bindings from a node's dedicated lsp_binding field. - + Reads from the typed ``node.lsp_binding`` (NodeLspBinding) field and converts to LspBinding records. Also checks config dict for backward compatibility with legacy ``lsp_bindings`` config key. - + Args: node: The node definition to extract bindings from. - + Returns: List of LspBinding records for this node. """ bindings: list[LspBinding] = [] - + # Primary path: read from the dedicated typed field (NodeLspBinding) if node.lsp_binding is not None: bindings.append( @@ -177,15 +177,15 @@ def _extract_lsp_bindings(node: NodeDefinition) -> list[LspBinding]: node_name=node.id, lsp_server_name=node.lsp_binding.server or "", languages=node.lsp_binding.languages or [], - auto_detect=node.lsp_binding.auto if node.lsp_binding.auto is not None else True, + auto_detect=node.lsp_binding.auto, ) ) - + # Fallback path: check config dict for backward compatibility raw_bindings = node.config.get("lsp_bindings", []) if not isinstance(raw_bindings, list): return bindings - + for entry in raw_bindings: if not isinstance(entry, dict): continue @@ -200,7 +200,7 @@ def _extract_lsp_bindings(node: NodeDefinition) -> list[LspBinding]: auto_detect=entry.get("auto_detect", True), ) ) - + return bindings -- 2.52.0