Skip to content

Plugin-System

源码版本v2026.7.20

Verantwortung

Plugins (plugins) sind das Erweiterungsskelett von Hermes: Provider-Profile, Plattform-Adapter (adapter), Context-Engine, Cron-Provider (provider), Memory-Backends, Browser, Bild-/Video-Generierung – alles wird über den Plugin-Mechanismus angebunden. plugins/ stellt PluginContext (Registrierungseinstieg) + Hilfsfunktionen; jedes Plugin-Unterverzeichnis ist domänen-lokal gekapselt. Es arbeitet mit ToolRegistry/PlatformRegistry zusammen: Das Plugin «deklariert und registriert» nur, die Registry (registry) ist zuständig für «Lookup und Dispatch (dispatch)».

Designmotiv

Warum «Verzeichnis-Konvention + Selbst-Registrierung beim Modul-Import» statt einer expliziten plugins.yaml? Kern ist, die Reibung beim Hinzufügen neuer Fähigkeiten zu senken: Ein Unterverzeichnis in $HERMES_HOME/plugins/model-providers/<name>/ werfen, beim Fund wird __init__.py automatisch importiert, der Modul-Code ruft register_provider() und schließt die Registrierung ab — keine Konfigurationsdatei anfassen. Der Preis: Reihenfolge und Überschreibung werden über Registry-Strategien abgesichert (Namespace, explizites override-Opt-in); die Gegenleistung ist Hot-Swap und eine Drittanbieter-Plugin-Anbindung ohne Reibung.

Schlüsseldateien

Datenfluss

  1. Beim Start löst init_agent (agent/agent_init.py:276) die Plugin-Discovery aus.
  2. Jedes Plugin-Unterverzeichnis wird importiert; der modulweite Code ruft die Registrierungsmethoden von PluginContext auf:
  3. Die Registrys (Singletons) werden zur Abfrage-Wahrheit; Hauptschleife und Gateway (gateway) lesen nur aus ihnen und hängen nicht direkt an konkreten Plugins.
  4. Plugins sind hot-swapable: Entfernen läuft über unregister; die Registry behandelt Priorität und Überschreibung.

Schlüsselcode

Verzeichnis-Scan zur Plugin-Discovery

_discover_providers ist der Discovery-Einstieg der model-providers. Es scannt zwei Verzeichnisse (das repo-eigene + das Benutzerverzeichnis $HERMES_HOME) und ruft für jedes Unterverzeichnis _import_plugin_dir auf:

python
def _discover_providers() -> None:
    """1. Bundled at <repo>/plugins/model-providers/<name>/
       2. User at $HERMES_HOME/plugins/model-providers/<name>/
       Later steps win on name collision."""
    global _discovered
    if _discovered: return
    _discovered = True
    if _BUNDLED_PLUGINS_DIR.is_dir():
        for child in sorted(_BUNDLED_PLUGINS_DIR.iterdir()):
            if child.is_dir() and not child.name.startswith(("_", ".")):
                _import_plugin_dir(child, "bundled")
    user_dir = _user_plugins_dir()
    if user_dir is not None:
        for child in sorted(user_dir.iterdir()):
            if child.is_dir() and not child.name.startswith(("_", ".")):
                _import_plugin_dir(child, "user")

«Benutzerverzeichnis überschreibt eingebaut» kommt aus dieser Reihenfolge: Beide Schritte rufen register_provider auf, der zweite überschreibt den ersten. Verzeichnisse mit _/.-Präfix werden ignoriert — das lässt einen Fluchtweg für __pycache__ und Konsorten.

Selbst-Registrierung: Modul-Import als Registrierung

_import_plugin_dir lädt das Verzeichnis via importlib.util als Modul; beim Ablauf des Modul-Codes schiebt register_provider(profile) das Profil in die globale Registry:

python
module_name = (f"plugins.model_providers.{safe_name}" if source == "bundled"
               else f"_hermes_user_provider_{safe_name}")
spec = importlib.util.spec_from_file_location(
    module_name, init_file, submodule_search_locations=[str(plugin_dir)])
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)

Bundled und User laufen über getrennte Modul-Namespaces, damit sich gleichnamige Profile aus zwei HERMES_HOME-Quellen in sys.modules nicht gegenseitig aliasen.

Registry: stillschweigendes Überschreiben eingebauter Tools verhindern

registry.register lehnt gleichnamige Überschreibungen über Toolset-Grenzen hinweg standardmäßig ab. Zwischen MCPs ist Überschreiben erlaubt (Refresh-Szenario legitim); ein Plugin, das ein eingebautes Tool ersetzen will, muss explizit override=True setzen und vorher in der Config allow_tool_override: true freigeschaltet haben — sonst fliegt direkt eine PermissionError statt stiller Ersetzung. Die Owner-Erkennung basiert auf handler.__globals__["__name__"], das beim Definieren gebunden wird; ein Plugin kann eine Überschreibung nicht über eine verschachtelte Lambda als «aus dem Modul eingebaut» waschen. Siehe registry.register:365-436.

Grenzen und Fehler

  • Plugin-Import fehlschlägt: _import_plugin_dir schluckt die Ausnahme, schreibt nur ein Warning und entfernt das Modul aus sys.modules, um einen halbgeladenen Zustand zu vermeiden. Ein kaputtes Plugin reißt nicht den ganzen Agenten mit, aber der entsprechende Provider/Plattform ist nicht erreichbar — nur im Log sichtbar.
  • Überschreiben eines eingebauten Tools ohne explizites Opt-in: registry.register wirft eine PermissionError statt still zu ersetzen; der Operator muss in der config.yaml plugins.entries.<plugin_id>.allow_tool_override: true setzen, damit es freigegeben wird.
  • Context-Engine nicht tief kopierbar: Die von mehreren Agenten geteilte Context-Engine wird beim Start eines Child-Agenten per copy.deepcopy vom Budget (budget)-Zustand isoliert; hält ein Plugin Objekte, die sich nicht tief kopieren lassen (Locks, DB-Verbindungen), schlägt das Deep-Copy fehl und fällt auf den eingebauten Compressor zurück.

Zusammenfassung

Plugin = «Unterverzeichnis + Registrierung». Alle querschnittlichen Fähigkeiten (Provider, Plattform, Werkzeug (tool), Memory, Cron, Multimodal) laufen über dasselbe Registrierungsskelett, wodurch die Kernschleife stabil bleibt. Eine neue Fähigkeit = neues Plugin-Unterverzeichnis plus Registrierung, ohne Eingriff in den Agent-Stamm.

Inoffizielle Community-Lernseite. Basiert auf dem MIT-lizenzierten NousResearch/hermes-agent-Quellcode.