1use codex_protocol::protocol::AgentStatus;
9use codex_protocol::protocol::EventMsg;
10use codex_protocol::protocol::SessionSource;
11use serde::Serialize;
12use std::path::Path;
13use std::path::PathBuf;
14use std::sync::Arc;
15use tracing::debug;
16use tracing::warn;
17use uuid::Uuid;
18
19use crate::AgentThreadId;
20use crate::CodeCellTraceContext;
21use crate::CodexTurnId;
22use crate::CompactionId;
23use crate::CompactionTraceContext;
24use crate::InferenceTraceContext;
25use crate::McpCallTraceContext;
26use crate::RawPayloadKind;
27use crate::RawPayloadRef;
28use crate::RawTraceEventContext;
29use crate::RawTraceEventPayload;
30use crate::RolloutStatus;
31use crate::ToolCallId;
32use crate::ToolDispatchInvocation;
33use crate::ToolDispatchTraceContext;
34use crate::TraceWriter;
35use crate::protocol_event::codex_turn_trace_event;
36use crate::protocol_event::tool_runtime_trace_event;
37use crate::protocol_event::wrapped_protocol_event_type;
38
39pub const CODEX_ROLLOUT_TRACE_ROOT_ENV: &str = "CODEX_ROLLOUT_TRACE_ROOT";
45
46#[derive(Serialize)]
51pub struct ThreadStartedTraceMetadata {
52 pub thread_id: String,
53 pub agent_path: String,
54 pub task_name: Option<String>,
55 pub nickname: Option<String>,
56 pub agent_role: Option<String>,
57 pub session_source: SessionSource,
58 pub cwd: std::path::PathBuf,
59 pub rollout_path: Option<std::path::PathBuf>,
60 pub model: String,
61 pub provider_name: String,
62 pub approval_policy: String,
63 pub sandbox_policy: String,
64}
65
66#[derive(Serialize)]
68pub struct AgentResultTracePayload<'a> {
69 pub child_agent_path: &'a str,
70 pub message: &'a str,
71 pub status: &'a AgentStatus,
72}
73
74#[derive(Clone, Debug)]
76pub struct ThreadTraceContext {
77 state: ThreadTraceContextState,
78}
79
80#[derive(Clone, Debug)]
81enum ThreadTraceContextState {
82 Disabled,
83 Enabled(EnabledThreadTraceContext),
84}
85
86#[derive(Clone, Debug)]
87struct EnabledThreadTraceContext {
88 writer: Arc<TraceWriter>,
89 root_thread_id: AgentThreadId,
90 thread_id: AgentThreadId,
91}
92
93impl ThreadTraceContext {
94 pub fn disabled() -> Self {
96 Self {
97 state: ThreadTraceContextState::Disabled,
98 }
99 }
100
101 pub fn start_root_or_disabled(metadata: ThreadStartedTraceMetadata) -> Self {
107 let Some(root) = std::env::var_os(CODEX_ROLLOUT_TRACE_ROOT_ENV) else {
108 return Self::disabled();
109 };
110 let root = PathBuf::from(root);
111 match start_root_in_root(root.as_path(), metadata) {
112 Ok(context) => context,
113 Err(err) => {
114 warn!("failed to initialize rollout trace bundle: {err:#}");
115 Self::disabled()
116 }
117 }
118 }
119
120 pub fn start_root_in_root_for_test(
125 root: &Path,
126 metadata: ThreadStartedTraceMetadata,
127 ) -> anyhow::Result<Self> {
128 start_root_in_root(root, metadata)
129 }
130
131 pub(crate) fn start(
133 writer: Arc<TraceWriter>,
134 root_thread_id: AgentThreadId,
135 metadata: ThreadStartedTraceMetadata,
136 ) -> Self {
137 let context = EnabledThreadTraceContext {
138 writer,
139 root_thread_id,
140 thread_id: metadata.thread_id.clone(),
141 };
142 record_thread_started(&context, metadata);
143 Self {
144 state: ThreadTraceContextState::Enabled(context),
145 }
146 }
147
148 pub fn is_enabled(&self) -> bool {
154 matches!(self.state, ThreadTraceContextState::Enabled(_))
155 }
156
157 pub fn start_child_thread_trace_or_disabled(
163 &self,
164 metadata: ThreadStartedTraceMetadata,
165 ) -> Self {
166 match &self.state {
167 ThreadTraceContextState::Disabled => Self::disabled(),
168 ThreadTraceContextState::Enabled(context) => Self::start(
169 Arc::clone(&context.writer),
170 context.root_thread_id.clone(),
171 metadata,
172 ),
173 }
174 }
175
176 pub fn record_ended(&self, status: RolloutStatus) {
182 let ThreadTraceContextState::Enabled(context) = &self.state else {
183 return;
184 };
185 context.append_best_effort(RawTraceEventPayload::ThreadEnded {
186 thread_id: context.thread_id.clone(),
187 status: status.clone(),
188 });
189 if context.thread_id == context.root_thread_id {
190 context.append_best_effort(RawTraceEventPayload::RolloutEnded { status });
191 }
192 }
193
194 pub fn record_protocol_event(&self, event: &EventMsg) {
199 let ThreadTraceContextState::Enabled(context) = &self.state else {
200 return;
201 };
202 let Some(event_type) = wrapped_protocol_event_type(event) else {
203 return;
204 };
205 let Some(event_payload) =
206 context.write_json_payload_best_effort(RawPayloadKind::ProtocolEvent, event)
207 else {
208 return;
209 };
210 context.append_best_effort(RawTraceEventPayload::ProtocolEventObserved {
211 event_type: event_type.to_string(),
212 event_payload,
213 });
214 }
215
216 pub fn record_codex_turn_event(&self, default_turn_id: &str, event: &EventMsg) {
218 let ThreadTraceContextState::Enabled(context) = &self.state else {
219 return;
220 };
221 let Some(trace_event) =
222 codex_turn_trace_event(context.thread_id.clone(), default_turn_id, event)
223 else {
224 return;
225 };
226 context.append_with_context_best_effort(
227 trace_event.context_turn_id.clone(),
228 trace_event.payload,
229 );
230 }
231
232 pub fn record_tool_call_event(&self, codex_turn_id: impl Into<CodexTurnId>, event: &EventMsg) {
238 let ThreadTraceContextState::Enabled(context) = &self.state else {
239 return;
240 };
241 let Some(trace_event) = tool_runtime_trace_event(event) else {
242 return;
243 };
244 let Some(payload) = context.raw_tool_runtime_payload(trace_event) else {
245 return;
246 };
247 context.append_with_context_best_effort(codex_turn_id.into(), payload);
248 }
249
250 pub fn record_agent_result_interaction(
257 &self,
258 child_codex_turn_id: impl Into<CodexTurnId>,
259 parent_thread_id: impl Into<AgentThreadId>,
260 payload: &AgentResultTracePayload<'_>,
261 ) {
262 let ThreadTraceContextState::Enabled(context) = &self.state else {
263 return;
264 };
265 let child_codex_turn_id = child_codex_turn_id.into();
266 let parent_thread_id = parent_thread_id.into();
267 let carried_payload =
268 context.write_json_payload_best_effort(RawPayloadKind::AgentResult, payload);
269 context.append_with_context_best_effort(
270 child_codex_turn_id.clone(),
271 RawTraceEventPayload::AgentResultObserved {
272 edge_id: format!(
273 "edge:agent_result:{}:{child_codex_turn_id}:{parent_thread_id}",
274 context.thread_id
275 ),
276 child_thread_id: context.thread_id.clone(),
277 child_codex_turn_id,
278 parent_thread_id,
279 message: payload.message.to_string(),
280 carried_payload,
281 },
282 );
283 }
284
285 pub fn record_codex_turn_started(&self, codex_turn_id: impl Into<CodexTurnId>) {
291 let ThreadTraceContextState::Enabled(context) = &self.state else {
292 return;
293 };
294 let codex_turn_id = codex_turn_id.into();
295 context.append_with_context_best_effort(
296 codex_turn_id.clone(),
297 RawTraceEventPayload::CodexTurnStarted {
298 codex_turn_id,
299 thread_id: context.thread_id.clone(),
300 },
301 );
302 }
303
304 pub fn start_code_cell_trace(
306 &self,
307 codex_turn_id: impl Into<CodexTurnId>,
308 runtime_cell_id: impl Into<String>,
309 model_visible_call_id: impl Into<String>,
310 source_js: impl Into<String>,
311 ) -> CodeCellTraceContext {
312 let context = self.code_cell_trace_context(codex_turn_id, runtime_cell_id);
313 context.record_started(model_visible_call_id, source_js);
314 context
315 }
316
317 pub fn code_cell_trace_context(
319 &self,
320 codex_turn_id: impl Into<CodexTurnId>,
321 runtime_cell_id: impl Into<String>,
322 ) -> CodeCellTraceContext {
323 let ThreadTraceContextState::Enabled(context) = &self.state else {
324 return CodeCellTraceContext::disabled();
325 };
326 CodeCellTraceContext::enabled(
327 Arc::clone(&context.writer),
328 context.thread_id.clone(),
329 codex_turn_id,
330 runtime_cell_id,
331 )
332 }
333
334 pub fn start_tool_dispatch_trace(
340 &self,
341 invocation: impl FnOnce() -> Option<ToolDispatchInvocation>,
342 ) -> ToolDispatchTraceContext {
343 let ThreadTraceContextState::Enabled(context) = &self.state else {
344 return ToolDispatchTraceContext::disabled();
345 };
346 let Some(invocation) = invocation() else {
347 return ToolDispatchTraceContext::disabled();
348 };
349 ToolDispatchTraceContext::start(Arc::clone(&context.writer), invocation)
350 }
351
352 pub fn inference_trace_context(
358 &self,
359 codex_turn_id: impl Into<CodexTurnId>,
360 model: impl Into<String>,
361 provider_name: impl Into<String>,
362 ) -> InferenceTraceContext {
363 let ThreadTraceContextState::Enabled(context) = &self.state else {
364 return InferenceTraceContext::disabled();
365 };
366 InferenceTraceContext::enabled(
367 Arc::clone(&context.writer),
368 context.thread_id.clone(),
369 codex_turn_id.into(),
370 model.into(),
371 provider_name.into(),
372 )
373 }
374
375 pub fn compaction_trace_context(
382 &self,
383 codex_turn_id: impl Into<CodexTurnId>,
384 compaction_id: impl Into<CompactionId>,
385 model: impl Into<String>,
386 provider_name: impl Into<String>,
387 ) -> CompactionTraceContext {
388 let ThreadTraceContextState::Enabled(context) = &self.state else {
389 return CompactionTraceContext::disabled();
390 };
391 CompactionTraceContext::enabled(
392 Arc::clone(&context.writer),
393 context.thread_id.clone(),
394 codex_turn_id.into(),
395 compaction_id.into(),
396 model.into(),
397 provider_name.into(),
398 )
399 }
400
401 pub fn start_mcp_call_trace(&self, tool_call_id: impl Into<ToolCallId>) -> McpCallTraceContext {
407 let ThreadTraceContextState::Enabled(context) = &self.state else {
408 return McpCallTraceContext::disabled();
409 };
410 let mcp_call_id = Uuid::new_v4().to_string();
411 let trace = McpCallTraceContext::enabled(mcp_call_id.clone());
412 context.append_best_effort(RawTraceEventPayload::McpToolCallCorrelationAssigned {
413 tool_call_id: tool_call_id.into(),
414 mcp_call_id,
415 });
416 trace
417 }
418}
419
420fn start_root_in_root(
421 root: &Path,
422 metadata: ThreadStartedTraceMetadata,
423) -> anyhow::Result<ThreadTraceContext> {
424 let trace_id = Uuid::new_v4().to_string();
425 let thread_id = metadata.thread_id.clone();
426 let bundle_dir = root.join(format!("trace-{trace_id}-{thread_id}"));
427 let writer = TraceWriter::create(
428 &bundle_dir,
429 trace_id.clone(),
430 thread_id.clone(),
431 thread_id.clone(),
432 )?;
433 let writer = Arc::new(writer);
434
435 if let Err(err) = writer.append(RawTraceEventPayload::RolloutStarted {
436 trace_id,
437 root_thread_id: thread_id.clone(),
438 }) {
439 warn!("failed to append rollout trace event: {err:#}");
440 }
441
442 debug!("recording rollout trace at {}", bundle_dir.display());
443 Ok(ThreadTraceContext::start(writer, thread_id, metadata))
444}
445
446fn record_thread_started(
447 context: &EnabledThreadTraceContext,
448 metadata: ThreadStartedTraceMetadata,
449) {
450 let metadata_payload =
451 context.write_json_payload_best_effort(RawPayloadKind::SessionMetadata, &metadata);
452 context.append_best_effort(RawTraceEventPayload::ThreadStarted {
453 thread_id: metadata.thread_id,
454 agent_path: metadata.agent_path,
455 metadata_payload,
456 });
457}
458
459impl EnabledThreadTraceContext {
460 fn write_json_payload_best_effort(
461 &self,
462 kind: RawPayloadKind,
463 payload: &impl Serialize,
464 ) -> Option<RawPayloadRef> {
465 match self.writer.write_json_payload(kind, payload) {
466 Ok(payload_ref) => Some(payload_ref),
467 Err(err) => {
468 warn!("failed to write rollout trace payload: {err:#}");
469 None
470 }
471 }
472 }
473
474 fn raw_tool_runtime_payload(
475 &self,
476 trace_event: crate::protocol_event::ToolRuntimeTraceEvent<'_>,
477 ) -> Option<RawTraceEventPayload> {
478 match trace_event {
479 crate::protocol_event::ToolRuntimeTraceEvent::Started {
480 tool_call_id,
481 payload,
482 } => {
483 let runtime_payload = self
484 .write_json_payload_best_effort(RawPayloadKind::ToolRuntimeEvent, &payload)?;
485 Some(RawTraceEventPayload::ToolCallRuntimeStarted {
486 tool_call_id: tool_call_id.to_string(),
487 runtime_payload,
488 })
489 }
490 crate::protocol_event::ToolRuntimeTraceEvent::Ended {
491 tool_call_id,
492 status,
493 payload,
494 } => {
495 let runtime_payload = self
496 .write_json_payload_best_effort(RawPayloadKind::ToolRuntimeEvent, &payload)?;
497 Some(RawTraceEventPayload::ToolCallRuntimeEnded {
498 tool_call_id: tool_call_id.to_string(),
499 status,
500 runtime_payload,
501 })
502 }
503 }
504 }
505
506 fn append_best_effort(&self, payload: RawTraceEventPayload) {
507 if let Err(err) = self.writer.append(payload) {
508 warn!("failed to append rollout trace event: {err:#}");
509 }
510 }
511
512 fn append_with_context_best_effort(
513 &self,
514 codex_turn_id: CodexTurnId,
515 payload: RawTraceEventPayload,
516 ) {
517 let event_context = RawTraceEventContext {
518 thread_id: Some(self.thread_id.clone()),
519 codex_turn_id: Some(codex_turn_id),
520 };
521 if let Err(err) = self.writer.append_with_context(event_context, payload) {
522 warn!("failed to append rollout trace event: {err:#}");
523 }
524 }
525}
526
527#[cfg(test)]
528#[path = "thread_tests.rs"]
529mod tests;