agtrace_providers/codex/
io.rs1use anyhow::{Context, Result};
2use std::io::{BufRead, BufReader};
3use std::path::Path;
4
5use super::parser::normalize_codex_session;
6use super::schema::CodexRecord;
7
8pub fn normalize_codex_file(path: &Path) -> Result<Vec<agtrace_types::AgentEvent>> {
10 let text = std::fs::read_to_string(path)
11 .with_context(|| format!("Failed to read Codex file: {}", path.display()))?;
12
13 let mut records: Vec<CodexRecord> = Vec::new();
14 let mut session_id_from_meta: Option<String> = None;
15
16 for line in text.lines() {
17 let line = line.trim();
18 if line.is_empty() {
19 continue;
20 }
21 let record: CodexRecord = serde_json::from_str(line)
22 .with_context(|| format!("Failed to parse JSON line: {}", line))?;
23
24 if let CodexRecord::SessionMeta(ref meta) = record {
26 session_id_from_meta = Some(meta.payload.id.clone());
27 }
28
29 records.push(record);
30 }
31
32 let session_id = session_id_from_meta.unwrap_or_else(|| "unknown-session".to_string());
34
35 Ok(normalize_codex_session(records, &session_id))
36}
37
38pub fn extract_cwd_from_codex_file(path: &Path) -> Option<String> {
40 let file = std::fs::File::open(path).ok()?;
41 let reader = BufReader::new(file);
42
43 for line in reader.lines().take(10).flatten() {
44 if let Ok(record) = serde_json::from_str::<CodexRecord>(&line) {
45 match record {
46 CodexRecord::SessionMeta(meta) => {
47 return Some(meta.payload.cwd.clone());
48 }
49 CodexRecord::TurnContext(turn) => {
50 return Some(turn.payload.cwd.clone());
51 }
52 _ => continue,
53 }
54 }
55 }
56 None
57}
58
59#[derive(Debug)]
60pub struct CodexHeader {
61 pub session_id: Option<String>,
62 pub cwd: Option<String>,
63 pub timestamp: Option<String>,
64 pub snippet: Option<String>,
65}
66
67pub fn extract_codex_header(path: &Path) -> Result<CodexHeader> {
69 let file = std::fs::File::open(path)
70 .with_context(|| format!("Failed to open file: {}", path.display()))?;
71 let reader = BufReader::new(file);
72
73 let mut session_id = None;
74 let mut cwd = None;
75 let mut timestamp = None;
76 let mut snippet = None;
77
78 for line in reader.lines().take(20).flatten() {
79 if let Ok(record) = serde_json::from_str::<CodexRecord>(&line) {
80 match &record {
81 CodexRecord::SessionMeta(meta) => {
82 if session_id.is_none() {
83 session_id = Some(meta.payload.id.clone());
84 }
85 if cwd.is_none() {
86 cwd = Some(meta.payload.cwd.clone());
87 }
88 if timestamp.is_none() {
89 timestamp = Some(meta.timestamp.clone());
90 }
91 }
92 CodexRecord::TurnContext(turn) => {
93 if cwd.is_none() {
94 cwd = Some(turn.payload.cwd.clone());
95 }
96 if timestamp.is_none() {
97 timestamp = Some(turn.timestamp.clone());
98 }
99 }
100 CodexRecord::EventMsg(event) => {
101 if timestamp.is_none() {
102 timestamp = Some(event.timestamp.clone());
103 }
104 if snippet.is_none()
105 && let super::schema::EventMsgPayload::UserMessage(msg) = &event.payload
106 {
107 snippet = Some(msg.message.clone());
108 }
109 }
110 CodexRecord::ResponseItem(response) => {
111 if timestamp.is_none() {
112 timestamp = Some(response.timestamp.clone());
113 }
114 if snippet.is_none()
115 && let super::schema::ResponseItemPayload::Message(msg) = &response.payload
116 && msg.role == "user"
117 {
118 let text = msg.content.iter().find_map(|c| match c {
119 super::schema::MessageContent::InputText { text } => Some(text.clone()),
120 super::schema::MessageContent::OutputText { text } => {
121 Some(text.clone())
122 }
123 _ => None,
124 });
125 if let Some(t) = &text
126 && !t.contains("<environment_context>")
127 {
128 snippet = text;
129 }
130 }
131 }
132 _ => {}
133 }
134
135 if session_id.is_some() && cwd.is_some() && timestamp.is_some() && snippet.is_some() {
136 break;
137 }
138 }
139 }
140
141 Ok(CodexHeader {
142 session_id,
143 cwd,
144 timestamp,
145 snippet,
146 })
147}
148
149pub fn is_empty_codex_session(path: &Path) -> bool {
151 let Ok(file) = std::fs::File::open(path) else {
152 return true;
153 };
154 let reader = BufReader::new(file);
155
156 let mut line_count = 0;
157 let mut has_event = false;
158
159 for line in reader.lines().take(20).flatten() {
160 line_count += 1;
161 if let Ok(record) = serde_json::from_str::<CodexRecord>(&line) {
162 match record {
163 CodexRecord::SessionMeta(_) | CodexRecord::TurnContext(_) => {
164 has_event = true;
165 break;
166 }
167 CodexRecord::EventMsg(_) | CodexRecord::ResponseItem(_) => {
168 has_event = true;
169 break;
170 }
171 _ => {}
172 }
173 }
174 }
175
176 line_count <= 2 && !has_event
177}