fix: disclose enabled plugins' registered skills in the system prompt's available_skills index
This commit is contained in:
@@ -1315,6 +1315,38 @@ def _skill_should_show(
|
||||
)
|
||||
|
||||
|
||||
def _plugin_skill_prompt_rows(
|
||||
disabled: "set[str]", available_tools: "set[str] | None", available_toolsets: "set[str] | None",
|
||||
session_platform: "str | None",
|
||||
) -> "list[tuple[str, str]]":
|
||||
"""``(qualified_name, description)`` for every skill registered by an ENABLED plugin
|
||||
(``ctx.register_skill``), filtered through the same offer-time gates as on-disk skills.
|
||||
Plugin skills live in the plugin-manager registry — never under the profile skills tree —
|
||||
so the disk scans above cannot see them; this is their one path into ``<available_skills>``.
|
||||
The qualified ``plugin:skill`` name is exactly what ``skill_view`` resolves, and disabling
|
||||
or unloading a plugin removes its registry entries, so enablement gating is inherent."""
|
||||
rows: "list[tuple[str, str]]" = []
|
||||
try:
|
||||
from hermes_cli.plugins import discover_plugins, get_plugin_manager
|
||||
discover_plugins() # idempotent; joins an in-flight discovery (same call skills_list makes)
|
||||
for meta in get_plugin_manager().list_plugin_skill_metadata():
|
||||
name = str(meta.get("name") or "")
|
||||
if not name or name in disabled:
|
||||
continue
|
||||
frontmatter = meta.get("frontmatter") or {}
|
||||
if not (skill_matches_platform(frontmatter) and skill_matches_environment(frontmatter)
|
||||
and skill_matches_apps(frontmatter)):
|
||||
continue
|
||||
if not _skill_should_show(extract_skill_conditions(frontmatter), available_tools,
|
||||
available_toolsets, session_platform):
|
||||
continue
|
||||
desc = str(meta.get("description") or "").strip() or extract_skill_description(frontmatter)
|
||||
rows.append((name, desc))
|
||||
except Exception:
|
||||
logger.debug("Plugin skill prompt rows unavailable", exc_info=True)
|
||||
return rows
|
||||
|
||||
|
||||
def _current_session_platform_hint() -> str:
|
||||
"""Active platform without importing the gateway package on CLI startup."""
|
||||
platform = os.environ.get("HERMES_PLATFORM") or os.environ.get("HERMES_SESSION_PLATFORM")
|
||||
@@ -1476,12 +1508,15 @@ def _build_skills_system_prompt_inner(
|
||||
# The resolved platform is part of the key: per-platform disabled-skill lists need distinct cache entries.
|
||||
_platform_hint = _current_session_platform_hint()
|
||||
disabled = get_disabled_skill_names(_platform_hint or None)
|
||||
# Plugin-registered skills (ctx.register_skill) are registry state, not files under any scanned
|
||||
# root — the snapshot manifest can't see them change, so they participate in the cache key.
|
||||
plugin_rows = _plugin_skill_prompt_rows(disabled, available_tools, available_toolsets, _platform_hint or None)
|
||||
cache_key = (
|
||||
str(skills_dir), tuple((t, str(d)) for t, d in extra_roots),
|
||||
tuple(sorted(str(t) for t in (available_tools or set()))),
|
||||
tuple(sorted(str(ts) for ts in (available_toolsets or set()))),
|
||||
_platform_hint, tuple(sorted(disabled)), tuple(sorted(compact_categories or ())),
|
||||
_oneshot_prompt_variant(),
|
||||
_oneshot_prompt_variant(), tuple(plugin_rows),
|
||||
)
|
||||
snapshot = _load_skills_snapshot(skills_dir)
|
||||
app_gated = snapshot is not None and any(
|
||||
@@ -1532,6 +1567,14 @@ def _build_skills_system_prompt_inner(
|
||||
visible_entries = [e for e in resolved
|
||||
if e["visible"] and e["status"] != "shadowed" and not is_disabled_entry(e, disabled)]
|
||||
_label_visible_entries(visible_entries, skills_by_category)
|
||||
if plugin_rows:
|
||||
# Same category label skills_list gives registry skills; qualified names are already
|
||||
# namespaced (plugin:skill) so they cannot collide with on-disk load_names.
|
||||
listed = {name for entries in skills_by_category.values() for name, _ in entries}
|
||||
for name, desc in plugin_rows:
|
||||
if name not in listed:
|
||||
listed.add(name)
|
||||
skills_by_category.setdefault("plugin", []).append((name, desc))
|
||||
if snapshot is None: # persist for fast cold-start reuse (best-effort)
|
||||
category_descriptions.update(_read_category_descriptions(skills_dir, "Could not read skill description %s: %s"))
|
||||
try:
|
||||
|
||||
@@ -24,10 +24,10 @@ def register_skill(
|
||||
self, name: str, path: Path, description: str = "",
|
||||
frontmatter: Optional[Mapping[str, Any]] = None,
|
||||
) -> PluginRegistration:
|
||||
"""Register a read-only skill resolvable as ``'<plugin_name>:<name>'`` via ``skill_view()``
|
||||
and listed by ``skills_list``. Not copied into ``~/.hermes/skills/`` and not in the system
|
||||
prompt's ``<available_skills>``. Raises ``ValueError`` (``':'``/invalid chars) or
|
||||
``FileNotFoundError``."""
|
||||
"""Register a read-only skill resolvable as ``'<plugin_name>:<name>'`` via ``skill_view()``,
|
||||
listed by ``skills_list`` and in the system prompt's ``<available_skills>`` while the plugin
|
||||
is enabled. Not copied into ``~/.hermes/skills/``. Raises ``ValueError`` (``':'``/invalid
|
||||
chars) or ``FileNotFoundError``."""
|
||||
from agent.skill_utils import _NAMESPACE_RE
|
||||
if ":" in name:
|
||||
raise ValueError(f"Skill name '{name}' must not contain ':' (the namespace is derived from the "
|
||||
|
||||
Reference in New Issue
Block a user