mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
feat(gateway): report per-skill usage and origin in commands.catalog
The catalog advertises every skill command but nothing about which ones the user actually reaches for, so consumers can only sort them alphabetically. Add a `skills` map keyed by slash command carrying the activity count and origin (hub / bundled / local) already tracked in the skills sidecars, read once per catalog build. Additive: older clients ignore the field, and an unreadable sidecar degrades to zero usage rather than failing the catalog.
This commit is contained in:
parent
3d0e543aa7
commit
75bf13e032
2 changed files with 111 additions and 0 deletions
|
|
@ -7338,6 +7338,66 @@ def test_commands_catalog_surfaces_quick_commands(monkeypatch):
|
|||
assert resp["result"]["canon"]["/notes"] == "/notes"
|
||||
|
||||
|
||||
def test_commands_catalog_ranks_skill_commands_by_recorded_usage(monkeypatch):
|
||||
"""Skill entries carry the usage + origin the `/` menu ranks on.
|
||||
|
||||
Without it the menu is alphabetical, so a bundled skill the user has never
|
||||
opened outranks the one they invoke daily.
|
||||
"""
|
||||
monkeypatch.setattr(
|
||||
server,
|
||||
"_skill_usage_lookup",
|
||||
lambda: (
|
||||
lambda name: {"research": 60, "work": 172}.get(name, 0),
|
||||
lambda name: "bundled" if name == "research-paper-writing" else "local",
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"agent.skill_commands.scan_skill_commands",
|
||||
lambda: {
|
||||
"/research": {"name": "research", "description": "Look it up"},
|
||||
"/research-paper-writing": {
|
||||
"name": "research-paper-writing",
|
||||
"description": "Write a paper",
|
||||
},
|
||||
"/work": {"name": "work", "description": "Fresh worktree"},
|
||||
},
|
||||
)
|
||||
|
||||
resp = server.handle_request(
|
||||
{"id": "1", "method": "commands.catalog", "params": {}}
|
||||
)
|
||||
|
||||
skills = resp["result"]["skills"]
|
||||
assert skills["/work"] == {"usage": 172, "origin": "local"}
|
||||
assert skills["/research"] == {"usage": 60, "origin": "local"}
|
||||
assert skills["/research-paper-writing"] == {"usage": 0, "origin": "bundled"}
|
||||
|
||||
# Every advertised skill command is rankable — a missing entry silently
|
||||
# sorts that skill to the bottom of the menu.
|
||||
advertised = {name for name, _ in resp["result"]["pairs"]}
|
||||
assert set(skills) <= advertised
|
||||
assert resp["result"]["skill_count"] == len(skills)
|
||||
|
||||
|
||||
def test_commands_catalog_survives_an_unreadable_usage_sidecar(monkeypatch):
|
||||
"""A broken/absent .usage.json degrades to no ranking, never a broken menu."""
|
||||
monkeypatch.setattr(
|
||||
"tools.skill_usage.load_usage",
|
||||
lambda: (_ for _ in ()).throw(OSError("sidecar is gone")),
|
||||
)
|
||||
|
||||
resp = server.handle_request(
|
||||
{"id": "1", "method": "commands.catalog", "params": {}}
|
||||
)
|
||||
|
||||
assert "error" not in resp
|
||||
assert all(
|
||||
entry == {"usage": 0, "origin": "local"}
|
||||
for entry in resp["result"]["skills"].values()
|
||||
)
|
||||
|
||||
|
||||
def test_commands_catalog_includes_tui_mouse_command():
|
||||
resp = server.handle_request(
|
||||
{"id": "1", "method": "commands.catalog", "params": {}}
|
||||
|
|
|
|||
|
|
@ -15411,6 +15411,47 @@ _PENDING_INPUT_COMMANDS: frozenset[str] = frozenset(
|
|||
_WORKER_BLOCKED_COMMANDS: frozenset[str] = frozenset({"snapshot", "snap"})
|
||||
|
||||
|
||||
def _skill_usage_lookup():
|
||||
"""Build ``(usage, origin)`` callables for the skill-command catalog.
|
||||
|
||||
``usage(name)`` is the skill's observed activity count (use + view +
|
||||
patch); ``origin(name)`` is ``"hub"``, ``"bundled"``, or ``"local"`` — the
|
||||
same classification ``/api/skills`` reports as ``provenance`` (where
|
||||
"local" is spelled "agent"). Both read sidecar files that are cheap and
|
||||
already parsed once per catalog build. Any failure degrades to zero usage
|
||||
and ``"local"`` so a missing/corrupt sidecar can never break the catalog.
|
||||
"""
|
||||
try:
|
||||
from tools.skill_usage import (
|
||||
_read_bundled_manifest_names,
|
||||
_read_hub_installed_names,
|
||||
activity_count,
|
||||
load_usage,
|
||||
)
|
||||
|
||||
records = load_usage()
|
||||
bundled = _read_bundled_manifest_names()
|
||||
hub = _read_hub_installed_names()
|
||||
except Exception as e:
|
||||
logger.debug("skill usage lookup unavailable: %s", e)
|
||||
return (lambda _name: 0), (lambda _name: "local")
|
||||
|
||||
def usage(name: str) -> int:
|
||||
try:
|
||||
return activity_count(records.get(name) or {})
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def origin(name: str) -> str:
|
||||
if name in hub:
|
||||
return "hub"
|
||||
if name in bundled:
|
||||
return "bundled"
|
||||
return "local"
|
||||
|
||||
return usage, origin
|
||||
|
||||
|
||||
@method("commands.catalog")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Registry-backed slash metadata for the TUI — categorized, no aliases."""
|
||||
|
|
@ -15488,12 +15529,21 @@ def _(rid, params: dict) -> dict:
|
|||
warning = f"quick_commands discovery unavailable: {e}"
|
||||
|
||||
skill_count = 0
|
||||
skills: dict[str, dict] = {}
|
||||
try:
|
||||
from agent.skill_commands import scan_skill_commands
|
||||
|
||||
# Usage + origin per skill command. Surfaces here rather than in a
|
||||
# second RPC because every consumer that renders the catalog also
|
||||
# wants to rank it, and both reads are cheap sidecar files already
|
||||
# loaded once per catalog build.
|
||||
usage, origin_of = _skill_usage_lookup()
|
||||
|
||||
for k, info in sorted(scan_skill_commands().items()):
|
||||
d = str(info.get("description", "Skill"))
|
||||
all_pairs.append([k, d[:120] + ("…" if len(d) > 120 else "")])
|
||||
name = str(info.get("name") or k.lstrip("/"))
|
||||
skills[k] = {"usage": usage(name), "origin": origin_of(name)}
|
||||
skill_count += 1
|
||||
except Exception as e:
|
||||
warning = f"skill discovery unavailable: {e}"
|
||||
|
|
@ -15509,6 +15559,7 @@ def _(rid, params: dict) -> dict:
|
|||
"sub": sub,
|
||||
"canon": canon,
|
||||
"categories": categories,
|
||||
"skills": skills,
|
||||
"skill_count": skill_count,
|
||||
"warning": warning,
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue