Средства выполнения инструментов
Инструменты Hermes — это самостоятельные функции, сгруппированные в наборы инструментов и выполняемые через центральную систему реестра/диспетчеризации.
Основные файлы:
инструменты/регистрация.pymodel_tools.pytoolsets.pyинструменты/terminal_tool.pyинструменты/среды/*
Инструменты регистрации моделей
Каждый модульный инструмент предоставляет registry.register(...) во время импорта.
model_tools.py отвечает за импорт/обнаружение модулей инструментов и построение списка схем, внедрение модели.
Как работает registry.register()
Каждый файл-инструмент в tools/ вызывает registry.register() на уровне модуля, чтобы объявить себя. Сигнатурная функция:
registry.register(
name="terminal", # Уникальное имя инструмента (используется в схемах API)
toolset="terminal", # Набор инструментов, к которому принадлежит этот инструмент
schema={...}, # Схема вызова функции OpenAI (описание, параметры)
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
- Установлен ли бинарник — например, проверка доступности
Драматургдля инструментов браузера
Когда 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; используется для отображения в наборах инструментов пользовательского интерфейса и разрешения.
разрешения наборов инструментов
Наборы инструментов — это именованные группы инструментов. Гермес разрешает их через:
- явные включенные/отключенные наборы инструментов
- платформенные пресеты (
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 для обеспечения обратной совместимости.
Диспетчеризация
Во время выполнения инструменты контролируются через центральный реестр, за исключением исключений для агентов некоторых уровней, таких как обработка памяти/задач/поиска по сессиям.
Поток диспетчеризации: модель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()— оборачивает всю диспетчеризацию во вторичный 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) -
Форк-бомбы, завершение процессов и т.д.
-
Обнаружение — перед выполнением любой команды терминала
detect_dangerous_command(command)проверяет все шаблоны. -
Запрос утверждения — если найдено совпадение:
- Режим CLI — интерактивный запрос предлагает пользователю утвердить, отклонить или разрешить навсегда
- Режим шлюза — асинхронный обратный вызов утверждения отправляет запрос в платформу обмена сообщениями
-
Умное утверждение — опционально вспомогательная LLM может автоматически утверждать низкорисковые команды, соответствующие шаблонам (например,
rm -rf node_modules/безопасно, но соответствует "рекурсивному удалению") -
Состояние сессии — утверждения отслеживаются для каждой сессии. После утверждения "рекурсивного удаления" для сессии последующие команды
rm -rfне запрашивают подтверждение повторно. -
Постоянный белый список — опция "разрешить навсегда" записывает шаблон в
config.yamlв разделcommand_allowlist, сохраняясь между сессиями.
Среды выполнения терминала
Система терминала поддерживает несколько бэкендов:
- local
- docker
- ssh
- singularity
- modal
- daytona
- vercel_sandbox
Она также поддерживает:
- переопределения cwd для каждой задачи
- управление фоновыми процессами
- режим PTY
- обратные вызовы утверждения для опасных команд
Параллельность
Вызовы инструментов могут выполняться последовательно или параллельно в зависимости от состава инструментов и требований взаимодействия.