Выполнение кода (программный вызов инструментов)
Инструмент execute_code даёт агенту возможность писать Python-скрипты, которые программно вызывают инструменты Hermes. Несколько шагов сворачиваются в один ход LLM. Скрипт работает в дочернем процессе на хосте агента и общается с Hermes через RPC поверх Unix-сокета.
Как это работает
- Агент пишет Python-скрипт, используя
from hermes_tools import ... - Hermes генерирует модуль-заглушку
hermes_tools.pyс RPC-функциями - Hermes открывает Unix-сокет и запускает поток-слушатель RPC
- Скрипт выполняется в дочернем процессе — вызовы инструментов уходят по сокету обратно в Hermes
- В LLM возвращается только то, что скрипт вывел через
print(); промежуточные результаты инструментов в окно контекста не попадают
# Агент может писать скрипты вроде:
from hermes_tools import web_search, web_extract
results = web_search("Python 3.13 features", limit=5)
for r in results["data"]["web"]:
content = web_extract([r["url"]])
# ... фильтруем и обрабатываем ...
print(summary)
Какие инструменты доступны внутри скриптов: web_search, web_extract, read_file, write_file, search_files, patch, terminal (только в режиме переднего плана).
Когда агент к этому прибегает
Агент берётся за execute_code, если в задаче есть:
- 3+ вызова инструментов с логикой обработки между ними
- массовая фильтрация данных или ветвление по условию
- циклы по результатам
Главный выигрыш — промежуточные результаты инструментов не попадают в окно контекста: назад возвращается лишь итоговый вывод print(). Расход токенов падает в разы.
Практические примеры
Конвейер обработки данных
from hermes_tools import search_files, read_file
import json
# Находим все конфиги и вытаскиваем настройки базы данных
matches = search_files("database", path=".", file_glob="*.yaml", limit=20)
configs = []
for match in matches.get("matches", []):
content = read_file(match["path"])
configs.append({"file": match["path"], "preview": content["content"][:200]})
print(json.dumps(configs, indent=2))
Многошаговое веб-исследование
from hermes_tools import web_search, web_extract
import json
# Поиск, извлечение и сводка за один ход
results = web_search("Rust async runtime comparison 2025", limit=5)
summaries = []
for r in results["data"]["web"]:
page = web_extract([r["url"]])
for p in page.get("results", []):
if p.get("content"):
summaries.append({
"title": r["title"],
"url": r["url"],
"excerpt": p["content"][:500]
})
print(json.dumps(summaries, indent=2))
Массовый рефакторинг файлов
from hermes_tools import search_files, read_file, patch
# Находим все Python-файлы с устаревшим API и правим их
matches = search_files("old_api_call", path="src/", file_glob="*.py")
fixed = 0
for match in matches.get("matches", []):
result = patch(
path=match["path"],
old_string="old_api_call(",
new_string="new_api_call(",
replace_all=True
)
if "error" not in str(result):
fixed += 1
print(f"Fixed {fixed} files out of {len(matches.get('matches', []))} matches")
Сборка и прогон тестов
from hermes_tools import terminal, read_file
import json
# Запускаем тесты, разбираем результат и формируем отчёт
result = terminal("cd /project && python -m pytest --tb=short -q 2>&1", timeout=120)
output = result.get("output", "")
# Парсим вывод тестов
passed = output.count(" passed")
failed = output.count(" failed")
errors = output.count(" error")
report = {
"passed": passed,
"failed": failed,
"errors": errors,
"exit_code": result.get("exit_code", -1),
"summary": output[-500:] if len(output) > 500 else output
}
print(json.dumps(report, indent=2))
Режим выполнения
У execute_code два режима выполнения, ими управляет параметр code_execution.mode в ~/.hermes/config.yaml:
| Режим | Рабочая директория | Интерпретатор Python |
|---|---|---|
project (по умолчанию) | Рабочая директория сессии (та же, что и у terminal()) | Python из активного VIRTUAL_ENV / CONDA_PREFIX, при их отсутствии — собственный python Hermes |
strict | Временная служебная директория, изолированная от проекта пользователя | sys.executable (собственный python Hermes) |
Когда оставить project: вы хотите, чтобы import pandas, from my_project import foo или относительные пути вроде open(".env") работали так же, как в terminal(). В подавляющем большинстве случаев нужно именно это.
Когда переключиться на strict: вам важна максимальная воспроизводимость — один и тот же интерпретатор в каждой сессии, независимо от того, какой venv активировал пользователь, плюс изоляция скриптов от дерева проекта (нельзя случайно прочитать файл проекта по относительному пути).
# ~/.hermes/config.yaml
code_execution:
mode: project # or "strict"
Как ведёт себя откат в режиме project: если VIRTUAL_ENV / CONDA_PREFIX не задан, сломан или указывает на Python старше 3.8, резолвер аккуратно переключается на sys.executable. Без рабочего интерпретатора агент не остаётся никогда.
Критичные для безопасности инварианты в обоих режимах одинаковы:
- очистка окружения (API-ключи, токены, учётные данные вырезаются)
- белый список инструментов (скрипты не могут рекурсивно вызывать
execute_code,delegate_taskили MCP-инструменты) - ограничения ресурсов (таймаут, потолок stdout, потолок числа вызовов инструментов)
Смена режима меняет, где запускаются скрипты и какой интерпретатор их исполняет, но не то, какие учётные данные им видны и какие инструменты они могут вызывать.
Ограничения ресурсов
| Ресурс | Лимит | Примечания |
|---|---|---|
| Таймаут | 5 минут (300 с) | Скрипт убивается через SIGTERM, затем SIGKILL спустя 5 с отсрочки |
| Stdout | 50 КБ | Вывод обрезается с пометкой [output truncated at 50KB] |
| Stderr | 10 КБ | Добавляется в вывод при ненулевом коде выхода — для отладки |
| Вызовы инструментов | 50 на один запуск | При достижении лимита возвращается ошибка |
Все лимиты настраиваются через config.yaml:
# In ~/.hermes/config.yaml
code_execution:
mode: project # project (default) | strict
timeout: 300 # Max seconds per script (default: 300)
max_tool_calls: 50 # Max tool calls per execution (default: 50)
Как работают вызовы инструментов внутри скриптов
Когда ваш скрипт вызывает функцию вроде web_search("query"), происходит вот что:
- Вызов сериализуется в JSON и отправляется по Unix-сокету родительскому процессу
- Родитель диспетчеризует его через штатный обработчик
handle_function_call - Результат отправляется обратно по сокету
- Функция возвращает разобранный результат
То есть вызовы инструментов внутри скриптов ведут себя ровно так же, как обычные: те же лимиты частоты, та же обработка ошибок, те же возможности. Единственное ограничение — terminal() доступен только на переднем плане (без параметров background и pty).
Обработка ошибок
Когда скрипт падает, агент получает структурированные сведения об ошибке:
- Ненулевой код выхода: stderr добавляется в вывод, поэтому агент видит полный traceback
- Таймаут: скрипт убивается, и агент видит
"Script timed out after 300s and was killed." - Прерывание: если во время выполнения пользователь присылает новое сообщение, скрипт завершается, а агент получает
[execution interrupted — user sent a new message] - Лимит вызовов инструментов: при достижении 50 вызовов все последующие возвращают сообщение об ошибке
В ответе всегда есть status (success/error/timeout/interrupted), output, tool_calls_made и duration_seconds.
Безопасность
Дочерний процесс запускается с минимальным окружением. API-ключи, токены и учётные данные по умолчанию вырезаются. Скрипт работает с инструментами исключительно через RPC-канал — прочитать секреты из переменных окружения он не может, если это не разрешено явно.
Переменные окружения, в имени которых есть KEY, TOKEN, SECRET, PASSWORD, CREDENTIAL, PASSWD или AUTH, исключаются. Пробрасываются только безопасные системные переменные (PATH, HOME, LANG, SHELL, PYTHONPATH, VIRTUAL_ENV и т. п.).
Проброс переменных окружения навыка
Если навык объявляет в своём frontmatter required_environment_variables, после загрузки навыка эти переменные автоматически пробрасываются в дочерние процессы и execute_code, и terminal. Так навыки получают свои объявленные API-ключи, не ослабляя при этом защиту для произвольного кода.
Для случаев вне навыков переменные можно явно добавить в список разрешений в config.yaml:
terminal:
env_passthrough:
- MY_CUSTOM_KEY
- ANOTHER_TOKEN
Подробности — в руководстве по безопасности.
Переменные HERMES_* в дочернем процессе
Дочерний процесс получает лишь небольшой фиксированный набор операционных переменных HERMES_*, причём по точному имени:
HERMES_HOMEHERMES_PROFILEHERMES_CONFIGHERMES_ENV
(плюс HERMES_RPC_DIR / HERMES_RPC_SOCKET / TZ / HOME, которые Hermes
подставляет явно, чтобы работал RPC-канал).
Прежние версии пробрасывали в дочерний процесс любую переменную, имя которой начиналось с HERMES_. Этот широкий префикс убрали ради усиления безопасности: он мог утянуть в произвольный песочный код конфигурацию с именем вида HERMES_*, не совпадающую ни с одной секретной подстрокой (например HERMES_BASE_URL, HERMES_KANBAN_DB или эндпоинт HERMES_*_WEBHOOK).
Если скрипт execute_code — или модуль репозитория/плагина, который он импортирует на этапе импорта — полагался на переменную HERMES_* вне четырёх операционных имён выше, теперь она окажется в дочернем процессе неустановленной. Это убрано намеренно, а не баг.
Обходной путь — явно вернуть переменную в проброс. Оба способа пробрасывают переменную в дочерние процессы и execute_code, и terminal, и ни один не ослабляет гарантию вырезания секретов (учётные данные провайдеров, которыми управляет Hermes, так вернуть нельзя):
-
Для конкретной машины, в
config.yaml— добавьте точное имя переменной в список разрешений проброса:terminal: env_passthrough: - HERMES_KANBAN_DB - HERMES_BASE_URL -
Для конкретного навыка, в его frontmatter — объявите её, чтобы она регистрировалась автоматически при каждой загрузке навыка:
required_environment_variables: - HERMES_KANBAN_DB
Как диагностировать. Когда дочерний процесс отбрасывает одну или несколько переменных HERMES_* не из списка разрешений, Hermes пишет однострочный debug-лог с их именами и подсказкой про лазейку env_passthrough. Запустите с отладочным логированием (hermes logs --level DEBUG или загляните в ~/.hermes/logs/agent.log) и ищите строку execute_code: dropped N non-allowlisted HERMES_* var(s), если скрипт ведёт себя так, будто переменной HERMES_* не хватает.
Сам скрипт и автосгенерированную RPC-заглушку hermes_tools.py Hermes всегда пишет во временную служебную директорию, которая удаляется после выполнения. В режиме strict скрипт там же и выполняется; в режиме project он работает в рабочей директории сессии (служебная директория остаётся в PYTHONPATH, поэтому импорты по-прежнему разрешаются). Дочерний процесс запускается в собственной группе процессов — это позволяет чисто прибить его при таймауте или прерывании.
execute_code против terminal
| Сценарий | execute_code | terminal |
|---|---|---|
| Многошаговые сценарии с вызовами инструментов между шагами | ✅ | ❌ |
| Простая команда shell | ❌ | ✅ |
| Фильтрация/обработка большого вывода инструментов | ✅ | ❌ |
| Запуск сборки или набора тестов | ❌ | ✅ |
| Цикл по результатам поиска | ✅ | ❌ |
| Интерактивные/фоновые процессы | ❌ | ✅ |
| Нужны API-ключи в окружении | ⚠️ Только через проброс | ✅ (большинство пробрасывается) |
Эмпирическое правило: берите execute_code, когда нужно программно вызывать инструменты Hermes с логикой между вызовами. Для команд shell, сборок и процессов используйте terminal.
Поддержка платформ
Выполнению кода нужны Unix-сокеты, поэтому оно доступно только на Linux и macOS. На Windows функция автоматически отключается — агент откатывается к обычным последовательным вызовам инструментов.