Архитектура цикла агента
Основным движком оркестрации является класс AIAgent в run_agent.py — большой файл (более 15 000 строк), который обрабатывает всё: от сборки промптов до диспетчеризации инструментов и переключения провайдеров при сбоях.
Основные обязанности
AIAgent отвечает за:
-
Сборку итогового системного промпта и схем инструментов через
prompt_builder.py -
Выбор правильного провайдера/режима API (chat_completions, codex_responses, anthropic_messages)
-
Прерываемые вызовы моделей с поддержкой отмены
-
Выполнение вызовов инструментов (последовательно или конкурентно через пул потоков)
-
Ведение истории диалога в формате сообщений OpenAI
-
Обработку сжатия, повторных попыток и переключения на запасные модели
-
Отслеживание бюджетов итераций для родительских и дочерних агентов
-
Сброс персистентной памяти перед потерей контекста
Две точки входа
# 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.
Порядок определения режима:
-
Явный аргумент конструктора
api_mode(наивысший приоритет) -
Определение на основе провайдера (например, провайдер
anthropic→anthropic_messages) -
Эвристика базового URL (например,
api.anthropic.com→anthropic_messages) -
По умолчанию:
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.
Правила чередования сообщений
Цикл агента обеспечивает строгое чередование ролей сообщений:
-
После системного сообщения:
User → Assistant → User → Assistant → ... -
Во время вызова инструментов:
Assistant (with tool_calls) → Tool → Tool → ... → Assistant -
Никогда два сообщения assistant подряд
-
Никогда два сообщения user подряд
-
Только роль
toolможет иметь последовательные записи (результаты параллельных инструментов)
Провайдеры проверяют эти последовательности и отклоняют некорректную историю.
Прерываемые вызовы API
Запросы к API обёрнуты в _interruptible_api_call(), который выполняет реальный HTTP-вызов в фоновом потоке, одновременно отслеживая событие прерывания:
┌────────────────────────────────────────────────────┐
│ Main thread API thread │
│ │
│ wait on: HTTP POST │
│ - response ready ───▶ to provider │
│ - interrupt event │
│ - timeout │
└────────────────────────────────────────────────────┘
При прерывании (пользователь отправляет новое сообщение, команда /stop или сигнал):
-
Поток API покидается (ответ отбрасывается)
-
Агент может обработать новый ввод или корректно завершить работу
-
Частичный ответ не вставляется в историю диалога
Выполнение инструментов
Последовательно или конкурентно
Когда модель возвращает вызовы инструментов:
-
Одиночный вызов инструмента → выполняется напрямую в главном потоке
-
Несколько вызовов инструментов → выполняются конкурентно через
ThreadPoolExecutor - Исключение: инструменты, помеченные как интерактивные (например,
clarify), выполняются принудительно последовательно - Результаты вставляются в исходном порядке вызовов инструментов независимо от порядка завершения
Поток выполнения
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:
-
По умолчанию: 90 итераций (настраивается через
agent.max_turns) -
Каждый агент получает свой собственный бюджет. Подагенты получают независимые бюджеты с лимитом
delegation.max_iterations(по умолчанию 50) — общее количество итераций родительского и дочерних агентов может превышать лимит родителя -
При 100% агент останавливается и возвращает сводку выполненной работы
Запасная модель
Когда основная модель выдаёт ошибку (429 превышение лимита, 5xx ошибка сервера, 401/403 ошибка аутентификации):
-
Проверка списка
fallback_providersв конфиге -
Попытка каждого запасного провайдера по порядку
-
При успехе — продолжение диалога с новым провайдером
-
При 401/403 — попытка обновления учётных данных перед переключением
Система запасных провайдеров также независимо покрывает вспомогательные задачи — зрение, сжатие, извлечение веб-страниц и поиск по сессиям. Для каждой из них настраивается собственная цепочка запасных провайдеров через секцию конфига auxiliary.*.
Сжатие и персистентность
Когда срабатывает сжатие
-
Preflight (перед вызовом API): Если диалог превышает 50% контекстного окна модели
-
Автосжатие шлюза: Если диалог превышает 85% (более агрессивно, выполняется между ходами)
Что происходит при сжатии
-
Память сначала сбрасывается на диск (предотвращение потери данных)
-
Средние ходы диалога сводятся в компактную сводку
-
Последние N сообщений сохраняются нетронутыми (
compression.protect_last_n, по умолчанию: 20) -
Пары «вызов инструмента / результат» сохраняются вместе (никогда не разделяются)
-
Создаётся новый идентификатор линии сессии (сжатие создаёт «дочернюю» сессию)
Персистентность сессии
После каждого хода:
-
Сообщения сохраняются в хранилище сессий (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 ABC — подключаемое управление контекстом |
agent/context_compressor.py |
Движок по умолчанию — алгоритм сжатия с потерями |
agent/prompt_caching.py |
Маркеры кэширования промптов Anthropic и метрики кэша |
agent/auxiliary_client.py |
Вспомогательный LLM-клиент для побочных задач (зрение, суммаризация) |
model_tools.py |
Коллекция схем инструментов, диспетчеризация handle_function_call() |