Программная интеграция
У Hermes есть три протокола, чтобы управлять агентом из внешних программ — плагинов IDE, своих интерфейсов, CI-пайплайнов, встроенных субагентов. Выбирайте тот, что подходит по транспорту и типу потребителя.
| Протокол | Транспорт | Лучше всего подходит для | Реализован в |
|---|---|---|---|
| ACP | JSON-RPC поверх stdio | IDE-клиентов (VS Code, Zed, JetBrains), которые уже говорят на Agent Client Protocol | acp_adapter/ |
| TUI gateway | JSON-RPC поверх stdio (или WebSocket) | Кастомных хостов с тонким управлением сессиями, слэш-командами, подтверждениями и потоковыми событиями | tui_gateway/server.py |
| API-сервер | HTTP + Server-Sent Events | Фронтендов, совместимых с OpenAI (Open WebUI, LobeChat, LibreChat…), и веб-клиентов на любом языке | gateway/platforms/api_server.py |
Все три работают поверх одного ядра AIAgent. Разница лишь в формате сообщений и наборе доступных возможностей.
ACP (Agent Client Protocol)
hermes acp запускает stdio JSON-RPC сервер, говорящий на ACP. В продакшне его используют VS Code (расширение ACP от Zed Industries), Zed и любая JetBrains IDE с ACP-плагином.
Доступные возможности: создание сессии, отправка промпта, потоковая передача фрагментов сообщений агента, события вызова инструментов, запросы разрешений, форк сессии, отмена и аутентификация. Вывод инструментов оборачивается в ACP-блоки Diff/ToolCall, понятные IDE.
Полный жизненный цикл, событийный мост и процесс подтверждения: ACP Internals.
hermes acp # запустить ACP на stdio
hermes acp --check # проверить зависимости ACP и импорты адаптера
hermes acp --setup # интерактивная настройка провайдера/модели для авторизации ACP в терминале
TUI Gateway JSON-RPC
tui_gateway/server.py — протокол, которым пользуются Ink TUI (hermes --tui) и встроенный PTY-мост дашборда. Любой внешний хост может говорить на том же протоколе через stdio или WebSocket (tui_gateway/ws.py).
Каталог методов (выборка)
prompt.submit prompt.background session.steer
session.create session.list session.active_list
session.activate session.close session.interrupt
session.history session.compress session.branch
session.title session.usage session.status
clarify.lock config.set / config.get commands.catalog
client.capabilities gateway.capabilities ping
command.resolve command.dispatch cli.exec
reload.mcp reload.env process.stop
delegation.status subagent.interrupt subagent.steer
spawn_tree.save / list / load
terminal.resize clipboard.paste image.attach
session.active_list, session.activate и session.close управляют живыми сессиями локально внутри процесса — их использует переключатель сессий TUI. Для работы с сохранёнными транскриптами берите session.list / /resume; методы для активных сессий применяйте только к тем, что открыты в процессе TUI gateway.
В пределах одного аутентифицированного gateway возобновление или активация живой сессии подключает ещё одного подписчика событий, а не вытесняет предыдущее соединение. События стрима и терминала уходят всем подключённым клиентам; отключение одного клиента не завершает сессию, которую смотрит другой. Исключительность submit и настроенная политика ввода при занятой сессии продолжают действовать. Подключённые клиенты могут управлять субагентами сессии, а результаты browser-контроллера по-прежнему требуют того соединения, которое этот контроллер зарегистрировало. Писать в одну и ту же сессию из независимых процессов gateway это не разрешает и устойчивого приёма промптов через перезапуск владельца не обещает.
Переопределение модели в session.create
session.create принимает переопределения model / provider для отдельной сессии. Пару, которую провайдер обслужить не может (model: gpt-5.5 с provider: anthropic или без provider, когда в профиле настроен провайдер Anthropic), gateway отклоняет сразу с JSON-RPC-кодом -32602, а не создаёт сессию, чей первый ход упадёт на стороне провайдера. В error.data приходят model, provider и до пяти suggestions из каталога этого провайдера, error.message их повторяет. Проверка офлайновая и отклоняет только имена, о которых Hermes точно знает, что они принадлежат другому провайдеру. Кастомные эндпоинты (custom, custom:<name>), агрегаторы (OpenRouter, Nous, …), модели из собственного семейства провайдера, до которых курируемый список ещё не дорос, и имена, которых нет ни в одном каталоге, принимаются, как и раньше.
Перемотка истории в prompt.submit
Перемотка / правка / регенерация — это prompt.submit, который перед запуском нового хода отбрасывает часть сохранённого транскрипта. Такая запись переписывает постоянные строки сессии деструктивно, поэтому gateway принимает её только тогда, когда клиент явно заявляет о своём намерении:
| Параметр | Значение |
|---|---|
truncate_before_user_ordinal | Индекс (с нуля) пользовательского хода, на котором делается отсечение. Всё начиная с этого хода отбрасывается. Строки таймлайна только для отображения (display_kind) не учитываются. Должно быть настоящим целым числом — JSON-булево значение отклоняется с кодом 4004. |
truncate_before_row_id | Целочисленный ID строки SQLite (messages.id / row_id) целевого пользовательского хода, на котором делается отсечение. Предпочтительный постоянный адрес. Если переданы и ordinal, и row ID, gateway проверяет их соответствие (при несовпадении возвращает 4030). Неизвестный/устаревший row ID отклоняется с кодом 4018 — отката к ordinal не происходит. |
confirm_truncate | Обязателен при передаче ordinal, message ID или row ID. Подтверждает, что этот submit — действительно перемотка, а не обычная отправка, в которой случайно остались параметры от прошлого запроса. Отправка без цели отклоняется с кодом 4004. |
confirm_empty_truncate | Дополнительно обязателен, если отсечение оставит транскрипт пустым (ordinal 0). |
confirm_deep_truncate | Отправляйте, если отсечение заархивирует больше одного хода пользователя (глубже, чем перегенерация или правка последнего хода), и только после вопроса пользователю. В этом релизе неподтверждённое глубокое отсечение пишется в лог и всё равно выполняется, чтобы старые клиенты продолжали работать. Со следующего релиза оно будет отклоняться с кодом 4033 (error.data содержит archived_messages и archived_user_turns, ничего не записывается). |
Параметр усечения без confirm_truncate отклоняется с кодом 4004 или 4029, и ничего не записывается. Хосты, реализующие перемотку, должны выставлять флаг в момент, когда пользователь её запрашивает, и никогда не должны держать параметры усечения в состоянии между обычными отправками. Предпочитайте truncate_before_row_id (из row_id / _row_id при resume) ordinal-адресации; ordinal оставляйте как запасной путь для совместимости / оптимистичных строк только тогда, когда постоянного ID ещё нет.
Усекающий submit никогда не поглощается политикой ввода при занятой сессии. Пока ход ещё идёт, обычный prompt.submit направляется в текущий ход, перенаправляется или ставится в очередь (display.busy_input_mode), а перемотка / правка / регенерация вместо этого отклоняется с кодом 4009 (session busy): постановка в очередь потеряла бы обрезку истории, и правка выполнилась бы как обычное продолжение после неотредактированного хода. Хосты вызывают session.interrupt и повторяют тот же submit, пока он не пройдёт. Десктопное приложение делает это автоматически, так что правка сообщения, пока Hermes ещё думает, останавливает живой ход и перезапускает его с отредактированного промпта.
При успешном усекающем submit к постоянной сессии результат prompt.submit дополнительно содержит survivor_user_row_ids — свежие row ID уцелевших пользовательских ходов после перезаписи, в порядке видимого пользовательского ordinal. Перезапись заново вставляет сохранённый префикс как новые строки, поэтому любой row ID, закэшированный хостом до перемотки, после неё устаревает; перепривязывайте закэшированные ID по этому списку (запись null означает, что у этого хода нет постоянного ID — закэшированный нужно отбросить), иначе следующая перемотка, нацеленная на более ранний уцелевший ход, будет отклонена с кодом 4018.
Потоковые события
message.delta, message.complete, tool.start, tool.generating, tool.complete, gateway.ready, request.cancel, а также события жизненного цикла сессий и ошибки.
Запросы сервер→клиент (вопросы, которые агент задаёт вам)
Подтверждения, уточняющие вопросы, запросы sudo/секретов, разблокировка хранилища, настройка MCP и десктопные мосты чтения/действий — это JSON-RPC-запросы от шлюза к клиенту, а не события. Кадр несёт строковый id, а клиент отвечает обычным JSON-RPC-ответом с тем же id:
← {"jsonrpc":"2.0","id":"srq-7","method":"approval","params":{"session_id":"…","request_id":"…","command":"rm -rf build","description":"…"}}
→ {"jsonrpc":"2.0","id":"srq-7","result":{"choice":"once"}}
Методы: approval → {choice}; clarify → {answers} с ключами по id вопроса или {} для отмены (clarify.lock фиксирует один ответ заранее; null пропускает этот вопрос); sudo, secret, vault.code, vault.unlock_prompt → {value}; connection → {settled_by, targets} (карточка manage_connections: по одному исходу на каждую цель); terminal.read, window.read, preview.act, tour → {value} (текст JSON). На метод, который ваш хост не реализует, отвечайте JSON-RPC-ошибкой (-32601), чтобы агент быстро получил отказ, а не ждал истечения таймаута.
Объявите, что вы на них отвечаете (ломающее изменение для существующих WebSocket-интеграций). Один раз на соединение, после gateway.ready, вызовите client.capabilities с {"server_requests": true}; результат перечисляет методы запросов, которые этот бэкенд может присылать. WebSocket-клиент, который этого не делает, считается сборкой, появившейся до запросов сервер→клиент: шлюз сразу проваливает для него каждый такой запрос (агент видит то же «нет ответа», что и при ответе-ошибке; подтверждение отзывается, а не отклоняется), вместо того чтобы простаивать весь срок ожидания. Переходного периода нет: сторонний WebSocket-клиент, который до этого изменения отвечал на clarify/approval/sudo/…, но не шлёт client.capabilities, теперь получает отказ на каждый такой запрос, пока не добавит один этот вызов. Сессии без подключённого клиента это не касается — её открытые вопросы ждут в open_requests до повторного воспроизведения при переподключении. stdio TUI, десктопное приложение и дашборд объявляют это через общий JsonRpcRequestChannel.
Когда шлюз отзывает вопрос (таймаут, прерывание, ответ с другой поверхности), он выдаёт request.cancel { id, method, reason }; очищайте только соответствующий запрос. Результаты session.resume / session.activate и session.events.since несут open_requests — ещё открытые кадры, — так что переподключившийся клиент отрисовывает их заново (и всё ещё может ответить).
Восстановление текущего хода при переподключении
Результаты session.resume / session.activate несут inflight — ход, который ещё выполняется (или сохранённый упавший), которого в истории пока нет: user, уже отстримленный assistant, streaming, corrections посреди хода и поля ошибок. Если ход запустил сам шлюз, а не набрал человек (завершение фонового процесса, результат асинхронного делегирования, скрытый служебный промпт), inflight несёт ещё и те же display_kind / display_metadata, которые получит сохранённая строка messages, — так клиент отрисует живой промпт ровно так, как отрисует историю после завершения хода: маркер process_complete на таймлайне с display_metadata.display_text, вовсе ничего для hidden. Для настоящего пользовательского ввода обоих полей нет; никогда не выводите происхождение из текста промпта (пользователь, процитировавший строку-маркер, остаётся пользователем).
Маппинг RPC-команд в стиле Pi
Каждой команде из RPC-спецификации Pi-mono (issue #360) соответствует эквивалент в TUI gateway:
| Команда Pi | Эквивалент в Hermes |
|---|---|
prompt | prompt.submit (или ACP session/prompt) |
steer | session.steer |
follow_up | prompt.submit в очереди после текущего хода |
abort | session.interrupt |
set_model | command.dispatch для /model <provider:model> (в рамках сессии, постоянно) |
compact | session.compress |
get_state | session.status |
get_messages | session.history |
switch_session | session.resume |
fork | session.branch |
ui_request / ui_response | запросы сервер→клиент clarify / sudo / secret / approval, на которые отвечают кадрами JSON-RPC-ответов |
API-сервер, совместимый с OpenAI
gateway/platforms/api_server.py открывает доступ к Hermes по HTTP для любого клиента, говорящего на формате OpenAI. Удобно, когда нужен веб-фронтенд, CI-раннер на curl или потребитель не на Python.
Эндпоинты:
POST /v1/chat/completions OpenAI Chat Completions (стриминг через SSE)
POST /v1/responses OpenAI Responses API (с сохранением состояния)
POST /v1/runs Запустить задачу, возвращает run_id (202)
GET /v1/runs/{id} Статус задачи
GET /v1/runs/{id}/events SSE-поток событий жизненного цикла
POST /v1/runs/{id}/approval Разрешить ожидающее подтверждение
POST /v1/runs/{id}/steer Внедрить промежуточное указание на следующей границе вызова инструмента
POST /v1/runs/{id}/stop Прервать выполнение
GET /v1/capabilities Флаги возможностей в машиночитаемом формате
POST /v1/browser-control/register Зарегистрировать контроллер браузера
GET /v1/browser-control/ws WebSocket контроллера браузера
GET /v1/models Список — hermes-agent
GET /api/model/options Инвентарь пикера с учётом провайдера
GET /health, /health/detailed
Настройка, заголовки (X-Hermes-Session-Id, X-Hermes-Session-Key) и подключение фронтенда: API Server.
Браузерные расширения могут по желанию подключиться к протоколу контроллера (по умолчанию он выключен) и управлять именно той сессией браузера, из которой началась беседа с Hermes. Транспорты API и дашборда используют общий брокер, привязанный к принципалу, и один явный allowlist возможностей — подробности в разделе Управление через расширение браузера.
Поверхности каталога моделей
Совместимый с OpenAI API намеренно держит GET /v1/models минимальным: это
эндпоинт совместимости, которого ждут фронтенды, а не полный каталог пикера
провайдеров/моделей Hermes.
Если внешней control plane нужны курируемые Hermes строки провайдеров, цены по моделям или подсказки о возможностях — используйте одну из аутентифицированных поверхностей пикера:
- REST API-сервера:
GET /api/model/optionsс bearer-ключом API-сервера - REST бэкенда дашборда:
GET /api/model/optionsсX-Hermes-Session-Token - RPC TUI gateway:
model.options
Эти поверхности используют один и тот же сборщик payload и одну и ту же политику проб кастомных провайдеров:
- Обычное открытие: пробуется только текущий кастомный провайдер, чтобы оффлайн-эндпоинты из сохранённых настроек не тормозили пикер.
- Явное обновление (
refresh=1илиrefresh: true): сбрасывается кэш моделей провайдера и пробуются все сохранённые кастомные провайдеры, чтобы живые каталоги пересобрались полностью.
Используйте /v1/models для совместимости с OpenAI-клиентами. Используйте
/api/model/options или model.options, когда строите пикер моделей,
осведомлённый о Hermes.
POST /v1/runs/{id}/steer — HTTP-эквивалент Hermes /steer: он не создаёт новый пользовательский ход и не переписывает немедленно вывод ассистента, уже находящийся в процессе генерации. Вместо этого текст добавляется к выполняющемуся run и становится видимым агенту на следующей границе вызова инструмента, так что агент может скорректировать курс, не сбрасывая текущий цикл вызова инструментов.
/v1/runs/{id}/steer принимается, только пока статус run — running. Для run в очереди, на паузе ожидания подтверждения, останавливающегося, отменённого, упавшего и завершённого возвращается 409 run_not_accepting_steer, даже если сервер всё ещё хранит внутренние ссылки на агента во время кооперативного завершения.
200 (и событие run.steered) означает, что текст был поставлен в очередь, а не что агент его учёл. Если steer приходит уже после финального ответа агента — когда дальше нет границы вызова инструмента, на которой его можно доставить, — недоставленный текст возвращается как pending_steer в терминальном событии (run.completed, run.failed или run.cancelled) и в статусе run, так что клиент может воспроизвести его как следующий пользовательский ход, а не потерять.
Терминальный статус run
Терминальный статус run выводится из того, как на самом деле закончился ход агента, и имя терминального события всегда ему соответствует (run.<status>):
| Исход хода | Статус | Терминальное событие | Флаги в событии / статусе |
|---|---|---|---|
| Выдан финальный ответ | completed | run.completed | completed: true |
Прерван (/stop или прерывание внутри агента) | cancelled | run.cancelled | completed: false, interrupted: true, turn_exit_reason с указанием инициатора — interrupted_by_user при остановке человеком, interrupted_by_system(<issuer>) / interrupted_during_api_call(<issuer>), когда ход завершил вотчдог (например, cron_inactivity_watchdog, turn_liveness_watchdog, gateway_inactivity_watchdog, session_turn_lease_lost) |
| Сбой провайдера/агента | failed | run.failed | completed: false, error |
Gateway остановлен, пока ход был активен (только /v1/runs) | interrupted | run.interrupted | error: "Gateway shutdown interrupted the run." — записывается до прерывания агента и никогда не перезаписывается запоздавшим результатом хода; клиентский /stop по-прежнему завершается как cancelled |
| Закончился, не завершившись (бюджет итераций, обрезанный или частичный ответ) | failed | run.failed | completed: false, partial, когда применимо, turn_exit_reason (например, max_iterations_reached(60/60)), output с запасным текстом, если он есть |
Run никогда не сообщается как completed с completed: false или partial: true в той же полезной нагрузке. То же правило действует для /api/sessions/{id}/chat/stream: его полезная нагрузка assistant.completed несёт настоящие флаги completed / partial / interrupted, а терминальное событие — run.completed, run.failed или run.cancelled.
Какой протокол выбрать?
- Пишете плагин для IDE, которая уже поддерживает ACP → ACP. Никакой дополнительной работы с протоколом на стороне IDE.
- Строите кастомный десктоп / веб / TUI-хост и хотите весь набор возможностей Hermes (слэш-команды, подтверждения, уточняющие запросы, мультиагентность, ветвление сессий) → TUI gateway JSON-RPC.
- Нужен любой фронтенд с поддержкой OpenAI, HTTP-клиент на произвольном языке или curl-автоматизация → API-сервер.
- Хотите встроить агента прямо в Python-процесс без сабпроцессов → импортируйте
run_agent.AIAgentнапрямую. Подробнее: Agent Loop.
Горячая замена модели
Сменить модель прямо во время сессии можно на любом из протоколов — под капотом везде работает слэш-команда /model.
- CLI / TUI:
/model claude-sonnet-4или/model openrouter:anthropic/claude-sonnet-4.6 - TUI gateway RPC:
command.dispatchс{"command": "/model claude-sonnet-4"} - ACP: IDE передаёт слэш-команду как промпт, а агент её обрабатывает
- API-сервер: передайте поле
modelв теле запроса
Разрешение имени с учётом провайдера встроено изначально: одно и то же имя модели подбирает нужный формат под любого подключённого провайдера. См. hermes_cli/model_switch.py.
О флаге --mode rpc
В Hermes нет флага --mode rpc. Три описанных протокола уже закрывают все сценарии: ACP — для IDE-клиентов, TUI gateway — для stdio JSON-RPC хостов, API-сервер — для HTTP. Нашли реальный случай, который ни один из них не покрывает, — откройте issue с конкретным описанием того, что строите.