From 91fe89b07869fbab71034d88b2eda8be8222fdf1 Mon Sep 17 00:00:00 2001 From: serrrfirat Date: Wed, 25 Mar 2026 09:26:04 +0300 Subject: [PATCH] fix: wrap preflight tool rejection errors for llm safety Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/agent/dispatcher.rs | 48 ++++++++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/agent/dispatcher.rs b/src/agent/dispatcher.rs index 34b0619f..94969aa0 100644 --- a/src/agent/dispatcher.rs +++ b/src/agent/dispatcher.rs @@ -492,10 +492,6 @@ impl<'a> LoopDelegate for ChatDelegate<'a> { // Walk tool_calls checking approval and hooks. Classify // each tool as Rejected (by hook) or Runnable. Stop at the // first tool that needs approval. - enum PreflightOutcome { - Rejected(String), - Runnable, - } let mut preflight: Vec<(crate::llm::ToolCall, PreflightOutcome)> = Vec::new(); let mut runnable: Vec<(usize, crate::llm::ToolCall)> = Vec::new(); let mut approval_needed: Option<( @@ -748,17 +744,21 @@ impl<'a> LoopDelegate for ChatDelegate<'a> { for (pf_idx, (tc, outcome)) in preflight.into_iter().enumerate() { match outcome { PreflightOutcome::Rejected(error_msg) => { + let (result_content, tool_message) = preflight_rejection_tool_message( + self.agent.safety(), + &tc.name, + &tc.id, + &error_msg, + ); { let mut sess = self.session.lock().await; if let Some(thread) = sess.threads.get_mut(&self.thread_id) && let Some(turn) = thread.last_turn_mut() { - turn.record_tool_error(error_msg.clone()); + turn.record_tool_error(result_content.clone()); } } - reason_ctx - .messages - .push(ChatMessage::tool_result(&tc.id, &tc.name, error_msg)); + reason_ctx.messages.push(tool_message); } PreflightOutcome::Runnable => { let tool_result = exec_results[pf_idx].take().unwrap_or_else(|| { @@ -994,6 +994,21 @@ pub(super) fn check_auth_required( Some((name, instructions)) } +enum PreflightOutcome { + Rejected(String), + Runnable, +} + +fn preflight_rejection_tool_message( + safety: &crate::safety::SafetyLayer, + tool_name: &str, + tool_call_id: &str, + error_msg: &str, +) -> (String, ChatMessage) { + let result: Result = Err(error_msg); + crate::tools::execute::process_tool_result(safety, tool_name, tool_call_id, &result) +} + /// Build a contextual thinking message based on tool names. /// /// Instead of a generic "Executing 2 tool(s)..." this returns messages like @@ -2529,4 +2544,21 @@ mod tests { assert!(result_msg.contains("approval")); assert!(result_msg.contains("DM")); } + + #[test] + fn test_preflight_rejection_tool_message_is_wrapped() { + let safety = crate::safety::SafetyLayer::new(&crate::config::SafetyConfig { + max_output_length: 1000, + injection_check_enabled: true, + }); + let rejection = "requires approval override"; + + let (content, message) = + super::preflight_rejection_tool_message(&safety, "shell", "call_1", rejection); + + assert!(content.contains("tool_output")); + assert!(content.contains("Tool 'shell' failed:")); + assert!(!content.contains("\n")); + assert_eq!(message.content, content); + } }