Support model-specific function description prefixes (#51930)

## What changed

Add `functions_namespace_functions_description_prefixes` to model tool messages, keyed by unqualified tool name. Prepend this guidance to descriptions in the `functions` namespace across direct declarations, Code Mode documentation, and tool-search results, including custom tools.

Trim prefixes, ignore empty values and unavailable functions, and enforce a combined 256-byte UTF-8 limit including separators for reachable tools. Keep overrides local to each step, preserve search ranking text, and retain runtime caches for unaffected tools.

## Testing

Extend coverage for direct and indirect descriptions, clearing prefixes when switching models, namespace scoping for deferred dynamic tools, and tool-search cache reuse and reset.

GitOrigin-RevId: f434e37cec010764fe3e09fb12c339e933b3c21c
This commit is contained in:
tongzhou wang
2026-10-08 03:20:54 +00:00
committed by copyberry
parent 82e70121f8
commit e974aad3b1
15 changed files with 527 additions and 134 deletions
+47 -15
View File
@@ -74,11 +74,11 @@ impl ToolSearchHandlerCache {
.entries()
.filter(|tool| tool.exposure.is_deferred())
.filter_map(|tool| {
if tool.runtime.immutable_spec().is_some() {
Some(ToolSearchSource::Immutable(Arc::downgrade(&tool.runtime)))
if let Some(runtime) = tool.cached_runtime() {
Some(ToolSearchSource::Immutable(Arc::downgrade(runtime)))
} else {
tool.runtime
.search_info()
registry
.search_info(tool)
.map(Box::new)
.map(ToolSearchSource::Dynamic)
}
@@ -313,12 +313,12 @@ impl ToolSearchHandler {
else {
continue;
};
let Some(runtime) = router.tool_runtime(tool_name) else {
let Some(tool) = router.registered_tool(tool_name) else {
continue;
};
let definitions = prepare_code_mode_tool_definitions(
Some(runtime.as_ref()),
|| Cow::Owned(runtime.spec()),
tool.cached_runtime().map(Arc::as_ref),
|| Cow::Owned(Arc::unwrap_or_clone(tool.spec())),
code_mode_input_schema_max_bytes,
tool_description_first,
);
@@ -375,10 +375,10 @@ mod tests {
#[test]
fn cache_reuses_immutable_handlers_and_rebuilds_for_current_registry_changes() {
let cache = ToolSearchHandlerCache::default();
let runtime: Arc<dyn CoreToolRuntime> = Arc::new(
McpHandler::new(tool_info("calendar", "create_event", "Create events"))
.expect("MCP tool should convert"),
);
let mut info = tool_info("calendar", "create_event", "Create events");
info.callable_namespace = "functions".to_string();
let runtime: Arc<dyn CoreToolRuntime> =
Arc::new(McpHandler::new(info.clone()).expect("MCP tool should convert"));
let mut registry = ToolRegistry::default();
registry.register_trusted_with_exposure(Arc::clone(&runtime), ToolExposure::Deferred);
@@ -390,14 +390,46 @@ mod tests {
assert!(!Arc::ptr_eq(&first, &without_sources));
let mut replacement_registry = ToolRegistry::default();
let replacement = Arc::new(
McpHandler::new(tool_info("calendar", "create_event", "Create events"))
.expect("replacement MCP tool should convert"),
);
let replacement =
Arc::new(McpHandler::new(info).expect("replacement MCP tool should convert"));
replacement_registry.register_trusted_with_exposure(replacement, ToolExposure::Deferred);
let replacement = cache.get_or_build(&replacement_registry, ToolSearchSourceListing::Omit);
assert!(!Arc::ptr_eq(&without_sources, &replacement));
registry
.apply_functions_namespace_function_prefixes(
Some(&std::collections::BTreeMap::from([(
"create_event".to_string(),
"Guidance.".to_string(),
)])),
[ToolName::plain("create_event")],
)
.expect("valid function prefixes");
let prefixed = cache.get_or_build(&registry, ToolSearchSourceListing::Include);
let reused = cache.get_or_build(&registry, ToolSearchSourceListing::Include);
assert!(Arc::ptr_eq(&prefixed, &reused));
assert_eq!(
prefixed.search_infos[0].entry.search_text,
first.search_infos[0].entry.search_text
);
let mut expected = first.search("Create events", /*limit*/ 1);
let LoadableToolSpec::Namespace(namespace) = &mut expected[0] else {
panic!("namespace")
};
let ResponsesApiNamespaceTool::Function(tool) = &mut namespace.tools[0] else {
panic!("function")
};
tool.description = format!("Guidance.\n\n{}", tool.description);
assert_eq!(prefixed.search("Create events", /*limit*/ 1), expected);
registry
.apply_functions_namespace_function_prefixes(/*prefixes*/ None, [])
.expect("clear prefixes");
let cleared = cache.get_or_build(&registry, ToolSearchSourceListing::Include);
assert_eq!(
cleared.search("Create events", /*limit*/ 1),
first.search("Create events", /*limit*/ 1)
);
let mut disabled_registry = ToolRegistry::default();
disabled_registry.register_trusted_with_exposure(runtime, ToolExposure::Direct);
let disabled = cache.get_or_build(&disabled_registry, ToolSearchSourceListing::Omit);
+3 -1
View File
@@ -139,7 +139,9 @@ impl ToolCallRuntime {
.record_tool_call(&call, &source, &step_context);
let router = &step_context.tool_router;
let supports_parallel = router.tool_supports_parallel(&call);
let tool_runtime = router.tool_runtime(&call.tool_name);
let tool_runtime = router
.registered_tool(&call.tool_name)
.map(|tool| Arc::clone(&tool.runtime));
let finishes_on_cancellation = tool_runtime
.as_ref()
.is_some_and(|runtime| runtime.finishes_on_cancellation());
+80 -6
View File
@@ -38,7 +38,9 @@ use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::EventMsg;
use codex_rollout::state_db;
use codex_shell_command::parse_command::parse_shell_script;
use codex_tools::FunctionsNamespaceFunctionPrefixes;
use codex_tools::ToolName;
use codex_tools::ToolSearchInfo;
use codex_tools::ToolSpec;
use futures::future::BoxFuture;
use indexmap::IndexMap;
@@ -303,16 +305,83 @@ pub(crate) struct PostToolUsePayload {
pub(crate) struct RegisteredTool {
pub(crate) runtime: Arc<dyn CoreToolRuntime>,
pub(crate) exposure: ToolExposure,
/// Per-step description override; never written back to a shared runtime.
pub(crate) model_spec: Option<Arc<ToolSpec>>,
}
impl RegisteredTool {
/// Direct declarations and Code Mode read this spec; execution uses the runtime.
pub(crate) fn spec(&self) -> Arc<ToolSpec> {
self.model_spec
.as_ref()
.or_else(|| self.runtime.immutable_spec())
.cloned()
.unwrap_or_else(|| Arc::new(self.runtime.spec()))
}
/// Runtime caches describe the original spec, so cannot serve an overridden one.
pub(crate) fn cached_runtime(&self) -> Option<&Arc<dyn CoreToolRuntime>> {
(self.model_spec.is_none() && self.runtime.immutable_spec().is_some())
.then_some(&self.runtime)
}
}
#[derive(Default)]
pub struct ToolRegistry {
tools: IndexMap<ToolName, RegisteredTool>,
functions_namespace_function_prefixes: FunctionsNamespaceFunctionPrefixes,
first_collision: Option<ToolName>,
pub(crate) tool_policy: Arc<ToolPolicy>,
}
impl ToolRegistry {
pub(crate) fn search_info(&self, tool: &RegisteredTool) -> Option<ToolSearchInfo> {
let info = tool.runtime.search_info()?;
// A runtime may advertise a different search schema or description. Preserve it,
// along with its ranking text and source, rather than substituting the normal spec.
match self
.functions_namespace_function_prefixes
.prepare(&tool.runtime.tool_name(), || {
info.entry.to_loadable_spec().into()
}) {
Some(spec) => ToolSearchInfo::from_spec(info.entry.search_text, spec, info.source_info),
None => Some(info),
}
}
/// Apply catalog prefixes to this step's specs, budgeting only reachable tools.
/// The planner supplies the names; this does not change execution or exposure.
pub(crate) fn apply_functions_namespace_function_prefixes(
&mut self,
prefixes: Option<&BTreeMap<String, String>>,
available_tools: impl IntoIterator<Item = ToolName>,
) -> Result<(), &'static str> {
let prefixes = FunctionsNamespaceFunctionPrefixes::new(prefixes, available_tools)?;
for tool in self.tools.values_mut() {
tool.model_spec = prefixes
.prepare(&tool.runtime.tool_name(), || tool.runtime.spec())
.map(Arc::new);
}
self.functions_namespace_function_prefixes = prefixes;
Ok(())
}
fn prepare_tool(
&self,
runtime: Arc<dyn CoreToolRuntime>,
exposure: ToolExposure,
) -> RegisteredTool {
let model_spec = self
.functions_namespace_function_prefixes
.prepare(&runtime.tool_name(), || runtime.spec())
.map(Arc::new);
RegisteredTool {
runtime,
exposure,
model_spec,
}
}
pub(crate) fn with_tool_policy(tool_policy: Arc<ToolPolicy>) -> Self {
Self {
tool_policy,
@@ -359,9 +428,10 @@ impl ToolRegistry {
if !self.tool_policy.allows(&tool_name) {
return;
}
let tool = self.prepare_tool(runtime, exposure);
match self.tools.entry(tool_name) {
Entry::Vacant(entry) => {
entry.insert(RegisteredTool { runtime, exposure });
entry.insert(tool);
}
Entry::Occupied(entry) => {
let tool_name = entry.key();
@@ -381,8 +451,8 @@ impl ToolRegistry {
}
let exposure = runtime.exposure();
self.tools
.shift_insert(0, tool_name, RegisteredTool { runtime, exposure });
let tool = self.prepare_tool(runtime, exposure);
self.tools.shift_insert(0, tool_name, tool);
}
pub(crate) fn register_external(&mut self, runtime: Arc<dyn CoreToolRuntime>) -> bool {
@@ -409,9 +479,10 @@ impl ToolRegistry {
return false;
}
let tool = self.prepare_tool(runtime, exposure);
match self.tools.entry(tool_name) {
Entry::Vacant(entry) => {
entry.insert(RegisteredTool { runtime, exposure });
entry.insert(tool);
true
}
Entry::Occupied(entry) => {
@@ -503,11 +574,14 @@ impl ToolRegistry {
}
pub(crate) fn tool(&self, name: &ToolName) -> Option<Arc<dyn CoreToolRuntime>> {
self.tools
.get(&name.clone().with_default_namespace())
self.registered_tool(name)
.map(|tool| Arc::clone(&tool.runtime))
}
pub(crate) fn registered_tool(&self, name: &ToolName) -> Option<&RegisteredTool> {
self.tools.get(&name.clone().with_default_namespace())
}
#[cfg(test)]
pub(crate) fn tool_names_for_test(&self) -> Vec<ToolName> {
let mut names = self.tools.keys().cloned().collect::<Vec<_>>();
+3 -3
View File
@@ -11,7 +11,7 @@ use crate::tools::context::ToolPayload;
#[cfg(test)]
use crate::tools::handlers::ToolSearchHandlerCache;
use crate::tools::registry::AnyToolResult;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::RegisteredTool;
use crate::tools::registry::ToolArgumentDiffConsumer;
use crate::tools::registry::ToolExposure;
use crate::tools::registry::ToolRegistry;
@@ -249,8 +249,8 @@ impl ToolRouter {
.unwrap_or(false)
}
pub(crate) fn tool_runtime(&self, tool_name: &ToolName) -> Option<Arc<dyn CoreToolRuntime>> {
self.registry.tool(tool_name)
pub(crate) fn registered_tool(&self, tool_name: &ToolName) -> Option<&RegisteredTool> {
self.registry.registered_tool(tool_name)
}
#[instrument(level = "trace", skip_all, err)]
+8 -7
View File
@@ -190,8 +190,8 @@ async fn parallel_support_does_not_match_namespaced_local_tool_names() -> anyhow
assert_eq!(
router
.tool_runtime(&ToolName::plain(parallel_tool_name))
.map(|runtime| runtime.tool_name()),
.registered_tool(&ToolName::plain(parallel_tool_name))
.map(|tool| tool.runtime.tool_name()),
Some(ToolName::plain(parallel_tool_name))
);
@@ -362,8 +362,8 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
assert!(router.tool_supports_parallel(&call));
assert_eq!(
router
.tool_runtime(&call.tool_name)
.map(|runtime| runtime.tool_name()),
.registered_tool(&call.tool_name)
.map(|tool| tool.runtime.tool_name()),
Some(call.tool_name.clone())
);
@@ -378,8 +378,8 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
assert!(!router.tool_supports_parallel(&different_server_call));
assert_eq!(
router
.tool_runtime(&different_server_call.tool_name)
.map(|runtime| runtime.tool_name()),
.registered_tool(&different_server_call.tool_name)
.map(|tool| tool.runtime.tool_name()),
Some(different_server_call.tool_name.clone())
);
@@ -392,7 +392,7 @@ async fn mcp_parallel_support_uses_handler_data() -> anyhow::Result<()> {
encrypted_function_args: None,
};
assert!(!router.tool_supports_parallel(&hidden_call));
assert!(router.tool_runtime(&hidden_call.tool_name).is_some());
assert!(router.registered_tool(&hidden_call.tool_name).is_some());
let nested_only_call = ToolCall {
tool_name: ToolName::namespaced("mcp__nested_echo__", "query_with_delay"),
@@ -524,6 +524,7 @@ fn mcp_runtime(tool_info: codex_mcp::ToolInfo) -> RegisteredTool {
RegisteredTool {
exposure: runtime.exposure(),
runtime,
model_spec: None,
}
}
+184 -79
View File
@@ -54,7 +54,6 @@ use crate::tools::handlers::view_image_spec::ViewImageToolOptions;
use crate::tools::hosted_spec::WebSearchToolOptions;
use crate::tools::hosted_spec::create_web_search_tool;
use crate::tools::multi_agent_tool::multi_agent_v2_handler;
#[cfg(test)]
use crate::tools::registry::RegisteredTool;
use crate::tools::registry::ToolExposure;
use crate::tools::registry::ToolRegistry;
@@ -374,6 +373,7 @@ pub(crate) fn finalize_tool_router(
mut hosted_specs: Vec<ToolSpec>,
tool_search_handler_cache: &ToolSearchHandlerCache,
) -> CodexResult<ToolRouter> {
let model_messages = ResolvedModelMessages::from_model(model_info);
hosted_specs.retain(|spec| registry.tool_policy.allows(&ToolName::plain(spec.name())));
apply_direct_model_only_namespace_overrides(turn_context, &mut registry);
enforce_strict_3p_tools(turn_context, model_info, &mut registry);
@@ -390,13 +390,32 @@ pub(crate) fn finalize_tool_router(
}
}
let tool_search_name = ToolName::plain(TOOL_SEARCH_TOOL_NAME);
if model_info.supports_search_tool
let search_exposure = if model_info.supports_search_tool
&& registry.tool_policy.allows(&tool_search_name)
&& registry.entries().any(|tool| {
tool.runtime.tool_name() != tool_search_name
&& tool.exposure.is_deferred()
&& tool.runtime.search_info().is_some()
})
{
}) {
Some(
if turn_context
.config
.features
.enabled(Feature::CodeModeToolSearch)
{
tool_exposure_with_namespace_override(
turn_context,
tool_search_name.clone(),
ToolExposure::Direct,
)
} else {
ToolExposure::Direct
},
)
} else {
None
};
if search_exposure.is_some() {
if registry.remove(&tool_search_name).is_some() {
registry.record_collision(tool_search_name.clone());
}
@@ -423,10 +442,24 @@ pub(crate) fn finalize_tool_router(
registry.record_collision(tool_name);
}
}
append_tool_search_executor(turn_context, &mut registry, tool_search_handler_cache);
}
let model_messages = ResolvedModelMessages::from_model(model_info);
prepare_function_description_prefixes(
turn_context,
model_info,
&mut registry,
search_exposure,
)?;
if let Some(exposure) = search_exposure {
append_tool_search_executor(
turn_context,
&mut registry,
tool_search_handler_cache,
exposure,
);
}
let indirect_prefixes = IndirectNamespacePrefixes::new(
model_messages.indirect_description_prefixes(),
registry.mcp_namespaces(),
@@ -621,16 +654,12 @@ fn build_model_visible_specs(
let mut specs = Vec::new();
for tool in registry.entries() {
let exposure = tool.exposure;
if !exposure.is_direct() {
if !is_directly_visible(turn_context, model_info, tool) {
continue;
}
let tool_name = tool.runtime.tool_name();
if is_hidden_by_code_mode_only(turn_context, model_info, &tool_name, exposure) {
continue;
}
let spec = tool.runtime.spec();
let spec = Arc::unwrap_or_clone(tool.spec());
specs.push(spec_for_model_request(
turn_context,
model_info,
@@ -849,6 +878,20 @@ fn is_hidden_by_code_mode_only(
))
}
fn is_directly_visible(
turn_context: &TurnContext,
model_info: &ModelInfo,
tool: &RegisteredTool,
) -> bool {
tool.exposure.is_direct()
&& !is_hidden_by_code_mode_only(
turn_context,
model_info,
&tool.runtime.tool_name(),
tool.exposure,
)
}
fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName) -> bool {
let tool_name = tool_name.clone().with_default_namespace();
tool_name.namespace.as_ref().is_some_and(|namespace| {
@@ -860,6 +903,131 @@ fn is_excluded_from_code_mode(turn_context: &TurnContext, tool_name: &ToolName)
})
}
fn prepare_function_description_prefixes(
turn_context: &TurnContext,
model_info: &ModelInfo,
registry: &mut ToolRegistry,
search_exposure: Option<ToolExposure>,
) -> CodexResult<()> {
let model_messages = ResolvedModelMessages::from_model(model_info);
let prefixes = model_messages.functions_namespace_functions_description_prefixes();
let mut available_tools = Vec::new();
if prefixes.is_some_and(|prefixes| !prefixes.is_empty()) {
let code_mode_enabled = matches!(
effective_tool_mode(turn_context, model_info),
ToolMode::CodeMode | ToolMode::CodeModeOnly
);
let search_name = ToolName::plain(TOOL_SEARCH_TOOL_NAME);
let search_visible = search_exposure.is_some_and(|exposure| {
!is_hidden_by_code_mode_only(turn_context, model_info, &search_name, exposure)
|| (code_mode_enabled
&& registry
.tool_policy
.allows(&ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME))
&& turn_context
.config
.features
.enabled(Feature::CodeModeToolSearch)
&& exposure.is_available_in_code_mode()
&& !is_excluded_from_code_mode(turn_context, &search_name))
});
available_tools.extend(
registry
.entries()
.filter(|tool| {
is_directly_visible(turn_context, model_info, tool)
|| (search_visible
&& tool.exposure.is_deferred()
&& tool.runtime.search_info().is_some())
})
.map(|tool| tool.runtime.tool_name()),
);
available_tools.extend(
code_mode_tools(turn_context, model_info, registry)
.map(|(_, _, tool)| tool.runtime.tool_name()),
);
// These wrappers are generated after their inner descriptions are ready.
if code_mode_enabled {
available_tools.extend(
[
codex_code_mode::PUBLIC_TOOL_NAME,
codex_code_mode::WAIT_TOOL_NAME,
]
.into_iter()
.map(ToolName::plain)
.filter(|name| registry.tool_policy.allows(name)),
);
}
}
registry
.apply_functions_namespace_function_prefixes(prefixes, available_tools)
.map_err(|error| CodexErrorDetails::InvalidRequest(error.to_owned()).into())
}
/// Select the same nested tools for prefix budgeting and Code Mode construction.
/// A registered tool alone is not reachable when policy disallows the exec wrapper.
fn code_mode_tools<'a>(
turn_context: &'a TurnContext,
model_info: &'a ModelInfo,
registry: &'a ToolRegistry,
) -> impl Iterator<Item = (String, Arc<ToolSpec>, &'a RegisteredTool)> {
let tool_mode = effective_tool_mode(turn_context, model_info);
let enabled = matches!(tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly)
&& registry
.tool_policy
.allows(&ToolName::plain(codex_code_mode::PUBLIC_TOOL_NAME));
let mut seen = HashSet::new();
registry.entries().filter_map(move |tool| {
if !enabled || !tool.exposure.is_available_in_code_mode() {
return None;
}
let tool_name = tool.runtime.tool_name();
// Deferred third-party tools still need an exec route to be discoverable.
if is_excluded_from_code_mode(turn_context, &tool_name)
&& !(code_mode_only_strict_3p_tools(turn_context, model_info)
&& tool.runtime.is_third_party_tool())
{
return None;
}
let spec = tool.spec();
// Derive the name without serializing and augmenting every tool schema.
let code_mode_name = match spec.as_ref() {
ToolSpec::Function(_) | ToolSpec::Freeform(_) => {
codex_tools::code_mode_name_for_tool_name(&tool_name)
}
ToolSpec::Namespace(namespace) if !namespace.tools.is_empty() => {
codex_tools::code_mode_name_for_tool_name(&tool_name)
}
ToolSpec::ToolSearch { .. }
if turn_context
.config
.features
.enabled(Feature::CodeModeToolSearch) =>
{
codex_tools::code_mode_name_for_tool_name(&tool_name)
}
ToolSpec::Namespace(_) | ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } => {
return None;
}
};
if !codex_code_mode::is_code_mode_nested_tool(&code_mode_name) {
return None;
}
let code_mode_name = codex_code_mode::normalize_code_mode_identifier(&code_mode_name);
if !seen.insert(code_mode_name.clone()) {
tracing::warn!(
tool_name = %tool_name,
code_mode_name = %code_mode_name,
"skipping tool with a duplicate normalized code-mode name"
);
return None;
}
Some((code_mode_name, spec, tool))
})
}
fn register_code_mode_executors(
turn_context: &TurnContext,
model_info: &ModelInfo,
@@ -877,64 +1045,10 @@ fn register_code_mode_executors(
let mut deferred_exec_prompt_tool_specs = Vec::new();
let mut included_deferred_mcp_output_schema = false;
let deferred_tools_guidance_enabled = model_info.supports_search_tool;
for tool in registry.entries() {
for (name, spec, tool) in code_mode_tools(turn_context, model_info, registry) {
code_mode_tool_names.insert(name, tool.runtime.tool_name());
let exposure = tool.exposure;
if !exposure.is_available_in_code_mode() {
continue;
}
let tool_name = tool.runtime.tool_name();
// Deferred third-party tools still need an exec route to be discoverable.
if is_excluded_from_code_mode(turn_context, &tool_name)
&& !(code_mode_only_strict_3p_tools(turn_context, model_info)
&& tool.runtime.is_third_party_tool())
{
continue;
}
let immutable_spec = tool.runtime.immutable_spec();
let cached_runtime = immutable_spec.map(|_| Arc::clone(&tool.runtime));
let spec = immutable_spec
.map(Arc::clone)
.unwrap_or_else(|| Arc::new(tool.runtime.spec()));
// Derive the name without serializing and augmenting every tool schema.
let code_mode_name = match spec.as_ref() {
ToolSpec::Function(_) | ToolSpec::Freeform(_) => {
codex_tools::code_mode_name_for_tool_name(&tool_name)
}
ToolSpec::Namespace(namespace) if !namespace.tools.is_empty() => {
codex_tools::code_mode_name_for_tool_name(&tool_name)
}
ToolSpec::ToolSearch { .. }
if turn_context
.config
.features
.enabled(Feature::CodeModeToolSearch) =>
{
codex_tools::code_mode_name_for_tool_name(&tool_name)
}
ToolSpec::Namespace(_) | ToolSpec::ToolSearch { .. } | ToolSpec::WebSearch { .. } => {
continue;
}
};
if !codex_code_mode::is_code_mode_nested_tool(&code_mode_name) {
continue;
}
match code_mode_tool_names.entry(codex_code_mode::normalize_code_mode_identifier(
&code_mode_name,
)) {
Entry::Vacant(entry) => {
entry.insert(tool_name);
}
Entry::Occupied(_) => {
tracing::warn!(
tool_name = %tool_name,
code_mode_name = %code_mode_name,
"skipping tool with a duplicate normalized code-mode name"
);
continue;
}
}
let cached_runtime = tool.cached_runtime().cloned();
if exposure == ToolExposure::Deferred {
if deferred_tools_guidance_enabled
@@ -1542,6 +1656,7 @@ fn append_tool_search_executor(
turn_context: &TurnContext,
registry: &mut ToolRegistry,
tool_search_handler_cache: &ToolSearchHandlerCache,
exposure: ToolExposure,
) {
let source_listing = if turn_context
.config
@@ -1553,16 +1668,6 @@ fn append_tool_search_executor(
ToolSearchSourceListing::Include
};
let handler = tool_search_handler_cache.get_or_build(registry, source_listing);
// Preserve native search's exposure when nested search is disabled.
let exposure = if turn_context
.config
.features
.enabled(Feature::CodeModeToolSearch)
{
tool_exposure_with_namespace_override(turn_context, handler.tool_name(), handler.exposure())
} else {
handler.exposure()
};
registry.register_trusted_with_exposure(handler, exposure);
}
@@ -430,6 +430,7 @@ fn mcp_runtime(
RegisteredTool {
runtime: handler,
exposure,
model_spec: None,
}
}
@@ -1766,6 +1767,7 @@ async fn strict_namespace_ownership_requires_tool_namespace_inventory_opt_in() {
RegisteredTool {
runtime: Arc::new(McpHandler::new(tool).expect("MCP tool spec should build")),
exposure,
model_spec: None,
}
})
.collect();
@@ -2062,6 +2064,7 @@ async fn strict_tool_collisions_allow_multiple_tools_in_one_namespace() {
McpHandler::new(undocumented_tool).expect("MCP tool spec should build"),
),
exposure: ToolExposure::Direct,
model_spec: None,
},
mcp_runtime("shared", "shared", "lookup", ToolExposure::Direct),
mcp_runtime("shared", "shared", "list", ToolExposure::Direct),
@@ -2101,6 +2104,7 @@ async fn relaxed_tool_collisions_preserve_first_nonempty_namespace_description()
RegisteredTool {
runtime: Arc::new(McpHandler::new(tool).expect("MCP tool spec should build")),
exposure: ToolExposure::Direct,
model_spec: None,
}
};
let plan = probe_with(