Files
cleveragents-core/robot/helper_skill_flatten.py
hamza.khyari bdd1ea4f3a
CI / push-validation (pull_request) Successful in 10s
CI / build (pull_request) Successful in 16s
CI / helm (pull_request) Successful in 16s
CI / typecheck (pull_request) Successful in 30s
CI / lint (pull_request) Successful in 35s
CI / quality (pull_request) Successful in 35s
CI / security (pull_request) Successful in 50s
CI / integration_tests (pull_request) Successful in 4m3s
CI / e2e_tests (pull_request) Successful in 4m14s
CI / unit_tests (pull_request) Successful in 5m13s
CI / docker (pull_request) Successful in 8s
CI / coverage (pull_request) Successful in 11m2s
CI / status-check (pull_request) Successful in 1s
fix(checkpoint): wire CheckpointManager into PlanExecutor execution path
CheckpointManager was never wired into PlanExecutor — the CLI factory
constructed PlanExecutor without a checkpoint_manager (defaulted to
None), silently skipping all checkpoint hooks.

Fix:
- Register CheckpointManager as Singleton in DI container
- Resolve container singleton in _get_plan_executor() and pass to
  PlanExecutor constructor
- Bridge infra→domain: _try_create_checkpoint() now persists
  last_checkpoint_id on the plan via _commit_plan(), raises PlanError
  if persistence fails
- Default checkpointable=True for writable+sandboxable resources and
  write-capable tools (model_validators on ResourceCapabilities and
  ToolCapability)
- Validate that non-writable/non-sandboxable resources cannot be
  checkpointable (ValueError guard)
- Add post-execute A2A facade notification using plan.status to avoid
  duplicate execute→execute transition errors

Tests:
- 10 Behave scenarios covering DI wiring, singleton identity, checkpoint
  creation, plan metadata update, rollback, graceful fallback, no-arg
  constructor, capability defaults (positive + 2 negative)
- Updated consolidated_resource, consolidated_skill, and Robot
  helper_skill_flatten for new checkpointable defaults

ISSUES CLOSED: #1253
2026-04-17 11:46:38 +00:00

368 lines
11 KiB
Python

