Codex Handbook
core/src/tools/events.rs 815 lines
use crate::function_tool::FunctionCallError;use crate::session::session::Session;use crate::session::turn_context::TurnContext;use crate::tools::context::SharedTurnDiffTracker;use crate::tools::sandboxing::ToolError;use crate::turn_timing::now_unix_timestamp_ms;use codex_apply_patch::AppliedPatchDelta;use codex_protocol::error::CodexErr;use codex_protocol::error::SandboxErr;use codex_protocol::exec_output::ExecToolCallOutput;use codex_protocol::items::FileChangeItem;use codex_protocol::items::TurnItem;use codex_protocol::parse_command::ParsedCommand;use codex_protocol::protocol::EventMsg;use codex_protocol::protocol::ExecCommandBeginEvent;use codex_protocol::protocol::ExecCommandEndEvent;use codex_protocol::protocol::ExecCommandSource;use codex_protocol::protocol::ExecCommandStatus;use codex_protocol::protocol::FileChange;use codex_protocol::protocol::PatchApplyStatus;use codex_protocol::protocol::TurnDiffEvent;use codex_shell_command::parse_command::parse_command;use codex_utils_absolute_path::AbsolutePathBuf;use std::collections::HashMap;use std::path::PathBuf;use std::time::Duration;use super::format_exec_output_str;#[derive(Clone, Copy)]pub(crate) struct ToolEventCtx<'a> {    pub session: &'a Session,    pub turn: &'a TurnContext,    pub call_id: &'a str,    pub turn_diff_tracker: Option<&'a SharedTurnDiffTracker>,}impl<'a> ToolEventCtx<'a> {    pub fn new(        session: &'a Session,        turn: &'a TurnContext,        call_id: &'a str,        turn_diff_tracker: Option<&'a SharedTurnDiffTracker>,    ) -> Self {        Self {            session,            turn,            call_id,            turn_diff_tracker,        }    }}pub(crate) enum ToolEventStage<'a> {    Begin,    Success {        output: ExecToolCallOutput,        applied_patch_delta: Option<&'a AppliedPatchDelta>,    },    Failure(ToolEventFailure<'a>),}pub(crate) enum ToolEventFailure<'a> {    Output(ExecToolCallOutput),    Message(String),    Rejected {        message: String,        applied_patch_delta: Option<&'a AppliedPatchDelta>,    },}enum TurnDiffTrackerUpdate<'a> {    Track {        environment_id: Option<String>,        delta: &'a AppliedPatchDelta,    },    Invalidate,    None,}fn tracker_update_for_known_delta<'a>(    environment_id: Option<&str>,    delta: &'a AppliedPatchDelta,) -> TurnDiffTrackerUpdate<'a> {    if delta.is_exact() && delta.is_empty() {        TurnDiffTrackerUpdate::None    } else {        TurnDiffTrackerUpdate::Track {            environment_id: environment_id.map(str::to_string),            delta,        }    }}pub(crate) async fn emit_exec_command_begin(    ctx: ToolEventCtx<'_>,    command: &[String],    cwd: &AbsolutePathBuf,    parsed_cmd: &[ParsedCommand],    source: ExecCommandSource,    interaction_input: Option<String>,    process_id: Option<&str>,) {    ctx.session        .send_event(            ctx.turn,            EventMsg::ExecCommandBegin(ExecCommandBeginEvent {                call_id: ctx.call_id.to_string(),                process_id: process_id.map(str::to_owned),                turn_id: ctx.turn.sub_id.clone(),                started_at_ms: now_unix_timestamp_ms(),                command: command.to_vec(),                cwd: cwd.clone(),                parsed_cmd: parsed_cmd.to_vec(),                source,                interaction_input,            }),        )        .await;}// Concrete, allocation-free emitter: avoid trait objects and boxed futures.pub(crate) enum ToolEmitter {    Shell {        command: Vec<String>,        cwd: AbsolutePathBuf,        source: ExecCommandSource,        parsed_cmd: Vec<ParsedCommand>,    },    ApplyPatch {        changes: HashMap<PathBuf, FileChange>,        auto_approved: bool,        environment_id: Option<String>,    },    UnifiedExec {        command: Vec<String>,        cwd: AbsolutePathBuf,        source: ExecCommandSource,        parsed_cmd: Vec<ParsedCommand>,        process_id: Option<String>,    },}impl ToolEmitter {    pub fn shell(command: Vec<String>, cwd: AbsolutePathBuf, source: ExecCommandSource) -> Self {        let parsed_cmd = parse_command(&command);        Self::Shell {            command,            cwd,            source,            parsed_cmd,        }    }    pub fn apply_patch_for_environment(        changes: HashMap<PathBuf, FileChange>,        auto_approved: bool,        environment_id: String,    ) -> Self {        Self::ApplyPatch {            changes,            auto_approved,            environment_id: Some(environment_id),        }    }    pub fn unified_exec(        command: &[String],        cwd: AbsolutePathBuf,        source: ExecCommandSource,        process_id: Option<String>,    ) -> Self {        let parsed_cmd = parse_command(command);        Self::UnifiedExec {            command: command.to_vec(),            cwd,            source,            parsed_cmd,            process_id,        }    }    pub async fn emit(&self, ctx: ToolEventCtx<'_>, stage: ToolEventStage<'_>) {        match (self, stage) {            (                Self::Shell {                    command,                    cwd,                    source,                    parsed_cmd,                    ..                },                stage,            ) => {                emit_exec_stage(                    ctx,                    ExecCommandInput::new(                        command, cwd, parsed_cmd, *source, /*interaction_input*/ None,                        /*process_id*/ None,                    ),                    stage,                )                .await;            }            (                Self::ApplyPatch {                    changes,                    auto_approved,                    ..                },                ToolEventStage::Begin,            ) => {                ctx.session                    .emit_turn_item_started(                        ctx.turn,                        &TurnItem::FileChange(FileChangeItem {                            id: ctx.call_id.to_string(),                            changes: changes.clone(),                            status: None,                            auto_approved: Some(*auto_approved),                            stdout: None,                            stderr: None,                        }),                    )                    .await;            }            (                Self::ApplyPatch {                    changes,                    environment_id,                    ..                },                ToolEventStage::Success {                    output,                    applied_patch_delta,                },            ) => {                let status = if output.exit_code == 0 {                    PatchApplyStatus::Completed                } else {                    PatchApplyStatus::Failed                };                let tracker_update = applied_patch_delta                    .map(|delta| tracker_update_for_known_delta(environment_id.as_deref(), delta))                    .unwrap_or(TurnDiffTrackerUpdate::Invalidate);                emit_patch_end(                    ctx,                    changes.clone(),                    output.stdout.text.clone(),                    output.stderr.text.clone(),                    status,                    tracker_update,                )                .await;            }            (                Self::ApplyPatch { changes, .. },                ToolEventStage::Failure(ToolEventFailure::Output(output)),            ) => {                emit_patch_end(                    ctx,                    changes.clone(),                    output.stdout.text.clone(),                    output.stderr.text.clone(),                    if output.exit_code == 0 {                        PatchApplyStatus::Completed                    } else {                        PatchApplyStatus::Failed                    },                    TurnDiffTrackerUpdate::Invalidate,                )                .await;            }            (                Self::ApplyPatch { changes, .. },                ToolEventStage::Failure(ToolEventFailure::Message(message)),            ) => {                emit_patch_end(                    ctx,                    changes.clone(),                    String::new(),                    (*message).to_string(),                    PatchApplyStatus::Failed,                    TurnDiffTrackerUpdate::None,                )                .await;            }            (                Self::ApplyPatch {                    changes,                    environment_id,                    ..                },                ToolEventStage::Failure(ToolEventFailure::Rejected {                    message,                    applied_patch_delta,                }),            ) => {                emit_patch_end(                    ctx,                    changes.clone(),                    String::new(),                    (*message).to_string(),                    PatchApplyStatus::Declined,                    applied_patch_delta                        .map(|delta| {                            tracker_update_for_known_delta(environment_id.as_deref(), delta)                        })                        .unwrap_or(TurnDiffTrackerUpdate::None),                )                .await;            }            (                Self::UnifiedExec {                    command,                    cwd,                    source,                    parsed_cmd,                    process_id,                },                stage,            ) => {                emit_exec_stage(                    ctx,                    ExecCommandInput::new(                        command,                        cwd,                        parsed_cmd,                        *source,                        /*interaction_input*/ None,                        process_id.as_deref(),                    ),                    stage,                )                .await;            }        }    }    pub async fn begin(&self, ctx: ToolEventCtx<'_>) {        self.emit(ctx, ToolEventStage::Begin).await;    }    fn format_exec_output_for_model(        &self,        output: &ExecToolCallOutput,        ctx: ToolEventCtx<'_>,    ) -> String {        super::format_exec_output_for_model(output, ctx.turn.truncation_policy)    }    pub async fn finish(        &self,        ctx: ToolEventCtx<'_>,        out: Result<ExecToolCallOutput, ToolError>,        applied_patch_delta: Option<&AppliedPatchDelta>,    ) -> Result<String, FunctionCallError> {        let (event, result) = match out {            Ok(output) => {                let content = self.format_exec_output_for_model(&output, ctx);                let exit_code = output.exit_code;                let event = ToolEventStage::Success {                    output,                    applied_patch_delta,                };                let result = if exit_code == 0 {                    Ok(content)                } else {                    Err(FunctionCallError::RespondToModel(content))                };                (event, result)            }            Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Timeout { output }))) => {                let response = self.format_exec_output_for_model(&output, ctx);                let event = ToolEventStage::Failure(ToolEventFailure::Output(*output));                let result = Err(FunctionCallError::RespondToModel(response));                (event, result)            }            Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { output, .. }))) => {                let response = self.format_exec_output_for_model(&output, ctx);                // apply_patch can be denied after it has already committed a                // known prefix. Reuse the output-bearing path so the visible                // item still fails while the turn diff consumes that prefix.                let event = match (self, applied_patch_delta) {                    (Self::ApplyPatch { .. }, Some(delta)) => ToolEventStage::Success {                        output: *output,                        applied_patch_delta: Some(delta),                    },                    _ => ToolEventStage::Failure(ToolEventFailure::Output(*output)),                };                let result = Err(FunctionCallError::RespondToModel(response));                (event, result)            }            Err(ToolError::Codex(err)) => {                let message = format!("execution error: {err:?}");                let event = ToolEventStage::Failure(ToolEventFailure::Message(message.clone()));                let result = Err(FunctionCallError::RespondToModel(message));                (event, result)            }            Err(ToolError::Rejected(msg)) => {                // Normalize common rejection messages for exec tools so tests and                // users see a clear, consistent phrase.                //                // NOTE: ToolError::Rejected is currently used for both user-declined approvals                // and some operational/runtime rejection paths (for example setup failures).                // We intentionally map all of them through the "rejected" event path for now,                // which means a subset of non-user failures may be reported as Declined.                //                // TODO: We should add a new ToolError variant for user-declined approvals.                let normalized = if msg == "rejected by user" {                    match self {                        Self::Shell { .. } | Self::UnifiedExec { .. } => {                            "exec command rejected by user".to_string()                        }                        Self::ApplyPatch { .. } => "patch rejected by user".to_string(),                    }                } else {                    msg                };                let event = ToolEventStage::Failure(ToolEventFailure::Rejected {                    message: normalized.clone(),                    applied_patch_delta,                });                let result = Err(FunctionCallError::RespondToModel(normalized));                (event, result)            }        };        self.emit(ctx, event).await;        result    }}struct ExecCommandInput<'a> {    command: &'a [String],    cwd: &'a AbsolutePathBuf,    parsed_cmd: &'a [ParsedCommand],    source: ExecCommandSource,    interaction_input: Option<&'a str>,    process_id: Option<&'a str>,}impl<'a> ExecCommandInput<'a> {    fn new(        command: &'a [String],        cwd: &'a AbsolutePathBuf,        parsed_cmd: &'a [ParsedCommand],        source: ExecCommandSource,        interaction_input: Option<&'a str>,        process_id: Option<&'a str>,    ) -> Self {        Self {            command,            cwd,            parsed_cmd,            source,            interaction_input,            process_id,        }    }}struct ExecCommandResult {    stdout: String,    stderr: String,    aggregated_output: String,    exit_code: i32,    duration: Duration,    formatted_output: String,    status: ExecCommandStatus,}async fn emit_exec_stage(    ctx: ToolEventCtx<'_>,    exec_input: ExecCommandInput<'_>,    stage: ToolEventStage<'_>,) {    match stage {        ToolEventStage::Begin => {            emit_exec_command_begin(                ctx,                exec_input.command,                exec_input.cwd,                exec_input.parsed_cmd,                exec_input.source,                exec_input.interaction_input.map(str::to_owned),                exec_input.process_id,            )            .await;        }        ToolEventStage::Success { output, .. }        | ToolEventStage::Failure(ToolEventFailure::Output(output)) => {            let exec_result = ExecCommandResult {                stdout: output.stdout.text.clone(),                stderr: output.stderr.text.clone(),                aggregated_output: output.aggregated_output.text.clone(),                exit_code: output.exit_code,                duration: output.duration,                formatted_output: format_exec_output_str(&output, ctx.turn.truncation_policy),                status: if output.exit_code == 0 {                    ExecCommandStatus::Completed                } else {                    ExecCommandStatus::Failed                },            };            emit_exec_end(ctx, exec_input, exec_result).await;        }        ToolEventStage::Failure(ToolEventFailure::Message(message)) => {            let text = message.to_string();            let exec_result = ExecCommandResult {                stdout: String::new(),                stderr: text.clone(),                aggregated_output: text.clone(),                exit_code: -1,                duration: Duration::ZERO,                formatted_output: text,                status: ExecCommandStatus::Failed,            };            emit_exec_end(ctx, exec_input, exec_result).await;        }        ToolEventStage::Failure(ToolEventFailure::Rejected { message, .. }) => {            let text = message.to_string();            let exec_result = ExecCommandResult {                stdout: String::new(),                stderr: text.clone(),                aggregated_output: text.clone(),                exit_code: -1,                duration: Duration::ZERO,                formatted_output: text,                status: ExecCommandStatus::Declined,            };            emit_exec_end(ctx, exec_input, exec_result).await;        }    }}async fn emit_exec_end(    ctx: ToolEventCtx<'_>,    exec_input: ExecCommandInput<'_>,    exec_result: ExecCommandResult,) {    ctx.session        .send_event(            ctx.turn,            EventMsg::ExecCommandEnd(ExecCommandEndEvent {                call_id: ctx.call_id.to_string(),                process_id: exec_input.process_id.map(str::to_owned),                turn_id: ctx.turn.sub_id.clone(),                completed_at_ms: now_unix_timestamp_ms(),                command: exec_input.command.to_vec(),                cwd: exec_input.cwd.clone(),                parsed_cmd: exec_input.parsed_cmd.to_vec(),                source: exec_input.source,                interaction_input: exec_input.interaction_input.map(str::to_owned),                stdout: exec_result.stdout,                stderr: exec_result.stderr,                aggregated_output: exec_result.aggregated_output,                exit_code: exec_result.exit_code,                duration: exec_result.duration,                formatted_output: exec_result.formatted_output,                status: exec_result.status,            }),        )        .await;}async fn emit_patch_end(    ctx: ToolEventCtx<'_>,    changes: HashMap<PathBuf, FileChange>,    stdout: String,    stderr: String,    status: PatchApplyStatus,    tracker_update: TurnDiffTrackerUpdate<'_>,) {    ctx.session        .emit_turn_item_completed(            ctx.turn,            TurnItem::FileChange(FileChangeItem {                id: ctx.call_id.to_string(),                changes,                status: Some(status),                auto_approved: None,                stdout: Some(stdout),                stderr: Some(stderr),            }),        )        .await;    if let Some(tracker) = ctx.turn_diff_tracker {        let (should_emit_turn_diff, unified_diff) = {            let mut guard = tracker.lock().await;            let had_unified_diff = guard.has_unified_diff();            let tracker_changed = match tracker_update {                TurnDiffTrackerUpdate::Track {                    environment_id,                    delta,                } => {                    guard.track_delta(environment_id.as_deref().unwrap_or_default(), delta);                    true                }                TurnDiffTrackerUpdate::Invalidate => {                    guard.invalidate();                    true                }                TurnDiffTrackerUpdate::None => false,            };            let unified_diff = guard.get_unified_diff();            (                tracker_changed && (had_unified_diff || unified_diff.is_some()),                unified_diff.unwrap_or_default(),            )        };        if should_emit_turn_diff {            ctx.session                .send_event(ctx.turn, EventMsg::TurnDiff(TurnDiffEvent { unified_diff }))                .await;        }    }}#[cfg(test)]mod tests {    use super::*;    use crate::session::tests::make_session_and_context_with_dynamic_tools_and_rx;    use crate::turn_diff_tracker::TurnDiffTracker;    use codex_exec_server::LOCAL_FS;    use codex_protocol::error::CodexErr;    use codex_protocol::error::SandboxErr;    use codex_protocol::exec_output::ExecToolCallOutput;    use codex_protocol::items::TurnItem;    use codex_protocol::protocol::PatchApplyStatus;    use codex_utils_absolute_path::AbsolutePathBuf;    use std::sync::Arc;    use tempfile::tempdir;    use tokio::sync::Mutex;    async fn assert_failed_apply_patch_tracks_committed_delta(        out: Result<ExecToolCallOutput, ToolError>,        expected_status: PatchApplyStatus,    ) {        let (session, turn, rx_event) =            make_session_and_context_with_dynamic_tools_and_rx(Vec::new()).await;        let tracker = Arc::new(Mutex::new(TurnDiffTracker::new()));        let dir = tempdir().expect("tempdir");        let cwd = AbsolutePathBuf::from_absolute_path(dir.path()).expect("absolute cwd");        let mut stdout = Vec::new();        let mut stderr = Vec::new();        let delta = codex_apply_patch::apply_patch(            "*** Begin Patch\n*** Add File: out/dest.txt\n+after\n*** End Patch",            &cwd,            &mut stdout,            &mut stderr,            LOCAL_FS.as_ref(),            /*sandbox*/ None,        )        .await        .expect("apply patch");        ToolEmitter::ApplyPatch {            changes: HashMap::new(),            auto_approved: false,            environment_id: None,        }        .finish(            ToolEventCtx::new(session.as_ref(), turn.as_ref(), "call-id", Some(&tracker)),            out,            Some(&delta),        )        .await        .expect_err("failed patch");        let completed = rx_event.recv().await.expect("item completed event");        assert!(matches!(            completed.msg,            EventMsg::ItemCompleted(event)                if matches!(                    &event.item,                    TurnItem::FileChange(FileChangeItem {                        status: Some(status),                        ..                    }) if status == &expected_status                )        ));        let unified_diff = loop {            let event = tokio::time::timeout(Duration::from_secs(1), rx_event.recv())                .await                .expect("turn diff event")                .expect("channel open");            if let EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) = event.msg {                break unified_diff;            }        };        assert!(unified_diff.contains("out/dest.txt"));        assert!(unified_diff.contains("+after"));    }    #[tokio::test]    async fn denied_apply_patch_tracks_committed_delta() {        let output = ExecToolCallOutput {            exit_code: 1,            ..Default::default()        };        assert_failed_apply_patch_tracks_committed_delta(            Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {                output: Box::new(output),                network_policy_decision: None,            }))),            PatchApplyStatus::Failed,        )        .await;    }    #[tokio::test]    async fn rejected_apply_patch_tracks_committed_delta() {        assert_failed_apply_patch_tracks_committed_delta(            Err(ToolError::Rejected("rejected by user".to_string())),            PatchApplyStatus::Declined,        )        .await;    }    #[tokio::test]    async fn net_zero_patch_emits_empty_turn_diff() {        let (session, turn, rx_event) =            make_session_and_context_with_dynamic_tools_and_rx(Vec::new()).await;        let tracker = Arc::new(Mutex::new(TurnDiffTracker::new()));        let dir = tempdir().expect("tempdir");        let cwd = AbsolutePathBuf::from_absolute_path(dir.path()).expect("absolute cwd");        for patch in [            "*** Begin Patch\n*** Add File: a.txt\n+one\n*** End Patch",            "*** Begin Patch\n*** Delete File: a.txt\n*** End Patch",        ] {            let mut stdout = Vec::new();            let mut stderr = Vec::new();            let delta = codex_apply_patch::apply_patch(                patch,                &cwd,                &mut stdout,                &mut stderr,                LOCAL_FS.as_ref(),                /*sandbox*/ None,            )            .await            .expect("apply patch");            emit_patch_end(                ToolEventCtx::new(session.as_ref(), turn.as_ref(), "call-id", Some(&tracker)),                HashMap::new(),                String::new(),                String::new(),                PatchApplyStatus::Completed,                TurnDiffTrackerUpdate::Track {                    environment_id: None,                    delta: &delta,                },            )            .await;            rx_event.recv().await.expect("item completed event");            let unified_diff = loop {                let event = rx_event.recv().await.expect("turn diff event");                if let EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) = event.msg {                    break unified_diff;                }            };            if patch.contains("Delete File") {                assert_eq!(unified_diff, "");            } else {                assert!(unified_diff.contains("+one"));            }        }    }    #[tokio::test]    async fn invalidation_emits_empty_turn_diff() {        let (session, turn, rx_event) =            make_session_and_context_with_dynamic_tools_and_rx(Vec::new()).await;        let tracker = Arc::new(Mutex::new(TurnDiffTracker::new()));        let dir = tempdir().expect("tempdir");        let cwd = AbsolutePathBuf::from_absolute_path(dir.path()).expect("absolute cwd");        let mut stdout = Vec::new();        let mut stderr = Vec::new();        let delta = codex_apply_patch::apply_patch(            "*** Begin Patch\n*** Add File: a.txt\n+one\n*** End Patch",            &cwd,            &mut stdout,            &mut stderr,            LOCAL_FS.as_ref(),            /*sandbox*/ None,        )        .await        .expect("apply patch");        tracker.lock().await.track_delta("", &delta);        emit_patch_end(            ToolEventCtx::new(session.as_ref(), turn.as_ref(), "call-id", Some(&tracker)),            HashMap::new(),            String::new(),            String::new(),            PatchApplyStatus::Completed,            TurnDiffTrackerUpdate::Invalidate,        )        .await;        rx_event.recv().await.expect("item completed event");        loop {            let event = rx_event.recv().await.expect("turn diff event");            if let EventMsg::TurnDiff(TurnDiffEvent { unified_diff }) = event.msg {                assert_eq!(unified_diff, "");                break;            }        }    }}