Expose structured standalone web search results (#32898)

## Why

Standalone web search can return structured result DTOs separately from its
model-facing text output. App-server clients need access to that data without
coupling Codex to every result type or field.

## What changed

- Preserve the optional `results` array from `/v1/alpha/search` as opaque JSON
  through web-search completion events, thread history, and app-server
  `webSearch` items.
- Keep responses from older endpoints compatible when `results` is absent, and
  allow clients to ignore result types and fields they do not understand.
- Regenerate the protocol schemas and TypeScript definitions for the optional
  field.

## Testing

- Cover responses with missing, empty, and forward-compatible result payloads.
- Verify standalone web-search results round-trip through app-server item
  notifications and thread-item conversion.

GitOrigin-RevId: 739ef5694ece5733f7b8749ec41d6bfa3f33c8c5
This commit is contained in:
briansong-oai
2026-07-13 23:37:35 +00:00
committed by copyberry
parent 090f4de37b
commit 2ca20d5224
38 changed files with 329 additions and 4 deletions
@@ -634,6 +634,7 @@ impl ThreadHistoryBuilder {
id: payload.call_id.clone(),
query: String::new(),
action: None,
results: None,
});
self.upsert_item_in_current_turn(item);
}
@@ -643,6 +644,7 @@ impl ThreadHistoryBuilder {
id: payload.call_id.clone(),
query: payload.query.clone(),
action: Some(web_search_action_from_core(payload.action.clone())),
results: payload.results.clone(),
});
self.upsert_item_in_current_turn(item);
}
@@ -2664,6 +2666,11 @@ mod tests {
query: Some("codex".into()),
queries: None,
},
results: Some(vec![serde_json::json!({
"type": "text_result",
"ref_id": "turn0search0",
"url": "https://example.com/codex",
})]),
}),
EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: "exec-1".into(),
@@ -2720,6 +2727,11 @@ mod tests {
query: Some("codex".into()),
queries: None,
}),
results: Some(vec![serde_json::json!({
"type": "text_result",
"ref_id": "turn0search0",
"url": "https://example.com/codex",
})]),
})
);
assert_eq!(
@@ -4195,6 +4207,7 @@ mod tests {
query: Some("codex".into()),
queries: None,
},
results: None,
}),
));
assert_eq!(
@@ -4209,6 +4222,7 @@ mod tests {
query: Some("codex".into()),
queries: None,
}),
results: None,
}),
}],
changed_turns: Vec::new(),
@@ -4330,6 +4344,7 @@ mod tests {
query: Some("codex".into()),
queries: None,
},
results: None,
})),
]);
assert_eq!(
@@ -4344,6 +4359,7 @@ mod tests {
query: Some("codex".into()),
queries: None,
}),
results: None,
}),
}],
changed_turns: vec![ThreadHistoryTurnChange {
@@ -902,6 +902,7 @@ impl From<CoreTurnItem> for ThreadItem {
id: search.id,
query: search.query,
action: Some(web_search_action_from_core(search.action)),
results: search.results,
}),
CoreTurnItem::ImageView(image) => ThreadItem::ImageView {
id: image.id,
@@ -2777,6 +2777,11 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
query: Some("docs".to_string()),
queries: None,
},
results: Some(vec![serde_json::json!({
"type": "text_result",
"ref_id": "turn0search0",
"url": "https://example.com/docs",
})]),
});
let expected_search_item = WebSearchItem {
@@ -2786,6 +2791,11 @@ fn core_turn_item_into_thread_item_converts_supported_variants() {
query: Some("docs".to_string()),
queries: None,
}),
results: Some(vec![serde_json::json!({
"type": "text_result",
"ref_id": "turn0search0",
"url": "https://example.com/docs",
})]),
};
assert_eq!(