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.v1
  • middleware_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_requestrequest, original_request{"request": {...}}Заменить действующие kwargs провайдера до выполнения запроса к провайдеру.
tool_requesttool_name, args, original_args{"args": {...}}Заменить действующие аргументы инструмента до хуков, guardrails, подтверждений и выполнения.
llm_executionrequest, original_request, next_callЛюбой ответ провайдераОбернуть или заменить реальный вызов провайдера.
tool_executiontool_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 в таком порядке:

  1. Собирает kwargs провайдера из текущей беседы.
  2. Применяет middleware llm_request.
  3. Испускает хуки-наблюдатели pre_api_request с действующим запросом.
  4. Выполняет запрос к провайдеру через middleware llm_execution.
  5. Испускает хуки-наблюдатели post_api_request или api_request_error.

Middleware запроса видит полные kwargs провайдера, включая messages или input Responses API, настройки модели, определения инструментов, параметры стриминга и специфичные для провайдера опции. Middleware исполнения получает тот же действующий запрос плюс next_call.

Вызовы инструментов

Для каждого вызова инструмента Hermes применяет middleware в таком порядке:

  1. Разбирает и приводит к нужным типам аргументы инструмента, переданные моделью.
  2. Применяет middleware tool_request.
  3. Прогоняет обычный предварительный путь Hermes по действующим аргументам: проверки доступности инструмента, блокирующие директивы наблюдателей, guardrails и проверки подтверждений.
  4. Выполняет инструмент через middleware tool_execution.
  5. Испускает хуки-наблюдатели post_tool_call.
  6. Применяет хуки 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 только тогда, когда плагину нужно изменить или обернуть поведение.
перевод и редактура:

обновлено

⌕ ESC