Среда выполнения инструментов

Инструменты Hermes — это саморегистрирующиеся функции. Их группируют в наборы инструментов и выполняют через единый реестр с центральной диспетчеризацией.

Основные файлы:

  • 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",               # Уникальное имя инструмента (используется в 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- и плагин-инструменты:

  1. MCP-инструментыtools.mcp_tool.discover_mcp_tools() читает конфигурацию MCP-серверов и регистрирует инструменты из внешних серверов.
  2. Инструменты плагинов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):

  1. Если передан enabled_toolsets — берутся только инструменты из указанных наборов. Каждое имя набора разрешается через resolve_toolset(), который разворачивает составные наборы в отдельные имена инструментов.

  2. Если передан disabled_toolsets — берутся ВСЕ наборы, из них вычитаются отключённые.

  3. Если ничего не передано — включаются все известные наборы инструментов.

  4. Фильтрация в реестре — разрешённое множество имён уходит в registry.get_definitions(), который применяет фильтр через check_fn и возвращает схемы в формате OpenAI.

  5. Динамическая правка схем — после фильтрации схемы 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", ...)

Обёртка ошибок

Выполнение каждого инструмента защищено обработкой ошибок на двух уровнях:

  1. registry.dispatch() — перехватывает любое исключение из обработчика и возвращает {"error": "Tool execution failed: ExceptionType: message"} в виде JSON.

  2. 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:

  1. Обнаружение паттерновDANGEROUS_PATTERNS — это список кортежей (regex, description), покрывающих деструктивные операции:

    • Рекурсивное удаление (rm -rf)
    • Форматирование файловых систем (mkfs, dd)
    • Деструктивные SQL-операции (DROP TABLE, DELETE FROM без WHERE)
    • Перезапись системных конфигов (> /etc/)
    • Управление сервисами (systemctl stop)
    • Удалённое выполнение кода (curl | sh)
    • Fork-бомбы, принудительное завершение процессов и т. д.
  2. Проверка — перед выполнением любой команды в терминале detect_dangerous_command(command) сверяет её со всеми паттернами.

  3. Запрос подтверждения — при совпадении:

    • Режим CLI — интерактивный запрос пользователю: подтвердить, отклонить или разрешить навсегда
    • Режим шлюза — асинхронный колбэк отправляет запрос на платформу обмена сообщениями
    • Умное подтверждение — по желанию вспомогательная LLM сама одобряет низкорисковые команды, попавшие под паттерн (например, rm -rf node_modules/ безопасна, хотя и подходит под «рекурсивное удаление»)
  4. Состояние сессии — подтверждения отслеживаются в рамках сессии. Стоит один раз одобрить «рекурсивное удаление» — и дальнейшие rm -rf в этой же сессии больше не спрашивают.

  5. Постоянный список разрешений — опция «разрешить навсегда» записывает паттерн в command_allowlist файла config.yaml, и разрешение переживает перезапуск сессий.

Терминальные/рантайм-окружения

Терминальная система поддерживает несколько бэкендов:

  • local
  • docker
  • ssh
  • singularity
  • modal
  • daytona

Также поддерживаются:

  • переопределение рабочей директории на уровне задачи
  • управление фоновыми процессами
  • PTY-режим
  • колбэки подтверждения для опасных команд

Параллелизм

Вызовы инструментов идут последовательно или параллельно — смотря какой набор инструментов и какие требования к взаимодействию.

Связанные разделы

ESC