Skip to content

プラグインシステム

源码版本v2026.7.20

職務

プラグイン (plugin) は Hermes の拡張骨格:provider profile、プラットフォームアダプタ (platform adapter)、context engine、cron provider、記憶バックエンド、ブラウザ、画像/動画生成……すべてがプラグイン機構で接入される。plugins/PluginContext(登録入口)+ ユーティリティ関数を提供し、各プラグインのサブディレクトリはドメイン別に自己完結する。ToolRegistry/PlatformRegistry と協調し、プラグインは「宣言して登録する」だけ、レジストリ (registry) が「照合とスケジュール」を担う。

設計動機

なぜ明示的な plugins.yaml ではなく「ディレクトリ規約 + モジュール import 時の自己登録」なのか? 核心は新規能力追加の摩擦を下げることだ。サブディレクトリを $HERMES_HOME/plugins/model-providers/<name>/ に置けば、__init__.py が発見された時点で自動 import され、モジュールレベルのコードが register_provider() を呼んで登録を済ませる——設定ファイルをいじる必要がない。代償は、順序と上書きをレジストリの戦略(名前空間、override の明示 opt-in)で賄うことだ。見返りはホットプラグとサードパーティプラグインの摩擦ゼロ接入だ。

主要ファイル

データフロー

  1. 起動期 init_agent(agent/agent_init.py:276) がプラグイン発見をトリガーする。
  2. 各プラグインのサブディレクトリがインポートされ、モジュールレベルのコードが PluginContext の登録メソッドを呼ぶ:
  3. レジストリ(シングルトン)が照合の真実の情報源となり、主ループとゲートウェイ (gateway) はレジストリだけを読み、個別プラグインに直接依存しない。
  4. プラグインはホットプラグ可能:外すには unregister だけで、レジストリが優先度と上書きを処理する。

主要コード

ディレクトリ走査によるプラグイン発見

_discover_providers は model-providers の発見入口だ。二つのディレクトリ(リポジトリ内蔵 + $HERMES_HOME ユーザーディレクトリ)を走査し、各サブディレクトリで _import_plugin_dir を呼ぶ。

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")

「ユーザーディレクトリが内蔵を上書きする」のはこの順序由来だ。両ステップが register_provider を呼び、後者が前者を上書きする。_/. で始まるディレクトリは無視し、__pycache__ などの逃げ口を空けておく。

自己登録:モジュール import 時に登録

_import_plugin_dirimportlib.util でディレクトリをモジュールとして読み込み、モジュールレベルのコードが実行されると register_provider(profile) を呼んで profile をグローバルレジストリに押し込む。

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 と user で異なるモジュール名前空間を使い、二つの HERMES_HOME 下の同名 profile が sys.modules で互いに alias するのを避ける。

レジストリ:黙って内蔵ツールを上書きさせない

registry.register はデフォルトで toolset をまたぐ同名上書きを拒否する。MCP 同士の上書きは許可される(リフレッシュシナリオは合法)が、プラグインが内蔵ツールを置き換えるには明示的に override=True を指定し、かつ operator が config で allow_tool_override: true を設定していなければ通さない——さもないと PermissionError を投げ、黙って置き換えない。owner 判定は handler.__globals__["__name__"] に基づき、定義時に束縛される。プラグインがネストした lambda で上書きを「内蔵モジュール由来」に洗浄するのは通らない。詳細は registry.register:365-436 を参照。

境界と失敗

  • プラグイン import 失敗:_import_plugin_dir は例外を飲んで warning だけ出し、半ロード状態を避けるため sys.modules から弾く。一つの壊れたプラグインが agent 全体を道連れにしないが、該当する provider/platform は取得できず、ログを見ないと気づかない。
  • 内蔵ツールの上書きに明示 opt-in がない:registry.register は黙って置き換えず PermissionError を投げる。operator が config.yaml に plugins.entries.<plugin_id>.allow_tool_override: true を書いて初めて通す。
  • context engine は deepcopy 不可:複数 agent で共有する context engine は child agent 起動時に copy.deepcopy で budget 状態を隔離する。プラグインがロックや DB 接続などの deepcopy 不可オブジェクトを保持していると、deepcopy 失敗で内蔵 compressor にフォールバック (fallback) する。

まとめ

プラグイン = 「サブディレクトリ + 登録」。provider、プラットフォーム、ツール、記憶、cron、マルチモーダルといったすべての横断能力が同じ登録骨格を通るので、コアループは安定を保てる。新機能の追加 = プラグインサブディレクトリを一つ作って登録するだけで、agent 主幹には触れない。

非公式コミュニティ学習サイト。MIT ライセンスの NousResearch/hermes-agent ソースに基づく。