Среда выполнения инструментов
Инструменты 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", # Уникальное имя инструмента (используется в API-схемах)
toolset="terminal", # Набор инструментов, к которому относится инструмент
schema={...}, # Схема function-calling в формате OpenAI (description, parameters)
handler=handle_terminal, # Функция, вызываемая при активации инструмента
check_fn=check_terminal, # Необязательно: возвращает True/False в зависимости от доступности
requires_env=["SOME_VAR"], # Необязательно: переменные окружения (для отображения в UI)
is_async=False, # Является ли обработчик асинхронной корутиной
description="Run commands", # Человекочитаемое описание
emoji="💻", # Эмодзи для спиннера/индикатора прогресса
)
Каждый вызов создаёт объект ToolEntry, который хранится в синглтоне ToolRegistry._tools — словаре с ключом по имени инструмента. Если имена в разных наборах столкнутся, в лог уйдёт предупреждение, а победит последняя регистрация.
Обнаружение инструментов: discover_builtin_tools()
При импорте model_tools.py вызывается discover_builtin_tools() из tools/registry.py. Функция перебирает все файлы tools/*.py через AST-парсинг, находит модули с вызовами 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() собирает список схем для модели, у каждого инструмента вызывается 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.get_definitions(), который применяет фильтр черезcheck_fnи возвращает схемы в формате OpenAI. -
Динамическая правка схем — после фильтрации схемы
execute_codeиbrowser_navigateправятся динамически: в них остаются только инструменты, прошедшие фильтр (чтобы модель не выдумывала недоступные инструменты).
Устаревшие имена наборов инструментов
Старые имена наборов с суффиксом _tools (например, web_tools, terminal_tools) отображаются на актуальные имена инструментов через _LEGACY_TOOLSET_MAP — ради обратной совместимости.
Диспетчеризация
В рантайме инструменты проходят через центральный реестр. Исключение — несколько инструментов уровня агента: работа с памятью, todo и поиск по сессиям обрабатываются прямо в цикле агента.
Поток диспетчеризации: 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)
↓
[Pre-хук плагина] → invoke_hook("pre_tool_call", ...)
↓
registry.dispatch(name, args, **kwargs)
↓
Поиск ToolEntry по имени
↓
[Асинхронный обработчик?] → мост через _run_async()
[Синхронный обработчик?] → прямой вызов
↓
Возврат строки результата (или JSON с ошибкой)
↓
[Post-хук плагина] → invoke_hook("post_tool_call", ...)
Обёртка ошибок
Выполнение каждого инструмента защищено обработкой ошибок на двух уровнях:
-
registry.dispatch()— перехватывает любое исключение из обработчика и возвращает{"error": "Tool execution failed: ExceptionType: message"}в виде JSON. -
handle_function_call()— оборачивает весь вызов dispatch во второй 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() - Рабочие потоки (параллельные инструменты) — держат по постоянному циклу на поток в 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 — интерактивный запрос пользователю: подтвердить, отклонить или разрешить навсегда
- Режим шлюза — асинхронный колбэк отправляет запрос на платформу обмена сообщениями
- Умное подтверждение — по желанию вспомогательная LLM сама одобряет низкорисковые команды, попавшие под паттерн (например,
rm -rf node_modules/безопасна, хотя и подходит под «рекурсивное удаление»)
-
Состояние сессии — подтверждения отслеживаются в рамках сессии. Стоит один раз одобрить «рекурсивное удаление» — и дальнейшие
rm -rfв этой же сессии больше не спрашивают. -
Постоянный список разрешений — опция «разрешить навсегда» записывает паттерн в
command_allowlistфайлаconfig.yaml, и разрешение переживает перезапуск сессий.
Терминальные/рантайм-окружения
Терминальная система поддерживает несколько бэкендов:
- local
- docker
- ssh
- singularity
- modal
- daytona
Также поддерживаются:
- переопределение рабочей директории на уровне задачи
- управление фоновыми процессами
- PTY-режим
- колбэки подтверждения для опасных команд
Параллелизм
Вызовы инструментов идут последовательно или параллельно — смотря какой набор инструментов и какие требования к взаимодействию.