Session 的输入主循环保证的是“控制操作按 Submission 到达顺序分派”,不是“整个 Agent 一次只能做一件异步工作”。用户 Turn 会被放到独立 Tokio 任务里执行,主循环立即回来继续接收 Interrupt、审批结果和 Steer。
Submission 是控制信封
每条 Submission 除 Op 外还有 id、可选 client user message id、W3C trace context 和 parent turn id。主循环从有界 channel 逐项 recv,为每个操作建立 dispatch span,然后按 Op 分类调用 handler。
操作大致分成五组:
- Turn 控制:UserInput、Interrupt、Compact、Review、UserShell;
- 同步回填:Exec/MCP 审批、RequestUserInput、Permissions、DynamicToolResponse;
- 配置维护:ThreadSettings、Reload、RefreshMcpServers、技能和插件刷新;
- Realtime:Start、Audio、Text、Speech、Close、ListVoices;
- 终止:Shutdown 及连接关闭后的隐式 teardown。
async def submission_loop(session, rx):
while submission := await rx.recv():
with dispatch_span(submission.trace, submission.op):
match submission.op:
case UserInput(data):
await user_input_or_turn(session, submission.id, data)
case Interrupt():
await session.abort_all_tasks(INTERRUPTED)
case Approval(reply):
await resolve_turn_waiter(session, reply.call_id, reply)
case Shutdown():
await shutdown_session_runtime(session)
break
case op:
await dispatch_other_op(session, submission, op)
else:
await shutdown_session_runtime(session)
await emit_thread_stop_and_close_live_thread(session)
Rust 的 Op 标记为 non-exhaustive,循环对未来未知变体保留忽略分支。这不是网络协议的普遍兼容保证,而是 Core 内部在版本演进中避免旧分派器崩溃的防御;App Server 的 JSON-RPC 层仍有自己的 method/version 校验。
为什么不能在主循环内 await 整个 Turn
如果 UserInput handler 直接等待 run_turn 返回,模型请求、工具执行或用户审批可能持续数分钟。此时同一 receiver 中的 Interrupt 永远无法被处理,形成“只有任务自己结束才能中断任务”的死锁式设计。
当前实现只在 handler 中完成 TurnContext 构造和 start_task 登记:
async def user_input_or_turn(session, submission):
candidate = await session.new_turn_with_sub_id(submission.id, submission.settings)
steered = await session.steer_input(candidate.input, submission.expected_turn_id)
if steered.ok:
return
if steered.reason == NO_ACTIVE_TURN:
await session.start_task(candidate, RegularTask())
start_task 内部 tokio::spawn 后便返回。任务结果由统一完成处理器收口,主循环不参与采样细节。
串行分派仍可能启动并发工作
刷新 MCP、网络代理预热、工具执行和 Realtime fanout 都可能派生后台任务。主循环保证 handler 的入口顺序,却不自动保证这些后台结果按提交顺序完成。需要严格次序的写入必须由 LiveThread、Rollout recorder 或具体服务另设队列和屏障。
这也是为什么“Submission A 先到,所以它产生的所有 Event 都先于 B”并不成立。成立的是 A 的 handler 先被调用;若 A 启动异步任务,B 的同步错误事件完全可能先出现。协议消费者应按 Turn/Submission/Call ID 关联,而不是只凭全局墙钟直觉。
通道关闭与显式 Shutdown 的共同收尾
显式 Shutdown handler 返回停止标志;receive 得到 channel closed 也跳出循环。两条路径最终都运行 session runtime teardown、ThreadStop lifecycle 和 LiveThread shutdown。测试确保隐式关闭不会漏掉活动 Turn。
评论
登录后即可评论