mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Add optional IDs to response items (#28812)
## Why `ResponseItem` variants do not have a consistent internal ID shape: some variants carry required IDs, some carry optional IDs, and some cannot represent an ID at all. The existing fields also use inconsistent serde, TypeScript, and JSON-schema annotations. A single enum-level access path is needed before history recording can assign and retain IDs. This PR establishes that internal model only. It intentionally does not generate or serialize IDs; allocation and wire persistence are isolated in the stacked follow-up. ## What changed - Give every concrete `ResponseItem` variant an `Option<String>` ID field. - Apply the same internal-only annotations to every ID field: `#[serde(default, skip_serializing)]`, `#[ts(skip)]`, and `#[schemars(skip)]`. - Add `ResponseItem::id()` and `ResponseItem::set_id()` as the shared accessors. - Preserve IDs when history items are rewritten for truncation. - Adapt consumers that previously assumed reasoning and image-generation IDs were required. - Regenerate app-server schemas so the hidden fields are represented consistently. The serde catch-all `ResponseItem::Other` remains ID-less because it must remain a unit variant. ## Test plan - `cargo check --tests -p codex-core -p codex-api -p codex-rollout-trace -p codex-image-generation-extension` - `just test -p codex-protocol` - `just test -p codex-app-server-protocol` - `just test -p codex-api -p codex-rollout-trace -p codex-image-generation-extension` - `just test -p codex-core event_mapping`
This commit is contained in:
committed by
GitHub
Unverified
parent
c274a83f8b
commit
dbd2857f4b
@@ -920,6 +920,7 @@ pub enum ResponseItem {
|
||||
Message {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
role: String,
|
||||
content: Vec<ContentItem>,
|
||||
@@ -934,6 +935,10 @@ pub enum ResponseItem {
|
||||
metadata: Option<ResponseItemMetadata>,
|
||||
},
|
||||
AgentMessage {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
author: String,
|
||||
recipient: String,
|
||||
content: Vec<AgentMessageInputContent>,
|
||||
@@ -945,7 +950,7 @@ pub enum ResponseItem {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: String,
|
||||
id: Option<String>,
|
||||
summary: Vec<ReasoningItemReasoningSummary>,
|
||||
#[serde(default, skip_serializing_if = "should_serialize_reasoning_content")]
|
||||
#[ts(optional)]
|
||||
@@ -959,6 +964,7 @@ pub enum ResponseItem {
|
||||
/// Legacy id field retained for compatibility with older payloads.
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
/// Set when using the Responses API.
|
||||
call_id: Option<String>,
|
||||
@@ -971,6 +977,7 @@ pub enum ResponseItem {
|
||||
FunctionCall {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -988,6 +995,7 @@ pub enum ResponseItem {
|
||||
ToolSearchCall {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
call_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -1006,6 +1014,10 @@ pub enum ResponseItem {
|
||||
// - an array of structured content items (`content_items`)
|
||||
// We keep this behavior centralized in `FunctionCallOutputPayload`.
|
||||
FunctionCallOutput {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
call_id: String,
|
||||
#[ts(as = "FunctionCallOutputBody")]
|
||||
#[schemars(with = "FunctionCallOutputBody")]
|
||||
@@ -1017,6 +1029,7 @@ pub enum ResponseItem {
|
||||
CustomToolCall {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1033,6 +1046,10 @@ pub enum ResponseItem {
|
||||
// `function_call_output.output` so freeform tools can return either plain
|
||||
// text or structured content items.
|
||||
CustomToolCallOutput {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
call_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1045,6 +1062,10 @@ pub enum ResponseItem {
|
||||
metadata: Option<ResponseItemMetadata>,
|
||||
},
|
||||
ToolSearchOutput {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
call_id: Option<String>,
|
||||
status: String,
|
||||
execution: String,
|
||||
@@ -1065,6 +1086,7 @@ pub enum ResponseItem {
|
||||
WebSearchCall {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1086,7 +1108,10 @@ pub enum ResponseItem {
|
||||
// "result":"..."
|
||||
// }
|
||||
ImageGenerationCall {
|
||||
id: String,
|
||||
/// Existing provider ID retained on serialized history for compatibility.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
id: Option<String>,
|
||||
status: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -1098,17 +1123,29 @@ pub enum ResponseItem {
|
||||
},
|
||||
#[serde(alias = "compaction_summary")]
|
||||
Compaction {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
encrypted_content: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
metadata: Option<ResponseItemMetadata>,
|
||||
},
|
||||
CompactionTrigger {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
metadata: Option<ResponseItemMetadata>,
|
||||
},
|
||||
ContextCompaction {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
#[schemars(skip)]
|
||||
id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
encrypted_content: Option<String>,
|
||||
@@ -1126,6 +1163,50 @@ impl ResponseItem {
|
||||
matches!(self, Self::Message { role, .. } if role == "user")
|
||||
}
|
||||
|
||||
/// Returns the non-empty Responses API item ID, if present.
|
||||
pub fn id(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Message { id, .. }
|
||||
| Self::AgentMessage { id, .. }
|
||||
| Self::LocalShellCall { id, .. }
|
||||
| Self::FunctionCall { id, .. }
|
||||
| Self::ToolSearchCall { id, .. }
|
||||
| Self::FunctionCallOutput { id, .. }
|
||||
| Self::CustomToolCall { id, .. }
|
||||
| Self::CustomToolCallOutput { id, .. }
|
||||
| Self::ToolSearchOutput { id, .. }
|
||||
| Self::WebSearchCall { id, .. }
|
||||
| Self::Reasoning { id, .. }
|
||||
| Self::ImageGenerationCall { id, .. }
|
||||
| Self::Compaction { id, .. }
|
||||
| Self::CompactionTrigger { id, .. }
|
||||
| Self::ContextCompaction { id, .. } => id.as_deref().filter(|id| !id.is_empty()),
|
||||
Self::Other => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the Responses API item ID for variants that carry one.
|
||||
pub fn set_id(&mut self, new_id: String) {
|
||||
match self {
|
||||
Self::Message { id, .. }
|
||||
| Self::AgentMessage { id, .. }
|
||||
| Self::LocalShellCall { id, .. }
|
||||
| Self::FunctionCall { id, .. }
|
||||
| Self::ToolSearchCall { id, .. }
|
||||
| Self::FunctionCallOutput { id, .. }
|
||||
| Self::CustomToolCall { id, .. }
|
||||
| Self::CustomToolCallOutput { id, .. }
|
||||
| Self::ToolSearchOutput { id, .. }
|
||||
| Self::WebSearchCall { id, .. }
|
||||
| Self::Reasoning { id, .. }
|
||||
| Self::ImageGenerationCall { id, .. }
|
||||
| Self::Compaction { id, .. }
|
||||
| Self::CompactionTrigger { id, .. }
|
||||
| Self::ContextCompaction { id, .. } => *id = Some(new_id),
|
||||
Self::Other => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the non-empty turn ID stamped onto this item, if present.
|
||||
pub fn turn_id(&self) -> Option<&str> {
|
||||
self.metadata()
|
||||
@@ -1168,7 +1249,7 @@ impl ResponseItem {
|
||||
| Self::WebSearchCall { metadata, .. }
|
||||
| Self::ImageGenerationCall { metadata, .. }
|
||||
| Self::Compaction { metadata, .. }
|
||||
| Self::CompactionTrigger { metadata }
|
||||
| Self::CompactionTrigger { metadata, .. }
|
||||
| Self::ContextCompaction { metadata, .. } => metadata.as_ref(),
|
||||
Self::Other => None,
|
||||
}
|
||||
@@ -1189,7 +1270,7 @@ impl ResponseItem {
|
||||
| Self::WebSearchCall { metadata, .. }
|
||||
| Self::ImageGenerationCall { metadata, .. }
|
||||
| Self::Compaction { metadata, .. }
|
||||
| Self::CompactionTrigger { metadata }
|
||||
| Self::CompactionTrigger { metadata, .. }
|
||||
| Self::ContextCompaction { metadata, .. } => Some(metadata),
|
||||
Self::Other => None,
|
||||
}
|
||||
@@ -1435,6 +1516,7 @@ impl From<ResponseInputItem> for ResponseItem {
|
||||
metadata: None,
|
||||
},
|
||||
ResponseInputItem::FunctionCallOutput { call_id, output } => Self::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id,
|
||||
output,
|
||||
metadata: None,
|
||||
@@ -1442,6 +1524,7 @@ impl From<ResponseInputItem> for ResponseItem {
|
||||
ResponseInputItem::McpToolCallOutput { call_id, output } => {
|
||||
let output = output.into_function_call_output_payload();
|
||||
Self::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id,
|
||||
output,
|
||||
metadata: None,
|
||||
@@ -1452,6 +1535,7 @@ impl From<ResponseInputItem> for ResponseItem {
|
||||
name,
|
||||
output,
|
||||
} => Self::CustomToolCallOutput {
|
||||
id: None,
|
||||
call_id,
|
||||
name,
|
||||
output,
|
||||
@@ -1467,6 +1551,7 @@ impl From<ResponseInputItem> for ResponseItem {
|
||||
status,
|
||||
execution,
|
||||
tools,
|
||||
id: None,
|
||||
metadata: None,
|
||||
},
|
||||
}
|
||||
@@ -2060,6 +2145,16 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_item_id_getter_and_setter() {
|
||||
let mut item = response_item_with_metadata(/*metadata*/ None);
|
||||
assert_eq!(item.id(), None);
|
||||
|
||||
item.set_id("msg_test".to_string());
|
||||
|
||||
assert_eq!(item.id(), Some("msg_test"));
|
||||
}
|
||||
|
||||
fn response_item_with_metadata(metadata: Option<ResponseItemMetadata>) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
@@ -2176,7 +2271,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::ImageGenerationCall {
|
||||
id: "ig_123".to_string(),
|
||||
id: Some("ig_123".to_string()),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: Some("A small blue square".to_string()),
|
||||
result: "Zm9v".to_string(),
|
||||
@@ -2198,7 +2293,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::ImageGenerationCall {
|
||||
id: "ig_123".to_string(),
|
||||
id: Some("ig_123".to_string()),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt: None,
|
||||
result: "Zm9v".to_string(),
|
||||
@@ -2900,6 +2995,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::Compaction {
|
||||
id: None,
|
||||
encrypted_content: "abc".into(),
|
||||
metadata: None,
|
||||
}
|
||||
@@ -2916,6 +3012,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::ContextCompaction {
|
||||
id: None,
|
||||
encrypted_content: Some("abc".into()),
|
||||
metadata: None,
|
||||
}
|
||||
@@ -2925,7 +3022,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn serializes_compaction_trigger_without_payload() -> Result<()> {
|
||||
let item = ResponseItem::CompactionTrigger { metadata: None };
|
||||
let item = ResponseItem::CompactionTrigger {
|
||||
id: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(item)?,
|
||||
@@ -2938,7 +3038,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn serializes_stamped_compaction_trigger_metadata() -> Result<()> {
|
||||
let mut item = ResponseItem::CompactionTrigger { metadata: None };
|
||||
let mut item = ResponseItem::CompactionTrigger {
|
||||
id: None,
|
||||
metadata: None,
|
||||
};
|
||||
item.stamp_turn_id_if_missing("turn-1");
|
||||
|
||||
assert_eq!(
|
||||
@@ -2959,7 +3062,13 @@ mod tests {
|
||||
|
||||
let item: ResponseItem = serde_json::from_str(json)?;
|
||||
|
||||
assert_eq!(item, ResponseItem::CompactionTrigger { metadata: None });
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::CompactionTrigger {
|
||||
id: None,
|
||||
metadata: None,
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3188,6 +3297,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
ResponseItem::from(input.clone()),
|
||||
ResponseItem::ToolSearchOutput {
|
||||
id: None,
|
||||
call_id: Some("search-1".to_string()),
|
||||
status: "completed".to_string(),
|
||||
execution: "client".to_string(),
|
||||
@@ -3275,6 +3385,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
parsed_output,
|
||||
ResponseItem::ToolSearchOutput {
|
||||
id: None,
|
||||
call_id: None,
|
||||
status: "completed".to_string(),
|
||||
execution: "server".to_string(),
|
||||
|
||||
@@ -762,6 +762,7 @@ impl InterAgentCommunication {
|
||||
}],
|
||||
};
|
||||
ResponseItem::AgentMessage {
|
||||
id: None,
|
||||
author: self.author.to_string(),
|
||||
recipient: self.recipient.to_string(),
|
||||
content,
|
||||
@@ -4284,6 +4285,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
communication.to_model_input_item(),
|
||||
ResponseItem::AgentMessage {
|
||||
id: None,
|
||||
author: "/root/worker".to_string(),
|
||||
recipient: "/root".to_string(),
|
||||
content: vec![
|
||||
|
||||
Reference in New Issue
Block a user