Observer Hooks

Observer hooks 是插件的只读遥测入口点。插件注册回调以观察运行事件(agent 启动/停止、LLM 请求/响应、工具调用、记忆写入……)并对它们做任何事——日志、指标、推送通知、外部追踪——而不改变 Hermes 的行为。观察者看到与 Hermes 内部相同的数据形状,但绝不修改它。

这与中间件互补。中间件包裹执行路径以改变请求、响应或参数;observer hook 只观察。

契约

插件从 register(ctx) 注册 observer hook:

def register(ctx):
    ctx.register_observer_hook("post_api_request", my_observer)

每个回调收到一个 hook 参数,载荷经 hook.payload 访问。

载荷形状是 {"event": ..., "data": ...}:event 是事件名,data 是事件专属字段的 dict(见下文)。

可用事件:

事件触发时机关键载荷字段(data 中)
agent_start一次 agent 运行开始时session_id、task_id、turn_id、run_start_time_ms
agent_end一次 agent 运行完成时session_id、task_id、turn_id、run_duration_ms、stop_reason
pre_api_requestprovider 请求发出之前api_request_id、session_id、task_id、provider、model、api_mode、request
post_api_requestprovider 响应成功之后api_request_id、session_id、task_id、provider、model、api_mode、request、response、usage、duration_ms、middleware_trace
api_request_errorprovider 请求失败时api_request_id、session_id、task_id、provider、model、api_mode、request、error、error_type、duration_ms
pre_tool_call工具执行之前session_id、task_id、tool_name、tool_args、tool_call_id
post_tool_call工具执行之后session_id、task_id、tool_name、tool_args、tool_result、tool_call_id、duration_ms、error、middleware_trace
memory_write记忆写入完成时session_id、task_id、memory_provider、memory_path、memory_name、action、size_bytes
turn_end一次用户轮次完成时session_id、turn_id、turn_start_time_ms、turn_end_time_ms、turn_duration_ms
session_event会话生命周期事件(created、switched、reset、resumed)session_id、session_key、event、timestamp

载荷元数据: 每个载荷含 telemetry_schema_version(当前 hermes.observer.v1)。post_api_request / api_request_error / post_tool_call 还含 middleware_trace——中间件产生的追踪条目。

它是只读的

关键不变量:观察者不可见于系统其余部分。 一个 observer hook 可以:

  • 记录事件。
  • 计算指标或计时。
  • 把数据推给外部系统。
  • 打印到 stderr。

它不可以:

  • 修改 request / response / tool_args / tool_result(hook.payload 是冻结的映射)。
  • 取消或短路执行(在 pre_* 事件上做不到;它已经在执行中)。
  • 影响其他 hook——每个 hook 独立运行。
  • 抛错从而破坏 Hermes——异常被捕获并记录。

若你的插件需要改变行为,用中间件。

生命周期与错误处理

  • Observer hook 在 agent 循环的热路径上触发。它们被包裹:hook 抛错会被捕获并记一条 warning,但不影响 agent 轮次。一个持续以同样方式失败的回调(通常因为它给它不发送的载荷取键)在 WARNING 上记一次——消息列出它确实收到的键——随后重复进 DEBUG,因此一个错误声明的 observer 不会刷爆日志;插件重载会重置该报告。
  • Hook 在 worker 线程中运行;它们不阻塞主循环(在热路径上,例如 post_tool_call,Hermes 经独立 executor 派发它们,因此慢 observer 不会放慢工具流)。
  • 顺序:插件按注册顺序被调用。

示例:一个最小 observer

def register(ctx):
    ctx.register_observer_hook("post_api_request", on_api)
    ctx.register_observer_hook("post_tool_call", on_tool)


def on_api(hook):
    p = hook.payload
    data = p["data"]
    print(
        f"[observer] {data['provider']}/{data['model']} "
        f"→ {data['usage']} in {data['duration_ms']}ms"
    )


def on_tool(hook):
    data = hook.payload["data"]
    print(f"[observer] tool={data['tool_name']} dur={data['duration_ms']}ms")

示例:一个运行持续时间 observer

def register(ctx):
    ctx.register_observer_hook("agent_start", on_start)
    ctx.register_observer_hook("agent_end", on_end)


def on_start(hook):
    data = hook.payload["data"]
    print(f"[run] start session={data['session_id']}")


def on_end(hook):
    data = hook.payload["data"]
    print(f"[run] end session={data['session_id']} "
          f"duration={data['run_duration_ms']}ms reason={data['stop_reason']}")

完整载荷形状参考

gateway 在每次事件触发时经 emit_observer_hooks(payload)(gateway/observer_hooks.py)派发。确切的载荷构造器见:

事件源位置
agent_start / agent_endagent/runner_state.py
pre_api_requestagent/provider_telemetry.py
post_api_request / api_request_erroragent/provider_telemetry.py
pre_tool_calltools/registry.py
post_tool_calltools/registry.py
memory_writehermes_cli/skill_sync_hook.py
turn_endagent/session_turn.py
session_eventgateway/session.py

Observer 与 Middleware 的区别

Observer HookMiddleware
目的只读遥测改变/包裹行为
能否改 request/args/response否是
能否短路执行否是(可选)
异常影响 agent否(捕获)是(返回时的错误)
典型用途日志、指标、追踪路由、策略、缓存控制、外部执行

规则:默认用 observer 做遥测。仅当你需要改变或包裹执行路径时才用中间件。

安全注意

  • Hook 载荷可含敏感数据(消息文本、工具参数)——把它们推给外部系统时小心。
  • Hook 在 Hermes 的安全边界外运行;它们像任何其他插件代码一样继承插件的信任。
  • 不要在 observer hook 中做长阻塞工作——它们在热路径上运行,且慢 hook 会累积 executor 队列(派发是非阻塞的,但太多挂起的 hook 会累积内存)。把重活交给后台线程。