Skip to content

工具调度追踪模型

从工具注册边界追踪 ToolDispatchTrace 的事件写入、调用来源、运行时观察、失败配对与 RolloutTrace 重建。

基于rust-v0.150.0
CodexRustToolsTrace

工具调度追踪模型 ​

Codex 的工具追踪不是把日志字符串写进一个文件,而是把一次调度拆成“调用边界”和“运行时观察”两层,再通过 tool_call_id、模型可见 call_id、Code Mode runtime id 和 terminal operation id 逐步连接。调用边界记录“谁请求了哪个 工具、携带什么 payload、最终返回什么”;运行时观察记录“进程、补丁、MCP 或协作 runtime 实际发生了什么”。最后, reducer 按 raw event 的 seq 重放这些事件,生成可以查询的 RolloutTrace 图。

本文面向已经读过ToolOrchestrator执行流程、ToolLifecycle事件 和并行结果排序的读者。前文解释工具如何进入 handler、lifecycle 如何计算终态以及并行结果 如何回到历史;本文只研究诊断追踪,不把 trace event 当成模型历史,也不展开 terminal 或 MCP handler 的完整实现。读完后, 读者应能定位一个 trace bundle 中的调用起点、区分 canonical result 与 runtime payload,并解释为什么事件到达顺序可以不同于 模型调用顺序而仍然被 reducer 正确还原。

1. 追踪边界 ​

1.1 两层事实 ​

一条工具调用至少有两个时间窗口。第一个是 Core Registry 接受 ToolInvocation 到产生 caller-facing result 的 dispatch 窗口;第二个是 runtime 发出 begin/end protocol event 的执行窗口。对于 exec_command,前者的结果是给模型的 response item, 后者包含进程、cwd、stdout、stderr 和 exit code。对于 Code Mode nested tool,前者的结果是返回 JavaScript 的 JSON 值,后者仍 可以继续关联到 cell 或具体 runtime。

层事件主要回答不能替代
调用边界ToolCallStarted / ToolCallEnded谁请求、请求什么、返回给谁OS 进程细节
运行时观察ToolCallRuntimeStarted / ToolCallRuntimeEndedCore 怎样执行、运行多久、产生什么 runtime payload原始模型调用身份
还原结果RolloutTrace.tool_calls 与 terminal/code cell 对象如何查询完整因果关系原始事件的逐行顺序

这张图的关键是 ToolCallEnded 与 RuntimeEnded 不一定是同一个结束点。Reducer 会把二者都保存到同一个 ToolCall,但 只有 runtime 观察足够丰富时,才进一步生成 terminal operation 或 agent interaction 等领域对象。

1.2 可选开关 ​

生产 Session 通过 CODEX_ROLLOUT_TRACE_ROOT 决定是否建立根 trace。没有环境变量,或创建 bundle 失败时,返回 Disabled context;诊断失败不会让工具调用失败。这不是 feature flag 的“关闭后少写几条日志”,而是让热路径拥有 no-op handle,调用者 仍可无条件调用追踪方法。

源码位置:codex-rs/rollout-trace/src/thread.rs :: ThreadTraceContext::start_root_or_disabled

rust
pub fn start_root_or_disabled(metadata: ThreadStartedTraceMetadata) -> Self {
    let Some(root) = std::env::var_os(CODEX_ROLLOUT_TRACE_ROOT_ENV) else {
        return Self::disabled();
    };
    let root = PathBuf::from(root);
    match start_root_in_root(root.as_path(), metadata) {
        Ok(context) => context,
        Err(err) => {
            warn!("failed to initialize rollout trace bundle: {err:#}");
            Self::disabled()
        }
    }
}

Session 初始化把根线程或子线程 trace 放入 SessionServices。子线程复用父 bundle 的 writer;如果父线程没有 trace,子线程 不会自行创建一个看似独立的 bundle。

源码位置:codex-rs/core/src/session/session.rs :: Session::new

rust
let rollout_thread_trace = if matches!(
    session_configuration.session_source,
    SessionSource::SubAgent(SubAgentSource::ThreadSpawn { .. })
) {
    parent_rollout_thread_trace.start_child_thread_trace_or_disabled(trace_metadata)
} else {
    ThreadTraceContext::start_root_or_disabled(trace_metadata)
};

2. Bundle写入 ​

2.1 文件布局 ​

启用追踪后,一个 bundle 由 manifest、append-only trace.jsonl 和 payloads/ 目录组成。事件只保存轻量的 RawPayloadRef,完整 JSON 放在 payload 文件中;这样 reducer 可以先读事件结构,再按需打开某个请求、结果或 runtime 文件。

源码位置:codex-rs/rollout-trace/src/writer.rs :: TraceWriter::create

rust
pub fn create(
    bundle_dir: impl AsRef<Path>,
    trace_id: String,
    rollout_id: String,
    root_thread_id: AgentThreadId,
) -> Result<Self> {
    let bundle_dir = bundle_dir.as_ref().to_path_buf();
    let payloads_dir = bundle_dir.join(PAYLOADS_DIR_NAME);
    std::fs::create_dir_all(&payloads_dir)
        .with_context(|| format!("create trace payload dir {}", payloads_dir.display()))?;
    let started_at_unix_ms = unix_time_ms();
    let manifest =
        TraceBundleManifest::new(trace_id, rollout_id, root_thread_id, started_at_unix_ms);
    write_json_file(&bundle_dir.join(MANIFEST_FILE_NAME), &manifest)?;
    let event_log_path = bundle_dir.join(RAW_EVENT_LOG_FILE_NAME);
    let event_log = OpenOptions::new()
        .create(true)
        .append(true)
        .open(&event_log_path)
        .with_context(|| format!("open trace event log {}", event_log_path.display()))?;
    Ok(Self {
        inner: Mutex::new(TraceWriterInner {
            manifest,
            payloads_dir,
            event_log: BufWriter::new(event_log),
            next_seq: 1,
            next_payload_ordinal: 1,
        }),
    })
}

2.2 写入顺序 ​

写 payload 时,writer 先创建 JSON 文件,再返回引用;追加 event 时,writer 在同一 mutex 下分配递增 seq、填充时间和上下文, 写入一行 JSON 并 flush。于是一个已写入事件不会指向“计划创建但尚未落盘”的 payload 文件。

源码位置:codex-rs/rollout-trace/src/writer.rs :: TraceWriter::write_json_payload

rust
pub fn write_json_payload(
    &self,
    kind: RawPayloadKind,
    value: &impl Serialize,
) -> Result<RawPayloadRef> {
    let mut inner = self.lock_inner();
    let ordinal = inner.next_payload_ordinal;
    inner.next_payload_ordinal += 1;
    let raw_payload_id = format!("raw_payload:{ordinal}");
    let relative_path = format!("{PAYLOADS_DIR_NAME}/{ordinal}.json");
    let absolute_path = inner.payloads_dir.join(format!("{ordinal}.json"));
    write_json_file(&absolute_path, value)?;
    Ok(RawPayloadRef {
        raw_payload_id,
        kind,
        path: relative_path,
    })
}

源码位置:codex-rs/rollout-trace/src/writer.rs :: TraceWriter::append_with_context

rust
pub fn append_with_context(
    &self,
    context: RawTraceEventContext,
    payload: RawTraceEventPayload,
) -> Result<RawTraceEvent> {
    let mut inner = self.lock_inner();
    let event = RawTraceEvent {
        schema_version: RAW_TRACE_EVENT_SCHEMA_VERSION,
        seq: inner.next_seq,
        wall_time_unix_ms: unix_time_ms(),
        rollout_id: inner.manifest.rollout_id.clone(),
        thread_id: context.thread_id,
        codex_turn_id: context.codex_turn_id,
        payload,
    };
    inner.next_seq += 1;
    serde_json::to_writer(&mut inner.event_log, &event)?;
    inner.event_log.write_all(b"\n")?;
    inner.event_log.flush()?;
    Ok(event)
}

seq 是因果排序主键,wall_time_unix_ms 只适合展示延迟和同一事件的时间窗口。两个事件可能拥有相同毫秒时间,但不会 拥有相同的 writer sequence。

3. 调度入口 ​

3.1 Core适配器 ​

Core 不直接构造 raw event。ToolDispatchTrace 只保存一个 ToolDispatchTraceContext,启动时把 Core 的 invocation 转换为 trace crate 的 ToolDispatchInvocation;结束时把 ToolOutput 转为 DirectResponse 或 CodeModeResponse。

源码位置:codex-rs/core/src/tools/tool_dispatch_trace.rs :: ToolDispatchTrace

rust
pub(crate) struct ToolDispatchTrace {
    context: ToolDispatchTraceContext,
}

impl ToolDispatchTrace {
    pub(crate) fn start(invocation: &ToolInvocation) -> Self {
        let context = invocation
            .session
            .services
            .rollout_thread_trace
            .start_tool_dispatch_trace(|| tool_dispatch_invocation(invocation));
        Self { context }
    }

    pub(crate) fn record_completed(
        &self,
        invocation: &ToolInvocation,
        call_id: &str,
        payload: &ToolPayload,
        result: &dyn ToolOutput,
    ) {
        if !self.context.is_enabled() {
            return;
        }
        let Some(result_payload) = tool_dispatch_result(
            invocation,
            call_id,
            payload,
            result,
        ) else {
            return;
        };
        let status = if result.success_for_logging() {
            ExecutionStatus::Completed
        } else {
            ExecutionStatus::Failed
        };
        self.context.record_completed(status, result_payload);
    }

    pub(crate) fn record_failed(&self, error: &FunctionCallError) {
        self.context.record_failed(error);
    }
}

启动 trace 的位置在 Registry 已经增加 active-turn tool count 后、查找 runtime 前。因此 unsupported tool 和 payload kind mismatch 也能形成完整的 started/failed 调度记录。

源码位置:codex-rs/core/src/tools/registry.rs :: ToolRegistry::dispatch_any_with_terminal_outcome

rust
{
    let mut active = invocation.session.active_turn.lock().await;
    if let Some(active_turn) = active.as_mut() {
        let mut turn_state = active_turn.turn_state.lock().await;
        turn_state.tool_calls = turn_state.tool_calls.saturating_add(1);
    }
}

let dispatch_trace = ToolDispatchTrace::start(&invocation);
let tool = match self.tool(&tool_name) {
    Some(tool) => tool,
    None => {
        let err = FunctionCallError::RespondToModel(message);
        dispatch_trace.record_failed(&err);
        return Err(err);
    }
};

3.2 正常结束 ​

handler 成功返回后,Registry 先计算 lifecycle outcome,再处理 PostToolUse 的反馈或 block,最后将 caller-facing result 交给 trace。于是 trace 的 ToolCallEnded 记录最终返回给调用者的 payload;PostToolUse block 会让 dispatch 返回错误,并以 Error response 结束 trace,但不会把已完成的 handler 副作用倒回去。

源码位置:codex-rs/core/src/tools/registry.rs :: ToolRegistry::dispatch_any_with_terminal_outcome

rust
if let Some(outcome) = post_tool_use_outcome {
    if outcome.should_block {
        let message = outcome.feedback_message.unwrap_or_else(|| {
            "PostToolUse hook blocked the tool result".to_string()
        });
        let err = FunctionCallError::RespondToModel(message);
        dispatch_trace.record_failed(&err);
        return Err(err);
    }
    if let Some(feedback_message) = outcome.feedback_message {
        result.result = Box::new(PostToolUseFeedbackOutput {
            original: result.result,
            model_visible: FunctionToolOutput::from_text(
                feedback_message,
                /*success*/ None,
            ),
        });
    }
}
dispatch_trace.record_completed(
    &invocation,
    &result.call_id,
    &result.payload,
    result.result.as_ref(),
);
Ok(result)

4. 身份投影 ​

4.1 请求者 ​

Core 的 ToolCallSource 有两套身份:Direct/DirectPlaintextMessage 代表模型直接请求,CodeMode 代表模型写的 JavaScript 通过 cell 发起 nested tool。trace 不把这两者强行压成一个 call id,而是保存 model_visible_call_id 或 code_mode_runtime_tool_id,让 reducer 能把运行时对象连接回不同的父节点。

源码位置:codex-rs/core/src/tools/tool_dispatch_trace.rs :: tool_dispatch_invocation

