第3章:Thread 状态机 —— 双层循环与会话生命周期
在第 2 章中,我们看到 NativeAgentConnection 将所有业务请求直接委派给了原生状态机。在 Zed 源码中,这个核心引擎就是 crates/agent/src/thread.rs 中的 Thread。单文件近 9000 行的 thread.rs 是整个原生 Agent 的动力心脏。本章将逐层解构 Thread 的结构体字段设计、外层轮次循环与内层流式竞争的双层循环引擎、drop(events) 规避死锁的精妙构思,以及中途转向(Steering)机制。
一、开场:原生 Agent 的中枢
如果说 ACP 是 Zed Agent 对外展示的“外交礼仪”,那么 Thread 就是其内部真正统筹一切的“内阁中枢”。
在 crates/agent/src/thread.rs 中,Thread 承担了极其厚重的职责:
- 记忆管理者:维护全部历史消息、压缩摘要标记与实时 Token 消耗;
- 调度指挥官:管理大模型的单次调用与多次工具循环(Turn / Step);
- 工具兵工厂:注册、过滤并动态裁决 23 个内建工具的可用性;
- 安全与沙箱的持有者:维护当前线程专属的临时沙箱目录与“当前会话记忆”授权缓存;
- 子智能体的母体:维护派生子 Agent 的弱引用,负责父子生命周期的协同取消与配置同步。
二、Thread 结构解构:字段全景与职责分区
先看 Thread 结构体的完整定义:
rust
// crates/agent/src/thread.rs:1261-1310
pub struct Thread {
id: acp::SessionId,
prompt_id: PromptId,
updated_at: DateTime<Utc>,
title: Option<SharedString>,
pending_title_generation: Option<Task<()>>,
title_generation_error: Option<SharedString>,
pending_summary_generation: Option<Shared<Task<Option<SharedString>>>>,
summary: Option<SharedString>,
messages: Vec<Arc<Message>>,
user_store: Entity<UserStore>,
/// 核心运行态:持有当前正在执行轮次的后台 Task 与取消广播
running_turn: Option<RunningTurn>,
/// 中途引导标志位:为 true 时在下一个步骤边界优雅停止,而非跑完全部工具
end_turn_at_next_boundary: bool,
pending_message: Option<AgentMessage>,
/// 动态工具表:按名字索引的类型擦除工具实体
pub(crate) tools: BTreeMap<SharedString, Arc<dyn AnyAgentTool>>,
/// 上下文与 Token 遥测监控
request_token_usage: HashMap<ClientUserMessageId, language_model::TokenUsage>,
cumulative_token_usage: TokenUsage,
current_request_token_usage: TokenUsage,
pending_compaction_telemetry: Option<CompactionTelemetry>,
/// 外部与工程上下文
pub(crate) context_server_registry: Entity<ContextServerRegistry>, // MCP 动态服务
profile_id: AgentProfileId,
profile_downgraded_for_restricted_workspace: bool,
project_context: Entity<ProjectContext>, // 包含 AGENTS.md / rules
pub(crate) templates: Arc<Templates>, // 编译期 Handlebars 模板
/// 模型配置与参数
model: ThreadModel,
summarization_model: Option<Arc<dyn LanguageModel>>,
thinking_enabled: bool,
thinking_effort: Option<String>,
speed: Option<Speed>,
pub(crate) project: Entity<Project>,
pub(crate) action_log: Entity<ActionLog>, // 代码修改审计与撤销栈
/// 子 Agent 关联
subagent_context: Option<SubagentContext>,
running_subagents: Vec<WeakEntity<Thread>>,
inherits_parent_model_settings: bool,
/// 沙箱与安全授权(生命周期仅限于当前线程,从不落盘持久化!)
sandboxed_terminal_temp_dir: Option<PathBuf>,
sandbox_grants: Rc<RefCell<ThreadSandboxGrants>>,
}
【来源:crates/agent/src/thread.rs:1261-1310】
我们可以将这 30 多个字段清晰划分为五大职能区:
| 职能分区 | 代表字段 | 架构设计意图 |
|---|
| 标识与标题 | id, prompt_id, title, summary | 会话身份、生成标题任务与长会话滚动摘要 |
| 运行时驱动 | running_turn, end_turn_at_next_boundary, messages | 驱动当前异步执行任务,控制轮次生命周期 |
| 工具与扩展 | tools, context_server_registry, action_log | 23 个本地静态工具 + MCP 动态注入工具 + 编辑撤销日志 |
| 上下文与模型 | model, thinking_effort, templates, project_context | LLM 交互参数、Thinking 预算、Handlebars 模板、规则文件 |
| 安全与临时态 | sandboxed_terminal_temp_dir, sandbox_grants | 线程私有 $TMPDIR 与用户在当前对话中“总是允许”的内存临时授权 |
特别注意 sandbox_grants:
rust
/// Sandbox permissions the user approved "for the rest of the thread".
/// Never persisted — lives and dies with this thread.
sandbox_grants: Rc<RefCell<ThreadSandboxGrants>>,
在许多不良实现的 Agent 中,如果用户点击了“本会话总是允许访问某目录”,系统会粗暴地将其写入全局配置文件,造成权限越权泄漏。而在 Zed 中,会话级授权被严格约束在 Thread 实例的生命周期内,关闭会话即刻灰飞烟灭,绝不持久化到磁盘。
三、一次轮次的生命周期:从 send() 到 run_turn()
当用户或上层调用发起一次新的交互时,调用链如下展开:

