use crate::ai::AiTool;
use crate::toolbox::framework::ToolRegistry;
use anyhow::Result;
use serde_json::Value;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
pub mod framework;
pub mod utils;
pub mod git_blame;
pub mod git_diff;
pub mod git_find_files;
pub mod git_grep;
pub mod git_log;
pub mod git_ls;
pub mod git_read_files;
pub mod git_show;
pub mod read_prompt;
pub struct SashikoToolContext {
pub worktree_path: PathBuf,
pub prompts_path: Option<PathBuf>,
pub active_patch_files: RwLock<Vec<String>>,
pub virtual_head: RwLock<Option<String>>,
pub(crate) cache: Arc<RwLock<std::collections::HashMap<String, Value>>>,
}
impl SashikoToolContext {
pub fn virtualize_ref(&self, r: &str) -> String {
let vhead_lock = self.virtual_head.read().unwrap();
let Some(ref vhead) = *vhead_lock else {
return r.to_string();
};
static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
let re = RE.get_or_init(|| regex::Regex::new(r"(^|[^/])\bHEAD($|[~^:.@])").unwrap());
re.replace_all(r, format!("${{1}}{}${{2}}", vhead))
.into_owned()
}
}
pub struct ToolBox {
context: SashikoToolContext,
registry: ToolRegistry<SashikoToolContext>,
pub(crate) cache: Arc<RwLock<std::collections::HashMap<String, Value>>>,
}
impl ToolBox {
pub fn new(worktree_path: PathBuf, prompts_path: Option<PathBuf>) -> Self {
let cache = Arc::new(RwLock::new(std::collections::HashMap::new()));
let context = SashikoToolContext {
worktree_path,
prompts_path,
active_patch_files: RwLock::new(Vec::new()),
virtual_head: RwLock::new(None),
cache: cache.clone(),
};
let mut registry = ToolRegistry::new();
registry.register(git_read_files::GitReadFilesTool);
registry.register(git_blame::GitBlameTool);
registry.register(git_diff::GitDiffTool);
registry.register(git_show::GitShowTool);
registry.register(git_log::GitLogTool);
registry.register(git_ls::GitLsTool);
registry.register(git_grep::GitGrepTool);
registry.register(git_find_files::GitFindFilesTool);
if context.prompts_path.is_some() {
registry.register(read_prompt::ReadPromptTool);
}
Self {
context,
registry,
cache,
}
}
pub fn set_virtual_head(&mut self, sha: String) {
let mut vhead = self.context.virtual_head.write().unwrap();
*vhead = Some(sha);
}
pub fn set_active_patch_files(&mut self, files: Vec<String>) {
let mut active = self.context.active_patch_files.write().unwrap();
*active = files;
}
pub fn virtualize_ref(&self, r: &str) -> String {
self.context.virtualize_ref(r)
}
pub fn get_worktree_path(&self) -> &Path {
&self.context.worktree_path
}
pub fn get_declarations_generic(&self) -> Vec<AiTool> {
self.registry
.declarations()
.into_iter()
.map(|decl| AiTool {
name: decl["name"].as_str().unwrap().to_string(),
description: decl["description"].as_str().unwrap().to_string(),
parameters: decl["parameters"].clone(),
})
.collect()
}
pub async fn call(&self, name: &str, args: Value) -> Result<Value> {
let name_normalized = name.trim().to_lowercase();
let should_cache = name_normalized != "todowrite";
let normalized_args = self.registry.normalize_tool_args(&name_normalized, &args);
let key = if should_cache {
let k = format!(
"{}:{}",
name_normalized,
serde_json::to_string(&normalized_args)?
);
{
let cache = self.cache.read().unwrap();
if let Some(val) = cache.get(&k) {
return Ok(val.clone());
}
}
Some(k)
} else {
None
};
let res = self
.registry
.call(&name_normalized, args, &self.context)
.await?;
if let Some(k) = key {
let mut cache = self.cache.write().unwrap();
cache.insert(k, res.clone());
}
Ok(res)
}
}
#[cfg(test)]
mod tools_test;