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