乐于分享
好东西不私藏

Hermes Agent 源码-Provider 抽象层

Hermes Agent 源码-Provider 抽象层

Hermes Agent 源码解析

第 5 讲:Provider 抽象层——ProviderProfile、模型路由、凭证池与多源认证

基于 Hermes Agent v0.16.0 源码 · 2026-06-18

一、Provider 抽象层:Hermes 的"AI 推理路由器"

前四讲我们建立了全局架构地图、深入了对话循环、解析了工具系统的三层架构、拆解了插件系统。这一讲聚焦 Hermes 最核心的基础设施之一——Provider 抽象层

Provider 抽象层是 Hermes 的"AI 推理路由器":它决定了你的请求发给哪个模型提供商、用什么协议、携带什么凭证、走什么传输通道。从 Nous Portal 到 OpenRouter,从 Anthropic 到 Kimi,从本地 Ollama 到远程 Bedrock——超过 30 个提供商全部通过同一套抽象层接入。

📦 源码仓库

https://github.com/NousResearch/hermes-agent

本地源码:~/.hermes/hermes-agent/

本讲核心文件:

providers/base.py(214 行)— ProviderProfile 基类

providers/__init__.py(191 行)— 注册表与发现

plugins/model-providers/(30+ 提供商插件)

hermes_cli/auth.py(7926 行)— 多源认证系统

hermes_cli/runtime_provider.py(1775 行)— 运行时路由

agent/credential_pool.py(2184 行)— 凭证池

agent/transports/chat_completions.py(704 行)— 传输层

agent/auxiliary_client.py(5949 行)— 辅助客户端路由

二、ProviderProfile:声明式提供商契约

一切始于 ProviderProfile——一个声明式 dataclass,将每个提供商的完整行为描述集中在一个对象中。设计哲学是:"传输层读 profile,而不是接收 20+ 个布尔标志"

# providers/base.py:38-95
@dataclass
class ProviderProfile:
    """Base provider profile — subclass or instantiate with overrides."""

    # ── Identity ──
    name: str                        # 唯一标识: "anthropic", "openrouter"
    api_mode: str = "chat_completions"  # 协议模式
    aliases: tuple = ()              # 别名: ("claude", "claude-oauth")

    # ── 人类可读元数据 ──
    display_name: str = ""           # 显示名称: "GMI Cloud"
    description: str = ""            # 描述: "GMI Cloud (multi-model direct API)"
    signup_url: str = ""             # 注册链接

    # ── 认证与端点 ──
    env_vars: tuple = ()             # 环境变量: ("ANTHROPIC_API_KEY",)
    base_url: str = ""               # API 基础 URL
    models_url: str = ""             # 模型列表端点(可选覆盖)
    auth_type: str = "api_key"       # api_key|oauth_device_code|oauth_external|copilot|aws_sdk
    supports_health_check: bool = True

    # ── 视觉支持 ──
    supports_vision: bool = False
    supports_vision_tool_messages: bool = True

    # ── 模型目录 ──
    fallback_models: tuple = ()      # 离线时的备选模型列表
    hostname: str = ""

    # ── 客户端级特性 ──
    default_headers: dict = field(default_factory=dict)

    # ── 请求级特性 ──
    fixed_temperature: Any = None    # None=默认, OMIT_TEMPERATURE=不发送
    default_max_tokens: int | None = None
    default_aux_model: str = ""      # 辅助任务用廉价模型

可覆盖的钩子方法

Profile 定义了五个可覆盖钩子,每个解决一类提供商特异性问题:

钩子方法                                    作用                      典型消费者
─────────────────────────────────────────────────────────────────────────────
get_hostname()                              URL 反查                model_metadata.py
prepare_messages(msgs)                      消息预处理              Qwen 归一化 + cache_control
build_extra_body(**ctx)                     extra_body 字段         OpenRouter provider prefs
build_api_kwargs_extras(**ctx)              (extra_body, top_level) Kimi reasoning_effort
fetch_models(*, api_key)                    实时模型目录              模型选择器

实际 Profile 示例

以 Anthropic 为例,它需要覆盖 fetch_models 因为 Anthropic 用 x-api-key 头而非 Bearer:

