プラグインシステム
職務
プラグイン (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)で賄うことだ。見返りはホットプラグとサードパーティプラグインの摩擦ゼロ接入だ。
主要ファイル
plugins/__init__.py—PluginContext、登録入口(register_platform、register_providerなど)plugin_utils.py— プラグイン共用ユーティリティmodel-providers/— LLM provider profile(Providers参照)platforms/— Telegram/Discord/Slack などのプラットフォームアダプタ(プラットフォームアダプタ参照)context_engine/— コンテキストエンジン (context engine) 拡張cron_providers/— cron スケジュールバックエンドmemory/— 記憶バックエンドbrowser//web/— ブラウザと Web 能力image_gen//video_gen/— マルチモーダル生成observability//security-guidance/— 可観測性とセキュリティ
データフロー
- 起動期
init_agent(agent/agent_init.py:276) がプラグイン発見をトリガーする。 - 各プラグインのサブディレクトリがインポートされ、モジュールレベルのコードが
PluginContextの登録メソッドを呼ぶ:- model-provider →
register_provider:53 - platform →
platform_registry.register:231 - ツール (tool) →
registry.register:365(名前空間ポリシー経由)
- model-provider →
- レジストリ(シングルトン)が照合の真実の情報源となり、主ループとゲートウェイ (gateway) はレジストリだけを読み、個別プラグインに直接依存しない。
- プラグインはホットプラグ可能:外すには
unregisterだけで、レジストリが優先度と上書きを処理する。
主要コード
ディレクトリ走査によるプラグイン発見
_discover_providers は model-providers の発見入口だ。二つのディレクトリ(リポジトリ内蔵 + $HERMES_HOME ユーザーディレクトリ)を走査し、各サブディレクトリで _import_plugin_dir を呼ぶ。
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_dir は importlib.util でディレクトリをモジュールとして読み込み、モジュールレベルのコードが実行されると register_provider(profile) を呼んで profile をグローバルレジストリに押し込む。
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 主幹には触れない。