standalone websearch extension (#23823)

## Summary

Add the extension-backed standalone `web.run` tool so Codex can call the
standalone search endpoint through the `codex-api` search client and
return its encrypted output to Responses.

- gate the new tool behind `standalone_web_search`
- install the extension in the app-server thread registry and hide
hosted `web_search` when standalone search is enabled for OpenAI
providers so the two paths stay mutually exclusive
- build search context from persisted history using a small tail
heuristic: previous user message, assistant text between the last two
user turns capped at about 1k tokens, and current user message

## Test Plan

- `cargo test -p codex-web-search-extension`
- `cargo test -p codex-api`
- `cargo test -p codex-core
hosted_tools_follow_provider_auth_model_and_config_gates`
This commit is contained in:
sayan-oai
2026-05-26 11:12:24 -07:00
committed by GitHub
Unverified
parent aad59a0916
commit a22706dfae
26 changed files with 1238 additions and 22 deletions
+3
View File
@@ -8,6 +8,7 @@ mod image_detail;
mod json_schema;
mod mcp_tool;
mod request_plugin_install;
mod response_history;
mod responses_api;
mod tool_call;
mod tool_config;
@@ -45,6 +46,8 @@ pub use request_plugin_install::RequestPluginInstallResult;
pub use request_plugin_install::all_requested_connectors_picked_up;
pub use request_plugin_install::build_request_plugin_install_elicitation_request;
pub use request_plugin_install::verified_connector_install_completed;
pub use response_history::retain_tail_from_last_n_user_messages;
pub use response_history::truncate_assistant_output_text_to_token_budget;
pub use responses_api::FreeformTool;
pub use responses_api::FreeformToolFormat;
pub use responses_api::LoadableToolSpec;
+149
View File
@@ -0,0 +1,149 @@
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_utils_output_truncation::TruncationPolicy;
use codex_utils_output_truncation::approx_token_count;
use codex_utils_output_truncation::truncate_text;
/// Retains items from the earliest of the last `user_message_count` user
/// messages through the latest user message.
pub fn retain_tail_from_last_n_user_messages(
items: &mut Vec<ResponseItem>,
user_message_count: usize,
) {
if user_message_count == 0 {
items.clear();
return;
}
let Some(latest_user_idx) = items.iter().rposition(ResponseItem::is_user_message) else {
items.clear();
return;
};
items.truncate(latest_user_idx + 1);
let earliest_retained_user_idx = items
.iter()
.enumerate()
.rev()
.filter(|(_, item)| item.is_user_message())
.take(user_message_count)
.last()
.map(|(idx, _)| idx)
.unwrap_or(latest_user_idx);
items.drain(..earliest_retained_user_idx);
}
/// Truncates assistant output text to a shared token budget across items.
pub fn truncate_assistant_output_text_to_token_budget(
items: &mut Vec<ResponseItem>,
max_tokens: usize,
) {
let mut remaining_budget = max_tokens;
items.retain_mut(|item| {
let ResponseItem::Message { role, content, .. } = item else {
return true;
};
if role != "assistant" {
return true;
}
content.retain_mut(|content_item| {
let ContentItem::OutputText { text } = content_item else {
return true;
};
if remaining_budget == 0 {
return false;
}
let token_count = approx_token_count(text);
if token_count <= remaining_budget {
remaining_budget = remaining_budget.saturating_sub(token_count);
return true;
}
*text = truncate_text(text, TruncationPolicy::Tokens(remaining_budget));
remaining_budget = 0;
true
});
!content.is_empty()
});
}
#[cfg(test)]
mod tests {
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_utils_output_truncation::TruncationPolicy;
use codex_utils_output_truncation::truncate_text;
use pretty_assertions::assert_eq;
use super::retain_tail_from_last_n_user_messages;
use super::truncate_assistant_output_text_to_token_budget;
fn message(role: &str, text: &str) -> ResponseItem {
ResponseItem::Message {
id: None,
role: role.to_string(),
content: vec![if role == "assistant" {
ContentItem::OutputText {
text: text.to_string(),
}
} else {
ContentItem::InputText {
text: text.to_string(),
}
}],
phase: None,
}
}
#[test]
fn retains_tail_through_latest_user_message() {
let mut items = vec![
message("system", "system"),
message("user", "old user"),
message("assistant", "old assistant"),
message("user", "previous user"),
message("assistant", "previous assistant"),
message("user", "current user"),
message("assistant", "later assistant"),
];
retain_tail_from_last_n_user_messages(&mut items, /*user_message_count*/ 2);
assert_eq!(
items,
vec![
message("user", "previous user"),
message("assistant", "previous assistant"),
message("user", "current user"),
]
);
}
#[test]
fn truncates_assistant_output_text_across_items() {
let long_assistant = "a".repeat(16);
let mut items = vec![
message("user", "previous user"),
message("assistant", &long_assistant),
message("assistant", "after budget"),
message("user", "current user"),
];
truncate_assistant_output_text_to_token_budget(&mut items, /*max_tokens*/ 2);
assert_eq!(
items,
vec![
message("user", "previous user"),
message(
"assistant",
&truncate_text(&long_assistant, TruncationPolicy::Tokens(2)),
),
message("user", "current user"),
]
);
}
}