mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(gateway): resolve Telegram's underscored /commands to skill/plugin keys
Telegram's Bot API disallows hyphens in command names, so _build_telegram_menu registers /claude-code as /claude_code. When the user taps it from autocomplete, the gateway dispatch did a direct lookup against skill_cmds (keyed on the hyphenated form) and missed, silently falling through to the LLM as plain text. The model would then typically call delegate_task, spawning a Hermes subagent instead of invoking the intended skill. Normalize underscores to hyphens in skill and plugin command lookup, matching the existing pattern in _check_unavailable_skill.
This commit is contained in:
parent
afccbf253c
commit
4a75aec433
3 changed files with 82 additions and 5 deletions
|
|
@ -217,6 +217,25 @@ def get_skill_commands() -> Dict[str, Dict[str, Any]]:
|
|||
return _skill_commands
|
||||
|
||||
|
||||
def resolve_skill_command_key(command: str) -> Optional[str]:
|
||||
"""Resolve a user-typed /command to its canonical skill_cmds key.
|
||||
|
||||
Skills are always stored with hyphens — ``scan_skill_commands`` normalizes
|
||||
spaces and underscores to hyphens when building the key. Hyphens and
|
||||
underscores are treated interchangeably in user input: this matches
|
||||
``_check_unavailable_skill`` and accommodates Telegram bot-command names
|
||||
(which disallow hyphens, so ``/claude-code`` is registered as
|
||||
``/claude_code`` and comes back in the underscored form).
|
||||
|
||||
Returns the matching ``/slug`` key from ``get_skill_commands()`` or
|
||||
``None`` if no match.
|
||||
"""
|
||||
if not command:
|
||||
return None
|
||||
cmd_key = f"/{command.replace('_', '-')}"
|
||||
return cmd_key if cmd_key in get_skill_commands() else None
|
||||
|
||||
|
||||
def build_skill_invocation_message(
|
||||
cmd_key: str,
|
||||
user_instruction: str = "",
|
||||
|
|
|
|||
|
|
@ -2112,7 +2112,10 @@ class GatewayRunner:
|
|||
if command:
|
||||
try:
|
||||
from hermes_cli.plugins import get_plugin_command_handler
|
||||
plugin_handler = get_plugin_command_handler(command)
|
||||
# Normalize underscores to hyphens so Telegram's underscored
|
||||
# autocomplete form matches plugin commands registered with
|
||||
# hyphens. See hermes_cli/commands.py:_build_telegram_menu.
|
||||
plugin_handler = get_plugin_command_handler(command.replace("_", "-"))
|
||||
if plugin_handler:
|
||||
user_args = event.get_command_args().strip()
|
||||
import asyncio as _aio
|
||||
|
|
@ -2123,13 +2126,20 @@ class GatewayRunner:
|
|||
except Exception as e:
|
||||
logger.debug("Plugin command dispatch failed (non-fatal): %s", e)
|
||||
|
||||
# Skill slash commands: /skill-name loads the skill and sends to agent
|
||||
# Skill slash commands: /skill-name loads the skill and sends to agent.
|
||||
# resolve_skill_command_key() handles the Telegram underscore/hyphen
|
||||
# round-trip so /claude_code from Telegram autocomplete still resolves
|
||||
# to the claude-code skill.
|
||||
if command:
|
||||
try:
|
||||
from agent.skill_commands import get_skill_commands, build_skill_invocation_message
|
||||
from agent.skill_commands import (
|
||||
get_skill_commands,
|
||||
build_skill_invocation_message,
|
||||
resolve_skill_command_key,
|
||||
)
|
||||
skill_cmds = get_skill_commands()
|
||||
cmd_key = f"/{command}"
|
||||
if cmd_key in skill_cmds:
|
||||
cmd_key = resolve_skill_command_key(command)
|
||||
if cmd_key is not None:
|
||||
# Check per-platform disabled status before executing.
|
||||
# get_skill_commands() only applies the *global* disabled
|
||||
# list at scan time; per-platform overrides need checking
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from agent.skill_commands import (
|
|||
build_plan_path,
|
||||
build_preloaded_skills_prompt,
|
||||
build_skill_invocation_message,
|
||||
resolve_skill_command_key,
|
||||
scan_skill_commands,
|
||||
)
|
||||
|
||||
|
|
@ -101,6 +102,53 @@ class TestScanSkillCommands:
|
|||
assert "/disabled-skill" not in result
|
||||
|
||||
|
||||
class TestResolveSkillCommandKey:
|
||||
"""Telegram bot-command names disallow hyphens, so the menu registers
|
||||
skills with hyphens swapped for underscores. When Telegram autocomplete
|
||||
sends the underscored form back, we need to find the hyphenated key.
|
||||
"""
|
||||
|
||||
def test_hyphenated_form_matches_directly(self, tmp_path):
|
||||
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
||||
_make_skill(tmp_path, "claude-code")
|
||||
scan_skill_commands()
|
||||
assert resolve_skill_command_key("claude-code") == "/claude-code"
|
||||
|
||||
def test_underscore_form_resolves_to_hyphenated_skill(self, tmp_path):
|
||||
"""/claude_code from Telegram autocomplete must resolve to /claude-code."""
|
||||
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
||||
_make_skill(tmp_path, "claude-code")
|
||||
scan_skill_commands()
|
||||
assert resolve_skill_command_key("claude_code") == "/claude-code"
|
||||
|
||||
def test_single_word_command_resolves(self, tmp_path):
|
||||
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
||||
_make_skill(tmp_path, "investigate")
|
||||
scan_skill_commands()
|
||||
assert resolve_skill_command_key("investigate") == "/investigate"
|
||||
|
||||
def test_unknown_command_returns_none(self, tmp_path):
|
||||
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
||||
_make_skill(tmp_path, "claude-code")
|
||||
scan_skill_commands()
|
||||
assert resolve_skill_command_key("does_not_exist") is None
|
||||
assert resolve_skill_command_key("does-not-exist") is None
|
||||
|
||||
def test_empty_command_returns_none(self, tmp_path):
|
||||
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
||||
scan_skill_commands()
|
||||
assert resolve_skill_command_key("") is None
|
||||
|
||||
def test_hyphenated_command_is_not_mangled(self, tmp_path):
|
||||
"""A user-typed /foo-bar (hyphen) must not trigger the underscore fallback."""
|
||||
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
||||
_make_skill(tmp_path, "foo-bar")
|
||||
scan_skill_commands()
|
||||
assert resolve_skill_command_key("foo-bar") == "/foo-bar"
|
||||
# Underscore form also works (Telegram round-trip)
|
||||
assert resolve_skill_command_key("foo_bar") == "/foo-bar"
|
||||
|
||||
|
||||
class TestBuildPreloadedSkillsPrompt:
|
||||
def test_builds_prompt_for_multiple_named_skills(self, tmp_path):
|
||||
with patch("tools.skills_tool.SKILLS_DIR", tmp_path):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue