diff --git a/CHANGELOG.md b/CHANGELOG.md index 3872d17a0..c382817f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1033,6 +1033,16 @@ iteration` and data corruption under concurrent plan execution. All public response format from the OpenCode API `/session/status` endpoint instead of an array. Workers now dispatch and verify correctly, preventing incorrect session deletion. +- **Actor compiler ignores `actor_ref` field on SUBGRAPH nodes** (#1429): Fixed the + actor compiler (`src/cleveragents/actor/compiler.py`) to read `actor_ref` from the + top-level `NodeDefinition.actor_ref` field instead of `node.config.get("actor_ref")`. + Before this fix, `CompiledActor.metadata.subgraph_refs` was always empty and + `NodeConfig.subgraph` on every SUBGRAPH node was always `None`, silently breaking all + hierarchical/nested actor graph compositions. Added Behave regression tests covering + subgraph compilation with `actor_ref` fields and Robot Framework integration tests + verifying that `subgraph_refs` is correctly populated after compilation. + + --- ### Fixed diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index d121f9b9e..1264eaf25 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -54,3 +54,4 @@ Below are some of the specific details of various contributions. 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. +* HAL 9000 has contributed the actor compiler `actor_ref` field fix (issue #1429): corrected `_map_node()` and `compile_actor()` in `src/cleveragents/actor/compiler.py` to read `actor_ref` from the top-level `NodeDefinition.actor_ref` field instead of `node.config.get("actor_ref")`, resolving silent failures on all SUBGRAPH nodes where `subgraph_refs` was always empty and `NodeConfig.subgraph` was always `None`. diff --git a/benchmarks/actor_compiler_bench.py b/benchmarks/actor_compiler_bench.py index b3885073b..ea706fc9f 100644 --- a/benchmarks/actor_compiler_bench.py +++ b/benchmarks/actor_compiler_bench.py @@ -142,7 +142,8 @@ class SubgraphResolutionSuite: type=NodeType.SUBGRAPH, name="Sub", description="Subgraph ref", - config={"actor_ref": "bench/inner"}, + config={}, + actor_ref="bench/inner", ), ] outer_edges = [EdgeDefinition(from_node="main", to_node="sub")] diff --git a/features/tdd_actor_compiler_actor_ref_1429.feature b/features/tdd_actor_compiler_actor_ref_1429.feature new file mode 100644 index 000000000..366f2b5fd --- /dev/null +++ b/features/tdd_actor_compiler_actor_ref_1429.feature @@ -0,0 +1,39 @@ +Feature: Actor compiler reads actor_ref from NodeDefinition field + As a CleverAgents developer + I want the actor compiler to read actor_ref from the NodeDefinition.actor_ref field + So that SUBGRAPH nodes correctly populate NodeConfig.subgraph and metadata.subgraph_refs + + Background: + Given the actor compiler is available + + # Issue #1429: _map_node() and compile_actor() were reading + # node.config.get("actor_ref") instead of node.actor_ref, causing SUBGRAPH + # nodes to silently lose their actor references during compilation. + + @tdd_issue @tdd_issue_1429 + Scenario: Compiled SUBGRAPH node has subgraph field populated from actor_ref + Given actor "test/outer-1429-a" has a subgraph node with actor_ref "test/inner-1429-a" + And actor "test/inner-1429-a" has no subgraph nodes + And a registry containing both actors + When I compile actor "test/outer-1429-a" with the registry resolver + Then the actor compilation should succeed + And the compiled node "sub" should have subgraph set to "test/inner-1429-a" + + @tdd_issue @tdd_issue_1429 + Scenario: Compiled actor metadata subgraph_refs populated from actor_ref field + Given actor "test/outer-1429-b" has a subgraph node with actor_ref "test/inner-1429-b" + And actor "test/inner-1429-b" has no subgraph nodes + And a registry containing both actors + When I compile actor "test/outer-1429-b" with the registry resolver + Then the actor compilation should succeed + And the actor subgraph_refs should map "sub" to "test/inner-1429-b" + + @tdd_issue @tdd_issue_1429 + Scenario: Both NodeConfig.subgraph and subgraph_refs populated for SUBGRAPH node + Given actor "test/outer-1429-c" has a subgraph node with actor_ref "local/code-reviewer" + And actor "local/code-reviewer" has no subgraph nodes + And a registry containing both actors + When I compile actor "test/outer-1429-c" with the registry resolver + Then the actor compilation should succeed + And the compiled node "sub" should have subgraph set to "local/code-reviewer" + And the actor subgraph_refs should map "sub" to "local/code-reviewer" diff --git a/robot/helper_tdd_actor_compiler_actor_ref_1429.py b/robot/helper_tdd_actor_compiler_actor_ref_1429.py new file mode 100644 index 000000000..1bdfd6ca7 --- /dev/null +++ b/robot/helper_tdd_actor_compiler_actor_ref_1429.py @@ -0,0 +1,157 @@ +"""Robot Framework helper for tdd_actor_compiler_actor_ref_1429 tests. + +Regression tests for issue #1429: the actor compiler must read actor_ref +from NodeDefinition.actor_ref (a typed Pydantic field), not from +node.config.get("actor_ref") (a raw dict lookup). + +Two aspects are tested: + subgraph-field -- _map_node() sets NodeConfig.subgraph from node.actor_ref + subgraph-refs -- compile_actor() populates metadata.subgraph_refs from + node_def.actor_ref + +Usage: + python robot/helper_tdd_actor_compiler_actor_ref_1429.py subgraph-field + python robot/helper_tdd_actor_compiler_actor_ref_1429.py subgraph-refs +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_SRC = str(Path(__file__).resolve().parents[1] / "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +from cleveragents.actor.compiler import compile_actor # noqa: E402 +from cleveragents.actor.schema import ( # noqa: E402 + ActorConfigSchema, + ActorType, + EdgeDefinition, + NodeDefinition, + NodeType, + RouteDefinition, +) + +_ACTOR_REF = "local/code-reviewer" + + +def _make_outer_actor() -> ActorConfigSchema: + """GRAPH actor with one AGENT node and one SUBGRAPH node referencing _ACTOR_REF.""" + nodes = [ + NodeDefinition( + id="start", + type=NodeType.AGENT, + name="Start", + description="Start node", + config={"agent": "default"}, + ), + NodeDefinition( + id="sub", + type=NodeType.SUBGRAPH, + name="Sub", + description="Subgraph node", + config={}, + actor_ref=_ACTOR_REF, + ), + ] + route = RouteDefinition( + nodes=nodes, + edges=[EdgeDefinition(from_node="start", to_node="sub")], + entry_node="start", + exit_nodes=["sub"], + ) + return ActorConfigSchema( + name="test/outer-1429", + type=ActorType.GRAPH, + description="Outer actor for #1429 regression test", + provider="openai", + model="gpt-4", + route=route, + ) + + +def _make_inner_actor() -> ActorConfigSchema: + """Simple leaf GRAPH actor with no subgraph nodes.""" + nodes = [ + NodeDefinition( + id="leaf", + type=NodeType.AGENT, + name="Leaf", + description="Leaf node", + config={"agent": "default"}, + ) + ] + route = RouteDefinition( + nodes=nodes, + edges=[], + entry_node="leaf", + exit_nodes=["leaf"], + ) + return ActorConfigSchema( + name=_ACTOR_REF, + type=ActorType.GRAPH, + description="Inner actor for #1429 regression test", + provider="openai", + model="gpt-4", + route=route, + ) + + +def main() -> int: + if len(sys.argv) < 2: + print( + "Usage: helper_tdd_actor_compiler_actor_ref_1429.py" + " " + ) + return 1 + + command = sys.argv[1] + outer = _make_outer_actor() + inner = _make_inner_actor() + registry = {outer.name: outer, inner.name: inner} + + if command == "subgraph-field": + # Tests _map_node(): NodeConfig.subgraph must be set from node.actor_ref + try: + compiled = compile_actor(outer, actor_resolver=registry.get) + node_cfg = compiled.nodes.get("sub") + if node_cfg is None: + print("tdd-1429-subgraph-field-fail: 'sub' not in compiled.nodes") + return 1 + if node_cfg.subgraph != _ACTOR_REF: + print( + f"tdd-1429-subgraph-field-fail: " + f"expected subgraph={_ACTOR_REF!r}, got {node_cfg.subgraph!r}" + ) + return 1 + print(f"tdd-1429-subgraph-field-ok: subgraph={node_cfg.subgraph!r}") + return 0 + except Exception as exc: + print(f"tdd-1429-subgraph-field-error: {exc}") + return 1 + + if command == "subgraph-refs": + # Tests compile_actor(): metadata.subgraph_refs must be populated + # from node_def.actor_ref + try: + compiled = compile_actor(outer, actor_resolver=registry.get) + refs = compiled.metadata.subgraph_refs + if refs.get("sub") != _ACTOR_REF: + print( + f"tdd-1429-subgraph-refs-fail: " + f"expected subgraph_refs['sub']={_ACTOR_REF!r}, got {refs!r}" + ) + return 1 + print(f"tdd-1429-subgraph-refs-ok: subgraph_refs={refs!r}") + return 0 + except Exception as exc: + print(f"tdd-1429-subgraph-refs-error: {exc}") + return 1 + + print(f"Unknown command: {command}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/robot/tdd_actor_compiler_actor_ref_1429.robot b/robot/tdd_actor_compiler_actor_ref_1429.robot new file mode 100644 index 000000000..a9527bbdf --- /dev/null +++ b/robot/tdd_actor_compiler_actor_ref_1429.robot @@ -0,0 +1,39 @@ +*** Settings *** +Documentation TDD regression tests for issue #1429: actor compiler reads +... actor_ref from NodeDefinition.actor_ref field, not from the +... raw node.config dict. +... +... Two compiler functions are covered: +... _map_node() -- sets NodeConfig.subgraph +... compile_actor() -- populates metadata.subgraph_refs +Resource ${CURDIR}/common.resource +Suite Setup Setup Test Environment +Suite Teardown Cleanup Test Environment + +*** Variables *** +${HELPER} ${CURDIR}/helper_tdd_actor_compiler_actor_ref_1429.py + +*** Test Cases *** +NodeConfig subgraph Set From actor_ref Field In _map_node + [Documentation] Verify _map_node() reads actor_ref from NodeDefinition.actor_ref + ... so that NodeConfig.subgraph is populated for SUBGRAPH nodes. + ... Before the fix for #1429 this always returned None because the compiler + ... used config.get("actor_ref") on a dict that never contained the key. + [Tags] slow + ${result}= Run Process ${PYTHON} ${HELPER} subgraph-field cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tdd-1429-subgraph-field-ok + +Metadata subgraph_refs Populated From actor_ref Field In compile_actor + [Documentation] Verify compile_actor() reads actor_ref from NodeDefinition.actor_ref + ... so that metadata.subgraph_refs maps SUBGRAPH node IDs to referenced actor names. + ... Before the fix for #1429 subgraph_refs was always empty because the compiler + ... used node_def.config.get("actor_ref", "") on a dict that never contained the key. + [Tags] slow + ${result}= Run Process ${PYTHON} ${HELPER} subgraph-refs cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tdd-1429-subgraph-refs-ok