Files
cleveragents-core/robot/helper_skill_discovery.py
freemo c47e6445d0
CI / quality (pull_request) Successful in 19s
CI / lint (pull_request) Successful in 22s
CI / benchmark-publish (pull_request) Has been skipped
CI / security (pull_request) Successful in 33s
CI / build (pull_request) Successful in 26s
CI / typecheck (pull_request) Successful in 1m0s
CI / integration_tests (pull_request) Successful in 4m56s
CI / unit_tests (pull_request) Successful in 15m11s
CI / docker (pull_request) Successful in 1m33s
CI / benchmark-regression (pull_request) Successful in 20m55s
CI / coverage (pull_request) Successful in 33m42s
CI / lint (push) Successful in 13s
CI / build (push) Successful in 15s
CI / quality (push) Successful in 17s
CI / security (push) Successful in 28s
CI / typecheck (push) Successful in 32s
CI / benchmark-regression (push) Has been skipped
CI / integration_tests (push) Successful in 3m3s
CI / benchmark-publish (push) Successful in 10m8s
CI / unit_tests (push) Successful in 12m42s
CI / docker (push) Successful in 39s
CI / coverage (push) Has been cancelled
feat(actor): compile hierarchical actor configs to LangGraph
Add ActorCompiler module that translates GRAPH-type ActorConfigSchema
definitions into LangGraph NodeConfig/Edge structures with LSP binding
metadata. Includes subgraph resolution with cross-actor cycle detection,
entry/exit validation, and CompilationMetadata for diagnostics.

New files:
- src/cleveragents/actor/compiler.py: Core compiler with compile_actor()
- features/actor_compiler.feature: 13 Behave scenarios
- features/steps/actor_compiler_steps.py: Step definitions
- robot/actor_compiler.robot: 4 Robot smoke tests
- benchmarks/actor_compiler_bench.py: ASV performance benchmarks
- docs/reference/actor_compiler.md: Compilation pipeline reference

Modified:
- src/cleveragents/actor/__init__.py: Export compiler types
- vulture_whitelist.py: Whitelist new public API

ISSUES CLOSED: #158
2026-02-24 17:57:18 +00:00

148 lines
4.2 KiB
Python

"""Robot Framework helper for Agent Skills discovery tests."""
from __future__ import annotations
import shutil
import tempfile
from pathlib import Path
from typing import Any
from cleveragents.application.services.config_service import ConfigService
from cleveragents.skills.discovery import (
DiscoveredAgentSkill,
build_tool_spec,
register_discovered_skills,
scan_directory,
)
from cleveragents.skills.discovery import (
parse_agent_skills_paths as _parse_paths,
)
from cleveragents.tool.registry import ToolRegistry
from cleveragents.tool.runtime import ToolSpec
def _noop_handler(**_kwargs: Any) -> dict[str, Any]:
return {"status": "placeholder"}
def _create_skill_md(folder: Path, name: str, description: str) -> None:
folder.mkdir(parents=True, exist_ok=True)
content = (
"---\n"
f"name: {name}\n"
f"description: {description}\n"
"---\n"
f"\n# {name}\n\n{description}\n"
)
(folder / "SKILL.md").write_text(content, encoding="utf-8")
# ---------------------------------------------------------------------------
# Robot keywords
# ---------------------------------------------------------------------------
def parse_agent_skills_paths(raw_value: str) -> list[str]:
"""Parse comma-separated paths and return as string list."""
paths = _parse_paths(raw_value)
return [str(p) for p in paths]
def create_agent_skills_directory(count: int) -> str:
"""Create a temp directory with *count* agent skill folders."""
tmpdir = Path(tempfile.mkdtemp())
for i in range(int(count)):
_create_skill_md(tmpdir / f"skill-{i}", f"skill-{i}", f"Skill {i}")
return str(tmpdir)
def create_mixed_directory() -> str:
"""Create a directory with one valid skill and one without SKILL.md."""
tmpdir = Path(tempfile.mkdtemp())
_create_skill_md(tmpdir / "valid-skill", "valid-skill", "A valid skill")
(tmpdir / "no-skill").mkdir()
return str(tmpdir)
def scan_agent_skills_directory(directory: str) -> list[dict[str, Any]]:
"""Scan a directory and return discovered skills as dicts."""
results = scan_directory(Path(directory))
return [
{"name": s.name, "description": s.description, "path": s.path} for s in results
]
def build_agent_skill_toolspec(name: str, path: str) -> ToolSpec:
"""Build a ToolSpec from a discovered agent skill."""
skill = DiscoveredAgentSkill(
name=name,
description=f"Agent skill {name}",
path=path,
)
return build_tool_spec(skill)
def register_agent_skills_in_fresh_registry(
count: int,
) -> dict[str, int]:
"""Register discovered skills in a fresh ToolRegistry."""
registry = ToolRegistry()
discovered = [
DiscoveredAgentSkill(
name=f"tool-{i}",
description=f"Tool {i}",
path=f"/tmp/skills/tool-{i}",
)
for i in range(int(count))
]
registered, conflicts = register_discovered_skills(
discovered, registry, on_conflict="skip"
)
return {
"registered_count": len(registered),
"conflict_count": len(conflicts),
}
def register_agent_skills_with_collision(
strategy: str,
) -> dict[str, int]:
"""Register a skill that collides with an existing tool."""
registry = ToolRegistry()
existing = ToolSpec(
name="agent_skills/collider",
description="Existing tool",
handler=_noop_handler,
)
registry.register(existing)
discovered = [
DiscoveredAgentSkill(
name="collider",
description="Conflicting skill",
path="/tmp/skills/collider",
)
]
try:
registered, conflicts = register_discovered_skills(
discovered, registry, on_conflict=strategy
)
except ValueError:
return {"registered_count": 0, "conflict_count": 1}
return {
"registered_count": len(registered),
"conflict_count": len(conflicts),
}
def get_config_entry(key: str) -> Any:
"""Get a config entry from the registry."""
return ConfigService.get_entry(key)
def cleanup_temp_directory(path: str) -> None:
"""Remove a temporary directory."""
shutil.rmtree(path, ignore_errors=True)