feat: structured skill packages with config overrides, chaining, and TUI integration
Add a skill package system where each skill is a directory with a skill.yaml manifest and prompt markdown files. Skills support /command triggers, scoped config overrides (temperature, max_tokens, tool filtering), chain dependencies with cycle-safe resolution, and a finish_skill completion signal. Includes four built-in skills: explore, brainstorm, write-document, and plan. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@ class ToolRegistry:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._tools: dict[str, BaseTool] = {}
|
||||
self._disabled: set[str] = set()
|
||||
|
||||
def register(self, tool: BaseTool) -> None:
|
||||
"""Register a tool instance. Raises ValueError on duplicate name."""
|
||||
@@ -29,24 +30,74 @@ class ToolRegistry:
|
||||
logger.debug("Registered tool: %s", tool.name)
|
||||
|
||||
def get(self, name: str) -> BaseTool | None:
|
||||
"""Look up a tool by name."""
|
||||
"""Look up a tool by name. Returns None if disabled or not found."""
|
||||
if name in self._disabled:
|
||||
return None
|
||||
return self._tools.get(name)
|
||||
|
||||
def get_all(self) -> dict[str, BaseTool]:
|
||||
"""Return all registered tools."""
|
||||
return dict(self._tools)
|
||||
"""Return all registered tools (excluding disabled)."""
|
||||
return {k: v for k, v in self._tools.items() if k not in self._disabled}
|
||||
|
||||
def get_openai_tools_schema(self) -> list[dict[str, Any]]:
|
||||
"""Return OpenAI function-calling schemas for all registered tools."""
|
||||
return [tool.get_openai_schema() for tool in self._tools.values()]
|
||||
"""Return OpenAI function-calling schemas for all active tools."""
|
||||
return [
|
||||
tool.get_openai_schema()
|
||||
for tool in self._tools.values()
|
||||
if tool.name not in self._disabled
|
||||
]
|
||||
|
||||
def apply_filter(
|
||||
self,
|
||||
*,
|
||||
enable: list[str] | None = None,
|
||||
disable: list[str] | None = None,
|
||||
) -> set[str]:
|
||||
"""Apply a tool filter, returning the previous disabled set for restoration.
|
||||
|
||||
Args:
|
||||
enable: If set, only these tools (plus always-on tools) are available.
|
||||
disable: Specific tools to disable.
|
||||
|
||||
Returns:
|
||||
The previous disabled set (snapshot for restore).
|
||||
"""
|
||||
previous = set(self._disabled)
|
||||
|
||||
if enable is not None:
|
||||
# Whitelist mode: disable everything not in the enable list
|
||||
self._disabled = {name for name in self._tools if name not in enable}
|
||||
elif disable is not None:
|
||||
# Blacklist mode: add to existing disabled set (preserves global disables)
|
||||
self._disabled = set(self._disabled) | set(disable)
|
||||
else:
|
||||
self._disabled = set()
|
||||
|
||||
return previous
|
||||
|
||||
def restore_filter(self, previous: set[str]) -> None:
|
||||
"""Restore a previous filter state."""
|
||||
self._disabled = previous
|
||||
|
||||
def all_tool_names(self) -> list[str]:
|
||||
"""Return all registered tool names (including disabled)."""
|
||||
return list(self._tools.keys())
|
||||
|
||||
|
||||
def create_default_registry(
|
||||
workspace_root: Path,
|
||||
config: AppConfig,
|
||||
skills_manager: SkillsManager | None = None,
|
||||
skill_runner: object | None = None,
|
||||
) -> ToolRegistry:
|
||||
"""Create a ToolRegistry populated with all built-in tools."""
|
||||
"""Create a ToolRegistry populated with all built-in tools.
|
||||
|
||||
Args:
|
||||
workspace_root: Workspace root path.
|
||||
config: Application configuration.
|
||||
skills_manager: Optional skills manager for skill tools.
|
||||
skill_runner: Optional SkillRunner for package skill activation.
|
||||
"""
|
||||
# Read tools
|
||||
from app.tools.filesystem import ListDirTool, ReadFileTool
|
||||
|
||||
@@ -92,8 +143,11 @@ def create_default_registry(
|
||||
|
||||
# Skills (conditional)
|
||||
if skills_manager is not None:
|
||||
from app.tools.skills import LoadSkillTool
|
||||
from app.services.skill_runner import SkillRunner as SkillRunnerType
|
||||
from app.tools.skills import FinishSkillTool, LoadSkillTool
|
||||
|
||||
registry.register(LoadSkillTool(workspace_root, config, skills_manager))
|
||||
runner = skill_runner if isinstance(skill_runner, SkillRunnerType) else None
|
||||
registry.register(LoadSkillTool(workspace_root, config, skills_manager, runner))
|
||||
registry.register(FinishSkillTool(workspace_root, config, runner))
|
||||
|
||||
return registry
|
||||
|
||||
Reference in New Issue
Block a user