Simplify tool implemetations (#4160)

Use Result<String, FunctionCallError> for all tool handling code and
rely on error propagation instead of creating failed items everywhere.
This commit is contained in:
pakrym-oai
2025-09-24 10:27:35 -07:00
committed by GitHub
parent bffdbec2c5
commit addc946d13
7 changed files with 223 additions and 391 deletions

View File

@@ -1,13 +1,12 @@
use crate::codex::Session; use crate::codex::Session;
use crate::codex::TurnContext; use crate::codex::TurnContext;
use crate::function_tool::FunctionCallError;
use crate::protocol::FileChange; use crate::protocol::FileChange;
use crate::protocol::ReviewDecision; use crate::protocol::ReviewDecision;
use crate::safety::SafetyCheck; use crate::safety::SafetyCheck;
use crate::safety::assess_patch_safety; use crate::safety::assess_patch_safety;
use codex_apply_patch::ApplyPatchAction; use codex_apply_patch::ApplyPatchAction;
use codex_apply_patch::ApplyPatchFileChange; use codex_apply_patch::ApplyPatchFileChange;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
@@ -17,7 +16,7 @@ pub(crate) enum InternalApplyPatchInvocation {
/// The `apply_patch` call was handled programmatically, without any sort /// The `apply_patch` call was handled programmatically, without any sort
/// of sandbox, because the user explicitly approved it. This is the /// of sandbox, because the user explicitly approved it. This is the
/// result to use with the `shell` function call that contained `apply_patch`. /// result to use with the `shell` function call that contained `apply_patch`.
Output(ResponseInputItem), Output(Result<String, FunctionCallError>),
/// The `apply_patch` call was approved, either automatically because it /// The `apply_patch` call was approved, either automatically because it
/// appears that it should be allowed based on the user's sandbox policy /// appears that it should be allowed based on the user's sandbox policy
@@ -33,12 +32,6 @@ pub(crate) struct ApplyPatchExec {
pub(crate) user_explicitly_approved_this_action: bool, pub(crate) user_explicitly_approved_this_action: bool,
} }
impl From<ResponseInputItem> for InternalApplyPatchInvocation {
fn from(item: ResponseInputItem) -> Self {
InternalApplyPatchInvocation::Output(item)
}
}
pub(crate) async fn apply_patch( pub(crate) async fn apply_patch(
sess: &Session, sess: &Session,
turn_context: &TurnContext, turn_context: &TurnContext,
@@ -77,25 +70,15 @@ pub(crate) async fn apply_patch(
}) })
} }
ReviewDecision::Denied | ReviewDecision::Abort => { ReviewDecision::Denied | ReviewDecision::Abort => {
ResponseInputItem::FunctionCallOutput { InternalApplyPatchInvocation::Output(Err(FunctionCallError::RespondToModel(
call_id: call_id.to_owned(), "patch rejected by user".to_string(),
output: FunctionCallOutputPayload { )))
content: "patch rejected by user".to_string(),
success: Some(false),
},
}
.into()
} }
} }
} }
SafetyCheck::Reject { reason } => ResponseInputItem::FunctionCallOutput { SafetyCheck::Reject { reason } => InternalApplyPatchInvocation::Output(Err(
call_id: call_id.to_owned(), FunctionCallError::RespondToModel(format!("patch rejected: {reason}")),
output: FunctionCallOutputPayload { )),
content: format!("patch rejected: {reason}"),
success: Some(false),
},
}
.into(),
} }
} }

View File

