Skip to content

插件系统

源码版本v2026.7.20

职责

插件是 Hermes 的扩展骨架:provider profile、平台适配器、context engine、cron provider、记忆后端、浏览器、图像/视频生成……都通过插件机制接入。plugins/ 提供 PluginContext(注册入口)+ 工具函数,各插件子目录按领域自包含。它和 ToolRegistry/PlatformRegistry 协同:插件只负责「声明并注册」,注册表负责「查询与调度」。

设计动机

为什么是「目录约定 + 模块导入时自注册」,而不是显式 plugins.yaml?核心是降低新增能力的摩擦:把子目录扔进 $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. 注册表(单例)成为查询真相,主循环与网关只读注册表,不直接依赖具体插件。
  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_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.registerPermissionError 而不是默默替换;operator 必须在 config.yaml 里写 plugins.entries.<plugin_id>.allow_tool_override: true 才放行。
  • context engine 不可深拷贝:多 agent 共享的 context engine 在 child agent 启动时 copy.deepcopy 隔离 budget 状态;插件若持有锁、DB 连接等不可深拷贝对象,深拷贝失败会 fallback 到内置 compressor。

小结

插件 = 「子目录 + 注册」。所有跨切面能力(provider、平台、工具、记忆、cron、多模态)都走同一套注册骨架,核心循环因此保持稳定。新增能力 = 新建一个插件子目录并注册,无需动 agent 主干。

非官方社区学习站,内容以 MIT 许可的 NousResearch/hermes-agent 源码为依据。