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_request | provider 请求发出之前 | api_request_id、session_id、task_id、provider、model、api_mode、request |
post_api_request | provider 响应成功之后 | api_request_id、session_id、task_id、provider、model、api_mode、request、response、usage、duration_ms、middleware_trace |
api_request_error | provider 请求失败时 | 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_end | agent/runner_state.py |
pre_api_request | agent/provider_telemetry.py |
post_api_request / api_request_error | agent/provider_telemetry.py |
pre_tool_call | tools/registry.py |
post_tool_call | tools/registry.py |
memory_write | hermes_cli/skill_sync_hook.py |
turn_end | agent/session_turn.py |
session_event | gateway/session.py |
Observer 与 Middleware 的区别
| Observer Hook | Middleware | |
|---|---|---|
| 目的 | 只读遥测 | 改变/包裹行为 |
能否改 request/args/response | 否 | 是 |
| 能否短路执行 | 否 | 是(可选) |
| 异常影响 agent | 否(捕获) | 是(返回时的错误) |
| 典型用途 | 日志、指标、追踪 | 路由、策略、缓存控制、外部执行 |
规则:默认用 observer 做遥测。仅当你需要改变或包裹执行路径时才用中间件。
安全注意
- Hook 载荷可含敏感数据(消息文本、工具参数)——把它们推给外部系统时小心。
- Hook 在 Hermes 的安全边界外运行;它们像任何其他插件代码一样继承插件的信任。
- 不要在 observer hook 中做长阻塞工作——它们在热路径上运行,且慢 hook 会累积 executor 队列(派发是非阻塞的,但太多挂起的 hook 会累积内存)。把重活交给后台线程。