# plugins/model-providers/anthropic/__init__.py:13-52
class AnthropicProfile(ProviderProfile):
    def fetch_models(self, *, api_key, timeout=8.0):
        req = urllib.request.Request("https://api.anthropic.com/v1/models")
        req.add_header("x-api-key", api_key)        # ← 非 Bearer!
        req.add_header("anthropic-version", "2023-06-01")
        ...

anthropic = AnthropicProfile(
    name="anthropic",
    aliases=("claude", "claude-oauth", "claude-code"),
    api_mode="anthropic_messages",
    env_vars=("ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"),
    base_url="https://api.anthropic.com",
    auth_type="api_key",
    default_aux_model="claude-haiku-4-5-20251001",
)
register_provider(anthropic)

三、Provider 注册表:懒发现与插件化

Provider 注册表在 providers/__init__.py 中实现,采用懒发现策略——首次调用 get_provider_profile()list_providers() 时才扫描所有插件。

# providers/__init__.py:43-73
_REGISTRY: dict[str, ProviderProfile] = {}
_ALIASES: dict[str, str] = {}
_discovered = False

def get_provider_profile(name: str) -> ProviderProfile | None:
    if not _discovered:
        _discover_providers()          # ← 懒加载触发点
    canonical = _ALIASES.get(name, name)
    return _REGISTRY.get(canonical)

# providers/__init__.py:140-191
def _discover_providers() -> None:
    """三阶段发现,后者覆盖前者:
      1. Bundled plugins: <repo>/plugins/model-providers/<name>/
      2. User plugins:    $HERMES_HOME/plugins/model-providers/<name>/
      3. Legacy modules:  providers/<name>.py (向后兼容)
    """
    global _discovered
    _discovered = True
    # 阶段 1: bundled
    for child in sorted(_BUNDLED_PLUGINS_DIR.iterdir()):
        _import_plugin_dir(child, "bundled")
    # 阶段 2: user (覆盖同名 bundled)
    for child in sorted(user_dir.iterdir()):
        _import_plugin_dir(child, "user")
    # 阶段 3: legacy single-file
    for _, modname, _ in pkgutil.iter_modules(_pkg.__path__):
        importlib.import_module(f"providers.{modname}")

注册表如何被下游消费

下游消费者                    读取内容
────────────────────────────────────────────
hermes_cli/auth.py            → PROVIDER_REGISTRY 扩展 api-key profile
hermes_cli/models.py          → 调用 profile.fetch_models()
hermes_cli/doctor.py          → 对 auth_type="api_key" 做 /models 健康检查
hermes_cli/config.py          → 注入 env_vars 到 OPTIONAL_ENV_VARS
hermes_cli/runtime_provider.py → profile.api_mode 作为 URL 检测失败时的回退
agent/model_metadata.py       → profile.get_hostname() 做 hostname→provider 映射
agent/auxiliary_client.py     → profile.default_aux_model 优先于硬编码 dict
agent/transports/chat_completions.py → 调用 prepare_messages(),
                                       build_extra_body(), build_api_kwargs_extras()
run_agent.py                  → 传递 provider_profile= 取代旧标志路径

四、多源认证系统:OAuth + API Key + 凭证池

hermes_cli/auth.py(7926 行)是 Hermes 的认证中枢,支持五种认证类型:

认证类型                    机制                          典型提供商
─────────────────────────────────────────────────────────────────
api_key                   环境变量 / config.yaml          OpenRouter, Gemini, Kimi
oauth_device_code         RFC 8628 设备码流程             Nous Portal, OpenAI Codex
oauth_external            外部 OAuth 流程                 Qwen, xAI Grok, MiniMax
copilot                   GitHub Copilot ACP              Copilot, Copilot ACP
aws_sdk                   AWS SDK 凭证                    Bedrock

ProviderConfig 注册表

每个提供商在 PROVIDER_REGISTRY 中有对应的 ProviderConfig

# hermes_cli/auth.py:150-200
@dataclass
class ProviderConfig:
    id: str                        # 唯一 ID: "nous", "openai-codex"
    name: str                      # 显示名: "Nous Portal"
    auth_type: str                 # 认证类型
    portal_base_url: str = ""      # OAuth 门户 URL
    inference_base_url: str = ""   # 推理端点 URL
    client_id: str = ""            # OAuth client_id
    scope: str = ""                # OAuth scope
    extra: Dict[str, Any] = field(default_factory=dict)
    api_key_env_vars: tuple = ()   # API key 环境变量
    base_url_env_var: str = ""     # 可选的 base_url 环境变量

