Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion codex-rs/exec/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ pub struct Cli {
pub include_plan_tool: bool,

/// Specifies file where the last message from the agent should be written.
#[arg(long = "output-last-message")]
#[arg(long = "output-last-message", short = 'o', value_name = "FILE")]
pub last_message_file: Option<PathBuf>,

/// Initial instructions for the agent. If not provided as an argument (or
Expand Down
120 changes: 69 additions & 51 deletions codex-rs/exec/src/event_processor_with_human_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub(crate) struct EventProcessorWithHumanOutput {
show_raw_agent_reasoning: bool,
last_message_path: Option<PathBuf>,
last_total_token_usage: Option<codex_core::protocol::TokenUsageInfo>,
final_message: Option<String>,
}

impl EventProcessorWithHumanOutput {
Expand All @@ -83,6 +84,7 @@ impl EventProcessorWithHumanOutput {
show_raw_agent_reasoning: config.show_raw_agent_reasoning,
last_message_path,
last_total_token_usage: None,
final_message: None,
}
} else {
Self {
Expand All @@ -98,6 +100,7 @@ impl EventProcessorWithHumanOutput {
show_raw_agent_reasoning: config.show_raw_agent_reasoning,
last_message_path,
last_total_token_usage: None,
final_message: None,
}
}
}
Expand All @@ -108,11 +111,10 @@ struct PatchApplyBegin {
auto_approved: bool,
}

// Timestamped println helper. The timestamp is styled with self.dimmed.
#[macro_export]
macro_rules! ts_println {
/// Timestamped helper. The timestamp is styled with self.dimmed.
macro_rules! ts_msg {
($self:ident, $($arg:tt)*) => {{
println!($($arg)*);
eprintln!($($arg)*);
}};
}

Expand All @@ -127,7 +129,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
session_configured_event: &SessionConfiguredEvent,
) {
const VERSION: &str = env!("CARGO_PKG_VERSION");
ts_println!(
ts_msg!(
self,
"OpenAI Codex v{} (research preview)\n--------",
VERSION
Expand All @@ -140,37 +142,41 @@ impl EventProcessor for EventProcessorWithHumanOutput {
));

for (key, value) in entries {
println!("{} {}", format!("{key}:").style(self.bold), value);
eprintln!("{} {}", format!("{key}:").style(self.bold), value);
}

println!("--------");
eprintln!("--------");

// Echo the prompt that will be sent to the agent so it is visible in the
// transcript/logs before any events come in. Note the prompt may have been
// read from stdin, so it may not be visible in the terminal otherwise.
ts_println!(self, "{}\n{}", "user".style(self.cyan), prompt);
ts_msg!(self, "{}\n{}", "user".style(self.cyan), prompt);
}

fn process_event(&mut self, event: Event) -> CodexStatus {
let Event { id: _, msg } = event;
match msg {
EventMsg::Error(ErrorEvent { message }) => {
let prefix = "ERROR:".style(self.red);
ts_println!(self, "{prefix} {message}");
ts_msg!(self, "{prefix} {message}");
}
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
ts_println!(self, "{}", message.style(self.dimmed));
ts_msg!(self, "{}", message.style(self.dimmed));
}
EventMsg::StreamError(StreamErrorEvent { message }) => {
ts_println!(self, "{}", message.style(self.dimmed));
ts_msg!(self, "{}", message.style(self.dimmed));
}
EventMsg::TaskStarted(_) => {
// Ignore.
}
EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => {
let last_message = last_agent_message.as_deref();
if let Some(output_file) = self.last_message_path.as_deref() {
handle_last_message(last_agent_message.as_deref(), output_file);
handle_last_message(last_message, output_file);
}

self.final_message = last_agent_message;

return CodexStatus::InitiateShutdown;
}
EventMsg::TokenCount(ev) => {
Expand All @@ -181,11 +187,11 @@ impl EventProcessor for EventProcessorWithHumanOutput {
if !self.show_agent_reasoning {
return CodexStatus::Running;
}
println!();
eprintln!();
}
EventMsg::AgentReasoningRawContent(AgentReasoningRawContentEvent { text }) => {
if self.show_raw_agent_reasoning {
ts_println!(
ts_msg!(
self,
"{}\n{}",
"thinking".style(self.italic).style(self.magenta),
Expand All @@ -194,15 +200,15 @@ impl EventProcessor for EventProcessorWithHumanOutput {
}
}
EventMsg::AgentMessage(AgentMessageEvent { message }) => {
ts_println!(
ts_msg!(
self,
"{}\n{}",
"codex".style(self.italic).style(self.magenta),
message,
);
}
EventMsg::ExecCommandBegin(ExecCommandBeginEvent { command, cwd, .. }) => {
print!(
eprint!(
"{}\n{} in {}",
"exec".style(self.italic).style(self.magenta),
escape_command(&command).style(self.bold),
Expand All @@ -226,20 +232,20 @@ impl EventProcessor for EventProcessorWithHumanOutput {
match exit_code {
0 => {
let title = format!(" succeeded{duration}:");
ts_println!(self, "{}", title.style(self.green));
ts_msg!(self, "{}", title.style(self.green));
}
_ => {
let title = format!(" exited {exit_code}{duration}:");
ts_println!(self, "{}", title.style(self.red));
ts_msg!(self, "{}", title.style(self.red));
}
}
println!("{}", truncated_output.style(self.dimmed));
eprintln!("{}", truncated_output.style(self.dimmed));
}
EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
call_id: _,
invocation,
}) => {
ts_println!(
ts_msg!(
self,
"{} {}",
"tool".style(self.magenta),
Expand All @@ -264,21 +270,21 @@ impl EventProcessor for EventProcessorWithHumanOutput {
format_mcp_invocation(&invocation)
);

ts_println!(self, "{}", title.style(title_style));
ts_msg!(self, "{}", title.style(title_style));

if let Ok(res) = result {
let val: serde_json::Value = res.into();
let pretty =
serde_json::to_string_pretty(&val).unwrap_or_else(|_| val.to_string());

for line in pretty.lines().take(MAX_OUTPUT_LINES_FOR_EXEC_TOOL_CALL) {
println!("{}", line.style(self.dimmed));
eprintln!("{}", line.style(self.dimmed));
}
}
}
EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id: _ }) => {}
EventMsg::WebSearchEnd(WebSearchEndEvent { call_id: _, query }) => {
ts_println!(self, "🌐 Searched: {query}");
ts_msg!(self, "🌐 Searched: {query}");
}
EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
call_id,
Expand All @@ -295,7 +301,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
},
);

ts_println!(
ts_msg!(
self,
"{}",
"file update".style(self.magenta).style(self.italic),
Expand All @@ -311,9 +317,9 @@ impl EventProcessor for EventProcessorWithHumanOutput {
format_file_change(change),
path.to_string_lossy()
);
println!("{}", header.style(self.magenta));
eprintln!("{}", header.style(self.magenta));
for line in content.lines() {
println!("{}", line.style(self.green));
eprintln!("{}", line.style(self.green));
}
}
FileChange::Delete { content } => {
Expand All @@ -322,9 +328,9 @@ impl EventProcessor for EventProcessorWithHumanOutput {
format_file_change(change),
path.to_string_lossy()
);
println!("{}", header.style(self.magenta));
eprintln!("{}", header.style(self.magenta));
for line in content.lines() {
println!("{}", line.style(self.red));
eprintln!("{}", line.style(self.red));
}
}
FileChange::Update {
Expand All @@ -341,20 +347,20 @@ impl EventProcessor for EventProcessorWithHumanOutput {
} else {
format!("{} {}", format_file_change(change), path.to_string_lossy())
};
println!("{}", header.style(self.magenta));
eprintln!("{}", header.style(self.magenta));

// Colorize diff lines. We keep file header lines
// (--- / +++) without extra coloring so they are
// still readable.
for diff_line in unified_diff.lines() {
if diff_line.starts_with('+') && !diff_line.starts_with("+++") {
println!("{}", diff_line.style(self.green));
eprintln!("{}", diff_line.style(self.green));
} else if diff_line.starts_with('-')
&& !diff_line.starts_with("---")
{
println!("{}", diff_line.style(self.red));
eprintln!("{}", diff_line.style(self.red));
} else {
println!("{diff_line}");
eprintln!("{diff_line}");
}
}
}
Expand Down Expand Up @@ -391,18 +397,18 @@ impl EventProcessor for EventProcessorWithHumanOutput {
};

let title = format!("{label} exited {exit_code}{duration}:");
ts_println!(self, "{}", title.style(title_style));
ts_msg!(self, "{}", title.style(title_style));
for line in output.lines() {
println!("{}", line.style(self.dimmed));
eprintln!("{}", line.style(self.dimmed));
}
}
EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => {
ts_println!(
ts_msg!(
self,
"{}",
"file update:".style(self.magenta).style(self.italic)
);
println!("{unified_diff}");
eprintln!("{unified_diff}");
}
EventMsg::ExecApprovalRequest(_) => {
// Should we exit?
Expand All @@ -412,7 +418,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
}
EventMsg::AgentReasoning(agent_reasoning_event) => {
if self.show_agent_reasoning {
ts_println!(
ts_msg!(
self,
"{}\n{}",
"thinking".style(self.italic).style(self.magenta),
Expand All @@ -431,41 +437,41 @@ impl EventProcessor for EventProcessorWithHumanOutput {
rollout_path: _,
} = session_configured_event;

ts_println!(
ts_msg!(
self,
"{} {}",
"codex session".style(self.magenta).style(self.bold),
conversation_id.to_string().style(self.dimmed)
);

ts_println!(self, "model: {}", model);
println!();
ts_msg!(self, "model: {}", model);
eprintln!();
}
EventMsg::PlanUpdate(plan_update_event) => {
let UpdatePlanArgs { explanation, plan } = plan_update_event;

// Header
ts_println!(self, "{}", "Plan update".style(self.magenta));
ts_msg!(self, "{}", "Plan update".style(self.magenta));

// Optional explanation
if let Some(explanation) = explanation
&& !explanation.trim().is_empty()
{
ts_println!(self, "{}", explanation.style(self.italic));
ts_msg!(self, "{}", explanation.style(self.italic));
}

// Pretty-print the plan items with simple status markers.
for item in plan {
use codex_core::plan_tool::StepStatus;
match item.status {
StepStatus::Completed => {
ts_println!(self, " {} {}", "✓".style(self.green), item.step);
ts_msg!(self, " {} {}", "✓".style(self.green), item.step);
}
StepStatus::InProgress => {
ts_println!(self, " {} {}", "→".style(self.cyan), item.step);
ts_msg!(self, " {} {}", "→".style(self.cyan), item.step);
}
StepStatus::Pending => {
ts_println!(
ts_msg!(
self,
" {} {}",
"•".style(self.dimmed),
Expand All @@ -485,7 +491,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
// Currently ignored in exec output.
}
EventMsg::ViewImageToolCall(view) => {
ts_println!(
ts_msg!(
self,
"{} {}",
"viewed image".style(self.magenta),
Expand All @@ -494,13 +500,13 @@ impl EventProcessor for EventProcessorWithHumanOutput {
}
EventMsg::TurnAborted(abort_reason) => match abort_reason.reason {
TurnAbortReason::Interrupted => {
ts_println!(self, "task interrupted");
ts_msg!(self, "task interrupted");
}
TurnAbortReason::Replaced => {
ts_println!(self, "task aborted: replaced by a new task");
ts_msg!(self, "task aborted: replaced by a new task");
}
TurnAbortReason::ReviewEnded => {
ts_println!(self, "task aborted: review ended");
ts_msg!(self, "task aborted: review ended");
}
},
EventMsg::ShutdownComplete => return CodexStatus::Shutdown,
Expand All @@ -517,13 +523,25 @@ impl EventProcessor for EventProcessorWithHumanOutput {

fn print_final_output(&mut self) {
if let Some(usage_info) = &self.last_total_token_usage {
ts_println!(
self,
eprintln!(
"{}\n{}",
"tokens used".style(self.magenta).style(self.italic),
format_with_separators(usage_info.total_token_usage.blended_total())
);
}

// If the user has not piped the final message to a file, they will see
// it twice: once written to stderr as part of the normal event
// processing, and once here on stdout. We print the token summary above
// to help break up the output visually in that case.
#[allow(clippy::print_stdout)]
if let Some(message) = &self.final_message {
if message.ends_with('\n') {
print!("{message}");
} else {
println!("{message}");
}
}
}
}

Expand Down
1 change: 1 addition & 0 deletions codex-rs/exec/src/event_processor_with_jsonl_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ impl EventProcessor for EventProcessorWithJsonOutput {
});
}

#[allow(clippy::print_stdout)]
fn process_event(&mut self, event: Event) -> CodexStatus {
let aggregated = self.collect_thread_events(&event);
for conv_event in aggregated {
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/exec/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
// - In the default output mode, it is paramount that the only thing written to
// stdout is the final message (if any).
// - In --json mode, stdout must be valid JSONL, one event per line.
// For both modes, any other output must be written to stderr.
#![deny(clippy::print_stdout)]

mod cli;
mod event_processor;
mod event_processor_with_human_output;
Expand Down
Loading
Loading