Middleware в Hermes
Middleware в Hermes — это меняющий поведение напарник хуков-наблюдателей. Хуки-наблюдатели сообщают, что произошло. Middleware может изменить то, что происходит: переписать запрос до выполнения или обернуть сам колбэк выполнения.
Контракт намеренно не привязан к бэкенду. Плагин может использовать его для локальной политики, формирования запросов, трассировки, адаптивной маршрутизации, управления кэшем, выбора песочницы или передачи в рантаймы вроде NeMo Relay — не меняя планировщик Hermes, адаптеры провайдеров моделей, реестр инструментов, память или UX CLI.
С включённым middleware плагины могут:
- Переписывать kwargs запроса к LLM-провайдеру до того, как Hermes вызовет провайдера.
- Переписывать аргументы инструмента до того, как их увидят guardrails, проверки подтверждений, хуки и выполнение инструмента.
- Оборачивать реальный колбэк выполнения LLM, сохраняя поведение Hermes при ретраях, стриминге, прерываниях и хуках.
- Оборачивать реальный колбэк выполнения инструмента, сохраняя guardrails, подтверждения, post-tool-хуки и преобразование результата инструмента.
Контракт
Плагины регистрируют middleware из register(ctx):
def register(ctx):
ctx.register_middleware("llm_request", on_llm_request)
ctx.register_middleware("llm_execution", on_llm_execution)
ctx.register_middleware("tool_request", on_tool_request)
ctx.register_middleware("tool_execution", on_tool_execution)
Каждый колбэк middleware получает:
telemetry_schema_version: сейчасhermes.observer.v1middleware_schema_version: сейчасhermes.middleware.v1- Контекст выполнения —
session_id,task_id,turn_id,api_request_id,provider,model,api_mode,tool_nameиtool_call_id, когда они применимы.
Поддерживаемые виды middleware:
| Вид | Полезная нагрузка | Форма возврата | Назначение |
|---|---|---|---|
llm_request | request, original_request | {"request": {...}} | Заменить действующие kwargs провайдера до выполнения запроса к провайдеру. |
tool_request | tool_name, args, original_args | {"args": {...}} | Заменить действующие аргументы инструмента до хуков, guardrails, подтверждений и выполнения. |
llm_execution | request, original_request, next_call | Любой ответ провайдера | Обернуть или заменить реальный вызов провайдера. |
tool_execution | tool_name, args, original_args, next_call | Любой результат инструмента | Обернуть или заменить реальный вызов инструмента. |
Middleware запроса может вернуть необязательные поля трассировки:
return {
"request": updated_request,
"source": "my-plugin",
"reason": "selected fallback model",
}
Hermes сохраняет эти записи трассы в полезных нагрузках последующих
хуков-наблюдателей как middleware_trace.
Middleware исполнения получает колбэк next_call. Вызовите его, чтобы продолжить
цепочку:
def on_tool_execution(**kwargs):
result = kwargs["next_call"](kwargs["args"])
return result
Если один и тот же вид middleware исполнения регистрируют несколько плагинов, Hermes выполняет их как вложенную цепочку в порядке регистрации. Сбои middleware не блокируют работу (fail-open): Hermes пишет предупреждение в лог и переходит к следующему middleware или к базовому пути рантайма. Колбэк, который падает одинаково на каждом вызове (как правило, из-за сигнатуры с именем поля, которое middleware не передаёт), сообщается один раз на уровне WARNING — сообщение перечисляет поля, которые middleware действительно передаёт, — а одинаковые повторы уходят в DEBUG, так что неверно объявленный middleware не может завалить лог; перезагрузка плагина сбрасывает этот отчёт.
Порядок выполнения
LLM-вызовы
Для каждого запроса к провайдеру Hermes применяет middleware в таком порядке:
- Собирает kwargs провайдера из текущей беседы.
- Применяет middleware
llm_request. - Испускает хуки-наблюдатели
pre_api_requestс действующим запросом. - Выполняет запрос к провайдеру через middleware
llm_execution. - Испускает хуки-наблюдатели
post_api_requestилиapi_request_error.
Middleware запроса видит полные kwargs провайдера, включая messages или input
Responses API, настройки модели, определения инструментов, параметры стриминга и
специфичные для провайдера опции. Middleware исполнения получает тот же действующий
запрос плюс next_call.
Вызовы инструментов
Для каждого вызова инструмента Hermes применяет middleware в таком порядке:
- Разбирает и приводит к нужным типам аргументы инструмента, переданные моделью.
- Применяет middleware
tool_request. - Прогоняет обычный предварительный путь Hermes по действующим аргументам: проверки доступности инструмента, блокирующие директивы наблюдателей, guardrails и проверки подтверждений.
- Выполняет инструмент через middleware
tool_execution. - Испускает хуки-наблюдатели
post_tool_call. - Применяет хуки
transform_tool_resultдо того, как результат будет добавлен обратно в контекст беседы.
Middleware запроса инструмента выполняется до проверок подтверждений. Обращайтесь с ним осторожно: переписанный путь, команда или URL — это и есть значение, которое будет оценивать политика ниже по цепочке.
Включение
Middleware работает только для включённых плагинов. Для встроенного плагина:
hermes plugins enable <plugin-name>
Для изолированного локального тестирования используйте один HERMES_HOME и для
включения плагина, и для запуска агента:
export HERMES_HOME=$HOME/.hermes/cache/scratch/hermes-middleware-test
mkdir -p "$HERMES_HOME"
hermes plugins enable <plugin-name>
hermes chat --query 'Reply exactly ok'
Для checkout из исходников используйте рабочий процесс разработчика в PM и отдельный домашний каталог для разработки, чтобы рантайм видел плагины и middleware из рабочего дерева:
export HERMES_HOME="$HOME/hermes-middleware-test"
export HERMES_RUNTIME_DIR="$HERMES_HOME/tools"
source ./activate
python hermes plugins enable <plugin-name>
python hermes chat --query 'Reply exactly ok'
Общие примеры плагинов
Примеры ниже намеренно маленькие. Они показывают форму контракта middleware без зависимости от NeMo Relay.
Middleware LLM-запроса
Этот плагин помечает запросы к провайдеру и записывает запись в трассу middleware:
def register(ctx):
ctx.register_middleware("llm_request", tag_llm_request)
def tag_llm_request(**kwargs):
request = dict(kwargs["request"])
extra_body = dict(request.get("extra_body") or {})
extra_body.setdefault("metadata", {})["hermes_middleware_demo"] = True
request["extra_body"] = extra_body
return {
"request": request,
"source": "middleware-demo",
"reason": "tagged provider request",
}
Действующий запрос передаётся в pre_api_request, на выполнение у провайдера и в
post_api_request.
Middleware запроса инструмента
Этот плагин ограничивает вызовы terminal известным рабочим каталогом:
from pathlib import Path
def register(ctx):
ctx.register_middleware("tool_request", normalize_terminal_workdir)
def normalize_terminal_workdir(**kwargs):
if kwargs.get("tool_name") != "terminal":
return None
args = dict(kwargs["args"])
args.setdefault("workdir", str(Path.home() / ".hermes" / "cache" / "scratch" / "hermes-middleware-demo"))
return {
"args": args,
"source": "middleware-demo",
"reason": "defaulted terminal workdir",
}
Поскольку он выполняется до хуков и подтверждений, телеметрия и политика ниже по
цепочке видят уже переписанный workdir.
Middleware исполнения LLM
Этот плагин оборачивает вызов провайдера и сохраняет сырой ответ провайдера:
import time
def register(ctx):
ctx.register_middleware("llm_execution", time_llm_execution)
def time_llm_execution(**kwargs):
started = time.monotonic()
response = kwargs["next_call"](kwargs["request"])
elapsed_ms = int((time.monotonic() - started) * 1000)
print(f"llm_execution elapsed_ms={elapsed_ms}")
return response
Возвращайте ответ той же формы, какую Hermes ожидает от адаптера провайдера. Не заворачивайте ответ в конверт, специфичный для плагина, если остальной рантайм такого конверта не ожидает.
Middleware исполнения инструмента
Этот плагин оборачивает выполнение инструмента, сохраняя его результат:
def register(ctx):
ctx.register_middleware("tool_execution", annotate_tool_execution)
def annotate_tool_execution(**kwargs):
result = kwargs["next_call"](kwargs["args"])
# Здесь можно собирать метрики, логировать или маршрутизировать во внешние системы.
return result
Middleware исполнения может вызвать next_call(modified_args), чтобы передать
изменённую полезную нагрузку следующим middleware и базовому диспетчеру
инструментов.
Примеры, специфичные для плагина, должны жить рядом с плагином, который владеет
этим поведением. Middleware исполнения NeMo Relay устанавливается через найденную
пользовательскую и системную конфигурацию Relay либо через явный plugins.toml,
выбранный переменной HERMES_NEMO_RELAY_PLUGINS_TOML; см.
Общие метрики Relay.
Замечания по безопасности
- Middleware должен быть детерминированным для одного и того же входа, если только он явно не маршрутизирует в динамическую внешнюю систему.
- Middleware запроса должен возвращать полную замену полезной нагрузки, а не частичные патчи.
- Middleware исполнения должен вызывать
next_call(...)ровно один раз, если только он намеренно не обрывает выполнение. - Если middleware исполнения бросает исключение до вызова
next_call(...), Hermes считает это сбоем middleware и продолжает с оставшейся цепочкой middleware и базовым выполнением. - Если middleware исполнения успешно вызвал
next_call(...), а затем упал на постобработке, Hermes сохраняет результат, полученный ниже по цепочке, и не запускает провайдера или инструмент второй раз. - Если ниже по цепочке падает провайдер или инструмент, middleware может
пропустить эту ошибку дальше или намеренно преобразовать её. Hermes не превращает
сбой ниже по цепочке в успешный результат
None. - Middleware запроса инструмента выполняется до подтверждений. Если он изменяет пути к файлам, команды, URL или аргументы, именно изменённые значения оценивают guardrails и подтверждения.
- Для телеметрии «только чтение» по-прежнему правильное место — хуки-наблюдатели. Используйте middleware только тогда, когда плагину нужно изменить или обернуть поведение.