一次 send() 的前半程:消息入列 → advance_prompt_id() 生成全局单调递增的 PromptId → run_turn()。真正的执行体是 cx.spawn 出去的后台异步任务,进任务前先清空残留缓冲区、取消上一次仍在飞的任务。
关键源码剖析:
rust
// crates/agent/src/thread.rs:2524-2555
pub fn send<T>(
&mut self,
id: ClientUserMessageId,
content: impl IntoIterator<Item = T>,
cx: &mut Context<Self>,
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>>
where
T: Into<UserMessageContent>,
{
let content = content.into_iter().map(Into::into).collect::<Arc<_>>();
self.messages.push(Arc::new(Message::User(UserMessage { id, content })));
cx.notify();
self.send_existing(cx)
}
pub fn send_existing(
&mut self,
cx: &mut Context<Self>,
) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>> {
let model = self.model().ok_or_else(|| anyhow!(NoModelConfiguredError))?;
self.advance_prompt_id(); // 每次交互递增 PromptId,用于精确关联遥测与并发去重
self.run_turn(cx)
}
【来源:crates/agent/src/thread.rs:2524-2555】
在 run_turn 中,系统会首先强制同步清空上一轮的待处理消息(flush_pending_message),并执行 self.cancel(cx).detach()。这杜绝了当用户快速连续按回车时,新旧两轮请求在后台并发执行篡改消息历史的竞态条件。
四、双层循环引擎:run_turn_internal 深度拆解
整个原生 Agent 最核心的执行算法,全封闭在 crates/agent/src/thread.rs:2753 的 run_turn_internal 中。
这是一个典型的双层状态机架构:
- 外层循环(Turn Iteration Loop):负责轮次维度的状态演进、上下文压缩检测、提示词重新组装与模型重试;
- 内层循环(Stream & Event Processing Loop):负责单次模型请求中,流式 token、并发工具返回与用户打断信号的三向竞争。

外层是 Turn 演进(压缩判定 → 刷新工具列表 → 装配请求 → 发起流式请求),内层是 futures::select! 的三向竞态(收 token / 收工具结果 / 用户打断)。内层退出后那句 drop(events) 是全章最关键的一处防御——它释放并发限流信号量的许可证,否则等待工具完成时会把自己锁死。
4.1 外层循环的关键执行点
rust
// crates/agent/src/thread.rs:2753-2850 逻辑精粹
async fn run_turn_internal(
this: &WeakEntity<Self>,
event_stream: &ThreadEventStream,
mut cancellation_rx: watch::Receiver<bool>,
cx: &mut AsyncApp,
) -> Result<()> {
let mut attempt = 0;
let mut intent = CompletionIntent::UserPrompt;
let mut refusal_fallback_model: Option<Arc<dyn LanguageModel>> = None;
loop {
// 1. 上下文压缩检测 (Compaction Check)
match Self::perform_compaction_if_needed(this, event_stream, cancellation_rx.clone(), cx).await {
Ok(ControlFlow::Continue(())) => {}
Ok(ControlFlow::Break(())) => return Ok(()), // 压缩被取消
Err(error) => { /* 指数退避重试或失败退出 */ }
}
// 2. 动态刷新工具与装配请求
// 关键设计:每一轮必须重新 read 模型和工具,以便实时捕获用户在中途切换 Profile 或开关工具的操作!
let (model, request) = this.update(cx, |this, cx| {
let model = refusal_fallback_model.clone()
.or_else(|| this.model().cloned())
.ok_or_else(|| anyhow!(NoModelConfiguredError))?;
this.refresh_turn_tools(cx);
let request = this.build_completion_request(intent, cx)?;
this.current_request_token_usage = TokenUsage::default();
anyhow::Ok((model, request))
})??;
// 3. 发起流式模型调用
let (mut events, mut error) = match model.stream_completion(request, cx).await {
Ok(events) => (events.fuse(), None),
Err(err) => (stream::empty().boxed().fuse(), Some(err)),
};
// 4. 进入内层循环 ...
【来源:crates/agent/src/thread.rs:2753-2850】
4.2 内层循环的三向竞争与批合并优化
在内层循环中,Zed 面对一个经典挑战:大模型通常以非常高频的小 token 块(几十毫秒一块)推送事件。如果每一个 token 到达都立刻调用 cx.notify() 触发界面重绘,GPUI 框架将被极其频繁的重排刷爆,导致严重的卡顿。
Zed 给出了教科书级的优化解法:now_or_never 批合并(Batch Chunking):
rust
// crates/agent/src/thread.rs:2877-2935
let mut tool_results: FuturesUnordered<Task<(usize, LanguageModelToolResult)>> = FuturesUnordered::new();
loop {
let first_event = futures::select! {
event = events.next().fuse() => event,
tool_result = tool_results.select_next_some() => {
// 工具返回结果处理 ...
continue;
}
_ = cancellation_rx.changed().fuse() => {
if *cancellation_rx.borrow() { cancelled = true; break; }
continue;
}
};
let Some(first_event) = first_event else { break; };
// 【关键优化】单帧批合并:将微任务队列中当前立即可读的所有事件一次性打包!
let mut batch = vec![first_event];
while let Some(event) = events.next().now_or_never().flatten() {
batch.push(event);
}
// 在单次状态更新事务中批量处理整批 chunks,只发出一次 cx.notify()
let batch_result = this.update(cx, |this, cx| {
let mut batch_tool_results = Vec::new();
for event in batch {
if let Ok(Some(task)) = this.handle_completion_event(event?, event_stream, cancellation_rx.clone(), cx) {
batch_tool_results.push(task);
}
}
cx.notify(); // 仅通知一次重绘!
batch_tool_results
})?;
tool_results.extend(batch_result);
}
【来源:crates/agent/src/thread.rs:2877-2945】
五、经典死锁陷阱与 drop(events) 的救赎
在内层循环刚一结束的第 2975 行,出现了一段极为深邃的源码与注释:
rust
// crates/agent/src/thread.rs:2975-2983
// Drop the stream to release the rate limit permit before tool execution.
// The stream holds a semaphore guard that limits concurrent requests.
// Without this, the permit would be held during potentially long-running
// tool execution, which could cause deadlocks when tools spawn subagents
// that need their own permits.
drop(events);
【来源:crates/agent/src/thread.rs:2975-2983】
为什么这一行 drop(events) 是系统的救命稻草?
让我们复原一个灾难性的死锁场景:
- 底层模型服务为了防止并发超限,在 RateLimiter 内部维护着一个信号量(例如最多允许 1 个活跃生成);
- 当调用 model.stream_completion() 时,返回的 events 流对象内部持有了这个信号量的 OwnedSemaphorePermit;
- 模型在流的末尾发出一个工具调用:spawn_agent(派生子智能体);
- 如果我们不主动 drop(events),这个流对象会一直存活,直到工具执行完毕返回后才被垃圾回收;
- 子智能体在自己的独立线程中启动,准备调用模型,向 RateLimiter 申请许可;
- 死锁爆发:父线程持有着许可证等待子智能体完成,子智能体挂起等待父线程释放许可证!系统彻底冻结。
通过在内层流接收完毕的瞬间,强制显式 drop(events),信号量许可证在工具运行前被立刻归还。无论后面的工具是运行漫长的终端命令还是递归衍生子 Agent,都不会受到任何并发许可约束。这就是顶级 Rust 工程师对资源生命周期控制的极致体现。
六、中途引导(Steering)与优雅打断
在许多 Agent 产品中,“打断”只有一种粗暴的方式:点击 Cancel 强行终止,已经生成的工具结果和上下文可能被撕裂损坏。
Zed 引入了更加精细的 Steering(中途引导) 机制:
rust
// crates/agent/src/thread.rs:2313-2318
pub fn set_end_turn_at_next_boundary(&mut self, end_at_boundary: bool) {
self.end_turn_at_next_boundary = end_at_boundary;
}
当模型一口气发起了 5 个工具调用,但在第 1 个执行完后,用户在输入框里输入了一句:“不对,别动那个文件,改看另一个”:
- 此时系统不会立刻杀死线程;
- 系统将 end_turn_at_next_boundary 置为 true;
- 当前正在跑的工具正常平稳结束并保存结果;
- 状态机在进入下一个工具前检测到该标志位,提前终止当前的 Turn 并安全退出;
- 新的用户提示词紧接着作为下一个消息被无缝消费。
这保证了代码库处于确定性的状态,既尊重了用户的打断意图,又避免了半途写坏文件的脏状态。
七、动手复核
你可以在源码检出中执行以下命令,直接复核本章提到的循环逻辑与关键代码:
bash
# 1. 定位 run_turn_internal 双层循环入口
grep -n "async fn run_turn_internal" crates/agent/src/thread.rs
# 2. 复核内层 select! 宏的三向竞态分支
sed -n '2880,2910p' crates/agent/src/thread.rs
# 3. 检查 drop(events) 的死锁防御注释
sed -n '2974,2985p' crates/agent/src/thread.rs
# 4. 查看 now_or_never 批合并优化代码
sed -n '2915,2930p' crates/agent/src/thread.rs
# 5. 查看 Thread 结构体中 sandbox_grants 的非持久化注释
grep -n -C 5 "sandbox_grants" crates/agent/src/thread.rs
八、总结
Thread 状态机完美诠释了什么是高性能桌面 Agent 引擎:
- 双层分离:外层管事务与轮次,内层管流与竞争;
- 批合并提速:now_or_never 解决流式卡顿,实现 60 FPS 稳定渲染;
- 确定性资源管理:通过 drop(events) 巧妙化解并发死锁;
- 温柔的掌控力:以 end_turn_at_next_boundary 实现平滑中途引导。
在下一章中,我们将继续深入 Thread 内部的消息系统,揭开内部状态表象与外部模型请求表象之间严格分离的工程奥秘。