"""Skill tools — load and finish skills during agent operation.""" from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar from pydantic import BaseModel, Field from app.models.config import AppConfig from app.models.tool_call import ToolResult, ToolResultStatus from app.tools.base import BaseTool if TYPE_CHECKING: from app.services.skill_runner import SkillRunner from app.services.skills import SkillsManager class LoadSkillParams(BaseModel): """Parameters for the load_skill tool.""" name: str = Field(description="Name of the skill to load") class LoadSkillTool(BaseTool): """Load a skill's full instructions by name. Use when a skill is relevant to the current task. For package skills, this activates the full skill lifecycle (config overrides, chaining, prompt injection). """ name: ClassVar[str] = "load_skill" description: ClassVar[str] = ( "Load a skill's full instructions by name. " "Use when a skill is relevant to the current task." ) params_model: ClassVar[type[BaseModel]] = LoadSkillParams def __init__( self, workspace_root: Path, config: AppConfig, skills_manager: SkillsManager, skill_runner: SkillRunner | None = None, ) -> None: super().__init__(workspace_root, config) self._skills = skills_manager self._runner = skill_runner def set_skill_runner(self, runner: SkillRunner) -> None: """Late-bind the SkillRunner (avoids circular init dependencies).""" self._runner = runner def execute(self, *, tool_call_id: str, **kwargs: Any) -> ToolResult: skill_name: str = kwargs["name"] # Check if skill exists skill = self._skills.get_skill(skill_name) if skill is None: available = [s.name for s in self._skills.list_skills()] return ToolResult( tool_call_id=tool_call_id, tool_name=self.name, status=ToolResultStatus.ERROR, error=f"Unknown skill '{skill_name}'. Available: {available}", ) # For package skills with a runner, use full activation flow if skill.manifest is not None and self._runner is not None: content = self._runner.activate(skill_name) if content is None: return ToolResult( tool_call_id=tool_call_id, tool_name=self.name, status=ToolResultStatus.ERROR, error=f"Failed to activate skill '{skill_name}'", ) return ToolResult( tool_call_id=tool_call_id, tool_name=self.name, status=ToolResultStatus.SUCCESS, output=f"Skill '{skill_name}' activated.\n\n{content}", ) # Legacy skill: just load content content = self._skills.load_skill(skill_name) if content is None: return ToolResult( tool_call_id=tool_call_id, tool_name=self.name, status=ToolResultStatus.ERROR, error=f"Failed to load skill '{skill_name}'", ) return ToolResult( tool_call_id=tool_call_id, tool_name=self.name, status=ToolResultStatus.SUCCESS, output=content, ) class FinishSkillParams(BaseModel): """Parameters for the finish_skill tool.""" summary: str = Field( default="Skill complete.", description="Brief summary of what was accomplished during the skill", ) class FinishSkillTool(BaseTool): """Signal that the active skill is complete and should be deactivated. Restores config overrides and tool availability to pre-skill state. The agent loop continues after this (unlike the finish tool). """ name: ClassVar[str] = "finish_skill" description: ClassVar[str] = ( "Call this when the active skill's task is complete. " "Deactivates the skill and restores normal config. " "The conversation continues after this." ) params_model: ClassVar[type[BaseModel]] = FinishSkillParams def __init__( self, workspace_root: Path, config: AppConfig, skill_runner: SkillRunner | None = None, ) -> None: super().__init__(workspace_root, config) self._runner = skill_runner def set_skill_runner(self, runner: SkillRunner) -> None: """Late-bind the SkillRunner (avoids circular init dependencies).""" self._runner = runner def execute(self, *, tool_call_id: str, **kwargs: Any) -> ToolResult: summary: str = kwargs.get("summary", "Skill complete.") if self._runner is None or not self._runner.is_active: return ToolResult( tool_call_id=tool_call_id, tool_name=self.name, status=ToolResultStatus.ERROR, error="No skill is currently active.", ) skill_name = self._runner.active_skill_name self._runner.deactivate(summary=summary) return ToolResult( tool_call_id=tool_call_id, tool_name=self.name, status=ToolResultStatus.SUCCESS, output=f"Skill '{skill_name}' completed: {summary}", )