mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
perf(startup): lazy-load gateway platform adapters (#54448)
Bundled platform plugins (telegram, discord, feishu, teams, ...) were eagerly imported at plugin-discovery time on every `hermes` invocation, including plain `hermes chat` which never touches a gateway platform. Their modules import heavy platform SDKs at module level (lark_oapi, microsoft_teams, discord.py, slack_bolt, ...) — feishu alone pulled in lark_oapi (~2.6s), teams pulled microsoft_teams (~1.9s). Discovery now registers a cheap deferred loader per platform in the platform_registry; the adapter module is imported only when the gateway / cron / setup / send_message path actually asks for that platform. is_registered() and the iterate-all accessors stay correct (deferred counts as registered; plugin_entries()/all_entries() materialize all deferred loaders, since those paths genuinely need every adapter). Cold start: ~4.4s -> ~2.45s to banner. discover_and_load: 2.0s -> 0.3s (warm), and the heavy SDKs are no longer imported at all in CLI mode. Every shipped platform remains available out of the box — it just loads on first use.
This commit is contained in:
parent
b0b7ff0d75
commit
95f2919f91
2 changed files with 153 additions and 6 deletions
|
|
@ -306,6 +306,10 @@ class LoadedPlugin:
|
|||
commands_registered: List[str] = field(default_factory=list)
|
||||
enabled: bool = False
|
||||
error: Optional[str] = None
|
||||
# True for a bundled platform plugin recorded as a deferred (not-yet-
|
||||
# imported) loader. The module loads on first real use via the
|
||||
# platform_registry; see PluginManager._register_deferred_platform.
|
||||
deferred: bool = False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -1318,14 +1322,25 @@ class PluginManager:
|
|||
# just work. Selection among them (e.g. which image_gen backend
|
||||
# services calls) is driven by ``<category>.provider`` config,
|
||||
# enforced by the tool wrapper.
|
||||
#
|
||||
# Bundled platform plugins (gateway adapters like IRC) auto-load
|
||||
# for the same reason: every platform Hermes ships must be
|
||||
# available out of the box without the user having to opt in.
|
||||
if manifest.source == "bundled" and manifest.kind in {"backend", "platform"}:
|
||||
if manifest.source == "bundled" and manifest.kind == "backend":
|
||||
self._load_plugin(manifest)
|
||||
continue
|
||||
|
||||
# Bundled platform plugins (gateway adapters: telegram, discord,
|
||||
# feishu, teams, ...) are registered LAZILY. Their modules import
|
||||
# heavy, platform-specific SDKs at module level (lark_oapi,
|
||||
# microsoft_teams, discord.py, slack_bolt, ...), so eagerly loading
|
||||
# all ~20 of them added several seconds to every `hermes`
|
||||
# invocation — including plain `hermes chat`, which never touches a
|
||||
# gateway platform. Instead we register a cheap deferred loader in
|
||||
# the platform_registry keyed on the platform name; the real module
|
||||
# is imported only when the gateway / cron / setup / send_message
|
||||
# path actually asks for that platform. Every platform Hermes ships
|
||||
# remains available out of the box — it just loads on first use.
|
||||
if manifest.source == "bundled" and manifest.kind == "platform":
|
||||
self._register_deferred_platform(manifest)
|
||||
continue
|
||||
|
||||
# Everything else (standalone, user-installed backends,
|
||||
# entry-point plugins) is opt-in via plugins.enabled.
|
||||
# Accept both the path-derived key and the legacy bare name
|
||||
|
|
@ -1564,6 +1579,66 @@ class PluginManager:
|
|||
# Loading
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
def _platform_name_from_manifest(self, manifest: PluginManifest) -> str:
|
||||
"""Derive the gateway platform name (e.g. ``feishu``) for a platform plugin.
|
||||
|
||||
The platform name registered via ``register_platform(name=...)`` lives
|
||||
inside the adapter module (which we are explicitly trying NOT to import
|
||||
early). It is not carried in ``plugin.yaml``. Across every bundled
|
||||
platform plugin the manifest name is ``<platform>-platform`` and the
|
||||
plugin directory basename is ``<platform>``, so we derive the name
|
||||
without importing: strip a trailing ``-platform`` from the manifest
|
||||
name, falling back to the directory basename. This is also a sensible
|
||||
convention for third-party platform plugins.
|
||||
"""
|
||||
name = manifest.name or ""
|
||||
if name.endswith("-platform"):
|
||||
return name[: -len("-platform")]
|
||||
if manifest.path:
|
||||
return Path(manifest.path).name
|
||||
return name
|
||||
|
||||
def _register_deferred_platform(self, manifest: PluginManifest) -> None:
|
||||
"""Register a lazy loader for a bundled platform plugin.
|
||||
|
||||
The platform adapter module is imported only when the gateway / cron /
|
||||
setup / send_message path first asks the ``platform_registry`` for this
|
||||
platform. Until then we record a lightweight ``LoadedPlugin`` so
|
||||
``hermes plugins list`` still shows the platform as available, and we
|
||||
hand the registry a loader that runs the normal eager-load path.
|
||||
"""
|
||||
lookup_key = manifest.key or manifest.name
|
||||
platform_name = self._platform_name_from_manifest(manifest)
|
||||
|
||||
# Record an enabled placeholder for introspection (`hermes plugins
|
||||
# list`). The real module load swaps in a fully-populated LoadedPlugin
|
||||
# (tools/hooks/commands attribution) when the loader fires.
|
||||
loaded = LoadedPlugin(manifest=manifest, enabled=True)
|
||||
loaded.deferred = True
|
||||
self._plugins[lookup_key] = loaded
|
||||
|
||||
def _loader(_manifest: PluginManifest = manifest) -> None:
|
||||
self._load_plugin(_manifest)
|
||||
|
||||
try:
|
||||
from gateway.platform_registry import platform_registry
|
||||
|
||||
platform_registry.register_deferred(platform_name, _loader)
|
||||
logger.debug(
|
||||
"Registered deferred platform loader: %s (plugin=%s)",
|
||||
platform_name,
|
||||
lookup_key,
|
||||
)
|
||||
except Exception:
|
||||
# If the registry import fails for any reason, fall back to eager
|
||||
# loading so the platform is never silently lost.
|
||||
logger.debug(
|
||||
"Deferred platform registration failed for '%s'; eager-loading",
|
||||
lookup_key,
|
||||
exc_info=True,
|
||||
)
|
||||
self._load_plugin(manifest)
|
||||
|
||||
def _load_plugin(self, manifest: PluginManifest) -> None:
|
||||
"""Import a plugin module and call its ``register(ctx)`` function."""
|
||||
loaded = LoadedPlugin(manifest=manifest)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue