Skip to main content

codex_rollout_trace/
code_cell.rs

1//! Hot-path helpers for recording code-mode runtime cell lifecycles.
2//!
3//! The public `exec` tool is reduced as a first-class `CodeCell` instead of a
4//! generic tool call. This module keeps the runtime response serialization and
5//! lifecycle event policy inside the trace crate while core carries a compact,
6//! no-op capable handle through execution and waits.
7
8use std::sync::Arc;
9
10use codex_code_mode::RuntimeResponse;
11use serde::Serialize;
12use tracing::warn;
13
14use crate::model::AgentThreadId;
15use crate::model::CodeCellRuntimeStatus;
16use crate::model::CodexTurnId;
17use crate::model::ModelVisibleCallId;
18use crate::payload::RawPayloadKind;
19use crate::payload::RawPayloadRef;
20use crate::raw_event::RawTraceEventContext;
21use crate::raw_event::RawTraceEventPayload;
22use crate::writer::TraceWriter;
23
24/// No-op capable trace handle for one code-mode runtime cell.
25#[derive(Clone, Debug)]
26pub struct CodeCellTraceContext {
27    state: CodeCellTraceContextState,
28}
29
30#[derive(Clone, Debug)]
31enum CodeCellTraceContextState {
32    Disabled,
33    Enabled(EnabledCodeCellTraceContext),
34}
35
36#[derive(Clone, Debug)]
37struct EnabledCodeCellTraceContext {
38    writer: Arc<TraceWriter>,
39    thread_id: AgentThreadId,
40    codex_turn_id: CodexTurnId,
41    runtime_cell_id: String,
42}
43
44/// Raw code-mode response captured at the runtime boundary.
45///
46/// This is not the model-visible custom-tool output. The reducer links that
47/// output through `CodeCell.output_item_ids` once the conversation item appears.
48/// Keeping the raw runtime payload here preserves stored-value and lifecycle
49/// evidence without duplicating the model-facing transcript.
50#[derive(Serialize)]
51struct CodeCellResponseTracePayload<'a> {
52    response: &'a RuntimeResponse,
53}
54
55impl CodeCellTraceContext {
56    /// Builds a context that accepts trace calls and records nothing.
57    pub(crate) fn disabled() -> Self {
58        Self {
59            state: CodeCellTraceContextState::Disabled,
60        }
61    }
62
63    /// Builds a context for an already-known code-mode runtime cell.
64    pub(crate) fn enabled(
65        writer: Arc<TraceWriter>,
66        thread_id: impl Into<AgentThreadId>,
67        codex_turn_id: impl Into<CodexTurnId>,
68        runtime_cell_id: impl Into<String>,
69    ) -> Self {
70        Self {
71            state: CodeCellTraceContextState::Enabled(EnabledCodeCellTraceContext {
72                writer,
73                thread_id: thread_id.into(),
74                codex_turn_id: codex_turn_id.into(),
75                runtime_cell_id: runtime_cell_id.into(),
76            }),
77        }
78    }
79
80    /// Records the parent runtime object before JavaScript can issue nested tool calls.
81    pub fn record_started(
82        &self,
83        model_visible_call_id: impl Into<ModelVisibleCallId>,
84        source_js: impl Into<String>,
85    ) {
86        let CodeCellTraceContextState::Enabled(context) = &self.state else {
87            return;
88        };
89        append_with_context_best_effort(
90            context,
91            RawTraceEventPayload::CodeCellStarted {
92                runtime_cell_id: context.runtime_cell_id.clone(),
93                model_visible_call_id: model_visible_call_id.into(),
94                source_js: source_js.into(),
95            },
96        );
97    }
98
99    /// Records the first response returned by the public code-mode `exec` tool.
100    ///
101    /// A yielded response returns control to the model while the cell keeps
102    /// running. Terminal initial responses should be followed by `record_ended`
103    /// by the caller so the reducer can distinguish model-visible output from
104    /// runtime completion.
105    pub fn record_initial_response(&self, response: &RuntimeResponse) {
106        let CodeCellTraceContextState::Enabled(context) = &self.state else {
107            return;
108        };
109        append_with_context_best_effort(
110            context,
111            RawTraceEventPayload::CodeCellInitialResponse {
112                runtime_cell_id: context.runtime_cell_id.clone(),
113                status: code_cell_status_for_runtime_response(response),
114                response_payload: code_cell_response_payload(context, response),
115            },
116        );
117    }
118
119    /// Records the terminal lifecycle point for a code-mode runtime cell.
120    pub fn record_ended(&self, response: &RuntimeResponse) {
121        let CodeCellTraceContextState::Enabled(context) = &self.state else {
122            return;
123        };
124        append_with_context_best_effort(
125            context,
126            RawTraceEventPayload::CodeCellEnded {
127                runtime_cell_id: context.runtime_cell_id.clone(),
128                status: code_cell_status_for_runtime_response(response),
129                response_payload: code_cell_response_payload(context, response),
130            },
131        );
132    }
133}
134
135fn code_cell_status_for_runtime_response(response: &RuntimeResponse) -> CodeCellRuntimeStatus {
136    match response {
137        RuntimeResponse::Yielded { .. } => CodeCellRuntimeStatus::Yielded,
138        RuntimeResponse::Terminated { .. } => CodeCellRuntimeStatus::Terminated,
139        RuntimeResponse::Result { error_text, .. } => {
140            if error_text.is_some() {
141                CodeCellRuntimeStatus::Failed
142            } else {
143                CodeCellRuntimeStatus::Completed
144            }
145        }
146    }
147}
148
149fn code_cell_response_payload(
150    context: &EnabledCodeCellTraceContext,
151    response: &RuntimeResponse,
152) -> Option<RawPayloadRef> {
153    write_json_payload_best_effort(
154        &context.writer,
155        RawPayloadKind::ToolResult,
156        &CodeCellResponseTracePayload { response },
157    )
158}
159
160fn write_json_payload_best_effort(
161    writer: &TraceWriter,
162    kind: RawPayloadKind,
163    payload: &impl Serialize,
164) -> Option<RawPayloadRef> {
165    match writer.write_json_payload(kind, payload) {
166        Ok(payload_ref) => Some(payload_ref),
167        Err(err) => {
168            warn!("failed to write rollout trace payload: {err:#}");
169            None
170        }
171    }
172}
173
174fn append_with_context_best_effort(
175    context: &EnabledCodeCellTraceContext,
176    payload: RawTraceEventPayload,
177) {
178    let event_context = RawTraceEventContext {
179        thread_id: Some(context.thread_id.clone()),
180        codex_turn_id: Some(context.codex_turn_id.clone()),
181    };
182    if let Err(err) = context.writer.append_with_context(event_context, payload) {
183        warn!("failed to append rollout trace event: {err:#}");
184    }
185}