use std::collections::HashSet;
use std::path::Path;
use thiserror::Error;
use super::{ToolDefinition, ToolOutput, ToolUseRequest, WriteResult, shell};
use crate::search::SearchConfig;
const BUILTIN_TOOLS: &[ToolEntry] = &[
ToolEntry {
name: "find_files",
definition: super::find_files::definition,
execute: super::find_files::execute_request,
example_input: r#"{"pattern":"Cargo.toml"}"#,
},
ToolEntry {
name: "list_searchable_files",
definition: super::list_searchable_files::definition,
execute: super::list_searchable_files::execute_request,
example_input: r#"{"glob":"src/**/*.rs"}"#,
},
ToolEntry {
name: "search_text",
definition: super::search_text::definition,
execute: super::search_text::execute_request,
example_input: r#"{"pattern":"fn main","glob":"src/**/*.rs"}"#,
},
ToolEntry {
name: "read_file_range",
definition: super::read_file_range::definition,
execute: super::read_file_range::execute_request,
example_input: r#"{"path":"Cargo.toml","start_line":1,"end_line":3}"#,
},
ToolEntry {
name: "sawk",
definition: super::sawk::definition,
execute: super::sawk::execute_request,
example_input: r#"{"action":"sed_print","path":"Cargo.toml","start_line":1,"end_line":3}"#,
},
ToolEntry {
name: "web_search",
definition: super::web_search::definition,
execute: super::web_search::execute_request,
example_input: r#"{"query":"Rust serde documentation","max_results":3}"#,
},
ToolEntry {
name: "read_url",
definition: super::read_url::definition,
execute: super::read_url::execute_request,
example_input: r#"{"url":"https://example.com"}"#,
},
ToolEntry {
name: "create_file",
definition: super::create_file::definition,
execute: super::create_file::execute_request,
example_input: r#"{"path":"notes.txt","content":"hello\n"}"#,
},
ToolEntry {
name: "replace_range",
definition: super::replace_range::definition,
execute: super::replace_range::execute_request,
example_input: r#"{"path":"notes.txt","old_string":"hello","new_string":"hi"}"#,
},
ToolEntry {
name: "write_patch",
definition: super::write_patch::definition,
execute: super::write_patch::execute_request,
example_input: r#"{"patches":[{"op":"edit","path":"notes.txt","old_string":"hello","new_string":"hi"}]}"#,
},
ToolEntry {
name: "run_shell",
definition: super::shell::definition,
execute: super::shell::execute_request,
example_input: r#"{"argv":["cargo","test","tools"]}"#,
},
];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProviderSchemaFormat {
Anthropic,
#[allow(dead_code)]
OpenAiFunction,
}
#[derive(Clone, Debug)]
pub struct ToolContext<'a> {
pub root: &'a Path,
pub search: SearchConfig,
pub process_registry: Option<shell::ProcessRegistry>,
}
impl<'a> ToolContext<'a> {
pub fn new(root: &'a Path) -> Self {
Self { root, search: SearchConfig::default(), process_registry: None }
}
pub fn with_search(root: &'a Path, search: &SearchConfig) -> Self {
Self { root, search: search.clone(), process_registry: None }
}
pub fn with_process_registry(mut self, process_registry: shell::ProcessRegistry) -> Self {
self.process_registry = Some(process_registry);
self
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ToolExecution {
pub output: ToolOutput,
pub write_result: Option<WriteResult>,
pub shell_result: Option<shell::ProcessResult>,
}
impl ToolExecution {
pub fn output(output: ToolOutput) -> Self {
Self { output, write_result: None, shell_result: None }
}
pub fn full(
output: ToolOutput, write_result: Option<WriteResult>, shell_result: Option<shell::ProcessResult>,
) -> Self {
Self { output, write_result, shell_result }
}
}
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum ToolError {
#[error("unknown tool: {0}")]
UnknownTool(String),
#[error("invalid tool registry: {0}")]
InvalidRegistry(String),
#[error("invalid tool arguments: {0}")]
InvalidArguments(String),
}
#[derive(Clone, Copy)]
pub struct ToolEntry {
pub name: &'static str,
pub definition: fn() -> ToolDefinition,
pub execute: fn(&ToolUseRequest, &ToolContext<'_>) -> ToolExecution,
pub example_input: &'static str,
}
pub fn builtins() -> &'static [ToolEntry] {
BUILTIN_TOOLS
}
pub fn get(name: &str) -> Option<&'static ToolEntry> {
builtins().iter().find(|entry| entry.name == name)
}
pub fn validate() -> Result<(), ToolError> {
let mut names = HashSet::new();
for entry in builtins() {
if !names.insert(entry.name) {
return Err(ToolError::InvalidRegistry(format!(
"duplicate tool name: {}",
entry.name
)));
}
let definition = (entry.definition)();
if definition.name != entry.name {
return Err(ToolError::InvalidRegistry(format!(
"entry `{}` returned definition `{}`",
entry.name, definition.name
)));
}
if !definition.input_schema.is_object() {
return Err(ToolError::InvalidRegistry(format!(
"entry `{}` schema is not a JSON object",
entry.name
)));
}
let example = serde_json::from_str::<serde_json::Value>(entry.example_input).map_err(|err| {
ToolError::InvalidArguments(format!("{} example input is invalid JSON: {err}", entry.name))
})?;
if !example.is_object() {
return Err(ToolError::InvalidArguments(format!(
"{} example input is not a JSON object",
entry.name
)));
}
}
Ok(())
}
pub fn tool_definitions() -> Vec<ToolDefinition> {
validate().expect("built-in tool registry should be valid");
builtins().iter().map(|entry| (entry.definition)()).collect()
}
pub fn execute(request: &ToolUseRequest, ctx: &ToolContext<'_>) -> ToolExecution {
match get(&request.name) {
Some(entry) => (entry.execute)(request, ctx),
None => ToolExecution::output(ToolOutput::failed(
&request.name,
ToolError::UnknownTool(request.name.clone()).to_string(),
)),
}
}
pub fn provider_tool_catalog_schemas(defs: &[ToolDefinition], format: ProviderSchemaFormat) -> serde_json::Value {
match format {
ProviderSchemaFormat::Anthropic => serde_json::Value::Array(
defs.iter()
.map(|tool| {
serde_json::json!({
"name": tool.name,
"description": tool.description,
"input_schema": tool.input_schema,
})
})
.collect(),
),
ProviderSchemaFormat::OpenAiFunction => serde_json::Value::Array(
defs.iter()
.map(|tool| {
serde_json::json!({
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.input_schema,
}
})
})
.collect(),
),
}
}