Архитектура цикла агента

Основным движком оркестрации является класс AIAgent в run_agent.py — большой файл (более 15 000 строк), который обрабатывает всё: от сборки промптов до диспетчеризации инструментов и переключения провайдеров при сбоях.

Основные обязанности

AIAgent отвечает за:

Две точки входа

# Simple interface — returns final response string
response = agent.chat("Fix the bug in main.py")

# Full interface — returns dict with messages, metadata, usage stats
result = agent.run_conversation(
    user_message="Fix the bug in main.py",
    system_message=None,           # auto-built if omitted
    conversation_history=None,      # auto-loaded from session if omitted
    task_id="task_abc123"
)

chat() — это тонкая обёртка вокруг run_conversation(), извлекающая поле final_response из словаря результата.

Режимы API

Hermes поддерживает три режима выполнения API, определяемые на основе выбора провайдера, явных аргументов и эвристики базового URL:

Режим API Используется для Тип клиента
chat_completions Совместимые с OpenAI конечные точки (OpenRouter, кастомные, большинство провайдеров) openai.OpenAI
codex_responses OpenAI Codex / Responses API openai.OpenAI с форматом Responses
anthropic_messages Нативный API Anthropic Messages anthropic.Anthropic через адаптер

Режим определяет, как форматируются сообщения, как структурируются вызовы инструментов, как парсятся ответы и как работают кэширование/стриминг. Все три режима сходятся к одному и тому же внутреннему формату сообщений (словари OpenAI-стиля role/content/tool_calls) до и после вызовов API.

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

  1. Явный аргумент конструктора api_mode (наивысший приоритет)

  2. Определение на основе провайдера (например, провайдер anthropicanthropic_messages)

  3. Эвристика базового URL (например, api.anthropic.comanthropic_messages)

  4. По умолчанию: chat_completions

Жизненный цикл хода

Каждая итерация цикла агента следует этой последовательности:

run_conversation()
  1. Generate task_id if not provided
  2. Append user message to conversation history
  3. Build or reuse cached system prompt (prompt_builder.py)
  4. Check if preflight compression is needed (>50% context)
  5. Build API messages from conversation history
     - chat_completions: OpenAI format as-is
     - codex_responses: convert to Responses API input items
     - anthropic_messages: convert via anthropic_adapter.py
  6. Inject ephemeral prompt layers (budget warnings, context pressure)
  7. Apply prompt caching markers if on Anthropic
  8. Make interruptible API call (_interruptible_api_call)
  9. Parse response:
     - If tool_calls: execute them, append results, loop back to step 5
     - If text response: persist session, flush memory if needed, return

Формат сообщений

Все сообщения внутренне используют совместимый с OpenAI формат:

{"role": "system", "content": "..."}
{"role": "user", "content": "..."}
{"role": "assistant", "content": "...", "tool_calls": [...]}
{"role": "tool", "tool_call_id": "...", "content": "..."}

Содержимое рассуждений (от моделей, поддерживающих расширенное мышление) хранится в assistant_msg["reasoning"] и опционально отображается через reasoning_callback.

Правила чередования сообщений

Цикл агента обеспечивает строгое чередование ролей сообщений:

Провайдеры проверяют эти последовательности и отклоняют некорректную историю.

Прерываемые вызовы API

Запросы к API обёрнуты в _interruptible_api_call(), который выполняет реальный HTTP-вызов в фоновом потоке, одновременно отслеживая событие прерывания:

┌────────────────────────────────────────────────────┐
│  Main thread                  API thread           │
│                                                    │
│   wait on:                     HTTP POST           │
│    - response ready     ───▶   to provider         │
│    - interrupt event                               │
│    - timeout                                       │
└────────────────────────────────────────────────────┘

При прерывании (пользователь отправляет новое сообщение, команда /stop или сигнал):

Выполнение инструментов

Последовательно или конкурентно

Когда модель возвращает вызовы инструментов:

