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,
子智能体总是从空容器开始,这避免父钩子被隐式放大到整棵子树。