rust
let requester = match &invocation.source {
    ToolCallSource::Direct | ToolCallSource::DirectPlaintextMessage => {
        ToolDispatchRequester::Model {
            model_visible_call_id: invocation.call_id.clone(),
        }
    }
    ToolCallSource::CodeMode {
        cell_id,
        runtime_tool_call_id,
    } => ToolDispatchRequester::CodeCell {
        runtime_cell_id: cell_id.clone(),
        runtime_tool_call_id: runtime_tool_call_id.clone(),
    },
};

4.2 命名空间 ​

默认 namespace 不写入 tool_namespace,namespaced tool 则保留 namespace。这样 reducer 的工具标签可以显示 namespace.tool_name,同时保持默认工具的简洁形式。请求 payload 另存 function、tool search 和 custom 的原始形状, 不会把不同 payload 变体拼成一个字符串。

源码位置:codex-rs/core/src/tools/tool_dispatch_trace.rs :: tool_dispatch_invocation

rust
Some(ToolDispatchInvocation {
    thread_id: invocation.session.thread_id.to_string(),
    codex_turn_id: invocation.turn.sub_id.clone(),
    tool_call_id: invocation.call_id.clone(),
    tool_name: invocation.tool_name.name.clone(),
    tool_namespace: invocation
        .tool_name
        .namespace
        .as_ref()
        .filter(|_| !invocation.tool_name.is_default_namespace())
        .cloned(),
    requester,
    payload: tool_dispatch_payload(&invocation.payload),
})

4.3 结果形状 ​

结束结果与请求者一致:Direct 结果调用 ToolOutput::to_response_item,得到模型可见 response item;Code Mode 结果调用 ToolOutput::code_mode_result,得到 JavaScript 立即消费的 JSON 值。两者都写入同一个 ToolCallEnded,但 raw result 的 语义不同。

源码位置:codex-rs/core/src/tools/tool_dispatch_trace.rs :: tool_dispatch_result

rust
match invocation.source {
    ToolCallSource::Direct | ToolCallSource::DirectPlaintextMessage => {
        Some(ToolDispatchResult::DirectResponse {
            response_item: result.to_response_item(call_id, payload),
        })
    }
    ToolCallSource::CodeMode { .. } => {
        Some(ToolDispatchResult::CodeModeResponse {
            value: result.code_mode_result(payload),
        })
    }
}

5. Trace事件 ​

5.1 禁用与抑制 ​

ThreadTraceContext 禁用时,start_tool_dispatch_trace 甚至不会执行传入的 closure,因此 Core 不会为 disabled tracing 克隆大参数。即便 tracing 已启用,裸的默认 namespace exec custom call 也会被 trace crate 抑制,因为它是 Code Mode 的公共外壳,不是一次 canonical nested dispatch。

源码位置:codex-rs/rollout-trace/src/thread.rs :: ThreadTraceContext::start_tool_dispatch_trace

rust
pub fn start_tool_dispatch_trace(
    &self,
    invocation: impl FnOnce() -> Option<ToolDispatchInvocation>,
) -> ToolDispatchTraceContext {
    let ThreadTraceContextState::Enabled(context) = &self.state else {
        return ToolDispatchTraceContext::disabled();
    };
    let Some(invocation) = invocation() else {
        return ToolDispatchTraceContext::disabled();
    };
    ToolDispatchTraceContext::start(Arc::clone(&context.writer), invocation)
}

源码位置:codex-rs/rollout-trace/src/tool_dispatch.rs :: suppresses_tool_dispatch_trace

rust
fn suppresses_tool_dispatch_trace(invocation: &ToolDispatchInvocation) -> bool {
    matches!(invocation.payload, ToolDispatchPayload::Custom { .. })
        && invocation.tool_namespace.is_none()
        && invocation.tool_name == codex_code_mode::PUBLIC_TOOL_NAME
}

5.2 开始事件 ​

ToolDispatchTraceContext::start 先判断抑制条件,再保存 thread/turn/call 三个稳定身份,最后写 ToolCallStarted。事件还 会保存 kind、短输入预览和完整 invocation payload 的 RawPayloadRef。预览最多 160 个字符,只服务列表和诊断摘要,不能 替代 payload 文件。

源码位置:codex-rs/rollout-trace/src/tool_dispatch.rs :: ToolDispatchTraceContext::start

