跳转至

链式取消

取消是协作式的:框架负责传播标志,代码自己负责在合适的点检查。 取消事件组成一条链,父智能体取消会级联到所有子智能体,反过来不会。

API

成员 说明
agent.cancel_event ChainedEvent,event + parent 两段
ChainedEvent.is_set() 递归看自己或任一祖先是否被置位
ChainedEvent.derive() 派生一个以自己为父的新事件
agent.cancel() 置位;智能体空闲时返回 False 且不置位,避免取消信号滞留
agent.check_cancel() 已置位则抛 CancelledError,自带默认原因
agent.is_running 是否处在 run_scope() 中
agent.run_scope() 上下文管理器:跟踪运行状态、进出各查一次取消、把 Ctrl+C 转成取消、退出时清空事件
flowchart TD
    P["父 cancel_event"] -->|derive| C1["子 1 cancel_event"]
    P -->|derive| C2["子 2 cancel_event"]
    P --> X["cancel()"]
    X -.级联.-> C1
    X -.级联.-> C2
    C1 -.不会反向影响.-> P

长任务里的检查点

工具是同步函数,长循环必须自己留检查点,否则取消要等到函数返回才生效:

import time

def long_running_task(ctx):
    for i in range(10):
        print(f"step {i + 1}/10")
        time.sleep(1)
        ctx.agent.check_cancel()      # 检查点

框架内部的检查点已经铺好:每轮迭代、模型流的每个数据块、每个工具调用之前都会查; 内置工具里 sleep、bash / bash_wait 等也各自调了 check_cancel()。

传播规则

Agent.inherit(parent) 默认把子智能体的 cancel_event.parent 指向父事件,于是:

  • 父 cancel() → 子的 check_cancel() 立刻抛;
  • 子 cancel() → 只影响子自己与它的后代。

自己创建的子智能体必须把链接上,两种写法等价:

child = Agent.inherit(parent)                       # 自动挂链
child = Agent(display=..., cancel_event=parent.cancel_event.derive())

Web 端等待用户回答提示时同样响应取消:待答提示会带一个 cancel_check, 一旦取消链被置位就丢弃该提示并广播解除,父智能体不会卡死在等人上。

触发取消

入口 方式
终端 Ctrl+C:run_scope() 把 KeyboardInterrupt 转成取消并继续抛出;交互层会弹出最后一条用户消息,方便 /retry
Web 停止按钮发一条 cancel 消息,服务端按 agent_id 调 cancel();空闲时忽略
程序内 任意线程调 agent.cancel()
后台子任务 没有单独取消工具;靠取消链级联,或等父智能体终结时由任务池收尾

CancelledError 带原因

from xun import CancelledError

try:
    agent.instruct("…").execute()
except CancelledError as e:
    print(e.reason)      # 例如 “Operation cancelled by user.” 或自定义原因

execute() 会先把原因通过 agent.error() 报给显示层,再原样抛出—— CancelledError 与 KeyboardInterrupt 不会被包成 Result.Err,这一点与其他异常不同。 想在钩子里否决一轮执行,抛带自定义原因的 CancelledError 是唯一可靠的办法。

收尾顺序

run_scope() 退出的 finally 里会清掉取消事件,以免下一轮一进来就被取消。 因此后台派生的子智能体不能只靠事件链收尾——agent_wait 与任务池会额外直接调用 child.cancel(), 确保父智能体结束后子智能体不会继续跑模型调用。