diff --git a/codex-rs/context-fragments/src/additional_context.rs b/codex-rs/context-fragments/src/additional_context.rs index 71b286b560..9c27336ab1 100644 --- a/codex-rs/context-fragments/src/additional_context.rs +++ b/codex-rs/context-fragments/src/additional_context.rs @@ -25,7 +25,7 @@ impl ContextualUserFragment for AdditionalContextUserFragment { } fn content_kind(&self) -> ContentItemKind { - ContentItemKind("generic.user_additional_context".to_string()) + ContentItemKind(format!("additional_content.{}", self.key)) } fn markers(&self) -> (&'static str, &'static str) { @@ -75,7 +75,7 @@ impl ContextualUserFragment for AdditionalContextDeveloperFragment { } fn content_kind(&self) -> ContentItemKind { - ContentItemKind("generic.developer_additional_context".to_string()) + ContentItemKind(format!("additional_content.{}", self.key)) } fn markers(&self) -> (&'static str, &'static str) { diff --git a/codex-rs/context-fragments/src/fragment.rs b/codex-rs/context-fragments/src/fragment.rs index d6840dc1fe..a81e6e3954 100644 --- a/codex-rs/context-fragments/src/fragment.rs +++ b/codex-rs/context-fragments/src/fragment.rs @@ -1,7 +1,6 @@ use crate::AnnotatedContent; -use codex_protocol::models::ContentItem; use codex_protocol::models::ContentItemKind; -use codex_protocol::models::ResponseInputItem; +use codex_protocol::models::InternalChatMessageMetadataPassthrough; use codex_protocol::models::ResponseItem; /// A rendered contextual fragment and the role that owns its annotated content. @@ -33,6 +32,26 @@ impl RenderedFragment { } } +impl From for ResponseItem { + fn from(fragment: RenderedFragment) -> Self { + let (role, annotated_content) = fragment.into_parts(); + let (content, content_kind) = annotated_content.into_parts(); + + Self::Message { + id: None, + role: role.to_string(), + content: vec![content], + phase: None, + internal_chat_message_metadata_passthrough: Some( + InternalChatMessageMetadataPassthrough { + content_item_kinds: Some(vec![content_kind]), + ..Default::default() + }, + ), + } + } +} + /// Context payload that is injected as a message fragment. /// /// Implementations own the response role and provide the exact fragment body. @@ -91,40 +110,11 @@ pub trait ContextualUserFragment { where Self: Sized, { - ResponseItem::Message { - id: None, - role: self.role().to_string(), - content: vec![ContentItem::InputText { - text: self.render(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - } + ResponseItem::from(self.render_fragment()) } fn into_boxed_response_item(self: Box) -> ResponseItem { - ResponseItem::Message { - id: None, - role: self.role().to_string(), - content: vec![ContentItem::InputText { - text: self.render(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - } - } - - fn into_response_input_item(self) -> ResponseInputItem - where - Self: Sized, - { - ResponseInputItem::Message { - role: self.role().to_string(), - content: vec![ContentItem::InputText { - text: self.render(), - }], - phase: None, - } + ResponseItem::from(self.render_fragment()) } } diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 4ab6bc0509..b706a412c6 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -1513,15 +1513,9 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() { expected_developer_message, expected_final_answer, expected_standalone_output, - ResponseItem::Message { - id: None, - role: "developer".to_string(), - content: vec![ContentItem::InputText { - text: "Child subagent guidance.".to_string(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }, + ContextualUserFragment::into(MultiAgentRoleInstructions::unmarked( + "Child subagent guidance.", + )), ]; assert_eq!( strip_response_item_ids(&history_items), diff --git a/codex-rs/core/src/image_preparation_tests.rs b/codex-rs/core/src/image_preparation_tests.rs index ee0b94c068..192d11a69d 100644 --- a/codex-rs/core/src/image_preparation_tests.rs +++ b/codex-rs/core/src/image_preparation_tests.rs @@ -2,8 +2,10 @@ use std::io::Cursor; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use codex_protocol::models::ContentItemKind; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputPayload; +use codex_protocol::models::InternalChatMessageMetadataPassthrough; use codex_utils_image::data_url_from_bytes; use image::DynamicImage; use image::GenericImageView; @@ -267,7 +269,14 @@ fn resize_notices_preserve_original_image_positions_and_skip_failed_images() { text: expected_user_notice.to_string(), }], phase: None, - internal_chat_message_metadata_passthrough: None, + internal_chat_message_metadata_passthrough: Some( + InternalChatMessageMetadataPassthrough { + content_item_kinds: Some(vec![ContentItemKind( + "images.resize_notice".to_string() + )]), + ..Default::default() + }, + ), } ); @@ -305,7 +314,14 @@ fn resize_notices_preserve_original_image_positions_and_skip_failed_images() { .to_string(), }], phase: None, - internal_chat_message_metadata_passthrough: None, + internal_chat_message_metadata_passthrough: Some( + InternalChatMessageMetadataPassthrough { + content_item_kinds: Some(vec![ContentItemKind( + "images.resize_notice".to_string() + )]), + ..Default::default() + }, + ), } ); } diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 8c120ec3fd..583d7451ed 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -106,7 +106,9 @@ use codex_protocol::models::ActivePermissionProfile; use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::BaseInstructions; use codex_protocol::models::BaseInstructionsProvenance; +use codex_protocol::models::ContentItem; use codex_protocol::models::ContentItemKind; +use codex_protocol::models::InternalChatMessageMetadataPassthrough; use codex_protocol::models::PermissionProfile; use codex_protocol::models::SandboxEnforcement; use codex_protocol::openai_models::ModelInfo; @@ -3034,10 +3036,38 @@ impl Session { } pub(crate) fn response_item_from_user_input(&self, input: Vec) -> ResponseItem { - ResponseItem::from(ResponseInputItem::from_user_input( + let mut item = ResponseItem::from(ResponseInputItem::from_user_input( input, LocalImagePreparation::Defer, - )) + )); + if let ResponseItem::Message { + content, + internal_chat_message_metadata_passthrough, + .. + } = &mut item + { + let content_item_kinds = content + .iter() + .map(|content| { + ContentItemKind( + match content { + ContentItem::InputText { .. } | ContentItem::OutputText { .. } => { + "user.text" + } + ContentItem::InputImage { .. } => "user.image", + ContentItem::InputAudio { .. } => "user.audio", + } + .to_string(), + ) + }) + .collect(); + *internal_chat_message_metadata_passthrough = + Some(InternalChatMessageMetadataPassthrough { + content_item_kinds: Some(content_item_kinds), + ..Default::default() + }); + } + item } #[tracing::instrument(level = "trace", skip_all, fields(item_count = items.len()))] diff --git a/codex-rs/core/src/session/turn_input.rs b/codex-rs/core/src/session/turn_input.rs index 424faf3c3a..f4985c33f1 100644 --- a/codex-rs/core/src/session/turn_input.rs +++ b/codex-rs/core/src/session/turn_input.rs @@ -20,7 +20,6 @@ use crate::tasks::RegularTask; use codex_protocol::config_types::ModeKind; use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; -use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AdditionalContextEntry; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::ErrorEvent; @@ -579,7 +578,6 @@ async fn merge_additional_context_input( }; additional_context_input .into_iter() - .map(ResponseItem::from) .map(|item| session.annotate_client_response_item(item)) .map(TurnInput::ResponseItem) .collect() diff --git a/codex-rs/core/src/state/additional_context.rs b/codex-rs/core/src/state/additional_context.rs index eb5727ac23..83144f8b9b 100644 --- a/codex-rs/core/src/state/additional_context.rs +++ b/codex-rs/core/src/state/additional_context.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use crate::context::AdditionalContextDeveloperFragment; use crate::context::AdditionalContextUserFragment; use crate::context::ContextualUserFragment; -use codex_protocol::models::ResponseInputItem; +use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AdditionalContextEntry; use codex_protocol::protocol::AdditionalContextKind; @@ -16,19 +16,17 @@ impl AdditionalContextStore { pub(crate) fn merge( &mut self, values: BTreeMap, - ) -> Vec { + ) -> Vec { let fragments = values .iter() .filter(|(key, value)| self.values.get(*key) != Some(*value)) .map(|(key, entry)| match entry.kind { - AdditionalContextKind::Untrusted => { - AdditionalContextUserFragment::new(key.clone(), entry.value.clone()) - .into_response_input_item() - } - AdditionalContextKind::Application => { - AdditionalContextDeveloperFragment::new(key.clone(), entry.value.clone()) - .into_response_input_item() - } + AdditionalContextKind::Untrusted => ContextualUserFragment::into( + AdditionalContextUserFragment::new(key.clone(), entry.value.clone()), + ), + AdditionalContextKind::Application => ContextualUserFragment::into( + AdditionalContextDeveloperFragment::new(key.clone(), entry.value.clone()), + ), }) .collect(); self.values = values; diff --git a/codex-rs/core/src/tasks/mod.rs b/codex-rs/core/src/tasks/mod.rs index 08448dc7d6..f677781215 100644 --- a/codex-rs/core/src/tasks/mod.rs +++ b/codex-rs/core/src/tasks/mod.rs @@ -35,6 +35,7 @@ use crate::state::RunningTask; use crate::state::TaskKind; use codex_analytics::TurnProfileFact; use codex_analytics::TurnTokenUsageFact; +use codex_context_fragments::RenderedFragment; use codex_otel::SessionTelemetry; use codex_otel::TURN_E2E_DURATION_METRIC; use codex_otel::TURN_MEMORY_METRIC; @@ -55,7 +56,6 @@ use codex_thread_store::PersistContext; use codex_features::Feature; use codex_protocol::error::CodexErrorDetails; use codex_protocol::error::Result as CodexResult; -use codex_protocol::models::ContentItem; pub(crate) use compact::CompactTask; pub(crate) use regular::RegularTask; pub(crate) use review::ReviewTask; @@ -111,15 +111,8 @@ pub(crate) fn interrupted_turn_history_marker( let marker = crate::context::TurnAborted::new( crate::context::TurnAborted::INTERRUPTED_DEVELOPER_GUIDANCE, ); - Some(ResponseItem::Message { - id: None, - role: "developer".to_string(), - content: vec![ContentItem::InputText { - text: marker.render(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }) + let (_, content) = marker.render_fragment().into_parts(); + Some(RenderedFragment::new("developer", content).into()) } } } diff --git a/codex-rs/core/src/thread_manager_tests.rs b/codex-rs/core/src/thread_manager_tests.rs index df50a1f3bf..017745548a 100644 --- a/codex-rs/core/src/thread_manager_tests.rs +++ b/codex-rs/core/src/thread_manager_tests.rs @@ -22,6 +22,8 @@ use codex_protocol::mcp::ClientMcpExtensions; use codex_protocol::mcp::MCP_APP_UI_EXTENSION_ID; use codex_protocol::mcp::OPENAI_FORM_EXTENSION_ID; use codex_protocol::models::ContentItem; +use codex_protocol::models::ContentItemKind; +use codex_protocol::models::InternalChatMessageMetadataPassthrough; use codex_protocol::models::ReasoningItemReasoningSummary; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelsResponse; @@ -2153,19 +2155,27 @@ fn interrupted_snapshot_is_not_mid_turn() { #[test] fn multi_agent_v2_interrupted_marker_uses_developer_input_message() { - let marker = developer_interrupted_marker(); - - let ResponseItem::Message { role, content, .. } = marker else { - panic!("expected interrupted marker to be a message"); - }; - assert_eq!(role, "developer"); - assert!( - matches!( - content.as_slice(), - [ContentItem::InputText { text }] - if text.contains(crate::context::TurnAborted::INTERRUPTED_DEVELOPER_GUIDANCE) - ), - "expected interrupted marker to use developer InputText content" + assert_eq!( + developer_interrupted_marker(), + ResponseItem::Message { + id: None, + role: "developer".to_string(), + content: vec![ContentItem::InputText { + text: format!( + "\n{}\n", + crate::context::TurnAborted::INTERRUPTED_DEVELOPER_GUIDANCE + ), + }], + phase: None, + internal_chat_message_metadata_passthrough: Some( + InternalChatMessageMetadataPassthrough { + content_item_kinds: Some(vec![ContentItemKind( + "generic.turn_aborted".to_string() + )]), + ..Default::default() + } + ), + } ); } diff --git a/codex-rs/core/tests/common/responses.rs b/codex-rs/core/tests/common/responses.rs index f06ba73cb1..defa0bffa9 100644 --- a/codex-rs/core/tests/common/responses.rs +++ b/codex-rs/core/tests/common/responses.rs @@ -296,6 +296,21 @@ impl ResponsesRequest { .clone() } + /// Returns whether an input item's content annotations exactly match the given sequence. + pub fn has_content_kinds(&self, kinds: &[&str]) -> bool { + self.input().into_iter().any(|item| { + item["internal_chat_message_metadata_passthrough"]["content_item_kinds"] + .as_array() + .is_some_and(|actual| { + actual.len() == kinds.len() + && actual + .iter() + .zip(kinds) + .all(|(actual, expected)| actual.as_str() == Some(*expected)) + }) + }) + } + pub fn inputs_of_type(&self, ty: &str) -> Vec { self.input() .iter() diff --git a/codex-rs/core/tests/suite/additional_context.rs b/codex-rs/core/tests/suite/additional_context.rs index 6b3859b334..485a9f50a6 100644 --- a/codex-rs/core/tests/suite/additional_context.rs +++ b/codex-rs/core/tests/suite/additional_context.rs @@ -81,6 +81,9 @@ async fn additional_context_is_model_visible_but_not_a_user_message_item() -> Re .await; let request = request.single_request(); + assert!(request.has_content_kinds(&["additional_content.automation_info"])); + assert!(request.has_content_kinds(&["additional_content.browser_info"])); + assert!(request.has_content_kinds(&["user.text"])); insta::assert_snapshot!( "additional_context_simple_input", context_snapshot::format_labeled_requests_snapshot( diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index e4dbce2193..4e9078082f 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -384,8 +384,9 @@ async fn sends_audio_urls_to_responses() { .unwrap(); wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; - let user_message = response_mock - .single_request() + let request = response_mock.single_request(); + assert!(request.has_content_kinds(&["user.audio"])); + let user_message = request .input() .into_iter() .rev() @@ -428,8 +429,9 @@ async fn sends_local_audio_to_responses() -> anyhow::Result<()> { .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; - let user_message = response_mock - .single_request() + let request = response_mock.single_request(); + assert!(request.has_content_kinds(&["user.text", "user.audio", "user.text"])); + let user_message = request .input() .into_iter() .rev() diff --git a/codex-rs/core/tests/suite/context_annotations.rs b/codex-rs/core/tests/suite/context_annotations.rs index b0106b241b..f7db8c50c2 100644 --- a/codex-rs/core/tests/suite/context_annotations.rs +++ b/codex-rs/core/tests/suite/context_annotations.rs @@ -1,4 +1,14 @@ use anyhow::Result; +use codex_core::TurnInputRequest; +use codex_core::config::CurrentTimeReminderConfig; +use codex_core::config::RolloutBudgetConfig; +use codex_core::config::TokenBudgetConfig; +use codex_features::Feature; +use codex_protocol::openai_models::InputModality; +use codex_protocol::protocol::AdditionalContextEntry; +use codex_protocol::protocol::AdditionalContextKind; +use codex_protocol::protocol::EventMsg; +use codex_protocol::user_input::UserInput; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once; @@ -6,6 +16,8 @@ use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event; +use std::collections::BTreeMap; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn first_request_item_types_roles_and_content_annotations() -> Result<()> { @@ -18,13 +30,79 @@ async fn first_request_item_types_roles_and_content_annotations() -> Result<()> ) .await; let test = test_codex() + .with_model_info_override("gpt-5.5", |model_info| { + model_info.input_modalities.push(InputModality::Audio); + }) .with_config(|config| { config.developer_instructions = Some("Keep world-state annotations aligned.".into()); + config.model_context_window = Some(128_000); + config.current_time_reminder = Some(CurrentTimeReminderConfig::default()); + config.token_budget = Some(TokenBudgetConfig { + guidance_message: Some("Preserve important context.".into()), + ..TokenBudgetConfig::default() + }); + config.rollout_budget = Some(RolloutBudgetConfig { + limit_tokens: 100, + reminder_at_remaining_tokens: Vec::new(), + sampling_token_weight: 1.0, + prefill_token_weight: 1.0, + }); + config.multi_agent_v2.root_agent_usage_hint_text = + Some("Coordinate available subagents.".into()); + config.multi_agent_v2.multi_agent_mode_hint_text = + Some("Delegate independent work.".into()); + for feature in [ + Feature::CurrentTimeReminder, + Feature::DeferredExecutor, + Feature::MultiAgentV2, + Feature::TokenBudget, + ] { + config + .features + .enable(feature) + .expect("test config should allow feature update"); + } }) .build_with_auto_env(&server) .await?; - test.submit_text_turn("inspect world state").await?; + test.codex + .start_or_steer_turn( + TurnInputRequest::user_input(vec![ + UserInput::Text { + text: "inspect world state".to_string(), + text_elements: Vec::new(), + }, + UserInput::Image { + image_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==".to_string(), + detail: None, + }, + UserInput::Audio { + audio_url: "data:audio/wav;base64,AAAA".to_string(), + }, + ]) + .with_additional_context(BTreeMap::from([ + ( + "browser_info".to_string(), + AdditionalContextEntry { + value: "tab one".to_string(), + kind: AdditionalContextKind::Untrusted, + }, + ), + ( + "automation_info".to_string(), + AdditionalContextEntry { + value: "run one".to_string(), + kind: AdditionalContextKind::Application, + }, + ), + ])), + ) + .await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; let items = response .single_request() @@ -40,9 +118,16 @@ async fn first_request_item_types_roles_and_content_annotations() -> Result<()> .collect::>() .join("\n"); insta::assert_snapshot!(items, @r#" - message developer ["generic.developer_instructions","generic.permissions_instructions"] + message developer ["generic.developer_instructions","token_budget.context_window_guidance","generic.permissions_instructions","environments.instructions"] + message developer ["token_budget.context_window"] + message developer ["multi_agent.usage_hint"] + message developer ["multi_agent.mode_instructions"] message user ["environments.environment_context"] - message user null + message developer ["additional_content.automation_info"] + message user ["additional_content.browser_info"] + message user ["user.text","user.image","user.audio"] + message developer ["rollout_budget.remaining_tokens"] + message developer ["current_time.reminder"] "#); Ok(()) diff --git a/codex-rs/core/tests/suite/current_time_reminder.rs b/codex-rs/core/tests/suite/current_time_reminder.rs index 937cae27c3..040c2515bb 100644 --- a/codex-rs/core/tests/suite/current_time_reminder.rs +++ b/codex-rs/core/tests/suite/current_time_reminder.rs @@ -153,6 +153,7 @@ async fn environment_context_uses_external_current_time_on_each_turn() -> Result .iter() .zip([FIRST_TIME_UNIX_SECONDS, FIRST_TIME_UNIX_SECONDS + 86_400]) { + assert!(request.has_content_kinds(&["environments.environment_context"])); let current_date = DateTime::::from_timestamp(timestamp, 0) .expect("test timestamp should be valid") .with_timezone(&Local) @@ -341,7 +342,9 @@ async fn system_time_source_adds_current_time_reminder() -> Result<()> { test.submit_turn("what time is it?").await?; - let reminders = current_time_reminders(&responses.single_request()); + let request = responses.single_request(); + assert!(request.has_content_kinds(&["current_time.reminder"])); + let reminders = current_time_reminders(&request); assert_eq!(reminders.len(), 1); assert_regex_match( r"^It is \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC\.$", diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index f2c767ba89..76b7bc0b9e 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -384,6 +384,8 @@ async fn resumed_history_only_emits_resize_notices_for_new_images() -> anyhow::R .await; let request = resumed_mock.single_request(); + assert!(request.has_content_kinds(&["images.resize_notice"])); + assert!(request.has_content_kinds(&["user.image"])); let input = request.input(); let image_message_indices = input .iter() diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 743fe4d181..0eaa0748ae 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -411,6 +411,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( assert_eq!(requests.len(), 2, "expected two model requests"); let second_request = requests.last().expect("expected second request"); + assert!(second_request.has_content_kinds(&["model_switch.instructions"])); let developer_texts = second_request.message_input_texts("developer"); let model_switch_text = developer_texts .iter() diff --git a/codex-rs/core/tests/suite/rollout_budget.rs b/codex-rs/core/tests/suite/rollout_budget.rs index 991cac1d9c..d9fc921348 100644 --- a/codex-rs/core/tests/suite/rollout_budget.rs +++ b/codex-rs/core/tests/suite/rollout_budget.rs @@ -103,6 +103,7 @@ async fn adds_weighted_initial_and_threshold_reminders( test.submit_turn("second turn").await?; let requests = responses.requests(); + assert!(requests[0].has_content_kinds(&["rollout_budget.remaining_tokens"])); assert_eq!( rollout_budget_texts(&requests[0]), vec![rollout_budget_message(/*remaining_tokens*/ 100)] diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index cd84ad1e81..b53ca6a7bb 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -148,6 +148,7 @@ async fn user_turn_includes_skill_instructions() -> Result<()> { }), "expected skill instructions in user input, got {user_texts:?}" ); + assert!(request.has_content_kinds(&["skills.selected_skill_instructions"])); Ok(()) } diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 4a1bd75af8..ee6697e5e0 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -1327,6 +1327,7 @@ async fn spawned_full_history_v2_child_uses_model_precedence_without_dropping_co selection, FullHistoryV2ModelSelection::MultiAgentModeTransitions ) { + assert!(child_request.has_content_kinds(&["multi_agent.role_instructions"])); assert_eq!( child_developer_messages .iter() diff --git a/codex-rs/core/tests/suite/token_budget.rs b/codex-rs/core/tests/suite/token_budget.rs index 9e2981edc7..d3b7b24e40 100644 --- a/codex-rs/core/tests/suite/token_budget.rs +++ b/codex-rs/core/tests/suite/token_budget.rs @@ -264,7 +264,13 @@ async fn token_budget_guidance_precedes_standalone_context_window() -> Result<() test.submit_turn("inspect context guidance").await?; - let developer_texts = response.single_request().message_input_texts("developer"); + let request = response.single_request(); + assert!(request.has_content_kinds(&[ + "token_budget.context_window_guidance", + "generic.permissions_instructions", + ])); + assert!(request.has_content_kinds(&["token_budget.context_window"])); + let developer_texts = request.message_input_texts("developer"); let context_window_index = developer_texts .iter() .position(|text| text.starts_with(CONTEXT_WINDOW_OPEN_TAG)) diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index daf24916e7..f1237cb21e 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -280,7 +280,9 @@ async fn assert_user_turn_local_image_resizes_to( ) .await; - let body = mock.single_request().body_json(); + let request = mock.single_request(); + assert!(request.has_content_kinds(&["user.text", "user.image", "user.text"])); + let body = request.body_json(); let input = body .get("input") .and_then(Value::as_array) @@ -303,6 +305,7 @@ async fn assert_user_turn_local_image_resizes_to( assert_eq!(resize_notice_indices, Vec::::new()); } ResizeNoticeExpectation::Enabled => { + assert!(request.has_content_kinds(&["images.resize_notice"])); assert_eq!(resize_notice_indices, vec![image_message_index + 1]); assert_developer_text_message( &input[image_message_index + 1],