rust
pub(crate) fn start(writer: Arc<TraceWriter>, invocation: ToolDispatchInvocation) -> Self {
    if suppresses_tool_dispatch_trace(&invocation) {
        return Self::disabled();
    }
    let context = EnabledToolDispatchTraceContext {
        writer,
        thread_id: invocation.thread_id.clone(),
        codex_turn_id: invocation.codex_turn_id.clone(),
        tool_call_id: invocation.tool_call_id.clone(),
    };
    record_started(&context, invocation);
    Self {
        state: ToolDispatchTraceContextState::Enabled(context),
    }
}

源码位置:codex-rs/rollout-trace/src/tool_dispatch.rs :: record_started

rust
let kind = dispatched_tool_kind(&tool_name, &invocation.payload);
let label = dispatched_tool_label(
    &tool_name,
    tool_namespace.as_deref(),
    &invocation.payload,
);
let input_preview = Some(invocation.payload.log_payload_preview());
let payload = invocation.payload.into_json_payload();
let request = DispatchedToolTraceRequest {
    tool_name: tool_name.as_str(),
    tool_namespace: tool_namespace.as_deref(),
    payload: &payload,
};
let request_payload = write_json_payload_best_effort(
    &context.writer,
    RawPayloadKind::ToolInvocation,
    &request,
);

5.3 结束事件 ​

成功返回和错误返回都写 ToolCallEnded。正常结果中的 status 由 success_for_logging 决定;没有正常 result payload 的 early return 则使用 Error { error }。写 payload 或 append event 失败只发 warning,不能反过来改变工具调用的业务结果。

源码位置:codex-rs/rollout-trace/src/tool_dispatch.rs :: ToolDispatchTraceContext::record_completed

rust
pub fn record_completed(&self, status: ExecutionStatus, result: ToolDispatchResult) {
    let ToolDispatchTraceContextState::Enabled(context) = &self.state else {
        return;
    };
    let response = match &result {
        ToolDispatchResult::DirectResponse { response_item } => {
            DispatchedToolTraceResponse::DirectResponse { response_item }
        }
        ToolDispatchResult::CodeModeResponse { value } => {
            DispatchedToolTraceResponse::CodeModeResponse { value }
        }
    };
    append_tool_call_ended(context, status, &response);
}

源码位置:codex-rs/rollout-trace/src/tool_dispatch.rs :: ToolDispatchTraceContext::record_failed

rust
pub fn record_failed(&self, error: impl Display) {
    let ToolDispatchTraceContextState::Enabled(context) = &self.state else {
        return;
    };
    append_tool_call_ended(
        context,
        ExecutionStatus::Failed,
        &DispatchedToolTraceResponse::Error {
            error: error.to_string(),
        },
    );
}

6. 运行时观察 ​

6.1 Protocol映射 ​

ThreadTraceContext::record_tool_call_event 接收已有的 EventMsg,只挑选真正代表工具 runtime 的变体。UserShell 的 ExecCommandBegin/End 被排除,避免把用户在终端直接输入的命令混进模型工具调用。exec、patch、MCP 和协作事件分别映射 为 Started/Ended;结束状态由协议结果转换为 ExecutionStatus。

源码位置:codex-rs/rollout-trace/src/protocol_event.rs :: tool_runtime_trace_event

rust
match event {
    EventMsg::ExecCommandBegin(event)
        if event.source != ExecCommandSource::UserShell =>
    {
        Some(ToolRuntimeTraceEvent::Started {
            tool_call_id: &event.call_id,
            payload: ToolRuntimePayload::ExecCommandBegin(event),
        })
    }
    EventMsg::ExecCommandEnd(event)
        if event.source != ExecCommandSource::UserShell =>
    {
        Some(ToolRuntimeTraceEvent::Ended {
            tool_call_id: &event.call_id,
            status: event.status.trace_execution_status(),
            payload: ToolRuntimePayload::ExecCommandEnd(event),
        })
    }
    EventMsg::McpToolCallEnd(event) => Some(ToolRuntimeTraceEvent::Ended {
        tool_call_id: &event.call_id,
        status: if event.result.is_ok() {
            ExecutionStatus::Completed
        } else {
            ExecutionStatus::Failed
        },
        payload: ToolRuntimePayload::McpToolCallEnd(event),
    }),
    _ => None,
}

