1use std::collections::HashSet;
14use std::path::Path;
15
16use thiserror::Error;
17
18use super::{ToolDefinition, ToolOutput, ToolUseRequest, WriteResult, shell};
19use crate::search::SearchConfig;
20
21const BUILTIN_TOOLS: &[ToolEntry] = &[
22 ToolEntry {
23 name: "find_files",
24 definition: super::find_files::definition,
25 execute: super::find_files::execute_request,
26 example_input: r#"{"pattern":"Cargo.toml"}"#,
27 },
28 ToolEntry {
29 name: "list_searchable_files",
30 definition: super::list_searchable_files::definition,
31 execute: super::list_searchable_files::execute_request,
32 example_input: r#"{"glob":"src/**/*.rs"}"#,
33 },
34 ToolEntry {
35 name: "search_text",
36 definition: super::search_text::definition,
37 execute: super::search_text::execute_request,
38 example_input: r#"{"pattern":"fn main","glob":"src/**/*.rs"}"#,
39 },
40 ToolEntry {
41 name: "read_file_range",
42 definition: super::read_file_range::definition,
43 execute: super::read_file_range::execute_request,
44 example_input: r#"{"path":"Cargo.toml","start_line":1,"end_line":3}"#,
45 },
46 ToolEntry {
47 name: "sawk",
48 definition: super::sawk::definition,
49 execute: super::sawk::execute_request,
50 example_input: r#"{"action":"sed_print","path":"Cargo.toml","start_line":1,"end_line":3}"#,
51 },
52 ToolEntry {
53 name: "web_search",
54 definition: super::web_search::definition,
55 execute: super::web_search::execute_request,
56 example_input: r#"{"query":"Rust serde documentation","max_results":3}"#,
57 },
58 ToolEntry {
59 name: "read_url",
60 definition: super::read_url::definition,
61 execute: super::read_url::execute_request,
62 example_input: r#"{"url":"https://example.com"}"#,
63 },
64 ToolEntry {
65 name: "create_file",
66 definition: super::create_file::definition,
67 execute: super::create_file::execute_request,
68 example_input: r#"{"path":"notes.txt","content":"hello\n"}"#,
69 },
70 ToolEntry {
71 name: "replace_range",
72 definition: super::replace_range::definition,
73 execute: super::replace_range::execute_request,
74 example_input: r#"{"path":"notes.txt","old_string":"hello","new_string":"hi"}"#,
75 },
76 ToolEntry {
77 name: "write_patch",
78 definition: super::write_patch::definition,
79 execute: super::write_patch::execute_request,
80 example_input: r#"{"patches":[{"op":"edit","path":"notes.txt","old_string":"hello","new_string":"hi"}]}"#,
81 },
82 ToolEntry {
83 name: "run_shell",
84 definition: super::shell::definition,
85 execute: super::shell::execute_request,
86 example_input: r#"{"argv":["cargo","test","tools"]}"#,
87 },
88];
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub enum ProviderSchemaFormat {
93 Anthropic,
95 #[allow(dead_code)]
97 OpenAiFunction,
98}
99
100#[derive(Clone, Debug)]
102pub struct ToolContext<'a> {
103 pub root: &'a Path,
105 pub search: SearchConfig,
107 pub process_registry: Option<shell::ProcessRegistry>,
110}
111
112impl<'a> ToolContext<'a> {
113 pub fn new(root: &'a Path) -> Self {
115 Self { root, search: SearchConfig::default(), process_registry: None }
116 }
117
118 pub fn with_search(root: &'a Path, search: &SearchConfig) -> Self {
120 Self { root, search: search.clone(), process_registry: None }
121 }
122
123 pub fn with_process_registry(mut self, process_registry: shell::ProcessRegistry) -> Self {
125 self.process_registry = Some(process_registry);
126 self
127 }
128}
129
130#[derive(Clone, Debug, Eq, PartialEq)]
132pub struct ToolExecution {
133 pub output: ToolOutput,
135 pub write_result: Option<WriteResult>,
137 pub shell_result: Option<shell::ProcessResult>,
139}
140
141impl ToolExecution {
142 pub fn output(output: ToolOutput) -> Self {
144 Self { output, write_result: None, shell_result: None }
145 }
146
147 pub fn full(
149 output: ToolOutput, write_result: Option<WriteResult>, shell_result: Option<shell::ProcessResult>,
150 ) -> Self {
151 Self { output, write_result, shell_result }
152 }
153}
154
155#[derive(Clone, Debug, Error, Eq, PartialEq)]
157pub enum ToolError {
158 #[error("unknown tool: {0}")]
160 UnknownTool(String),
161 #[error("invalid tool registry: {0}")]
163 InvalidRegistry(String),
164 #[error("invalid tool arguments: {0}")]
166 InvalidArguments(String),
167}
168
169#[derive(Clone, Copy)]
171pub struct ToolEntry {
172 pub name: &'static str,
174 pub definition: fn() -> ToolDefinition,
176 pub execute: fn(&ToolUseRequest, &ToolContext<'_>) -> ToolExecution,
178 pub example_input: &'static str,
180}
181
182pub fn builtins() -> &'static [ToolEntry] {
184 BUILTIN_TOOLS
185}
186
187pub fn get(name: &str) -> Option<&'static ToolEntry> {
189 builtins().iter().find(|entry| entry.name == name)
190}
191
192pub fn validate() -> Result<(), ToolError> {
194 let mut names = HashSet::new();
195 for entry in builtins() {
196 if !names.insert(entry.name) {
197 return Err(ToolError::InvalidRegistry(format!(
198 "duplicate tool name: {}",
199 entry.name
200 )));
201 }
202
203 let definition = (entry.definition)();
204 if definition.name != entry.name {
205 return Err(ToolError::InvalidRegistry(format!(
206 "entry `{}` returned definition `{}`",
207 entry.name, definition.name
208 )));
209 }
210
211 if !definition.input_schema.is_object() {
212 return Err(ToolError::InvalidRegistry(format!(
213 "entry `{}` schema is not a JSON object",
214 entry.name
215 )));
216 }
217
218 let example = serde_json::from_str::<serde_json::Value>(entry.example_input).map_err(|err| {
219 ToolError::InvalidArguments(format!("{} example input is invalid JSON: {err}", entry.name))
220 })?;
221 if !example.is_object() {
222 return Err(ToolError::InvalidArguments(format!(
223 "{} example input is not a JSON object",
224 entry.name
225 )));
226 }
227 }
228
229 Ok(())
230}
231
232pub fn tool_definitions() -> Vec<ToolDefinition> {
234 validate().expect("built-in tool registry should be valid");
235 builtins().iter().map(|entry| (entry.definition)()).collect()
236}
237
238pub fn execute(request: &ToolUseRequest, ctx: &ToolContext<'_>) -> ToolExecution {
240 match get(&request.name) {
241 Some(entry) => (entry.execute)(request, ctx),
242 None => ToolExecution::output(ToolOutput::failed(
243 &request.name,
244 ToolError::UnknownTool(request.name.clone()).to_string(),
245 )),
246 }
247}
248
249pub fn provider_tool_catalog_schemas(defs: &[ToolDefinition], format: ProviderSchemaFormat) -> serde_json::Value {
251 match format {
252 ProviderSchemaFormat::Anthropic => serde_json::Value::Array(
253 defs.iter()
254 .map(|tool| {
255 serde_json::json!({
256 "name": tool.name,
257 "description": tool.description,
258 "input_schema": tool.input_schema,
259 })
260 })
261 .collect(),
262 ),
263 ProviderSchemaFormat::OpenAiFunction => serde_json::Value::Array(
264 defs.iter()
265 .map(|tool| {
266 serde_json::json!({
267 "type": "function",
268 "function": {
269 "name": tool.name,
270 "description": tool.description,
271 "parameters": tool.input_schema,
272 }
273 })
274 })
275 .collect(),
276 ),
277 }
278}