"""Robot Framework helper for skill flattening smoke tests.
Provides a CLI-style interface for Robot to invoke skill flattening,
capability summary computation, validate_plan(), and the tools() method.
Usage:
python robot/helper_skill_flatten.py flatten-simple
python robot/helper_skill_flatten.py flatten-deep
python robot/helper_skill_flatten.py flatten-wide
python robot/helper_skill_flatten.py flatten-inline
python robot/helper_skill_flatten.py flatten-cycle
python robot/helper_skill_flatten.py flatten-include-overrides
python robot/helper_skill_flatten.py flatten-non-overridable
python robot/helper_skill_flatten.py capability-summary
python robot/helper_skill_flatten.py tools-method
python robot/helper_skill_flatten.py validate-plan-ok
python robot/helper_skill_flatten.py validate-plan-missing
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
# Ensure the src directory is on the import path.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.domain.models.core.skill import ( # noqa: E402
Skill,
SkillInclude,
SkillInlineTool,
SkillResolver,
)
from cleveragents.domain.models.core.tool import ( # noqa: E402
ToolCapability,
ToolSource,
)
from cleveragents.skills.protocol import ( # noqa: E402
SkillDefinition,
SkillMetadata,
)
from cleveragents.skills.registry import SkillRegistry # noqa: E402
def _make_defn(skill: Skill) -> SkillDefinition:
"""Build a SkillDefinition from a Skill with default metadata."""
resolver = SkillResolver()
resolved = resolver.resolve_tools(skill, {})
meta = SkillMetadata.from_skill(skill, resolved=resolved)
return SkillDefinition(skill=skill, resolved_tools=resolved, metadata=meta)
def _flatten_simple() -> int:
"""Flatten a skill with 5 tool refs."""
skill = Skill(
name="local/simple",
description="Simple skill",
tool_refs=[f"local/tool-{i}" for i in range(5)],
)
resolver = SkillResolver()
result = resolver.resolve_tools(skill, {})
names = [e.name for e in result]
expected = [f"local/tool-{i}" for i in range(5)]
if names != expected:
print(f"flatten-fail: expected {expected}, got {names}")
return 1
print(f"flatten-simple-ok: {json.dumps(names)}")
return 0
def _flatten_deep() -> int:
"""Flatten a skill with 3 levels of includes."""
base = Skill(name="local/base", description="Base", tool_refs=["local/base-t"])
mid = Skill(
name="local/mid",
description="Mid",
tool_refs=["local/mid-t"],
includes=[SkillInclude(name="local/base")],
)
top = Skill(
name="local/top",
description="Top",
tool_refs=["local/top-t"],
includes=[SkillInclude(name="local/mid")],
)
registry = {
"local/base": base,
"local/mid": mid,
"local/top": top,
}
resolver = SkillResolver()
result = resolver.resolve_tools(top, registry)
names = [e.name for e in result]
expected = ["local/base-t", "local/mid-t", "local/top-t"]
if names != expected:
print(f"flatten-fail: expected {expected}, got {names}")
return 1
print(f"flatten-deep-ok: {json.dumps(names)}")
return 0
def _flatten_wide() -> int:
"""Flatten a skill that includes 10 other skills."""
children: dict[str, Skill] = {}
includes: list[SkillInclude] = []
for i in range(10):
name = f"local/child-{i}"
children[name] = Skill(
name=name,
description=f"Child {i}",
tool_refs=[f"local/child-{i}-tool"],
)
includes.append(SkillInclude(name=name))
top = Skill(name="local/wide", description="Wide", includes=includes)
registry = {**children, "local/wide": top}
resolver = SkillResolver()
result = resolver.resolve_tools(top, registry)
if len(result) != 10:
print(f"flatten-fail: expected 10 tools, got {len(result)}")
return 1
print(f"flatten-wide-ok: {len(result)} tools")
return 0
def _flatten_inline() -> int:
"""Flatten a skill with inline tools."""
skill = Skill(
name="local/inline",
description="Inline",
anonymous_tools=[
SkillInlineTool(
description=f"Inline {i}",
source=ToolSource.CUSTOM,
code=f"return {i}",
)
for i in range(3)
],
)
resolver = SkillResolver()
result = resolver.resolve_tools(skill, {})
if len(result) != 3:
print(f"flatten-fail: expected 3, got {len(result)}")
return 1
for i, entry in enumerate(result):
if not entry.is_inline:
print(f"flatten-fail: entry {i} not inline")
return 1
print(f"flatten-inline-ok: {len(result)} inline tools")
return 0
def _flatten_cycle() -> int:
"""Detect a cycle and verify error."""
a = Skill(
name="local/cyc-a",
description="A",
includes=[SkillInclude(name="local/cyc-b")],
)
b = Skill(
name="local/cyc-b",
description="B",
includes=[SkillInclude(name="local/cyc-a")],
)
registry = {"local/cyc-a": a, "local/cyc-b": b}
resolver = SkillResolver()
try:
resolver.resolve_tools(a, registry)
print("flatten-fail: expected cycle error")
return 1
except ValueError as exc:
if "ycle" not in str(exc):
print(f"flatten-fail: wrong error: {exc}")
return 1
print(f"flatten-cycle-ok: {exc}")
return 0
def _flatten_include_overrides() -> int:
"""Apply per-include overrides."""
child = Skill(
name="local/child",
description="Child",
tool_refs=["local/tool-x"],
)
parent = Skill(
name="local/parent",
description="Parent",
includes=[
SkillInclude(
name="local/child",
overrides={"timeout": 600},
)
],
)
registry = {"local/child": child, "local/parent": parent}
resolver = SkillResolver()
result = resolver.resolve_tools(parent, registry)
entry = result[0]
if entry.overrides.get("timeout") != 600:
print(f"flatten-fail: override not applied: {entry.overrides}")
return 1
print(f"flatten-include-overrides-ok: {entry.overrides}")
return 0
def _flatten_non_overridable() -> int:
"""Reject non-overridable fields."""
child = Skill(
name="local/child-nr",
description="Child",
tool_refs=["local/tool-y"],
)
parent = Skill(
name="local/parent-nr",
description="Parent",
includes=[
SkillInclude(
name="local/child-nr",
overrides={"name": "bad"},
)
],
)
registry = {"local/child-nr": child, "local/parent-nr": parent}
resolver = SkillResolver()
try:
resolver.resolve_tools(parent, registry)
print("flatten-fail: expected override rejection error")
return 1
except ValueError as exc:
if "Non-overridable" not in str(exc):
print(f"flatten-fail: wrong error: {exc}")
return 1
print(f"flatten-non-overridable-ok: {exc}")
return 0
def _capability_summary() -> int:
"""Compute capability summary with diverse inline tools."""
skill = Skill(
name="local/cap",
description="Cap test",
anonymous_tools=[
SkillInlineTool(
description="RO",
source=ToolSource.CUSTOM,
code="pass",
capability=ToolCapability(read_only=True),
),
SkillInlineTool(
description="Writer",
source=ToolSource.CUSTOM,
code="pass",
capability=ToolCapability(writes=True, side_effects=["fs"]),
),
SkillInlineTool(
description="Checkpoint",
source=ToolSource.CUSTOM,
code="pass",
capability=ToolCapability(checkpointable=True),
),
],
)
resolver = SkillResolver()
entries = resolver.resolve_tools(skill, {})
summary = resolver.compute_capability_summary(skill, entries)
checks = [
summary.total_tools == 3,
summary.read_only_tools == 1,
summary.write_tools == 1,
# Writer tool now defaults to checkpointable=True (writes=True
# AND NOT read_only), plus the explicit Checkpoint tool = 2.
summary.checkpointable_tools == 2,
summary.has_side_effects is True,
]
if not all(checks):
print(f"flatten-fail: summary checks failed: {summary}")
return 1
print("flatten-capability-summary-ok")
return 0
def _tools_method() -> int:
"""Call SkillRegistry.tools()."""
skill = Skill(
name="local/tm",
description="Tools method test",
tool_refs=["local/t1", "local/t2"],
)
defn = _make_defn(skill)
sr = SkillRegistry()
sr.register(defn)
entries, summary = sr.tools("local/tm")
if len(entries) != 2:
print(f"flatten-fail: expected 2 entries, got {len(entries)}")
return 1
if summary.total_tools != 2:
print(f"flatten-fail: summary total={summary.total_tools}")
return 1
print("flatten-tools-method-ok")
return 0
def _validate_plan_ok() -> int:
"""validate_plan with valid plan."""
skill = Skill(
name="local/vp",
description="VP test",
tool_refs=["local/t1"],
)
defn = _make_defn(skill)
sr = SkillRegistry()
sr.register(defn)
errors = sr.validate_plan({"skills": ["local/vp"]})
if errors:
print(f"flatten-fail: unexpected errors: {errors}")
return 1
print("flatten-validate-plan-ok")
return 0
def _validate_plan_missing() -> int:
"""validate_plan with missing skill."""
sr = SkillRegistry()
errors = sr.validate_plan({"skills": ["local/ghost"]})
if not errors:
print("flatten-fail: expected error for missing skill")
return 1
if "local/ghost" not in errors[0]:
print(f"flatten-fail: wrong error: {errors[0]}")
return 1
print(f"flatten-validate-plan-missing-ok: {errors[0]}")
return 0
def main() -> int:
"""Entry point called by Robot Framework ``Run Process``."""
if len(sys.argv) < 2:
print("Usage: helper_skill_flatten.py <command>")
return 1
commands = {
"flatten-simple": _flatten_simple,
"flatten-deep": _flatten_deep,
"flatten-wide": _flatten_wide,
"flatten-inline": _flatten_inline,
"flatten-cycle": _flatten_cycle,
"flatten-include-overrides": _flatten_include_overrides,
"flatten-non-overridable": _flatten_non_overridable,
"capability-summary": _capability_summary,
"tools-method": _tools_method,
"validate-plan-ok": _validate_plan_ok,
"validate-plan-missing": _validate_plan_missing,
}
handler = commands.get(sys.argv[1])
if handler is None:
print(f"Unknown command: {sys.argv[1]}")
return 1
return handler()
if __name__ == "__main__":
sys.exit(main())