feat(skills): /reload-skills slash command + skills_reload agent tool
Adds a public reload path for the in-process skill caches so newly installed (or removed) skills become visible mid-session without a gateway restart. Mirrors the shape of /reload-mcp. Three surfaces: * /reload-skills slash command — CLI (cli.py) and gateway (gateway/run.py), with /reload_skills alias for Telegram autocomplete and an explicit Discord registration. * skills_reload agent tool (tools/skills_tool.py) — lets agents/subagents pick up freshly-installed skills via tool call. * agent.skill_commands.reload_skills() — shared helper that clears _skill_commands, _SKILLS_PROMPT_CACHE (in-process LRU), and the on-disk .skills_prompt_snapshot.json, then returns an added/removed diff plus the new total count. Tested: * tests/agent/test_skill_commands_reload.py (9 cases) * tests/cli/test_cli_reload_skills.py (3 cases) * tests/gateway/test_reload_skills_command.py (4 cases) Use case: NemoClaw / OpenShell-style sandboxed orchestrators that drop skills into ~/.hermes/skills mid-session, plus agentic flows where the agent itself installs a skill via the shell tool and needs it bound without a gateway restart. The Python helper clear_skills_system_prompt_cache(clear_snapshot=True) already exists internally — this PR just exposes it via slash command and tool.
This commit is contained in:
@@ -2270,6 +2270,10 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||
async def slash_reload_mcp(interaction: discord.Interaction):
|
||||
await self._run_simple_slash(interaction, "/reload-mcp")
|
||||
|
||||
@tree.command(name="reload-skills", description="Re-scan ~/.hermes/skills/ for new or removed skills")
|
||||
async def slash_reload_skills(interaction: discord.Interaction):
|
||||
await self._run_simple_slash(interaction, "/reload-skills")
|
||||
|
||||
@tree.command(name="voice", description="Toggle voice reply mode")
|
||||
@discord.app_commands.describe(mode="Voice mode: on, off, tts, channel, leave, or status")
|
||||
@discord.app_commands.choices(mode=[
|
||||
|
||||
@@ -4219,6 +4219,9 @@ class GatewayRunner:
|
||||
if canonical == "reload-mcp":
|
||||
return await self._handle_reload_mcp_command(event)
|
||||
|
||||
if canonical == "reload-skills":
|
||||
return await self._handle_reload_skills_command(event)
|
||||
|
||||
if canonical == "approve":
|
||||
return await self._handle_approve_command(event)
|
||||
|
||||
@@ -8208,6 +8211,58 @@ class GatewayRunner:
|
||||
logger.warning("MCP reload failed: %s", e)
|
||||
return f"❌ MCP reload failed: {e}"
|
||||
|
||||
async def _handle_reload_skills_command(self, event: MessageEvent) -> str:
|
||||
"""Handle /reload-skills — re-scan skills dir and clear prompt cache."""
|
||||
loop = asyncio.get_running_loop()
|
||||
try:
|
||||
from agent.skill_commands import reload_skills
|
||||
|
||||
result = await loop.run_in_executor(None, reload_skills)
|
||||
added = result.get("added", [])
|
||||
removed = result.get("removed", [])
|
||||
total = result.get("total", 0)
|
||||
|
||||
lines = ["🔄 **Skills Reloaded**\n"]
|
||||
if added:
|
||||
lines.append(f"➕ Added: {', '.join(added)}")
|
||||
if removed:
|
||||
lines.append(f"➖ Removed: {', '.join(removed)}")
|
||||
if not added and not removed:
|
||||
lines.append("No changes detected.")
|
||||
lines.append(f"\n📚 {total} skill(s) available")
|
||||
|
||||
# Inject a session-history note so the model sees the new skill
|
||||
# list on its next turn. Appended after all existing messages
|
||||
# to preserve prompt-cache for the prefix.
|
||||
change_parts = []
|
||||
if added:
|
||||
change_parts.append(f"Added skills: {', '.join(added)}")
|
||||
if removed:
|
||||
change_parts.append(f"Removed skills: {', '.join(removed)}")
|
||||
if change_parts:
|
||||
change_detail = ". ".join(change_parts) + ". "
|
||||
reload_msg = {
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"[IMPORTANT: Skills have been reloaded. {change_detail}"
|
||||
f"{total} skill(s) now available. Use skills_list to "
|
||||
f"see the updated catalog.]"
|
||||
),
|
||||
}
|
||||
try:
|
||||
session_entry = self.session_store.get_or_create_session(event.source)
|
||||
self.session_store.append_to_transcript(
|
||||
session_entry.session_id, reload_msg
|
||||
)
|
||||
except Exception:
|
||||
pass # Best-effort; don't fail the reload over a transcript write
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("Skills reload failed: %s", e)
|
||||
return f"❌ Skills reload failed: {e}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# /approve & /deny — explicit dangerous-command approval
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user