Skip to content

Turn端到端链路

从一次用户提交出发,追踪 Turn 如何经过 App Server、Core、模型与工具循环、rollout 持久化,最终投影为客户端可见状态。

基于rust-v0.150.0
CodexRustApp ServerArchitecture

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 同时存在请求响应、运行事件和持久化记录。把它们画成一次普通函数返回,会丢失 重试、工具回灌、流式输出和中断语义。

阶段代表消息生产者消费者含义
接受提交TurnStartResponseApp ServerTUI、Exec、SDKsubmission 已进入 Core channel
开始运行TurnStarted / turn/startedCore / App ServerApp Server / 客户端Turn task 已开始执行
运行中ItemStarted、delta、ItemCompleted、ErrorCoreApp Server 与客户端模型、工具和条目的增量状态
结束运行TurnComplete 或 TurnAbortedCoreApp ServerCore task 已进入终态
客户端终态turn/completedApp ServerTUI、Exec、SDKCompleted、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

rust
// 只有准入判断确认需要新 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

rust
// 请求 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,顺序很重要:

  1. load_thread 找到目标 CodexThread;
  2. ensure_direct_input_allowed 检查当前 Thread 是否接受直接输入;
  3. validate_v2_input_limit 汇总文本字符数并拒绝超限输入;
  4. 把协议 UserInput 转为 Core input;
  5. 解析 cwd、environment、workspace roots 和本 Turn 的设置覆盖;
  6. 构造 Op::UserInput 并提交;
  7. 以 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

rust
// 协议输入在 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

rust
// 先尝试 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(&current_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

rust
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

rust
// 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

rust
// 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.rscore/src/session/tests.rs::regular_turn_emits_turn_started_with_trace_id_without_waiting_for_startup_prewarm
turn ID 是否贯穿响应与终态turn_processor.rs、tasks/mod.rsapp-server/tests/common/test_app_server.rs::start_turn_and_wait_for_completion
工具输出是否进入下一次采样stream_events_utils.rs、tools/parallel.rs、session/turn.rscore/tests/suite/turn_state.rs 及各工具 suite 的 follow-up request 断言
rollout 是否包含事件和响应项session/mod.rs、rollout reconstructioncore/src/session/tests.rs 中 rollout 与 terminal flush 测试
中断是否变成 Interruptedtasks/mod.rs、bespoke_event_handling.rsapp-server/tests/suite/v2/turn_interrupt.rs::turn_interrupt_aborts_running_turn
启动请求失败是否不退出 TUIapp/event_dispatch.rs、chatwidget/turn_runtime.rstui/src/app/tests/turn_submission.rs::turn_start_failure_is_shown_without_exiting

如果只想在源码中复走一次正常路径,可以按下面的符号顺序阅读:

text
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 把 Core TurnComplete 归约为 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如何解释OpSession运行时处理已到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 主线落回源码:

bash
rg -n "turn_start|run_turn|TurnCompleted|record_response" codex-rs/core/src codex-rs/app-server/src