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::UNIFIED_EXEC.to_string(),
22            "list_dir" | "list_directory" => tools::UNIFIED_SEARCH.to_string(),
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::UNIFIED_SEARCH => &[tools::UNIFIED_FILE],
47            tools::UNIFIED_EXEC => &[tools::UNIFIED_SEARCH],
48            tools::UNIFIED_FILE | tools::APPLY_PATCH => &[tools::UNIFIED_SEARCH],
49            // Task trackers require action-specific arguments; generic fallback names
50            // create low-signal retries.
51            tools::TASK_TRACKER | tools::PLAN_TASK_TRACKER => &[],
52            // Unknown tools: prefer no fallback over noisy generic suggestions.
53            _ => &[],
54        };
55
56        for candidate in candidates {
57            if *candidate != failed_name && available.iter().any(|tool| tool == candidate) {
58                return Some((*candidate).to_string());
59            }
60        }
61        None
62    }
63
64    /// Get a list of all available tools, including MCP tools.
65    pub async fn available_tools(&self) -> Vec<String> {
66        // Use try_read to avoid blocking on contested locks
67        if let Ok(cache) = self.cached_available_tools.try_read()
68            && let Some(tools) = cache.as_ref()
69        {
70            return tools.clone();
71        }
72
73        let tools = self
74            .public_tool_names(SessionSurface::Interactive, CapabilityLevel::CodeSearch)
75            .await;
76
77        // Update cache with try_write to avoid blocking
78        if let Ok(mut cache) = self.cached_available_tools.try_write() {
79            *cache = Some(tools.clone());
80        }
81
82        tools
83    }
84
85    /// Get the schema for a specific tool.
86    pub async fn get_tool_schema(&self, tool_name: &str) -> Option<Value> {
87        let wrap_schema = |requested_name: &str, description: &str, schema: &Value| {
88            // Wrap in full declaration if it's just parameters
89            if schema.get("properties").is_some() && schema.get("name").is_none() {
90                serde_json::json!({
91                    "name": requested_name,
92                    "description": description,
93                    "parameters": schema
94                })
95            } else {
96                schema.clone()
97            }
98        };
99
100        if let Some(entry) = self
101            .schema_for_public_name(
102                tool_name,
103                SessionToolsConfig::full_public(
104                    SessionSurface::Interactive,
105                    CapabilityLevel::CodeSearch,
106                    ToolDocumentationMode::Full,
107                    ToolModelCapabilities::default(),
108                ),
109            )
110            .await
111        {
112            return Some(wrap_schema(
113                entry.name.as_str(),
114                entry.description.as_str(),
115                &entry.parameters,
116            ));
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 Some(schema) = registration.parameter_schema()
122        {
123            return Some(wrap_schema(
124                tool_name,
125                registration.metadata().description().unwrap_or(""),
126                schema,
127            ));
128        }
129
130        None
131    }
132
133    /// Check if a tool with the given name is registered.
134    ///
135    /// # Arguments
136    /// * `name` - The name of the tool to check
137    ///
138    /// # Returns
139    /// `bool` indicating whether the tool exists (including aliases)
140    pub async fn has_tool(&self, name: &str) -> bool {
141        if self.resolve_public_tool_name_sync(name).is_ok() {
142            return true;
143        }
144
145        // First check the main tool registry
146        if self.inventory.has_tool(name) {
147            return true;
148        }
149
150        // If not found, check if it's an MCP tool
151        if let Some(tool_name) = legacy_mcp_tool_name(name) {
152            if self.has_mcp_tool(tool_name).await {
153                return true;
154            }
155
156            // Check if it's an alias
157            if let Some(resolved_name) = self.resolve_mcp_tool_alias(tool_name).await
158                && resolved_name != tool_name
159            {
160                return true;
161            }
162        }
163
164        false
165    }
166}