2025-07-30 10:05:40 -07:00
|
|
|
use codex_common::elapsed::format_duration;
|
2025-07-17 15:10:15 -07:00
|
|
|
use codex_common::elapsed::format_elapsed;
|
|
|
|
|
use codex_core::config::Config;
|
|
|
|
|
use codex_core::protocol::AgentMessageEvent;
|
2025-08-05 01:56:13 -07:00
|
|
|
use codex_core::protocol::AgentReasoningRawContentEvent;
|
2025-07-17 15:10:15 -07:00
|
|
|
use codex_core::protocol::BackgroundEventEvent;
|
|
|
|
|
use codex_core::protocol::ErrorEvent;
|
|
|
|
|
use codex_core::protocol::Event;
|
|
|
|
|
use codex_core::protocol::EventMsg;
|
|
|
|
|
use codex_core::protocol::ExecCommandBeginEvent;
|
|
|
|
|
use codex_core::protocol::ExecCommandEndEvent;
|
|
|
|
|
use codex_core::protocol::FileChange;
|
2025-07-30 10:05:40 -07:00
|
|
|
use codex_core::protocol::McpInvocation;
|
2025-07-17 15:10:15 -07:00
|
|
|
use codex_core::protocol::McpToolCallBeginEvent;
|
|
|
|
|
use codex_core::protocol::McpToolCallEndEvent;
|
|
|
|
|
use codex_core::protocol::PatchApplyBeginEvent;
|
|
|
|
|
use codex_core::protocol::PatchApplyEndEvent;
|
|
|
|
|
use codex_core::protocol::SessionConfiguredEvent;
|
2025-08-21 01:15:24 -07:00
|
|
|
use codex_core::protocol::StreamErrorEvent;
|
2025-07-23 15:03:26 -07:00
|
|
|
use codex_core::protocol::TaskCompleteEvent;
|
2025-08-17 21:40:31 -07:00
|
|
|
use codex_core::protocol::TurnAbortReason;
|
2025-08-04 08:57:04 -07:00
|
|
|
use codex_core::protocol::TurnDiffEvent;
|
2025-08-23 22:58:56 -07:00
|
|
|
use codex_core::protocol::WebSearchBeginEvent;
|
2025-08-28 19:24:38 -07:00
|
|
|
use codex_core::protocol::WebSearchEndEvent;
|
2025-09-08 14:48:48 -07:00
|
|
|
use codex_protocol::num_format::format_with_separators;
|
2025-07-17 15:10:15 -07:00
|
|
|
use owo_colors::OwoColorize;
|
|
|
|
|
use owo_colors::Style;
|
|
|
|
|
use shlex::try_join;
|
|
|
|
|
use std::collections::HashMap;
|
2025-07-23 15:03:26 -07:00
|
|
|
use std::path::PathBuf;
|
2025-07-17 15:10:15 -07:00
|
|
|
use std::time::Instant;
|
|
|
|
|
|
2025-07-23 15:03:26 -07:00
|
|
|
use crate::event_processor::CodexStatus;
|
2025-07-17 15:10:15 -07:00
|
|
|
use crate::event_processor::EventProcessor;
|
2025-07-23 15:03:26 -07:00
|
|
|
use crate::event_processor::handle_last_message;
|
2025-08-05 23:57:52 -07:00
|
|
|
use codex_common::create_config_summary_entries;
|
chore: refactor tool handling (#4510)
# Tool System Refactor
- Centralizes tool definitions and execution in `core/src/tools/*`:
specs (`spec.rs`), handlers (`handlers/*`), router (`router.rs`),
registry/dispatch (`registry.rs`), and shared context (`context.rs`).
One registry now builds the model-visible tool list and binds handlers.
- Router converts model responses to tool calls; Registry dispatches
with consistent telemetry via `codex-rs/otel` and unified error
handling. Function, Local Shell, MCP, and experimental `unified_exec`
all flow through this path; legacy shell aliases still work.
- Rationale: reduce per‑tool boilerplate, keep spec/handler in sync, and
make adding tools predictable and testable.
Example: `read_file`
- Spec: `core/src/tools/spec.rs` (see `create_read_file_tool`,
registered by `build_specs`).
- Handler: `core/src/tools/handlers/read_file.rs` (absolute `file_path`,
1‑indexed `offset`, `limit`, `L#: ` prefixes, safe truncation).
- E2E test: `core/tests/suite/read_file.rs` validates the tool returns
the requested lines.
## Next steps:
- Decompose `handle_container_exec_with_params`
- Add parallel tool calls
2025-10-03 13:21:06 +01:00
|
|
|
use codex_protocol::plan_tool::StepStatus;
|
|
|
|
|
use codex_protocol::plan_tool::UpdatePlanArgs;
|
2025-07-17 15:10:15 -07:00
|
|
|
|
|
|
|
|
/// This should be configurable. When used in CI, users may not want to impose
|
|
|
|
|
/// a limit so they can see the full transcript.
|
|
|
|
|
const MAX_OUTPUT_LINES_FOR_EXEC_TOOL_CALL: usize = 20;
|
|
|
|
|
pub(crate) struct EventProcessorWithHumanOutput {
|
|
|
|
|
call_id_to_patch: HashMap<String, PatchApplyBegin>,
|
|
|
|
|
|
|
|
|
|
// To ensure that --color=never is respected, ANSI escapes _must_ be added
|
|
|
|
|
// using .style() with one of these fields. If you need a new style, add a
|
|
|
|
|
// new field here.
|
|
|
|
|
bold: Style,
|
|
|
|
|
italic: Style,
|
|
|
|
|
dimmed: Style,
|
|
|
|
|
|
|
|
|
|
magenta: Style,
|
|
|
|
|
red: Style,
|
|
|
|
|
green: Style,
|
|
|
|
|
cyan: Style,
|
|
|
|
|
|
|
|
|
|
/// Whether to include `AgentReasoning` events in the output.
|
|
|
|
|
show_agent_reasoning: bool,
|
2025-08-05 01:56:13 -07:00
|
|
|
show_raw_agent_reasoning: bool,
|
2025-07-23 15:03:26 -07:00
|
|
|
last_message_path: Option<PathBuf>,
|
2025-10-02 14:17:42 -07:00
|
|
|
last_total_token_usage: Option<codex_core::protocol::TokenUsageInfo>,
|
2025-10-03 09:22:12 -07:00
|
|
|
final_message: Option<String>,
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl EventProcessorWithHumanOutput {
|
2025-07-23 15:03:26 -07:00
|
|
|
pub(crate) fn create_with_ansi(
|
|
|
|
|
with_ansi: bool,
|
|
|
|
|
config: &Config,
|
|
|
|
|
last_message_path: Option<PathBuf>,
|
|
|
|
|
) -> Self {
|
2025-07-17 15:10:15 -07:00
|
|
|
let call_id_to_patch = HashMap::new();
|
|
|
|
|
|
|
|
|
|
if with_ansi {
|
|
|
|
|
Self {
|
|
|
|
|
call_id_to_patch,
|
|
|
|
|
bold: Style::new().bold(),
|
|
|
|
|
italic: Style::new().italic(),
|
|
|
|
|
dimmed: Style::new().dimmed(),
|
|
|
|
|
magenta: Style::new().magenta(),
|
|
|
|
|
red: Style::new().red(),
|
|
|
|
|
green: Style::new().green(),
|
|
|
|
|
cyan: Style::new().cyan(),
|
|
|
|
|
show_agent_reasoning: !config.hide_agent_reasoning,
|
2025-08-05 01:56:13 -07:00
|
|
|
show_raw_agent_reasoning: config.show_raw_agent_reasoning,
|
2025-07-23 15:03:26 -07:00
|
|
|
last_message_path,
|
2025-10-02 14:17:42 -07:00
|
|
|
last_total_token_usage: None,
|
2025-10-03 09:22:12 -07:00
|
|
|
final_message: None,
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Self {
|
|
|
|
|
call_id_to_patch,
|
|
|
|
|
bold: Style::new(),
|
|
|
|
|
italic: Style::new(),
|
|
|
|
|
dimmed: Style::new(),
|
|
|
|
|
magenta: Style::new(),
|
|
|
|
|
red: Style::new(),
|
|
|
|
|
green: Style::new(),
|
|
|
|
|
cyan: Style::new(),
|
|
|
|
|
show_agent_reasoning: !config.hide_agent_reasoning,
|
2025-08-05 01:56:13 -07:00
|
|
|
show_raw_agent_reasoning: config.show_raw_agent_reasoning,
|
2025-07-23 15:03:26 -07:00
|
|
|
last_message_path,
|
2025-10-02 14:17:42 -07:00
|
|
|
last_total_token_usage: None,
|
2025-10-03 09:22:12 -07:00
|
|
|
final_message: None,
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct PatchApplyBegin {
|
|
|
|
|
start_time: Instant,
|
|
|
|
|
auto_approved: bool,
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-03 09:22:12 -07:00
|
|
|
/// Timestamped helper. The timestamp is styled with self.dimmed.
|
|
|
|
|
macro_rules! ts_msg {
|
2025-07-17 15:10:15 -07:00
|
|
|
($self:ident, $($arg:tt)*) => {{
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!($($arg)*);
|
2025-07-17 15:10:15 -07:00
|
|
|
}};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.
|
2025-10-02 14:17:42 -07:00
|
|
|
fn print_config_summary(
|
|
|
|
|
&mut self,
|
|
|
|
|
config: &Config,
|
|
|
|
|
prompt: &str,
|
|
|
|
|
session_configured_event: &SessionConfiguredEvent,
|
|
|
|
|
) {
|
2025-07-17 15:10:15 -07:00
|
|
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(
|
2025-07-17 15:10:15 -07:00
|
|
|
self,
|
|
|
|
|
"OpenAI Codex v{} (research preview)\n--------",
|
|
|
|
|
VERSION
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-02 14:17:42 -07:00
|
|
|
let mut entries = create_config_summary_entries(config);
|
|
|
|
|
entries.push((
|
|
|
|
|
"session id",
|
|
|
|
|
session_configured_event.session_id.to_string(),
|
|
|
|
|
));
|
2025-07-17 15:10:15 -07:00
|
|
|
|
|
|
|
|
for (key, value) in entries {
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{} {}", format!("{key}:").style(self.bold), value);
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("--------");
|
2025-07-17 15:10:15 -07:00
|
|
|
|
|
|
|
|
// 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.
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "{}\n{}", "user".style(self.cyan), prompt);
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
|
2025-07-23 15:03:26 -07:00
|
|
|
fn process_event(&mut self, event: Event) -> CodexStatus {
|
2025-07-17 15:10:15 -07:00
|
|
|
let Event { id: _, msg } = event;
|
|
|
|
|
match msg {
|
|
|
|
|
EventMsg::Error(ErrorEvent { message }) => {
|
|
|
|
|
let prefix = "ERROR:".style(self.red);
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "{prefix} {message}");
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "{}", message.style(self.dimmed));
|
2025-08-21 01:15:24 -07:00
|
|
|
}
|
|
|
|
|
EventMsg::StreamError(StreamErrorEvent { message }) => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "{}", message.style(self.dimmed));
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
2025-08-27 00:04:21 -07:00
|
|
|
EventMsg::TaskStarted(_) => {
|
2025-07-17 15:10:15 -07:00
|
|
|
// Ignore.
|
|
|
|
|
}
|
2025-07-23 15:03:26 -07:00
|
|
|
EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => {
|
2025-10-03 09:22:12 -07:00
|
|
|
let last_message = last_agent_message.as_deref();
|
2025-08-04 15:56:32 -07:00
|
|
|
if let Some(output_file) = self.last_message_path.as_deref() {
|
2025-10-03 09:22:12 -07:00
|
|
|
handle_last_message(last_message, output_file);
|
2025-08-04 15:56:32 -07:00
|
|
|
}
|
2025-10-03 09:22:12 -07:00
|
|
|
|
|
|
|
|
self.final_message = last_agent_message;
|
|
|
|
|
|
2025-07-23 15:03:26 -07:00
|
|
|
return CodexStatus::InitiateShutdown;
|
|
|
|
|
}
|
2025-09-06 08:19:23 -07:00
|
|
|
EventMsg::TokenCount(ev) => {
|
2025-10-02 14:17:42 -07:00
|
|
|
self.last_total_token_usage = ev.info;
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
2025-10-02 14:17:42 -07:00
|
|
|
|
2025-08-12 17:37:28 -07:00
|
|
|
EventMsg::AgentReasoningSectionBreak(_) => {
|
|
|
|
|
if !self.show_agent_reasoning {
|
|
|
|
|
return CodexStatus::Running;
|
|
|
|
|
}
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!();
|
2025-08-12 17:37:28 -07:00
|
|
|
}
|
2025-08-05 01:56:13 -07:00
|
|
|
EventMsg::AgentReasoningRawContent(AgentReasoningRawContentEvent { text }) => {
|
2025-10-02 14:17:42 -07:00
|
|
|
if self.show_raw_agent_reasoning {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(
|
2025-07-17 15:10:15 -07:00
|
|
|
self,
|
|
|
|
|
"{}\n{}",
|
2025-10-02 14:17:42 -07:00
|
|
|
"thinking".style(self.italic).style(self.magenta),
|
|
|
|
|
text,
|
2025-07-17 15:10:15 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-02 14:17:42 -07:00
|
|
|
EventMsg::AgentMessage(AgentMessageEvent { message }) => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(
|
2025-07-17 15:10:15 -07:00
|
|
|
self,
|
2025-10-02 14:17:42 -07:00
|
|
|
"{}\n{}",
|
|
|
|
|
"codex".style(self.italic).style(self.magenta),
|
|
|
|
|
message,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
EventMsg::ExecCommandBegin(ExecCommandBeginEvent { command, cwd, .. }) => {
|
2025-10-03 09:22:12 -07:00
|
|
|
eprint!(
|
2025-10-02 14:17:42 -07:00
|
|
|
"{}\n{} in {}",
|
|
|
|
|
"exec".style(self.italic).style(self.magenta),
|
2025-07-17 15:10:15 -07:00
|
|
|
escape_command(&command).style(self.bold),
|
|
|
|
|
cwd.to_string_lossy(),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-08-01 14:00:19 -07:00
|
|
|
EventMsg::ExecCommandOutputDelta(_) => {}
|
2025-07-17 15:10:15 -07:00
|
|
|
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
2025-08-23 09:54:31 -07:00
|
|
|
aggregated_output,
|
2025-08-03 11:33:44 -07:00
|
|
|
duration,
|
2025-07-17 15:10:15 -07:00
|
|
|
exit_code,
|
2025-08-22 16:32:31 -07:00
|
|
|
..
|
2025-07-17 15:10:15 -07:00
|
|
|
}) => {
|
2025-10-02 14:17:42 -07:00
|
|
|
let duration = format!(" in {}", format_duration(duration));
|
2025-07-17 15:10:15 -07:00
|
|
|
|
2025-08-23 09:54:31 -07:00
|
|
|
let truncated_output = aggregated_output
|
2025-07-17 15:10:15 -07:00
|
|
|
.lines()
|
|
|
|
|
.take(MAX_OUTPUT_LINES_FOR_EXEC_TOOL_CALL)
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join("\n");
|
|
|
|
|
match exit_code {
|
|
|
|
|
0 => {
|
2025-10-02 14:17:42 -07:00
|
|
|
let title = format!(" succeeded{duration}:");
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "{}", title.style(self.green));
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
_ => {
|
2025-10-02 14:17:42 -07:00
|
|
|
let title = format!(" exited {exit_code}{duration}:");
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "{}", title.style(self.red));
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{}", truncated_output.style(self.dimmed));
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
|
2025-07-30 10:05:40 -07:00
|
|
|
call_id: _,
|
|
|
|
|
invocation,
|
2025-07-17 15:10:15 -07:00
|
|
|
}) => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(
|
2025-07-17 15:10:15 -07:00
|
|
|
self,
|
|
|
|
|
"{} {}",
|
|
|
|
|
"tool".style(self.magenta),
|
2025-07-30 10:05:40 -07:00
|
|
|
format_mcp_invocation(&invocation).style(self.bold),
|
2025-07-17 15:10:15 -07:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
EventMsg::McpToolCallEnd(tool_call_end_event) => {
|
|
|
|
|
let is_success = tool_call_end_event.is_success();
|
2025-07-30 10:05:40 -07:00
|
|
|
let McpToolCallEndEvent {
|
|
|
|
|
call_id: _,
|
|
|
|
|
result,
|
2025-07-17 15:10:15 -07:00
|
|
|
invocation,
|
2025-07-30 10:05:40 -07:00
|
|
|
duration,
|
|
|
|
|
} = tool_call_end_event;
|
|
|
|
|
|
|
|
|
|
let duration = format!(" in {}", format_duration(duration));
|
2025-07-17 15:10:15 -07:00
|
|
|
|
|
|
|
|
let status_str = if is_success { "success" } else { "failed" };
|
|
|
|
|
let title_style = if is_success { self.green } else { self.red };
|
2025-07-30 10:05:40 -07:00
|
|
|
let title = format!(
|
|
|
|
|
"{} {status_str}{duration}:",
|
|
|
|
|
format_mcp_invocation(&invocation)
|
|
|
|
|
);
|
2025-07-17 15:10:15 -07:00
|
|
|
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "{}", title.style(title_style));
|
2025-07-17 15:10:15 -07:00
|
|
|
|
|
|
|
|
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) {
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{}", line.style(self.dimmed));
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-28 19:24:38 -07:00
|
|
|
EventMsg::WebSearchBegin(WebSearchBeginEvent { call_id: _ }) => {}
|
|
|
|
|
EventMsg::WebSearchEnd(WebSearchEndEvent { call_id: _, query }) => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "🌐 Searched: {query}");
|
2025-08-23 22:58:56 -07:00
|
|
|
}
|
2025-07-17 15:10:15 -07:00
|
|
|
EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
|
|
|
|
|
call_id,
|
|
|
|
|
auto_approved,
|
|
|
|
|
changes,
|
|
|
|
|
}) => {
|
|
|
|
|
// Store metadata so we can calculate duration later when we
|
|
|
|
|
// receive the corresponding PatchApplyEnd event.
|
|
|
|
|
self.call_id_to_patch.insert(
|
2025-09-11 11:59:37 -07:00
|
|
|
call_id,
|
2025-07-17 15:10:15 -07:00
|
|
|
PatchApplyBegin {
|
|
|
|
|
start_time: Instant::now(),
|
|
|
|
|
auto_approved,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(
|
2025-07-17 15:10:15 -07:00
|
|
|
self,
|
2025-10-02 14:17:42 -07:00
|
|
|
"{}",
|
|
|
|
|
"file update".style(self.magenta).style(self.italic),
|
2025-07-17 15:10:15 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Pretty-print the patch summary with colored diff markers so
|
|
|
|
|
// it's easy to scan in the terminal output.
|
|
|
|
|
for (path, change) in changes.iter() {
|
|
|
|
|
match change {
|
|
|
|
|
FileChange::Add { content } => {
|
|
|
|
|
let header = format!(
|
|
|
|
|
"{} {}",
|
|
|
|
|
format_file_change(change),
|
|
|
|
|
path.to_string_lossy()
|
|
|
|
|
);
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{}", header.style(self.magenta));
|
2025-07-17 15:10:15 -07:00
|
|
|
for line in content.lines() {
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{}", line.style(self.green));
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-02 10:29:58 -07:00
|
|
|
FileChange::Delete { content } => {
|
2025-07-17 15:10:15 -07:00
|
|
|
let header = format!(
|
|
|
|
|
"{} {}",
|
|
|
|
|
format_file_change(change),
|
|
|
|
|
path.to_string_lossy()
|
|
|
|
|
);
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{}", header.style(self.magenta));
|
2025-09-02 10:29:58 -07:00
|
|
|
for line in content.lines() {
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{}", line.style(self.red));
|
2025-09-02 10:29:58 -07:00
|
|
|
}
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
FileChange::Update {
|
|
|
|
|
unified_diff,
|
|
|
|
|
move_path,
|
|
|
|
|
} => {
|
|
|
|
|
let header = if let Some(dest) = move_path {
|
|
|
|
|
format!(
|
|
|
|
|
"{} {} -> {}",
|
|
|
|
|
format_file_change(change),
|
|
|
|
|
path.to_string_lossy(),
|
|
|
|
|
dest.to_string_lossy()
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
format!("{} {}", format_file_change(change), path.to_string_lossy())
|
|
|
|
|
};
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{}", header.style(self.magenta));
|
2025-07-17 15:10:15 -07:00
|
|
|
|
|
|
|
|
// 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("+++") {
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{}", diff_line.style(self.green));
|
2025-07-17 15:10:15 -07:00
|
|
|
} else if diff_line.starts_with('-')
|
|
|
|
|
&& !diff_line.starts_with("---")
|
|
|
|
|
{
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{}", diff_line.style(self.red));
|
2025-07-17 15:10:15 -07:00
|
|
|
} else {
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{diff_line}");
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
EventMsg::PatchApplyEnd(PatchApplyEndEvent {
|
|
|
|
|
call_id,
|
|
|
|
|
stdout,
|
|
|
|
|
stderr,
|
|
|
|
|
success,
|
2025-08-04 08:57:04 -07:00
|
|
|
..
|
2025-07-17 15:10:15 -07:00
|
|
|
}) => {
|
|
|
|
|
let patch_begin = self.call_id_to_patch.remove(&call_id);
|
|
|
|
|
|
|
|
|
|
// Compute duration and summary label similar to exec commands.
|
|
|
|
|
let (duration, label) = if let Some(PatchApplyBegin {
|
|
|
|
|
start_time,
|
|
|
|
|
auto_approved,
|
|
|
|
|
}) = patch_begin
|
|
|
|
|
{
|
|
|
|
|
(
|
|
|
|
|
format!(" in {}", format_elapsed(start_time)),
|
|
|
|
|
format!("apply_patch(auto_approved={auto_approved})"),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
(String::new(), format!("apply_patch('{call_id}')"))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let (exit_code, output, title_style) = if success {
|
|
|
|
|
(0, stdout, self.green)
|
|
|
|
|
} else {
|
|
|
|
|
(1, stderr, self.red)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let title = format!("{label} exited {exit_code}{duration}:");
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "{}", title.style(title_style));
|
2025-07-17 15:10:15 -07:00
|
|
|
for line in output.lines() {
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{}", line.style(self.dimmed));
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-04 08:57:04 -07:00
|
|
|
EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(
|
2025-10-02 14:17:42 -07:00
|
|
|
self,
|
|
|
|
|
"{}",
|
|
|
|
|
"file update:".style(self.magenta).style(self.italic)
|
|
|
|
|
);
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!("{unified_diff}");
|
2025-08-04 08:57:04 -07:00
|
|
|
}
|
2025-07-17 15:10:15 -07:00
|
|
|
EventMsg::ExecApprovalRequest(_) => {
|
|
|
|
|
// Should we exit?
|
|
|
|
|
}
|
|
|
|
|
EventMsg::ApplyPatchApprovalRequest(_) => {
|
|
|
|
|
// Should we exit?
|
|
|
|
|
}
|
|
|
|
|
EventMsg::AgentReasoning(agent_reasoning_event) => {
|
|
|
|
|
if self.show_agent_reasoning {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(
|
2025-10-02 14:17:42 -07:00
|
|
|
self,
|
|
|
|
|
"{}\n{}",
|
|
|
|
|
"thinking".style(self.italic).style(self.magenta),
|
|
|
|
|
agent_reasoning_event.text,
|
|
|
|
|
);
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
EventMsg::SessionConfigured(session_configured_event) => {
|
|
|
|
|
let SessionConfiguredEvent {
|
2025-09-07 20:22:25 -07:00
|
|
|
session_id: conversation_id,
|
2025-07-17 15:10:15 -07:00
|
|
|
model,
|
2025-09-11 21:04:40 -07:00
|
|
|
reasoning_effort: _,
|
2025-07-17 15:10:15 -07:00
|
|
|
history_log_id: _,
|
|
|
|
|
history_entry_count: _,
|
2025-09-03 21:47:00 -07:00
|
|
|
initial_messages: _,
|
2025-09-09 00:11:48 -07:00
|
|
|
rollout_path: _,
|
2025-07-17 15:10:15 -07:00
|
|
|
} = session_configured_event;
|
|
|
|
|
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(
|
2025-07-17 15:10:15 -07:00
|
|
|
self,
|
|
|
|
|
"{} {}",
|
|
|
|
|
"codex session".style(self.magenta).style(self.bold),
|
2025-09-07 20:22:25 -07:00
|
|
|
conversation_id.to_string().style(self.dimmed)
|
2025-07-17 15:10:15 -07:00
|
|
|
);
|
|
|
|
|
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "model: {}", model);
|
|
|
|
|
eprintln!();
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
2025-07-29 11:22:02 -07:00
|
|
|
EventMsg::PlanUpdate(plan_update_event) => {
|
|
|
|
|
let UpdatePlanArgs { explanation, plan } = plan_update_event;
|
2025-09-23 16:50:59 -07:00
|
|
|
|
|
|
|
|
// Header
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "{}", "Plan update".style(self.magenta));
|
2025-09-23 16:50:59 -07:00
|
|
|
|
|
|
|
|
// Optional explanation
|
|
|
|
|
if let Some(explanation) = explanation
|
|
|
|
|
&& !explanation.trim().is_empty()
|
|
|
|
|
{
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "{}", explanation.style(self.italic));
|
2025-09-23 16:50:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pretty-print the plan items with simple status markers.
|
|
|
|
|
for item in plan {
|
|
|
|
|
match item.status {
|
|
|
|
|
StepStatus::Completed => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, " {} {}", "✓".style(self.green), item.step);
|
2025-09-23 16:50:59 -07:00
|
|
|
}
|
|
|
|
|
StepStatus::InProgress => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, " {} {}", "→".style(self.cyan), item.step);
|
2025-09-23 16:50:59 -07:00
|
|
|
}
|
|
|
|
|
StepStatus::Pending => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(
|
2025-09-23 16:50:59 -07:00
|
|
|
self,
|
|
|
|
|
" {} {}",
|
|
|
|
|
"•".style(self.dimmed),
|
|
|
|
|
item.step.style(self.dimmed)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-29 11:22:02 -07:00
|
|
|
}
|
2025-07-17 15:10:15 -07:00
|
|
|
EventMsg::GetHistoryEntryResponse(_) => {
|
|
|
|
|
// Currently ignored in exec output.
|
|
|
|
|
}
|
2025-08-19 09:00:31 -07:00
|
|
|
EventMsg::McpListToolsResponse(_) => {
|
|
|
|
|
// Currently ignored in exec output.
|
|
|
|
|
}
|
2025-08-28 19:16:39 -07:00
|
|
|
EventMsg::ListCustomPromptsResponse(_) => {
|
|
|
|
|
// Currently ignored in exec output.
|
|
|
|
|
}
|
2025-10-02 11:36:03 -07:00
|
|
|
EventMsg::ViewImageToolCall(view) => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(
|
2025-10-02 11:36:03 -07:00
|
|
|
self,
|
|
|
|
|
"{} {}",
|
|
|
|
|
"viewed image".style(self.magenta),
|
|
|
|
|
view.path.display()
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-08-17 21:40:31 -07:00
|
|
|
EventMsg::TurnAborted(abort_reason) => match abort_reason.reason {
|
|
|
|
|
TurnAbortReason::Interrupted => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "task interrupted");
|
2025-08-17 21:40:31 -07:00
|
|
|
}
|
|
|
|
|
TurnAbortReason::Replaced => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "task aborted: replaced by a new task");
|
2025-08-17 21:40:31 -07:00
|
|
|
}
|
2025-09-18 14:14:16 -07:00
|
|
|
TurnAbortReason::ReviewEnded => {
|
2025-10-03 09:22:12 -07:00
|
|
|
ts_msg!(self, "task aborted: review ended");
|
2025-09-18 14:14:16 -07:00
|
|
|
}
|
2025-08-17 21:40:31 -07:00
|
|
|
},
|
2025-07-23 15:03:26 -07:00
|
|
|
EventMsg::ShutdownComplete => return CodexStatus::Shutdown,
|
2025-09-10 17:42:54 -07:00
|
|
|
EventMsg::ConversationPath(_) => {}
|
2025-09-03 22:34:50 -07:00
|
|
|
EventMsg::UserMessage(_) => {}
|
Review Mode (Core) (#3401)
## 📝 Review Mode -- Core
This PR introduces the Core implementation for Review mode:
- New op `Op::Review { prompt: String }:` spawns a child review task
with isolated context, a review‑specific system prompt, and a
`Config.review_model`.
- `EnteredReviewMode`: emitted when the child review session starts.
Every event from this point onwards reflects the review session.
- `ExitedReviewMode(Option<ReviewOutputEvent>)`: emitted when the review
finishes or is interrupted, with optional structured findings:
```json
{
"findings": [
{
"title": "<≤ 80 chars, imperative>",
"body": "<valid Markdown explaining *why* this is a problem; cite files/lines/functions>",
"confidence_score": <float 0.0-1.0>,
"priority": <int 0-3>,
"code_location": {
"absolute_file_path": "<file path>",
"line_range": {"start": <int>, "end": <int>}
}
}
],
"overall_correctness": "patch is correct" | "patch is incorrect",
"overall_explanation": "<1-3 sentence explanation justifying the overall_correctness verdict>",
"overall_confidence_score": <float 0.0-1.0>
}
```
## Questions
### Why separate out its own message history?
We want the review thread to match the training of our review models as
much as possible -- that means using a custom prompt, removing user
instructions, and starting a clean chat history.
We also want to make sure the review thread doesn't leak into the parent
thread.
### Why do this as a mode, vs. sub-agents?
1. We want review to be a synchronous task, so it's fine for now to do a
bespoke implementation.
2. We're still unclear about the final structure for sub-agents. We'd
prefer to land this quickly and then refactor into sub-agents without
rushing that implementation.
2025-09-12 16:25:10 -07:00
|
|
|
EventMsg::EnteredReviewMode(_) => {}
|
|
|
|
|
EventMsg::ExitedReviewMode(_) => {}
|
2025-10-02 14:17:42 -07:00
|
|
|
EventMsg::AgentMessageDelta(_) => {}
|
|
|
|
|
EventMsg::AgentReasoningDelta(_) => {}
|
|
|
|
|
EventMsg::AgentReasoningRawContentDelta(_) => {}
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
2025-07-23 15:03:26 -07:00
|
|
|
CodexStatus::Running
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
2025-10-02 14:17:42 -07:00
|
|
|
|
|
|
|
|
fn print_final_output(&mut self) {
|
|
|
|
|
if let Some(usage_info) = &self.last_total_token_usage {
|
2025-10-03 09:22:12 -07:00
|
|
|
eprintln!(
|
2025-10-02 14:17:42 -07:00
|
|
|
"{}\n{}",
|
|
|
|
|
"tokens used".style(self.magenta).style(self.italic),
|
|
|
|
|
format_with_separators(usage_info.total_token_usage.blended_total())
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-10-03 09:22:12 -07:00
|
|
|
|
|
|
|
|
// 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}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-02 14:17:42 -07:00
|
|
|
}
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn escape_command(command: &[String]) -> String {
|
2025-09-22 20:30:16 +01:00
|
|
|
try_join(command.iter().map(String::as_str)).unwrap_or_else(|_| command.join(" "))
|
2025-07-17 15:10:15 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn format_file_change(change: &FileChange) -> &'static str {
|
|
|
|
|
match change {
|
|
|
|
|
FileChange::Add { .. } => "A",
|
2025-09-02 10:29:58 -07:00
|
|
|
FileChange::Delete { .. } => "D",
|
2025-07-17 15:10:15 -07:00
|
|
|
FileChange::Update {
|
|
|
|
|
move_path: Some(_), ..
|
|
|
|
|
} => "R",
|
|
|
|
|
FileChange::Update {
|
|
|
|
|
move_path: None, ..
|
|
|
|
|
} => "M",
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-30 10:05:40 -07:00
|
|
|
|
|
|
|
|
fn format_mcp_invocation(invocation: &McpInvocation) -> String {
|
|
|
|
|
// Build fully-qualified tool name: server.tool
|
|
|
|
|
let fq_tool_name = format!("{}.{}", invocation.server, invocation.tool);
|
|
|
|
|
|
|
|
|
|
// Format arguments as compact JSON so they fit on one line.
|
|
|
|
|
let args_str = invocation
|
|
|
|
|
.arguments
|
|
|
|
|
.as_ref()
|
|
|
|
|
.map(|v: &serde_json::Value| serde_json::to_string(v).unwrap_or_else(|_| v.to_string()))
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
if args_str.is_empty() {
|
|
|
|
|
format!("{fq_tool_name}()")
|
|
|
|
|
} else {
|
|
|
|
|
format!("{fq_tool_name}({args_str})")
|
|
|
|
|
}
|
|
|
|
|
}
|