Tools Runtime
Инструменты Hermes — это саморегистрирующиеся функции, сгруппированные в наборы инструментов (toolsets) и выполняемые через центральную систему регистрации/диспетчеризации.
Основные файлы:
-
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_builtin_tools()
При импорте model_tools.py вызывает discover_builtin_tools() из tools/registry.py. Эта функция сканирует все файлы tools/*.py с помощью AST-парсинга, чтобы найти модули, содержащие вызовы registry.register() на верхнем уровне, после чего импортирует их:
# tools/registry.py (simplified)
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 check for top-level 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() собирает список схем для модели, он запускает 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 для обратной совместимости.
Диспетчеризация
Во время выполнения инструменты диспетчеризуются через центральный реестр, за исключением некоторых инструментов уровня агента (таких как обработка memory/todo/session-search), которые обрабатываются в цикле агента.
Поток диспетчеризации: model 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-путь (нет запущенного цикла событий) — использует постоянный цикл событий для поддержания кэшированных асинхронных клиентов
-
Gateway-путь (запущенный цикл событий) — создает одноразовый поток с
asyncio.run() -
Рабочие потоки (параллельные инструменты) — использует постоянные циклы для каждого потока, хранящиеся в thread-local storage
Поток подтверждения DANGEROUS_PATTERNS
Терминальный инструмент включает систему подтверждения опасных команд, определенную в tools/approval.py:
- Обнаружение шаблонов —
DANGEROUS_PATTERNS— это список кортежей(regex, description), охватывающий деструктивные операции: - Рекурсивное удаление (
rm -rf) - Форматирование файловой системы (
mkfs,dd) - Деструктивные SQL-операции (
DROP TABLE,DELETE FROMбезWHERE) - Перезапись системных конфигов (
> /etc/) - Манипуляции с сервисами (
systemctl stop) - Удаленное выполнение кода (
curl | sh) -
Fork-бомбы, завершение процессов и т.д.
-
Обнаружение — перед выполнением любой терминальной команды
detect_dangerous_command(command)проверяет ее по всем шаблонам. -
Запрос подтверждения — при совпадении:
- CLI-режим — интерактивный запрос предлагает пользователю подтвердить, отклонить или разрешить навсегда
- Gateway-режим — асинхронный callback подтверждения отправляет запрос на платформу обмена сообщениями
-
Умное подтверждение — опционально вспомогательная LLM может автоматически одобрять низкорисковые команды, совпадающие с шаблонами (например,
rm -rf node_modules/безопасно, но совпадает с «рекурсивное удаление») -
Состояние сессии — подтверждения отслеживаются для каждой сессии. После подтверждения «рекурсивного удаления» в сессии последующие команды
rm -rfповторно не запрашивают подтверждение. -
Постоянный белый список — опция «разрешить навсегда» записывает шаблон в
command_allowlistвconfig.yaml, сохраняя его между сессиями.
Терминальные/среды выполнения
Терминальная система поддерживает несколько бэкендов:
-
local
-
docker
-
ssh
-
singularity
-
modal
-
daytona
-
vercel_sandbox
Она также поддерживает:
-
переопределение рабочей директории (cwd) для каждой задачи
-
управление фоновыми процессами
-
PTY-режим
-
callback-функции подтверждения для опасных команд
Параллельность
Вызовы инструментов могут выполняться последовательно или параллельно в зависимости от состава инструментов и требований взаимодействия.