use codex_api::SearchInput;use codex_core::parse_turn_item;use codex_protocol::items::TurnItem;use codex_protocol::models::ContentItem;use codex_protocol::models::ResponseItem;use codex_protocol::models::plaintext_agent_message_content;use codex_tools::retain_tail_from_last_n_user_messages;use codex_tools::truncate_assistant_output_text_to_token_budget;const ASSISTANT_CONTEXT_TOKEN_LIMIT: usize = 1_000;const ASSISTANT_ROLE: &str = "assistant";const USER_ROLE: &str = "user";/// Builds the conversation tail for standalone web search.////// The tail keeps the previous user text message, up to 1k tokens of assistant/// text that followed it, and the current user text message.pub(crate) fn recent_input(items: &[ResponseItem]) -> Option<SearchInput> { let mut messages = Vec::new(); for item in items { push_visible_message(&mut messages, item); } retain_tail_from_last_n_user_messages(&mut messages, /*user_message_count*/ 2); truncate_assistant_output_text_to_token_budget(&mut messages, ASSISTANT_CONTEXT_TOKEN_LIMIT); (!messages.is_empty()).then_some(SearchInput::Items(messages))}fn push_visible_message(messages: &mut Vec<ResponseItem>, item: &ResponseItem) { match item { ResponseItem::Message { role, .. } if role == ASSISTANT_ROLE => { messages.push(item.clone()); } ResponseItem::AgentMessage { author, content, metadata, .. } => { if let Some(text) = plaintext_agent_message_content(content) { messages.push(ResponseItem::Message { id: None, role: ASSISTANT_ROLE.to_string(), content: vec![ContentItem::OutputText { text: format!("Agent message from {author}:\n{text}"), }], phase: None, metadata: metadata.clone(), }); } } ResponseItem::Message { id, role, content, phase, metadata, } if role == USER_ROLE && matches!(parse_turn_item(item), Some(TurnItem::UserMessage(_))) => { let content = content .iter() .filter(|item| matches!(item, ContentItem::InputText { .. })) .cloned() .collect::<Vec<_>>(); if !content.is_empty() { messages.push(ResponseItem::Message { id: id.clone(), role: role.clone(), content, phase: phase.clone(), metadata: metadata.clone(), }); } } _ => {} }}#[cfg(test)]mod tests { use codex_api::SearchInput; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use pretty_assertions::assert_eq; use super::ASSISTANT_ROLE; use super::USER_ROLE; use super::recent_input; fn message(role: &str, text: &str) -> ResponseItem { ResponseItem::Message { id: None, role: role.to_string(), content: vec![if role == ASSISTANT_ROLE { ContentItem::OutputText { text: text.to_string(), } } else { ContentItem::InputText { text: text.to_string(), } }], phase: None, metadata: None, } } #[test] fn keeps_current_user_and_previous_visible_turn() { let items = vec![ message("system", "system"), message(USER_ROLE, "old user"), message(ASSISTANT_ROLE, "old assistant"), message(USER_ROLE, "previous user"), ResponseItem::FunctionCall { id: None, name: "tool".to_string(), namespace: None, arguments: "{}".to_string(), call_id: "call-1".to_string(), metadata: None, }, message(ASSISTANT_ROLE, "previous assistant"), message("developer", "developer"), message(USER_ROLE, "current user"), message(ASSISTANT_ROLE, "current commentary"), ]; assert_eq!( recent_input(&items), Some(SearchInput::Items(vec![ message(USER_ROLE, "previous user"), message(ASSISTANT_ROLE, "previous assistant"), message(USER_ROLE, "current user"), ])) ); } #[test] fn keeps_only_text_from_recent_user_messages() { let previous_user = ResponseItem::Message { id: None, role: USER_ROLE.to_string(), content: vec![ ContentItem::InputText { text: "previous user".to_string(), }, ContentItem::InputImage { image_url: "data:image/png;base64,image".to_string(), detail: None, }, ], phase: None, metadata: None, }; let items = vec![ previous_user, message(ASSISTANT_ROLE, "previous assistant"), message(USER_ROLE, "current user"), ]; assert_eq!( recent_input(&items), Some(SearchInput::Items(vec![ message(USER_ROLE, "previous user"), message(ASSISTANT_ROLE, "previous assistant"), message(USER_ROLE, "current user"), ])) ); } #[test] fn ignores_contextual_user_messages_when_selecting_recent_turns() { let items = vec![ message(USER_ROLE, "previous user"), message(ASSISTANT_ROLE, "previous assistant"), message( USER_ROLE, "<environment_context>\n<cwd>/tmp</cwd>\n</environment_context>", ), message(USER_ROLE, "current user"), ]; assert_eq!( recent_input(&items), Some(SearchInput::Items(vec![ message(USER_ROLE, "previous user"), message(ASSISTANT_ROLE, "previous assistant"), message(USER_ROLE, "current user"), ])) ); }}