@@ -10,6 +10,7 @@ use std::time::Duration;
use crate::AuthManager; use crate::AuthManager;
use crate::client_common::REVIEW_PROMPT; use crate::client_common::REVIEW_PROMPT;
use crate::event_mapping::map_response_item_to_event_messages; use crate::event_mapping::map_response_item_to_event_messages;
use crate::function_tool::FunctionCallError;
use crate::review_format::format_review_findings_block; use crate::review_format::format_review_findings_block;
use crate::user_notification::UserNotifier; use crate::user_notification::UserNotifier;
use async_channel::Receiver; use async_channel::Receiver;
@@ -2232,18 +2233,41 @@ async fn handle_response_item(
.. ..
} => { } => {
info!("FunctionCall: {name}({arguments})"); info!("FunctionCall: {name}({arguments})");
Some( if let Some((server, tool_name)) = sess.mcp_connection_manager.parse_tool_name(&name) {
handle_function_call( let resp = handle_mcp_tool_call(
sess,
sub_id,
call_id.clone(),
server,
tool_name,
arguments,
)
.await;
Some(resp)
} else {
let result = handle_function_call(
sess, sess,
turn_context, turn_context,
turn_diff_tracker, turn_diff_tracker,
sub_id.to_string(), sub_id.to_string(),
name, name,
arguments, arguments,
call_id, call_id.clone(),
) )
.await, .await;
)
let output = match result {
Ok(content) => FunctionCallOutputPayload {
content,
success: Some(true),
},
Err(FunctionCallError::RespondToModel(msg)) => FunctionCallOutputPayload {
content: msg,
success: Some(false),
},
};
Some(ResponseInputItem::FunctionCallOutput { call_id, output })
}
} }
ResponseItem::LocalShellCall { ResponseItem::LocalShellCall {
id, id,
@@ -2276,17 +2300,32 @@ async fn handle_response_item(
}; };
let exec_params = to_exec_params(params, turn_context); let exec_params = to_exec_params(params, turn_context);
Some( {
handle_container_exec_with_params( let result = handle_container_exec_with_params(
exec_params, exec_params,
sess, sess,
turn_context, turn_context,
turn_diff_tracker, turn_diff_tracker,
sub_id.to_string(), sub_id.to_string(),
effective_call_id, effective_call_id.clone(),
) )
.await, .await;
)
let output = match result {
Ok(content) => FunctionCallOutputPayload {
content,
success: Some(true),
},
Err(FunctionCallError::RespondToModel(msg)) => FunctionCallOutputPayload {
content: msg,
success: Some(false),
},
};
Some(ResponseInputItem::FunctionCallOutput {
call_id: effective_call_id,
output,
})
}
} }
ResponseItem::CustomToolCall { ResponseItem::CustomToolCall {
id: _, id: _,
@@ -2294,18 +2333,24 @@ async fn handle_response_item(
name, name,
input, input,
status: _, status: _,
} => Some( } => {
handle_custom_tool_call( let result = handle_custom_tool_call(
sess, sess,
turn_context, turn_context,
turn_diff_tracker, turn_diff_tracker,
sub_id.to_string(), sub_id.to_string(),
name, name,
input, input,
call_id, call_id.clone(),
) )
.await, .await;
),
let output = match result {
Ok(content) => content,
Err(FunctionCallError::RespondToModel(msg)) => msg,
};
Some(ResponseInputItem::CustomToolCallOutput { call_id, output })
}
ResponseItem::FunctionCallOutput { .. } => { ResponseItem::FunctionCallOutput { .. } => {
debug!("unexpected FunctionCallOutput from stream"); debug!("unexpected FunctionCallOutput from stream");
None None
@@ -2342,22 +2387,17 @@ async fn handle_response_item(
async fn handle_unified_exec_tool_call( async fn handle_unified_exec_tool_call(
sess: &Session, sess: &Session,
call_id: String,
session_id: Option<String>, session_id: Option<String>,
arguments: Vec<String>, arguments: Vec<String>,
timeout_ms: Option<u64>, timeout_ms: Option<u64>,
) -> ResponseInputItem { ) -> Result<String, FunctionCallError> {
let parsed_session_id = if let Some(session_id) = session_id { let parsed_session_id = if let Some(session_id) = session_id {
match session_id.parse::<i32>() { match session_id.parse::<i32>() {
Ok(parsed) => Some(parsed), Ok(parsed) => Some(parsed),
Err(output) => { Err(output) => {
return ResponseInputItem::FunctionCallOutput { return Err(FunctionCallError::RespondToModel(format!(
call_id: call_id.to_string(), "invalid session_id: {session_id} due to error {output:?}"
output: FunctionCallOutputPayload { )));
content: format!("invalid session_id: {session_id} due to error {output}"),
success: Some(false),
},
};
} }
} }
} else { } else {
@@ -2370,40 +2410,29 @@ async fn handle_unified_exec_tool_call(
timeout_ms, timeout_ms,
}; };
let result = sess.unified_exec_manager.handle_request(request).await; let value = sess
.unified_exec_manager
.handle_request(request)
.await
.map_err(|err| {
FunctionCallError::RespondToModel(format!("unified exec failed: {err:?}"))
})?;
let output_payload = match result { #[derive(Serialize)]
Ok(value) => { struct SerializedUnifiedExecResult {
#[derive(Serialize)] session_id: Option<String>,
struct SerializedUnifiedExecResult<'a> { output: String,
session_id: Option<String>,
output: &'a str,
}
match serde_json::to_string(&SerializedUnifiedExecResult {
session_id: value.session_id.map(|id| id.to_string()),
output: &value.output,
}) {
Ok(serialized) => FunctionCallOutputPayload {
content: serialized,
success: Some(true),
},
Err(err) => FunctionCallOutputPayload {
content: format!("failed to serialize unified exec output: {err}"),
success: Some(false),
},
}
}
Err(err) => FunctionCallOutputPayload {
content: format!("unified exec failed: {err}"),
success: Some(false),
},
};
ResponseInputItem::FunctionCallOutput {
call_id,
output: output_payload,
} }
serde_json::to_string(&SerializedUnifiedExecResult {
session_id: value.session_id.map(|id| id.to_string()),
output: value.output,
})
.map_err(|err| {
FunctionCallError::RespondToModel(format!(
"failed to serialize unified exec output: {err:?}"
))
})
} }
async fn handle_function_call( async fn handle_function_call(
@@ -2414,15 +2443,10 @@ async fn handle_function_call(
name: String, name: String,
arguments: String, arguments: String,
call_id: String, call_id: String,
) -> ResponseInputItem { ) -> Result<String, FunctionCallError> {
match name.as_str() { match name.as_str() {
"container.exec" | "shell" => { "container.exec" | "shell" => {
let params = match parse_container_exec_arguments(arguments, turn_context, &call_id) { let params = parse_container_exec_arguments(arguments, turn_context, &call_id)?;
Ok(params) => params,
Err(output) => {
return *output;
}
};
handle_container_exec_with_params( handle_container_exec_with_params(
params, params,
sess, sess,
@@ -2443,74 +2467,41 @@ async fn handle_function_call(
timeout_ms: Option<u64>, timeout_ms: Option<u64>,
} }
let args = match serde_json::from_str::<UnifiedExecArgs>(&arguments) { let args: UnifiedExecArgs = serde_json::from_str(&arguments).map_err(|err| {
Ok(args) => args, FunctionCallError::RespondToModel(format!(
Err(err) => { "failed to parse function arguments: {err:?}"
return ResponseInputItem::FunctionCallOutput { ))
call_id, })?;
output: FunctionCallOutputPayload {
content: format!("failed to parse function arguments: {err}"),
success: Some(false),
},
};
}
};
handle_unified_exec_tool_call( handle_unified_exec_tool_call(sess, args.session_id, args.input, args.timeout_ms).await
sess,
call_id,
args.session_id,
args.input,
args.timeout_ms,
)
.await
} }
"view_image" => { "view_image" => {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
struct SeeImageArgs { struct SeeImageArgs {
path: String, path: String,
} }
let args = match serde_json::from_str::<SeeImageArgs>(&arguments) { let args: SeeImageArgs = serde_json::from_str(&arguments).map_err(|e| {
Ok(a) => a, FunctionCallError::RespondToModel(format!(
Err(e) => { "failed to parse function arguments: {e:?}"
return ResponseInputItem::FunctionCallOutput { ))
call_id, })?;
output: FunctionCallOutputPayload {
content: format!("failed to parse function arguments: {e}"),
success: Some(false),
},
};
}
};
let abs = turn_context.resolve_path(Some(args.path)); let abs = turn_context.resolve_path(Some(args.path));
let output = match sess sess.inject_input(vec![InputItem::LocalImage { path: abs }])
.inject_input(vec![InputItem::LocalImage { path: abs }])
.await .await
{ .map_err(|_| {
Ok(()) => FunctionCallOutputPayload { FunctionCallError::RespondToModel(
content: "attached local image path".to_string(), "unable to attach image (no active task)".to_string(),
success: Some(true), )
}, })?;
Err(_) => FunctionCallOutputPayload {
content: "unable to attach image (no active task)".to_string(), Ok("attached local image path".to_string())
success: Some(false),
},
};
ResponseInputItem::FunctionCallOutput { call_id, output }
} }
"apply_patch" => { "apply_patch" => {
let args = match serde_json::from_str::<ApplyPatchToolArgs>(&arguments) { let args: ApplyPatchToolArgs = serde_json::from_str(&arguments).map_err(|e| {
Ok(a) => a, FunctionCallError::RespondToModel(format!(
Err(e) => { "failed to parse function arguments: {e:?}"
return ResponseInputItem::FunctionCallOutput { ))
call_id, })?;
output: FunctionCallOutputPayload {
content: format!("failed to parse function arguments: {e}"),
success: None,
},
};
}
};
let exec_params = ExecParams { let exec_params = ExecParams {
command: vec!["apply_patch".to_string(), args.input.clone()], command: vec!["apply_patch".to_string(), args.input.clone()],
cwd: turn_context.cwd.clone(), cwd: turn_context.cwd.clone(),
@@ -2532,69 +2523,39 @@ async fn handle_function_call(
"update_plan" => handle_update_plan(sess, arguments, sub_id, call_id).await, "update_plan" => handle_update_plan(sess, arguments, sub_id, call_id).await,
EXEC_COMMAND_TOOL_NAME => { EXEC_COMMAND_TOOL_NAME => {
// TODO(mbolin): Sandbox check. // TODO(mbolin): Sandbox check.
let exec_params = match serde_json::from_str::<ExecCommandParams>(&arguments) { let exec_params: ExecCommandParams = serde_json::from_str(&arguments).map_err(|e| {
Ok(params) => params, FunctionCallError::RespondToModel(format!(
Err(e) => { "failed to parse function arguments: {e:?}"
return ResponseInputItem::FunctionCallOutput { ))
call_id, })?;
output: FunctionCallOutputPayload {
content: format!("failed to parse function arguments: {e}"),
success: Some(false),
},
};
}
};
let result = sess let result = sess
.session_manager .session_manager
.handle_exec_command_request(exec_params) .handle_exec_command_request(exec_params)
.await; .await;
let function_call_output = crate::exec_command::result_into_payload(result); match result {
ResponseInputItem::FunctionCallOutput { Ok(output) => Ok(output.to_text_output()),
call_id, Err(err) => Err(FunctionCallError::RespondToModel(err)),
output: function_call_output,
} }
} }
WRITE_STDIN_TOOL_NAME => { WRITE_STDIN_TOOL_NAME => {
let write_stdin_params = match serde_json::from_str::<WriteStdinParams>(&arguments) { let write_stdin_params =
Ok(params) => params, serde_json::from_str::<WriteStdinParams>(&arguments).map_err(|e| {
Err(e) => { FunctionCallError::RespondToModel(format!(
return ResponseInputItem::FunctionCallOutput { "failed to parse function arguments: {e:?}"
call_id, ))
output: FunctionCallOutputPayload { })?;
content: format!("failed to parse function arguments: {e}"),
success: Some(false),
},
};
}
};
let result = sess let result = sess
.session_manager .session_manager
.handle_write_stdin_request(write_stdin_params) .handle_write_stdin_request(write_stdin_params)
.await; .await
let function_call_output: FunctionCallOutputPayload = .map_err(FunctionCallError::RespondToModel)?;
crate::exec_command::result_into_payload(result);
ResponseInputItem::FunctionCallOutput { Ok(result.to_text_output())
call_id,
output: function_call_output,
}
}
_ => {
match sess.mcp_connection_manager.parse_tool_name(&name) {
Some((server, tool_name)) => {
handle_mcp_tool_call(sess, &sub_id, call_id, server, tool_name, arguments).await
}
None => {
// Unknown function: reply with structured failure so the model can adapt.
ResponseInputItem::FunctionCallOutput {
call_id,
output: FunctionCallOutputPayload {
content: format!("unsupported call: {name}"),
success: None,
},
}
}
}
} }
_ => Err(FunctionCallError::RespondToModel(format!(
"unsupported call: {name}"
))),
} }
} }
@@ -2606,7 +2567,7 @@ async fn handle_custom_tool_call(
name: String, name: String,
input: String, input: String,
call_id: String, call_id: String,
) -> ResponseInputItem { ) -> Result<String, FunctionCallError> {
info!("CustomToolCall: {name} {input}"); info!("CustomToolCall: {name} {input}");
match name.as_str() { match name.as_str() {
"apply_patch" => { "apply_patch" => {
@@ -2618,7 +2579,8 @@ async fn handle_custom_tool_call(
with_escalated_permissions: None, with_escalated_permissions: None,
justification: None, justification: None,
}; };
let resp = handle_container_exec_with_params(
handle_container_exec_with_params(
exec_params, exec_params,
sess, sess,
turn_context, turn_context,
@@ -2626,26 +2588,13 @@ async fn handle_custom_tool_call(
sub_id, sub_id,
call_id, call_id,
) )
.await; .await
// Convert function-call style output into a custom tool call output
match resp {
ResponseInputItem::FunctionCallOutput { call_id, output } => {
ResponseInputItem::CustomToolCallOutput {
call_id,
output: output.content,
}
}
// Pass through if already a custom tool output or other variant
other => other,
}
} }
_ => { _ => {
debug!("unexpected CustomToolCall from stream"); debug!("unexpected CustomToolCall from stream");
ResponseInputItem::CustomToolCallOutput { Err(FunctionCallError::RespondToModel(format!(
call_id, "unsupported custom tool call: {name}"
output: format!("unsupported custom tool call: {name}"), )))
}
} }
} }
} }
@@ -2664,23 +2613,13 @@ fn to_exec_params(params: ShellToolCallParams, turn_context: &TurnContext) -> Ex
fn parse_container_exec_arguments( fn parse_container_exec_arguments(
arguments: String, arguments: String,
turn_context: &TurnContext, turn_context: &TurnContext,
call_id: &str, _call_id: &str,
) -> Result<ExecParams, Box<ResponseInputItem>> { ) -> Result<ExecParams, FunctionCallError> {
// parse command serde_json::from_str::<ShellToolCallParams>(&arguments)
match serde_json::from_str::<ShellToolCallParams>(&arguments) { .map(|p| to_exec_params(p, turn_context))
Ok(shell_tool_call_params) => Ok(to_exec_params(shell_tool_call_params, turn_context)), .map_err(|e| {
Err(e) => { FunctionCallError::RespondToModel(format!("failed to parse function arguments: {e:?}"))
// allow model to re-sample })
let output = ResponseInputItem::FunctionCallOutput {
call_id: call_id.to_string(),
output: FunctionCallOutputPayload {
content: format!("failed to parse function arguments: {e}"),
success: None,
},
};
Err(Box::new(output))
}
}
} }
pub struct ExecInvokeArgs<'a> { pub struct ExecInvokeArgs<'a> {
@@ -2717,20 +2656,14 @@ async fn handle_container_exec_with_params(
turn_diff_tracker: &mut TurnDiffTracker, turn_diff_tracker: &mut TurnDiffTracker,
sub_id: String, sub_id: String,
call_id: String, call_id: String,
) -> ResponseInputItem { ) -> Result<String, FunctionCallError> {
if params.with_escalated_permissions.unwrap_or(false) if params.with_escalated_permissions.unwrap_or(false)
&& !matches!(turn_context.approval_policy, AskForApproval::OnRequest) && !matches!(turn_context.approval_policy, AskForApproval::OnRequest)
{ {
return ResponseInputItem::FunctionCallOutput { return Err(FunctionCallError::RespondToModel(format!(
call_id, "approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}",
output: FunctionCallOutputPayload { policy = turn_context.approval_policy
content: 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
),
success: None,
},
};
} }
// check if this was a patch, and apply it if so // check if this was a patch, and apply it if so
@@ -2747,13 +2680,9 @@ async fn handle_container_exec_with_params(
// It looks like an invocation of `apply_patch`, but we // It looks like an invocation of `apply_patch`, but we
// could not resolve it into a patch that would apply // could not resolve it into a patch that would apply
// cleanly. Return to model for resample. // cleanly. Return to model for resample.
return ResponseInputItem::FunctionCallOutput { return Err(FunctionCallError::RespondToModel(format!(
call_id, "error: {parse_error:#?}"
output: FunctionCallOutputPayload { )));
content: format!("error: {parse_error:#}"),
success: None,
},
};
} }
MaybeApplyPatchVerified::ShellParseError(error) => { MaybeApplyPatchVerified::ShellParseError(error) => {
trace!("Failed to parse shell command, {error:?}"); trace!("Failed to parse shell command, {error:?}");
@@ -2771,13 +2700,9 @@ async fn handle_container_exec_with_params(
.ok() .ok()
.map(|p| p.to_string_lossy().to_string()); .map(|p| p.to_string_lossy().to_string());
let Some(path_to_codex) = path_to_codex else { let Some(path_to_codex) = path_to_codex else {
return ResponseInputItem::FunctionCallOutput { return Err(FunctionCallError::RespondToModel(
call_id, "failed to determine path to codex executable".to_string(),
output: FunctionCallOutputPayload { ));
content: "failed to determine path to codex executable".to_string(),
success: None,
},
};
}; };
let params = ExecParams { let params = ExecParams {
@@ -2843,13 +2768,9 @@ async fn handle_container_exec_with_params(
sess.add_approved_command(params.command.clone()).await; sess.add_approved_command(params.command.clone()).await;
} }
ReviewDecision::Denied | ReviewDecision::Abort => { ReviewDecision::Denied | ReviewDecision::Abort => {
return ResponseInputItem::FunctionCallOutput { return Err(FunctionCallError::RespondToModel(
call_id, "exec command rejected by user".to_string(),
output: FunctionCallOutputPayload { ));
content: "exec command rejected by user".to_string(),
success: None,
},
};
} }
} }
// No sandboxing is applied because the user has given // No sandboxing is applied because the user has given
@@ -2859,13 +2780,9 @@ async fn handle_container_exec_with_params(
SandboxType::None SandboxType::None
} }
SafetyCheck::Reject { reason } => { SafetyCheck::Reject { reason } => {
return ResponseInputItem::FunctionCallOutput { return Err(FunctionCallError::RespondToModel(format!(
call_id, "exec command rejected: {reason:?}"
output: FunctionCallOutputPayload { )));
content: format!("exec command rejected: {reason}"),
success: None,
},
};
} }
}; };
@@ -2912,15 +2829,11 @@ async fn handle_container_exec_with_params(
match output_result { match output_result {
Ok(output) => { Ok(output) => {
let ExecToolCallOutput { exit_code, .. } = &output; let ExecToolCallOutput { exit_code, .. } = &output;
let is_success = *exit_code == 0;
let content = format_exec_output(&output); let content = format_exec_output(&output);
ResponseInputItem::FunctionCallOutput { if *exit_code == 0 {
call_id: call_id.clone(), Ok(content)
output: FunctionCallOutputPayload { } else {
content, Err(FunctionCallError::RespondToModel(content))
success: Some(is_success),
},
} }
} }
Err(CodexErr::Sandbox(error)) => { Err(CodexErr::Sandbox(error)) => {
@@ -2935,13 +2848,9 @@ async fn handle_container_exec_with_params(
) )
.await .await
} }
Err(e) => ResponseInputItem::FunctionCallOutput { Err(e) => Err(FunctionCallError::RespondToModel(format!(
call_id: call_id.clone(), "execution error: {e:?}"
output: FunctionCallOutputPayload { ))),
content: format!("execution error: {e}"),
success: None,
},
},
} }
} }
@@ -2953,35 +2862,23 @@ async fn handle_sandbox_error(
sandbox_type: SandboxType, sandbox_type: SandboxType,
sess: &Session, sess: &Session,
turn_context: &TurnContext, turn_context: &TurnContext,
) -> ResponseInputItem { ) -> Result<String, FunctionCallError> {
let call_id = exec_command_context.call_id.clone(); let call_id = exec_command_context.call_id.clone();
let sub_id = exec_command_context.sub_id.clone(); let sub_id = exec_command_context.sub_id.clone();
let cwd = exec_command_context.cwd.clone(); let cwd = exec_command_context.cwd.clone();
if let SandboxErr::Timeout { output } = &error { if let SandboxErr::Timeout { output } = &error {
let content = format_exec_output(output); let content = format_exec_output(output);
return ResponseInputItem::FunctionCallOutput { return Err(FunctionCallError::RespondToModel(content));
call_id,
output: FunctionCallOutputPayload {
content,
success: Some(false),
},
};
} }
// Early out if either the user never wants to be asked for approval, or // Early out if either the user never wants to be asked for approval, or
// we're letting the model manage escalation requests. Otherwise, continue // we're letting the model manage escalation requests. Otherwise, continue
match turn_context.approval_policy { match turn_context.approval_policy {
AskForApproval::Never | AskForApproval::OnRequest => { AskForApproval::Never | AskForApproval::OnRequest => {
return ResponseInputItem::FunctionCallOutput { return Err(FunctionCallError::RespondToModel(format!(
call_id, "failed in sandbox {sandbox_type:?} with execution error: {error:?}"
output: FunctionCallOutputPayload { )));
content: format!(
"failed in sandbox {sandbox_type:?} with execution error: {error}"
),
success: Some(false),
},
};
} }
AskForApproval::UnlessTrusted | AskForApproval::OnFailure => (), AskForApproval::UnlessTrusted | AskForApproval::OnFailure => (),
} }
@@ -3047,36 +2944,23 @@ async fn handle_sandbox_error(
match retry_output_result { match retry_output_result {
Ok(retry_output) => { Ok(retry_output) => {
let ExecToolCallOutput { exit_code, .. } = &retry_output; let ExecToolCallOutput { exit_code, .. } = &retry_output;
let is_success = *exit_code == 0;
let content = format_exec_output(&retry_output); let content = format_exec_output(&retry_output);
if *exit_code == 0 {
ResponseInputItem::FunctionCallOutput { Ok(content)
call_id: call_id.clone(), } else {
output: FunctionCallOutputPayload { Err(FunctionCallError::RespondToModel(content))
content,
success: Some(is_success),
},
} }
} }
Err(e) => ResponseInputItem::FunctionCallOutput { Err(e) => Err(FunctionCallError::RespondToModel(format!(
call_id: call_id.clone(), "retry failed: {e}"
output: FunctionCallOutputPayload { ))),
content: format!("retry failed: {e}"),
success: None,
},
},
} }
} }
ReviewDecision::Denied | ReviewDecision::Abort => { ReviewDecision::Denied | ReviewDecision::Abort => {
// Fall through to original failure handling. // Fall through to original failure handling.
ResponseInputItem::FunctionCallOutput { Err(FunctionCallError::RespondToModel(
call_id, "exec command rejected by user".to_string(),
output: FunctionCallOutputPayload { ))
content: "exec command rejected by user".to_string(),
success: None,
},
}
} }
} }
} }
@@ -3798,8 +3682,8 @@ mod tests {
) )
.await; .await;
let ResponseInputItem::FunctionCallOutput { output, .. } = resp else { let Err(FunctionCallError::RespondToModel(output)) = resp else {
panic!("expected FunctionCallOutput"); panic!("expected error result");
}; };
let expected = format!( let expected = format!(
@@ -3807,7 +3691,7 @@ mod tests {
policy = turn_context.approval_policy policy = turn_context.approval_policy
); );
pretty_assertions::assert_eq!(output.content, expected); pretty_assertions::assert_eq!(output, expected);
// Now retry the same command WITHOUT escalated permissions; should succeed. // Now retry the same command WITHOUT escalated permissions; should succeed.
// Force DangerFullAccess to avoid platform sandbox dependencies in tests. // Force DangerFullAccess to avoid platform sandbox dependencies in tests.
@@ -3823,9 +3707,7 @@ mod tests {
) )
.await; .await;
let ResponseInputItem::FunctionCallOutput { output, .. } = resp2 else { let output = resp2.expect("expected Ok result");
panic!("expected FunctionCallOutput on retry");
};
#[derive(Deserialize, PartialEq, Eq, Debug)] #[derive(Deserialize, PartialEq, Eq, Debug)]
struct ResponseExecMetadata { struct ResponseExecMetadata {
@@ -3839,10 +3721,9 @@ mod tests {
} }
let exec_output: ResponseExecOutput = let exec_output: ResponseExecOutput =
serde_json::from_str(&output.content).expect("valid exec output json"); serde_json::from_str(&output).expect("valid exec output json");
pretty_assertions::assert_eq!(exec_output.metadata, ResponseExecMetadata { exit_code: 0 }); pretty_assertions::assert_eq!(exec_output.metadata, ResponseExecMetadata { exit_code: 0 });
assert!(exec_output.output.contains("hi")); assert!(exec_output.output.contains("hi"));
pretty_assertions::assert_eq!(output.success, Some(true));
} }
} }

View File

@@ -12,4 +12,3 @@ pub use responses_api::WRITE_STDIN_TOOL_NAME;
pub use responses_api::create_exec_command_tool_for_responses_api; pub use responses_api::create_exec_command_tool_for_responses_api;
pub use responses_api::create_write_stdin_tool_for_responses_api; pub use responses_api::create_write_stdin_tool_for_responses_api;
pub use session_manager::SessionManager as ExecSessionManager; pub use session_manager::SessionManager as ExecSessionManager;
pub use session_manager::result_into_payload;

View File

@@ -21,7 +21,6 @@ use crate::exec_command::exec_command_params::WriteStdinParams;
use crate::exec_command::exec_command_session::ExecCommandSession; use crate::exec_command::exec_command_session::ExecCommandSession;
use crate::exec_command::session_id::SessionId; use crate::exec_command::session_id::SessionId;
use crate::truncate::truncate_middle; use crate::truncate::truncate_middle;
use codex_protocol::models::FunctionCallOutputPayload;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct SessionManager { pub struct SessionManager {
@@ -38,7 +37,7 @@ pub struct ExecCommandOutput {
} }
impl ExecCommandOutput { impl ExecCommandOutput {
fn to_text_output(&self) -> String { pub(crate) fn to_text_output(&self) -> String {
let wall_time_secs = self.wall_time.as_secs_f32(); let wall_time_secs = self.wall_time.as_secs_f32();
let termination_status = match self.exit_status { let termination_status = match self.exit_status {
ExitStatus::Exited(code) => format!("Process exited with code {code}"), ExitStatus::Exited(code) => format!("Process exited with code {code}"),
@@ -68,19 +67,6 @@ pub enum ExitStatus {
Ongoing(SessionId), Ongoing(SessionId),
} }
pub fn result_into_payload(result: Result<ExecCommandOutput, String>) -> FunctionCallOutputPayload {
match result {
Ok(output) => FunctionCallOutputPayload {
content: output.to_text_output(),
success: Some(true),
},
Err(err) => FunctionCallOutputPayload {
content: err,
success: Some(false),
},
}
}
impl SessionManager { impl SessionManager {
/// Processes the request and is required to send a response via `outgoing`. /// Processes the request and is required to send a response via `outgoing`.
pub async fn handle_exec_command_request( pub async fn handle_exec_command_request(

View File

@@ -0,0 +1,7 @@
use thiserror::Error;
#[derive(Debug, Error, PartialEq)]
pub enum FunctionCallError {
#[error("{0}")]
RespondToModel(String),
}

View File

@@ -75,6 +75,7 @@ pub use rollout::find_conversation_path_by_id_str;
pub use rollout::list::ConversationItem; pub use rollout::list::ConversationItem;
pub use rollout::list::ConversationsPage; pub use rollout::list::ConversationsPage;
pub use rollout::list::Cursor; pub use rollout::list::Cursor;
mod function_tool;
mod user_notification; mod user_notification;
pub mod util; pub mod util;

View File

@@ -2,13 +2,12 @@ use std::collections::BTreeMap;
use std::sync::LazyLock; use std::sync::LazyLock;
use crate::codex::Session; use crate::codex::Session;
use crate::function_tool::FunctionCallError;
use crate::openai_tools::JsonSchema; use crate::openai_tools::JsonSchema;
use crate::openai_tools::OpenAiTool; use crate::openai_tools::OpenAiTool;
use crate::openai_tools::ResponsesApiTool; use crate::openai_tools::ResponsesApiTool;
use crate::protocol::Event; use crate::protocol::Event;
use crate::protocol::EventMsg; use crate::protocol::EventMsg;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
// Use the canonical plan tool types from the protocol crate to ensure // Use the canonical plan tool types from the protocol crate to ensure
// type-identity matches events transported via `codex_protocol`. // type-identity matches events transported via `codex_protocol`.
@@ -67,44 +66,20 @@ pub(crate) async fn handle_update_plan(
session: &Session, session: &Session,
arguments: String, arguments: String,
sub_id: String, sub_id: String,
call_id: String, _call_id: String,
) -> ResponseInputItem { ) -> Result<String, FunctionCallError> {
match parse_update_plan_arguments(arguments, &call_id) { let args = parse_update_plan_arguments(&arguments)?;
Ok(args) => { session
let output = ResponseInputItem::FunctionCallOutput { .send_event(Event {
call_id, id: sub_id.to_string(),
output: FunctionCallOutputPayload { msg: EventMsg::PlanUpdate(args),
content: "Plan updated".to_string(), })
success: Some(true), .await;
}, Ok("Plan updated".to_string())
};
session
.send_event(Event {
id: sub_id.to_string(),
msg: EventMsg::PlanUpdate(args),
})
.await;
output
}
Err(output) => *output,
}
} }
fn parse_update_plan_arguments( fn parse_update_plan_arguments(arguments: &str) -> Result<UpdatePlanArgs, FunctionCallError> {
arguments: String, serde_json::from_str::<UpdatePlanArgs>(arguments).map_err(|e| {
call_id: &str, FunctionCallError::RespondToModel(format!("failed to parse function arguments: {e}"))
) -> Result<UpdatePlanArgs, Box<ResponseInputItem>> { })
match serde_json::from_str::<UpdatePlanArgs>(&arguments) {
Ok(args) => Ok(args),
Err(e) => {
let output = ResponseInputItem::FunctionCallOutput {
call_id: call_id.to_string(),
output: FunctionCallOutputPayload {
content: format!("failed to parse function arguments: {e}"),
success: None,
},
};
Err(Box::new(output))
}
}
} }