跳转至

Hooks

钩子是执行循环上开放的插入点。参数对象就地可改,改完继续执行,因此既能观测也能干预。

from xun import Agent, Hooks

hooks = Hooks()
hooks.after_initialize.add(lambda a: print("initialized:", a.agent.name))
hooks.before_tool_call.add(lambda a: print([t.function.name for t in a.tool_calls]))

agent = Agent(name="HookAgent", hooks=hooks).initialize()

容器与注册表

每个钩子都是一个 HookRegistry:

方法 行为
add(fn) 常驻,每次触发都调用
add_once(fn) 只生效一次,触发后自动移除
invoke(args) 按注册顺序依次调用,然后把非常驻的丢掉
flowchart LR
    A["add(fn)"] --> L["回调列表<br/>persistent=True"]
    B["add_once(fn)"] --> L2["persistent=False"]
    L --> I["invoke(args)"]
    L2 --> I
    I --> K["调用全部,然后只保留常驻项"]

钩子异常被吞掉

每个回调都被 except_safe 包装:抛出的异常会转成 Result 后被丢弃,执行继续。 要在钩子里让错误可见,请自己 agent.error(...) 或抛 CancelledError / KeyboardInterrupt(这两个会正常传播)。 另外没有移除接口——常驻钩子无法撤销,一次性需求用 add_once,或者自建一个 Hooks 容器。

十七个钩子

钩子 参数类 字段 时机
run_start RunArgs agent 最外层 run_scope() 进入时(嵌套执行不触发)
run_end RunArgs agent 对应的作用域退出时,无论成功、出错还是取消
before_execution BeforeExecutionArgs agent, schema, max_iterations, context_value, takeover_result 一轮执行开始前
before_command CommandArgs agent, command, arguments 命令解析成功之后(未知命令不触发)
after_command CommandArgs 同上 命令执行之后,成功或被处理的错误都算
before_execution_step BeforeExecutionStepArgs agent 每轮模型调用前;自动压缩挂在这里
after_execution_step AfterExecutionStepArgs agent 工具结果写入会话后、下一轮模型调用前
before_tool_call BeforeToolCallArgs agent, tool_calls 一批工具调用执行前,列表可改
after_tool_call AfterToolCallArgs agent, tool_results 之后,(tool_id, Result) 列表可改
after_initialize AfterInitializeArgs agent initialize() 末尾,此时配置已完整
before_finalize before_finalize agent finalize() 开头,适合做资源收尾
model_text_delta TextDelta agent, model_call_id, content 正文增量落地前,可改 content
model_reasoning_delta TextDelta 同上 推理增量
completion_token_update TokenMetrics model_call_id, completion_tokens, prompt_tokens, total_tokens, prompt_tokens_cached 每次模型调用结束,字段只读,主要给统计用
before_display_info AgentMessageArgs agent, message agent.info() 发事件前,message 可改
before_display_warning AgentMessageArgs 同上 agent.warning()
before_display_error AgentMessageArgs 同上 agent.error()

干预执行

改参数

before_execution 收到的就是执行参数本身,就地改即可:

from xun import HookArgs

def limit(args: HookArgs.BeforeExecutionArgs) -> None:
    args.max_iterations = 20
    args.context_value = {"budget": 100}

agent.hooks.before_execution.add(limit)

直接接管一轮执行

def stub(args: HookArgs.BeforeExecutionArgs) -> None:
    args.takeover_result = "缓存命中,本轮不必调用模型"

agent.hooks.before_execution.add(stub)

设置 takeover_result 后:不调用模型、不再触发执行内部的其他钩子与事件、不写入会话历史; 若本轮带 schema,接管内容同样会按 schema 校验。

否决本轮执行

钩子里抛异常会被吞掉,想真正拦住执行请抛取消:

from xun import CancelledError, HookArgs

def budget_guard(args: HookArgs.BeforeExecutionArgs) -> None:
    if over_budget():
        raise CancelledError("预算用尽")

agent.hooks.before_execution.add(budget_guard)

给输出加过滤

agent.hooks.before_display_info.add(
    lambda a: setattr(a, "message", a.message.replace("TODO", "待办"))
)

用完即走

def once_after_write(args: HookArgs.AfterToolCallArgs) -> None:
    agent.display_event(HTMLInfoEvent(html="<em>已写入,记得复核</em>"))

agent.hooks.after_tool_call.add_once(once_after_write)

自带钩子的组件

  • AutoCompactor 在 initialize() 时把自己挂到 before_execution_step——替换 agent.compactor 即可换策略。
  • 子智能体任务池在 before_finalize 上注册收尾,父智能体终结时顺带回收后台任务。
  • 显示层扩展(例如按 Web 显示才生效的日志扩展)通常挂 before_display_*,见 插件开发指南。

自定义 Hooks 容器

Hooks 是个 dataclass,可以自建一个只装需要钩子的容器再传给 Agent(hooks=…); 不传就用默认的 Hooks()。Agent.inherit 不继承 hooks, 子智能体总是从空容器开始,这避免父钩子被隐式放大到整棵子树。