Skip to main content

vtcode_core/tools/registry/
availability_facade.rs

1//! Tool availability and schema accessors for ToolRegistry.
2
3use crate::config::ToolDocumentationMode;
4use crate::config::constants::tools;
5use crate::config::types::CapabilityLevel;
6use crate::tools::handlers::{SessionSurface, SessionToolsConfig, ToolModelCapabilities};
7use crate::tools::names::canonical_tool_name;
8use serde_json::Value;
9
10use super::ToolRegistry;
11use crate::tools::mcp::legacy_mcp_tool_name;
12
13impl ToolRegistry {
14    fn resolve_fallback_seed_tool(&self, failed_tool: &str) -> String {
15        if let Ok(resolved) = self.resolve_public_tool_name_sync(failed_tool) {
16            return resolved;
17        }
18
19        let lower = failed_tool.trim().to_ascii_lowercase();
20        match lower.as_str() {
21            "exec_code" => tools::EXEC_COMMAND.to_string(),
22            "list_dir" | "list_directory" => lower,
23            _ => {
24                if let Some((_, suffix)) = lower.rsplit_once('.')
25                    && let Ok(resolved) = self.resolve_public_tool_name_sync(suffix)
26                {
27                    return resolved;
28                }
29                lower
30            }
31        }
32    }
33
34    /// Suggest a fallback tool for a failed invocation using lightweight heuristics.
35    pub async fn suggest_fallback_tool(&self, failed_tool: &str) -> Option<String> {
36        let available = self.available_tools().await;
37        let failed = canonical_tool_name(failed_tool);
38        let failed_name = failed;
39        let seed = self.resolve_fallback_seed_tool(failed_name);
40
41        if seed != failed_name && available.iter().any(|tool| tool == &seed) {
42            return Some(seed);
43        }
44
45        let candidates: &[&str] = match seed.as_str() {
46            tools::EXEC_COMMAND => &[],
47            tools::APPLY_PATCH => &[tools::EXEC_COMMAND],
48            // Task trackers require action-specific arguments; generic fallback names
49            // create low-signal retries.
50            tools::TASK_TRACKER => &[],
51            // Unknown tools: prefer no fallback over noisy generic suggestions.
52            _ => &[],
53        };
54
55        for candidate in candidates {
56            if *candidate != failed_name && available.iter().any(|tool| tool == candidate) {
57                return Some((*candidate).to_string());
58            }
59        }
60        None
61    }
62
63    /// Get a list of all available tools, including MCP tools.
64    pub async fn available_tools(&self) -> Vec<String> {
65        // Use try_read to avoid blocking on contested locks
66        if let Some(cache) = self.cached_available_tools.try_read()
67            && let Some(tools) = cache.as_ref()
68        {
69            return tools.clone();
70        }
71
72        let tools = self
73            .public_tool_names(SessionSurface::Interactive, CapabilityLevel::CodeSearch)
74            .await;
75
76        // Update cache with try_write to avoid blocking
77        if let Some(mut cache) = self.cached_available_tools.try_write() {
78            *cache = Some(tools.clone());
79        }
80
81        tools
82    }
83
84    /// Get the schema for a specific tool.
85    pub async fn get_tool_schema(&self, tool_name: &str) -> Option<Value> {
86        if is_removed_default_public_tool(tool_name) {
87            return None;
88        }
89
90        let wrap_schema = |requested_name: &str, description: &str, schema: &Value| {
91            // Wrap in full declaration if it's just parameters
92            if schema.get("properties").is_some() && schema.get("name").is_none() {
93                serde_json::json!({
94                    "name": requested_name,
95                    "description": description,
96                    "parameters": schema
97                })
98            } else {
99                schema.clone()
100            }
101        };
102
103        if let Some(entry) = self
104            .schema_for_public_name(
105                tool_name,
106                SessionToolsConfig::full_public(
107                    SessionSurface::Interactive,
108                    CapabilityLevel::CodeSearch,
109                    ToolDocumentationMode::Full,
110                    ToolModelCapabilities::default(),
111                )
112                .with_planning_active(self.is_planning_active()),
113            )
114            .await
115        {
116            return Some(wrap_schema(entry.name.as_str(), entry.description.as_str(), &entry.parameters));
117        }
118
119        // Resolve tool (handles built-ins, MCP proxies, and aliases)
120        if let Some(registration) = self.inventory.get_registration(tool_name) {
121            let description = if registration.name() == tools::TASK_TRACKER {
122                crate::tools::handlers::task_tracker::task_tracker_description_for_workflow(self.is_planning_active())
123            } else {
124                registration.metadata().description().unwrap_or("")
125            };
126            let schema = if registration.name() == tools::TASK_TRACKER {
127                crate::tools::handlers::task_tracker::task_tracker_parameter_schema_for_workflow(
128                    self.is_planning_active(),
129                )
130            } else {
131                registration.parameter_schema()?.clone()
132            };
133            return Some(wrap_schema(tool_name, description, &schema));
134        }
135
136        None
137    }
138
139    /// Check if a tool with the given name is registered.
140    ///
141    /// # Arguments
142    /// * `name` - The name of the tool to check
143    ///
144    /// # Returns
145    /// `bool` indicating whether the tool exists (including aliases)
146    pub async fn has_tool(&self, name: &str) -> bool {
147        if is_removed_default_public_tool(name) {
148            return false;
149        }
150
151        if self.resolve_public_tool_name_sync(name).is_ok() {
152            return true;
153        }
154
155        // First check the main tool registry
156        if self.inventory.has_tool(name) {
157            return true;
158        }
159
160        // If not found, check if it's an MCP tool
161        if let Some(tool_name) = legacy_mcp_tool_name(name) {
162            if self.has_mcp_tool(tool_name).await {
163                return true;
164            }
165
166            // Check if it's an alias
167            if let Some(resolved_name) = self.resolve_mcp_tool_alias(tool_name).await
168                && resolved_name != tool_name
169            {
170                return true;
171            }
172        }
173
174        false
175    }
176}
177
178fn is_removed_default_public_tool(tool_name: &str) -> bool {
179    let lower = tool_name.trim().to_ascii_lowercase();
180    matches!(
181        lower.as_str(),
182        tools::UNIFIED_EXEC
183            | tools::UNIFIED_FILE
184            | tools::UNIFIED_SEARCH
185            | tools::LIST_FILES
186            | tools::READ_FILE
187            | tools::WRITE_FILE
188            | tools::EDIT_FILE
189            | tools::CREATE_FILE
190            | tools::DELETE_FILE
191            | tools::MOVE_FILE
192            | tools::COPY_FILE
193            | tools::SEARCH_REPLACE
194            | tools::FILE_OP
195    )
196}