[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:
pakrym-oai
2026-06-17 18:27:43 -07:00
committed by GitHub
Unverified
parent c274a83f8b
commit dbd2857f4b
36 changed files with 278 additions and 258 deletions
+6 -1
View File
@@ -611,6 +611,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() {
RolloutLine {
timestamp: "2024-01-01T00:00:02.000Z".to_string(),
item: RolloutItem::ResponseItem(ResponseItem::CustomToolCallOutput {
id: None,
call_id: "legacy-js-call".to_string(),
name: None,
output: FunctionCallOutputPayload::from_text("legacy js_repl stdout".to_string()),
@@ -751,6 +752,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
RolloutLine {
timestamp: "2024-01-01T00:00:01.500Z".to_string(),
item: RolloutItem::ResponseItem(ResponseItem::FunctionCallOutput {
id: None,
call_id: function_call_id.to_string(),
output: FunctionCallOutputPayload::from_content_items(vec![
FunctionCallOutputContentItem::InputImage {
@@ -775,6 +777,7 @@ async fn resume_replays_image_tool_outputs_with_detail() {
RolloutLine {
timestamp: "2024-01-01T00:00:02.500Z".to_string(),
item: RolloutItem::ResponseItem(ResponseItem::CustomToolCallOutput {
id: None,
call_id: custom_call_id.to_string(),
name: None,
output: FunctionCallOutputPayload::from_content_items(vec![
@@ -2518,7 +2521,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
let mut prompt = Prompt::default();
prompt.input.push(ResponseItem::Reasoning {
id: "reasoning-id".into(),
id: Some("reasoning-id".into()),
summary: vec![ReasoningItemReasoningSummary::SummaryText {
text: "summary".into(),
}],
@@ -2555,6 +2558,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
metadata: None,
});
prompt.input.push(ResponseItem::FunctionCallOutput {
id: None,
call_id: "function-call-id".into(),
output: FunctionCallOutputPayload::from_text("ok".into()),
metadata: None,
@@ -2581,6 +2585,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() {
metadata: None,
});
prompt.input.push(ResponseItem::CustomToolCallOutput {
id: None,
call_id: "custom-tool-call-id".into(),
name: None,
output: FunctionCallOutputPayload::from_text("ok".into()),
+3
View File
@@ -1995,6 +1995,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
metadata: None,
},
codex_protocol::models::ResponseItem::Compaction {
id: None,
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
},
@@ -3994,6 +3995,7 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() {
metadata: None,
},
codex_protocol::models::ResponseItem::Compaction {
id: None,
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
},
@@ -4121,6 +4123,7 @@ async fn auto_compact_runs_when_reasoning_header_clears_between_turns() {
metadata: None,
},
codex_protocol::models::ResponseItem::Compaction {
id: None,
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
},
@@ -157,6 +157,7 @@ fn format_labeled_requests_snapshot(
fn compacted_summary_only_output(summary: &str) -> Vec<ResponseItem> {
vec![ResponseItem::Compaction {
id: None,
encrypted_content: summary_with_prefix(summary),
metadata: None,
}]
@@ -329,6 +330,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> {
.await;
let compacted_history = vec![ResponseItem::Compaction {
id: None,
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
}];
@@ -2356,6 +2358,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()>
let compacted_history = vec![
ResponseItem::Compaction {
id: None,
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
},
@@ -2511,6 +2514,7 @@ async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Res
metadata: None,
},
ResponseItem::Compaction {
id: None,
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
},
@@ -2653,6 +2657,7 @@ async fn remote_compact_refreshes_stale_developer_instructions_without_resume()
metadata: None,
},
ResponseItem::Compaction {
id: None,
encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(),
metadata: None,
},
@@ -4059,6 +4064,7 @@ async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinject
.await;
let compacted_history = vec![ResponseItem::Compaction {
id: None,
encrypted_content: summary_with_prefix("REMOTE_SUMMARY_ONLY"),
metadata: None,
}];