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#[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}