跳到主要内容

工具运行时

Hermes 工具是自注册函数,按工具集分组,并通过中央注册/分派系统执行。

主要文件:

  • tools/registry.py
  • model_tools.py
  • toolsets.py
  • tools/terminal_tool.py
  • tools/environments/*

工具注册模型

每个工具模块在导入时调用 registry.register(...)

model_tools.py 负责导入/发现工具模块,并构建模型使用的模式列表。

registry.register() 的工作原理

tools/ 目录中的每个工具文件在模块级别调用 registry.register() 以声明自身。函数签名如下:

registry.register(
name="terminal", # Unique tool name (used in API schemas)
toolset="terminal", # Toolset this tool belongs to
schema={...}, # OpenAI function-calling schema (description, parameters)
handler=handle_terminal, # The function that executes when the tool is called
check_fn=check_terminal, # Optional: returns True/False for availability
requires_env=["SOME_VAR"], # Optional: env vars needed (for UI display)
is_async=False, # Whether the handler is an async coroutine
description="Run commands", # Human-readable description
emoji="💻", # Emoji for spinner/progress display
)

每次调用都会创建一个 ToolEntry,存储在单例 ToolRegistry._tools 字典中,以工具名称为键。如果不同工具集中出现名称冲突,将记录警告信息,后注册的版本将覆盖先注册的版本。

发现:_discover_tools()

model_tools.py 被导入时,它会调用 _discover_tools(),按顺序导入所有工具模块:

_modules = [
"tools.web_tools",
"tools.terminal_tool",
"tools.file_tools",
"tools.vision_tools",
"tools.mixture_of_agents_tool",
"tools.image_generation_tool",
"tools.skills_tool",
"tools.skill_manager_tool",
"tools.browser_tool",
"tools.cronjob_tools",
"tools.rl_training_tool",
"tools.tts_tool",
"tools.todo_tool",
"tools.memory_tool",
"tools.session_search_tool",
"tools.clarify_tool",
"tools.code_execution_tool",
"tools.delegate_tool",
"tools.process_registry",
"tools.send_message_tool",
# "tools.honcho_tools", # Removed — Honcho is now a memory provider plugin
"tools.homeassistant_tool",
]

每次导入都会触发模块中 registry.register() 的调用。对于可选工具(例如缺少 fal_client 时的图像生成),导入错误会被捕获并记录——这不会阻止其他工具的加载。

核心工具发现完成后,还会发现 MCP 工具和插件工具:

  1. MCP 工具tools.mcp_tool.discover_mcp_tools() 读取 MCP 服务器配置,并从外部服务器注册工具。
  2. 插件工具hermes_cli.plugins.discover_plugins() 加载用户/项目/Pip 插件,这些插件可能注册额外的工具。

工具可用性检查(check_fn

每个工具可选择性地提供一个 check_fn —— 一个返回 True 表示工具可用、返回 False 表示不可用的可调用对象。典型的检查包括:

  • API 密钥存在 —— 例如 lambda: bool(os.environ.get("SERP_API_KEY")) 用于网络搜索
  • 服务正在运行 —— 例如检查 Honcho 服务器是否已配置
  • 二进制已安装 —— 例如验证 playwright 是否可用于浏览器工具

registry.get_definitions() 为模型构建模式列表时,会运行每个工具的 check_fn()

# Simplified from registry.py
if entry.check_fn:
try:
available = bool(entry.check_fn())
except Exception:
available = False # Exceptions = unavailable
if not available:
continue # Skip this tool entirely

关键行为:

  • 检查结果是按调用缓存的 —— 如果多个工具共享相同的 check_fn,它只会运行一次。
  • check_fn() 中的异常被视为“不可用”(安全降级)。
  • is_toolset_available() 方法用于检查工具集的 check_fn 是否通过,该方法用于 UI 显示和工具集解析。

工具集解析

工具集是工具的命名捆绑包。Hermes 通过以下方式解析它们:

  • 显式启用/禁用的工具集列表
  • 平台预设(如 hermes-clihermes-telegram 等)
  • 动态 MCP 工具集
  • 精心策划的专用工具集,如 hermes-acp

get_tool_definitions() 如何过滤工具

主要入口点是 model_tools.get_tool_definitions(enabled_toolsets, disabled_toolsets, quiet_mode)

  1. 如果提供了 enabled_toolsets —— 仅包含这些工具集中的工具。每个工具集名称通过 resolve_toolset() 解析,将复合工具集展开为单个工具名称。

  2. 如果提供了 disabled_toolsets —— 从所有工具集开始,然后减去被禁用的工具集。

  3. 如果两者均未提供 —— 包含所有已知工具集。

  4. 注册表过滤 —— 解析后的工具名称集合传递给 registry.get_definitions(),该函数应用 check_fn 过滤,并返回 OpenAI 格式的模式。

  5. 动态模式修补 —— 过滤后,execute_codebrowser_navigate 模式会动态调整,仅引用实际通过过滤的工具(防止模型幻觉出不可用的工具)。

旧版工具集名称

带有 _tools 后缀的旧版工具集名称(如 web_toolsterminal_tools)通过 _LEGACY_TOOLSET_MAP 映射到现代工具名称,以保证向后兼容。

分派

运行时,工具通过中央注册表进行分派,但某些代理层工具(如记忆/待办事项/会话搜索处理)除外。

分派流程:模型 tool_call → 处理程序执行

当模型返回 tool_call 时,流程如下:

Model response with tool_call

run_agent.py agent loop

model_tools.handle_function_call(name, args, task_id, user_task)

[Agent-loop tools?] → handled directly by agent loop (todo, memory, session_search, delegate_task)

[Plugin pre-hook] → invoke_hook("pre_tool_call", ...)

registry.dispatch(name, args, **kwargs)

Look up ToolEntry by name

[Async handler?] → bridge via _run_async()
[Sync handler?] → call directly

Return result string (or JSON error)

[Plugin post-hook] → invoke_hook("post_tool_call", ...)

错误包装

所有工具执行在两个层级上都进行了错误处理:

  1. registry.dispatch() —— 捕获处理程序中的任何异常,并返回 {"error": "Tool execution failed: ExceptionType: message"} 作为 JSON。

  2. handle_function_call() —— 将整个分派包装在二级 try/except 中,返回 {"error": "Error executing tool_name: message"}

这确保模型始终接收到格式良好的 JSON 字符串,而不会收到未处理的异常。

代理循环工具

有四个工具在注册表分派前被拦截,因为它们需要代理层状态(如 TodoStore、MemoryStore 等):

  • todo —— 规划/任务跟踪
  • memory —— 持久化记忆写入
  • session_search —— 跨会话回忆
  • delegate_task —— 启动子代理会话

这些工具的模式仍注册在注册表中(用于 get_tool_definitions),但如果分派意外到达它们,其处理程序将返回一个存根错误。

异步桥接

当工具处理器为异步时,_run_async() 会将其桥接到同步分发路径:

  • CLI 路径(无运行中的事件循环) —— 使用持久化事件循环,以保持缓存的异步客户端处于活跃状态
  • 网关路径(正在运行的事件循环) —— 使用 asyncio.run() 启动一个可丢弃的线程
  • 工作线程(并行工具) —— 使用存储在线程局部存储中的每线程持久化事件循环

DANGEROUS_PATTERNS 审批流程

终端工具集成了在 tools/approval.py 中定义的危险命令审批系统:

  1. 模式检测 —— DANGEROUS_PATTERNS 是一组 (正则表达式, 描述) 元组,涵盖破坏性操作:

    • 递归删除(rm -rf
    • 文件系统格式化(mkfs, dd
    • SQL 破坏性操作(DROP TABLEDELETE FROMWHERE 子句)
    • 系统配置覆盖(> /etc/
    • 服务操作(systemctl stop
    • 远程代码执行(curl | sh
    • 分叉炸弹、进程终止等
  2. 检测 —— 在执行任何终端命令之前,detect_dangerous_command(command) 会与所有模式进行比对。

  3. 审批提示 —— 若发现匹配项:

    • CLI 模式 —— 交互式提示要求用户批准、拒绝或永久允许
    • 网关模式 —— 异步审批回调将请求发送至消息平台
    • 智能审批 —— 可选地,辅助 LLM 可自动批准低风险且匹配模式的命令(例如 rm -rf node_modules/ 虽匹配“递归删除”模式,但属于安全操作)
  4. 会话状态 —— 审批记录按会话进行。一旦在当前会话中批准了“递归删除”,后续的 rm -rf 命令将不再重复提示。

  5. 永久白名单 —— “永久允许”选项会将该模式写入 config.yamlcommand_allowlist,实现跨会话持久化。

终端/运行时环境

终端系统支持多种后端:

  • local
  • docker
  • ssh
  • singularity
  • modal
  • daytona

同时支持:

  • 每任务的 cwd 覆盖
  • 后台进程管理
  • PTY 模式
  • 危险命令的审批回调

并发性

工具调用可根据工具组合和交互需求,选择顺序执行或并发执行。