mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
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:
@@ -4421,6 +4421,7 @@ async fn turn_event_counts_completed_tool_items() {
|
||||
id: "web-1".to_string(),
|
||||
query: "codex".to_string(),
|
||||
action: None,
|
||||
results: None,
|
||||
}),
|
||||
ThreadItem::ImageGeneration(ImageGenerationItem {
|
||||
id: "image-1".to_string(),
|
||||
|
||||
@@ -4511,6 +4511,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
+9
@@ -18551,6 +18551,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
+9
@@ -16330,6 +16330,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1102,6 +1102,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1102,6 +1102,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1247,6 +1247,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1775,6 +1775,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1566,6 +1566,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
+9
@@ -1566,6 +1566,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1566,6 +1566,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1775,6 +1775,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1566,6 +1566,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1775,6 +1775,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1566,6 +1566,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1566,6 +1566,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1247,6 +1247,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1247,6 +1247,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1247,6 +1247,15 @@
|
||||
"query": {
|
||||
"type": "string"
|
||||
},
|
||||
"results": {
|
||||
"default": null,
|
||||
"description": "Structured search results returned out-of-band by standalone web search.\n\nThese stay as opaque JSON at the extension/app-server boundary so new result fields and result types can pass through without a Codex release.",
|
||||
"items": true,
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"webSearch"
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { JsonValue } from "./serde_json/JsonValue";
|
||||
import type { WebSearchAction } from "./v2/WebSearchAction";
|
||||
|
||||
export type WebSearchItem = { id: string, query: string, action: WebSearchAction | null, };
|
||||
export type WebSearchItem = { id: string, query: string, action: WebSearchAction | null,
|
||||
/**
|
||||
* Structured search results returned out-of-band by standalone web search.
|
||||
*
|
||||
* These stay as opaque JSON at the extension/app-server boundary so new
|
||||
* result fields and result types can pass through without a Codex release.
|
||||
*/
|
||||
results: Array<JsonValue> | null, };
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -1375,7 +1375,7 @@ Today both notifications carry an empty `items` array even when item events were
|
||||
- `fileChange` — `{id, changes, status}` describing proposed edits; `changes` list `{path, kind, diff}` and `status` is `inProgress`, `completed`, `failed`, or `declined`.
|
||||
- `mcpToolCall` — `{id, server, tool, status, arguments, appContext, mcpAppResourceUri?, pluginId, result?, error?}` describing MCP calls; `appContext` is `{connectorId, linkId, resourceUri, appName, templateId, actionName}` for calls through a trusted MCP app, where `connectorId` identifies the connector that owns the tool, `linkId` identifies the app link, `resourceUri` points to the widget template, `appName` is the connector's display name, `templateId` identifies the app template, and `actionName` is the stable connector `Action.name`. `appName`, `templateId`, and `actionName` may be null for older rollout entries. The top-level `mcpAppResourceUri` is deprecated and temporarily duplicated for client migration. `tool` identifies the raw MCP tool. `status` is `inProgress`, `completed`, or `failed`.
|
||||
- `collabToolCall` — `{id, tool, status, senderThreadId, receiverThreadId?, newThreadId?, prompt?, agentStatus?}` describing collab tool calls (`spawn_agent`, `send_input`, `resume_agent`, `wait`, `close_agent`); `status` is `inProgress`, `completed`, or `failed`.
|
||||
- `webSearch` — `{id, query, action?}` for a web search request issued by the agent; `action` mirrors the Responses API web_search action payload (`search`, `open_page`, `find_in_page`) and may be omitted until completion.
|
||||
- `webSearch` — `{id, query, action?, results?}` for a web search request issued by the agent; `action` mirrors the Responses API web_search action payload (`search`, `open_page`, `find_in_page`) and may be omitted until completion. For standalone web search, `results` contains the out-of-band structured result DTOs returned by `/v1/alpha/search`; clients should ignore result types and fields they do not understand.
|
||||
- `imageView` — `{id, path}` emitted when the agent invokes the image viewer tool.
|
||||
- `sleep` — `{id, durationMs}` emitted while the agent waits for a duration or new input.
|
||||
- `enteredReviewMode` — `{id, review}` sent when the reviewer starts; `review` is a short user-facing label such as `"current changes"` or the requested target description.
|
||||
|
||||
@@ -162,6 +162,10 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
|
||||
let search_body = search_request
|
||||
.body_json::<Value>()
|
||||
.context("search request body should be JSON")?;
|
||||
assert!(
|
||||
search_body.get("result_fields").is_none(),
|
||||
"standalone search should use the endpoint's default result projection"
|
||||
);
|
||||
assert_eq!(search_body["model"], json!("mock-model"));
|
||||
assert_eq!(
|
||||
search_body["commands"],
|
||||
@@ -228,6 +232,7 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
|
||||
id: call_id.to_string(),
|
||||
query: String::new(),
|
||||
action: None,
|
||||
results: None,
|
||||
})
|
||||
);
|
||||
let expected_completed_item = ThreadItem::WebSearch(WebSearchItem {
|
||||
@@ -237,6 +242,14 @@ async fn standalone_web_search_round_trips_output() -> Result<()> {
|
||||
query: Some("standalone web search".to_string()),
|
||||
queries: None,
|
||||
}),
|
||||
results: Some(vec![json!({
|
||||
"type": "text_result",
|
||||
"ref_id": "turn0search0",
|
||||
"url": "https://example.com/search-result",
|
||||
"title": "Search Result",
|
||||
"snippet": "A result snippet",
|
||||
"future_field": {"preserved": true},
|
||||
})]),
|
||||
});
|
||||
assert_eq!(completed.item, expected_completed_item);
|
||||
|
||||
@@ -310,6 +323,14 @@ async fn mount_search_response(server: &MockServer) {
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||
"encrypted_output": "ciphertext",
|
||||
"output": "Search result",
|
||||
"results": [{
|
||||
"type": "text_result",
|
||||
"ref_id": "turn0search0",
|
||||
"url": "https://example.com/search-result",
|
||||
"title": "Search Result",
|
||||
"snippet": "A result snippet",
|
||||
"future_field": {"preserved": true},
|
||||
}],
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(server)
|
||||
|
||||
@@ -139,6 +139,12 @@ mod tests {
|
||||
serde_json::to_vec(&json!({
|
||||
"encrypted_output": "ciphertext",
|
||||
"output": "search result",
|
||||
"results": [{
|
||||
"type": "text_result",
|
||||
"ref_id": "turn0search0",
|
||||
"url": "https://example.com/result",
|
||||
"future_field": {"preserved": true},
|
||||
}],
|
||||
}))
|
||||
.expect("serialize response"),
|
||||
);
|
||||
@@ -209,6 +215,12 @@ mod tests {
|
||||
SearchResponse {
|
||||
encrypted_output: Some("ciphertext".to_string()),
|
||||
output: "search result".to_string(),
|
||||
results: Some(vec![json!({
|
||||
"type": "text_result",
|
||||
"ref_id": "turn0search0",
|
||||
"url": "https://example.com/result",
|
||||
"future_field": {"preserved": true},
|
||||
})]),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -267,4 +279,40 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn search_response_defaults_missing_results_for_older_endpoints() {
|
||||
let response: SearchResponse = serde_json::from_value(json!({
|
||||
"encrypted_output": null,
|
||||
"output": "search result",
|
||||
}))
|
||||
.expect("response without results should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
SearchResponse {
|
||||
encrypted_output: None,
|
||||
output: "search result".to_string(),
|
||||
results: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_response_preserves_supported_empty_results() {
|
||||
let response: SearchResponse = serde_json::from_value(json!({
|
||||
"encrypted_output": null,
|
||||
"output": "search result",
|
||||
"results": [],
|
||||
}))
|
||||
.expect("response with empty results should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
response,
|
||||
SearchResponse {
|
||||
encrypted_output: None,
|
||||
output: "search result".to_string(),
|
||||
results: Some(Vec::new()),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use codex_protocol::models::ResponseItem;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
pub struct SearchRequest {
|
||||
@@ -297,4 +298,8 @@ pub enum AllowedCaller {
|
||||
pub struct SearchResponse {
|
||||
pub encrypted_output: Option<String>,
|
||||
pub output: String,
|
||||
/// Structured result DTOs are passed to clients out-of-band from `output`.
|
||||
/// Keep them opaque here so newer result variants remain forward-compatible.
|
||||
#[serde(default)]
|
||||
pub results: Option<Vec<JsonValue>>,
|
||||
}
|
||||
|
||||
@@ -206,6 +206,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
|
||||
id: id.as_deref().unwrap_or_default().to_string(),
|
||||
query,
|
||||
action,
|
||||
results: None,
|
||||
}))
|
||||
}
|
||||
ResponseItem::ImageGenerationCall {
|
||||
|
||||
@@ -509,6 +509,7 @@ fn parses_web_search_call() {
|
||||
query: Some("weather".to_string()),
|
||||
queries: None,
|
||||
},
|
||||
results: None,
|
||||
}
|
||||
),
|
||||
other => panic!("expected TurnItem::WebSearch, got {other:?}"),
|
||||
@@ -537,6 +538,7 @@ fn parses_web_search_open_page_call() {
|
||||
action: WebSearchAction::OpenPage {
|
||||
url: Some("https://example.com".to_string()),
|
||||
},
|
||||
results: None,
|
||||
}
|
||||
),
|
||||
other => panic!("expected TurnItem::WebSearch, got {other:?}"),
|
||||
@@ -567,6 +569,7 @@ fn parses_web_search_find_in_page_call() {
|
||||
url: Some("https://example.com".to_string()),
|
||||
pattern: Some("needle".to_string()),
|
||||
},
|
||||
results: None,
|
||||
}
|
||||
),
|
||||
other => panic!("expected TurnItem::WebSearch, got {other:?}"),
|
||||
@@ -590,6 +593,7 @@ fn parses_partial_web_search_call_without_action_as_other() {
|
||||
id: "ws_partial".to_string(),
|
||||
query: String::new(),
|
||||
action: WebSearchAction::Other,
|
||||
results: None,
|
||||
}
|
||||
),
|
||||
other => panic!("expected TurnItem::WebSearch, got {other:?}"),
|
||||
|
||||
@@ -272,6 +272,7 @@ mod tests {
|
||||
id: call.call_id.clone(),
|
||||
query: String::new(),
|
||||
action: None,
|
||||
results: None,
|
||||
}),
|
||||
legacy_events: Vec::new(),
|
||||
})
|
||||
@@ -421,6 +422,7 @@ mod tests {
|
||||
id: "call-extension".to_string(),
|
||||
query: String::new(),
|
||||
action: None,
|
||||
results: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -370,6 +370,7 @@ fn web_search_completion_preserves_query_and_action() {
|
||||
query: Some("rust async await".to_string()),
|
||||
queries: None,
|
||||
}),
|
||||
results: None,
|
||||
}),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
@@ -408,6 +409,7 @@ fn web_search_start_and_completion_reuse_item_id() {
|
||||
id: "search-1".to_string(),
|
||||
query: String::new(),
|
||||
action: None,
|
||||
results: None,
|
||||
}),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
@@ -423,6 +425,7 @@ fn web_search_start_and_completion_reuse_item_id() {
|
||||
query: Some("rust async await".to_string()),
|
||||
queries: None,
|
||||
}),
|
||||
results: None,
|
||||
}),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
|
||||
@@ -16,8 +16,8 @@ workspace = true
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
ts-rs = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -46,6 +46,7 @@ fn web_search_item_preserves_stable_wire_shape() {
|
||||
query: Some("docs".to_string()),
|
||||
queries: None,
|
||||
}),
|
||||
results: None,
|
||||
});
|
||||
let value = serde_json::to_value(&item).expect("serialize extension item");
|
||||
|
||||
@@ -60,12 +61,27 @@ fn web_search_item_preserves_stable_wire_shape() {
|
||||
"query": "docs",
|
||||
"queries": null,
|
||||
},
|
||||
"results": null,
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_value::<ExtensionItem>(value).expect("deserialize extension item"),
|
||||
item
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_value::<ExtensionItem>(json!({
|
||||
"kind": "web.search",
|
||||
"id": "search-1",
|
||||
"query": "docs",
|
||||
"action": {
|
||||
"type": "search",
|
||||
"query": "docs",
|
||||
"queries": null,
|
||||
},
|
||||
}))
|
||||
.expect("deserialize legacy extension item without results"),
|
||||
item
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use ts_rs::TS;
|
||||
|
||||
// Standalone web-search item owned by the web extension. This is also the
|
||||
@@ -13,6 +14,12 @@ pub struct WebSearchItem {
|
||||
pub id: String,
|
||||
pub query: String,
|
||||
pub action: Option<WebSearchAction>,
|
||||
/// Structured search results returned out-of-band by standalone web search.
|
||||
///
|
||||
/// These stay as opaque JSON at the extension/app-server boundary so new
|
||||
/// result fields and result types can pass through without a Codex release.
|
||||
#[serde(default)]
|
||||
pub results: Option<Vec<JsonValue>>,
|
||||
}
|
||||
|
||||
// App-server-facing description of the action performed by standalone web search.
|
||||
|
||||
@@ -127,6 +127,7 @@ impl WebSearchTool {
|
||||
id: call.call_id.clone(),
|
||||
query: String::new(),
|
||||
action: None,
|
||||
results: None,
|
||||
},
|
||||
EventMsg::WebSearchBegin(WebSearchBeginEvent {
|
||||
call_id: call.call_id.clone(),
|
||||
@@ -137,6 +138,8 @@ impl WebSearchTool {
|
||||
.search(&request, extra_headers)
|
||||
.await
|
||||
.map_err(|err| FunctionCallError::Fatal(err.to_string()))?;
|
||||
let output = response.output;
|
||||
let results = response.results;
|
||||
let legacy_action = match &command_action {
|
||||
WebSearchAction::Search { query, queries } => CoreWebSearchAction::Search {
|
||||
query: query.clone(),
|
||||
@@ -156,16 +159,18 @@ impl WebSearchTool {
|
||||
id: call.call_id.clone(),
|
||||
query: query.clone(),
|
||||
action: Some(command_action),
|
||||
results: results.clone(),
|
||||
},
|
||||
EventMsg::WebSearchEnd(WebSearchEndEvent {
|
||||
call_id: call.call_id.clone(),
|
||||
query,
|
||||
action: legacy_action,
|
||||
results,
|
||||
}),
|
||||
))
|
||||
.await;
|
||||
|
||||
Ok(Box::new(SearchOutput::new(response.output)))
|
||||
Ok(Box::new(SearchOutput::new(output)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ use quick_xml::se::to_string as to_xml_string;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
@@ -299,6 +300,14 @@ pub struct WebSearchItem {
|
||||
pub id: String,
|
||||
pub query: String,
|
||||
pub action: WebSearchAction,
|
||||
/// Structured search results returned out-of-band by standalone web search.
|
||||
///
|
||||
/// These stay as opaque JSON at the Codex transport boundary so new result
|
||||
/// fields and result types can pass through without changing model-visible
|
||||
/// context or requiring a Codex release.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub results: Option<Vec<JsonValue>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
|
||||
|
||||
@@ -411,6 +411,7 @@ impl WebSearchItem {
|
||||
call_id: self.id.clone(),
|
||||
query: self.query.clone(),
|
||||
action: self.action.clone(),
|
||||
results: self.results.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2462,6 +2462,10 @@ pub struct WebSearchEndEvent {
|
||||
pub call_id: String,
|
||||
pub query: String,
|
||||
pub action: WebSearchAction,
|
||||
/// Structured search results returned out-of-band by standalone web search.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub results: Option<Vec<Value>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
@@ -5102,6 +5106,7 @@ mod tests {
|
||||
query: Some("find docs".into()),
|
||||
queries: None,
|
||||
},
|
||||
results: None,
|
||||
}),
|
||||
started_at_ms: 0,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user