链式取消¶
取消是协作式的:框架负责传播标志,代码自己负责在合适的点检查。 取消事件组成一条链,父智能体取消会级联到所有子智能体,反过来不会。
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(),
确保父智能体结束后子智能体不会继续跑模型调用。