mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 00:11:39 +08:00
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:
77
tests/cli/test_cli_reload_skills.py
Normal file
77
tests/cli/test_cli_reload_skills.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Tests for the ``/reload-skills`` CLI slash command (``HermesCLI._reload_skills``)."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
def _make_cli():
|
||||
"""Build a minimal HermesCLI shell exposing ``_reload_skills``."""
|
||||
import cli as cli_mod
|
||||
|
||||
obj = object.__new__(cli_mod.HermesCLI)
|
||||
obj._command_running = False
|
||||
obj.conversation_history = []
|
||||
obj.agent = None
|
||||
return obj
|
||||
|
||||
|
||||
class TestReloadSkillsCLI:
|
||||
def test_reports_added_and_removed(self, capsys):
|
||||
cli = _make_cli()
|
||||
with patch(
|
||||
"agent.skill_commands.reload_skills",
|
||||
return_value={
|
||||
"added": ["alpha", "beta"],
|
||||
"removed": ["gamma"],
|
||||
"unchanged": ["delta"],
|
||||
"total": 3,
|
||||
"commands": 3,
|
||||
},
|
||||
):
|
||||
cli._reload_skills()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Added: alpha, beta" in out
|
||||
assert "Removed: gamma" in out
|
||||
assert "3 skill(s) available" in out
|
||||
# An informational message should be appended to the conversation
|
||||
# so the model picks up the diff on the next turn.
|
||||
assert len(cli.conversation_history) == 1
|
||||
msg = cli.conversation_history[0]
|
||||
assert msg["role"] == "user"
|
||||
assert "Skills have been reloaded" in msg["content"]
|
||||
assert "alpha" in msg["content"]
|
||||
assert "gamma" in msg["content"]
|
||||
|
||||
def test_reports_no_changes(self, capsys):
|
||||
cli = _make_cli()
|
||||
with patch(
|
||||
"agent.skill_commands.reload_skills",
|
||||
return_value={
|
||||
"added": [],
|
||||
"removed": [],
|
||||
"unchanged": ["alpha"],
|
||||
"total": 1,
|
||||
"commands": 1,
|
||||
},
|
||||
):
|
||||
cli._reload_skills()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "No changes detected" in out
|
||||
assert "1 skill(s) available" in out
|
||||
# Nothing changed — don't pollute history.
|
||||
assert cli.conversation_history == []
|
||||
|
||||
def test_handles_reload_failure_gracefully(self, capsys):
|
||||
cli = _make_cli()
|
||||
with patch(
|
||||
"agent.skill_commands.reload_skills",
|
||||
side_effect=RuntimeError("boom"),
|
||||
):
|
||||
cli._reload_skills()
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Skills reload failed" in out
|
||||
assert "boom" in out
|
||||
# Failure must not append a misleading "skills reloaded" note.
|
||||
assert cli.conversation_history == []
|
||||
Reference in New Issue
Block a user