Move tool search metadata onto ToolExecutor (#25684)

Deferred tools need to be searchable even when they are not implemented
inside `codex-core`. Extension-provided tools can be registered for
later discovery, but the search metadata path was still owned by
core-specific runtime hooks, which meant the shared `ToolExecutor`
abstraction could not describe how a deferred extension tool should
appear in `tool_search`.

## Changes

- Move `ToolSearchEntry` and `ToolSearchInfo` into `codex-tools` and
re-export them from the shared tools crate.
- Add a default `ToolExecutor::search_info` implementation that derives
loadable tool-search metadata from function and namespace specs.
- Forward search metadata through extension adapters and exposure
overrides while keeping custom search text/source metadata for dynamic,
MCP, and multi-agent tools.
- Remove the old core-local `tool_search_entry` module now that search
metadata lives with the shared executor APIs.

## Testing

- Added `deferred_extension_tools_are_discoverable_with_tool_search`
coverage in `core/src/tools/spec_plan_tests.rs`.
This commit is contained in:
jif-oai
2026-06-02 00:24:41 +02:00
committed by GitHub
Unverified
parent 8ee49a2f74
commit 8d720feb69
18 changed files with 302 additions and 149 deletions
+13 -13
View File
@@ -9,7 +9,6 @@ use crate::tools::handlers::parse_arguments;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolExposure;
use crate::tools::tool_search_entry::ToolSearchInfo;
use crate::turn_timing::now_unix_timestamp_ms;
use codex_protocol::dynamic_tools::DynamicToolCallRequest;
use codex_protocol::dynamic_tools::DynamicToolResponse;
@@ -20,6 +19,7 @@ use codex_protocol::protocol::EventMsg;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolName;
use codex_tools::ToolSearchInfo;
use codex_tools::ToolSearchSourceInfo;
use codex_tools::ToolSpec;
use codex_tools::default_namespace_description;
@@ -75,6 +75,17 @@ impl ToolExecutor<ToolInvocation> for DynamicToolHandler {
self.exposure
}
fn search_info(&self) -> Option<ToolSearchInfo> {
ToolSearchInfo::from_spec(
self.search_text.clone(),
self.spec(),
Some(ToolSearchSourceInfo {
name: "Dynamic tools".to_string(),
description: Some("Tools provided by the current Codex thread.".to_string()),
}),
)
}
async fn handle(
&self,
invocation: ToolInvocation,
@@ -126,18 +137,7 @@ impl ToolExecutor<ToolInvocation> for DynamicToolHandler {
}
}
impl CoreToolRuntime for DynamicToolHandler {
fn search_info(&self) -> Option<ToolSearchInfo> {
ToolSearchInfo::from_spec(
self.search_text.clone(),
self.spec(),
Some(ToolSearchSourceInfo {
name: "Dynamic tools".to_string(),
description: Some("Tools provided by the current Codex thread.".to_string()),
}),
)
}
}
impl CoreToolRuntime for DynamicToolHandler {}
#[expect(
clippy::await_holding_invalid_type,
@@ -7,6 +7,7 @@ use codex_tools::ExtensionTurnItem;
use codex_tools::ImageGenerationCompletionFuture;
use codex_tools::ToolCall as ExtensionToolCall;
use codex_tools::ToolName;
use codex_tools::ToolSearchInfo;
use codex_tools::ToolSpec;
use codex_tools::TurnItemEmissionFuture;
use codex_tools::TurnItemEmitter;
@@ -49,6 +50,10 @@ impl ToolExecutor<ToolInvocation> for ExtensionToolAdapter {
self.0.supports_parallel_tool_calls()
}
fn search_info(&self) -> Option<ToolSearchInfo> {
self.0.search_info()
}
async fn handle(
&self,
invocation: ToolInvocation,
+27 -27
View File
@@ -15,11 +15,11 @@ use crate::tools::registry::PostToolUsePayload;
use crate::tools::registry::PreToolUsePayload;
use crate::tools::registry::ToolExecutor;
use crate::tools::registry::ToolTelemetryTags;
use crate::tools::tool_search_entry::ToolSearchInfo;
use codex_mcp::ToolInfo;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolName;
use codex_tools::ToolSearchInfo;
use codex_tools::ToolSearchSourceInfo;
use codex_tools::ToolSpec;
use codex_tools::mcp_tool_to_responses_api_tool;
@@ -87,6 +87,32 @@ impl ToolExecutor<ToolInvocation> for McpHandler {
.unwrap_or(false)
}
fn search_info(&self) -> Option<ToolSearchInfo> {
let source_name = self
.tool_info
.connector_name
.as_deref()
.map(str::trim)
.filter(|connector_name| !connector_name.is_empty())
.unwrap_or_else(|| self.tool_info.server_name.trim());
let source_info = (!source_name.is_empty()).then(|| ToolSearchSourceInfo {
name: source_name.to_string(),
description: self
.tool_info
.namespace_description
.as_deref()
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string),
});
ToolSearchInfo::from_spec(
build_mcp_search_text(&self.tool_info),
self.spec(),
source_info,
)
}
async fn handle(
&self,
invocation: ToolInvocation,
@@ -131,32 +157,6 @@ impl ToolExecutor<ToolInvocation> for McpHandler {
}
impl CoreToolRuntime for McpHandler {
fn search_info(&self) -> Option<ToolSearchInfo> {
let source_name = self
.tool_info
.connector_name
.as_deref()
.map(str::trim)
.filter(|connector_name| !connector_name.is_empty())
.unwrap_or_else(|| self.tool_info.server_name.trim());
let source_info = (!source_name.is_empty()).then(|| ToolSearchSourceInfo {
name: source_name.to_string(),
description: self
.tool_info
.namespace_description
.as_deref()
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string),
});
ToolSearchInfo::from_spec(
build_mcp_search_text(&self.tool_info),
self.spec(),
source_info,
)
}
fn telemetry_tags<'a>(
&'a self,
_invocation: &'a ToolInvocation,
@@ -19,7 +19,6 @@ use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
use crate::tools::tool_search_entry::ToolSearchInfo;
use codex_protocol::ThreadId;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::openai_models::ReasoningEffort;
@@ -36,6 +35,7 @@ use codex_protocol::protocol::CollabWaitingBeginEvent;
use codex_protocol::protocol::CollabWaitingEndEvent;
use codex_protocol::user_input::UserInput;
use codex_tools::ToolName;
use codex_tools::ToolSearchInfo;
use codex_tools::ToolSearchSourceInfo;
use serde::Deserialize;
use serde::Serialize;
@@ -16,6 +16,13 @@ impl ToolExecutor<ToolInvocation> for Handler {
create_close_agent_tool_v1()
}
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"close_agent close shutdown stop agent subagent thread status target",
self.spec(),
)
}
async fn handle(
&self,
invocation: ToolInvocation,
@@ -109,13 +116,6 @@ async fn handle_close_agent(
}
impl CoreToolRuntime for Handler {
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"close_agent close shutdown stop agent subagent thread status target",
self.spec(),
)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -17,6 +17,13 @@ impl ToolExecutor<ToolInvocation> for Handler {
create_resume_agent_tool()
}
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"resume_agent resume reopen closed agent subagent thread id target",
self.spec(),
)
}
async fn handle(
&self,
invocation: ToolInvocation,
@@ -135,13 +142,6 @@ async fn handle_resume_agent(
}
impl CoreToolRuntime for Handler {
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"resume_agent resume reopen closed agent subagent thread id target",
self.spec(),
)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -16,6 +16,13 @@ impl ToolExecutor<ToolInvocation> for Handler {
create_send_input_tool_v1()
}
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"send_input send message existing agent subagent follow up interrupt redirect queue target",
self.spec(),
)
}
async fn handle(
&self,
invocation: ToolInvocation,
@@ -91,13 +98,6 @@ impl ToolExecutor<ToolInvocation> for Handler {
}
impl CoreToolRuntime for Handler {
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"send_input send message existing agent subagent follow up interrupt redirect queue target",
self.spec(),
)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -32,6 +32,13 @@ impl ToolExecutor<ToolInvocation> for Handler {
create_spawn_agent_tool_v1(self.options.clone())
}
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"spawn_agent spawn agent subagent sub-agent delegate delegation parallel work worker explorer no-apps fork model reasoning",
self.spec(),
)
}
async fn handle(
&self,
invocation: ToolInvocation,
@@ -204,13 +211,6 @@ async fn handle_spawn_agent(
}
impl CoreToolRuntime for Handler {
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"spawn_agent spawn agent subagent sub-agent delegate delegation parallel work worker explorer no-apps fork model reasoning",
self.spec(),
)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -37,6 +37,13 @@ impl ToolExecutor<ToolInvocation> for Handler {
create_wait_agent_tool_v1(self.options)
}
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"wait_agent wait agent subagent status final result complete timeout targets",
self.spec(),
)
}
async fn handle(
&self,
invocation: ToolInvocation,
@@ -203,13 +210,6 @@ impl ToolExecutor<ToolInvocation> for Handler {
}
impl CoreToolRuntime for Handler {
fn search_info(&self) -> Option<ToolSearchInfo> {
multi_agent_tool_search_info(
"wait_agent wait agent subagent status final result complete timeout targets",
self.spec(),
)
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(payload, ToolPayload::Function { .. })
}
@@ -6,8 +6,6 @@ use crate::tools::context::boxed_tool_output;
use crate::tools::handlers::tool_search_spec::create_tool_search_tool;
use crate::tools::registry::CoreToolRuntime;
use crate::tools::registry::ToolExecutor;
use crate::tools::tool_search_entry::ToolSearchEntry;
use crate::tools::tool_search_entry::ToolSearchInfo;
use bm25::Document;
use bm25::Language;
use bm25::SearchEngine;
@@ -16,6 +14,8 @@ use codex_tools::LoadableToolSpec;
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
use codex_tools::TOOL_SEARCH_TOOL_NAME;
use codex_tools::ToolName;
use codex_tools::ToolSearchEntry;
use codex_tools::ToolSearchInfo;
use codex_tools::ToolSearchSourceInfo;
use codex_tools::ToolSpec;
use codex_tools::coalesce_loadable_tool_specs;
-1
View File
@@ -14,7 +14,6 @@ pub(crate) mod runtimes;
pub(crate) mod sandboxing;
pub(crate) mod spec_plan;
pub(crate) mod tool_dispatch_trace;
pub(crate) mod tool_search_entry;
use std::borrow::Cow;
+5 -9
View File
@@ -24,13 +24,13 @@ use crate::tools::hook_names::HookToolName;
use crate::tools::lifecycle::notify_tool_finish;
use crate::tools::lifecycle::notify_tool_start;
use crate::tools::tool_dispatch_trace::ToolDispatchTrace;
use crate::tools::tool_search_entry::ToolSearchInfo;
use crate::util::error_or_panic;
use codex_extension_api::ToolCallOutcome;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::protocol::EventMsg;
use codex_tools::ToolName;
use codex_tools::ToolSearchInfo;
use codex_tools::ToolSpec;
use futures::future::BoxFuture;
use serde_json::Value;
@@ -46,10 +46,6 @@ pub use codex_tools::ToolExposure;
/// Implementers provide the shared `ToolExecutor` behavior plus optional
/// core-owned metadata for hooks, telemetry, tool search, and argument diffs.
pub(crate) trait CoreToolRuntime: ToolExecutor<ToolInvocation> {
fn search_info(&self) -> Option<ToolSearchInfo> {
None
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
matches!(
payload,
@@ -273,6 +269,10 @@ impl ToolExecutor<ToolInvocation> for ExposureOverride {
self.exposure != ToolExposure::Hidden && self.handler.supports_parallel_tool_calls()
}
fn search_info(&self) -> Option<ToolSearchInfo> {
self.handler.search_info()
}
async fn handle(
&self,
invocation: ToolInvocation,
@@ -282,10 +282,6 @@ impl ToolExecutor<ToolInvocation> for ExposureOverride {
}
impl CoreToolRuntime for ExposureOverride {
fn search_info(&self) -> Option<ToolSearchInfo> {
self.handler.search_info()
}
fn matches_kind(&self, payload: &ToolPayload) -> bool {
self.handler.matches_kind(payload)
}
+5 -4
View File
@@ -72,6 +72,7 @@ use codex_tools::ToolEnvironmentMode;
use codex_tools::ToolExecutor;
use codex_tools::ToolName;
use codex_tools::ToolOutput;
use codex_tools::ToolSearchInfo;
use codex_tools::ToolSpec;
use codex_tools::UnifiedExecShellMode;
use codex_tools::can_request_original_image_detail;
@@ -937,6 +938,10 @@ impl ToolExecutor<ToolInvocation> for MultiAgentV2NamespaceOverride {
self.handler.supports_parallel_tool_calls()
}
fn search_info(&self) -> Option<ToolSearchInfo> {
self.handler.search_info()
}
async fn handle(
&self,
invocation: ToolInvocation,
@@ -950,10 +955,6 @@ impl CoreToolRuntime for MultiAgentV2NamespaceOverride {
self.handler.matches_kind(payload)
}
fn search_info(&self) -> Option<crate::tools::tool_search_entry::ToolSearchInfo> {
self.handler.search_info()
}
fn create_diff_consumer(
&self,
) -> Option<Box<dyn crate::tools::registry::ToolArgumentDiffConsumer>> {
@@ -313,6 +313,44 @@ impl ToolExecutor<ExtensionToolCall> for WebRunExtensionTool {
}
}
struct DeferredExtensionTool;
#[async_trait::async_trait]
impl ToolExecutor<ExtensionToolCall> for DeferredExtensionTool {
fn tool_name(&self) -> ToolName {
ToolName::plain("extension_echo")
}
fn spec(&self) -> ToolSpec {
ToolSpec::Function(ResponsesApiTool {
name: "extension_echo".to_string(),
description: "Echoes arguments through an extension tool.".to_string(),
strict: true,
defer_loading: None,
parameters: codex_tools::JsonSchema::object(
BTreeMap::from([(
"message".to_string(),
codex_tools::JsonSchema::string(/*description*/ None),
)]),
Some(vec!["message".to_string()]),
Some(false.into()),
),
output_schema: None,
})
}
fn exposure(&self) -> ToolExposure {
ToolExposure::Deferred
}
async fn handle(
&self,
_call: ExtensionToolCall,
) -> Result<Box<dyn ToolOutput>, codex_tools::FunctionCallError> {
panic!("spec planning should not execute extension tools")
}
}
fn duplicate_primary_environment(turn: &mut TurnContext) {
let mut second_environment = turn.environments.turn_environments[0].clone();
second_environment.environment_id = "secondary".to_string();
@@ -702,6 +740,25 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
]);
}
#[tokio::test]
async fn deferred_extension_tools_are_discoverable_with_tool_search() {
let plan = probe_with(
|turn| {
turn.model_info.supports_search_tool = true;
},
ToolPlanInputs {
extension_tool_executors: vec![Arc::new(DeferredExtensionTool)],
..ToolPlanInputs::default()
},
)
.await;
plan.assert_visible_contains(&["tool_search"]);
plan.assert_visible_lacks(&["extension_echo"]);
plan.assert_registered_contains(&["extension_echo"]);
assert_eq!(plan.exposure("extension_echo"), ToolExposure::Deferred);
}
#[tokio::test]
async fn invalid_mcp_tools_are_not_registered() {
let plan = probe_with(
@@ -1,56 +0,0 @@
use codex_tools::LoadableToolSpec;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolSearchSourceInfo;
use codex_tools::ToolSpec;
use codex_tools::default_namespace_description;
#[derive(Clone)]
pub(crate) struct ToolSearchEntry {
pub(crate) search_text: String,
pub(crate) output: LoadableToolSpec,
}
#[derive(Clone)]
pub(crate) struct ToolSearchInfo {
pub(crate) entry: ToolSearchEntry,
pub(crate) source_info: Option<ToolSearchSourceInfo>,
}
impl ToolSearchInfo {
pub(crate) fn from_spec(
search_text: String,
spec: ToolSpec,
source_info: Option<ToolSearchSourceInfo>,
) -> Option<Self> {
let output = match spec {
ToolSpec::Function(mut tool) => {
tool.defer_loading = Some(true);
tool.output_schema = None;
LoadableToolSpec::Function(tool)
}
ToolSpec::Namespace(mut namespace) => {
if namespace.description.trim().is_empty() {
namespace.description = default_namespace_description(&namespace.name);
}
for tool in &mut namespace.tools {
let ResponsesApiNamespaceTool::Function(tool) = tool;
tool.defer_loading = Some(true);
tool.output_schema = None;
}
LoadableToolSpec::Namespace(namespace)
}
ToolSpec::ToolSearch { .. }
| ToolSpec::ImageGeneration { .. }
| ToolSpec::WebSearch { .. }
| ToolSpec::Freeform(_) => return None,
};
Some(Self {
entry: ToolSearchEntry {
search_text,
output,
},
source_info,
})
}
}
+4
View File
@@ -17,6 +17,7 @@ mod tool_discovery;
mod tool_executor;
mod tool_output;
mod tool_payload;
mod tool_search;
mod tool_spec;
pub use code_mode::augment_tool_spec_for_code_mode;
@@ -97,6 +98,9 @@ pub use tool_executor::ToolExposure;
pub use tool_output::JsonToolOutput;
pub use tool_output::ToolOutput;
pub use tool_payload::ToolPayload;
pub use tool_search::ToolSearchEntry;
pub use tool_search::ToolSearchInfo;
pub use tool_search::default_tool_search_text;
pub use tool_spec::ResponsesApiWebSearchFilters;
pub use tool_spec::ResponsesApiWebSearchUserLocation;
pub use tool_spec::ToolSpec;
+9 -1
View File
@@ -1,6 +1,7 @@
use crate::FunctionCallError;
use crate::ToolName;
use crate::ToolOutput;
use crate::ToolSearchInfo;
use crate::ToolSpec;
/// Controls where a tool is exposed to the model.
@@ -13,7 +14,9 @@ pub enum ToolExposure {
Direct,
/// Register this tool for later discovery, but omit it from the initial
/// model-visible tool list.
/// model-visible tool list. Deferred tools must provide search metadata via
/// [`ToolExecutor::search_info`]. The default implementation derives
/// metadata from function and namespace specs.
Deferred,
/// Include this tool in the initial model-visible tool list only.
@@ -48,6 +51,11 @@ pub trait ToolExecutor<Invocation>: Send + Sync {
ToolExposure::Direct
}
fn search_info(&self) -> Option<ToolSearchInfo> {
let spec = self.spec();
ToolSearchInfo::from_tool_spec(&self.tool_name(), spec, /*source_info*/ None)
}
fn supports_parallel_tool_calls(&self) -> bool {
false
}
+139
View File
@@ -0,0 +1,139 @@
use crate::JsonSchema;
use crate::LoadableToolSpec;
use crate::ResponsesApiNamespaceTool;
use crate::ResponsesApiTool;
use crate::ToolName;
use crate::ToolSearchSourceInfo;
use crate::ToolSpec;
use crate::default_namespace_description;
#[derive(Clone)]
pub struct ToolSearchEntry {
pub search_text: String,
pub output: LoadableToolSpec,
}
#[derive(Clone)]
pub struct ToolSearchInfo {
pub entry: ToolSearchEntry,
pub source_info: Option<ToolSearchSourceInfo>,
}
impl ToolSearchInfo {
pub fn from_tool_spec(
tool_name: &ToolName,
spec: ToolSpec,
source_info: Option<ToolSearchSourceInfo>,
) -> Option<Self> {
let search_text = default_tool_search_text(tool_name, &spec);
Self::from_spec(search_text, spec, source_info)
}
pub fn from_spec(
search_text: String,
spec: ToolSpec,
source_info: Option<ToolSearchSourceInfo>,
) -> Option<Self> {
let output = match spec {
ToolSpec::Function(mut tool) => {
tool.defer_loading = Some(true);
tool.output_schema = None;
LoadableToolSpec::Function(tool)
}
ToolSpec::Namespace(mut namespace) => {
if namespace.description.trim().is_empty() {
namespace.description = default_namespace_description(&namespace.name);
}
for tool in &mut namespace.tools {
let ResponsesApiNamespaceTool::Function(tool) = tool;
tool.defer_loading = Some(true);
tool.output_schema = None;
}
LoadableToolSpec::Namespace(namespace)
}
ToolSpec::ToolSearch { .. }
| ToolSpec::ImageGeneration { .. }
| ToolSpec::WebSearch { .. }
| ToolSpec::Freeform(_) => return None,
};
Some(Self {
entry: ToolSearchEntry {
search_text,
output,
},
source_info,
})
}
}
pub fn default_tool_search_text(tool_name: &ToolName, spec: &ToolSpec) -> String {
let mut parts = Vec::new();
push_search_part(&mut parts, tool_name.to_string());
push_search_part(&mut parts, tool_name.name.replace('_', " "));
if let Some(namespace) = &tool_name.namespace {
push_search_part(&mut parts, namespace.clone());
}
match spec {
ToolSpec::Function(tool) => append_function_search_text(tool, &mut parts),
ToolSpec::Namespace(namespace) => {
push_search_part(&mut parts, namespace.name.clone());
push_search_part(&mut parts, namespace.description.clone());
for tool in &namespace.tools {
let ResponsesApiNamespaceTool::Function(tool) = tool;
append_function_search_text(tool, &mut parts);
}
}
ToolSpec::ToolSearch { description, .. } => {
push_search_part(&mut parts, description.clone());
}
ToolSpec::ImageGeneration { .. } => {
push_search_part(&mut parts, "image generation".to_string());
}
ToolSpec::WebSearch { .. } => {
push_search_part(&mut parts, "web search".to_string());
}
ToolSpec::Freeform(tool) => {
push_search_part(&mut parts, tool.name.clone());
push_search_part(&mut parts, tool.description.clone());
push_search_part(&mut parts, tool.format.syntax.clone());
}
}
parts.join(" ")
}
fn append_function_search_text(tool: &ResponsesApiTool, parts: &mut Vec<String>) {
push_search_part(parts, tool.name.clone());
push_search_part(parts, tool.name.replace('_', " "));
push_search_part(parts, tool.description.clone());
append_schema_search_text(&tool.parameters, parts);
}
fn append_schema_search_text(schema: &JsonSchema, parts: &mut Vec<String>) {
if let Some(description) = &schema.description {
push_search_part(parts, description.clone());
}
if let Some(properties) = &schema.properties {
for (name, schema) in properties {
push_search_part(parts, name.clone());
append_schema_search_text(schema, parts);
}
}
if let Some(items) = &schema.items {
append_schema_search_text(items, parts);
}
if let Some(variants) = &schema.any_of {
for variant in variants {
append_schema_search_text(variant, parts);
}
}
}
fn push_search_part(parts: &mut Vec<String>, part: String) {
let part = part.trim();
if !part.is_empty() {
parts.push(part.to_string());
}
}