Hermes 工具是自註冊的函式,分組為工具組並透過中央 registry/分派系統執行。
主要檔案:
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", # 此工具所屬的工具組
schema={...}, # OpenAI function-calling schema(描述、參數)
handler=handle_terminal, # 當工具被呼叫時執行的函式
check_fn=check_terminal, # 選用:回傳 True/False 表示可用性
requires_env=["SOME_VAR"], # 選用:需要的環境變數(用於 UI 顯示)
is_async=False, # 處理器是否為非同步協程
description="Run commands", # 人類可讀的描述
emoji="💻", # 用於旋轉器/進度顯示的 Emoji
)
每次呼叫都會建立一個 ToolEntry,儲存在以工具名稱為金鑰的單例 ToolRegistry._tools 字典中。如果跨工具組發生名稱衝突,會記錄警告並以後註冊的為準。
發現機制: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): # 頂層 registry.register() 的 AST 檢查
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 key 存在 — 例如 web 搜尋的
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()方法檢查工具組的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 篩選 — 已解析的工具名稱集合傳遞給
registry.get_definitions(),該函式套用check_fn篩選並回傳 OpenAI 格式的 schema。 -
動態 schema 修補 — 篩選後,
execute_code和browser_navigate的 schema 會被動態調整為只引用實際通過篩選的工具(防止模型產生不存在工具的幻覺)。
舊式工具組名稱
帶有 _tools 後綴的舊工具組名稱(例如 web_tools、terminal_tools)透過 _LEGACY_TOOLSET_MAP 對應到它們的現代工具名稱以實現向後相容。
分派
在運行時,工具透過中央 registry 進行分派,代理程式迴圈對某些代理程式級工具(如記憶體/todo/session-search 處理)有例外處理。
分派流程:模型 tool_call → 處理器執行
當模型回傳 tool_call 時,流程為:
帶有 tool_call 的模型回應
↓
run_agent.py 代理程式迴圈
↓
model_tools.handle_function_call(name, args, task_id, user_task)
↓
[代理程式迴圈工具?] → 直接由代理程式迴圈處理(todo、memory、session_search、delegate_task)
↓
[外掛前鉤子] → invoke_hook("pre_tool_call", ...)
↓
registry.dispatch(name, args, **kwargs)
↓
按名稱查找 ToolEntry
↓
[非同步處理器?] → 透過 _run_async() 橋接
[同步處理器?] → 直接呼叫
↓
回傳結果字串(或 JSON 錯誤)
↓
[外掛後鉤子] → invoke_hook("post_tool_call", ...)
錯誤包裝
所有工具執行都在兩個層級進行錯誤處理:
-
registry.dispatch()— 捕獲處理器的任何異常並回傳{"error": "Tool execution failed: ExceptionType: message"}作為 JSON。 -
handle_function_call()— 將整個分派包裝在 secondary try/except 中,回傳{"error": "Error executing tool_name: message"}。
這確保模型始終收到格式正確的 JSON 字串,永遠不會收到未處理的異常。
代理程式迴圈工具
四個工具在 registry 分派之前被攔截,因為它們需要代理程式級狀態(TodoStore、MemoryStore 等):
todo— 規劃/任務追蹤memory— 持久化記憶體寫入session_search— 跨 session 召回delegate_task— 產生子代理程式 session
這些工具的 schema 仍然在 registry 中註冊(用於 get_tool_definitions),但如果分派不知何故直接到達它們,它們的處理器會回傳一個佔位錯誤。
非同步橋接
當工具處理器是非同步時,_run_async() 將其橋接到同步分派路徑:
- CLI 路徑(無運行中的迴圈) — 使用持久化事件迴圈來保持已快取的非同步客戶端存活
- 閘道器路徑(運行中的迴圈) — 使用
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 炸彈、程序終止等
- 遞迴刪除(
-
偵測 — 在執行任何終端機命令之前,
detect_dangerous_command(command)會對照所有模式進行檢查。 -
核准提示 — 如果找到匹配項:
- CLI 模式 — 互動式提示要求使用者核准、拒絕或永遠允許
- 閘道器模式 — 非同步核准回呼函式將請求發送到訊息平台
- 智慧核准 — 選擇性地,輔助 LLM 可以自動核准符合模式的低風險命令(例如
rm -rf node_modules/是安全的但匹配「遞迴刪除」)
-
Session 狀態 — 核准按 session 追蹤。一旦你為一個 session 核准了「遞迴刪除」,後續的
rm -rf命令不會再次提示。 -
永久允許清單 — 「永遠允許」選項會將模式寫入
config.yaml的command_allowlist,跨 session 持久化。
終端機/運行時環境
終端機系統支援多種後端:
- local(本地)
- docker
- ssh
- singularity
- modal
- daytona
它也支援:
- 每個任務的 cwd 覆寫
- 背景進程管理
- PTY 模式
- 危險命令的核准回呼函式
並行性
工具呼叫可能順序或並行執行,取決於工具組合和互動需求。