mirror of
https://github.com/openai/codex.git
synced 2026-09-20 12:47:38 +00:00
Preserve web search actions and results in exec JSON output (#46319)
## Why Web search events in `codex exec --json` dropped structured results and relied on a serialization round trip to convert action types. ## What changed Map web search actions explicitly, preserving `open_page` URLs and `find_in_page` URLs and patterns. Forward structured results through an optional `results` field, omitting it when unavailable while preserving empty arrays and error payloads. ## Testing Add regression coverage for page actions with absent, empty, successful, and error results in serialized `item.completed` events. GitOrigin-RevId: 9a7f4c9c2eb163ba62f5e1642437a9debffc3816
This commit is contained in:
@@ -13,6 +13,7 @@ use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadTokenUsage;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::WebSearchAction as ApiWebSearchAction;
|
||||
use codex_core::config::Config;
|
||||
use codex_protocol::models::WebSearchAction;
|
||||
use codex_protocol::protocol::SessionConfiguredEvent;
|
||||
@@ -305,12 +306,18 @@ impl EventProcessorWithJsonOutput {
|
||||
id: item.id,
|
||||
query: item.query,
|
||||
action: match item.action {
|
||||
Some(action) => serde_json::from_value(
|
||||
serde_json::to_value(action).unwrap_or_else(|_| json!("other")),
|
||||
)
|
||||
.unwrap_or(WebSearchAction::Other),
|
||||
None => WebSearchAction::Other,
|
||||
Some(ApiWebSearchAction::Search { query, queries }) => {
|
||||
WebSearchAction::Search { query, queries }
|
||||
}
|
||||
Some(ApiWebSearchAction::OpenPage { url }) => {
|
||||
WebSearchAction::OpenPage { url }
|
||||
}
|
||||
Some(ApiWebSearchAction::FindInPage { url, pattern }) => {
|
||||
WebSearchAction::FindInPage { url, pattern }
|
||||
}
|
||||
Some(ApiWebSearchAction::Other) | None => WebSearchAction::Other,
|
||||
},
|
||||
results: item.results,
|
||||
}),
|
||||
}),
|
||||
_ => None,
|
||||
|
||||
@@ -299,6 +299,10 @@ pub struct WebSearchItem {
|
||||
pub id: String,
|
||||
pub query: String,
|
||||
pub action: WebSearchAction,
|
||||
/// Structured results returned by web search, when available.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub results: Option<Vec<JsonValue>>,
|
||||
}
|
||||
|
||||
/// An error notification.
|
||||
|
||||
@@ -399,6 +399,7 @@ fn web_search_completion_preserves_query_and_action() {
|
||||
query: Some("rust async await".to_string()),
|
||||
queries: None,
|
||||
},
|
||||
results: None,
|
||||
}),
|
||||
},
|
||||
})],
|
||||
@@ -407,6 +408,62 @@ fn web_search_completion_preserves_query_and_action() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_page_actions_and_results_survive_json_output() {
|
||||
let url = "https://example.com/docs";
|
||||
for (action, expected_action) in [
|
||||
(
|
||||
ApiWebSearchAction::OpenPage {
|
||||
url: Some(url.to_string()),
|
||||
},
|
||||
json!({"type": "open_page", "url": url}),
|
||||
),
|
||||
(
|
||||
ApiWebSearchAction::FindInPage {
|
||||
url: Some(url.to_string()),
|
||||
pattern: Some("configuration".to_string()),
|
||||
},
|
||||
json!({"type": "find_in_page", "url": url, "pattern": "configuration"}),
|
||||
),
|
||||
] {
|
||||
for results in [
|
||||
None,
|
||||
Some(vec![]),
|
||||
Some(vec![json!({"url": url, "content": "configuration"})]),
|
||||
Some(vec![json!({"url": url, "error": {"status": 404}})]),
|
||||
] {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
let collected = processor.collect_thread_events(ServerNotification::ItemCompleted(
|
||||
ItemCompletedNotification {
|
||||
item: ThreadItem::WebSearch(ApiWebSearchItem {
|
||||
id: "search-1".to_string(),
|
||||
query: url.to_string(),
|
||||
action: Some(action.clone()),
|
||||
results: results.clone(),
|
||||
}),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
completed_at_ms: 0,
|
||||
},
|
||||
));
|
||||
|
||||
let mut expected_item = json!({
|
||||
"id": "search-1",
|
||||
"type": "web_search",
|
||||
"query": url,
|
||||
"action": expected_action,
|
||||
});
|
||||
if let Some(results) = results {
|
||||
expected_item["results"] = json!(results);
|
||||
}
|
||||
assert_eq!(
|
||||
serde_json::to_value(collected.events).expect("serialize web search events"),
|
||||
json!([{"type": "item.completed", "item": expected_item}]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_start_and_completion_reuse_item_id() {
|
||||
let mut processor = EventProcessorWithJsonOutput::new(/*last_message_path*/ None);
|
||||
@@ -451,6 +508,7 @@ fn web_search_start_and_completion_reuse_item_id() {
|
||||
id: "search-1".to_string(),
|
||||
query: String::new(),
|
||||
action: WebSearchAction::Other,
|
||||
results: None,
|
||||
}),
|
||||
},
|
||||
})],
|
||||
@@ -470,6 +528,7 @@ fn web_search_start_and_completion_reuse_item_id() {
|
||||
query: Some("rust async await".to_string()),
|
||||
queries: None,
|
||||
},
|
||||
results: None,
|
||||
}),
|
||||
},
|
||||
})],
|
||||
|
||||
Reference in New Issue
Block a user