Hermes 中间件

Hermes 中间件是 observer hook 的、可改变行为的搭档。Observer hook 报告发生了什么;中间件能通过在执行前重写请求、或包裹执行回调本身来改变发生什么。

本契约刻意后端中立。插件可用它做本地策略、请求塑形、追踪、自适应路由、缓存控制、沙箱选择,或交接给 NeMo Relay 等运行时,而无需改动 Hermes 的规划器、模型 provider 适配器、工具注册表、记忆或 CLI UX。

启用中间件后,插件可以:

  • 在 Hermes 调用 provider 之前重写 LLM provider 请求 kwargs。
  • 在护栏、审批检查、hook 与工具执行看到工具参数之前重写它们。
  • 包裹真实的 LLM 执行回调,同时保留 Hermes 的重试、流式、中断与 hook 行为。
  • 包裹真实的工具执行回调,同时保留 Hermes 的护栏、审批、工具后 hook 与工具结果变换。

契约

插件从 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)

每个中间件回调收到:

  • 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。

支持的中间件种类:

种类载荷返回形状用途
llm_requestrequest、original_request{"request": {...}}在 provider 执行前替换生效的 provider kwargs。
tool_requesttool_name、args、original_args{"args": {...}}在 hook、护栏、审批与执行之前替换生效的工具参数。
llm_executionrequest、original_request、next_call任意 provider 响应包裹或替换真实的 provider 调用。
tool_executiontool_name、args、original_args、next_call任意工具结果包裹或替换真实的工具调用。

请求中间件可返回可选的追踪字段:

return {
    "request": updated_request,
    "source": "my-plugin",
    "reason": "selected fallback model",
}

Hermes 把这些追踪条目作为 middleware_trace 存入后续 observer hook 载荷。

执行中间件收到一个 next_call 回调。调用它以继续链条:

def on_tool_execution(**kwargs):
    result = kwargs["next_call"](kwargs["args"])
    return result

若多个插件注册同一种执行中间件,Hermes 按注册顺序把它们跑成嵌套链。中间件失败是 fail-open:Hermes 记一条 warning,继续下一个中间件或基础运行时路径。一个每次调用都以同样方式失败的回调(通常是签名命名了中间件不发送的字段)会在 WARNING 报一次——消息列出它确实提供的字段——相同重复进 DEBUG,因此一个错误声明的中间件不会刷爆日志;插件重载会重置该报告。

执行顺序

LLM 调用

对每个 provider 请求,Hermes 按此顺序应用中间件:

  1. 从当前对话构建 provider kwargs。
  2. 应用 llm_request 中间件。
  3. 以生效请求发出 pre_api_request observer hook。
  4. 经 llm_execution 中间件运行 provider 执行。
  5. 发出 post_api_request 或 api_request_error observer hook。

请求中间件看到完整 provider kwargs,包括 messages 或 Responses API input、模型设置、工具定义、流选项与 provider 专属选项。执行中间件收到同一生效请求外加 next_call。

工具调用

对每次工具调用,Hermes 按此顺序应用中间件:

  1. 解析并强转模型给出的工具参数。
  2. 应用 tool_request 中间件。
  3. 对生效参数跑正常的 Hermes 执行前路径:工具可用性检查、observer 块指令、护栏与审批检查。
  4. 经 tool_execution 中间件运行工具执行。
  5. 发出 post_tool_call observer hook。
  6. 在结果追加回对话上下文之前应用 transform_tool_result hook。

工具请求中间件在审批检查之前运行。谨慎使用它:被重写的路径、命令或 URL 就是下游策略将要评估的值。

启用

中间件只对已启用插件运行。对内置插件:

hermes plugins enable <plugin-name>

隔离本地测试时,用同一个 HERMES_HOME 做插件启用与 agent 运行:

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 开发者工作流 和一个独立的开发 home,让运行时从工作树看到插件与中间件:

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'

通用插件示例

下面的示例刻意很小。它们展示中间件契约形状,不依赖 NeMo Relay。

LLM 请求中间件

这个插件给 provider 请求打标签并记录一条中间件追踪条目:

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、provider 执行与 post_api_request。

工具请求中间件

这个插件把 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",
    }

因为这在 hook 与审批之前运行,下游遥测与策略观察到的是被重写的 workdir。

LLM 执行中间件

这个插件包裹 provider 调用并保留原始 provider 响应:

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 从 provider 适配器期望的同一响应形状。不要把响应包进插件专属信封,除非运行时其余部分期望那个信封。

工具执行中间件

这个插件包裹工具执行,同时保留工具结果:

def register(ctx):
    ctx.register_middleware("tool_execution", annotate_tool_execution)


def annotate_tool_execution(**kwargs):
    result = kwargs["next_call"](kwargs["args"])
    # 指标、日志或外部路由可在此发生。
    return result

执行中间件可调 next_call(modified_args),把改变后的载荷传给后续中间件与基础工具分发器。

插件专属示例应与拥有该行为的插件放在一起。NeMo Relay 执行中间件经 Relay 的发现式用户与系统配置安装,或经 HERMES_NEMO_RELAY_PLUGINS_TOML 选择的显式 plugins.toml;见Relay 共享指标。

安全注意

  • 除非明确路由到动态外部系统,中间件应对同一输入确定性。
  • 请求中间件应返回完整替换载荷,而非部分补丁。
  • 执行中间件应恰好调一次 next_call(...),除非它有意短路执行。
  • 若执行中间件在调 next_call(...) 之前抛错,Hermes 视其为中间件失败,继续其余中间件链与基础执行。
  • 若执行中间件成功调了 next_call(...) 然后在后置处理中抛错,Hermes 保留下游结果,不二次运行 provider 或工具。
  • 若下游 provider 或工具执行失败,中间件可让该错误传播或有意翻译它。Hermes 不把下游失败转成成功的 None 结果。
  • 工具请求中间件在审批之前运行。若它改动文件路径、命令、URL 或参数,被改动的值就是护栏与审批评估的对象。
  • Observer hook 仍是只读遥测的正确位置。仅当插件需要改动或包裹行为时才用中间件。