diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index 613fedf0a1..53af25c7e9 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -51,6 +51,10 @@ pub struct Cli { #[arg(long = "color", value_enum, default_value_t = Color::Auto)] pub color: Color, + /// Print events to stdout as JSONL. + #[arg(long = "json", default_value_t = false)] + pub json: bool, + /// Specifies file where the last message from the agent should be written. #[arg(long = "output-last-message")] pub last_message_file: Option, diff --git a/codex-rs/exec/src/event_processor.rs b/codex-rs/exec/src/event_processor.rs index 5ab09994b1..79d1005b1e 100644 --- a/codex-rs/exec/src/event_processor.rs +++ b/codex-rs/exec/src/event_processor.rs @@ -30,7 +30,15 @@ use std::time::Instant; /// a limit so they can see the full transcript. const MAX_OUTPUT_LINES_FOR_EXEC_TOOL_CALL: usize = 20; -pub(crate) struct EventProcessor { +pub(crate) trait EventProcessor { + /// Print summary of effective configuration and user prompt. + fn print_config_summary(&mut self, config: &Config, prompt: &str); + + /// Handle a single event emitted by the agent. + fn process_event(&mut self, event: Event); +} + +pub(crate) struct EventProcessorWithHumanOutput { call_id_to_command: HashMap, call_id_to_patch: HashMap, @@ -57,7 +65,7 @@ pub(crate) struct EventProcessor { reasoning_started: bool, } -impl EventProcessor { +impl EventProcessorWithHumanOutput { pub(crate) fn create_with_ansi(with_ansi: bool, config: &Config) -> Self { let call_id_to_command = HashMap::new(); let call_id_to_patch = HashMap::new(); @@ -128,11 +136,11 @@ macro_rules! ts_println { }}; } -impl EventProcessor { +impl EventProcessor for EventProcessorWithHumanOutput { /// Print a concise summary of the effective configuration that will be used /// for the session. This mirrors the information shown in the TUI welcome /// screen. - pub(crate) fn print_config_summary(&mut self, config: &Config, prompt: &str) { + fn print_config_summary(&mut self, config: &Config, prompt: &str) { const VERSION: &str = env!("CARGO_PKG_VERSION"); ts_println!( self, @@ -177,7 +185,7 @@ impl EventProcessor { ); } - pub(crate) fn process_event(&mut self, event: Event) { + fn process_event(&mut self, event: Event) { let Event { id: _, msg } = event; match msg { EventMsg::Error(ErrorEvent { message }) => { diff --git a/codex-rs/exec/src/event_processor_with_json_output.rs b/codex-rs/exec/src/event_processor_with_json_output.rs new file mode 100644 index 0000000000..fe88e82722 --- /dev/null +++ b/codex-rs/exec/src/event_processor_with_json_output.rs @@ -0,0 +1,33 @@ +use codex_core::config::Config; +use codex_core::protocol::Event; +use codex_core::protocol::EventMsg; + +use crate::event_processor::EventProcessor; + +pub(crate) struct EventProcessorWithJsonOutput; + +impl EventProcessorWithJsonOutput { + pub fn new() -> Self { + Self {} + } +} + +impl EventProcessor for EventProcessorWithJsonOutput { + fn print_config_summary(&mut self, _config: &Config, _prompt: &str) { + let _ = _config; + // Intentionally left blank – human summary not needed in JSON mode. + } + + fn process_event(&mut self, event: Event) { + match event.msg { + EventMsg::AgentMessageDelta(_) | EventMsg::AgentReasoningDelta(_) => { + // Suppress streaming events in JSON mode. + } + _ => { + if let Ok(line) = serde_json::to_string(&event) { + println!("{line}"); + } + } + } + } +} diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index afefed1a93..95b333cd16 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -1,5 +1,6 @@ mod cli; mod event_processor; +mod event_processor_with_json_output; use std::io::IsTerminal; use std::io::Read; @@ -19,12 +20,15 @@ use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::TaskCompleteEvent; use codex_core::util::is_inside_git_repo; -use event_processor::EventProcessor; +use event_processor::EventProcessorWithHumanOutput; +use event_processor_with_json_output::EventProcessorWithJsonOutput; use tracing::debug; use tracing::error; use tracing::info; use tracing_subscriber::EnvFilter; +use crate::event_processor::EventProcessor; + pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> anyhow::Result<()> { let Cli { images, @@ -36,6 +40,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any skip_git_repo_check, color, last_message_file, + json: json_mode, sandbox_mode: sandbox_mode_cli_arg, prompt, config_overrides, @@ -115,9 +120,16 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any }; let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?; - let mut event_processor = EventProcessor::create_with_ansi(stdout_with_ansi, &config); - // Print the effective configuration and prompt so users can see what Codex - // is using. + let mut event_processor: Box = if json_mode { + Box::new(EventProcessorWithJsonOutput::new()) + } else { + Box::new(EventProcessorWithHumanOutput::create_with_ansi( + stdout_with_ansi, + &config, + )) + }; + + // Human processor prints summary; JSON processor is a no-op. event_processor.print_config_summary(&config, &prompt); if !skip_git_repo_check && !is_inside_git_repo(&config) {