Files
cleveragents-core/robot/helper_actor_compiler.py
T
HAL9000 5db663cb63 fix(actor): read actor_ref from NodeDefinition field in _detect_subgraph_cycles
Fix cross-actor subgraph cycle detection in the actor compiler by reading
actor_ref from the top-level NodeDefinition field instead of the config dict.

The _detect_subgraph_cycles(), _map_node(), and compile_actor() functions
were all reading node.config.get("actor_ref", "") instead of node.actor_ref.
Because actor_ref is a typed, validated Pydantic field on NodeDefinition (not
a key inside the untyped config dict), the old code always returned an empty
string, causing cross-actor cycle detection to silently fail and leaving the
system vulnerable to infinite recursion at runtime.

Changes:
- Fix all three call sites in src/cleveragents/actor/compiler.py
- Add Behave regression tests (features/actor_subgraph_cycle_detection.feature)
  with @tdd_issue and @tdd_issue_1431 tags on all scenarios
- Add Robot Framework integration test (robot/actor_compiler.robot)
- Fix existing test helpers to use actor_ref as top-level field
- Add CHANGELOG entry

ISSUES CLOSED: #1431
2026-05-06 04:08:25 +00:00

151 lines
5.1 KiB
Python

"""Robot Framework helper for actor compiler smoke tests.
Provides a CLI interface for Robot to invoke the actor compiler on
YAML-defined GRAPH actors and inspect compilation metadata.
Usage:
python robot/helper_actor_compiler.py compile <yaml_file>
python robot/helper_actor_compiler.py compile-fail <yaml_file>
python robot/helper_actor_compiler.py metadata <yaml_file>
"""
from __future__ import annotations
import json
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 ActorConfigSchema # noqa: E402
def main() -> int:
"""Entry point called by Robot Framework ``Run Process``."""
if len(sys.argv) < 3:
print("Usage: helper_actor_compiler.py <compile|compile-fail|metadata> <file>")
return 1
command = sys.argv[1]
yaml_path = sys.argv[2]
if command == "compile":
try:
config = ActorConfigSchema.from_yaml_file(yaml_path)
compiled = compile_actor(config)
print(f"actor-compiler-ok: {compiled.name}")
print(f"nodes: {len(compiled.nodes)}")
print(f"edges: {len(compiled.edges)}")
print(f"entry: {compiled.entry_point}")
return 0
except Exception as exc:
print(f"actor-compiler-fail: {exc}")
return 1
if command == "compile-fail":
try:
config = ActorConfigSchema.from_yaml_file(yaml_path)
compile_actor(config)
print("actor-compiler-unexpected-success")
return 1
except Exception as exc:
print(f"actor-compiler-expected-fail: {exc}")
return 0
if command == "metadata":
try:
config = ActorConfigSchema.from_yaml_file(yaml_path)
compiled = compile_actor(config)
meta = compiled.metadata.model_dump(mode="json")
print(f"actor-compiler-metadata: {json.dumps(meta)}")
return 0
except Exception as exc:
print(f"actor-compiler-fail: {exc}")
return 1
if command == "cycle-detect":
# Test cross-actor cycle detection using actor_ref top-level field.
# Creates two mutually-referencing actors and verifies SubgraphCycleError
# is raised, confirming the fix for issue #1431.
from cleveragents.actor.compiler import SubgraphCycleError
from cleveragents.actor.schema import (
ActorType,
EdgeDefinition,
NodeDefinition,
NodeType,
RouteDefinition,
)
def _make_actor(
name: str, subgraph_ref: str | None = None
) -> ActorConfigSchema:
if subgraph_ref is not None:
nodes = [
NodeDefinition(
id="start",
type=NodeType.AGENT,
name="Start",
description="Start",
config={"agent": "default"},
),
NodeDefinition(
id="sub",
type=NodeType.SUBGRAPH,
name="Sub",
description="Subgraph",
config={},
actor_ref=subgraph_ref,
),
]
edges = [EdgeDefinition(from_node="start", to_node="sub")]
entry, exits = "start", ["sub"]
else:
nodes = [
NodeDefinition(
id="leaf",
type=NodeType.AGENT,
name="Leaf",
description="Leaf",
config={"agent": "default"},
)
]
edges = []
entry, exits = "leaf", ["leaf"]
route = RouteDefinition(
nodes=nodes, edges=edges, entry_node=entry, exit_nodes=exits
)
return ActorConfigSchema(
name=name,
type=ActorType.GRAPH,
description=f"Test actor {name}",
model="gpt-4",
route=route,
)
actor_a = _make_actor("test/actor-a", subgraph_ref="test/actor-b")
actor_b = _make_actor("test/actor-b", subgraph_ref="test/actor-a")
registry = {"test/actor-a": actor_a, "test/actor-b": actor_b}
try:
compile_actor(actor_a, actor_resolver=registry.get)
print(
"actor-compiler-cycle-not-detected: FAIL — expected SubgraphCycleError"
)
return 1
except SubgraphCycleError as exc:
print(f"actor-compiler-cycle-detected: {exc}")
return 0
except Exception as exc:
print(f"actor-compiler-unexpected-error: {exc}")
return 1
print(f"Unknown command: {command}")
return 1
if __name__ == "__main__":
sys.exit(main())