diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 6a2de491..b261b083 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2316,7 +2316,11 @@ mod tests { use crate::tools::MODEL_FORMAT_MAX_LINES; use crate::tools::MODEL_FORMAT_TAIL_LINES; use crate::tools::ToolRouter; - use crate::tools::handle_container_exec_with_params; + use crate::tools::context::ToolInvocation; + use crate::tools::context::ToolOutput; + use crate::tools::context::ToolPayload; + use crate::tools::handlers::ShellHandler; + use crate::tools::registry::ToolHandler; use crate::turn_diff_tracker::TurnDiffTracker; use codex_app_server_protocol::AuthMode; use codex_protocol::models::ContentItem; @@ -3039,15 +3043,26 @@ mod tests { let tool_name = "shell"; let call_id = "test-call".to_string(); - let resp = handle_container_exec_with_params( - tool_name, - params, - Arc::clone(&session), - Arc::clone(&turn_context), - Arc::clone(&turn_diff_tracker), - call_id, - ) - .await; + let handler = ShellHandler; + let resp = handler + .handle(ToolInvocation { + session: Arc::clone(&session), + turn: Arc::clone(&turn_context), + tracker: Arc::clone(&turn_diff_tracker), + call_id, + tool_name: tool_name.to_string(), + payload: ToolPayload::Function { + arguments: serde_json::json!({ + "command": params.command.clone(), + "workdir": Some(turn_context.cwd.to_string_lossy().to_string()), + "timeout_ms": params.timeout_ms, + "with_escalated_permissions": params.with_escalated_permissions, + "justification": params.justification.clone(), + }) + .to_string(), + }, + }) + .await; let Err(FunctionCallError::RespondToModel(output)) = resp else { panic!("expected error result"); @@ -3066,17 +3081,30 @@ mod tests { .expect("unique turn context Arc") .sandbox_policy = SandboxPolicy::DangerFullAccess; - let resp2 = handle_container_exec_with_params( - tool_name, - params2, - Arc::clone(&session), - Arc::clone(&turn_context), - Arc::clone(&turn_diff_tracker), - "test-call-2".to_string(), - ) - .await; + let resp2 = handler + .handle(ToolInvocation { + session: Arc::clone(&session), + turn: Arc::clone(&turn_context), + tracker: Arc::clone(&turn_diff_tracker), + call_id: "test-call-2".to_string(), + tool_name: tool_name.to_string(), + payload: ToolPayload::Function { + arguments: serde_json::json!({ + "command": params2.command.clone(), + "workdir": Some(turn_context.cwd.to_string_lossy().to_string()), + "timeout_ms": params2.timeout_ms, + "with_escalated_permissions": params2.with_escalated_permissions, + "justification": params2.justification.clone(), + }) + .to_string(), + }, + }) + .await; - let output = resp2.expect("expected Ok result"); + let output = match resp2.expect("expected Ok result") { + ToolOutput::Function { content, .. } => content, + _ => panic!("unexpected tool output"), + }; #[derive(Deserialize, PartialEq, Eq, Debug)] struct ResponseExecMetadata { diff --git a/codex-rs/core/src/tools/events.rs b/codex-rs/core/src/tools/events.rs index 0a0bbaed..d45ac7cd 100644 --- a/codex-rs/core/src/tools/events.rs +++ b/codex-rs/core/src/tools/events.rs @@ -1,6 +1,9 @@ use crate::codex::Session; use crate::codex::TurnContext; +use crate::error::CodexErr; +use crate::error::SandboxErr; use crate::exec::ExecToolCallOutput; +use crate::function_tool::FunctionCallError; use crate::parse_command::parse_command; use crate::protocol::EventMsg; use crate::protocol::ExecCommandBeginEvent; @@ -10,6 +13,7 @@ use crate::protocol::PatchApplyBeginEvent; use crate::protocol::PatchApplyEndEvent; use crate::protocol::TurnDiffEvent; use crate::tools::context::SharedTurnDiffTracker; +use crate::tools::sandboxing::ToolError; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -202,6 +206,56 @@ impl ToolEmitter { } } } + + pub async fn begin(&self, ctx: ToolEventCtx<'_>) { + self.emit(ctx, ToolEventStage::Begin).await; + } + + pub async fn finish( + &self, + ctx: ToolEventCtx<'_>, + out: Result, + ) -> Result { + let event; + let result = match out { + Ok(output) => { + let content = super::format_exec_output_for_model(&output); + let exit_code = output.exit_code; + event = ToolEventStage::Success(output); + if exit_code == 0 { + Ok(content) + } else { + Err(FunctionCallError::RespondToModel(content)) + } + } + Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Timeout { output }))) + | Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { output }))) => { + let response = super::format_exec_output_for_model(&output); + event = ToolEventStage::Failure(ToolEventFailure::Output(*output)); + Err(FunctionCallError::RespondToModel(response)) + } + Err(ToolError::Codex(err)) => { + let message = format!("execution error: {err:?}"); + let response = super::format_exec_output(&message); + event = ToolEventStage::Failure(ToolEventFailure::Message(message)); + Err(FunctionCallError::RespondToModel(response)) + } + Err(ToolError::Rejected(msg)) | Err(ToolError::SandboxDenied(msg)) => { + // Normalize common rejection messages for exec tools so tests and + // users see a clear, consistent phrase. + let normalized = if msg == "rejected by user" { + "exec command rejected by user".to_string() + } else { + msg + }; + let response = super::format_exec_output(&normalized); + event = ToolEventStage::Failure(ToolEventFailure::Message(normalized)); + Err(FunctionCallError::RespondToModel(response)) + } + }; + self.emit(ctx, event).await; + result + } } async fn emit_exec_end( diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index d91db362..a6e6f195 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -1,19 +1,24 @@ use std::collections::BTreeMap; -use std::collections::HashMap; -use std::sync::Arc; +use crate::apply_patch; +use crate::apply_patch::InternalApplyPatchInvocation; +use crate::apply_patch::convert_apply_patch_to_protocol; use crate::client_common::tools::FreeformTool; use crate::client_common::tools::FreeformToolFormat; use crate::client_common::tools::ResponsesApiTool; use crate::client_common::tools::ToolSpec; -use crate::exec::ExecParams; use crate::function_tool::FunctionCallError; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolOutput; use crate::tools::context::ToolPayload; -use crate::tools::handle_container_exec_with_params; +use crate::tools::events::ToolEmitter; +use crate::tools::events::ToolEventCtx; +use crate::tools::orchestrator::ToolOrchestrator; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; +use crate::tools::runtimes::apply_patch::ApplyPatchRequest; +use crate::tools::runtimes::apply_patch::ApplyPatchRuntime; +use crate::tools::sandboxing::ToolCtx; use crate::tools::spec::ApplyPatchToolArgs; use crate::tools::spec::JsonSchema; use async_trait::async_trait; @@ -64,30 +69,85 @@ impl ToolHandler for ApplyPatchHandler { } }; - let exec_params = ExecParams { - command: vec!["apply_patch".to_string(), patch_input.clone()], - cwd: turn.cwd.clone(), - timeout_ms: None, - env: HashMap::new(), - with_escalated_permissions: None, - justification: None, - arg0: None, - }; + // Re-parse and verify the patch so we can compute changes and approval. + // Avoid building temporary ExecParams/command vectors; derive directly from inputs. + let cwd = turn.cwd.clone(); + let command = vec!["apply_patch".to_string(), patch_input.clone()]; + match codex_apply_patch::maybe_parse_apply_patch_verified(&command, &cwd) { + codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => { + match apply_patch::apply_patch(session.as_ref(), turn.as_ref(), &call_id, changes) + .await + { + InternalApplyPatchInvocation::Output(item) => { + let content = item?; + Ok(ToolOutput::Function { + content, + success: Some(true), + }) + } + InternalApplyPatchInvocation::DelegateToExec(apply) => { + let emitter = ToolEmitter::apply_patch( + convert_apply_patch_to_protocol(&apply.action), + !apply.user_explicitly_approved_this_action, + ); + let event_ctx = ToolEventCtx::new( + session.as_ref(), + turn.as_ref(), + &call_id, + Some(&tracker), + ); + emitter.begin(event_ctx).await; - let content = handle_container_exec_with_params( - tool_name.as_str(), - exec_params, - Arc::clone(&session), - Arc::clone(&turn), - Arc::clone(&tracker), - call_id.clone(), - ) - .await?; + let req = ApplyPatchRequest { + patch: apply.action.patch.clone(), + cwd, + timeout_ms: None, + user_explicitly_approved: apply.user_explicitly_approved_this_action, + codex_exe: turn.codex_linux_sandbox_exe.clone(), + }; - Ok(ToolOutput::Function { - content, - success: Some(true), - }) + let mut orchestrator = ToolOrchestrator::new(); + let mut runtime = ApplyPatchRuntime::new(); + let tool_ctx = ToolCtx { + session: session.as_ref(), + turn: turn.as_ref(), + call_id: call_id.clone(), + tool_name: tool_name.to_string(), + }; + let out = orchestrator + .run(&mut runtime, &req, &tool_ctx, &turn, turn.approval_policy) + .await; + let event_ctx = ToolEventCtx::new( + session.as_ref(), + turn.as_ref(), + &call_id, + Some(&tracker), + ); + let content = emitter.finish(event_ctx, out).await?; + Ok(ToolOutput::Function { + content, + success: Some(true), + }) + } + } + } + codex_apply_patch::MaybeApplyPatchVerified::CorrectnessError(parse_error) => { + Err(FunctionCallError::RespondToModel(format!( + "apply_patch verification failed: {parse_error}" + ))) + } + codex_apply_patch::MaybeApplyPatchVerified::ShellParseError(error) => { + tracing::trace!("Failed to parse apply_patch input, {error:?}"); + Err(FunctionCallError::RespondToModel( + "apply_patch handler received invalid patch input".to_string(), + )) + } + codex_apply_patch::MaybeApplyPatchVerified::NotApplyPatch => { + Err(FunctionCallError::RespondToModel( + "apply_patch handler received non-apply_patch input".to_string(), + )) + } + } } } diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index e19a40be..12dd746f 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -2,6 +2,9 @@ use async_trait::async_trait; use codex_protocol::models::ShellToolCallParams; use std::sync::Arc; +use crate::apply_patch; +use crate::apply_patch::InternalApplyPatchInvocation; +use crate::apply_patch::convert_apply_patch_to_protocol; use crate::codex::TurnContext; use crate::exec::ExecParams; use crate::exec_env::create_env; @@ -9,9 +12,16 @@ use crate::function_tool::FunctionCallError; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolOutput; use crate::tools::context::ToolPayload; -use crate::tools::handle_container_exec_with_params; +use crate::tools::events::ToolEmitter; +use crate::tools::events::ToolEventCtx; +use crate::tools::orchestrator::ToolOrchestrator; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; +use crate::tools::runtimes::apply_patch::ApplyPatchRequest; +use crate::tools::runtimes::apply_patch::ApplyPatchRuntime; +use crate::tools::runtimes::shell::ShellRequest; +use crate::tools::runtimes::shell::ShellRuntime; +use crate::tools::sandboxing::ToolCtx; pub struct ShellHandler; @@ -61,35 +71,27 @@ impl ToolHandler for ShellHandler { )) })?; let exec_params = Self::to_exec_params(params, turn.as_ref()); - let content = handle_container_exec_with_params( + Self::run_exec_like( tool_name.as_str(), exec_params, - Arc::clone(&session), - Arc::clone(&turn), - Arc::clone(&tracker), - call_id.clone(), + session, + turn, + tracker, + call_id, ) - .await?; - Ok(ToolOutput::Function { - content, - success: Some(true), - }) + .await } ToolPayload::LocalShell { params } => { let exec_params = Self::to_exec_params(params, turn.as_ref()); - let content = handle_container_exec_with_params( + Self::run_exec_like( tool_name.as_str(), exec_params, - Arc::clone(&session), - Arc::clone(&turn), - Arc::clone(&tracker), - call_id.clone(), + session, + turn, + tracker, + call_id, ) - .await?; - Ok(ToolOutput::Function { - content, - success: Some(true), - }) + .await } _ => Err(FunctionCallError::RespondToModel(format!( "unsupported payload for shell handler: {tool_name}" @@ -97,3 +99,134 @@ impl ToolHandler for ShellHandler { } } } + +impl ShellHandler { + async fn run_exec_like( + tool_name: &str, + exec_params: ExecParams, + session: Arc, + turn: Arc, + tracker: crate::tools::context::SharedTurnDiffTracker, + call_id: String, + ) -> Result { + // Approval policy guard for explicit escalation in non-OnRequest modes. + if exec_params.with_escalated_permissions.unwrap_or(false) + && !matches!( + turn.approval_policy, + codex_protocol::protocol::AskForApproval::OnRequest + ) + { + return Err(FunctionCallError::RespondToModel(format!( + "approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}", + policy = turn.approval_policy + ))); + } + + // Intercept apply_patch if present. + match codex_apply_patch::maybe_parse_apply_patch_verified( + &exec_params.command, + &exec_params.cwd, + ) { + codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => { + match apply_patch::apply_patch(session.as_ref(), turn.as_ref(), &call_id, changes) + .await + { + InternalApplyPatchInvocation::Output(item) => { + // Programmatic apply_patch path; return its result. + let content = item?; + return Ok(ToolOutput::Function { + content, + success: Some(true), + }); + } + InternalApplyPatchInvocation::DelegateToExec(apply) => { + let emitter = ToolEmitter::apply_patch( + convert_apply_patch_to_protocol(&apply.action), + !apply.user_explicitly_approved_this_action, + ); + let event_ctx = ToolEventCtx::new( + session.as_ref(), + turn.as_ref(), + &call_id, + Some(&tracker), + ); + emitter.begin(event_ctx).await; + + let req = ApplyPatchRequest { + patch: apply.action.patch.clone(), + cwd: exec_params.cwd.clone(), + timeout_ms: exec_params.timeout_ms, + user_explicitly_approved: apply.user_explicitly_approved_this_action, + codex_exe: turn.codex_linux_sandbox_exe.clone(), + }; + let mut orchestrator = ToolOrchestrator::new(); + let mut runtime = ApplyPatchRuntime::new(); + let tool_ctx = ToolCtx { + session: session.as_ref(), + turn: turn.as_ref(), + call_id: call_id.clone(), + tool_name: tool_name.to_string(), + }; + let out = orchestrator + .run(&mut runtime, &req, &tool_ctx, &turn, turn.approval_policy) + .await; + let event_ctx = ToolEventCtx::new( + session.as_ref(), + turn.as_ref(), + &call_id, + Some(&tracker), + ); + let content = emitter.finish(event_ctx, out).await?; + return Ok(ToolOutput::Function { + content, + success: Some(true), + }); + } + } + } + codex_apply_patch::MaybeApplyPatchVerified::CorrectnessError(parse_error) => { + return Err(FunctionCallError::RespondToModel(format!( + "apply_patch verification failed: {parse_error}" + ))); + } + codex_apply_patch::MaybeApplyPatchVerified::ShellParseError(error) => { + tracing::trace!("Failed to parse shell command, {error:?}"); + // Fall through to regular shell execution. + } + codex_apply_patch::MaybeApplyPatchVerified::NotApplyPatch => { + // Fall through to regular shell execution. + } + } + + // Regular shell execution path. + let emitter = ToolEmitter::shell(exec_params.command.clone(), exec_params.cwd.clone()); + let event_ctx = ToolEventCtx::new(session.as_ref(), turn.as_ref(), &call_id, None); + emitter.begin(event_ctx).await; + + let req = ShellRequest { + command: exec_params.command.clone(), + cwd: exec_params.cwd.clone(), + timeout_ms: exec_params.timeout_ms, + env: exec_params.env.clone(), + with_escalated_permissions: exec_params.with_escalated_permissions, + justification: exec_params.justification.clone(), + }; + let mut orchestrator = ToolOrchestrator::new(); + let mut runtime = ShellRuntime::new(); + let tool_ctx = ToolCtx { + session: session.as_ref(), + turn: turn.as_ref(), + call_id: call_id.clone(), + tool_name: tool_name.to_string(), + }; + let out = orchestrator + .run(&mut runtime, &req, &tool_ctx, &turn, turn.approval_policy) + .await; + let event_ctx = ToolEventCtx::new(session.as_ref(), turn.as_ref(), &call_id, None); + let content = emitter.finish(event_ctx, out).await?; + Ok(ToolOutput::Function { + content, + success: Some(true), + }) + } +} diff --git a/codex-rs/core/src/tools/mod.rs b/codex-rs/core/src/tools/mod.rs index 4cef0a17..f22d064b 100644 --- a/codex-rs/core/src/tools/mod.rs +++ b/codex-rs/core/src/tools/mod.rs @@ -9,37 +9,11 @@ pub mod runtimes; pub mod sandboxing; pub mod spec; -use crate::apply_patch; -use crate::apply_patch::InternalApplyPatchInvocation; -use crate::apply_patch::convert_apply_patch_to_protocol; -use crate::codex::Session; -use crate::codex::TurnContext; -use crate::error::CodexErr; -use crate::error::SandboxErr; -use crate::exec::ExecParams; use crate::exec::ExecToolCallOutput; -use crate::function_tool::FunctionCallError; -use crate::tools::context::SharedTurnDiffTracker; -use crate::tools::events::ToolEmitter; -use crate::tools::events::ToolEventCtx; -use crate::tools::events::ToolEventFailure; -use crate::tools::events::ToolEventStage; -use crate::tools::orchestrator::ToolOrchestrator; -use crate::tools::runtimes::apply_patch::ApplyPatchRequest; -use crate::tools::runtimes::apply_patch::ApplyPatchRuntime; -use crate::tools::runtimes::shell::ShellRequest; -use crate::tools::runtimes::shell::ShellRuntime; -use crate::tools::sandboxing::ToolCtx; -use crate::tools::sandboxing::ToolError; -use codex_apply_patch::MaybeApplyPatchVerified; -use codex_apply_patch::maybe_parse_apply_patch_verified; -use codex_protocol::protocol::AskForApproval; use codex_utils_string::take_bytes_at_char_boundary; use codex_utils_string::take_last_bytes_at_char_boundary; pub use router::ToolRouter; use serde::Serialize; -use std::sync::Arc; -use tracing::trace; // Model-formatting limits: clients get full streams; only content sent to the model is truncated. pub(crate) const MODEL_FORMAT_MAX_BYTES: usize = 10 * 1024; // 10 KiB @@ -54,186 +28,6 @@ pub(crate) const TELEMETRY_PREVIEW_MAX_LINES: usize = 64; // lines pub(crate) const TELEMETRY_PREVIEW_TRUNCATION_NOTICE: &str = "[... telemetry preview truncated ...]"; -// TODO(jif) break this down -pub(crate) async fn handle_container_exec_with_params( - tool_name: &str, - params: ExecParams, - sess: Arc, - turn_context: Arc, - turn_diff_tracker: SharedTurnDiffTracker, - call_id: String, -) -> Result { - let _otel_event_manager = turn_context.client.get_otel_event_manager(); - - if params.with_escalated_permissions.unwrap_or(false) - && !matches!(turn_context.approval_policy, AskForApproval::OnRequest) - { - return Err(FunctionCallError::RespondToModel(format!( - "approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}", - policy = turn_context.approval_policy - ))); - } - - // check if this was a patch, and apply it if so - let apply_patch_exec = match maybe_parse_apply_patch_verified(¶ms.command, ¶ms.cwd) { - MaybeApplyPatchVerified::Body(changes) => { - match apply_patch::apply_patch(sess.as_ref(), turn_context.as_ref(), &call_id, changes) - .await - { - InternalApplyPatchInvocation::Output(item) => return item, - InternalApplyPatchInvocation::DelegateToExec(apply_patch_exec) => { - Some(apply_patch_exec) - } - } - } - MaybeApplyPatchVerified::CorrectnessError(parse_error) => { - // It looks like an invocation of `apply_patch`, but we - // could not resolve it into a patch that would apply - // cleanly. Return to model for resample. - return Err(FunctionCallError::RespondToModel(format!( - "apply_patch verification failed: {parse_error}" - ))); - } - MaybeApplyPatchVerified::ShellParseError(error) => { - trace!("Failed to parse shell command, {error:?}"); - None - } - MaybeApplyPatchVerified::NotApplyPatch => None, - }; - - let (event_emitter, diff_opt) = match apply_patch_exec.as_ref() { - Some(exec) => ( - ToolEmitter::apply_patch( - convert_apply_patch_to_protocol(&exec.action), - !exec.user_explicitly_approved_this_action, - ), - Some(&turn_diff_tracker), - ), - None => ( - ToolEmitter::shell(params.command.clone(), params.cwd.clone()), - None, - ), - }; - - let event_ctx = ToolEventCtx::new(sess.as_ref(), turn_context.as_ref(), &call_id, diff_opt); - event_emitter.emit(event_ctx, ToolEventStage::Begin).await; - - // Build runtime contexts only when needed (shell/apply_patch below). - - if let Some(exec) = apply_patch_exec { - // Route apply_patch execution through the new orchestrator/runtime. - let req = ApplyPatchRequest { - patch: exec.action.patch.clone(), - cwd: params.cwd.clone(), - timeout_ms: params.timeout_ms, - user_explicitly_approved: exec.user_explicitly_approved_this_action, - codex_exe: turn_context.codex_linux_sandbox_exe.clone(), - }; - - let mut orchestrator = ToolOrchestrator::new(); - let mut runtime = ApplyPatchRuntime::new(); - let tool_ctx = ToolCtx { - session: sess.as_ref(), - turn: turn_context.as_ref(), - call_id: call_id.clone(), - tool_name: tool_name.to_string(), - }; - - let out = orchestrator - .run( - &mut runtime, - &req, - &tool_ctx, - &turn_context, - turn_context.approval_policy, - ) - .await; - - handle_exec_outcome(&event_emitter, event_ctx, out).await - } else { - // Route shell execution through the new orchestrator/runtime. - let req = ShellRequest { - command: params.command.clone(), - cwd: params.cwd.clone(), - timeout_ms: params.timeout_ms, - env: params.env.clone(), - with_escalated_permissions: params.with_escalated_permissions, - justification: params.justification.clone(), - }; - - let mut orchestrator = ToolOrchestrator::new(); - let mut runtime = ShellRuntime::new(); - let tool_ctx = ToolCtx { - session: sess.as_ref(), - turn: turn_context.as_ref(), - call_id: call_id.clone(), - tool_name: tool_name.to_string(), - }; - - let out = orchestrator - .run( - &mut runtime, - &req, - &tool_ctx, - &turn_context, - turn_context.approval_policy, - ) - .await; - - handle_exec_outcome(&event_emitter, event_ctx, out).await - } -} - -async fn handle_exec_outcome( - event_emitter: &ToolEmitter, - event_ctx: ToolEventCtx<'_>, - out: Result, -) -> Result { - let event; - let result = match out { - Ok(output) => { - let content = format_exec_output_for_model(&output); - let exit_code = output.exit_code; - event = ToolEventStage::Success(output); - if exit_code == 0 { - Ok(content) - } else { - Err(FunctionCallError::RespondToModel(content)) - } - } - Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Timeout { output }))) - | Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { output }))) => { - let response = format_exec_output_for_model(&output); - event = ToolEventStage::Failure(ToolEventFailure::Output(*output)); - Err(FunctionCallError::RespondToModel(response)) - } - Err(ToolError::Codex(err)) => { - let message = format!("execution error: {err:?}"); - let response = format_exec_output(&message); - event = ToolEventStage::Failure(ToolEventFailure::Message(message)); - Err(FunctionCallError::RespondToModel(format_exec_output( - &response, - ))) - } - Err(ToolError::Rejected(msg)) | Err(ToolError::SandboxDenied(msg)) => { - // Normalize common rejection messages for exec tools so tests and - // users see a clear, consistent phrase. - let normalized = if msg == "rejected by user" { - "exec command rejected by user".to_string() - } else { - msg - }; - let response = format_exec_output(&normalized); - event = ToolEventStage::Failure(ToolEventFailure::Message(normalized)); - Err(FunctionCallError::RespondToModel(format_exec_output( - &response, - ))) - } - }; - event_emitter.emit(event_ctx, event).await; - result -} - /// Format the combined exec output for sending back to the model. /// Includes exit code and duration metadata; truncates large bodies safely. pub fn format_exec_output_for_model(exec_output: &ExecToolCallOutput) -> String { @@ -363,6 +157,7 @@ fn truncate_formatted_exec_output(content: &str, total_lines: usize) -> String { #[cfg(test)] mod tests { use super::*; + use crate::function_tool::FunctionCallError; use regex_lite::Regex; fn truncate_function_error(err: FunctionCallError) -> FunctionCallError {