use serde::Deserialize;
pub const CODEX_DEFAULT_MODEL: &str = "gpt-5.5";
pub const CODEX_DEFAULT_REASONING_EFFORT: &str = "high";
#[derive(Debug, Deserialize)]
pub struct CodexEvent {
#[serde(rename = "type")]
pub event_type: String,
#[serde(default)]
pub thread_id: Option<String>,
#[serde(default)]
pub usage: Option<CodexUsage>,
#[serde(default)]
pub error: Option<CodexError>,
#[serde(default)]
pub item: Option<CodexItem>,
}
#[derive(Debug, Deserialize)]
pub struct CodexUsage {
#[serde(default)]
pub input_tokens: i64,
#[serde(default)]
pub cached_input_tokens: i64,
#[serde(default)]
pub output_tokens: i64,
}
#[derive(Debug, Deserialize)]
pub struct CodexError {
pub message: String,
}
#[derive(Debug, Deserialize)]
pub struct CodexItem {
pub id: String,
#[serde(rename = "type")]
pub item_type: String,
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub command: Option<String>,
#[serde(default)]
pub aggregated_output: Option<String>,
#[serde(default)]
pub exit_code: Option<i32>,
#[serde(default)]
pub status: Option<String>,
}
impl CodexItem {
pub fn agent_text(&self) -> Option<&str> {
if self.item_type == "agent_message" || self.item_type == "reasoning" {
self.text.as_deref()
} else {
None
}
}
}
pub fn codex_sdk_args(program: &str, thread_id: Option<&str>) -> (String, Vec<String>) {
let reasoning_config = format!("model_reasoning_effort=\"{CODEX_DEFAULT_REASONING_EFFORT}\"");
let mut args = vec![
"exec".to_string(),
"--json".to_string(),
"-m".to_string(),
CODEX_DEFAULT_MODEL.to_string(),
"-c".to_string(),
reasoning_config,
"--dangerously-bypass-approvals-and-sandbox".to_string(),
];
if let Some(tid) = thread_id {
args.push("resume".to_string());
args.push(tid.to_string());
}
args.push("-".to_string());
(program.to_string(), args)
}
pub fn codex_sdk_command(program: &str, prompt: &str, thread_id: Option<&str>) -> String {
let escaped_prompt = prompt.replace('\'', "'\\''");
let reasoning_config = format!("model_reasoning_effort=\"{CODEX_DEFAULT_REASONING_EFFORT}\"");
match thread_id {
Some(tid) => {
let escaped_tid = tid.replace('\'', "'\\''");
format!(
"exec {program} exec --json -m {CODEX_DEFAULT_MODEL} -c '{reasoning_config}' --dangerously-bypass-approvals-and-sandbox resume '{escaped_tid}' '{escaped_prompt}'"
)
}
None => {
format!(
"exec {program} exec --json -m {CODEX_DEFAULT_MODEL} -c '{reasoning_config}' --dangerously-bypass-approvals-and-sandbox '{escaped_prompt}'"
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_thread_started() {
let line = r#"{"type":"thread.started","thread_id":"abc-123"}"#;
let evt: CodexEvent = serde_json::from_str(line).unwrap();
assert_eq!(evt.event_type, "thread.started");
assert_eq!(evt.thread_id.as_deref(), Some("abc-123"));
}
#[test]
fn parse_turn_started() {
let line = r#"{"type":"turn.started"}"#;
let evt: CodexEvent = serde_json::from_str(line).unwrap();
assert_eq!(evt.event_type, "turn.started");
}
#[test]
fn parse_turn_completed() {
let line = r#"{"type":"turn.completed","usage":{"input_tokens":100,"cached_input_tokens":50,"output_tokens":30}}"#;
let evt: CodexEvent = serde_json::from_str(line).unwrap();
assert_eq!(evt.event_type, "turn.completed");
let usage = evt.usage.unwrap();
assert_eq!(usage.input_tokens, 100);
assert_eq!(usage.output_tokens, 30);
}
#[test]
fn parse_turn_failed() {
let line = r#"{"type":"turn.failed","error":{"message":"rate limit"}}"#;
let evt: CodexEvent = serde_json::from_str(line).unwrap();
assert_eq!(evt.event_type, "turn.failed");
assert_eq!(evt.error.unwrap().message, "rate limit");
}
#[test]
fn parse_item_agent_message() {
let line = r#"{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"Hello world"}}"#;
let evt: CodexEvent = serde_json::from_str(line).unwrap();
assert_eq!(evt.event_type, "item.completed");
let item = evt.item.unwrap();
assert_eq!(item.item_type, "agent_message");
assert_eq!(item.agent_text(), Some("Hello world"));
}
#[test]
fn parse_item_command_execution() {
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"}}"#;
let evt: CodexEvent = serde_json::from_str(line).unwrap();
let item = evt.item.unwrap();
assert_eq!(item.item_type, "command_execution");
assert_eq!(item.command.as_deref(), Some("ls"));
assert_eq!(item.exit_code, Some(0));
assert!(item.agent_text().is_none());
}
#[test]
fn parse_error_event() {
let line = r#"{"type":"error","message":"fatal"}"#;
let evt: CodexEvent = serde_json::from_str(line).unwrap();
assert_eq!(evt.event_type, "error");
}
#[test]
fn unknown_event_type_tolerated() {
let line = r#"{"type":"future.event","some_field":42}"#;
let evt: CodexEvent = serde_json::from_str(line).unwrap();
assert_eq!(evt.event_type, "future.event");
}
#[test]
fn codex_sdk_command_new_session() {
let cmd = codex_sdk_command("codex", "fix the bug", None);
assert!(cmd.contains("exec codex exec --json"));
assert!(cmd.contains("-m gpt-5.5"));
assert!(cmd.contains("-c 'model_reasoning_effort=\"high\"'"));
assert!(cmd.contains("--dangerously-bypass-approvals-and-sandbox"));
assert!(cmd.contains("'fix the bug'"));
assert!(!cmd.contains("resume"));
}
#[test]
fn codex_sdk_command_resume() {
let cmd = codex_sdk_command("codex", "next step", Some("tid-123"));
assert!(cmd.contains("-m gpt-5.5"));
assert!(cmd.contains("-c 'model_reasoning_effort=\"high\"'"));
assert!(cmd.contains("resume 'tid-123'"));
assert!(cmd.contains("'next step'"));
}
#[test]
fn codex_sdk_command_escapes_quotes() {
let cmd = codex_sdk_command("codex", "fix user's bug", None);
assert!(cmd.contains("user'\\''s"));
}
#[test]
fn codex_sdk_args_new_session() {
let (program, args) = codex_sdk_args("codex", None);
assert_eq!(program, "codex");
assert_eq!(
args,
vec![
"exec",
"--json",
"-m",
"gpt-5.5",
"-c",
"model_reasoning_effort=\"high\"",
"--dangerously-bypass-approvals-and-sandbox",
"-"
]
);
}
#[test]
fn codex_sdk_args_resume() {
let (program, args) = codex_sdk_args("codex", Some("tid-123"));
assert_eq!(program, "codex");
assert_eq!(
args,
vec![
"exec",
"--json",
"-m",
"gpt-5.5",
"-c",
"model_reasoning_effort=\"high\"",
"--dangerously-bypass-approvals-and-sandbox",
"resume",
"tid-123",
"-"
]
);
}
}