Cancellation Propagation
Cancellation Propagation 是服务端接受显式 CancelRun 后,将取消意图传播到实际执行链路的协作机制;它必须与浏览器断线/request context 取消分离。
[!info] related notes
- 所属 MOC: AI Agent Application MOC, Agent Runtime MOC
- 相关: Cancellation, Abort Generation
- 浏览器边界: AbortController 与异步取消
- Go 取消原语: Go Context
Cancellation Propagation
一句话定义
Cancellation Propagation 是在服务端已经确认“这个 Run 应该被取消”之后,把 cancellation signal 逐层传播给正在运行的 LLM、Agent loop、tool 和资源清理逻辑。
它解决的是“取消意图怎样真正停止工作”,而不是“如何根据网络断线猜用户想取消”。
两条链必须分开
Transport disconnect
Browser network lost / reader aborted
→ HTTP/SSE transport ends
→ request context may be cancelled
这只说明连接生命周期结束,不自动代表:
Run should be cancelled
对于 detached/durable Agent execution,正确策略可能是让 Run 继续并允许后续 recovery。
Explicit business cancellation
User clicks Stop
→ CancelRun(run_id) command
→ server validates authority and current Run state
→ durable transition to cancelled
→ propagate cancellation to live execution
只有第二条链才是业务取消传播的起点。
Propagation 的典型结构
CancelRun accepted
↓
Run cancellation registry / execution handle
↓
context.CancelFunc / cancellation token
↓
┌──────────────┬──────────────┬──────────────┐
↓ ↓ ↓
LLM stream Agent loop Tool execution
stop reading stop stepping cooperative abort
└──────────────┴──────────────┴──────────────┘
↓
cleanup / terminal reconciliation
核心是 cooperative cancellation:每个下游边界都要支持 signal 或提供可取消 API。
Go Context 的角色
Go context.Context 很适合承载 cancellation signal:
executionCtx, cancel := context.WithCancel(parent)
defer cancel()
// 注册到 run_id 对应的 active execution
registerRunCancellation(runID, cancel)
显式 CancelRun 时,通过 run_id 找到对应 cancel function:
CancelRun(R7)
→ lookup active execution R7
→ cancel()
之后下游通过:
select {
case <-ctx.Done():
// cooperative stop
default:
}
或把 ctx 继续传给支持取消的 HTTP/DB/LLM client。
关键边界:request context 可以被用于普通请求生命周期,但 durable Run 不应仅因浏览器 transport 消失就自动绑定成业务取消。
Python / Agent Runtime 边界
Go 传播 cancellation 后,Python/Agent runtime 仍需要真正支持停止:
cancel signal
→ stop scheduling next Agent step
→ stop/abort LLM stream where provider supports it
→ attempt to cancel cancellable tool calls
→ avoid emitting new non-terminal business work after cancellation
如果某个外部工具不支持取消,系统只能停止等待/停止后续步骤,或等待该操作自然完成;不能承诺任意 side effect 都能瞬间回滚。
State Transition 与 Execution Stop 是两个问题
一个可靠取消通常同时包含:
1. Authority/state:
Run durable state becomes cancelled
2. Execution:
live computation receives cancellation signal
只做第 1 个:后台任务可能继续浪费资源。
只做第 2 个:数据库/其他客户端不知道为什么 Run 停了,也缺少可恢复的 terminal fact。
因此两者需要协作。
BodySense 当前实践
BodySense 当前使用独立的 CancelRun 路径,而不是把 SSE disconnect 当业务取消:
POST /consultation-runs/:id/cancel
→ Runtime.CancelRun
→ RunService / RunRepository 做授权与状态转换
→ active/waiting_user → cancelled
→ registered execution cancellation is triggered where active
→ runtime emits/persists run.cancelled
相反,transport disconnect 后客户端通过 durable Runtime Event Log 做 recovery/catch-up。
主要代码边界:
apps/api/internal/consultation/runtime.go
apps/api/internal/service/run_service.go
apps/api/internal/repository/run_repository.go
常见坑
- 用
req.Context().Done()直接解释成“用户点击停止”。 - 服务端已经标 cancelled,但没有向实际执行传播 signal。
- 只停 LLM stream,却让正在运行的 tool/子任务继续创建新副作用。
- 把 cancellation 当成 rollback:已经提交到外部世界的副作用通常不能自动撤销。
- 不做幂等:重复 CancelRun 导致重复 cleanup/terminal event。
- Cancel 与 Complete race 没有原子状态裁决。
最短记忆
Disconnect
= transport fact
CancelRun
= business command
Cancellation propagation
= accepted cancel intent → active execution/resources
run.cancelled
= durable terminal fact