工具运行时
Hermes 的工具是自注册函数,按工具集分组,并通过中央注册/调度系统执行。
主要文件:
tools/registry.pymodel_tools.pytoolsets.pytools/terminal_tool.pytools/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 工具和插件工具:
- MCP 工具 —
tools.mcp_tool.discover_mcp_tools()读取 MCP 服务器配置,并从外部服务器注册工具。 - 插件工具 —
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-cli、hermes-telegram等) - 动态 MCP 工具集
- 经过精心设计的专用集合,如
hermes-acp
get_tool_definitions() 如何过滤工具
主入口点是 model_tools.get_tool_definitions(enabled_toolsets, disabled_toolsets, quiet_mode):
-
若提供了
enabled_toolsets—— 仅包含来自这些工具集的工具。每个工具集名称通过resolve_toolset()解析,将复合工具集展开为单独的工具名称。 -
若提供了
disabled_toolsets—— 从所有工具集开始,然后减去被禁用的。 -
若两者均未提供 —— 包含所有已知工具集。
-
注册表过滤 —— 解析后的工具名称集合传递给
registry.get_definitions(),该方法应用check_fn过滤,并返回 OpenAI 格式的模式。 -
动态模式修补 —— 过滤后,
execute_code和browser_navigate的模式会动态调整,仅引用实际通过过滤的工具(防止模型虚构不可用的工具)。
旧版工具集名称
带有 _tools 后缀的旧工具集名称(如 web_tools、terminal_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", ...)
错误包装
所有工具执行都在两个层级上进行错误处理:
-
registry.dispatch()—— 捕获处理器抛出的任何异常,并返回{"error": "Tool execution failed: ExceptionType: message"}作为 JSON。 -
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() - 工作线程(并行工具) —— 使用存储在线程局部存储中的每线程持久循环
危险模式审批流程
终端工具集成了一个危险命令审批系统,定义在 tools/approval.py 中:
-
模式检测 ——
DANGEROUS_PATTERNS是一组(regex, description)元组,涵盖破坏性操作:- 递归删除(
rm -rf) - 文件系统格式化(
mkfs、dd) - SQL 破坏性操作(
DROP TABLE、DELETE FROM且无WHERE) - 系统配置覆盖(
> /etc/) - 服务操作(
systemctl stop) - 远程代码执行(
curl | sh) - 分叉炸弹、进程终止等
- 递归删除(
-
检测 —— 在执行任何终端命令前,
detect_dangerous_command(command)会与所有模式进行比对。 -
审批提示 —— 若匹配成功:
- CLI 模式 —— 交互式提示要求用户批准、拒绝或永久允许
- 网关模式 —— 异步审批回调将请求发送至消息平台
- 智能审批 —— 可选地,辅助 LLM 可自动批准低风险且匹配模式的命令(例如
rm -rf node_modules/安全但匹配“递归删除”)
-
会话状态 —— 审批按会话追踪。一旦你在会话中批准“递归删除”,后续的
rm -rf命令不再重复提示。 -
永久白名单 —— “永久允许”选项会将该模式写入
config.yaml的command_allowlist,跨会话持久化。
终端/运行环境
终端系统支持多种后端:
- 本地
- docker
- ssh
- singularity
- modal
- daytona
还支持:
- 每任务的 cwd 覆盖
- 后台进程管理
- PTY 模式
- 危险命令的审批回调
并发性
工具调用可根据工具组合和交互需求,按顺序或并发执行。