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:
66
cli.py
66
cli.py
@@ -3107,6 +3107,8 @@ class HermesCLI:
|
||||
return "Processing skills command..."
|
||||
if cmd_lower == "/reload-mcp":
|
||||
return "Reloading MCP servers..."
|
||||
if cmd_lower == "/reload-skills" or cmd_lower == "/reload_skills":
|
||||
return "Reloading skills..."
|
||||
if cmd_lower.startswith("/browser"):
|
||||
return "Configuring browser..."
|
||||
return "Processing command..."
|
||||
@@ -6286,6 +6288,9 @@ class HermesCLI:
|
||||
elif canonical == "reload-mcp":
|
||||
with self._busy_command(self._slow_command_status(cmd_original)):
|
||||
self._reload_mcp()
|
||||
elif canonical == "reload-skills":
|
||||
with self._busy_command(self._slow_command_status(cmd_original)):
|
||||
self._reload_skills()
|
||||
elif canonical == "browser":
|
||||
self._handle_browser_command(cmd_original)
|
||||
elif canonical == "plugins":
|
||||
@@ -7497,6 +7502,67 @@ class HermesCLI:
|
||||
except Exception as e:
|
||||
print(f" ❌ MCP reload failed: {e}")
|
||||
|
||||
def _reload_skills(self) -> None:
|
||||
"""Reload skills: rescan ~/.hermes/skills/, clear prompt cache.
|
||||
|
||||
Mirrors the ``/reload-mcp`` UX. After rescanning, the system prompt
|
||||
for the next turn is rebuilt with the fresh skill list and any
|
||||
``/skill-name`` slash commands are picked up immediately.
|
||||
"""
|
||||
try:
|
||||
from agent.skill_commands import reload_skills
|
||||
|
||||
if not self._command_running:
|
||||
print("🔄 Reloading skills...")
|
||||
|
||||
result = reload_skills()
|
||||
added = result.get("added", [])
|
||||
removed = result.get("removed", [])
|
||||
total = result.get("total", 0)
|
||||
|
||||
if added:
|
||||
print(f" ➕ Added: {', '.join(added)}")
|
||||
if removed:
|
||||
print(f" ➖ Removed: {', '.join(removed)}")
|
||||
if not added and not removed:
|
||||
print(" No changes detected.")
|
||||
print(f" 📚 {total} skill(s) available")
|
||||
|
||||
# Inject a system-style note so the model sees the new skill
|
||||
# list on its next turn. Appended at the end of history 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) + ". "
|
||||
self.conversation_history.append({
|
||||
"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.]"
|
||||
),
|
||||
})
|
||||
|
||||
# Persist immediately so the session log reflects the
|
||||
# reload event.
|
||||
if self.agent is not None:
|
||||
try:
|
||||
self.agent._persist_session(
|
||||
self.conversation_history,
|
||||
self.conversation_history,
|
||||
)
|
||||
except Exception:
|
||||
pass # Best-effort
|
||||
|
||||
print(f" ✅ Skill cache cleared")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Skills reload failed: {e}")
|
||||
|
||||
# ====================================================================
|
||||
# Tool-call generation indicator (shown during streaming)
|
||||
# ====================================================================
|
||||
|
||||
Reference in New Issue
Block a user