Codex Handbook
rollout-trace/src/writer.rs 265 lines
//! Hot-path trace bundle writer.use std::fs::File;use std::fs::OpenOptions;use std::io::BufWriter;use std::io::Write;use std::path::Path;use std::path::PathBuf;use std::sync::Mutex;use std::sync::MutexGuard;use std::sync::PoisonError;use std::time::SystemTime;use std::time::UNIX_EPOCH;use anyhow::Context;use anyhow::Result;use serde::Serialize;use crate::bundle::MANIFEST_FILE_NAME;use crate::bundle::PAYLOADS_DIR_NAME;use crate::bundle::RAW_EVENT_LOG_FILE_NAME;use crate::bundle::TraceBundleManifest;use crate::model::AgentThreadId;use crate::payload::RawPayloadKind;use crate::payload::RawPayloadRef;use crate::raw_event::RAW_TRACE_EVENT_SCHEMA_VERSION;use crate::raw_event::RawTraceEvent;use crate::raw_event::RawTraceEventContext;use crate::raw_event::RawTraceEventPayload;/// Local trace bundle writer.////// The writer appends raw events and writes payload files. It does not keep a/// reduced `RolloutTrace` in memory; replay is owned by the reducer.#[derive(Debug)]pub struct TraceWriter {    inner: Mutex<TraceWriterInner>,}#[derive(Debug)]struct TraceWriterInner {    manifest: TraceBundleManifest,    payloads_dir: PathBuf,    event_log: BufWriter<File>,    next_seq: u64,    next_payload_ordinal: u64,}impl TraceWriter {    /// Creates a trace bundle directory and writes its manifest.    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,            }),        })    }    /// Writes a JSON payload file and returns its reduced-state reference.    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"));        // Payload files are created before the event that references them. A        // replay interrupted after an event is appended should never point at a        // payload file that the writer planned but had not written yet.        write_json_file(&absolute_path, value)?;        Ok(RawPayloadRef {            raw_payload_id,            kind,            path: relative_path,        })    }    /// Appends one raw event with no extra envelope context.    pub fn append(&self, payload: RawTraceEventPayload) -> Result<RawTraceEvent> {        self.append_with_context(RawTraceEventContext::default(), payload)    }    /// Appends one raw event with explicit thread/turn context.    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)    }    fn lock_inner(&self) -> MutexGuard<'_, TraceWriterInner> {        // Preserve the event log after a panic in tracing code. Dropping the        // writer would lose subsequent diagnostic events in exactly the session        // we are trying to debug.        self.inner.lock().unwrap_or_else(PoisonError::into_inner)    }}fn write_json_file(path: &Path, value: &impl Serialize) -> Result<()> {    let file = File::create(path).with_context(|| format!("create {}", path.display()))?;    serde_json::to_writer_pretty(file, value)        .with_context(|| format!("write JSON {}", path.display()))}pub(crate) fn unix_time_ms() -> i64 {    let duration = SystemTime::now()        .duration_since(UNIX_EPOCH)        .unwrap_or_default();    i64::try_from(duration.as_millis()).unwrap_or(i64::MAX)}#[cfg(test)]mod tests {    use pretty_assertions::assert_eq;    use serde_json::json;    use tempfile::TempDir;    use crate::model::ExecutionStatus;    use crate::model::RolloutStatus;    use crate::payload::RawPayloadKind;    use crate::raw_event::RawTraceEventPayload;    use crate::replay_bundle;    use crate::writer::TraceWriter;    #[test]    fn writer_records_payload_refs_and_replays_rollout_status() -> anyhow::Result<()> {        let temp = TempDir::new()?;        let writer = TraceWriter::create(            temp.path(),            "trace-1".to_string(),            "rollout-1".to_string(),            "thread-root".to_string(),        )?;        writer.append(RawTraceEventPayload::RolloutStarted {            trace_id: "trace-1".to_string(),            root_thread_id: "thread-root".to_string(),        })?;        let metadata_payload = writer.write_json_payload(            RawPayloadKind::ProtocolEvent,            &json!({                "source": "test",                "model": "gpt-test",            }),        )?;        writer.append(RawTraceEventPayload::ThreadStarted {            thread_id: "thread-root".to_string(),            agent_path: "/root".to_string(),            metadata_payload: Some(metadata_payload.clone()),        })?;        writer.append(RawTraceEventPayload::CodexTurnStarted {            codex_turn_id: "turn-1".to_string(),            thread_id: "thread-root".to_string(),        })?;        let inference_request = writer.write_json_payload(            RawPayloadKind::InferenceRequest,            &json!({                "model": "gpt-test",                "input": [{                    "type": "message",                    "role": "user",                    "content": [{"type": "input_text", "text": "hello"}]                }],            }),        )?;        writer.append(RawTraceEventPayload::InferenceStarted {            inference_call_id: "inference-1".to_string(),            thread_id: "thread-root".to_string(),            codex_turn_id: "turn-1".to_string(),            model: "gpt-test".to_string(),            provider_name: "test-provider".to_string(),            request_payload: inference_request.clone(),        })?;        let inference_response = writer.write_json_payload(            RawPayloadKind::InferenceResponse,            &json!({                "response_id": "resp-1",                "output_items": [],            }),        )?;        writer.append(RawTraceEventPayload::InferenceCompleted {            inference_call_id: "inference-1".to_string(),            response_id: Some("resp-1".to_string()),            upstream_request_id: Some("req-1".to_string()),            response_payload: inference_response.clone(),        })?;        writer.append(RawTraceEventPayload::CodexTurnEnded {            codex_turn_id: "turn-1".to_string(),            status: ExecutionStatus::Completed,        })?;        writer.append(RawTraceEventPayload::RolloutEnded {            status: RolloutStatus::Completed,        })?;        let rollout = replay_bundle(temp.path())?;        assert_eq!(rollout.status, RolloutStatus::Completed);        assert_eq!(rollout.root_thread_id, "thread-root");        assert_eq!(rollout.threads["thread-root"].agent_path, "/root");        assert_eq!(rollout.codex_turns["turn-1"].thread_id, "thread-root");        assert_eq!(            rollout.codex_turns["turn-1"].execution.status,            ExecutionStatus::Completed,        );        assert_eq!(            rollout.inference_calls["inference-1"].raw_request_payload_id,            inference_request.raw_payload_id,        );        assert_eq!(            rollout.inference_calls["inference-1"].raw_response_payload_id,            Some(inference_response.raw_payload_id),        );        assert_eq!(            rollout.raw_payloads[&metadata_payload.raw_payload_id].path,            "payloads/1.json"        );        Ok(())    }}