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;
6
7use super::schemas::{build_list_files_definition, build_read_file_definition};
8use super::titles::render_title;
9
10/// Enum of tools available via the Agent Client Protocol (ACP) integration.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub enum SupportedTool {
13    ReadFile,
14    ListFiles,
15}
16
17impl SupportedTool {
18    pub(crate) fn kind(&self) -> crate::acp::ToolKind {
19        match self {
20            Self::ReadFile => crate::acp::ToolKind::Read,
21            Self::ListFiles => crate::acp::ToolKind::Search,
22        }
23    }
24
25    pub(crate) fn default_title(&self) -> &'static str {
26        match self {
27            Self::ReadFile => "Read file",
28            Self::ListFiles => "List files",
29        }
30    }
31
32    pub fn function_name(&self) -> &'static str {
33        match self {
34            Self::ReadFile => tools::READ_FILE,
35            Self::ListFiles => tools::LIST_FILES,
36        }
37    }
38
39    fn sort_key(&self) -> u8 {
40        match self {
41            Self::ReadFile => 0,
42            Self::ListFiles => 1,
43        }
44    }
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum ToolDescriptor {
49    Acp(SupportedTool),
50    Local,
51}
52
53impl ToolDescriptor {
54    pub fn kind(self) -> crate::acp::ToolKind {
55        match self {
56            Self::Acp(tool) => tool.kind(),
57            Self::Local => crate::acp::ToolKind::Other,
58        }
59    }
60}
61
62struct ToolRegistryEntry {
63    tool: SupportedTool,
64    definition: ToolDefinition,
65}
66
67pub struct AcpToolRegistry {
68    entries: Vec<ToolRegistryEntry>,
69    local_definitions: Vec<ToolDefinition>,
70    mapping: HashMap<String, ToolDescriptor>,
71}
72
73impl AcpToolRegistry {
74    pub(crate) fn new(
75        workspace_root: &Path,
76        read_file_enabled: bool,
77        list_files_enabled: bool,
78        local_definitions: Vec<ToolDefinition>,
79    ) -> Self {
80        let mut entries = Vec::with_capacity(5);
81        let mut mapping = HashMap::with_capacity(10);
82
83        if read_file_enabled {
84            push_registry_entry(
85                &mut entries,
86                &mut mapping,
87                SupportedTool::ReadFile,
88                build_read_file_definition(workspace_root),
89            );
90        }
91        if list_files_enabled {
92            push_registry_entry(
93                &mut entries,
94                &mut mapping,
95                SupportedTool::ListFiles,
96                build_list_files_definition(workspace_root),
97            );
98        }
99
100        entries.sort_unstable_by_key(|entry| entry.tool.sort_key());
101
102        Self { entries, local_definitions, mapping }
103    }
104
105    pub(crate) fn registered_tools(&self) -> Vec<SupportedTool> {
106        self.entries.iter().map(|entry| entry.tool).collect()
107    }
108
109    pub(crate) fn definitions_for(&self, enabled_tools: &[SupportedTool], include_local: bool) -> Vec<ToolDefinition> {
110        self.definitions_for_filtered(enabled_tools, include_local, |_| true)
111    }
112
113    /// Like [`Self::definitions_for`], but filters the local tool definitions
114    /// through `local_tool_allowed`. This lets callers apply per-agent
115    /// permission gating to local tools while keeping the ACP-advertised tools
116    /// (already gated via `enabled_tools`) unchanged.
117    pub(crate) fn definitions_for_filtered(
118        &self,
119        enabled_tools: &[SupportedTool],
120        include_local: bool,
121        local_tool_allowed: impl Fn(&str) -> bool,
122    ) -> Vec<ToolDefinition> {
123        let mut definitions = Vec::with_capacity(self.entries.len());
124        for entry in &self.entries {
125            if enabled_tools.contains(&entry.tool) {
126                definitions.push(entry.definition.clone());
127            }
128        }
129
130        if include_local {
131            definitions.extend(
132                self.local_definitions
133                    .iter()
134                    .filter(|definition| local_tool_allowed(definition.function_name()))
135                    .cloned(),
136            );
137        }
138
139        definitions
140    }
141
142    pub(crate) fn render_title(&self, descriptor: ToolDescriptor, function_name: &str, args: &Value) -> String {
143        render_title(descriptor, function_name, args)
144    }
145
146    pub fn tool_kind(&self, function_name: &str) -> crate::acp::ToolKind {
147        self.tool_kind_for_call(function_name, None)
148    }
149
150    pub(crate) fn tool_kind_for_call(&self, function_name: &str, args: Option<&Value>) -> crate::acp::ToolKind {
151        let _ = args;
152        match function_name {
153            tools::READ_FILE => crate::acp::ToolKind::Read,
154            tools::GREP_FILE | tools::LIST_FILES | tools::CODE_SEARCH => crate::acp::ToolKind::Search,
155            tools::RUN_PTY_CMD
156            | tools::EXEC_PTY_CMD
157            | tools::EXEC_COMMAND
158            | tools::WRITE_STDIN
159            | tools::EXECUTE_CODE
160            | tools::SHELL => crate::acp::ToolKind::Execute,
161            tools::WRITE_FILE
162            | tools::CREATE_FILE
163            | tools::EDIT_FILE
164            | tools::APPLY_PATCH
165            | tools::SEARCH_REPLACE
166            | tools::FILE_OP
167            | tools::COPY_FILE => crate::acp::ToolKind::Edit,
168            tools::DELETE_FILE => crate::acp::ToolKind::Delete,
169            tools::MOVE_FILE => crate::acp::ToolKind::Move,
170            tools::WEB_FETCH | tools::FETCH_URL | tools::FETCH => crate::acp::ToolKind::Fetch,
171            tools::THINK => crate::acp::ToolKind::Think,
172            _ => crate::acp::ToolKind::Other,
173        }
174    }
175
176    pub(crate) fn lookup(&self, function_name: &str) -> Option<ToolDescriptor> {
177        self.mapping.get(function_name).copied().or_else(|| {
178            self.local_definitions
179                .iter()
180                .any(|definition| definition.function_name() == function_name)
181                .then_some(ToolDescriptor::Local)
182        })
183    }
184
185    pub(crate) fn has_local_tools(&self) -> bool {
186        !self.local_definitions.is_empty()
187    }
188}
189
190fn push_registry_entry(
191    entries: &mut Vec<ToolRegistryEntry>,
192    mapping: &mut HashMap<String, ToolDescriptor>,
193    tool: SupportedTool,
194    definition: ToolDefinition,
195) {
196    let _previous = mapping.insert(definition.function_name().to_string(), ToolDescriptor::Acp(tool));
197    entries.push(ToolRegistryEntry { tool, definition });
198}