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:
@@ -10,7 +10,7 @@ from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.widgets import Header, RichLog
|
||||
from textual.widgets import Header, Input, RichLog
|
||||
from textual import work
|
||||
|
||||
from app.agent.context import SessionContext
|
||||
@@ -58,6 +58,7 @@ class SneakyCodeApp(App):
|
||||
self._permissions: PermissionsService | None = None
|
||||
self._debug_logger = None
|
||||
self._skills_manager = None
|
||||
self._skill_runner = None
|
||||
self._current_worker: Worker | None = None
|
||||
self._cancel_count = 0
|
||||
self.sub_title = config.llm.model
|
||||
@@ -95,12 +96,31 @@ class SneakyCodeApp(App):
|
||||
self._config.skills, self._config.agent.workspace_root
|
||||
)
|
||||
|
||||
# Create tool registry (SkillRunner wired after registry exists)
|
||||
self._tool_registry = create_default_registry(
|
||||
self._config.agent.workspace_root,
|
||||
self._config,
|
||||
skills_manager=self._skills_manager,
|
||||
)
|
||||
|
||||
# Create SkillRunner and late-bind it to skill tools
|
||||
if self._skills_manager is not None and self._tool_registry is not None:
|
||||
from app.services.skill_runner import SkillRunner
|
||||
|
||||
self._skill_runner = SkillRunner(
|
||||
self._skills_manager,
|
||||
self._config,
|
||||
self._ctx,
|
||||
self._tool_registry,
|
||||
)
|
||||
# Late-bind runner to skill tools already in the registry
|
||||
load_tool = self._tool_registry.get("load_skill")
|
||||
if load_tool and hasattr(load_tool, "set_skill_runner"):
|
||||
load_tool.set_skill_runner(self._skill_runner)
|
||||
finish_tool = self._tool_registry.get("finish_skill")
|
||||
if finish_tool and hasattr(finish_tool, "set_skill_runner"):
|
||||
finish_tool.set_skill_runner(self._skill_runner)
|
||||
|
||||
# Set up permission prompt callback
|
||||
async def permission_prompt(tool_name: str, description: str) -> bool:
|
||||
return await self._show_permission_modal(tool_name, description)
|
||||
@@ -152,7 +172,24 @@ class SneakyCodeApp(App):
|
||||
async def _handle_slash_command(self, command: str, log: RichLog) -> None:
|
||||
"""Process slash commands."""
|
||||
cmd = command.lower()
|
||||
if cmd == "/quit":
|
||||
if cmd == "/help":
|
||||
from rich.table import Table
|
||||
|
||||
table = Table(title="SneakyCode Commands", show_lines=False)
|
||||
table.add_column("Command", style="cyan", no_wrap=True)
|
||||
table.add_column("Description")
|
||||
table.add_row("/help", "Show this help message")
|
||||
table.add_row("/quit, /exit, /bye", "Save session and exit")
|
||||
table.add_row("/clear", "Clear conversation history")
|
||||
table.add_row("/history", "Show conversation history")
|
||||
table.add_row("/save", "Manually save session")
|
||||
table.add_row("/session", "Show session info (messages, tokens, start time)")
|
||||
table.add_row("/models", "List available Ollama models")
|
||||
table.add_row("/models <name>", "Switch to a different model")
|
||||
table.add_row("/skills", "List available skills")
|
||||
table.add_row("/<skill>", "Load a skill by name")
|
||||
log.write(table)
|
||||
elif cmd in ("/quit", "/exit", "/bye"):
|
||||
self._save_session()
|
||||
self.exit()
|
||||
elif cmd == "/clear":
|
||||
@@ -221,7 +258,24 @@ class SneakyCodeApp(App):
|
||||
else:
|
||||
log.write(Text("Skills system is disabled", style="yellow"))
|
||||
else:
|
||||
# Try as skill invocation
|
||||
# Try as skill trigger (package skill via SkillRunner)
|
||||
if self._skill_runner and self._skills_manager:
|
||||
skill = self._skills_manager.get_skill_by_trigger(cmd.lstrip("/"))
|
||||
if skill is not None:
|
||||
content = self._skill_runner.activate(skill.name)
|
||||
status_bar = self.query_one(StatusBar)
|
||||
status_bar.set_active_skill(skill.name)
|
||||
log.write(Text(f"Skill activated: {skill.name}", style="bold green"))
|
||||
# Run an agent turn so the LLM sees the skill context
|
||||
self._cancel_count = 0
|
||||
self._current_worker = self.run_worker(
|
||||
self._run_agent_turn(f"[Skill activated: {skill.name}]"),
|
||||
name="agent-turn",
|
||||
exclusive=True,
|
||||
)
|
||||
return
|
||||
|
||||
# Try as legacy skill invocation
|
||||
skill_name = cmd.lstrip("/")
|
||||
if self._skills_manager:
|
||||
content = self._skills_manager.load_skill(skill_name)
|
||||
@@ -230,7 +284,7 @@ class SneakyCodeApp(App):
|
||||
self._ctx.add_message("system", f"[Skill: {skill_name}]\n{content}")
|
||||
log.write(Text(f"Loaded skill: {skill_name}", style="bold green"))
|
||||
return
|
||||
log.write(Text(f"⚠ Unknown command: {command}", style="yellow"))
|
||||
log.write(Text(f"Unknown command: {command}", style="yellow"))
|
||||
|
||||
async def _run_agent_turn(self, user_input: str) -> None:
|
||||
"""Run a single agent turn (called as a worker)."""
|
||||
@@ -270,12 +324,19 @@ class SneakyCodeApp(App):
|
||||
self._tool_registry, self._permissions, display,
|
||||
debug_logger=self._debug_logger,
|
||||
skills_manager=self._skills_manager,
|
||||
skill_runner=self._skill_runner,
|
||||
)
|
||||
|
||||
await agent.run_turn(user_input)
|
||||
|
||||
status_bar.stop_streaming()
|
||||
|
||||
# Update skill indicator (skill may have been deactivated via finish_skill)
|
||||
if self._skill_runner and not self._skill_runner.is_active:
|
||||
status_bar.set_active_skill(None)
|
||||
elif self._skill_runner and self._skill_runner.is_active:
|
||||
status_bar.set_active_skill(self._skill_runner.active_skill_name)
|
||||
|
||||
# Auto-save
|
||||
if self._config.session.auto_save:
|
||||
self._save_session()
|
||||
|
||||
Reference in New Issue
Block a user