PROVIDER_REGISTRY = {
    "nous": ProviderConfig(
        id="nous", name="Nous Portal",
        auth_type="oauth_device_code",
        portal_base_url="https://portal.nousresearch.com",
        inference_base_url="https://inference-api.nousresearch.com/v1",
        client_id="hermes-cli", scope="inference:invoke",
    ),
    "openai-api": ProviderConfig(
        id="openai-api", name="OpenAI API",
        auth_type="api_key",
        inference_base_url="https://api.openai.com/v1",
        api_key_env_vars=("OPENAI_API_KEY",),
        base_url_env_var="OPENAI_BASE_URL",
    ),
    ...
}

认证状态持久化

认证状态存储在 ~/.hermes/auth.json,带跨进程文件锁:

认证状态结构 (auth.json):
{
  "version": 1,
  "active_provider": "nous",
  "providers": {
    "nous": {
      "tokens": {
        "access_token": "eyJ...",
        "refresh_token": "...",
        "agent_key": "eyJ..."        ← Nous 专用 invoke JWT
      },
      "expires_at": "2026-07-18T...",
      "last_refresh": "2026-06-18T..."
    }
  },
  "credential_pools": {
    "openrouter": [ ... ],           ← 多 key 凭证池
    "custom:together-ai": [ ... ]
  }
}

五、运行时路由:从配置到凭证的决议链

runtime_provider.py(1775 行)是 Hermes 的"推理路由器",负责将用户请求路由到正确的提供商和模型。

Provider 决议优先级链

# hermes_cli/runtime_provider.py:426-442
def resolve_requested_provider(requested=None) -> str:
    """Provider 决议优先级链:
      1. 显式参数 (hermes --provider xxx)
      2. config.yaml model.provider
      3. HERMES_INFERENCE_PROVIDER 环境变量
      4. "auto" (自动检测)
    """
    if requested and requested.strip():
        return requested.strip().lower()     # ← 最高优先级
    model_cfg = _get_model_config()
    cfg_provider = model_cfg.get("provider")
    if isinstance(cfg_provider, str) and cfg_provider.strip():
        return cfg_provider.strip().lower()
    env_provider = os.getenv("HERMES_INFERENCE_PROVIDER", "").strip().lower()
    if env_provider:
        return env_provider
    return "auto"

api_mode 自动检测

当用户没有显式指定 api_mode 时,系统通过 URL 自动推断:

# runtime_provider.py:76-100
def _detect_api_mode_for_url(base_url: str) -> Optional[str]:
    """通过 URL 自动检测 api_mode:
      - api.openai.com → codex_responses (GPT-5.x 需要 Responses API)
      - api.x.ai → codex_responses
      - URL 以 /anthropic 结尾 → anthropic_messages
      - api.kimi.com/coding → anthropic_messages
    """
    hostname = base_url_hostname(base_url)
    if hostname == "api.x.ai":
        return "codex_responses"
    if hostname == "api.openai.com":
        return "codex_responses"
    if normalized.endswith("/anthropic"):
        return "anthropic_messages"
    if hostname == "api.kimi.com" and "/coding" in normalized:
        return "anthropic_messages"
    return None

# 有效的 api_mode 集合 (runtime_provider.py:240-251)
_VALID_API_MODES = {
    "chat_completions",      # OpenAI 兼容 (默认)
    "codex_responses",       # OpenAI Codex Responses API
    "anthropic_messages",    # Anthropic Messages API
    "bedrock_converse",      # AWS Bedrock
    "codex_app_server",      # 通过 codex app-server 子进程
}

运行时解析完整流程

