钩子与会话命令¶
钩子¶
Hooks 是一组 HookRegistry,注册在 agent.hooks.<name> 上:
from xun import Agent, HookArgs
def log_tool_call(args: HookArgs.BeforeToolCallArgs) -> None:
for call in args.tool_calls:
print(call.function.name, call.function.arguments)
agent = Agent()
agent.hooks.before_tool_call.add(log_tool_call)
注册表行为:
| 方法 | 语义 |
|---|---|
add(fn) |
持久回调,按注册顺序执行 |
add_once(fn) |
下一次 invoke 之后自动移除 |
invoke(args) |
依次调用全部回调,然后清掉一次性回调 |
回调返回值被丢弃,要改行为请就地修改参数对象。回调同样被 except_safe 包裹:普通异常被吞成 Result.Err(不会打断执行),只有 KeyboardInterrupt / CancelledError 会上抛。
钩子清单¶
| 钩子 | 参数(主要字段) | 时机 / 可改动内容 |
|---|---|---|
run_start |
agent |
由空闲转入运行时(嵌套作用域不重复触发) |
run_end |
agent |
真正触发过 run_start 的最外层作用域退出时,无论成功、报错还是取消 |
before_execution |
agent、schema、max_iterations、context_value、takeover_result |
执行循环开始前,字段都可改;takeover_result 可整体接管循环,见下 |
before_execution_step |
agent |
每轮模型调用前;自动压缩器挂在这里 |
after_execution_step |
agent |
工具结果已写回会话、下一轮模型调用之前 |
before_tool_call |
agent、tool_calls(列表) |
工具执行前,可增删改工具调用 |
after_tool_call |
agent、tool_results((id, Result) 列表) |
本轮有工具调用时,在全部工具执行完、写回会话前;改 tool_results 只影响写进会话的内容,结果的显示事件已先行发出 |
model_text_delta |
agent、model_call_id、content |
文本增量落地前,可改写 content |
model_reasoning_delta |
同上 | 推理增量落地前 |
before_display_info |
agent、message |
agent.info() 输出前,可改写 message |
before_display_warning |
同上 | agent.warning() 输出前 |
before_display_error |
同上 | agent.error() 输出前 |
after_initialize |
agent |
initialize() 最后一步(扩展、模型探测、显示绑定都已完成) |
before_finalize |
agent |
finalize() 开始处,适合释放资源 |
before_command |
agent、command、arguments(列表) |
命令解析成功之后、执行之前,arguments 可就地改;未知命令不触发 |
after_command |
同上 | 命令执行结束(含被处理的错误) |
接管执行循环¶
before_execution 的参数带一个 takeover_result(默认 None)。钩子把它设成字符串,本次 execute() 即被钩子接管:
def canned(args: HookArgs.BeforeExecutionArgs) -> None:
if cached := args.agent.state.get("cached_answer"):
args.takeover_result = cached # 直接作为 execute() 的结果
agent.hooks.before_execution.add(canned)
效果是跳过整个循环:不发模型调用、不再触发循环内的钩子与事件、不写入会话历史。该字符串就是 execute() 的返回值;若 execute(schema=...) 给了 schema,它仍会按 schema 校验,校验不过则整次运行失败。适合结果缓存、离线回放与测试替身。
常见用法:把工具调用镜像到日志、给输出加前缀、在 before_execution 里统一改 max_iterations 或命中缓存、在 before_finalize 里关闭外部资源(浏览器运行时就是这么清理的)。
会话命令¶
命令用于人机交互入口(终端 REPL、Web 界面的 / 输入框)。Agent.execute_command() 是统一入口:先 shlex.split 解析参数、触发 before_command,再执行,最后 after_command。
from xun import Agent, Command, CommandRegistry
def wordcount(agent: Agent, args: list[str]) -> None:
"""Count words in the given text."""
agent.info(f"{len(' '.join(args).split())} words")
agent.command.register(Command("wordcount", wordcount))
agent.command.register(Command.from_function(wordcount)) # 名称取函数名
agent.execute_command("wordcount", "one two three")
注册规则:
| 要点 | 说明 |
|---|---|
| 处理函数入参 | 1 个参数 → handler(agent);2 个 → handler(agent, arguments);否则 TypeError |
| 描述 | 默认取 docstring 首行;多行 docstring 会作为 -h 的长帮助 |
| 子命令组 | 把 CommandRegistry 作为 handler 传入即成为命令组,cmd sub -h 自动可用 |
| 帮助 | 参数恰好是 -h 或 --help 时显示帮助(cmd sub extra -h 会把 -h 当普通参数交给命令);help 为虚拟命令,始终可用 |
| 错误 | 命令内异常被捕获为 agent.error(...),KeyboardInterrupt / CancelledError 上抛 |
| 转义 | 输入以 / 开头即视为命令;\/ 开头表示普通消息 |
内置命令清单及说明见 xun · 终端会话,策略子命令组(policy ...)由 default_tools=True 时注册。
钩子不进子智能体,命令会被复制
Agent.inherit() 不继承 hooks,但会复制 command;子智能体在 initialize() 时会重放扩展,从而重新注册钩子。
会话历史的落盘¶
/save、/load 背后是 Store:
from xun.store import Store
store = Store() # 默认根目录 get_home_dir()
target = store.next_history_store() # .../conversation/000007.conversation
latest = store.latest_history_store()
one = store.get_history_store(3) # int 会补零:.../conversation/000003.conversation
two = store.get_history_store("000003") # 字符串按原样拼接,必须与目录名一致
/load 的编号要补零
/load 把参数当字符串处理,所以 /load 000003 能命中,/load 3 会报 History 3 not found.;拿不准编号时用 /load latest。
实际文件为 {XUN_HOME}/conversation/{NNNNNN}.conversation/conversation.json,键包括 conversation_id、time、tokens_used、messages、compacted_toolcalls、compaction。