diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 9db1df044..d22a023a3 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -2740,9 +2740,8 @@ impl Session { ResponseItem::WebSearchCall { .. } => "ws", ResponseItem::ImageGenerationCall { .. } => "ig", ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } => "cmp", - ResponseItem::AgentMessage { .. } - | ResponseItem::CompactionTrigger { .. } - | ResponseItem::Other => continue, + ResponseItem::AgentMessage { .. } => "amsg", + ResponseItem::CompactionTrigger { .. } | ResponseItem::Other => continue, }; item.set_id(Some(format!("{prefix}_{}", Uuid::now_v7()))); } @@ -2831,6 +2830,7 @@ impl Session { std::slice::from_ref(&response_item), ); let items = items.as_ref(); + communication.id = items.first().and_then(ResponseItem::id).map(str::to_string); { let mut state = self.state.lock().await; state.record_items( diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 685005b40..2da1b9a81 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -210,7 +210,7 @@ fn user_message(text: &str) -> ResponseItem { } #[test] -fn assign_missing_response_item_ids_skips_agent_messages() { +fn assign_missing_response_item_ids_assigns_agent_message_ids() { let items = Cow::Owned(vec![ ResponseItem::AgentMessage { id: None, @@ -226,7 +226,7 @@ fn assign_missing_response_item_ids_skips_agent_messages() { let items = Session::assign_missing_response_item_ids(items); - assert_eq!(items[0].id(), None); + assert!(items[0].id().is_some_and(|id| id.starts_with("amsg_"))); assert!(items[1].id().is_some_and(|id| id.starts_with("msg_"))); } @@ -1777,6 +1777,75 @@ async fn record_inter_agent_communication_sets_turn_id_in_rollout_and_resume() { ); } +#[tokio::test] +async fn record_inter_agent_communication_preserves_item_id_in_rollout_and_resume() { + let (mut session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx( + CodexAuth::from_api_key("Test API Key"), + Vec::new(), + |config| { + let _ = config.features.enable(Feature::ItemIds); + }, + ) + .await; + let rollout_path = + attach_thread_persistence(Arc::get_mut(&mut session).expect("unique session")).await; + let communication = InterAgentCommunication::new( + AgentPath::root().join("worker").expect("worker path"), + AgentPath::root(), + Vec::new(), + "child done".to_string(), + /*trigger_turn*/ false, + ); + + session + .record_inter_agent_communication(&turn_context, communication) + .await; + + let live_history = session.clone_history().await; + let [live_item] = live_history.raw_items() else { + panic!("expected exactly one live history item"); + }; + let live_item_id = live_item + .id() + .expect("live agent message should have an item id") + .to_string(); + assert!(live_item_id.starts_with("amsg_")); + + session.flush_rollout().await.expect("rollout should flush"); + let InitialHistory::Resumed(resumed) = RolloutRecorder::get_rollout_history(&rollout_path) + .await + .expect("read rollout history") + else { + panic!("expected resumed rollout history"); + }; + let persisted_communication = resumed.history.iter().find_map(|item| match item { + RolloutItem::InterAgentCommunication(communication) => Some(communication), + _ => None, + }); + assert_eq!( + persisted_communication.and_then(|communication| communication.id.as_deref()), + Some(live_item_id.as_str()) + ); + + let (resumed_session, _resumed_turn_context, _rx) = + make_session_and_context_with_auth_and_config_and_rx( + CodexAuth::from_api_key("Test API Key"), + Vec::new(), + |config| { + let _ = config.features.enable(Feature::ItemIds); + }, + ) + .await; + resumed_session + .record_initial_history(InitialHistory::Resumed(resumed)) + .await; + let resumed_history = resumed_session.clone_history().await; + let [resumed_item] = resumed_history.raw_items() else { + panic!("expected exactly one resumed history item"); + }; + assert_eq!(resumed_item.id(), Some(live_item_id.as_str())); +} + #[tokio::test] async fn prepares_image_failures_before_history_insertion() { let (session, turn_context, _rx) = make_session_and_context_with_auth_and_config_and_rx( diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index a5ff55d47..444e44075 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -692,6 +692,9 @@ impl From> for Op { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] pub struct InterAgentCommunication { + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub id: Option, pub author: AgentPath, pub recipient: AgentPath, #[serde(default)] @@ -715,6 +718,7 @@ impl InterAgentCommunication { trigger_turn: bool, ) -> Self { Self { + id: None, author, recipient, other_recipients, @@ -733,6 +737,7 @@ impl InterAgentCommunication { trigger_turn: bool, ) -> Self { Self { + id: None, author, recipient, other_recipients, @@ -752,6 +757,7 @@ impl InterAgentCommunication { pub fn to_response_input_item(&self) -> ResponseInputItem { let mut communication = self.clone(); + communication.id = None; communication.internal_chat_message_metadata_passthrough = None; ResponseInputItem::Message { role: "assistant".to_string(), @@ -787,7 +793,7 @@ impl InterAgentCommunication { }], }; ResponseItem::AgentMessage { - id: None, + id: self.id.clone(), author: self.author.to_string(), recipient: self.recipient.to_string(), content, @@ -4396,6 +4402,7 @@ mod tests { #[test] fn inter_agent_communication_response_input_item_preserves_commentary_phase() { let mut communication = InterAgentCommunication { + id: Some("amsg_1".to_string()), author: AgentPath::root(), recipient: AgentPath::root().join("reviewer").expect("recipient path"), other_recipients: vec![AgentPath::root().join("worker").expect("recipient path")], @@ -4406,6 +4413,7 @@ mod tests { }; communication.set_turn_id_if_missing("turn-1"); let mut serialized_communication = communication.clone(); + serialized_communication.id = None; serialized_communication.internal_chat_message_metadata_passthrough = None; assert_eq!(