运行时解析流程 (runtime_provider.py:289-423):

  用户请求
    │
    ▼
  resolve_requested_provider()
    │  显式参数 → config.yaml → 环境变量 → "auto"
    ▼
  从 CredentialPool 选取凭证
    │
    ▼
  _resolve_runtime_from_pool_entry()
    │
    ├─ provider == "openai-codex"  → api_mode="codex_responses"
    ├─ provider == "anthropic"     → api_mode="anthropic_messages"
    ├─ provider == "qwen-oauth"    → api_mode="chat_completions"
    ├─ provider == "xai-oauth"     → api_mode="codex_responses"
    ├─ provider == "minimax-oauth" → api_mode="anthropic_messages"
    ├─ provider == "nous"          → api_mode="chat_completions"
    └─ 其他                        → 检测 URL 或读 config
    │
    ▼
  返回运行时字典:
  {
    "provider": "nous",
    "api_mode": "chat_completions",
    "base_url": "https://inference-api.nousresearch.com/v1",
    "api_key": "eyJ...",
    "source": "pool:nous",
    "credential_pool": <CredentialPool>,
  }

六、凭证池:多 Key 容灾与轮换

agent/credential_pool.py(2184 行)实现了 Hermes 的凭证池系统——同一提供商可配置多个 API key,自动容灾切换。

PooledCredential 数据结构

# agent/credential_pool.py:129-163
@dataclass
class PooledCredential:
    provider: str                    # 提供商: "openrouter", "nous"
    id: str                          # 唯一 ID (6-char hex)
    label: str                       # 标签 (JWT email 或来源)
    auth_type: str                   # "oauth" 或 "api_key"
    priority: int                    # 优先级 (数字越小越优先)
    source: str                      # 来源: "device_code", "manual", "loopback_pkce"
    access_token: str                # 主凭证
    refresh_token: Optional[str]     # OAuth 刷新令牌
    last_status: Optional[str]       # "ok", "exhausted", "dead"
    last_status_at: Optional[float]  # 状态时间戳
    last_error_code: Optional[int]   # HTTP 错误码
    last_error_reason: Optional[str] # 错误原因
    last_error_reset_at: Optional[float]  # 恢复时间
    base_url: Optional[str]          # 端点 URL
    expires_at: Optional[str]        # 过期时间
    request_count: int = 0           # 请求计数
    extra: Dict[str, Any] = None     # 扩展字段

凭证状态机

凭证状态机 (credential_pool.py:55-63):

  ┌──────────┐    401 认证失败    ┌─────────────┐
  │   OK     │ ─────────────────→ │  EXHAUSTED  │
  │ (可用)    │                    │ (冷却中)     │
  └──────────┘                    └──────┬──────┘
       ▲                                │
       │  TTL 到期 / 手动重置            │ 终端失败
       │                                │ (token_invalidated)
       └────────────────────────────────┼──→ ┌────────┐
                                        │    │  DEAD  │
                                        │    │ (废弃) │
                                        │    └────────┘
                                        │
  冷却时间:
    401 认证失败 → 5 分钟
    429 频率限制 → 1 小时
    402 余额不足 → 1 小时
    其他错误     → 1 小时
    提供商 reset_at 时间戳 → 覆盖默认值

四种轮换策略

# credential_pool.py:96-105
SUPPORTED_POOL_STRATEGIES = {
    "fill_first",     # 填满第一个 key 的配额后再切下一个 (默认)
    "round_robin",    # 轮询: 均匀分配请求到所有 key
    "random",         # 随机选择
    "least_used",     # 选择请求数最少的 key
}

# 在 config.yaml 中配置:
credential_pool_strategies:
  openrouter: round_robin
  custom:my-api: least_used

跨进程凭证同步

凭证池实现了精密的跨进程同步机制,防止 refresh token 被重复消费:

同步路径 (credential_pool.py:541-770):

  _sync_anthropic_entry_from_credentials_file()
    → 检测 ~/.claude/.credentials.json 变化
    → OAuth refresh token 是单次使用的,外部刷新后必须同步

  _sync_codex_entry_from_auth_store()
    → 检测 auth.json 中 openai-codex 凭证更新
    → 防止 pool entry 冻结在 last_error_reset_at 之前

  _sync_xai_oauth_entry_from_auth_store()
    → 检测 xAI OAuth 凭证更新

  _sync_nous_entry_from_auth_store()
    → 检测 Nous OAuth 凭证 + agent_key 更新

  _sync_device_code_entry_to_auth_store()
    → 将 pool 刷新后的凭证写回 auth.json
    → 防止下次 load_pool() 用旧 token 覆盖新 token

七、传输层集成:Profile 取代标志

