diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index f7c4e95800..64a88dc2e8 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -156,7 +156,6 @@ use codex_async_utils::OrCancelExt; use codex_otel::OtelManager; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::models::ContentItem; -use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; @@ -1552,39 +1551,6 @@ impl Session { } } - pub(crate) async fn current_turn_images_tool_only(&self) -> bool { - let history = self.state.lock().await.clone_history(); - let mut saw_user = false; - let mut saw_tool = false; - for item in history.raw_items().iter().rev() { - match item { - ResponseItem::Message { role, content, .. } => { - if role == "assistant" { - break; - } - if role == "user" - && content - .iter() - .any(|item| matches!(item, ContentItem::InputImage { .. })) - { - saw_user = true; - } - } - ResponseItem::FunctionCallOutput { output, .. } => { - if let Some(items) = &output.content_items - && items.iter().any(|item| { - matches!(item, FunctionCallOutputContentItem::InputImage { .. }) - }) - { - saw_tool = true; - } - } - _ => {} - } - } - saw_tool && !saw_user - } - pub async fn list_resources( &self, server: &str, @@ -2483,19 +2449,25 @@ pub(crate) async fn run_turn( break; } Err(CodexErr::InvalidImageRequest()) => { - if sess.current_turn_images_tool_only().await { + let tool_only = { let mut state = sess.state.lock().await; - error_or_panic( - "Invalid image detected, replacing it in the last turn to prevent poisoning", - ); + let tool_only = state.history.current_turn_images_tool_only(); + error_or_panic(if tool_only { + "Invalid image detected, replacing it in the last turn to prevent poisoning" + } else { + "Invalid user image detected; replacing it in the last turn to prevent poisoning" + }); state.history.replace_last_turn_images("Invalid image"); - } else { - let err = CodexErr::InvalidImageRequest(); - info!("Turn error: {err:#}"); - let event = EventMsg::Error(err.to_error_event(None)); - sess.send_event(&turn_context, event).await; - break; + tool_only + }; + if tool_only { + continue; } + let err = CodexErr::InvalidImageRequest(); + info!("Turn error: {err:#}"); + let event = EventMsg::Error(err.to_error_event(None)); + sess.send_event(&turn_context, event).await; + break; } Err(e) => { info!("Turn error: {e:#}"); diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 3e0428c86d..42697597b4 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -155,6 +155,34 @@ impl ContextManager { } } + pub(crate) fn current_turn_images_tool_only(&self) -> bool { + let mut saw_tool = false; + for item in self.items.iter().rev() { + match item { + ResponseItem::Message { role, .. } if role == "assistant" => break, + ResponseItem::Message { role, content, .. } if role == "user" => { + if content + .iter() + .any(|item| matches!(item, ContentItem::InputImage { .. })) + { + return false; + } + } + ResponseItem::FunctionCallOutput { output, .. } => { + if output.content_items.as_ref().map_or(false, |items| { + items.iter().any(|item| { + matches!(item, FunctionCallOutputContentItem::InputImage { .. }) + }) + }) { + saw_tool = true; + } + } + _ => {} + } + } + saw_tool + } + /// Drop the last `num_turns` user turns from this history. /// /// "User turns" are identified as `ResponseItem::Message` entries whose role is `"user"`.