diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 099da35634..eb40c64de7 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1964,10 +1964,7 @@ "properties": { "metadata_id": { "description": "Client-visible metadata ID generated by Codex for this item.", - "type": [ - "string", - "null" - ] + "type": "string" }, "user_message_type": { "anyOf": [ @@ -1980,6 +1977,9 @@ ] } }, + "required": [ + "metadata_id" + ], "type": "object" }, "ResponsesApiWebSearchAction": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 2b1a25a881..9f81dd07af 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -10687,10 +10687,7 @@ "properties": { "metadata_id": { "description": "Client-visible metadata ID generated by Codex for this item.", - "type": [ - "string", - "null" - ] + "type": "string" }, "user_message_type": { "anyOf": [ @@ -10703,6 +10700,9 @@ ] } }, + "required": [ + "metadata_id" + ], "type": "object" }, "ResponsesApiWebSearchAction": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 153d3739c1..91dfa16fc9 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -7455,10 +7455,7 @@ "properties": { "metadata_id": { "description": "Client-visible metadata ID generated by Codex for this item.", - "type": [ - "string", - "null" - ] + "type": "string" }, "user_message_type": { "anyOf": [ @@ -7471,6 +7468,9 @@ ] } }, + "required": [ + "metadata_id" + ], "type": "object" }, "ResponsesApiWebSearchAction": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json index a9c2d2264f..8a2a1c9bf9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -812,10 +812,7 @@ "properties": { "metadata_id": { "description": "Client-visible metadata ID generated by Codex for this item.", - "type": [ - "string", - "null" - ] + "type": "string" }, "user_message_type": { "anyOf": [ @@ -828,6 +825,9 @@ ] } }, + "required": [ + "metadata_id" + ], "type": "object" }, "ResponsesApiWebSearchAction": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index a3201be548..bc57325e29 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -878,10 +878,7 @@ "properties": { "metadata_id": { "description": "Client-visible metadata ID generated by Codex for this item.", - "type": [ - "string", - "null" - ] + "type": "string" }, "user_message_type": { "anyOf": [ @@ -894,6 +891,9 @@ ] } }, + "required": [ + "metadata_id" + ], "type": "object" }, "ResponsesApiWebSearchAction": { diff --git a/codex-rs/app-server-protocol/schema/typescript/ResponseItemMessageMetadata.ts b/codex-rs/app-server-protocol/schema/typescript/ResponseItemMessageMetadata.ts index 8f19da5b4c..a4e0c36c96 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ResponseItemMessageMetadata.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ResponseItemMessageMetadata.ts @@ -7,4 +7,4 @@ export type ResponseItemMessageMetadata = { user_message_type?: UserMessageType, /** * Client-visible metadata ID generated by Codex for this item. */ -metadata_id?: string, }; +metadata_id: string, }; diff --git a/codex-rs/core/src/client_common_tests.rs b/codex-rs/core/src/client_common_tests.rs index 8f7b1abcdc..0ce7caf273 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_does_not_generate_message_metadata_id_when_disabled() { +fn formatted_input_preserves_message_metadata_id_when_disabled() { let prompt = Prompt { input: vec![ResponseItem::Message { id: Some("msg_123".to_string()), @@ -212,7 +212,7 @@ fn formatted_input_does_not_generate_message_metadata_id_when_disabled() { }], metadata: Some(ResponseItemMessageMetadata { user_message_type: Some(UserMessageType::Prompt), - metadata_id: None, + metadata_id: "2585a800-7d93-4f52-8648-d9cb39f413d2".to_string(), }), end_turn: None, phase: None, @@ -227,7 +227,10 @@ fn formatted_input_does_not_generate_message_metadata_id_when_disabled() { 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.metadata_id, None); + assert_eq!( + metadata.metadata_id, + "2585a800-7d93-4f52-8648-d9cb39f413d2".to_string() + ); } other => panic!("expected message item, got {other:?}"), } @@ -244,7 +247,7 @@ fn formatted_input_preserves_existing_message_metadata_id() { }], metadata: Some(ResponseItemMessageMetadata { user_message_type: Some(UserMessageType::Prompt), - metadata_id: Some("2585a800-7d93-4f52-8648-d9cb39f413d2".to_string()), + metadata_id: "2585a800-7d93-4f52-8648-d9cb39f413d2".to_string(), }), end_turn: None, phase: None, @@ -260,7 +263,7 @@ fn formatted_input_preserves_existing_message_metadata_id() { let metadata = metadata.as_ref().expect("metadata should be present"); assert_eq!(metadata.user_message_type, Some(UserMessageType::Prompt)); assert_eq!( - metadata.metadata_id.as_deref(), + Some(metadata.metadata_id.as_str()), Some("2585a800-7d93-4f52-8648-d9cb39f413d2") ); } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 86edfb1a07..812d7eb7ff 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -330,6 +330,7 @@ use codex_protocol::models::ContentItem; use codex_protocol::models::DeveloperInstructions; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; +use codex_protocol::models::ResponseItemMessageMetadata; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::InitialHistory; @@ -3298,7 +3299,26 @@ impl Session { items .iter() .cloned() - .map(ResponseItem::with_generated_metadata_id) + .map(|item| match item { + ResponseItem::Message { + id, + role, + content, + metadata, + end_turn, + phase, + } => ResponseItem::Message { + id, + role, + content, + metadata: Some( + metadata.unwrap_or_else(|| ResponseItemMessageMetadata::new(None)), + ), + end_turn, + phase, + }, + other => other, + }) .collect() } else { items.to_vec() diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 4807846150..a2b1806a95 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -4438,7 +4438,7 @@ async fn record_into_history_generates_message_metadata_id_when_item_metadata_en let metadata_id = metadata .as_ref() - .and_then(|metadata| metadata.metadata_id.as_deref()) + .map(|metadata| metadata.metadata_id.as_str()) .expect("metadata_id should be generated when item metadata is enabled"); uuid::Uuid::parse_str(metadata_id).expect("metadata_id should be valid"); } diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index e9aa0b89fc..4d542c82ad 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -269,54 +269,24 @@ pub enum UserMessageType { PromptQueued, } -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] pub struct ResponseItemMessageMetadata { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub user_message_type: Option, /// Client-visible metadata ID generated by Codex for this item. - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub metadata_id: Option, + pub metadata_id: String, } -impl ResponseItem { - /// Ensures message metadata includes a generated metadata ID. - pub fn with_generated_metadata_id(self) -> Self { - match self { - ResponseItem::Message { - id, - role, - content, - metadata, - end_turn, - phase, - } => { - let metadata = ensure_generated_metadata_id(metadata); - ResponseItem::Message { - id, - role, - content, - metadata, - end_turn, - phase, - } - } - other => other, +impl ResponseItemMessageMetadata { + pub fn new(user_message_type: Option) -> Self { + Self { + user_message_type, + metadata_id: uuid::Uuid::new_v4().to_string(), } } } -fn ensure_generated_metadata_id( - metadata: Option, -) -> Option { - let mut metadata = metadata.unwrap_or_default(); - if metadata.metadata_id.is_none() { - metadata.metadata_id = Some(uuid::Uuid::new_v4().to_string()); - } - Some(metadata) -} - #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ContentItem { @@ -3020,49 +2990,29 @@ mod tests { } #[test] - fn generates_metadata_id_for_message_items() -> Result<()> { - let item = ResponseItem::Message { - id: Some("msg_123".to_string()), - role: "assistant".to_string(), - content: vec![ContentItem::OutputText { - text: "hello".to_string(), - }], - metadata: Some(ResponseItemMessageMetadata { - user_message_type: Some(UserMessageType::Prompt), - metadata_id: None, - }), - end_turn: None, - phase: None, - }; - - let surfaced = item.with_generated_metadata_id(); - let serialized = serde_json::to_value(&surfaced)?; - let metadata = serialized - .get("metadata") - .and_then(serde_json::Value::as_object) - .expect("metadata should be present"); + fn response_item_message_metadata_serializes_required_metadata_id() -> Result<()> { + let metadata = ResponseItemMessageMetadata::new(Some(UserMessageType::Prompt)); + let serialized = serde_json::to_value(&metadata)?; + let metadata = serialized.as_object().expect("metadata should be present"); let metadata_id = metadata .get("metadata_id") .and_then(serde_json::Value::as_str) .expect("metadata_id should be present"); uuid::Uuid::parse_str(metadata_id).expect("metadata_id should be valid"); - - assert_eq!(serialized.get("type"), Some(&serde_json::json!("message"))); - assert_eq!( - serialized.get("role"), - Some(&serde_json::json!("assistant")) - ); - assert_eq!( - serialized.get("content"), - Some(&serde_json::json!([{ "type": "output_text", "text": "hello" }])) - ); assert_eq!( metadata.get("user_message_type"), Some(&serde_json::json!("prompt")) ); - assert_ne!(metadata_id, "msg_123"); Ok(()) } + + #[test] + fn response_item_message_metadata_new_generates_metadata_id() { + let metadata = ResponseItemMessageMetadata::new(Some(UserMessageType::Prompt)); + + assert_eq!(metadata.user_message_type, Some(UserMessageType::Prompt)); + uuid::Uuid::parse_str(&metadata.metadata_id).expect("metadata_id should be valid"); + } }