Skip to main content

vtcode_acp/tooling/
catalog.rs

1use hashbrown::HashMap;
2use serde_json::Value;
3use std::path::Path;
4use vtcode_core::config::constants::tools;
5use vtcode_core::llm::provider::ToolDefinition;
6use vtcode_core::tools::tool_intent;
7
8use super::schemas::{build_list_files_definition, build_read_file_definition};
9use super::titles::render_title;
10
11/// Enum of tools available via the Agent Client Protocol (ACP) integration.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub enum SupportedTool {
14    ReadFile,
15    ListFiles,
16}
17
18impl SupportedTool {
19    pub fn kind(&self) -> crate::acp::ToolKind {
20        match self {
21            Self::ReadFile => crate::acp::ToolKind::Read,
22            Self::ListFiles => crate::acp::ToolKind::Search,
23        }
24    }
25
26    pub fn default_title(&self) -> &'static str {
27        match self {
28            Self::ReadFile => "Read file",
29            Self::ListFiles => "List files",
30        }
31    }
32
33    pub fn function_name(&self) -> &'static str {
34        match self {
35            Self::ReadFile => tools::READ_FILE,
36            Self::ListFiles => tools::LIST_FILES,
37        }
38    }
39
40    pub fn sort_key(&self) -> u8 {
41        match self {
42            Self::ReadFile => 0,
43            Self::ListFiles => 1,
44        }
45    }
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum ToolDescriptor {
50    Acp(SupportedTool),
51    Local,
52}
53
54impl ToolDescriptor {
55    pub fn kind(self) -> crate::acp::ToolKind {
56        match self {
57            Self::Acp(tool) => tool.kind(),
58            Self::Local => crate::acp::ToolKind::Other,
59        }
60    }
61}
62
63struct ToolRegistryEntry {
64    tool: SupportedTool,
65    definition: ToolDefinition,
66}
67
68pub struct AcpToolRegistry {
69    entries: Vec<ToolRegistryEntry>,
70    local_definitions: Vec<ToolDefinition>,
71    mapping: HashMap<String, ToolDescriptor>,
72}
73
74impl AcpToolRegistry {
75    pub fn new(
76        workspace_root: &Path,
77        read_file_enabled: bool,
78        list_files_enabled: bool,
79        local_definitions: Vec<ToolDefinition>,
80    ) -> Self {
81        let mut entries = Vec::with_capacity(5);
82        let mut mapping = HashMap::with_capacity(10);
83
84        if read_file_enabled {
85            push_registry_entry(
86                &mut entries,
87                &mut mapping,
88                SupportedTool::ReadFile,
89                build_read_file_definition(workspace_root),
90            );
91        }
92        if list_files_enabled {
93            push_registry_entry(
94                &mut entries,
95                &mut mapping,
96                SupportedTool::ListFiles,
97                build_list_files_definition(workspace_root),
98            );
99        }
100
101        entries.sort_unstable_by_key(|entry| entry.tool.sort_key());
102
103        Self {
104            entries,
105            local_definitions,
106            mapping,
107        }
108    }
109
110    pub fn registered_tools(&self) -> Vec<SupportedTool> {
111        self.entries.iter().map(|entry| entry.tool).collect()
112    }
113
114    pub fn definitions_for(
115        &self,
116        enabled_tools: &[SupportedTool],
117        include_local: bool,
118    ) -> Vec<ToolDefinition> {
119        let mut definitions = Vec::with_capacity(self.entries.len());
120        for entry in &self.entries {
121            if enabled_tools.contains(&entry.tool) {
122                definitions.push(entry.definition.clone());
123            }
124        }
125
126        if include_local {
127            definitions.extend(self.local_definitions.iter().cloned());
128        }
129
130        definitions
131    }
132
133    pub fn render_title(
134        &self,
135        descriptor: ToolDescriptor,
136        function_name: &str,
137        args: &Value,
138    ) -> String {
139        render_title(descriptor, function_name, args)
140    }
141
142    pub fn tool_kind(&self, function_name: &str) -> crate::acp::ToolKind {
143        self.tool_kind_for_call(function_name, None)
144    }
145
146    pub fn tool_kind_for_call(
147        &self,
148        function_name: &str,
149        args: Option<&Value>,
150    ) -> crate::acp::ToolKind {
151        match function_name {
152            tools::UNIFIED_SEARCH => crate::acp::ToolKind::Search,
153            tools::UNIFIED_EXEC => crate::acp::ToolKind::Execute,
154            tools::UNIFIED_FILE => unified_file_tool_kind(args),
155            tools::READ_FILE => crate::acp::ToolKind::Read,
156            tools::GREP_FILE | tools::LIST_FILES => crate::acp::ToolKind::Search,
157            tools::RUN_PTY_CMD
158            | tools::EXEC_PTY_CMD
159            | tools::EXEC_COMMAND
160            | tools::EXECUTE_CODE
161            | tools::SHELL => crate::acp::ToolKind::Execute,
162            tools::WRITE_FILE
163            | tools::CREATE_FILE
164            | tools::EDIT_FILE
165            | tools::APPLY_PATCH
166            | tools::SEARCH_REPLACE
167            | tools::FILE_OP
168            | tools::COPY_FILE => crate::acp::ToolKind::Edit,
169            tools::DELETE_FILE => crate::acp::ToolKind::Delete,
170            tools::MOVE_FILE => crate::acp::ToolKind::Move,
171            tools::WEB_FETCH | tools::FETCH_URL | tools::FETCH => crate::acp::ToolKind::Fetch,
172            tools::THINK => crate::acp::ToolKind::Think,
173            _ => crate::acp::ToolKind::Other,
174        }
175    }
176
177    pub fn lookup(&self, function_name: &str) -> Option<ToolDescriptor> {
178        self.mapping.get(function_name).copied().or_else(|| {
179            self.local_definitions
180                .iter()
181                .any(|definition| definition.function_name() == function_name)
182                .then_some(ToolDescriptor::Local)
183        })
184    }
185
186    pub fn has_local_tools(&self) -> bool {
187        !self.local_definitions.is_empty()
188    }
189}
190
191fn unified_file_tool_kind(args: Option<&Value>) -> crate::acp::ToolKind {
192    let Some(action) = args.and_then(tool_intent::unified_file_action) else {
193        return crate::acp::ToolKind::Other;
194    };
195
196    match action {
197        action if action.eq_ignore_ascii_case("read") => crate::acp::ToolKind::Read,
198        action if action.eq_ignore_ascii_case("delete") => crate::acp::ToolKind::Delete,
199        action if action.eq_ignore_ascii_case("move") => crate::acp::ToolKind::Move,
200        action
201            if action.eq_ignore_ascii_case("write")
202                || action.eq_ignore_ascii_case("create")
203                || action.eq_ignore_ascii_case("edit")
204                || action.eq_ignore_ascii_case("patch")
205                || action.eq_ignore_ascii_case("copy") =>
206        {
207            crate::acp::ToolKind::Edit
208        }
209        _ => crate::acp::ToolKind::Other,
210    }
211}
212
213fn push_registry_entry(
214    entries: &mut Vec<ToolRegistryEntry>,
215    mapping: &mut HashMap<String, ToolDescriptor>,
216    tool: SupportedTool,
217    definition: ToolDefinition,
218) {
219    mapping.insert(
220        definition.function_name().to_string(),
221        ToolDescriptor::Acp(tool),
222    );
223    entries.push(ToolRegistryEntry { tool, definition });
224}