Поток выполнения

for each tool_call in response.tool_calls:
    1. Resolve handler from tools/registry.py
    2. Fire pre_tool_call plugin hook
    3. Check if dangerous command (tools/approval.py)
       - If dangerous: invoke approval_callback, wait for user
    4. Execute handler with args + task_id
    5. Fire post_tool_call plugin hook
    6. Append {"role": "tool", "content": result} to history

Инструменты уровня агента

Некоторые инструменты перехватываются run_agent.py до того, как они достигают handle_function_call():

Инструмент Причина перехвата
todo Читает/записывает локальное состояние задач агента
memory Записывает в файлы персистентной памяти с ограничением по символам
session_search Запрашивает историю сессий через БД сессий агента
delegate_task Создаёт подагента(ов) с изолированным контекстом

Эти инструменты изменяют состояние агента напрямую и возвращают синтетические результаты без прохождения через реестр.

Поверхности обратных вызовов

AIAgent поддерживает платформозависимые обратные вызовы, обеспечивающие отображение прогресса в реальном времени в CLI, шлюзе и интеграциях ACP:

Обратный вызов Когда срабатывает Используется
tool_progress_callback До/после каждого выполнения инструмента CLI-спиннер, сообщения прогресса шлюза
thinking_callback Когда модель начинает/заканчивает думать Индикатор CLI «думает...»
reasoning_callback Когда модель возвращает содержимое рассуждений Отображение рассуждений в CLI, блоки рассуждений шлюза
clarify_callback Когда вызывается инструмент clarify Приглашение ввода CLI, интерактивное сообщение шлюза
step_callback После каждого завершённого хода агента Отслеживание шагов шлюза, прогресс ACP
stream_delta_callback Каждый токен стриминга (когда включено) Отображение стриминга в CLI
tool_gen_callback Когда вызов инструмента извлекается из стрима Предпросмотр инструмента в CLI-спиннере
status_callback Изменения состояния (думает, выполняет и т.д.) Обновления статуса ACP

Бюджет и поведение при сбоях

Бюджет итераций

Агент отслеживает итерации через IterationBudget:

Запасная модель

Когда основная модель выдаёт ошибку (429 превышение лимита, 5xx ошибка сервера, 401/403 ошибка аутентификации):

  1. Проверка списка fallback_providers в конфиге

  2. Попытка каждого запасного провайдера по порядку

  3. При успехе — продолжение диалога с новым провайдером

  4. При 401/403 — попытка обновления учётных данных перед переключением

Система запасных провайдеров также независимо покрывает вспомогательные задачи — зрение, сжатие, извлечение веб-страниц и поиск по сессиям. Для каждой из них настраивается собственная цепочка запасных провайдеров через секцию конфига auxiliary.*.

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

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

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

  1. Память сначала сбрасывается на диск (предотвращение потери данных)

  2. Средние ходы диалога сводятся в компактную сводку

  3. Последние N сообщений сохраняются нетронутыми (compression.protect_last_n, по умолчанию: 20)

  4. Пары «вызов инструмента / результат» сохраняются вместе (никогда не разделяются)

  5. Создаётся новый идентификатор линии сессии (сжатие создаёт «дочернюю» сессию)

Персистентность сессии

После каждого хода:

Ключевые исходные файлы

Файл Назначение
run_agent.py Класс AIAgent — полный цикл агента
agent/prompt_builder.py Сборка системного промпта из памяти, навыков, контекстных файлов, личности
agent/context_engine.py ContextEngine ABC — подключаемое управление контекстом
agent/context_compressor.py Движок по умолчанию — алгоритм сжатия с потерями
agent/prompt_caching.py Маркеры кэширования промптов Anthropic и метрики кэша
agent/auxiliary_client.py Вспомогательный LLM-клиент для побочных задач (зрение, суммаризация)
model_tools.py Коллекция схем инструментов, диспетчеризация handle_function_call()