Turn端到端链路
在 Codex 中,按下回车并不等于“同步调用模型,然后拿回一个字符串”。一次正常 Turn 至少穿过 四套生命周期:客户端的提交状态、App Server 的请求与通知、Core 的任务与采样循环、rollout 的 持久化序列。它们共用同一个 turn_id,却有不同的开始与结束信号。
最容易误判的是 turn/start 的响应。它只表示 App Server 已把 Op::UserInput 放入 Core 的 submission channel,并立即返回 InProgress 的 Turn;客户端看到后续的 turn/started,才知道 Core 已建立并运行 Turn task;直到 turn/completed 到达,客户端才能停止等待。
本文沿默认 TUI 路径展开。codex exec 的入口和呈现方式不同,但从 ClientRequest::TurnStart 开始复用同一条 App Server 与 Core 主链路。
阅读前建议先看 Codex 产品形态全景 区分 TUI、Exec 与 App Server, 再看 核心数据对象关系 区分 Core Turn 与客户端 Turn。本篇只负责 用户提交 → App Server → Core → 模型/工具 → rollout → 客户端终态 的跨层总链;Core 组件职责由 Core运行时架构总览 展开,Turn loop 的逐条件实现属于 Turn主循环与退出条件。
1. 消息与标识符
同一个 Turn 同时存在请求响应、运行事件和持久化记录。把它们画成一次普通函数返回,会丢失 重试、工具回灌、流式输出和中断语义。
| 阶段 | 代表消息 | 生产者 | 消费者 | 含义 |
|---|---|---|---|---|
| 接受提交 | TurnStartResponse | App Server | TUI、Exec、SDK | submission 已进入 Core channel |
| 开始运行 | TurnStarted / turn/started | Core / App Server | App Server / 客户端 | Turn task 已开始执行 |
| 运行中 | ItemStarted、delta、ItemCompleted、Error | Core | App Server 与客户端 | 模型、工具和条目的增量状态 |
| 结束运行 | TurnComplete 或 TurnAborted | Core | App Server | Core task 已进入终态 |
| 客户端终态 | turn/completed | App Server | TUI、Exec、SDK | Completed、Failed 或 Interrupted |
TurnProcessor::turn_start_inner 把 Core 返回的 submission ID 直接用作 App Server 的 turn.id。因此请求响应、Core Event.id、TurnStartedEvent.turn_id 和最终通知中的 Turn.id 可以关联到同一次执行。这个 ID 不是模型 response ID;一次 Turn 可能包含多次采样, 每次采样各有自己的 response ID。
注意 TurnStartResponse 与 turn/started 的先后并不是 UI 状态机中可以合并的细节。TUI 在提交 响应后只记录 safety buffering 所需的 turn ID;收到 TurnStartedNotification 时才调用 on_task_started,清除 pending-start、显示中断提示并把状态改为 Working。
2. TUI请求组装
用户提交首先成为 AppCommand::UserTurn。App::submit_active_thread_op 位于 tui/src/app/thread_routing.rs:如果该 Thread 已有活动 Turn,它优先调用 turn/steer;没有活动 Turn,或 steer 与服务端状态发生可恢复竞态,才调用 AppServerSession::turn_start。
下面保留了新 Turn 分支中的真实调用关系;参数列表中的 cwd、权限、模型和输出 schema 都是 本次请求的一部分,而不是模型客户端随后从 UI 全局变量中读取:
源码位置:codex-rs/tui/src/app/thread_routing.rs :: turn start routing
// 只有准入判断确认需要新 Turn,TUI 才组装并发送请求。
if should_start_turn {
let config = self.chat_widget.config_ref();
let permissions_override = Self::turn_permissions_override_from_config(
config,
active_permission_profile.as_ref(),
self.runtime_permission_profile_override
.as_ref()
.map(|profile| &profile.permission_profile),
);
let response = app_server
.turn_start(
thread_id,
items.to_vec(),
cwd.clone(),
*approval_policy,
approvals_reviewer,
permissions_override,
config.permissions.user_visible_workspace_roots(),
model.to_string(),
effort.clone(),
*summary,
service_tier.clone(),
collaboration_mode.clone(),
*personality,
final_output_json_schema.clone(),
)
.await?;
// 响应只用于关联已接受的 submission;它不代表 Turn 已经完成。
self.chat_widget
.record_safety_buffering_turn(response.turn.id, op);
}AppServerSession::turn_start 再把 UI 参数编码为协议层 TurnStartParams,通过统一的 typed client 发送 ClientRequest::TurnStart。嵌入式 TUI 走内存 channel,daemon 或远端 TUI 走 WebSocket, 但这一层以上没有两套 Turn 业务逻辑。
源码位置:codex-rs/tui/src/app_server_session.rs :: turn_start
// 请求 ID 与权限覆盖在跨 App Server 边界前固定。
let request_id = self.next_request_id();
let (sandbox_policy, permissions) =
turn_permissions_overrides(permissions_override, cwd.as_path());
self.client
.request_typed(ClientRequest::TurnStart {
request_id,
params: TurnStartParams {
thread_id: thread_id.to_string(),
input: items,
cwd: Some(cwd),
runtime_workspace_roots: Some(workspace_roots.to_vec()),
approval_policy: Some(approval_policy),
approvals_reviewer: Some(approvals_reviewer.into()),
sandbox_policy,
permissions,
model: Some(model),
effort,
summary,
personality,
output_schema,
collaboration_mode,
// 其余可选字段在 TUI 常规路径中为 None。
..
},
})
.await上面的 .. 是本文为压缩展示使用的省略标记,不是可直接编译的 Rust struct update;真实源码 逐项填写了 client_user_message_id、responsesapi_client_metadata、additional_context、 environments、service_tier 和 multi_agent_mode。
图中 MP 代表请求路由和 Thread listener 两个 App Server 活动点,并不表示二者由同一个函数 同步串行执行。请求由 MessageProcessor 分派;Core 事件由每个 Thread 的 listener task 独立排空。
3. App Server准入
MessageProcessor 对 ClientRequest::TurnStart 的分支只负责调用 TurnProcessor::turn_start。真正的准入位于 turn_start_inner,顺序很重要:
load_thread找到目标CodexThread;ensure_direct_input_allowed检查当前 Thread 是否接受直接输入;validate_v2_input_limit汇总文本字符数并拒绝超限输入;- 把协议
UserInput转为 Core input; - 解析 cwd、environment、workspace roots 和本 Turn 的设置覆盖;
- 构造
Op::UserInput并提交; - 以 submission ID 构造
InProgress的TurnStartResponse。
关键源码位于 app-server/src/request_processors/turn_processor.rs:
源码位置:codex-rs/app-server/src/request_processors/turn_processor.rs :: turn_start_inner
// 协议输入在 App Server 边界转换为 Core 输入。
let mapped_items: Vec<CoreInputItem> = params
.input
.into_iter()
.map(V2UserInput::into_core)
.collect();
let thread_settings = self
.build_thread_settings_overrides(
thread.as_ref(),
ThreadSettingsBuildParams {
method: "turn/start",
environments,
approval_policy: params.approval_policy,
sandbox_policy: params.sandbox_policy,
permissions: params.permissions,
model: params.model,
effort: params.effort,
summary: params.summary,
collaboration_mode: params.collaboration_mode,
personality: params.personality,
..
},
)
.await?;
let turn_op = Op::UserInput {
items: mapped_items,
final_output_json_schema: params.output_schema,
responsesapi_client_metadata: params.responsesapi_client_metadata,
additional_context,
thread_settings,
};
// 这里得到的 submission ID 就是对外 turn ID。
let turn_id = thread
.submit_user_input_with_client_user_message_id(
turn_op,
self.request_trace_context(&request_id).await,
client_user_message_id,
)
.await?;
Ok(TurnStartResponse {
turn: Turn {
id: turn_id,
items: vec![],
items_view: TurnItemsView::NotLoaded,
status: TurnStatus::InProgress,
error: None,
started_at: None,
completed_at: None,
duration_ms: None,
},
})这里的两个 .. 同样只折叠与主链路无关的字段。一个重要后果是:请求验证失败发生在 submission 之前,不会产生 TurnStarted;而 submission 成功后发生的模型或工具错误属于 Turn 内错误,最终 会通过通知结束 Turn。
CodexThread::submit_user_input_with_client_user_message_id 还会先让 AgentControl 检查执行容量。 随后 SessionIo 生成 UUIDv7 submission ID,把 Submission 异步写入容量 512 的 channel。写入 失败意味着 session loop 已死亡,映射为 InternalAgentDied,App Server 把它转换为 turn/start 请求错误。
4. Core 准入
submission_loop 是每个 Session 的长期消费者。收到 Op::UserInput 后,它调用 user_input_or_turn,再进入 user_input_or_turn_inner。这一步并不无条件创建 task:Core 会先尝试 把输入 steer 到活动 Turn;只有 SteerInputError::NoActiveTurn 才创建 RegularTask。
源码位置:codex-rs/core/src/session/handlers.rs :: user_input_or_turn_inner
// 先尝试 steer,只有明确没有活动 Turn 才创建新 task。
match sess
.steer_input(
items.clone(),
additional_context.clone(),
/* expected_turn_id */ None,
client_user_message_id.clone(),
responsesapi_client_metadata.clone(),
)
.await
{
Ok(turn_id) => {
// 已有活动 Turn:输入进入其 pending queue,不新建 task。
current_context.session_telemetry.user_prompt(&items);
Ok(UserMessageAdmission::Steered { turn_id })
}
Err(SteerInputError::NoActiveTurn(items)) => {
let mut task_input = additional_context_input
.into_iter()
.map(ResponseItem::from)
.map(TurnInput::ResponseItem)
.collect::<Vec<_>>();
if !items.is_empty() {
task_input.push(TurnInput::UserInput {
content: items,
client_id: client_user_message_id,
});
}
sess.spawn_task(
Arc::clone(¤t_context),
task_input,
RegularTask::new(),
)
.await;
Ok(UserMessageAdmission::Started { turn_id: sub_id })
}
Err(err) => {
sess.send_event_raw(Event {
id: sub_id.clone(),
msg: EventMsg::Error(err.to_error_event()),
})
.await;
Err(CodexErr::InvalidRequest(format!(
"failed to admit user message: {err:?}"
)))
}
}App Server 的 TUI 路径通常先根据已知活动 Turn 选择 turn/steer,Core 的再次判断仍然必要: 客户端缓存可能落后,多客户端也可能同时提交。真正的活动 Turn 所有权在 Core,而不是 UI。
Session::spawn_task 会先以 Replaced 原因终止仍存在的 task,再由 start_task 建立 CancellationToken、运行计时、Turn 状态和一个 Tokio task。RegularTask::run 在真正采样前 发送 EventMsg::TurnStarted,然后调用 run_turn。这解释了为什么 turn/start 响应中的 started_at 为 None,而 turn/started 才携带真实开始时间。
5. run_turn
RegularTask 并不是只调用一次模型。run_turn 先记录用户输入、上下文更新、skills 与 plugins 注入,再进入 step loop。每个 step 都重新从历史构造 prompt,并使用当前 StepContext 的 ToolRouter 公开工具规格。
run_sampling_request 创建 ToolCallRuntime,然后调用 try_run_sampling_request。后者通过 ModelClientSession::stream 得到 ResponseEvent 流。OutputTextDelta 被解析为可见文本后立即 发出 AgentMessageContentDelta;OutputItemDone 则进入 handle_output_item_done。
工具项的处理顺序是这条主链路的核心:
源码位置:codex-rs/core/src/stream_events_utils.rs :: process_response_item
match ToolRouter::build_tool_call(item.clone()) {
Ok(Some(call)) => {
// 先把模型发出的工具调用写入 history 与 rollout,再启动外部副作用。
record_completed_response_item(
ctx.sess.as_ref(),
ctx.turn_context.as_ref(),
&item,
)
.await;
// 工具执行以 future 交回采样循环;结果稍后统一 drain。
let tool_future = Box::pin(
ctx.tool_runtime
.clone()
.handle_tool_call(call, ctx.cancellation_token.child_token()),
);
output.needs_follow_up = true;
output.tool_future = Some(tool_future);
}
Ok(None) => {
// 普通消息/推理转为 TurnItem,发出 started/completed,并记录原始响应项。
// 这里还提取最终 agent message,供 TurnComplete 使用。
}
Err(FunctionCallError::RespondToModel(message)) => {
// 可恢复的路由拒绝也被包装成 FunctionCallOutput 回给模型。
// 因此仍需下一次采样,而不是直接让整个 Turn 失败。
}
Err(FunctionCallError::Fatal(message)) => {
return Err(CodexErr::Fatal(message));
}
}流结束后,drain_in_flight 按 FuturesOrdered 的顺序等待工具 future,把每个 ResponseInputItem 转回 ResponseItem,再调用 record_conversation_items。下一次 step 从更新后的 history 构造 prompt,于是模型能看到成对的工具调用和工具输出。
ToolCallRuntime 内部再根据工具是否支持并行取得读锁或写锁:支持并行的调用共享读锁,不支持 并行的调用独占写锁。它调用 ToolRouter::dispatch_tool_call_with_terminal_outcome 进入具体 handler。 普通 FunctionCallError 被转换为 success: false 的工具输出,让模型有机会解释或修复;只有 Fatal 才升级为 Core Turn 错误。
模型流本身也有局部恢复。run_sampling_request 对 is_retryable() 的错误执行 provider 配置的 最大重试次数,按 backoff 等待,并可从 WebSocket 降级到 HTTPS。重试期间发出带 will_retry: true 语义的错误通知;超过重试上限或遇到不可重试错误,run_turn 才记录终端错误。
读到这里若目标已经变成“每一种退出条件如何改变 needs_follow_up、pending input 与 token 状态”,应 停止使用本总链,进入 Turn主循环与退出条件;若目标是 工具 handler 的准入与回复,则进入 Session运行时处理。
6. 历史与Rollout
Core 没有把“保存会话”和“通知 UI”做成两条互不相关的旁路。Session 的记录函数定义了明确 顺序:先更新内存历史,再追加 rollout,最后发出 raw response item;事件则先追加 RolloutItem::EventMsg,再写入无界 event channel。
源码位置:codex-rs/core/src/session/mod.rs :: record_conversation_items
// item 先进入内存 history,再按策略进入 rollout,确保后续采样可见。
pub(crate) async fn record_conversation_items(
&self,
turn_context: &TurnContext,
items: &[ResponseItem],
) {
let (items, image_preparations) =
self.prepare_conversation_items_for_history(turn_context, items);
{
let mut state = self.state.lock().await;
state.record_items(
items.iter(),
turn_context.model_info.truncation_policy.into(),
);
}
// ResponseItem 成为 durable rollout 项。
self.persist_rollout_response_items(items.as_ref()).await;
// 订阅 raw item 的客户端随后收到对应事件。
self.send_raw_response_items(turn_context, items.as_ref()).await;
}
async fn send_event_raw_with_persistence(&self, event: Event, persist: bool) {
if persist {
self.persist_rollout_items(&[
RolloutItem::EventMsg(event.msg.clone())
])
.await;
}
self.deliver_event_raw(event).await;
}
async fn deliver_event_raw(&self, event: Event) {
if let Some(status) = agent_status_from_event(&event.msg) {
self.agent_status.send_replace(status);
}
let _ = self.tx_event.send(event).await;
}错误并不会回滚已经记录的模型项或工具项。handle_output_item_done 特意在排队执行工具前就保存 模型发出的调用;即使随后被中断,rollout 仍能说明模型请求过什么、工具是否产生了输出。
“同时服务”不表示三者在一个原子事务中提交。rollout append 失败会被记录为错误,事件仍可能继续 投递;Turn task 正常返回后,start_task 会先显式 flush_rollout,若失败则发出 Warning,随后 仍调用 on_task_finished。终端事件写入后还会再 flush 一次,使读取者能观察到完整终态。
7. App Server
每个已加载 Thread 的 listener task 持续调用 CodexThread::next_event()。事件到达后,它先调用 ThreadState::track_current_turn_event 更新活动 Turn、最后一条 agent message 和最后错误,再进入 apply_bespoke_event_handling 转成 v2 notification。
这层投影承担三个不能由 Core 直接决定的客户端语义:
TurnStarted被包装为TurnStartedNotification,补上thread_id;- Core 的 item/delta 事件被转换为 App Server 的
ItemStarted、ItemCompleted、AgentMessageDelta等通知; TurnComplete本身不直接决定 App Server 状态。若ThreadState.turn_summary.last_error存在, App Server 发出Failed;否则发出Completed。TurnAborted则归约为Interrupted。
源码位置:codex-rs/app-server/src/bespoke_event_handling.rs :: handle_turn_complete
// terminal event 负责闭合客户端 Turn,并发布最终状态通知。
async fn handle_turn_complete(
conversation_id: ThreadId,
event_turn_id: String,
turn_complete_event: TurnCompleteEvent,
outgoing: &ThreadScopedOutgoingMessageSender,
thread_state: &Arc<Mutex<ThreadState>>,
) {
let turn_summary =
find_and_remove_turn_summary(conversation_id, thread_state).await;
let (status, error, last_agent_message) = match turn_summary.last_error {
Some(error) => (TurnStatus::Failed, Some(error), None),
None => (
TurnStatus::Completed,
None,
turn_summary.last_agent_message,
),
};
emit_turn_completed_with_status(
conversation_id,
event_turn_id,
TurnCompletionMetadata {
status,
error,
last_agent_message,
started_at: turn_summary.started_at,
completed_at: turn_complete_event.completed_at,
duration_ms: turn_complete_event.duration_ms,
},
outgoing,
)
.await;
}最终通知中的 items 不是完整 Turn 历史。成功时它至多携带最后一条 agent message,并把 items_view 标为 Summary;没有可用摘要时为 NotLoaded。需要完整历史的客户端必须调用 thread/read(includeTurns: true)。Exec 还会在必要时用 thread/read 回填在背压下漏掉的非终态 item 通知,不能假定 turn/completed.items 天然包含全部过程。
8. TUI通知呈现
App Server client 收到 notification 后,App::enqueue_thread_notification 先按 thread_id 路由到 对应 Thread 的 store 和 channel。活动 Thread 的通知再进入 ChatWidget::handle_server_notification。
| 通知 | ChatWidget 的关键动作 |
|---|---|
TurnStarted | 保存 turn ID,调用 on_task_started,显示 Working 和 interrupt hint |
AgentMessageDelta | 追加流式回答,不等待最终 item |
ItemStarted / ItemCompleted | 创建或完成命令、工具、文件修改等 history cell |
Error(will_retry=true) | 显示重连状态,但不结束 Turn |
Error(will_retry=false) | 记住非重试错误,准备失败终态 |
TurnCompleted(Completed) | 补齐最后消息并调用 on_task_complete |
TurnCompleted(Failed) | 展示错误并清理运行状态 |
TurnCompleted(Interrupted) | 进入中断清理路径 |
handle_turn_completed_notification 会按 TurnStatus 分支。成功分支从 summary items 找最后一条 agent message,避免重复渲染已经由 ItemCompleted 显示的内容,然后调用 on_task_complete;失败 和中断也都会调用清理逻辑,关闭流控制器、运行命令状态和状态栏 spinner,并尝试发送队列中的 下一条输入。
每条箭头分别对应 RegularTask::run / Session::send_event、 CodexThread::next_event、apply_bespoke_event_handling、 App::enqueue_thread_notification 和 ChatWidget::handle_server_notification。其中 delta 的 App Server method 名由协议转换器生成,图中使用其客户端语义名称而不是 Rust enum variant。
9. 失败与中断
主链路有三处性质不同的失败:
9.1 请求被拒绝
Thread 不存在、输入超限、cwd 非法、设置冲突或 submission channel 已关闭时,turn/start 返回 JSON-RPC error,不产生正常的 TurnStarted。TUI 的 handle_turn_start_rejection 把错误加入历史, 清除 pending-start,但整个应用继续运行。
9.2 Turn 内错误
模型的不可重试错误、无效图片或 fatal tool error 发生在 task 内。Core 发送 EventMsg::Error 并把 会影响状态的错误保存到 TurnContext.terminal_error;App Server 的 ThreadState 记录最后错误, 最后仍由 TurnComplete 触发 turn/completed,状态归约为 Failed。因此客户端不能在收到第一条 非重试 Error 后丢弃后续终态。
普通工具失败不一定让 Turn 失败。ToolCallRuntime::failure_response 把错误变为 FunctionCallOutput { success: false },模型可在下一次采样中恢复;这是“工具执行失败”与“Turn 基础设施失败”的分界。
9.3 用户中断
turn/interrupt 按 thread ID 和 turn ID 定位活动 Turn。Core 取消 CancellationToken;采样流和 工具 runtime 都监听该 token。task 返回 CodexErr::TurnAborted 后,on_task_finished 发送 TurnAborted,App Server 再把它转换为状态 Interrupted 的 turn/completed。
10. 源码定位
这条主链路分散在多个 crate,阅读时按“调用点—状态变化—测试”三列核对最可靠:
| 要验证的问题 | 主要源码位置 | 对应测试入口 |
|---|---|---|
| 请求响应与运行开始是否分离 | tui/src/app_server_session.rs、turn_processor.rs、tasks/regular.rs | core/src/session/tests.rs::regular_turn_emits_turn_started_with_trace_id_without_waiting_for_startup_prewarm |
| turn ID 是否贯穿响应与终态 | turn_processor.rs、tasks/mod.rs | app-server/tests/common/test_app_server.rs::start_turn_and_wait_for_completion |
| 工具输出是否进入下一次采样 | stream_events_utils.rs、tools/parallel.rs、session/turn.rs | core/tests/suite/turn_state.rs 及各工具 suite 的 follow-up request 断言 |
| rollout 是否包含事件和响应项 | session/mod.rs、rollout reconstruction | core/src/session/tests.rs 中 rollout 与 terminal flush 测试 |
| 中断是否变成 Interrupted | tasks/mod.rs、bespoke_event_handling.rs | app-server/tests/suite/v2/turn_interrupt.rs::turn_interrupt_aborts_running_turn |
| 启动请求失败是否不退出 TUI | app/event_dispatch.rs、chatwidget/turn_runtime.rs | tui/src/app/tests/turn_submission.rs::turn_start_failure_is_shown_without_exiting |
如果只想在源码中复走一次正常路径,可以按下面的符号顺序阅读:
App::submit_active_thread_op
→ AppServerSession::turn_start
→ MessageProcessor::process_request
→ TurnProcessor::turn_start_inner
→ CodexThread::submit_user_input_with_client_user_message_id
→ SessionIo::submit_user_input_with_client_user_message_id
→ submission_loop
→ user_input_or_turn_inner
→ Session::spawn_task / Session::start_task
→ RegularTask::run
→ run_turn
→ run_sampling_request / try_run_sampling_request
→ handle_output_item_done
→ ToolCallRuntime::handle_tool_call
→ Session::record_conversation_items / Session::send_event
→ CodexThread::next_event
→ apply_bespoke_event_handling
→ App::enqueue_thread_notification
→ ChatWidget::handle_server_notification这份顺序特意保留了两种回环:handle_output_item_done 产生工具 future 后回到下一次 sampling; 活动 Turn 接收 steer 后进入 pending input queue,也会让 run_turn 继续下一 step。因而一次 Turn 不是单个请求,而是由同一 turn_id 约束的一段可多次采样、可多次执行工具、可持续投影事件的 任务生命周期。
11. 升级观察点
后续版本若要确认本文是否仍成立,应优先检查以下稳定性较低的连接点:
TurnProcessor::turn_start_inner是否仍用 submission ID 作为 turn ID;user_input_or_turn_inner的 steer/new-task 准入规则是否变化;run_turn是否仍以needs_follow_up驱动采样循环,工具结果的 drain 顺序是否变化;Session::send_event与record_conversation_items的持久化先后是否变化;- App Server 是否仍由
ThreadState.last_error把 CoreTurnComplete归约为Failed; TurnCompletedNotification.items_view是否仍只提供摘要,以及 Exec 是否仍需回填完整 items。
这些连接点一旦变化,影响的不只是某个函数名,而是客户端何时可以认为 Turn 已开始、何时可以 安全结束等待,以及中断后 rollout 能否重建已经发生的模型与工具活动。
12. 问题定位入口
端到端文章的完成标准是能从一个 turn_id 追到请求、Core task、采样/工具回灌和客户端终态。达到该 标准后,不应继续在本篇堆叠字段参考,而应按问题转入 owner 专题。
| 当前要继续回答的问题 | 对应专题正文 | 本篇停止点 |
|---|---|---|
| Core由哪些组件和服务组成 | Core运行时架构总览 | 已从App Server进入Core边界 |
| Session loop与Task如何派生 | Core异步任务拓扑 | 已定位submission_loop和spawn_task |
| Thread/Turn究竟有哪些对象 | Thread与Turn概念模型 | 开始混淆客户端Turn与Core对象 |
| Event在哪一层可能背压或丢弃 | CodexThread背压 | 已离开Core无界event queue |
| Session handler如何解释Op | Session运行时处理 | 已到submission_loop分派 |
| 文件、网络和工具执行由谁强制 | Codex信任边界 | 已进入approval/sandbox分支 |
| terminal event如何持久化并释放资源 | Session关闭流程 | 已收到TurnComplete/TurnAborted |
run_turn每个continue/break条件 | Turn主循环与退出条件 | 已进入step loop内部 |
12.1 总链自检
读者应能回答:为什么 TurnStartResponse 不等于 turn/started;为什么工具调用必须先记录再执行; 为什么普通工具错误可能继续采样而 fatal error 结束 Turn;为什么客户端收到 Error 后仍要等待 terminal notification;以及中断如何从 cancellation token 归约为 Interrupted。回答不完整时回到第10节对应 测试,而不是继续打开更多 crate。
可以用下面的只读搜索把本文的 Turn lifecycle 主线落回源码:
rg -n "turn_start|run_turn|TurnCompleted|record_response" codex-rs/core/src codex-rs/app-server/src