mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-23 10:42:00 +00:00
fix(banner): don't advertise toolsets/skills the agent wasn't given (#50497)
The welcome banner's 'Available Tools' merged in every toolset from the global check_tool_availability() registry walk, regardless of whether it was enabled for the current platform. On a Blank Slate CLI (file + terminal only) that surfaced discord / feishu / kanban tools the agent was never actually given — they are not in the agent's tool schema, but the banner displayed them, making it look like they were exposed. - Filter the unavailable-toolset merge to toolsets actually in enabled_toolsets (a toolset that's enabled but has unmet deps still legitimately shows as disabled/lazy). - Gate the 'Available Skills' section on the skills toolset being enabled — when it's off, the agent can't load any skill, so show 'Skills toolset disabled' instead of the on-disk catalog. When enabled_toolsets is empty (older callers), behavior is unchanged. Validation: blank-slate banner now shows only file + terminal and 'Skills toolset disabled'; a skills-enabled banner still lists the catalog. Added regression tests; full banner suite green (15/15).
This commit is contained in:
parent
8cfcbd327d
commit
5bf23ff251
2 changed files with 104 additions and 3 deletions
|
|
@ -575,6 +575,18 @@ def build_welcome_banner(console: "Console", model: str, cwd: str,
|
|||
enabled_toolsets = enabled_toolsets or []
|
||||
|
||||
_, unavailable_toolsets = check_tool_availability(quiet=True)
|
||||
# The availability check walks the GLOBAL toolset registry, so it includes
|
||||
# toolsets that aren't part of this agent's platform set at all (e.g.
|
||||
# `discord`, `feishu_doc` on a CLI session). Those must never surface in the
|
||||
# banner's "Available Tools" — they aren't exposed to the agent. Restrict to
|
||||
# toolsets actually enabled for this agent; a toolset that's enabled but
|
||||
# currently has unmet deps legitimately shows as disabled/lazy below.
|
||||
_enabled_ts = {str(t) for t in enabled_toolsets}
|
||||
if _enabled_ts:
|
||||
unavailable_toolsets = [
|
||||
item for item in unavailable_toolsets
|
||||
if str(item.get("id", item.get("name", ""))) in _enabled_ts
|
||||
]
|
||||
disabled_tools = set()
|
||||
# Tools whose toolset has a check_fn are lazy-initialized (e.g. honcho,
|
||||
# homeassistant) — they show as unavailable at banner time because the
|
||||
|
|
@ -722,10 +734,21 @@ def build_welcome_banner(console: "Console", model: str, cwd: str,
|
|||
|
||||
right_lines.append("")
|
||||
right_lines.append(f"[bold {accent}]Available Skills[/]")
|
||||
skills_by_category = get_available_skills()
|
||||
total_skills = sum(len(s) for s in skills_by_category.values())
|
||||
# The skills catalog is only reachable when the `skills` toolset is enabled
|
||||
# (it exposes skill_view / skill_manage). When it's disabled — e.g. a Blank
|
||||
# Slate install — the agent literally cannot load any skill, so advertising
|
||||
# the on-disk catalog here is misleading. Reflect the real state instead.
|
||||
_skills_enabled = (not _enabled_ts) or ("skills" in _enabled_ts)
|
||||
if _skills_enabled:
|
||||
skills_by_category = get_available_skills()
|
||||
total_skills = sum(len(s) for s in skills_by_category.values())
|
||||
else:
|
||||
skills_by_category = {}
|
||||
total_skills = 0
|
||||
|
||||
if skills_by_category:
|
||||
if not _skills_enabled:
|
||||
right_lines.append(f"[dim {dim}]Skills toolset disabled[/]")
|
||||
elif skills_by_category:
|
||||
for category in sorted(skills_by_category.keys()):
|
||||
skill_names = sorted(skills_by_category[category])
|
||||
if len(skill_names) > 8:
|
||||
|
|
|
|||
|
|
@ -200,3 +200,81 @@ def test_build_welcome_banner_configured_mcp_is_not_failed():
|
|||
assert "docker-profile" in output
|
||||
assert "configured" in output
|
||||
assert "failed" not in output
|
||||
|
||||
|
||||
def test_banner_hides_toolsets_not_enabled_for_platform():
|
||||
"""A globally-registered toolset that isn't enabled for this agent (e.g.
|
||||
discord / feishu on a CLI session) must NOT appear in 'Available Tools'.
|
||||
|
||||
Regression: check_tool_availability() walks the global registry, so the
|
||||
banner used to merge in every unavailable toolset regardless of whether it
|
||||
was part of this platform's set. On a Blank Slate CLI (file + terminal only)
|
||||
that surfaced discord/feishu tools the agent was never given.
|
||||
"""
|
||||
with (
|
||||
patch.object(
|
||||
model_tools,
|
||||
"check_tool_availability",
|
||||
return_value=(
|
||||
["file", "terminal"],
|
||||
[
|
||||
{"name": "discord", "tools": ["discord_fetch_messages"]},
|
||||
{"name": "feishu_doc", "tools": ["feishu_doc_read"]},
|
||||
],
|
||||
),
|
||||
),
|
||||
patch.object(banner, "get_available_skills", return_value={}),
|
||||
patch.object(banner, "get_update_result", return_value=None),
|
||||
patch.object(tools.mcp_tool, "get_mcp_status", return_value=[]),
|
||||
):
|
||||
console = Console(record=True, force_terminal=False, color_system=None, width=160)
|
||||
banner.build_welcome_banner(
|
||||
console=console,
|
||||
model="anthropic/test-model",
|
||||
cwd="/tmp/project",
|
||||
tools=[{"function": {"name": "read_file"}}],
|
||||
enabled_toolsets=["file", "terminal"],
|
||||
get_toolset_for_tool=lambda n: "file",
|
||||
)
|
||||
|
||||
output = console.export_text()
|
||||
assert "discord" not in output
|
||||
assert "feishu" not in output
|
||||
|
||||
|
||||
def test_banner_skills_section_reflects_disabled_skills_toolset():
|
||||
"""When the `skills` toolset is disabled (Blank Slate), the banner must not
|
||||
advertise the on-disk skill catalog — the agent can't load any of them."""
|
||||
fake_skills = {"creative": ["ascii-art", "p5js"], "devops": ["bug-triage-work"]}
|
||||
|
||||
# skills toolset DISABLED -> catalog hidden, "disabled" message shown
|
||||
with (
|
||||
patch.object(model_tools, "check_tool_availability", return_value=(["file", "terminal"], [])),
|
||||
patch.object(banner, "get_available_skills", return_value=fake_skills),
|
||||
patch.object(banner, "get_update_result", return_value=None),
|
||||
patch.object(tools.mcp_tool, "get_mcp_status", return_value=[]),
|
||||
):
|
||||
console = Console(record=True, force_terminal=False, color_system=None, width=160)
|
||||
banner.build_welcome_banner(
|
||||
console=console, model="m", cwd="/tmp", tools=[{"function": {"name": "read_file"}}],
|
||||
enabled_toolsets=["file", "terminal"], get_toolset_for_tool=lambda n: "file",
|
||||
)
|
||||
out_disabled = console.export_text()
|
||||
assert "Skills toolset disabled" in out_disabled
|
||||
assert "ascii-art" not in out_disabled
|
||||
|
||||
# skills toolset ENABLED -> catalog listed as before
|
||||
with (
|
||||
patch.object(model_tools, "check_tool_availability", return_value=(["file", "terminal", "skills"], [])),
|
||||
patch.object(banner, "get_available_skills", return_value=fake_skills),
|
||||
patch.object(banner, "get_update_result", return_value=None),
|
||||
patch.object(tools.mcp_tool, "get_mcp_status", return_value=[]),
|
||||
):
|
||||
console = Console(record=True, force_terminal=False, color_system=None, width=160)
|
||||
banner.build_welcome_banner(
|
||||
console=console, model="m", cwd="/tmp", tools=[{"function": {"name": "read_file"}}],
|
||||
enabled_toolsets=["file", "terminal", "skills"], get_toolset_for_tool=lambda n: "file",
|
||||
)
|
||||
out_enabled = console.export_text()
|
||||
assert "Skills toolset disabled" not in out_enabled
|
||||
assert "ascii-art" in out_enabled
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue