diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9b4f216ccd..ddb1fb6af1 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -330,6 +330,7 @@ use codex_protocol::config_types::ServiceTier; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::ContentItem; use codex_protocol::models::DeveloperInstructions; +use codex_protocol::models::ReviewDecisionMetadata; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::models::ResponseItemMetadata; @@ -1050,6 +1051,7 @@ fn session_source_to_metadata(session_source: &SessionSource) -> SessionSourceMe SessionSource::VSCode => SessionSourceMetadata::Vscode, SessionSource::Exec => SessionSourceMetadata::Exec, SessionSource::Mcp => SessionSourceMetadata::Mcp, + SessionSource::Custom(_) => SessionSourceMetadata::Unknown, SessionSource::SubAgent(SubAgentSource::Review) => SessionSourceMetadata::SubagentReview, SessionSource::SubAgent(SubAgentSource::Compact) => SessionSourceMetadata::SubagentCompact, SessionSource::SubAgent(SubAgentSource::MemoryConsolidation) => { diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 478d206d06..7f1b45b9cb 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -5032,34 +5032,44 @@ async fn steer_input_returns_active_turn_id() { } #[tokio::test] -async fn record_into_history_generates_message_metadata_uuid_when_item_metadata_enabled() { +async fn record_into_history_generates_metadata_uuid_for_metadata_bearing_items_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, - }; + let items = [ + 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, + }, + ResponseItem::FunctionCallOutput { + call_id: "call_123".to_string(), + output: FunctionCallOutputPayload::from_text("ok".to_string()), + metadata: None, + }, + ]; - sess.record_into_history(std::slice::from_ref(&item), &tc) - .await; + sess.record_into_history(&items, &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"); - }; + for item in history.raw_items() { + let metadata = match item { + ResponseItem::Message { metadata, .. } + | ResponseItem::FunctionCallOutput { metadata, .. } => metadata, + other => panic!("unexpected item in history: {other:?}"), + }; - 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"); + 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] diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index a02686be29..480883216f 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -363,7 +363,7 @@ impl ResponseItemMetadata { } impl ResponseItem { - /// Ensures message metadata includes a generated UUID. + /// Ensures metadata includes a generated UUID for any item variant that carries metadata. pub fn with_generated_metadata_uuid(self) -> Self { match self { ResponseItem::Message { @@ -384,6 +384,148 @@ impl ResponseItem { phase, } } + ResponseItem::Reasoning { + id, + summary, + content, + encrypted_content, + metadata, + } => ResponseItem::Reasoning { + id, + summary, + content, + encrypted_content, + metadata: ensure_generated_metadata_uuid(metadata), + }, + ResponseItem::LocalShellCall { + id, + call_id, + status, + action, + metadata, + } => ResponseItem::LocalShellCall { + id, + call_id, + status, + action, + metadata: ensure_generated_metadata_uuid(metadata), + }, + ResponseItem::FunctionCall { + id, + name, + namespace, + arguments, + call_id, + metadata, + } => ResponseItem::FunctionCall { + id, + name, + namespace, + arguments, + call_id, + metadata: ensure_generated_metadata_uuid(metadata), + }, + ResponseItem::ToolSearchCall { + id, + call_id, + status, + execution, + arguments, + metadata, + } => ResponseItem::ToolSearchCall { + id, + call_id, + status, + execution, + arguments, + metadata: ensure_generated_metadata_uuid(metadata), + }, + ResponseItem::FunctionCallOutput { + call_id, + output, + metadata, + } => ResponseItem::FunctionCallOutput { + call_id, + output, + metadata: ensure_generated_metadata_uuid(metadata), + }, + ResponseItem::CustomToolCall { + id, + status, + call_id, + name, + input, + metadata, + } => ResponseItem::CustomToolCall { + id, + status, + call_id, + name, + input, + metadata: ensure_generated_metadata_uuid(metadata), + }, + ResponseItem::CustomToolCallOutput { + call_id, + name, + output, + metadata, + } => ResponseItem::CustomToolCallOutput { + call_id, + name, + output, + metadata: ensure_generated_metadata_uuid(metadata), + }, + ResponseItem::ToolSearchOutput { + call_id, + status, + execution, + tools, + metadata, + } => ResponseItem::ToolSearchOutput { + call_id, + status, + execution, + tools, + metadata: ensure_generated_metadata_uuid(metadata), + }, + ResponseItem::WebSearchCall { + id, + status, + action, + metadata, + } => ResponseItem::WebSearchCall { + id, + status, + action, + metadata: ensure_generated_metadata_uuid(metadata), + }, + ResponseItem::ImageGenerationCall { + id, + status, + revised_prompt, + result, + metadata, + } => ResponseItem::ImageGenerationCall { + id, + status, + revised_prompt, + result, + metadata: ensure_generated_metadata_uuid(metadata), + }, + ResponseItem::GhostSnapshot { + ghost_commit, + metadata, + } => ResponseItem::GhostSnapshot { + ghost_commit, + metadata: ensure_generated_metadata_uuid(metadata), + }, + ResponseItem::Compaction { + encrypted_content, + metadata, + } => ResponseItem::Compaction { + encrypted_content, + metadata: ensure_generated_metadata_uuid(metadata), + }, other => other, } } @@ -3155,7 +3297,7 @@ mod tests { } #[test] - fn generates_metadata_uuid_for_message_items() -> Result<()> { + fn generates_metadata_uuid_for_metadata_bearing_items() -> Result<()> { let item = ResponseItem::Message { id: Some("msg_123".to_string()), role: "assistant".to_string(), @@ -3202,6 +3344,39 @@ mod tests { Ok(()) } + #[test] + fn generates_metadata_uuid_for_function_call_output_items() -> Result<()> { + let item = ResponseItem::FunctionCallOutput { + call_id: "call_123".to_string(), + output: FunctionCallOutputPayload::from_text("ok".to_string()), + metadata: None, + }; + + let surfaced = item.with_generated_metadata_uuid(); + let serialized = serde_json::to_value(&surfaced)?; + let metadata = serialized + .get("metadata") + .and_then(serde_json::Value::as_object) + .expect("metadata should be present"); + let uuid = metadata + .get("uuid") + .and_then(serde_json::Value::as_str) + .expect("uuid should be present"); + + uuid::Uuid::parse_str(uuid).expect("uuid should be valid"); + + assert_eq!( + serialized.get("type"), + Some(&serde_json::json!("function_call_output")) + ); + assert_eq!( + serialized.get("call_id"), + Some(&serde_json::json!("call_123")) + ); + + Ok(()) + } + #[test] fn response_item_metadata_helpers_include_session_source() { let mut metadata = ResponseItemMetadata::default();