feat(core): add metadata field to ResponseItem (#28355)

## Description

This PR adds an optional `metadata` field to `ResponseItem` for
Responses API calls. Only mechanical plumbing, no actual values
populated and sent yet. Turns out just adding a new field to
`ResponseItem` has quite a large blast radius already.

This change is backwards compatible because `metadata` is optional and
omitted when absent, so existing response items and rollout history
without it still deserialize and requests that do not set it keep the
same wire shape. For provider compatibility, we strip out `metadata`
before non-OpenAI Responses requests so Azure and AWS Bedrock never see
this field.

My followup PR here will actually make use of it to start storing and
passing along `turn_id`: https://github.com/openai/codex/pull/28360

## What changed

- Added `ResponseItemMetadata` with optional `turn_id`, plus optional
`metadata` on Responses API item variants and inter-agent communication.
- Preserved item metadata through response-item rewrites such as
truncation, missing tool-output synthesis, compaction history
rebuilding, visible-history conversion, rollout/resume, and generated
app-server schemas/types.
- Strip item metadata from non-OpenAI Responses requests while
preserving it for OpenAI-shaped requests.
- Updated the mechanical fixture/test construction churn required by the
new optional field.
This commit is contained in:
Owen Lin
2026-06-15 15:05:28 -07:00
committed by GitHub
Unverified
parent bef99f861b
commit 040dafa32d
85 changed files with 1637 additions and 92 deletions
+69 -5
View File
@@ -2,6 +2,7 @@ use super::*;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::WireApi;
use codex_protocol::models::DEFAULT_IMAGE_DETAIL;
use codex_protocol::models::ResponseItemMetadata;
use pretty_assertions::assert_eq;
async fn process_compacted_history_with_test_session(
@@ -31,6 +32,14 @@ fn user_message(text: &str) -> ResponseItem {
text: text.to_string(),
}],
phase: None,
metadata: None,
}
}
fn compacted_user_message(text: &str) -> CompactedUserMessage {
CompactedUserMessage {
message: text.to_string(),
metadata: None,
}
}
@@ -75,6 +84,7 @@ fn collect_user_messages_extracts_user_text_only() {
text: "ignored".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: Some("user".to_string()),
@@ -83,13 +93,14 @@ fn collect_user_messages_extracts_user_text_only() {
text: "first".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Other,
];
let collected = collect_user_messages(&items);
assert_eq!(vec!["first".to_string()], collected);
assert_eq!(vec![compacted_user_message("first")], collected);
}
#[test]
@@ -107,6 +118,7 @@ do things
.to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -115,6 +127,7 @@ do things
text: "<ENVIRONMENT_CONTEXT>cwd=/tmp</ENVIRONMENT_CONTEXT>".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -123,12 +136,13 @@ do things
text: "real user message".to_string(),
}],
phase: None,
metadata: None,
},
];
let collected = collect_user_messages(&items);
assert_eq!(vec!["real user message".to_string()], collected);
assert_eq!(vec![compacted_user_message("real user message")], collected);
}
#[test]
@@ -148,7 +162,7 @@ fn collect_user_messages_filters_legacy_warnings() {
let collected = collect_user_messages(&items);
assert_eq!(vec!["real user message".to_string()], collected);
assert_eq!(vec![compacted_user_message("real user message")], collected);
}
#[test]
@@ -157,9 +171,10 @@ fn build_token_limited_compacted_history_truncates_overlong_user_messages() {
// that oversized user content is truncated.
let max_tokens = 16;
let big = "word ".repeat(200);
let user_message = compacted_user_message(&big);
let history = super::build_compacted_history_with_limit(
Vec::new(),
std::slice::from_ref(&big),
std::slice::from_ref(&user_message),
"SUMMARY",
max_tokens,
);
@@ -196,7 +211,7 @@ fn build_token_limited_compacted_history_truncates_overlong_user_messages() {
#[test]
fn build_token_limited_compacted_history_appends_summary_message() {
let initial_context: Vec<ResponseItem> = Vec::new();
let user_messages = vec!["first user message".to_string()];
let user_messages = vec![compacted_user_message("first user message")];
let summary_text = "summary text";
let history = build_compacted_history(initial_context, &user_messages, summary_text);
@@ -215,6 +230,23 @@ fn build_token_limited_compacted_history_appends_summary_message() {
assert_eq!(summary, summary_text);
}
#[test]
fn build_compacted_history_preserves_user_message_metadata() {
let history = build_compacted_history(
Vec::new(),
&[CompactedUserMessage {
message: "first user message".to_string(),
metadata: Some(ResponseItemMetadata {
turn_id: Some("turn-1".to_string()),
}),
}],
"summary text",
);
assert_eq!(history[0].turn_id(), Some("turn-1"));
assert_eq!(history[1].turn_id(), None);
}
#[test]
fn should_use_remote_compact_task_for_azure_provider() {
let provider = ModelProviderInfo {
@@ -249,6 +281,7 @@ async fn process_compacted_history_replaces_developer_messages() {
text: "stale permissions".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -257,6 +290,7 @@ async fn process_compacted_history_replaces_developer_messages() {
text: "summary".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -265,6 +299,7 @@ async fn process_compacted_history_replaces_developer_messages() {
text: "stale personality".to_string(),
}],
phase: None,
metadata: None,
},
];
let (refreshed, mut expected) = process_compacted_history_with_test_session(
@@ -279,6 +314,7 @@ async fn process_compacted_history_replaces_developer_messages() {
text: "summary".to_string(),
}],
phase: None,
metadata: None,
});
assert_eq!(refreshed, expected);
}
@@ -292,6 +328,7 @@ async fn process_compacted_history_reinjects_full_initial_context() {
text: "summary".to_string(),
}],
phase: None,
metadata: None,
}];
let (refreshed, mut expected) = process_compacted_history_with_test_session(
compacted_history,
@@ -305,6 +342,7 @@ async fn process_compacted_history_reinjects_full_initial_context() {
text: "summary".to_string(),
}],
phase: None,
metadata: None,
});
assert_eq!(refreshed, expected);
}
@@ -324,6 +362,7 @@ keep me updated
.to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -336,6 +375,7 @@ keep me updated
.to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -348,6 +388,7 @@ keep me updated
.to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -356,6 +397,7 @@ keep me updated
text: "summary".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -364,6 +406,7 @@ keep me updated
text: "stale developer instructions".to_string(),
}],
phase: None,
metadata: None,
},
];
let (refreshed, mut expected) = process_compacted_history_with_test_session(
@@ -378,6 +421,7 @@ keep me updated
text: "summary".to_string(),
}],
phase: None,
metadata: None,
});
assert_eq!(refreshed, expected);
}
@@ -417,6 +461,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
text: "older user".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -425,6 +470,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -433,6 +479,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
text: "latest user".to_string(),
}],
phase: None,
metadata: None,
},
];
@@ -449,6 +496,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
text: "older user".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -457,6 +505,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
phase: None,
metadata: None,
},
];
expected.extend(initial_context);
@@ -467,6 +516,7 @@ async fn process_compacted_history_inserts_context_before_last_real_user_message
text: "latest user".to_string(),
}],
phase: None,
metadata: None,
});
assert_eq!(refreshed, expected);
}
@@ -480,6 +530,7 @@ async fn process_compacted_history_reinjects_model_switch_message() {
text: "summary".to_string(),
}],
phase: None,
metadata: None,
}];
let previous_turn_settings = PreviousTurnSettings {
model: "previous-regular-model".to_string(),
@@ -510,6 +561,7 @@ async fn process_compacted_history_reinjects_model_switch_message() {
text: "summary".to_string(),
}],
phase: None,
metadata: None,
});
assert_eq!(refreshed, expected);
}
@@ -524,6 +576,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: "older user".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -532,6 +585,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: "latest user".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -540,6 +594,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
phase: None,
metadata: None,
},
];
let initial_context = vec![ResponseItem::Message {
@@ -549,6 +604,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: "fresh permissions".to_string(),
}],
phase: None,
metadata: None,
}];
let refreshed =
@@ -561,6 +617,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: "older user".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -569,6 +626,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: "fresh permissions".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -577,6 +635,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: "latest user".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Message {
id: None,
@@ -585,6 +644,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
text: format!("{SUMMARY_PREFIX}\nsummary text"),
}],
phase: None,
metadata: None,
},
];
assert_eq!(refreshed, expected);
@@ -594,6 +654,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_summary_last()
fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last() {
let compacted_history = vec![ResponseItem::Compaction {
encrypted_content: "encrypted".to_string(),
metadata: None,
}];
let initial_context = vec![ResponseItem::Message {
id: None,
@@ -602,6 +663,7 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
text: "fresh permissions".to_string(),
}],
phase: None,
metadata: None,
}];
let refreshed =
@@ -614,9 +676,11 @@ fn insert_initial_context_before_last_real_user_or_summary_keeps_compaction_last
text: "fresh permissions".to_string(),
}],
phase: None,
metadata: None,
},
ResponseItem::Compaction {
encrypted_content: "encrypted".to_string(),
metadata: None,
},
];
assert_eq!(refreshed, expected);