6.2 Runtime payload ​

运行时事件先写入 RawPayloadKind::ToolRuntimeEvent,再追加 ToolCallRuntimeStarted/Ended。Reducer 看到这些事件后, 不会覆盖 canonical invocation/result,而是把 payload id 放进 raw_runtime_payload_ids,再按工具 kind 创建 terminal 或 agent interaction 对象。

源码位置:codex-rs/rollout-trace/src/thread.rs :: EnabledThreadTraceContext::raw_tool_runtime_payload

rust
match trace_event {
    ToolRuntimeTraceEvent::Started {
        tool_call_id,
        payload,
    } => {
        let runtime_payload = self.write_json_payload_best_effort(
            RawPayloadKind::ToolRuntimeEvent,
            &payload,
        )?;
        Some(RawTraceEventPayload::ToolCallRuntimeStarted {
            tool_call_id: tool_call_id.to_string(),
            runtime_payload,
        })
    }
    ToolRuntimeTraceEvent::Ended {
        tool_call_id,
        status,
        payload,
    } => {
        let runtime_payload = self.write_json_payload_best_effort(
            RawPayloadKind::ToolRuntimeEvent,
            &payload,
        )?;
        Some(RawTraceEventPayload::ToolCallRuntimeEnded {
            tool_call_id: tool_call_id.to_string(),
            status,
            runtime_payload,
        })
    }
}

6.3 关联时机 ​

运行时开始事件可能先于模型 response item 被 reducer 看到。例如 Code Mode cell 很快返回,CodeCellStarted、nested tool 和 CodeCellEnded 可以在 inference response item 入图之前抵达。reducer 因此维护 pending code-cell starts 和 lifecycle events,而不是要求生产者人为延迟事件。

7. 图形还原 ​

7.1 Raw envelope ​

每行 raw event 都有 schema version、连续 seq、wall time、rollout id、thread/turn context 和 tagged payload。这个 envelope 让 replay 可以在理解具体 payload 之前检查顺序和 payload refs。

源码位置:codex-rs/rollout-trace/src/raw_event.rs :: RawTraceEvent

rust
pub struct RawTraceEvent {
    pub schema_version: u32,
    pub seq: RawEventSeq,
    pub wall_time_unix_ms: i64,
    pub rollout_id: String,
    pub thread_id: Option<AgentThreadId>,
    pub codex_turn_id: Option<CodexTurnId>,
    pub payload: RawTraceEventPayload,
}

7.2 调用对象 ​

reducer 首次遇到 ToolCallStarted 时创建一个 ToolCall。它同时保存模型可见 call/output item ids、canonical invocation payload、caller-facing result payload 和 runtime payload ids。这个对象不是聊天历史的一行,而是连接多个观察对象的索引。

源码位置:codex-rs/rollout-trace/src/model/runtime.rs :: ToolCall

rust
pub struct ToolCall {
    pub tool_call_id: ToolCallId,
    pub mcp_call_id: Option<McpCallId>,
    pub model_visible_call_id: Option<ModelVisibleCallId>,
    pub code_mode_runtime_tool_id: Option<CodeModeRuntimeToolId>,
    pub thread_id: AgentThreadId,
    pub started_by_codex_turn_id: Option<CodexTurnId>,
    pub execution: ExecutionWindow,
    pub requester: ToolCallRequester,
    pub kind: ToolCallKind,
    pub model_visible_call_item_ids: Vec<ConversationItemId>,
    pub model_visible_output_item_ids: Vec<ConversationItemId>,
    pub terminal_operation_id: Option<TerminalOperationId>,
    pub summary: ToolCallSummary,
    pub raw_invocation_payload_id: Option<RawPayloadId>,
    pub raw_result_payload_id: Option<RawPayloadId>,
    pub raw_runtime_payload_ids: Vec<RawPayloadId>,
}

7.3 结束与扩展 ​

end_tool_call 更新 execution 的结束时间、seq、status 和 result payload。若已有 runtime payload,terminal operation 会由 runtime end 负责结束;若没有 runtime payload,direct result 可以直接结束 terminal operation。这一条件避免同一个 exec 既被 dispatch result 结束一次,又被 runtime end 结束第二次。

