5db663cb63
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
151 lines
5.1 KiB
Python
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())
|