batty_cli/shim/
codex_types.rs1use 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_command(program: &str, prompt: &str, thread_id: Option<&str>) -> String {
100 let escaped_prompt = prompt.replace('\'', "'\\''");
101 match thread_id {
102 Some(tid) => {
103 let escaped_tid = tid.replace('\'', "'\\''");
104 format!(
105 "exec {program} exec --json --dangerously-bypass-approvals-and-sandbox resume '{escaped_tid}' '{escaped_prompt}'"
106 )
107 }
108 None => {
109 format!(
110 "exec {program} exec --json --dangerously-bypass-approvals-and-sandbox '{escaped_prompt}'"
111 )
112 }
113 }
114}
115
116#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn parse_thread_started() {
126 let line = r#"{"type":"thread.started","thread_id":"abc-123"}"#;
127 let evt: CodexEvent = serde_json::from_str(line).unwrap();
128 assert_eq!(evt.event_type, "thread.started");
129 assert_eq!(evt.thread_id.as_deref(), Some("abc-123"));
130 }
131
132 #[test]
133 fn parse_turn_started() {
134 let line = r#"{"type":"turn.started"}"#;
135 let evt: CodexEvent = serde_json::from_str(line).unwrap();
136 assert_eq!(evt.event_type, "turn.started");
137 }
138
139 #[test]
140 fn parse_turn_completed() {
141 let line = r#"{"type":"turn.completed","usage":{"input_tokens":100,"cached_input_tokens":50,"output_tokens":30}}"#;
142 let evt: CodexEvent = serde_json::from_str(line).unwrap();
143 assert_eq!(evt.event_type, "turn.completed");
144 let usage = evt.usage.unwrap();
145 assert_eq!(usage.input_tokens, 100);
146 assert_eq!(usage.output_tokens, 30);
147 }
148
149 #[test]
150 fn parse_turn_failed() {
151 let line = r#"{"type":"turn.failed","error":{"message":"rate limit"}}"#;
152 let evt: CodexEvent = serde_json::from_str(line).unwrap();
153 assert_eq!(evt.event_type, "turn.failed");
154 assert_eq!(evt.error.unwrap().message, "rate limit");
155 }
156
157 #[test]
158 fn parse_item_agent_message() {
159 let line = r#"{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Hello world"}}"#;
160 let evt: CodexEvent = serde_json::from_str(line).unwrap();
161 assert_eq!(evt.event_type, "item.completed");
162 let item = evt.item.unwrap();
163 assert_eq!(item.item_type, "agent_message");
164 assert_eq!(item.agent_text(), Some("Hello world"));
165 }
166
167 #[test]
168 fn parse_item_command_execution() {
169 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"}}"#;
170 let evt: CodexEvent = serde_json::from_str(line).unwrap();
171 let item = evt.item.unwrap();
172 assert_eq!(item.item_type, "command_execution");
173 assert_eq!(item.command.as_deref(), Some("ls"));
174 assert_eq!(item.exit_code, Some(0));
175 assert!(item.agent_text().is_none());
176 }
177
178 #[test]
179 fn parse_error_event() {
180 let line = r#"{"type":"error","message":"fatal"}"#;
181 let evt: CodexEvent = serde_json::from_str(line).unwrap();
184 assert_eq!(evt.event_type, "error");
185 }
186
187 #[test]
188 fn unknown_event_type_tolerated() {
189 let line = r#"{"type":"future.event","some_field":42}"#;
190 let evt: CodexEvent = serde_json::from_str(line).unwrap();
191 assert_eq!(evt.event_type, "future.event");
192 }
193
194 #[test]
195 fn codex_sdk_command_new_session() {
196 let cmd = codex_sdk_command("codex", "fix the bug", None);
197 assert!(cmd.contains("exec codex exec --json"));
198 assert!(cmd.contains("--dangerously-bypass-approvals-and-sandbox"));
199 assert!(cmd.contains("'fix the bug'"));
200 assert!(!cmd.contains("resume"));
201 }
202
203 #[test]
204 fn codex_sdk_command_resume() {
205 let cmd = codex_sdk_command("codex", "next step", Some("tid-123"));
206 assert!(cmd.contains("resume 'tid-123'"));
207 assert!(cmd.contains("'next step'"));
208 }
209
210 #[test]
211 fn codex_sdk_command_escapes_quotes() {
212 let cmd = codex_sdk_command("codex", "fix user's bug", None);
213 assert!(cmd.contains("user'\\''s"));
214 }
215}