Программная интеграция

У Hermes есть три протокола, чтобы управлять агентом из внешних программ — плагинов IDE, своих интерфейсов, CI-пайплайнов, встроенных субагентов. Выбирайте тот, что подходит по транспорту и типу потребителя.

ПротоколТранспортЛучше всего подходит дляРеализован в
ACPJSON-RPC поверх stdioIDE-клиентов (VS Code, Zed, JetBrains), которые уже говорят на Agent Client Protocolacp_adapter/
TUI gatewayJSON-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
promptprompt.submit (или ACP session/prompt)
steersession.steer
follow_upprompt.submit в очереди после текущего хода
abortsession.interrupt
set_modelcommand.dispatch для /model <provider:model> (в рамках сессии, постоянно)
compactsession.compress
get_statesession.status
get_messagessession.history
switch_sessionsession.resume
forksession.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>):

Исход ходаСтатусТерминальное событиеФлаги в событии / статусе
Выдан финальный ответcompletedrun.completedcompleted: true
Прерван (/stop или прерывание внутри агента)cancelledrun.cancelledcompleted: 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)
Сбой провайдера/агентаfailedrun.failedcompleted: false, error
Gateway остановлен, пока ход был активен (только /v1/runs)interruptedrun.interruptederror: "Gateway shutdown interrupted the run." — записывается до прерывания агента и никогда не перезаписывается запоздавшим результатом хода; клиентский /stop по-прежнему завершается как cancelled
Закончился, не завершившись (бюджет итераций, обрезанный или частичный ответ)failedrun.failedcompleted: 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 с конкретным описанием того, что строите.

перевод и редактура:

обновлено

⌕ ESC