mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-22 05:22:09 +00:00
feat(plugins): HERMES_PLUGINS_DEBUG=1 surfaces plugin discovery logs (#22684)
Plugin authors had no easy way to figure out why their plugin wasn't loading — failures were buried in agent.log at WARNING and skip reasons (disabled, not enabled, depth cap, exclusive) were DEBUG-only and invisible by default. Set HERMES_PLUGINS_DEBUG=1 to attach a stderr handler at DEBUG to the hermes_cli.plugins logger only. Surfaces: - which directories were scanned + manifest counts per source - per manifest: resolved key, name, kind, source, on-disk path - skip reasons (disabled, not enabled, exclusive, depth cap, no register) - per load: tools/hooks/slash/CLI commands the plugin registered - full traceback on YAML parse failure (exc_info on the existing warning) - full traceback on register() exceptions, pointing at the plugin author's line Env var off (default) → zero new stderr output, same as before. Touches only hermes_cli/plugins.py + a doc section in the plugin-build guide + an entry in the env-vars reference. 3 new tests lock the attach/idempotent/no-attach behavior.
This commit is contained in:
parent
8f83046f6c
commit
79694018f8
4 changed files with 206 additions and 13 deletions
|
|
@ -71,6 +71,56 @@ except ImportError: # pragma: no cover – yaml is optional at import time
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin developer debug logging
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Set ``HERMES_PLUGINS_DEBUG=1`` to surface verbose plugin-discovery logs to
|
||||
# stderr in addition to ~/.hermes/logs/agent.log. Aimed at plugin authors
|
||||
# trying to figure out why their plugin isn't showing up: which directories
|
||||
# were scanned, which manifests parsed, which plugins were skipped (and why),
|
||||
# what each ``register(ctx)`` call registered, and full tracebacks on load
|
||||
# failure.
|
||||
#
|
||||
# The env var is read once at import time; tests that need to flip it
|
||||
# mid-process can call ``_install_plugin_debug_handler(force=True)``.
|
||||
|
||||
_PLUGINS_DEBUG = os.getenv("HERMES_PLUGINS_DEBUG", "").strip().lower() in (
|
||||
"1", "true", "yes", "on",
|
||||
)
|
||||
_DEBUG_HANDLER_INSTALLED = False
|
||||
|
||||
|
||||
def _install_plugin_debug_handler(force: bool = False) -> None:
|
||||
"""When HERMES_PLUGINS_DEBUG is on, tee plugin logs to stderr at DEBUG.
|
||||
|
||||
Idempotent: only attaches the handler once per process unless ``force``
|
||||
is passed. Does not touch the root logger or other Hermes loggers.
|
||||
"""
|
||||
global _DEBUG_HANDLER_INSTALLED, _PLUGINS_DEBUG
|
||||
if force:
|
||||
_PLUGINS_DEBUG = os.getenv("HERMES_PLUGINS_DEBUG", "").strip().lower() in (
|
||||
"1", "true", "yes", "on",
|
||||
)
|
||||
if not _PLUGINS_DEBUG or _DEBUG_HANDLER_INSTALLED:
|
||||
return
|
||||
handler = logging.StreamHandler(sys.stderr)
|
||||
handler.setLevel(logging.DEBUG)
|
||||
handler.setFormatter(logging.Formatter("[plugins] %(levelname)s %(message)s"))
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
# Don't double-emit through the root logger when the central logging
|
||||
# config also writes to stderr. agent.log still captures everything.
|
||||
logger.propagate = True
|
||||
_DEBUG_HANDLER_INSTALLED = True
|
||||
logger.debug(
|
||||
"HERMES_PLUGINS_DEBUG=1 — verbose plugin discovery logging enabled"
|
||||
)
|
||||
|
||||
|
||||
_install_plugin_debug_handler()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -653,28 +703,43 @@ class PluginManager:
|
|||
# is a category holding platform adapters (scanned one level deeper
|
||||
# below).
|
||||
repo_plugins = get_bundled_plugins_dir()
|
||||
manifests.extend(
|
||||
self._scan_directory(
|
||||
repo_plugins,
|
||||
source="bundled",
|
||||
skip_names={"memory", "context_engine", "platforms", "model-providers"},
|
||||
)
|
||||
logger.debug("Scanning bundled plugins: %s", repo_plugins)
|
||||
bundled = self._scan_directory(
|
||||
repo_plugins,
|
||||
source="bundled",
|
||||
skip_names={"memory", "context_engine", "platforms", "model-providers"},
|
||||
)
|
||||
manifests.extend(
|
||||
self._scan_directory(repo_plugins / "platforms", source="bundled")
|
||||
logger.debug(" bundled (top-level): %d manifest(s)", len(bundled))
|
||||
manifests.extend(bundled)
|
||||
bundled_platforms = self._scan_directory(
|
||||
repo_plugins / "platforms", source="bundled"
|
||||
)
|
||||
logger.debug(" bundled/platforms: %d manifest(s)", len(bundled_platforms))
|
||||
manifests.extend(bundled_platforms)
|
||||
|
||||
# 2. User plugins (~/.hermes/plugins/)
|
||||
user_dir = get_hermes_home() / "plugins"
|
||||
manifests.extend(self._scan_directory(user_dir, source="user"))
|
||||
logger.debug("Scanning user plugins: %s", user_dir)
|
||||
user_manifests = self._scan_directory(user_dir, source="user")
|
||||
logger.debug(" user: %d manifest(s)", len(user_manifests))
|
||||
manifests.extend(user_manifests)
|
||||
|
||||
# 3. Project plugins (./.hermes/plugins/)
|
||||
if _env_enabled("HERMES_ENABLE_PROJECT_PLUGINS"):
|
||||
project_dir = Path.cwd() / ".hermes" / "plugins"
|
||||
manifests.extend(self._scan_directory(project_dir, source="project"))
|
||||
logger.debug("Scanning project plugins: %s", project_dir)
|
||||
project_manifests = self._scan_directory(project_dir, source="project")
|
||||
logger.debug(" project: %d manifest(s)", len(project_manifests))
|
||||
manifests.extend(project_manifests)
|
||||
else:
|
||||
logger.debug(
|
||||
"Project plugins disabled (set HERMES_ENABLE_PROJECT_PLUGINS=1 to enable)"
|
||||
)
|
||||
|
||||
# 4. Pip / entry-point plugins
|
||||
manifests.extend(self._scan_entry_points())
|
||||
ep_manifests = self._scan_entry_points()
|
||||
logger.debug(" entrypoints: %d manifest(s)", len(ep_manifests))
|
||||
manifests.extend(ep_manifests)
|
||||
|
||||
# Load each manifest (skip user-disabled plugins).
|
||||
# Later sources override earlier ones on key collision — user
|
||||
|
|
@ -923,6 +988,10 @@ class PluginManager:
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
logger.debug(
|
||||
"Parsed manifest: key=%s name=%s kind=%s source=%s path=%s",
|
||||
key, name, kind, source, plugin_dir,
|
||||
)
|
||||
return PluginManifest(
|
||||
name=name,
|
||||
version=str(data.get("version", "")),
|
||||
|
|
@ -937,7 +1006,9 @@ class PluginManager:
|
|||
key=key,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to parse %s: %s", manifest_file, exc)
|
||||
logger.warning(
|
||||
"Failed to parse %s: %s", manifest_file, exc, exc_info=_PLUGINS_DEBUG,
|
||||
)
|
||||
return None
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
|
|
@ -977,6 +1048,10 @@ class PluginManager:
|
|||
def _load_plugin(self, manifest: PluginManifest) -> None:
|
||||
"""Import a plugin module and call its ``register(ctx)`` function."""
|
||||
loaded = LoadedPlugin(manifest=manifest)
|
||||
logger.debug(
|
||||
"Loading plugin '%s' (source=%s, kind=%s, path=%s)",
|
||||
manifest.key or manifest.name, manifest.source, manifest.kind, manifest.path,
|
||||
)
|
||||
|
||||
try:
|
||||
if manifest.source in ("user", "project", "bundled"):
|
||||
|
|
@ -1019,10 +1094,23 @@ class PluginManager:
|
|||
if self._plugin_commands[c].get("plugin") == manifest.name
|
||||
]
|
||||
loaded.enabled = True
|
||||
logger.debug(
|
||||
" registered: %d tool(s), %d hook(s), %d slash command(s), %d CLI command(s)",
|
||||
len(loaded.tools_registered),
|
||||
len(loaded.hooks_registered),
|
||||
len(loaded.commands_registered),
|
||||
sum(
|
||||
1 for c in self._cli_commands
|
||||
if self._cli_commands[c].get("plugin") == manifest.name
|
||||
),
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
loaded.error = str(exc)
|
||||
logger.warning("Failed to load plugin '%s': %s", manifest.name, exc)
|
||||
logger.warning(
|
||||
"Failed to load plugin '%s': %s",
|
||||
manifest.name, exc, exc_info=_PLUGINS_DEBUG,
|
||||
)
|
||||
|
||||
self._plugins[manifest.key or manifest.name] = loaded
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue