cb82fc51df
Implemented AgentSkillSpec loader that parses SKILL.md frontmatter and progressive disclosure sections (discover/activate/deactivate) into structured SkillStep objects with stable 1-based ordering. Mapped Agent Skills to AgentSkillToolDescriptor with namespaced naming (namespace/short_name), source="agent_skill", read-only defaults, and AgentSkillResourceSlot bindings for scripts/, references/, and assets/ directories. All resource slots are unconditionally read_only. Added explicit validation for missing frontmatter fields (name, description) and invalid namespace format with actionable error messages. Added docs/reference/agent_skills.md covering folder layout, SKILL.md parsing rules, progressive disclosure model, and tool mapping. Added Behave scenarios covering valid/invalid SKILL.md parsing, namespaced naming, step ordering, missing frontmatter errors, progressive disclosure lifecycle, tool mapping, and resource binding slots. Added Robot Framework integration tests using the deploy-to-staging example skill folder (robot/agent_skills_loader.robot). Added ASV benchmarks for parsing throughput, folder load, progressive disclosure lifecycle, and resource listing (benchmarks/agent_skills_loader_bench.py). ISSUES CLOSED: #160
221 lines
6.6 KiB
Python
221 lines
6.6 KiB
Python
"""Robot Framework helper for Agent Skills Loader integration tests.
|
|
|
|
Provides a CLI-style interface for Robot to invoke Agent Skills loader
|
|
operations and verify the results. Exit code 0 = success, 1 = failure.
|
|
|
|
Usage:
|
|
python robot/helper_agent_skills_loader.py load-skill <folder_path>
|
|
python robot/helper_agent_skills_loader.py discover <folder_path>
|
|
python robot/helper_agent_skills_loader.py activate <folder_path>
|
|
python robot/helper_agent_skills_loader.py tool-descriptor <folder_path>
|
|
python robot/helper_agent_skills_loader.py invalid-skill <folder_path>
|
|
python robot/helper_agent_skills_loader.py list-resources <folder_path>
|
|
"""
|
|
|
|
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.skills.agent_skills_loader import AgentSkillLoader # noqa: E402
|
|
|
|
|
|
def _cmd_load_skill(folder: str) -> int:
|
|
"""Load a skill folder and verify the spec is populated."""
|
|
try:
|
|
loader = AgentSkillLoader.from_folder(Path(folder))
|
|
except (ValueError, FileNotFoundError) as exc:
|
|
print(f"agent-skills-fail: {exc}")
|
|
return 1
|
|
|
|
if not loader.spec.name:
|
|
print("agent-skills-fail: name is empty")
|
|
return 1
|
|
if not loader.spec.description:
|
|
print("agent-skills-fail: description is empty")
|
|
return 1
|
|
|
|
print(f"agent-skills-load-ok: {loader.spec.name}")
|
|
return 0
|
|
|
|
|
|
def _cmd_discover(folder: str) -> int:
|
|
"""Call discover() and verify metadata-only descriptor."""
|
|
try:
|
|
loader = AgentSkillLoader.from_folder(Path(folder))
|
|
except (ValueError, FileNotFoundError) as exc:
|
|
print(f"agent-skills-fail: {exc}")
|
|
return 1
|
|
|
|
descriptor = loader.discover()
|
|
|
|
if descriptor.source != "agent_skill":
|
|
print(
|
|
f"agent-skills-fail: expected source=agent_skill, got {descriptor.source}"
|
|
)
|
|
return 1
|
|
if descriptor.body:
|
|
print(
|
|
f"agent-skills-fail: body should be empty at discover, "
|
|
f"got: {descriptor.body!r}"
|
|
)
|
|
return 1
|
|
if not descriptor.name:
|
|
print("agent-skills-fail: name is empty")
|
|
return 1
|
|
|
|
print(f"agent-skills-discover-ok: {descriptor.name}")
|
|
return 0
|
|
|
|
|
|
def _cmd_activate(folder: str) -> int:
|
|
"""Call discover() then activate() and verify body is loaded."""
|
|
try:
|
|
loader = AgentSkillLoader.from_folder(Path(folder))
|
|
except (ValueError, FileNotFoundError) as exc:
|
|
print(f"agent-skills-fail: {exc}")
|
|
return 1
|
|
|
|
loader.discover()
|
|
activated = loader.activate()
|
|
|
|
if not activated.body:
|
|
print("agent-skills-fail: body is empty after activate")
|
|
return 1
|
|
|
|
print(f"agent-skills-activate-ok: body_len={len(activated.body)}")
|
|
return 0
|
|
|
|
|
|
def _cmd_tool_descriptor(folder: str) -> int:
|
|
"""Convert loaded skill to tool descriptor and verify fields."""
|
|
try:
|
|
loader = AgentSkillLoader.from_folder(Path(folder))
|
|
except (ValueError, FileNotFoundError) as exc:
|
|
print(f"agent-skills-fail: {exc}")
|
|
return 1
|
|
|
|
td = loader.to_tool_descriptor()
|
|
|
|
if td.source != "agent_skill":
|
|
print(f"agent-skills-fail: expected source=agent_skill, got {td.source}")
|
|
return 1
|
|
if not td.read_only:
|
|
print("agent-skills-fail: expected read_only=True")
|
|
return 1
|
|
if td.writes:
|
|
print("agent-skills-fail: expected writes=False")
|
|
return 1
|
|
if not td.agent_skill_path:
|
|
print("agent-skills-fail: agent_skill_path is empty")
|
|
return 1
|
|
if not td.name:
|
|
print("agent-skills-fail: tool descriptor name is empty")
|
|
return 1
|
|
|
|
print(f"agent-skills-tool-descriptor-ok: {td.name}")
|
|
print(f" resource_slots={len(td.resource_slots)}")
|
|
for slot in td.resource_slots:
|
|
print(f" slot: name={slot.name} access={slot.access}")
|
|
return 0
|
|
|
|
|
|
def _cmd_invalid_skill(folder: str) -> int:
|
|
"""Attempt to load an invalid skills folder and verify error."""
|
|
try:
|
|
AgentSkillLoader.from_folder(Path(folder))
|
|
print("agent-skills-fail: expected error but none was raised")
|
|
return 1
|
|
except (ValueError, FileNotFoundError) as exc:
|
|
print(f"agent-skills-invalid-ok: {exc}")
|
|
return 0
|
|
|
|
|
|
def _cmd_list_resources(folder: str) -> int:
|
|
"""List resources from a skill folder."""
|
|
try:
|
|
loader = AgentSkillLoader.from_folder(Path(folder))
|
|
except (ValueError, FileNotFoundError) as exc:
|
|
print(f"agent-skills-fail: {exc}")
|
|
return 1
|
|
|
|
resources = loader.list_resources()
|
|
print(f"agent-skills-list-resources-ok: count={len(resources)}")
|
|
for p in resources:
|
|
print(f" resource: {p.name}")
|
|
return 0
|
|
|
|
|
|
def _cmd_steps(folder: str) -> int:
|
|
"""Load a skill folder and print structured steps from the spec."""
|
|
try:
|
|
loader = AgentSkillLoader.from_folder(Path(folder))
|
|
except (ValueError, FileNotFoundError) as exc:
|
|
print(f"agent-skills-fail: {exc}")
|
|
return 1
|
|
|
|
steps = loader.spec.steps
|
|
print(f"agent-skills-steps-ok: count={len(steps)}")
|
|
for step in steps:
|
|
print(f" step[{step.index}]: {step.content}")
|
|
return 0
|
|
|
|
|
|
def _cmd_resource_slots(folder: str) -> int:
|
|
"""Load a skill folder and print resource binding slots."""
|
|
try:
|
|
loader = AgentSkillLoader.from_folder(Path(folder))
|
|
except (ValueError, FileNotFoundError) as exc:
|
|
print(f"agent-skills-fail: {exc}")
|
|
return 1
|
|
|
|
slots = loader.resource_slots
|
|
print(f"agent-skills-resource-slots-ok: count={len(slots)}")
|
|
for slot in slots:
|
|
print(
|
|
f" slot: name={slot.name} type={slot.resource_type} access={slot.access}"
|
|
)
|
|
return 0
|
|
|
|
|
|
_COMMANDS = {
|
|
"load-skill": _cmd_load_skill,
|
|
"discover": _cmd_discover,
|
|
"activate": _cmd_activate,
|
|
"tool-descriptor": _cmd_tool_descriptor,
|
|
"invalid-skill": _cmd_invalid_skill,
|
|
"list-resources": _cmd_list_resources,
|
|
"steps": _cmd_steps,
|
|
"resource-slots": _cmd_resource_slots,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
"""Entry point called by Robot Framework ``Run Process``."""
|
|
if len(sys.argv) < 3:
|
|
print(
|
|
"Usage: helper_agent_skills_loader.py "
|
|
"<load-skill|discover|activate|tool-descriptor"
|
|
"|invalid-skill|list-resources> "
|
|
"<folder_path>"
|
|
)
|
|
return 1
|
|
|
|
command = sys.argv[1]
|
|
folder = sys.argv[2]
|
|
|
|
handler = _COMMANDS.get(command)
|
|
if handler is None:
|
|
print(f"Unknown command: {command}")
|
|
return 1
|
|
|
|
return handler(folder)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|