diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 669c8b02e9..33a1f535c6 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -46,12 +46,7 @@ pub struct Prompt { impl Prompt { pub(crate) fn get_formatted_input(&self) -> Vec { - let mut input: Vec = self - .input - .clone() - .into_iter() - .map(ResponseItem::with_generated_metadata_uuid) - .collect(); + let mut input = self.input.clone(); // when using the *Freeform* apply_patch tool specifically, tool outputs // should be structured text, not json. Do NOT reserialize when using diff --git a/codex-rs/core/src/client_common_tests.rs b/codex-rs/core/src/client_common_tests.rs index e46d92d988..4430205475 100644 --- a/codex-rs/core/src/client_common_tests.rs +++ b/codex-rs/core/src/client_common_tests.rs @@ -202,7 +202,7 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() { } #[test] -fn formatted_input_generates_message_metadata_uuid() { +fn formatted_input_does_not_generate_message_metadata_uuid_when_disabled() { let prompt = Prompt { input: vec![ResponseItem::Message { id: Some("msg_123".to_string()), @@ -227,8 +227,42 @@ fn formatted_input_generates_message_metadata_uuid() { ResponseItem::Message { metadata, .. } => { let metadata = metadata.as_ref().expect("metadata should be present"); assert_eq!(metadata.user_message_type, Some(UserMessageType::Prompt)); - let uuid = metadata.uuid.as_deref().expect("uuid should be present"); - uuid::Uuid::parse_str(uuid).expect("uuid should be valid"); + assert_eq!(metadata.uuid, None); + } + other => panic!("expected message item, got {other:?}"), + } +} + +#[test] +fn formatted_input_preserves_existing_message_metadata_uuid() { + let prompt = Prompt { + input: vec![ResponseItem::Message { + id: Some("msg_123".to_string()), + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "hello".to_string(), + }], + metadata: Some(ResponseItemMetadata { + user_message_type: Some(UserMessageType::Prompt), + uuid: Some("2585a800-7d93-4f52-8648-d9cb39f413d2".to_string()), + }), + end_turn: None, + phase: None, + }], + ..Default::default() + }; + + let formatted = prompt.get_formatted_input(); + assert_eq!(formatted.len(), 1); + + match &formatted[0] { + ResponseItem::Message { metadata, .. } => { + let metadata = metadata.as_ref().expect("metadata should be present"); + assert_eq!(metadata.user_message_type, Some(UserMessageType::Prompt)); + assert_eq!( + metadata.uuid.as_deref(), + Some("2585a800-7d93-4f52-8648-d9cb39f413d2") + ); } other => panic!("expected message item, got {other:?}"), } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 35d71631d6..72b93f7f0a 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3259,12 +3259,9 @@ impl Session { turn_context: &TurnContext, items: &[ResponseItem], ) { - let items: Vec = items - .iter() - .cloned() - .map(ResponseItem::with_generated_metadata_uuid) - .collect(); - self.record_into_history(&items, turn_context).await; + let items = self.prepare_history_items(items); + self.record_into_history_prepared(&items, turn_context) + .await; self.persist_rollout_response_items(&items).await; self.send_raw_response_items(turn_context, &items).await; } @@ -3274,6 +3271,28 @@ impl Session { &self, items: &[ResponseItem], turn_context: &TurnContext, + ) { + let items = self.prepare_history_items(items); + self.record_into_history_prepared(&items, turn_context) + .await; + } + + fn prepare_history_items(&self, items: &[ResponseItem]) -> Vec { + if self.enabled(Feature::ItemMetadata) { + items + .iter() + .cloned() + .map(ResponseItem::with_generated_metadata_uuid) + .collect() + } else { + items.to_vec() + } + } + + async fn record_into_history_prepared( + &self, + items: &[ResponseItem], + turn_context: &TurnContext, ) { let mut state = self.state.lock().await; state.record_items(items.iter(), turn_context.truncation_policy); diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index e2168113b1..036fff32df 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -4386,6 +4386,37 @@ async fn steer_input_returns_active_turn_id() { assert!(sess.has_pending_input().await); } +#[tokio::test] +async fn record_into_history_generates_message_metadata_uuid_when_item_metadata_enabled() { + let (mut sess, tc) = make_session_and_context().await; + let _ = sess.features.enable(crate::features::Feature::ItemMetadata); + + let item = ResponseItem::Message { + id: Some("msg_123".to_string()), + role: "user".to_string(), + content: vec![ContentItem::InputText { + text: "hello".to_string(), + }], + metadata: None, + end_turn: None, + phase: None, + }; + + sess.record_into_history(std::slice::from_ref(&item), &tc) + .await; + + let history = sess.state.lock().await.clone_history(); + let [ResponseItem::Message { metadata, .. }] = history.raw_items() else { + panic!("expected a single message item in history"); + }; + + let uuid = metadata + .as_ref() + .and_then(|metadata| metadata.uuid.as_deref()) + .expect("uuid should be generated when item metadata is enabled"); + uuid::Uuid::parse_str(uuid).expect("uuid should be valid"); +} + #[tokio::test] async fn prepend_pending_input_keeps_older_tail_ahead_of_newer_input() { let (sess, tc, _rx) = make_session_and_context_with_rx().await; diff --git a/codex-rs/core/src/features.rs b/codex-rs/core/src/features.rs index bcd064302b..64cb263c71 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -172,6 +172,8 @@ pub enum Feature { ToolCallMcpElicitation, /// Enable personality selection in the TUI. Personality, + /// Emit item-level metadata such as user message type and generated UUIDs. + ItemMetadata, /// Enable native artifact tools. Artifact, /// Enable Fast mode selection in the TUI and request layer. @@ -805,6 +807,12 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::ItemMetadata, + key: "item_metadata", + stage: Stage::UnderDevelopment, + default_enabled: false, + }, FeatureSpec { id: Feature::Artifact, key: "artifact", diff --git a/codex-rs/core/src/features_tests.rs b/codex-rs/core/src/features_tests.rs index b7784730e9..7403e2f96f 100644 --- a/codex-rs/core/src/features_tests.rs +++ b/codex-rs/core/src/features_tests.rs @@ -136,6 +136,12 @@ fn image_detail_original_feature_is_under_development() { assert_eq!(Feature::ImageDetailOriginal.default_enabled(), false); } +#[test] +fn item_metadata_is_under_development_and_disabled_by_default() { + assert_eq!(Feature::ItemMetadata.stage(), Stage::UnderDevelopment); + assert_eq!(Feature::ItemMetadata.default_enabled(), false); +} + #[test] fn collab_is_legacy_alias_for_multi_agent() { assert_eq!(feature_for_key("multi_agent"), Some(Feature::Collab));