Preserve content item kinds in message metadata (#40174)

## What changed

- Add `ContentItemKind` as an open-ended string classification and carry an
  optional list of kinds in `InternalChatMessageMetadataPassthrough`.
- Keep unknown classification values intact during round trips, while treating
  a malformed `content_item_kinds` value as absent so the response item can
  still be loaded.

## Testing

- Cover round trips for future classification values and deserialization of
  malformed metadata.

GitOrigin-RevId: 5398cae5bb294a9f71ddd192e297177fab54a576
This commit is contained in:
pakrym-oai
2026-08-23 02:33:59 +00:00
committed by copyberry
parent 99660ab3c7
commit eff640f458
3 changed files with 100 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::ser::Serializer;
use serde_with::serde_as;
use ts_rs::TS;
use crate::local_media::audio_mime_for_path;
@@ -32,6 +33,7 @@ use crate::mcp::CallToolResult;
use codex_utils_path_uri::PathUri;
mod executed_tool_calls;
mod item_metadata;
pub use crate::local_media::MAX_PROMPT_AUDIO_INPUT_BYTES;
pub use crate::local_media::snapshot_local_user_input;
@@ -42,6 +44,7 @@ pub use executed_tool_calls::ExecutedToolCallTruncation;
pub use executed_tool_calls::bound_executed_tool_calls_for_prompt;
pub use executed_tool_calls::bound_executed_tool_calls_for_prompt_prioritizing_recent;
pub use executed_tool_calls::executed_tool_call_metadata_bytes;
pub use item_metadata::ContentItemKind;
/// Controls the per-command sandbox override requested by a shell-like tool call.
#[derive(
@@ -904,6 +907,7 @@ pub enum MessagePhase {
///
/// Responses API strongly types this payload. Do not modify it without first getting API
/// approval and making the corresponding Responses API change.
#[serde_as]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)]
pub struct InternalChatMessageMetadataPassthrough {
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -914,6 +918,12 @@ pub struct InternalChatMessageMetadataPassthrough {
#[schemars(skip)]
#[ts(skip)]
pub create_time: Option<serde_json::Number>,
/// Harness-owned classifications aligned with the item's content entries.
#[serde_as(deserialize_as = "serde_with::DefaultOnError")]
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(skip)]
#[ts(skip)]
pub content_item_kinds: Option<Vec<ContentItemKind>>,
/// Warehouse-only Responses metadata, not part of the public app-server protocol.
#[serde(default, skip_deserializing, skip_serializing_if = "Option::is_none")]
#[schemars(skip)]

View File

@@ -0,0 +1,11 @@
use serde::Deserialize;
use serde::Serialize;
/// Harness-owned classification for one position in an item's content array.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(transparent)]
pub struct ContentItemKind(pub String);
#[cfg(test)]
#[path = "item_metadata_tests.rs"]
mod tests;

View File

@@ -0,0 +1,79 @@
use super::ContentItemKind;
use crate::models::ContentItem;
use crate::models::InternalChatMessageMetadataPassthrough;
use crate::models::ResponseItem;
use anyhow::Result;
use pretty_assertions::assert_eq;
use serde_json::Value;
use serde_json::json;
#[test]
fn content_kinds_round_trip_without_restricting_future_values() -> Result<()> {
let item = response_item(InternalChatMessageMetadataPassthrough {
turn_id: Some("turn-1".to_string()),
content_item_kinds: Some(vec![ContentItemKind("future_content_kind".to_string())]),
..Default::default()
});
let serialized = serde_json::to_value(&item)?;
assert_eq!(
serialized,
json!({
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "hello"}],
"internal_chat_message_metadata_passthrough": {
"turn_id": "turn-1",
"content_item_kinds": ["future_content_kind"],
},
})
);
assert_eq!(serde_json::from_value::<ResponseItem>(serialized)?, item);
Ok(())
}
#[test]
fn malformed_content_kinds_do_not_prevent_loading_response_item() -> Result<()> {
for malformed_kinds in [
json!("user_text"),
json!({"kind": "user_text"}),
json!(["user_text", 123]),
] {
let item = serde_json::from_value::<ResponseItem>(response_item_json(json!({
"turn_id": "turn-1",
"content_item_kinds": malformed_kinds,
})))?;
assert_eq!(
item,
response_item(InternalChatMessageMetadataPassthrough {
turn_id: Some("turn-1".to_string()),
..Default::default()
})
);
}
Ok(())
}
fn response_item(metadata: InternalChatMessageMetadataPassthrough) -> ResponseItem {
ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "hello".to_string(),
}],
phase: None,
internal_chat_message_metadata_passthrough: Some(metadata),
}
}
fn response_item_json(metadata: Value) -> Value {
json!({
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "hello"}],
"internal_chat_message_metadata_passthrough": metadata,
})
}