源码位置:codex-rs/rollout-trace/src/reducer/tool.rs :: TraceReducer::end_tool_call

rust
let (terminal_operation_id, thread_id, end_terminal_from_result) = {
    let Some(tool_call) = self.rollout.tool_calls.get_mut(&tool_call_id) else {
        bail!("tool call end referenced unknown call {tool_call_id}");
    };
    tool_call.execution.ended_at_unix_ms = Some(wall_time_unix_ms);
    tool_call.execution.ended_seq = Some(seq);
    tool_call.execution.status = status.clone();
    tool_call.raw_result_payload_id = result_payload
        .as_ref()
        .map(|payload| payload.raw_payload_id.clone());
    (
        tool_call.terminal_operation_id.clone(),
        tool_call.thread_id.clone(),
        tool_call.raw_runtime_payload_ids.is_empty(),
    )
};
if end_terminal_from_result && let Some(operation_id) = terminal_operation_id {
    self.end_terminal_operation(
        seq,
        wall_time_unix_ms,
        &thread_id,
        &operation_id,
        status,
        result_payload.as_ref(),
    )?;
}

8. 非正常路径 ​

8.1 Early return ​

unknown tool 和 payload kind mismatch 都在 handler 前返回,但因为 ToolDispatchTrace::start 已经执行,reducer 会得到 ToolCallStarted → ToolCallEnded(Failed)。结果 payload 是 Error,而不是 DirectResponse;这使诊断者能区分“没有找到工具”与 “handler 执行后失败”。

8.2 Trace写入失败 ​

trace crate 的 write_json_payload_best_effort 和 append_with_context_best_effort 都把 IO 错误转成 warning。于是可能出现 调用已经成功、但 invocation/result payload ref 为 None,或者 raw event 缺少某条观察。trace 是诊断材料,不是工具业务事务; 不能因为 trace bundle 缺一行,就推断 handler 没有执行。

源码位置:codex-rs/rollout-trace/src/tool_dispatch.rs :: write_json_payload_best_effort

rust
fn write_json_payload_best_effort(
    writer: &TraceWriter,
    kind: RawPayloadKind,
    payload: &impl Serialize,
) -> Option<RawPayloadRef> {
    match writer.write_json_payload(kind, payload) {
        Ok(payload_ref) => Some(payload_ref),
        Err(err) => {
            warn!("failed to write rollout trace payload: {err:#}");
            None
        }
    }
}

8.3 Replay错误 ​

reducer 对未知 tool call、重复 start、重复 MCP correlation、缺失 payload 和不一致 terminal operation 会返回错误。严格失败 的目的是不生成一个看似完整但关系错误的 RolloutTrace。这与生产写入的 best effort 是两个阶段:写入阶段尽量不影响用户, 读取阶段拒绝静默修复关键身份关系。

源码位置:codex-rs/rollout-trace/src/reducer/mod.rs :: replay_bundle

rust
for (line_index, line) in BufReader::new(event_log).lines().enumerate() {
    let line = line.with_context(|| format!("read trace event line {}", line_index + 1))?;
    if line.trim().is_empty() {
        continue;
    }
    let event: RawTraceEvent = serde_json::from_str(&line)
        .with_context(|| format!("parse trace event line {}", line_index + 1))?;
    reducer.apply_event(event)?;
}
reducer.resolve_pending_spawn_edge_fallbacks()?;
Ok(reducer.rollout)

9. 测试验证 ​

9.1 来源身份 ​

dispatch_lifecycle_trace_records_direct_and_code_mode_requesters 构造一个 Direct invocation 和一个 Code Mode invocation, 把两者写入同一个临时 bundle,然后 replay。断言 Direct 调用保留 model-visible id 和 Model requester;Code Mode 调用不保留 model-visible id,而保留 runtime tool id,并通过 code_cell:call-code 连接到 cell。

源码位置:codex-rs/core/src/tools/tool_dispatch_trace_tests.rs :: dispatch_lifecycle_trace_records_direct_and_code_mode_requesters