传输层 ChatCompletionsTransport 有两条路径:Profile 路径(新)和标志路径(旧)。

双路径设计

# agent/transports/chat_completions.py:274-284
def build_kwargs(self, model, messages, tools=None, **params):
    sanitized = self.convert_messages(messages, model=model)

    # ── Profile 路径: 当 provider_profile 存在时 ──
    _profile = params.get("provider_profile")
    if _profile:
        return self._build_kwargs_from_profile(
            _profile, model, sanitized, tools, params
        )

    # ── 标志路径: 未注册/未知提供商的后备 ──
    # 使用 is_openrouter, is_nous, is_qwen_portal 等标志
    ...

Profile 路径的 kwargs 组装

# chat_completions.py:458-597
def _build_kwargs_from_profile(self, profile, model, msgs, tools, params):
    """Profile 路径 — 所有特性来自 profile 对象"""

    # 1. 消息预处理
    msgs = profile.prepare_messages(msgs)

    # 2. Developer role swap (GPT-5/Codex)
    if any(p in model_lower for p in DEVELOPER_ROLE_MODELS):
        msgs[0]["role"] = "developer"

    # 3. Temperature
    if profile.fixed_temperature == OMIT_TEMPERATURE:
        pass  # 不发送 temperature
    elif profile.fixed_temperature is not None:
        kwargs["temperature"] = profile.fixed_temperature

    # 4. max_tokens (优先级: ephemeral > user > profile default)
    profile_max = profile.get_max_tokens(model)

    # 5. Provider-specific extras
    extra_body, top_level = profile.build_api_kwargs_extras(
        reasoning_config=..., supports_reasoning=..., model=model, ...
    )
    kwargs.update(top_level)

    # 6. extra_body 组装
    profile_body = profile.build_extra_body(
        session_id=..., provider_preferences=..., model=model, ...
    )
    extra_body.update(profile_body)

    # 7. Gemini native 特殊处理 — 过滤 extra_body 中的 OpenAI 特有字段
    if is_native_gemini_base_url(base_url):
        extra_body = {k: v for k, v in extra_body.items()
                      if k in ("thinking_config", "thinkingConfig")}

    return kwargs

八、辅助客户端路由:自动降级链

agent/auxiliary_client.py(5949 行)为压缩、搜索、视觉分析等辅助任务提供自动降级:

# auxiliary_client.py:7-40
文本任务降级链:
  1. 用户主提供商 + 主模型
  2. OpenRouter  (OPENROUTER_API_KEY)
  3. Nous Portal (auth.json active provider)
  4. 自定义端点 (model.base_url + OPENAI_API_KEY)
  5. 原生 Anthropic
  6. 直连 API key 提供商 (z.ai/GLM, Kimi, MiniMax)
  7. None (无可用提供商)

视觉任务降级链:
  1. 主提供商 (如果支持视觉)
  2. OpenRouter
  3. Nous Portal
  4. 原生 Anthropic
  5. 自定义端点 (本地视觉模型: Qwen-VL, LLaVA, Pixtral)
  6. None

⚠️ OpenAI Codex OAuth 故意不在降级链中:
   OpenAI 对此端点使用未公开、不断变化的模型白名单,
   "硬编码模型尝试"会自行过期。

辅助客户端还实现了支付/额度耗尽自动降级

支付/额度耗尽降级 (auxiliary_client.py:36-41):
  当解析的提供商返回 HTTP 402 或额度相关错误时,
  call_llm() 自动用降级链中下一个可用提供商重试。
  处理常见场景: 用户耗尽 OpenRouter 余额但仍有 Codex OAuth
  或其他提供商可用。

九、架构图:Provider 抽象层全貌

