Агент цикла внутреннего устройства
Основной движок оркестрации — класс AIAgent из run_agent.py — большой файл (более 15 000 строк), который обрабатывает всё: от промпта сборки до инструментов диспетчеризации и переключения между провайдерами.
Основные обязанности
«AIAgent» отвечает за:
- Сборка эффективных системных промптов и инструментов схем через
prompt_builder.py - Выбор поставщика/режима API (
chat_completions,codex_responses,anthropic_messages) - Выполнение прерываемых вызовов модели с поддержкой сохранения.
- Исполнение вызовов инструментов (последовательно или параллельно через пуловые потоки)
- Ведение истории диалога в формате сообщений OpenAI
- Обработка сжатия, повторных операций и переключения на запасную модель.
- Отслеживание бюджетных итераций для родительских и дочерних агентов
- Сохранение постоянной памяти перед потерей контекста
Две точки входа
# Простой интерфейс — возвращает итоговую строку ответа
response = agent.chat("Исправь баг в main.py")
# Полный интерфейс — возвращает словарь с сообщениями, метаданными, статистикой использования
result = agent.run_conversation(
user_message="Исправь баг в main.py",
system_message=None, # создаётся автоматически, если не указан
conversation_history=None, # загружается из сессии, если не указана
task_id="task_abc123"
)
chat() — это тонкая окёртка вокруг run_conversation(), которая извлекает поле final_response из результирующего словаря.
Режимы API
Hermes поддерживает три режима выполнения API, определяемые на основе выбора провайдера, явных аргументов и эвристического базового URL:
| Режим API | Используется для | Тип клиента |
|---|---|---|
чат_завершения |
Эндпоинты, совместимые с OpenAI (OpenRouter, кастомные, большинство провайдеров) | openai.OpenAI |
codex_responses |
Кодекс OpenAI/API ответов | openai.OpenAI в формате Ответы |
антропные_сообщения |
Нативный API антропных сообщений | anthropic.Anthropic через адаптер |
Режим определяет, как форматируются сообщения, как структурируются инструменты вызова, как разбираются ответы и как работает кэширование/стриминг. Все три режима сводятся к единому внешнему формату сообщений (словари в стиле OpenAI с полями role/content/tool_calls) до и после вызовов API.
Порядок определения режима:
1. Явный аргумент конструктора api_mode (наивысший приоритет)
2. Определение на основе провайдера (например, провайдер anthropic → anthropic_messages)
3. Базовый URL Эвристики (например, api.anthropic.com → anthropic_messages)
4. По умолчанию: chat_completions
Жизненный цикл шага
каждая итерация цикла агента дает результат по следующей последовательности:
run_conversation()
1. Сгенерировать task_id, если не указан
2. Добавить сообщение пользователя в историю диалога
3. Собрать или использовать кэшированный системный промпт (prompt_builder.py)
4. Проверить, нужно ли предварительное сжатие (>50% контекста)
5. Собрать сообщения API из истории диалога
- chat_completions: формат OpenAI как есть
- codex_responses: преобразовать во входные элементы 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": "..."}
Содержимое рассуждений (от моделей, поддерживающих расширенное мышление) хранится в assistant_msg["reasoning"] и опционально отображается через reasoning_callback.
Правила очередности сообщений
Цикл агента строго соблюдает чередование ролей сообщений:
- После системного сообщения:
Пользователь → Помощник → Пользователь → Помощник →... - Во время вызова инструментов:
Ассистент (сtool_calls) → Инструмент → Инструмент →... → Ассистент - Никогда два сообщения помощника подряда
- Никогда два сообщения пользователя подряд
- Только роль
toolможет быть подряд (результаты параллельных инструментов)
Провайдеры проверяют эти последовательности и отклоняют некорректированную историю.
Прерываемые вызовы API
API-запросы обёрнуты в _interruptible_api_call(), который выполняет фактический HTTP-вызов в фоновом потоке, отслеживая события прерывания:
┌────────────────────────────────────────────────────┐
│ Основной поток Поток API │
│ │
│ ожидание: HTTP POST │
│ - готовность ответа ───▶ провайдеру │
│ - событие прерывания │
│ - таймаут │
└────────────────────────────────────────────────────┘
При отключении (пользователь отправил новое сообщение, команду /stop или сигнал):
- Поток API завершается (ответ отбрасывается)
- Агент может обработать новое введение или корректно выполнить работу.
- Частный ответ не добавляется в историю диалога
Инструменты для завершения
Последовательное и параллельное
Когда модель вызывает вызовы инструментов:
- Один инструмент вызова → значение непосредственно в потоке
- Несколько вызовов инструментов → выполняются параллельно через
ThreadPoolExecutor - Исключение: инструменты, помеченные как интерактивные (например, «уточнить»), проводить процедуры постоянно.
- Результаты в исходном порядке вызываются инструментами независимо от порядка.
Поток выполнения
для каждого tool_call в response.tool_calls:
1. Найти обработчик из tools/registry.py
2. Вызвать хук плагина pre_tool_call
3. Проверить, является ли команда опасной (tools/approval.py)
- Если опасная: вызвать approval_callback, ждать пользователя
4. Выполнить обработчик с аргументами + task_id
5. Вызвать хук плагина post_tool_call
6. Добавить {"role": "tool", "content": результат} в историю
Инструменты уровня агента
Некоторые инструменты перехватываются 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.*.
Сжатие и сохранение
Когда срабатывает сжатие
- Предварительное (перед вызовом 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 |
ABC ContextEngine — подключаемое управление контекстом |
agent/context_compressor.py |
Движок по умолчанию — алгоритм сжатия с потерями |
agent/prompt_caching.py |
Маркеры кэширования промпта Anthropic и метрики кэша |
agent/auxiliary_client.py |
Вспомогательный LLM-клиент для побочных задач (зрение, суммаризация) |
model_tools.py |
Сбор схем инструментов, диспетчеризация handle_function_call() |