Внутреннее устройство цикла агента
Ядро оркестрации — класс AIAgent из run_agent.py. Файл крупный и закрывает собой всё: от сборки промпта до диспетчеризации инструментов и переключения между провайдерами при сбоях.
Зоны ответственности
AIAgent занимается:
- Сборкой итогового системного промпта и схем инструментов через
prompt_builder.py - Выбором провайдера и режима API (chat_completions, codex_responses, anthropic_messages)
- Прерываемыми вызовами модели с поддержкой отмены
- Выполнением вызовов инструментов — последовательно или параллельно через пул потоков
- Ведением истории разговора в формате сообщений OpenAI
- Сжатием контекста, повторными попытками и переключением на резервную модель
- Отслеживанием бюджета итераций для родительского агента и субагентов
- Сбросом постоянной памяти перед тем, как контекст вытеснится
Две точки входа
# Простой интерфейс — возвращает строку с финальным ответом
response = agent.chat("Fix the bug in main.py")
# Полный интерфейс — возвращает dict с сообщениями, метаданными, статистикой токенов
result = agent.run_conversation(
user_message="Fix the bug in main.py",
system_message=None, # строится автоматически, если не задан
conversation_history=None, # загружается из сессии, если не задана
task_id="task_abc123"
)
chat() — тонкая обёртка над run_conversation(), которая вытаскивает поле final_response из результирующего dict.
Режимы API
Hermes поддерживает три режима выполнения запросов. Нужный выбирается по конфигурации провайдера, явным аргументам и эвристике по base URL:
| Режим API | Назначение | Тип клиента |
|---|---|---|
chat_completions | OpenAI-совместимые эндпоинты (OpenRouter, кастомные, большинство провайдеров) | openai.OpenAI |
codex_responses | OpenAI Codex / Responses API | openai.OpenAI в формате Responses |
anthropic_messages | Нативный Anthropic Messages API | anthropic.Anthropic через адаптер |
От режима зависит, как форматируются сообщения, как устроены вызовы инструментов, как разбираются ответы и как работают кэширование со стримингом. До и после вызовов API все три режима сводят данные к единому внутреннему формату — dict в стиле OpenAI: role/content/tool_calls.
Порядок определения режима:
- Явный аргумент
api_modeв конструкторе (наивысший приоритет) - Детектирование по провайдеру (например, провайдер
anthropic→anthropic_messages) - Эвристика по base URL (например,
api.anthropic.com→anthropic_messages) - По умолчанию:
chat_completions
Жизненный цикл хода
Каждая итерация цикла агента идёт по такой схеме:
run_conversation()
1. Генерация task_id, если не передан
2. Добавление сообщения пользователя в историю
3. Построение или переиспользование кэшированного системного промпта (prompt_builder.py)
4. Проверка необходимости предварительного сжатия (>50% контекста)
5. Формирование API-сообщений из истории разговора
- chat_completions: формат OpenAI без изменений
- codex_responses: конвертация в input items Responses API
- anthropic_messages: конвертация через anthropic_adapter.py
6. Внедрение эфемерных слоёв промпта (предупреждения о бюджете, давление контекста)
7. Простановка маркеров кэширования промпта при работе с Anthropic
8. Прерываемый вызов API (_interruptible_api_call)
9. Разбор ответа:
- Если tool_calls: выполнение, добавление результатов в историю, возврат к шагу 5
- Если текстовый ответ: сохранение сессии, сброс памяти при необходимости, возврат
Формат сообщений
Внутри все сообщения идут в OpenAI-совместимом формате:
{"role": "system", "content": "..."}
{"role": "user", "content": "..."}
{"role": "assistant", "content": "...", "tool_calls": [...]}
{"role": "tool", "tool_call_id": "...", "content": "..."}
Контент рассуждений (от моделей с поддержкой extended thinking) хранится в assistant_msg["reasoning"] и опционально выводится через reasoning_callback.
Правила чередования ролей
Цикл агента жёстко соблюдает чередование ролей:
- После системного сообщения:
User → Assistant → User → Assistant → ... - При вызовах инструментов:
Assistant (с tool_calls) → Tool → Tool → ... → Assistant - Никогда два подряд сообщения с ролью assistant
- Никогда два подряд сообщения с ролью user
- Только роль
toolдопускает несколько последовательных записей (параллельные результаты инструментов)
Провайдеры проверяют эти последовательности и отклоняют некорректную историю.
Прерываемые вызовы API
HTTP-запросы обёрнуты в _interruptible_api_call(): сам вызов уходит в фоновый поток, а основной поток тем временем следит за событиями прерывания:
┌────────────────────────────────────────────────────┐
│ Основной поток API-поток │
│ │
│ ожидает: HTTP POST │
│ - готовность ответа ───▶ к провайдеру │
│ - событие прерывания │
│ - таймаут │
└────────────────────────────────────────────────────┘
При прерывании (новое сообщение от пользователя, команда /stop или сигнал ОС):
- API-поток оставляют без обработки, ответ отбрасывается
- Агент обрабатывает новый ввод или корректно завершает работу
- Частичный ответ не попадает в историю разговора
Выполнение инструментов
Последовательно или параллельно
Когда модель возвращает вызовы инструментов:
- Один вызов → выполняется напрямую в основном потоке
- Несколько вызовов → выполняются параллельно через
ThreadPoolExecutor- Исключение: интерактивные инструменты (например,
clarify) принудительно идут последовательно - Результаты вставляются в историю в исходном порядке вызовов, независимо от того, что завершилось раньше
- Исключение: интерактивные инструменты (например,
Последовательность выполнения
for each tool_call in response.tool_calls:
1. Поиск обработчика в tools/registry.py
2. Вызов plugin-хука pre_tool_call
3. Проверка на опасную команду (tools/approval.py)
- Если опасная: вызов approval_callback, ожидание подтверждения
4. Выполнение обработчика с аргументами и task_id
5. Вызов plugin-хука post_tool_call
6. Добавление {"role": "tool", "content": result} в историю
Инструменты уровня агента
Часть инструментов перехватывается в run_agent.py до передачи в handle_function_call():
| Инструмент | Причина перехвата |
|---|---|
todo | Чтение/запись локального состояния задач агента |
memory | Запись в файлы постоянной памяти с учётом лимита символов |
session_search | Поиск по истории сессий через БД сессий агента |
delegate_task | Запуск субагента(ов) с изолированным контекстом |
Такие инструменты меняют состояние агента напрямую и возвращают синтетические результаты в обход реестра.
Поверхности коллбэков
AIAgent поддерживает платформо-зависимые коллбэки — они дают прогресс в реальном времени в CLI, шлюзе и интеграциях ACP:
| Коллбэк | Когда вызывается | Используется |
|---|---|---|
tool_progress_callback | До/после каждого выполнения инструмента | Спиннер CLI, прогресс-сообщения шлюза |
thinking_callback | Когда модель начинает/прекращает «думать» | Индикатор «thinking…» в CLI |
reasoning_callback | Когда модель возвращает контент рассуждений | Вывод рассуждений в CLI, блоки reasoning в шлюзе |
clarify_callback | При вызове инструмента clarify | Ввод в CLI, интерактивное сообщение шлюза |
step_callback | После каждого завершённого хода агента | Отслеживание шагов в шлюзе, прогресс ACP |
stream_delta_callback | Каждый токен стриминга (при включённом стриминге) | Потоковый вывод в CLI |
tool_gen_callback | При парсинге вызова инструмента из потока | Предпросмотр инструмента в спиннере CLI |
status_callback | Смена состояний (thinking, executing и др.) | Обновления статуса ACP |
Бюджет и резервное поведение
Бюджет итераций
Агент отслеживает итерации через IterationBudget:
- По умолчанию: 90 итераций (настраивается через
agent.max_turns) - У каждого агента собственный бюджет. Субагентам выдаются независимые бюджеты с потолком
delegation.max_iterations(по умолчанию 50) — суммарное число итераций родителя и субагентов может выйти за лимит родителя - Исчерпав бюджет, агент останавливается и возвращает сводку выполненной работы
Резервная модель
Если основная модель недоступна (429 rate limit, ошибки 5xx, ошибки аутентификации 401/403):
- Проверяется список
fallback_providersв конфигурации - Провайдеры из списка пробуются по порядку
- При успехе разговор продолжается с новым провайдером
- При ошибках 401/403 перед переключением сначала пробуется обновить учётные данные
Резервирование работает и для вспомогательных задач, причём независимо: у vision, сжатия и веб-извлечения свои цепочки фолбэков, настраиваемые в секции auxiliary.* конфига.
Сжатие и персистентность
Когда срабатывает сжатие
- Preflight (перед вызовом API): если разговор превышает 50% контекстного окна модели
- Автосжатие шлюза: при превышении 85% (более агрессивный режим, запускается между ходами)
Что происходит при сжатии
- Сначала память сбрасывается на диск — чтобы не потерять данные
- Промежуточные ходы разговора суммаризируются в компактную выжимку
- Последние N сообщений сохраняются полностью (
compression.protect_last_n, по умолчанию: 20) - Пары вызов инструмента/результат остаются вместе (никогда не разрываются)
- Генерируется новый ID ветки сессии (сжатие создаёт «дочернюю» сессию)
Персистентность сессии
После каждого хода:
- Сообщения сохраняются в хранилище сессий (SQLite через
hermes_state.py) - Изменения памяти сбрасываются в
MEMORY.md/USER.md - Сессию можно продолжить позже через
/resumeилиhermes chat --resume
Ключевые исходные файлы
| Файл | Назначение |
|---|---|
run_agent.py | Класс AIAgent — полная реализация цикла агента |
agent/prompt_builder.py | Сборка системного промпта из памяти, навыков, контекстных файлов и личности |
agent/context_engine.py | Абстрактный класс ContextEngine — подключаемое управление контекстом |
agent/context_compressor.py | Движок по умолчанию — алгоритм lossy-суммаризации |
agent/prompt_caching.py | Маркеры кэширования промптов Anthropic и метрики кэша |
agent/auxiliary_client.py | Вспомогательный LLM-клиент для побочных задач (vision, суммаризация) |
model_tools.py | Коллекция схем инструментов, диспетчеризация handle_function_call() |