┌─────────────────────────────────────────────────────────────────────┐
│                      Hermes Provider 抽象层                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌──────────────┐   ┌──────────────────┐   ┌─────────────────────┐ │
│  │  用户请求     │   │  runtime_provider │   │  auth.py            │ │
│  │  hermes /    │──→│  .py             │──→│  (认证系统)         │ │
│  │  cron / GW   │   │                  │   │                     │ │
│  └──────────────┘   │  决议优先级:      │   │  OAuth device_code  │ │
│                     │  ① 显式参数       │   │  OAuth external     │ │
│                     │  ② config.yaml    │   │  API key            │ │
│                     │  ③ 环境变量        │   │  Copilot ACP        │ │
│                     │  ④ "auto"         │   │  AWS SDK            │ │
│                     └────────┬─────────┘   └──────────┬──────────┘ │
│                              │                        │             │
│                     ┌────────▼─────────┐   ┌──────────▼──────────┐ │
│                     │  CredentialPool  │   │  auth.json          │ │
│                     │  .py             │   │  (认证状态持久化)    │ │
│                     │                  │   │                     │ │
│                     │  四种轮换策略:    │   │  providers:         │ │
│                     │  fill_first      │   │  credential_pools:  │ │
│                     │  round_robin     │   │  active_provider:   │ │
│                     │  random          │   └─────────────────────┘ │
│                     │  least_used      │                           │
│                     │                  │                           │
│                     │  状态机:         │                           │
│                     │  OK → EXHAUSTED  │                           │
│                     │      → DEAD      │                           │
│                     └────────┬─────────┘                           │
│                              │                                     │
│                     ┌────────▼─────────┐   ┌─────────────────────┐ │
│                     │  运行时字典       │   │  ProviderProfile    │ │
│                     │  {provider,      │   │  registry           │ │
│                     │   api_mode,      │←──│  (懒发现)           │ │
│                     │   base_url,      │   │                     │ │
│                     │   api_key,       │   │  三阶段发现:         │ │
│                     │   pool, ...}     │   │  ① bundled          │ │
│                     └────────┬─────────┘   │  ② user (覆盖)      │ │
│                              │              │  ③ legacy           │ │
│                     ┌────────▼─────────┐   └─────────────────────┘ │
│                     │  Transport       │                           │
│                     │  选择器          │                           │
│                     │                  │                           │
│                     │  api_mode:       │                           │
│                     │  chat_completions → ChatCompletionsTransport │
│                     │  codex_responses  → CodexTransport          │
│                     │  anthropic_msgs   → AnthropicTransport      │
│                     │  bedrock_converse → BedrockTransport        │
│                     └────────┬─────────┘                           │
│                              │                                     │
│                     ┌────────▼─────────┐                           │
│                     │  _build_kwargs   │                           │
│                     │  _from_profile() │                           │
│                     │                  │                           │
│                     │  profile.        │                           │
│                     │  prepare_messages│                           │
│                     │  build_extra_body│                           │
│                     │  build_api_kwargs│                           │
│                     └────────┬─────────┘                           │
│                              │                                     │
│                     ┌────────▼─────────┐                           │
│                     │  HTTP 请求       │                           │
│                     │  → 提供商 API    │                           │
│                     └──────────────────┘                           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

十、设计模式总结

Provider 抽象层使用的设计模式:

  ① 声明式配置 (Declarative Config)
     ProviderProfile dataclass 声明所有特性,
     而非散落在各层的硬编码标志。

  ② 插件注册 (Plugin Registry)
     三阶段懒发现,user 覆盖 bundled,
     后注册者胜出 (last-writer-wins)。

  ③ 策略模式 (Strategy Pattern)
     凭证池轮换: fill_first / round_robin / random / least_used
     传输层: chat_completions / codex_responses / anthropic_messages

  ④ 状态机 (State Machine)
     凭证状态: OK → EXHAUSTED → DEAD
     带 TTL 冷却和 provider-supplied reset_at 覆盖。

  ⑤ 钩子系统 (Hook System)
     Profile 的 5 个可覆盖钩子:
     get_hostname, prepare_messages, build_extra_body,
     build_api_kwargs_extras, fetch_models

  ⑥ 自动降级 (Automatic Fallback)
     辅助客户端的多层降级链 + 支付耗尽自动重试。

  ⑦ 跨进程同步 (Cross-process Sync)
     凭证池与 auth.json 的双向同步,
     防止 OAuth 刷新令牌被重复消费。

📖 下讲预告

第 6 讲将深入 Gateway 架构——消息流、平台适配器、Agent 缓存策略,解析 Hermes 如何同时服务 CLI、Telegram、Discord、Slack 等 20+ 平台。

源码: gateway/run.py(4884 行)

Hermes Agent 源码解析 · 第 5 讲 · 2026-06-18

基于 v0.16.0 · NousResearch/hermes-agent