mirror of
https://github.com/openai/codex.git
synced 2026-05-01 03:42:05 +03:00
feat: search_tool migrate to bring you own tool of Responses API (#14274)
## Why to support a new bring your own search tool in Responses API(https://developers.openai.com/api/docs/guides/tools-tool-search#client-executed-tool-search) we migrating our bm25 search tool to use official way to execute search on client and communicate additional tools to the model. ## What - replace the legacy `search_tool_bm25` flow with client-executed `tool_search` - add protocol, SSE, history, and normalization support for `tool_search_call` and `tool_search_output` - return namespaced Codex Apps search results and wire namespaced follow-up tool calls back into MCP dispatch
This commit is contained in:
@@ -240,6 +240,13 @@ pub enum ResponseInputItem {
|
||||
call_id: String,
|
||||
output: FunctionCallOutputPayload,
|
||||
},
|
||||
ToolSearchOutput {
|
||||
call_id: String,
|
||||
status: String,
|
||||
execution: String,
|
||||
#[ts(type = "unknown[]")]
|
||||
tools: Vec<serde_json::Value>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
@@ -320,12 +327,27 @@ pub enum ResponseItem {
|
||||
#[ts(skip)]
|
||||
id: Option<String>,
|
||||
name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
namespace: Option<String>,
|
||||
// The Responses API returns the function call arguments as a *string* that contains
|
||||
// JSON, not as an already‑parsed object. We keep it as a raw string here and let
|
||||
// Session::handle_function_call parse it into a Value.
|
||||
arguments: String,
|
||||
call_id: String,
|
||||
},
|
||||
ToolSearchCall {
|
||||
#[serde(default, skip_serializing)]
|
||||
#[ts(skip)]
|
||||
id: Option<String>,
|
||||
call_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
status: Option<String>,
|
||||
execution: String,
|
||||
#[ts(type = "unknown")]
|
||||
arguments: serde_json::Value,
|
||||
},
|
||||
// NOTE: The `output` field for `function_call_output` uses a dedicated payload type with
|
||||
// custom serialization. On the wire it is either:
|
||||
// - a plain string (`content`)
|
||||
@@ -354,6 +376,13 @@ pub enum ResponseItem {
|
||||
call_id: String,
|
||||
output: FunctionCallOutputPayload,
|
||||
},
|
||||
ToolSearchOutput {
|
||||
call_id: Option<String>,
|
||||
status: String,
|
||||
execution: String,
|
||||
#[ts(type = "unknown[]")]
|
||||
tools: Vec<serde_json::Value>,
|
||||
},
|
||||
// Emitted by the Responses API when the agent triggers a web search.
|
||||
// Example payload (from SSE `response.output_item.done`):
|
||||
// {
|
||||
@@ -883,6 +912,17 @@ impl From<ResponseInputItem> for ResponseItem {
|
||||
ResponseInputItem::CustomToolCallOutput { call_id, output } => {
|
||||
Self::CustomToolCallOutput { call_id, output }
|
||||
}
|
||||
ResponseInputItem::ToolSearchOutput {
|
||||
call_id,
|
||||
status,
|
||||
execution,
|
||||
tools,
|
||||
} => Self::ToolSearchOutput {
|
||||
call_id: Some(call_id),
|
||||
status,
|
||||
execution,
|
||||
tools,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -988,6 +1028,13 @@ impl From<Vec<UserInput>> for ResponseInputItem {
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
pub struct SearchToolCallParams {
|
||||
pub query: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
/// If the `name` of a `ResponseItem::FunctionCall` is either `container.exec`
|
||||
/// or `shell`, the `arguments` field should deserialize to this struct.
|
||||
@@ -1721,6 +1768,29 @@ mod tests {
|
||||
assert_eq!(text, Some("line 1".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_call_deserializes_optional_namespace() {
|
||||
let item: ResponseItem = serde_json::from_value(serde_json::json!({
|
||||
"type": "function_call",
|
||||
"name": "mcp__codex_apps__gmail_get_recent_emails",
|
||||
"namespace": "mcp__codex_apps__gmail",
|
||||
"arguments": "{\"top_k\":5}",
|
||||
"call_id": "call-1",
|
||||
}))
|
||||
.expect("function_call should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
item,
|
||||
ResponseItem::FunctionCall {
|
||||
id: None,
|
||||
name: "mcp__codex_apps__gmail_get_recent_emails".to_string(),
|
||||
namespace: Some("mcp__codex_apps__gmail".to_string()),
|
||||
arguments: "{\"top_k\":5}".to_string(),
|
||||
call_id: "call-1".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_sandbox_mode_into_developer_instructions() {
|
||||
let workspace_write: DeveloperInstructions = SandboxMode::WorkspaceWrite.into();
|
||||
@@ -2193,6 +2263,169 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_search_call_roundtrips() -> Result<()> {
|
||||
let parsed: ResponseItem = serde_json::from_str(
|
||||
r#"{
|
||||
"type": "tool_search_call",
|
||||
"call_id": "search-1",
|
||||
"execution": "client",
|
||||
"arguments": {
|
||||
"query": "calendar create",
|
||||
"limit": 1
|
||||
}
|
||||
}"#,
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ResponseItem::ToolSearchCall {
|
||||
id: None,
|
||||
call_id: Some("search-1".to_string()),
|
||||
status: None,
|
||||
execution: "client".to_string(),
|
||||
arguments: serde_json::json!({
|
||||
"query": "calendar create",
|
||||
"limit": 1,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(&parsed)?,
|
||||
serde_json::json!({
|
||||
"type": "tool_search_call",
|
||||
"call_id": "search-1",
|
||||
"execution": "client",
|
||||
"arguments": {
|
||||
"query": "calendar create",
|
||||
"limit": 1,
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_search_output_roundtrips() -> Result<()> {
|
||||
let input = ResponseInputItem::ToolSearchOutput {
|
||||
call_id: "search-1".to_string(),
|
||||
status: "completed".to_string(),
|
||||
execution: "client".to_string(),
|
||||
tools: vec![serde_json::json!({
|
||||
"type": "function",
|
||||
"name": "mcp__codex_apps__calendar_create_event",
|
||||
"description": "Create a calendar event.",
|
||||
"defer_loading": true,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"}
|
||||
},
|
||||
"required": ["title"],
|
||||
"additionalProperties": false,
|
||||
}
|
||||
})],
|
||||
};
|
||||
assert_eq!(
|
||||
ResponseItem::from(input.clone()),
|
||||
ResponseItem::ToolSearchOutput {
|
||||
call_id: Some("search-1".to_string()),
|
||||
status: "completed".to_string(),
|
||||
execution: "client".to_string(),
|
||||
tools: vec![serde_json::json!({
|
||||
"type": "function",
|
||||
"name": "mcp__codex_apps__calendar_create_event",
|
||||
"description": "Create a calendar event.",
|
||||
"defer_loading": true,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"}
|
||||
},
|
||||
"required": ["title"],
|
||||
"additionalProperties": false,
|
||||
}
|
||||
})],
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(input)?,
|
||||
serde_json::json!({
|
||||
"type": "tool_search_output",
|
||||
"call_id": "search-1",
|
||||
"status": "completed",
|
||||
"execution": "client",
|
||||
"tools": [{
|
||||
"type": "function",
|
||||
"name": "mcp__codex_apps__calendar_create_event",
|
||||
"description": "Create a calendar event.",
|
||||
"defer_loading": true,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string"}
|
||||
},
|
||||
"required": ["title"],
|
||||
"additionalProperties": false,
|
||||
}
|
||||
}]
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_search_server_items_allow_null_call_id() -> Result<()> {
|
||||
let parsed_call: ResponseItem = serde_json::from_str(
|
||||
r#"{
|
||||
"type": "tool_search_call",
|
||||
"execution": "server",
|
||||
"call_id": null,
|
||||
"status": "completed",
|
||||
"arguments": {
|
||||
"paths": ["crm"]
|
||||
}
|
||||
}"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
parsed_call,
|
||||
ResponseItem::ToolSearchCall {
|
||||
id: None,
|
||||
call_id: None,
|
||||
status: Some("completed".to_string()),
|
||||
execution: "server".to_string(),
|
||||
arguments: serde_json::json!({
|
||||
"paths": ["crm"],
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
let parsed_output: ResponseItem = serde_json::from_str(
|
||||
r#"{
|
||||
"type": "tool_search_output",
|
||||
"execution": "server",
|
||||
"call_id": null,
|
||||
"status": "completed",
|
||||
"tools": []
|
||||
}"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
parsed_output,
|
||||
ResponseItem::ToolSearchOutput {
|
||||
call_id: None,
|
||||
status: "completed".to_string(),
|
||||
execution: "server".to_string(),
|
||||
tools: vec![],
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_remote_and_local_images_share_label_sequence() -> Result<()> {
|
||||
let image_url = "data:image/png;base64,abc".to_string();
|
||||
|
||||
Reference in New Issue
Block a user