1use serde::Deserialize;
8
9#[derive(Debug, Deserialize)]
18pub struct CodexEvent {
19 #[serde(rename = "type")]
20 pub event_type: String,
21
22 #[serde(default)]
24 pub thread_id: Option<String>,
25
26 #[serde(default)]
28 pub usage: Option<CodexUsage>,
29
30 #[serde(default)]
32 pub error: Option<CodexError>,
33
34 #[serde(default)]
36 pub item: Option<CodexItem>,
37}
38
39#[derive(Debug, Deserialize)]
40pub struct CodexUsage {
41 #[serde(default)]
42 pub input_tokens: i64,
43 #[serde(default)]
44 pub cached_input_tokens: i64,
45 #[serde(default)]
46 pub output_tokens: i64,
47}
48
49#[derive(Debug, Deserialize)]
50pub struct CodexError {
51 pub message: String,
52}
53
54#[derive(Debug, Deserialize)]
60pub struct CodexItem {
61 pub id: String,
62 #[serde(rename = "type")]
63 pub item_type: String,
64
65 #[serde(default)]
67 pub text: Option<String>,
68
69 #[serde(default)]
71 pub command: Option<String>,
72 #[serde(default)]
73 pub aggregated_output: Option<String>,
74 #[serde(default)]
75 pub exit_code: Option<i32>,
76 #[serde(default)]
77 pub status: Option<String>,
78}
79
80impl CodexItem {
81 pub fn agent_text(&self) -> Option<&str> {
83 if self.item_type == "agent_message" || self.item_type == "reasoning" {
84 self.text.as_deref()
85 } else {
86 None
87 }
88 }
89}
90
91pub fn codex_sdk_args(program: &str, thread_id: Option<&str>) -> (String, Vec<String>) {
100 let mut args = vec![
101 "exec".to_string(),
102 "--json".to_string(),
103 "-m".to_string(),
104 "gpt-5.4".to_string(),
105 "--dangerously-bypass-approvals-and-sandbox".to_string(),
106 ];
107
108 if let Some(tid) = thread_id {
109 args.push("resume".to_string());
110 args.push(tid.to_string());
111 }
112
113 args.push("-".to_string());
114
115 (program.to_string(), args)
116}
117
118pub fn codex_sdk_command(program: &str, prompt: &str, thread_id: Option<&str>) -> String {
123 let escaped_prompt = prompt.replace('\'', "'\\''");
124 match thread_id {
125 Some(tid) => {
126 let escaped_tid = tid.replace('\'', "'\\''");
127 format!(
128 "exec {program} exec --json -m gpt-5.4 --dangerously-bypass-approvals-and-sandbox resume '{escaped_tid}' '{escaped_prompt}'"
129 )
130 }
131 None => {
132 format!(
133 "exec {program} exec --json -m gpt-5.4 --dangerously-bypass-approvals-and-sandbox '{escaped_prompt}'"
134 )
135 }
136 }
137}
138
139#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn parse_thread_started() {
149 let line = r#"{"type":"thread.started","thread_id":"abc-123"}"#;
150 let evt: CodexEvent = serde_json::from_str(line).unwrap();
151 assert_eq!(evt.event_type, "thread.started");
152 assert_eq!(evt.thread_id.as_deref(), Some("abc-123"));
153 }
154
155 #[test]
156 fn parse_turn_started() {
157 let line = r#"{"type":"turn.started"}"#;
158 let evt: CodexEvent = serde_json::from_str(line).unwrap();
159 assert_eq!(evt.event_type, "turn.started");
160 }
161
162 #[test]
163 fn parse_turn_completed() {
164 let line = r#"{"type":"turn.completed","usage":{"input_tokens":100,"cached_input_tokens":50,"output_tokens":30}}"#;
165 let evt: CodexEvent = serde_json::from_str(line).unwrap();
166 assert_eq!(evt.event_type, "turn.completed");
167 let usage = evt.usage.unwrap();
168 assert_eq!(usage.input_tokens, 100);
169 assert_eq!(usage.output_tokens, 30);
170 }
171
172 #[test]
173 fn parse_turn_failed() {
174 let line = r#"{"type":"turn.failed","error":{"message":"rate limit"}}"#;
175 let evt: CodexEvent = serde_json::from_str(line).unwrap();
176 assert_eq!(evt.event_type, "turn.failed");
177 assert_eq!(evt.error.unwrap().message, "rate limit");
178 }
179
180 #[test]
181 fn parse_item_agent_message() {
182 let line = r#"{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Hello world"}}"#;
183 let evt: CodexEvent = serde_json::from_str(line).unwrap();
184 assert_eq!(evt.event_type, "item.completed");
185 let item = evt.item.unwrap();
186 assert_eq!(item.item_type, "agent_message");
187 assert_eq!(item.agent_text(), Some("Hello world"));
188 }
189
190 #[test]
191 fn parse_item_command_execution() {
192 let line = r#"{"type":"item.completed","item":{"id":"item_1","type":"command_execution","command":"ls","aggregated_output":"file.txt\n","exit_code":0,"status":"completed"}}"#;
193 let evt: CodexEvent = serde_json::from_str(line).unwrap();
194 let item = evt.item.unwrap();
195 assert_eq!(item.item_type, "command_execution");
196 assert_eq!(item.command.as_deref(), Some("ls"));
197 assert_eq!(item.exit_code, Some(0));
198 assert!(item.agent_text().is_none());
199 }
200
201 #[test]
202 fn parse_error_event() {
203 let line = r#"{"type":"error","message":"fatal"}"#;
204 let evt: CodexEvent = serde_json::from_str(line).unwrap();
207 assert_eq!(evt.event_type, "error");
208 }
209
210 #[test]
211 fn unknown_event_type_tolerated() {
212 let line = r#"{"type":"future.event","some_field":42}"#;
213 let evt: CodexEvent = serde_json::from_str(line).unwrap();
214 assert_eq!(evt.event_type, "future.event");
215 }
216
217 #[test]
218 fn codex_sdk_command_new_session() {
219 let cmd = codex_sdk_command("codex", "fix the bug", None);
220 assert!(cmd.contains("exec codex exec --json"));
221 assert!(cmd.contains("-m gpt-5.4"));
222 assert!(cmd.contains("--dangerously-bypass-approvals-and-sandbox"));
223 assert!(cmd.contains("'fix the bug'"));
224 assert!(!cmd.contains("resume"));
225 }
226
227 #[test]
228 fn codex_sdk_command_resume() {
229 let cmd = codex_sdk_command("codex", "next step", Some("tid-123"));
230 assert!(cmd.contains("-m gpt-5.4"));
231 assert!(cmd.contains("resume 'tid-123'"));
232 assert!(cmd.contains("'next step'"));
233 }
234
235 #[test]
236 fn codex_sdk_command_escapes_quotes() {
237 let cmd = codex_sdk_command("codex", "fix user's bug", None);
238 assert!(cmd.contains("user'\\''s"));
239 }
240
241 #[test]
242 fn codex_sdk_args_new_session() {
243 let (program, args) = codex_sdk_args("codex", None);
244 assert_eq!(program, "codex");
245 assert_eq!(
246 args,
247 vec![
248 "exec",
249 "--json",
250 "-m",
251 "gpt-5.4",
252 "--dangerously-bypass-approvals-and-sandbox",
253 "-"
254 ]
255 );
256 }
257
258 #[test]
259 fn codex_sdk_args_resume() {
260 let (program, args) = codex_sdk_args("codex", Some("tid-123"));
261 assert_eq!(program, "codex");
262 assert_eq!(
263 args,
264 vec![
265 "exec",
266 "--json",
267 "-m",
268 "gpt-5.4",
269 "--dangerously-bypass-approvals-and-sandbox",
270 "resume",
271 "tid-123",
272 "-"
273 ]
274 );
275 }
276}