rust
assert_eq!(
    replayed.tool_calls["direct-call"].requester,
    ToolCallRequester::Model,
);
assert_eq!(
    replayed.tool_calls["code-mode-call"].model_visible_call_id,
    None,
);
assert_eq!(
    replayed.tool_calls["code-mode-call"].code_mode_runtime_tool_id,
    Some("tool-1".to_string()),
);
assert_eq!(
    replayed.tool_calls["code-mode-call"].requester,
    ToolCallRequester::CodeCell {
        code_cell_id: "code_cell:call-code".to_string(),
    },
);

这个测试证明两套身份投影和 result payload 都能通过 replay 找回,不证明真实 Code Mode host 的调度时序,也不证明模型 可见 transcript 一定与 runtime event 同时到达。

9.2 失败配对 ​

dispatch_lifecycle_trace_records_unsupported_tool_failures 使用空 Registry 调用不存在的工具。测试输入是 Direct、空参数和 missing_tool;断言 dispatch 返回 RespondToModel,replay 后对应 ToolCall.execution.status 为 Failed,且仍有 raw result payload。这验证了 early return 也拥有完整 trace 终态。

源码位置:codex-rs/core/src/tools/tool_dispatch_trace_tests.rs :: dispatch_lifecycle_trace_records_unsupported_tool_failures

rust
assert!(matches!(result, Err(FunctionCallError::RespondToModel(_))));
let replayed = codex_rollout_trace::replay_bundle(single_bundle_dir(temp.path())?)?;
let tool_call = &replayed.tool_calls["unsupported-call"];
assert_eq!(tool_call.execution.status, ExecutionStatus::Failed);
assert!(tool_call.raw_result_payload_id.is_some());

9.3 运行时关系 ​

rollout-trace 的 terminal reducer 测试手工写入 canonical start、runtime start、runtime end 和 dispatch result,断言一个 ToolCall 同时拥有 invocation/result/runtime payload,并只生成一个 terminal operation;write_stdin 测试进一步验证 后续操作复用已有 terminal session,而不是为每次输入重新创建 session。

源码位置:codex-rs/rollout-trace/src/reducer/tool/terminal_tests.rs :: write_stdin_operation_reuses_existing_terminal_session

rust
assert_eq!(
    rollout.terminal_operations[&stdin_operation_id]
        .terminal_id,
    Some("pty-1".to_string()),
);
assert_eq!(
    rollout.terminal_sessions["pty-1"].operation_ids,
    vec![startup_operation_id, stdin_operation_id],
);

这个测试证明 reducer 的 terminal join key 和 session lifetime;不证明 OS 进程仍然存活,也不证明 model-visible output 中的 字节与 runtime stdout 完全相同,因为两者之间还可能存在格式化和截断。

10. 阅读实践 ​

在源码仓库根目录运行下面的测试,可以先验证 Core 适配器的身份和失败配对,再验证 rollout-trace 的 runtime reducer:

bash
RUST_MIN_STACK=16777216 cargo test -p codex-core tool_dispatch_trace
RUST_MIN_STACK=16777216 cargo test -p codex-rollout-trace tool_dispatch
RUST_MIN_STACK=16777216 cargo test -p codex-rollout-trace exec_tool_reduces_to_terminal_operation_and_session
RUST_MIN_STACK=16777216 cargo test -p codex-rollout-trace write_stdin_operation_reuses_existing_terminal_session

然后用三个场景复述调用链:

  1. 一个不存在的 Direct tool:它为什么仍有 ToolCallStarted,错误结果保存在哪个 payload kind,最终 status 如何进入 RolloutTrace.tool_calls?
  2. 一个 exec_command:dispatch result、ExecCommandBegin/End 和 terminal operation 各自拥有什么身份,哪个时间窗口能回答 “进程何时启动”?
  3. 一个 Code Mode nested call:为什么 model_visible_call_id 可以为空,code_mode_runtime_tool_id 与 reducer 的 CodeCellId 又分别属于哪一层?

如果能从 ToolRegistry::dispatch_any_with_terminal_outcome 走到 ToolDispatchTrace::start、再走到 TraceWriter::write_json_payload/append_with_context,最后说明 TraceReducer::start_tool_call 与 end_tool_runtime_observation 如何合并对象,就已经掌握了这套追踪模型。接下来可以回看已执行调用追踪, 对比“给下一次模型请求的 attempted metadata”和“供诊断重放的 rollout trace”在所有者、消费者与可靠性上的差异。