add uuid to metadata

This commit is contained in:
Roy Han
2026-03-19 09:47:54 -07:00
parent 1879c53d6e
commit 2fc35f8b6b
11 changed files with 251 additions and 5 deletions
@@ -1998,6 +1998,13 @@
"type": "null"
}
]
},
"uuid": {
"description": "Client-visible UUID generated by Codex for this item.",
"type": [
"string",
"null"
]
}
},
"type": "object"
@@ -10589,6 +10589,13 @@
"type": "null"
}
]
},
"uuid": {
"description": "Client-visible UUID generated by Codex for this item.",
"type": [
"string",
"null"
]
}
},
"type": "object"
@@ -7377,6 +7377,13 @@
"type": "null"
}
]
},
"uuid": {
"description": "Client-visible UUID generated by Codex for this item.",
"type": [
"string",
"null"
]
}
},
"type": "object"
@@ -836,6 +836,13 @@
"type": "null"
}
]
},
"uuid": {
"description": "Client-visible UUID generated by Codex for this item.",
"type": [
"string",
"null"
]
}
},
"type": "object"
@@ -902,6 +902,13 @@
"type": "null"
}
]
},
"uuid": {
"description": "Client-visible UUID generated by Codex for this item.",
"type": [
"string",
"null"
]
}
},
"type": "object"
@@ -3,4 +3,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserMessageType } from "./UserMessageType";
export type ResponseItemMetadata = { user_message_type?: UserMessageType, };
export type ResponseItemMetadata = { user_message_type?: UserMessageType,
/**
* Client-visible UUID generated by Codex for this item.
*/
uuid?: string, };
@@ -2723,8 +2723,11 @@ mod tests {
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::TurnPlanStepStatus;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions;
use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions;
use codex_protocol::models::ResponseItem;
use codex_protocol::models::ResponseItemMetadata;
use codex_protocol::plan_tool::PlanItemArg;
use codex_protocol::plan_tool::StepStatus;
use codex_protocol::protocol::CollabResumeBeginEvent;
@@ -3756,4 +3759,74 @@ mod tests {
assert!(rx.try_recv().is_err(), "no messages expected");
Ok(())
}
#[tokio::test]
async fn raw_response_item_completed_preserves_existing_metadata_uuid() -> Result<()> {
let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY);
let outgoing = Arc::new(OutgoingMessageSender::new(tx));
let conversation_id = ThreadId::new();
let outgoing = ThreadScopedOutgoingMessageSender::new(
outgoing,
vec![ConnectionId(1)],
conversation_id.clone(),
);
let expected_uuid = uuid::Uuid::new_v4().to_string();
maybe_emit_raw_response_item_completed(
ApiVersion::V2,
conversation_id.clone(),
"turn-1",
ResponseItem::Message {
id: Some("msg_123".to_string()),
role: "assistant".to_string(),
content: vec![ContentItem::OutputText {
text: "hello".to_string(),
}],
metadata: Some(ResponseItemMetadata {
user_message_type: None,
uuid: Some(expected_uuid.clone()),
}),
end_turn: None,
phase: None,
},
&outgoing,
)
.await;
let msg = recv_broadcast_message(&mut rx).await?;
match msg {
OutgoingMessage::AppServerNotification(
ServerNotification::RawResponseItemCompleted(notification),
) => {
assert_eq!(notification.thread_id, conversation_id.to_string());
assert_eq!(notification.turn_id, "turn-1");
match notification.item {
ResponseItem::Message {
id,
metadata,
role,
content,
..
} => {
assert_eq!(id, Some("msg_123".to_string()));
assert_eq!(role, "assistant");
assert_eq!(
content,
vec![ContentItem::OutputText {
text: "hello".to_string()
}]
);
let metadata = metadata.expect("metadata should be present");
assert_eq!(metadata.user_message_type, None);
let uuid = metadata.uuid.expect("uuid should be present");
assert_eq!(uuid, expected_uuid);
}
other => bail!("unexpected response item: {other:?}"),
}
}
other => bail!("unexpected message: {other:?}"),
}
assert!(rx.try_recv().is_err(), "no extra messages expected");
Ok(())
}
}
+6 -1
View File
@@ -46,7 +46,12 @@ pub struct Prompt {
impl Prompt {
pub(crate) fn get_formatted_input(&self) -> Vec<ResponseItem> {
let mut input = self.input.clone();
let mut input: Vec<ResponseItem> = self
.input
.clone()
.into_iter()
.map(ResponseItem::with_generated_metadata_uuid)
.collect();
// when using the *Freeform* apply_patch tool specifically, tool outputs
// should be structured text, not json. Do NOT reserialize when using
+36
View File
@@ -3,7 +3,10 @@ use codex_api::common::OpenAiVerbosity;
use codex_api::common::TextControls;
use codex_api::create_text_param_for_request;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseItemMetadata;
use codex_protocol::models::UserMessageType;
use pretty_assertions::assert_eq;
use super::*;
@@ -196,6 +199,39 @@ fn reserializes_shell_outputs_for_function_and_custom_tool_calls() {
);
}
#[test]
fn formatted_input_generates_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: None,
}),
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));
let uuid = metadata.uuid.as_deref().expect("uuid should be present");
uuid::Uuid::parse_str(uuid).expect("uuid should be valid");
}
other => panic!("expected message item, got {other:?}"),
}
}
#[test]
fn tool_search_output_namespace_serializes_with_deferred_child_tools() {
let namespace = tools::ToolSearchOutputTool::Namespace(tools::ResponsesApiNamespace {
+8 -3
View File
@@ -3277,9 +3277,14 @@ impl Session {
turn_context: &TurnContext,
items: &[ResponseItem],
) {
self.record_into_history(items, turn_context).await;
self.persist_rollout_response_items(items).await;
self.send_raw_response_items(turn_context, items).await;
let items: Vec<ResponseItem> = items
.iter()
.cloned()
.map(ResponseItem::with_generated_metadata_uuid)
.collect();
self.record_into_history(&items, turn_context).await;
self.persist_rollout_response_items(&items).await;
self.send_raw_response_items(turn_context, &items).await;
}
/// Append ResponseItems to the in-memory conversation history only.
+88
View File
@@ -267,6 +267,47 @@ pub struct ResponseItemMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub user_message_type: Option<UserMessageType>,
/// Client-visible UUID generated by Codex for this item.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub uuid: Option<String>,
}
impl ResponseItem {
/// Ensures message metadata includes a generated UUID.
pub fn with_generated_metadata_uuid(self) -> Self {
match self {
ResponseItem::Message {
id,
role,
content,
metadata,
end_turn,
phase,
} => {
let metadata = ensure_generated_metadata_uuid(metadata);
ResponseItem::Message {
id,
role,
content,
metadata,
end_turn,
phase,
}
}
other => other,
}
}
}
fn ensure_generated_metadata_uuid(
metadata: Option<ResponseItemMetadata>,
) -> Option<ResponseItemMetadata> {
let mut metadata = metadata.unwrap_or_default();
if metadata.uuid.is_none() {
metadata.uuid = Some(uuid::Uuid::new_v4().to_string());
}
Some(metadata)
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
@@ -2958,4 +2999,51 @@ mod tests {
Ok(())
}
#[test]
fn generates_metadata_uuid_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(ResponseItemMetadata {
user_message_type: Some(UserMessageType::Prompt),
uuid: None,
}),
end_turn: None,
phase: 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!("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!(uuid, "msg_123");
Ok(())
}
}