Внутреннее устройство цикла агента

Ядро оркестрации — класс 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_completionsOpenAI-совместимые эндпоинты (OpenRouter, кастомные, большинство провайдеров)openai.OpenAI
codex_responsesOpenAI Codex / Responses APIopenai.OpenAI в формате Responses
anthropic_messagesНативный Anthropic Messages APIanthropic.Anthropic через адаптер

От режима зависит, как форматируются сообщения, как устроены вызовы инструментов, как разбираются ответы и как работают кэширование со стримингом. До и после вызовов API все три режима сводят данные к единому внутреннему формату — dict в стиле OpenAI: role/content/tool_calls.

Порядок определения режима:

  1. Явный аргумент api_mode в конструкторе (наивысший приоритет)
  2. Детектирование по провайдеру (например, провайдер anthropicanthropic_messages)
  3. Эвристика по base URL (например, api.anthropic.comanthropic_messages)
  4. По умолчанию: 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):

  1. Проверяется список fallback_providers в конфигурации
  2. Провайдеры из списка пробуются по порядку
  3. При успехе разговор продолжается с новым провайдером
  4. При ошибках 401/403 перед переключением сначала пробуется обновить учётные данные

Резервирование работает и для вспомогательных задач, причём независимо: у vision, сжатия и веб-извлечения свои цепочки фолбэков, настраиваемые в секции auxiliary.* конфига.

Сжатие и персистентность

Когда срабатывает сжатие

  • Preflight (перед вызовом API): если разговор превышает 50% контекстного окна модели
  • Автосжатие шлюза: при превышении 85% (более агрессивный режим, запускается между ходами)

Что происходит при сжатии

  1. Сначала память сбрасывается на диск — чтобы не потерять данные
  2. Промежуточные ходы разговора суммаризируются в компактную выжимку
  3. Последние N сообщений сохраняются полностью (compression.protect_last_n, по умолчанию: 20)
  4. Пары вызов инструмента/результат остаются вместе (никогда не разрываются)
  5. Генерируется новый 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()

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

ESC