* remove Vercel AI Gateway provider and Vercel Sandbox terminal backend Both Vercel-hosted integrations are removed end-to-end. Users on the AI Gateway should switch to OpenRouter or one of the other aggregators (Nous Portal, Kilo Code). Users on the Vercel Sandbox backend should switch to Docker, Modal, Daytona, or SSH. What's removed: - `plugins/model-providers/ai-gateway/` provider plugin - `hermes_cli/vercel_auth.py` Vercel-Sandbox auth helper - `tools/environments/vercel_sandbox.py` terminal backend - `ai-gateway` provider wiring across auth, doctor, setup, models, config, status, providers, main, web_server, model_normalize, dump - `vercel_sandbox` backend wiring across terminal_tool, file_tools, code_execution_tool, file_operations, approval, skills_tool, environments/local, credential_files, lazy_deps, prompt_builder, cli, gateway/run - `AI_GATEWAY_BASE_URL` constant, `_AI_GATEWAY_HEADERS` auxiliary-client header set, run_agent base-URL header/reasoning special-cases - `[vercel]` pyproject extra and `vercel`/`vercel-workers` from uv.lock - env vars: `AI_GATEWAY_API_KEY`, `AI_GATEWAY_BASE_URL`, `VERCEL_TOKEN`, `VERCEL_PROJECT_ID`, `VERCEL_TEAM_ID`, `VERCEL_OIDC_TOKEN`, `TERMINAL_VERCEL_RUNTIME` - Tests: deletes test_ai_gateway_models.py and test_vercel_sandbox_environment.py; scrubs references across 23 surviving test files (no entire tests deleted unless they were dedicated to AI Gateway / Sandbox) - Docs: provider tables, env-var reference, setup guides, security notes, tool config, terminal-backend tables — English plus zh-Hans i18n parity - `hermes-agent` skill: provider table entry and remote-backend list What stays (intentional): - `popular-web-designs/templates/vercel.md` — CSS design reference, unrelated to Vercel-the-AI-product - `x-vercel-id` in `stream_diag.py` headers — generic Vercel CDN response header, useful diag signal on any Vercel-hosted endpoint - `vercel-labs/agent-browser` URL in browser config — lightpanda browser project, different OSS effort - `userStories.json` historical contributor entry mentioning Vercel Sandbox — archive, not active docs Validation: - 1153 tests in the 22 targeted files pass (`scripts/run_tests.sh`) - Full repo `py_compile` clean - Live import of every touched module + invariant check (no `ai-gateway` in `PROVIDER_REGISTRY`, no `_AI_GATEWAY_HEADERS`, no `vercel_sandbox` in `_REMOTE_TERMINAL_BACKENDS`) * test: convert profile-count check from change-detector to invariant The hardcoded "== 34" assertion broke when ai-gateway was removed. Per AGENTS.md change-detector-test guidance, assert the relationship (registry count >= number of plugin dirs) instead of a literal count. Counts shift when providers are added/removed; that's expected.
9.7 KiB
| sidebar_position | title | description |
|---|---|---|
| 9 | 工具运行时 | 工具注册表、toolset、调度及终端环境的运行时行为 |
工具运行时
Hermes 工具是自注册函数,按 toolset(工具集)分组,并通过中央注册表/调度系统执行。
主要文件:
tools/registry.pymodel_tools.pytoolsets.pytools/terminal_tool.pytools/environments/*
工具注册模型
每个工具模块在导入时调用 registry.register(...)。
model_tools.py 负责导入/发现工具模块,并构建供模型使用的 schema 列表。
registry.register() 的工作原理
tools/ 中的每个工具文件在模块级别调用 registry.register() 来声明自身。函数签名如下:
registry.register(
name="terminal", # 唯一工具名称(用于 API schema)
toolset="terminal", # 该工具所属的 toolset
schema={...}, # OpenAI function-calling schema(描述、参数)
handler=handle_terminal, # 工具被调用时执行的函数
check_fn=check_terminal, # 可选:返回 True/False 表示是否可用
requires_env=["SOME_VAR"], # 可选:所需的环境变量(用于 UI 显示)
is_async=False, # handler 是否为异步协程
description="Run commands", # 人类可读的描述
emoji="💻", # 用于 spinner/进度显示的 emoji
)
每次调用都会创建一个 ToolEntry,以工具名称为键存储在单例 ToolRegistry._tools 字典中。若不同 toolset 之间出现名称冲突,会记录警告,后注册的条目覆盖前者。
发现机制:discover_builtin_tools()
当 model_tools.py 被导入时,会调用 tools/registry.py 中的 discover_builtin_tools()。该函数使用 AST 解析扫描所有 tools/*.py 文件,找出包含顶层 registry.register() 调用的模块,然后导入它们:
# tools/registry.py(简化版)
def discover_builtin_tools(tools_dir=None):
tools_path = Path(tools_dir) if tools_dir else Path(__file__).parent
for path in sorted(tools_path.glob("*.py")):
if path.name in {"__init__.py", "registry.py", "mcp_tool.py"}:
continue
if _module_registers_tools(path): # AST 检查顶层 registry.register()
importlib.import_module(f"tools.{path.stem}")
这种自动发现机制意味着新工具文件会被自动识别——无需手动维护列表。AST 检查只匹配顶层的 registry.register() 调用(不匹配函数内部的调用),因此 tools/ 中的辅助模块不会被导入。
每次导入都会触发模块的 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() 为模型构建 schema 列表时,会运行每个工具的 check_fn():
# 简化自 registry.py
if entry.check_fn:
try:
available = bool(entry.check_fn())
except Exception:
available = False # 异常 = 不可用
if not available:
continue # 完全跳过该工具
关键行为:
- 检查结果按调用缓存——若多个工具共享同一个
check_fn,只运行一次。 check_fn()中的异常被视为"不可用"(故障安全)。is_toolset_available()方法检查某个 toolset 的check_fn是否通过,用于 UI 显示和 toolset 解析。
Toolset 解析
Toolset 是工具的命名集合。Hermes 通过以下方式解析它们:
- 显式启用/禁用的 toolset 列表
- 平台预设(
hermes-cli、hermes-telegram等) - 动态 MCP toolset
- 精选的特殊用途集合,如
hermes-acp
get_tool_definitions() 如何过滤工具
主入口点为 model_tools.get_tool_definitions(enabled_toolsets, disabled_toolsets, quiet_mode):
-
若提供了
enabled_toolsets— 仅包含这些 toolset 中的工具。每个 toolset 名称通过resolve_toolset()解析,将复合 toolset 展开为单个工具名称。 -
若提供了
disabled_toolsets— 从所有 toolset 开始,减去已禁用的。 -
若两者均未提供 — 包含所有已知 toolset。
-
注册表过滤 — 解析后的工具名称集合传递给
registry.get_definitions(),后者应用check_fn过滤并返回 OpenAI 格式的 schema。 -
动态 schema 修补 — 过滤后,
execute_code和browser_navigate的 schema 会被动态调整,仅引用实际通过过滤的工具(防止模型幻觉出不可用的工具)。
旧版 toolset 名称
带有 _tools 后缀的旧版 toolset 名称(例如 web_tools、terminal_tools)通过 _LEGACY_TOOLSET_MAP 映射到其现代工具名称,以保持向后兼容性。
调度
运行时,工具通过中央注册表调度,但部分 agent 级别的工具(如 memory/todo/session-search 处理)由 agent 循环直接处理。
调度流程:模型 tool_call → handler 执行
当模型返回 tool_call 时,流程如下:
模型响应包含 tool_call
↓
run_agent.py agent 循环
↓
model_tools.handle_function_call(name, args, task_id, user_task)
↓
[Agent 循环工具?] → 由 agent 循环直接处理(todo、memory、session_search、delegate_task)
↓
[插件 pre-hook] → invoke_hook("pre_tool_call", ...)
↓
registry.dispatch(name, args, **kwargs)
↓
按名称查找 ToolEntry
↓
[异步 handler?] → 通过 _run_async() 桥接
[同步 handler?] → 直接调用
↓
返回结果字符串(或 JSON 错误)
↓
[插件 post-hook] → invoke_hook("post_tool_call", ...)
错误包装
所有工具执行在两个层级进行错误处理:
-
registry.dispatch()— 捕获 handler 抛出的任何异常,并以 JSON 形式返回{"error": "Tool execution failed: ExceptionType: message"}。 -
handle_function_call()— 将整个调度包裹在次级 try/except 中,返回{"error": "Error executing tool_name: message"}。
这确保模型始终收到格式正确的 JSON 字符串,而不会遇到未处理的异常。
Agent 循环工具
以下四个工具在注册表调度之前被拦截,因为它们需要 agent 级别的状态(TodoStore、MemoryStore 等):
todo— 规划/任务跟踪memory— 持久化 memory 写入session_search— 跨会话召回delegate_task— 生成子 agent 会话
这些工具的 schema 仍在注册表中注册(供 get_tool_definitions 使用),但若调度以某种方式直接到达它们,其 handler 会返回一个存根错误。
异步桥接
当工具 handler 为异步时,_run_async() 将其桥接到同步调度路径:
- CLI 路径(无运行中的事件循环) — 使用持久化事件循环以保持缓存的异步客户端存活
- Gateway 路径(有运行中的事件循环) — 使用
asyncio.run()启动一个一次性线程 - 工作线程(并行工具) — 使用存储在线程本地存储中的每线程持久化循环
DANGEROUS_PATTERNS 审批流程
终端工具集成了定义在 tools/approval.py 中的危险命令审批系统:
-
模式检测 —
DANGEROUS_PATTERNS是一个(regex, description)元组列表,涵盖破坏性操作:- 递归删除(
rm -rf) - 文件系统格式化(
mkfs、dd) - SQL 破坏性操作(
DROP TABLE、不带WHERE的DELETE FROM) - 系统配置覆写(
> /etc/) - 服务操控(
systemctl stop) - 远程代码执行(
curl | sh) - Fork bomb、进程终止等
- 递归删除(
-
检测 — 在执行任何终端命令之前,
detect_dangerous_command(command)会对所有模式进行检查。 -
审批提示 — 若发现匹配:
- CLI 模式 — 交互式提示要求用户批准、拒绝或永久允许
- Gateway 模式 — 异步审批回调将请求发送至消息平台
- 智能审批 — 可选地,辅助 LLM 可自动批准匹配模式但风险较低的命令(例如,
rm -rf node_modules/是安全的,但匹配"递归删除"模式)
-
会话状态 — 审批按会话跟踪。一旦在某个会话中批准了"递归删除",后续的
rm -rf命令不会再次提示。 -
永久允许列表 — "永久允许"选项会将该模式写入
config.yaml的command_allowlist,跨会话持久化。
终端/运行时环境
终端系统支持多种后端:
- local
- docker
- ssh
- singularity
- modal
- daytona
还支持:
- 按任务的 cwd 覆盖
- 后台进程管理
- PTY 模式
- 危险命令的审批回调
并发
工具调用可以顺序执行,也可以并发执行,具体取决于工具组合和交互需求。