diff --git a/codex-rs/exec/src/event_processor_with_jsonl_output.rs b/codex-rs/exec/src/event_processor_with_jsonl_output.rs index 488cbc52e9..73e0c9d911 100644 --- a/codex-rs/exec/src/event_processor_with_jsonl_output.rs +++ b/codex-rs/exec/src/event_processor_with_jsonl_output.rs @@ -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, diff --git a/codex-rs/exec/src/exec_events.rs b/codex-rs/exec/src/exec_events.rs index 30df7f176a..06565c097b 100644 --- a/codex-rs/exec/src/exec_events.rs +++ b/codex-rs/exec/src/exec_events.rs @@ -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>, } /// An error notification. diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index 1586c9303d..ca49011939 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -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, }), }, })],