#![allow(unused_doc_comments)]
use crate::types::ToolDefinition;
use async_trait::async_trait;
use serde_json::Value;
use std::fmt;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::oneshot;
#[derive(Debug, Clone, serde::Serialize)]
pub struct MemoryItem {
pub id: String,
pub kind: String,
pub content: String,
pub subject: String,
}
pub trait MemoryBackend: Send + Sync + std::fmt::Debug {
fn put<'a>(
&'a self,
content: &'a str,
kind: &'a str,
subject: &'a str,
) -> Pin<Box<dyn Future<Output = Result<String, ToolError>> + Send + 'a>>;
fn search<'a>(
&'a self,
query: &'a str,
k: usize,
) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryItem>, ToolError>> + Send + 'a>>;
fn list<'a>(
&'a self,
subject: &'a str,
) -> Pin<Box<dyn Future<Output = Result<Vec<MemoryItem>, ToolError>> + Send + 'a>>;
fn delete<'a>(
&'a self,
id: &'a str,
) -> Pin<Box<dyn Future<Output = Result<(), ToolError>> + Send + 'a>>;
}
pub struct ResolvedContent {
pub content: String,
pub content_type: String,
pub immutable: bool,
}
pub trait UrlResolver: Send + Sync + std::fmt::Debug {
fn can_resolve(&self, input: &str) -> bool;
fn resolve<'a>(
&'a self,
uri: &'a str,
) -> Pin<Box<dyn Future<Output = Result<ResolvedContent, ToolError>> + Send + 'a>>;
}
pub trait TodoStateProvider: Send + Sync + std::fmt::Debug {
fn get_phases(&self) -> Vec<crate::tools::todo::TodoPhase>;
fn apply_ops<'a>(
&'a self,
ops: Vec<crate::tools::todo::TodoOp>,
) -> Pin<
Box<dyn Future<Output = Result<crate::tools::todo::TodoUpdateResult, String>> + Send + 'a>,
>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentKind {
Main,
Task,
Advisor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentHubStatus {
Running,
Idle,
Parked,
Aborted,
}
#[derive(Debug, Clone)]
pub struct AgentInfo {
pub id: String,
pub display_name: String,
pub kind: AgentKind,
pub status: AgentHubStatus,
pub current_task: Option<String>,
}
pub trait AgentPoolProvider: Send + Sync + std::fmt::Debug {
fn list_agents(&self) -> Vec<AgentInfo>;
fn get_agent(&self, id: &str) -> Option<AgentInfo>;
}
#[derive(Debug, Clone)]
pub enum LspAction {
Diagnostics { file: String },
Definition {
file: String,
line: u32,
symbol: Option<String>,
},
References {
file: String,
line: u32,
symbol: Option<String>,
},
Hover {
file: String,
line: u32,
symbol: Option<String>,
},
Rename {
file: String,
line: u32,
symbol: String,
new_name: String,
apply: bool,
},
Symbols { file: String, query: Option<String> },
Status,
}
pub trait LspProvider: Send + Sync + std::fmt::Debug {
fn execute_action<'a>(
&'a self,
action: &'a LspAction,
) -> Pin<Box<dyn Future<Output = Result<String, ToolError>> + Send + 'a>>;
}
#[derive(Clone)]
pub struct ToolContext {
pub workspace_dir: PathBuf,
pub root_dir: Option<PathBuf>,
pub session_id: Option<String>,
pub snapshot_store: Option<Arc<dyn oxi_hashline::SnapshotStore>>,
pub memory: Option<Arc<dyn MemoryBackend>>,
pub url_resolver: Option<Arc<dyn UrlResolver>>,
pub todo: Option<Arc<dyn TodoStateProvider>>,
pub agent_pool: Option<Arc<dyn AgentPoolProvider>>,
pub lsp: Option<Arc<dyn LspProvider>>,
}
impl fmt::Debug for ToolContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ToolContext")
.field("workspace_dir", &self.workspace_dir)
.field("root_dir", &self.root_dir)
.field("session_id", &self.session_id)
.field(
"snapshot_store",
&self.snapshot_store.as_ref().map(|_| "<dyn SnapshotStore>"),
)
.field(
"memory",
&self.memory.as_ref().map(|_| "<dyn MemoryBackend>"),
)
.field(
"url_resolver",
&self.url_resolver.as_ref().map(|_| "<dyn UrlResolver>"),
)
.finish()
}
}
impl ToolContext {
pub fn new(workspace_dir: impl Into<PathBuf>) -> Self {
Self {
workspace_dir: workspace_dir.into(),
root_dir: None,
session_id: None,
snapshot_store: None,
memory: None,
url_resolver: None,
todo: None,
agent_pool: None,
lsp: None,
}
}
pub fn root(&self) -> &Path {
self.root_dir.as_deref().unwrap_or(&self.workspace_dir)
}
pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
pub fn with_root(mut self, root_dir: impl Into<PathBuf>) -> Self {
self.root_dir = Some(root_dir.into());
self
}
pub fn with_snapshot_store(mut self, store: Arc<dyn oxi_hashline::SnapshotStore>) -> Self {
self.snapshot_store = Some(store);
self
}
pub fn with_memory(mut self, memory: Arc<dyn MemoryBackend>) -> Self {
self.memory = Some(memory);
self
}
pub fn with_url_resolver(mut self, resolver: Arc<dyn UrlResolver>) -> Self {
self.url_resolver = Some(resolver);
self
}
pub fn with_todo(mut self, todo: Arc<dyn TodoStateProvider>) -> Self {
self.todo = Some(todo);
self
}
}
impl Default for ToolContext {
fn default() -> Self {
Self {
workspace_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
root_dir: None,
session_id: None,
snapshot_store: None,
memory: None,
url_resolver: None,
todo: None,
agent_pool: None,
lsp: None,
}
}
}
pub type ToolError = String;
#[derive(Debug)]
pub struct AgentToolResult {
pub success: bool,
pub output: String,
pub metadata: Option<serde_json::Value>,
pub content_blocks: Option<Vec<oxi_ai::ContentBlock>>,
pub terminate: bool,
}
impl AgentToolResult {
pub fn success(output: impl Into<String>) -> Self {
Self {
success: true,
output: output.into(),
metadata: None,
content_blocks: None,
terminate: false,
}
}
pub fn error(output: impl Into<String>) -> Self {
Self {
success: false,
output: output.into(),
metadata: None,
content_blocks: None,
terminate: false,
}
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
pub fn with_content_blocks(mut self, blocks: Vec<oxi_ai::ContentBlock>) -> Self {
self.content_blocks = Some(blocks);
self
}
pub fn with_terminate(mut self) -> Self {
self.terminate = true;
self
}
}
impl fmt::Display for AgentToolResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.output)
}
}
pub type ProgressCallback = Arc<dyn Fn(String) + Send + Sync>;
#[derive(Debug, Clone)]
pub enum ToolExecutionMode {
ParallelSafe,
SequentialOnly,
MutatesFile(std::path::PathBuf),
ReadOnly,
}
#[derive(Debug, Clone)]
pub struct RenderOutput {
pub content: String,
pub collapsed: bool,
pub summary: Option<String>,
}
#[async_trait]
pub trait AgentTool: Send + Sync {
fn name(&self) -> &str;
fn label(&self) -> &str;
fn description(&self) -> &str;
fn parameters_schema(&self) -> Value;
fn essential(&self) -> bool {
false
}
async fn execute(
&self,
tool_call_id: &str,
params: Value,
signal: Option<oneshot::Receiver<()>>,
ctx: &ToolContext,
) -> Result<AgentToolResult, ToolError>;
fn on_progress(&self, _callback: ProgressCallback) {
}
fn on_browse_progress(&self, _callback: crate::tools::browse::BrowseProgressCallback) {}
fn render_call(&self, _params: &serde_json::Value) -> Option<RenderOutput> {
None
}
fn render_result(&self, _result: &AgentToolResult) -> Option<RenderOutput> {
None
}
fn execution_mode(&self) -> ToolExecutionMode {
ToolExecutionMode::ParallelSafe
}
fn current_tab_id(&self) -> Option<uuid::Uuid> {
None
}
fn set_tab_id_slot(&self, _slot: Arc<parking_lot::Mutex<Option<uuid::Uuid>>>) {}
fn to_definition(&self) -> ToolDefinition {
ToolDefinition {
name: self.name().to_string(),
description: self.description().to_string(),
input_schema: serde_json::from_value(self.parameters_schema()).unwrap_or_default(),
}
}
}
pub mod bash;
pub mod browse;
pub mod commit;
pub mod context7;
pub mod edit;
pub mod edit_diff;
pub mod file_mutation_queue;
pub mod find;
pub mod generate_image;
pub mod github;
pub mod github_search;
pub mod grep;
pub mod hashline_fs;
pub mod http_client;
pub mod ls;
pub mod lsp;
pub mod memory_edit;
pub mod memory_recall;
pub mod memory_reflect;
pub mod memory_retain;
pub mod path_security;
pub mod path_utils;
pub mod questionnaire;
pub mod read;
pub mod render_utils;
pub mod search_cache;
pub mod subagent;
pub mod todo;
pub mod tool_definition_wrapper;
pub mod truncate;
pub mod web_search;
pub mod write;
pub use bash::BashTool;
pub use edit::EditTool;
pub use find::FindTool;
pub use grep::GrepTool;
pub use ls::LsTool;
pub use read::ReadTool;
pub use crate::mcp::McpTool;
pub use commit::CommitTool;
pub use context7::{Context7QueryDocsTool, Context7ResolveLibraryIdTool};
pub use memory_edit::MemoryEditTool;
pub use memory_recall::MemoryRecallTool;
pub use memory_reflect::MemoryReflectTool;
pub use memory_retain::MemoryRetainTool;
pub use questionnaire::{QuestionnaireBridge, QuestionnaireTool};
pub use subagent::SubagentTool;
pub use write::WriteTool;
#[derive(Clone)]
pub struct ToolRegistry {
tools: Arc<parking_lot::RwLock<std::collections::HashMap<String, Arc<dyn AgentTool>>>>,
mcp_manager: Arc<parking_lot::RwLock<Option<Arc<crate::mcp::McpManager>>>>,
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::new()
}
}
impl ToolRegistry {
pub fn new() -> Self {
Self {
tools: Arc::new(parking_lot::RwLock::new(std::collections::HashMap::new())),
mcp_manager: Arc::new(parking_lot::RwLock::new(None)),
}
}
pub fn set_mcp_manager(&self, mgr: Arc<crate::mcp::McpManager>) {
*self.mcp_manager.write() = Some(mgr);
}
pub fn mcp_manager(&self) -> Option<Arc<crate::mcp::McpManager>> {
self.mcp_manager.read().clone()
}
pub fn register(&self, tool: impl AgentTool + 'static) {
let name = tool.name().to_string();
self.tools.write().insert(name, Arc::new(tool));
}
pub fn register_arc(&self, tool: Arc<dyn AgentTool>) {
let name = tool.name().to_string();
self.tools.write().insert(name, tool);
}
pub fn get(&self, name: &str) -> Option<Arc<dyn AgentTool>> {
self.tools.read().get(name).cloned()
}
pub fn unregister(&self, name: &str) -> bool {
self.tools.write().remove(name).is_some()
}
pub fn names(&self) -> Vec<String> {
self.tools.read().keys().cloned().collect()
}
pub fn definitions(&self) -> Vec<ToolDefinition> {
self.tools
.read()
.values()
.map(|t| t.to_definition())
.collect()
}
pub fn get_tools(&self) -> Vec<Arc<dyn AgentTool>> {
self.tools.read().values().cloned().collect()
}
pub fn has_all(&self, required: &[&str]) -> bool {
let tools = self.tools.read();
required.iter().all(|name| tools.contains_key(*name))
}
pub fn missing<'a>(&self, required: &[&'a str]) -> Vec<&'a str> {
let tools = self.tools.read();
required
.iter()
.filter(|name| !tools.contains_key(**name))
.copied()
.collect()
}
pub fn with_builtins() -> Self {
Self::with_builtins_cwd(PathBuf::from("."), &[])
}
pub fn with_builtins_cwd(cwd: PathBuf, disabled_tools: &[String]) -> Self {
let registry = Self::new();
let disabled: std::collections::HashSet<&str> =
disabled_tools.iter().map(|s| s.as_str()).collect();
let cache_once: std::cell::OnceCell<Arc<search_cache::SearchCache>> =
std::cell::OnceCell::new();
let mcp_once: std::cell::OnceCell<Arc<crate::mcp::McpManager>> = std::cell::OnceCell::new();
let mcp_manager = mcp_once.get_or_init(crate::mcp::McpManager::spawn).clone();
let mut all_tools: Vec<Box<dyn AgentTool>> = vec![
Box::new(ReadTool::with_cwd(cwd.clone())),
Box::new(WriteTool::with_cwd(cwd.clone())),
Box::new(EditTool::with_cwd(cwd.clone())),
Box::new(BashTool::with_cwd(cwd.clone())),
Box::new(GrepTool::with_cwd(cwd.clone())),
Box::new(FindTool::with_cwd(cwd.clone())),
Box::new(LsTool::with_cwd(cwd.clone())),
Box::new(web_search::WebSearchTool::new(
cache_once
.get_or_init(|| Arc::new(search_cache::SearchCache::new()))
.clone(),
)),
Box::new(search_cache::GetSearchResultsTool::new(
cache_once
.get_or_init(|| Arc::new(search_cache::SearchCache::new()))
.clone(),
)),
Box::new(github::GitHubTool::new(
cache_once
.get_or_init(|| Arc::new(search_cache::SearchCache::new()))
.clone(),
)),
Box::new(SubagentTool::with_cwd(cwd.clone())),
Box::new(todo::TodoTool),
Box::new(memory_recall::MemoryRecallTool),
Box::new(memory_reflect::MemoryReflectTool),
Box::new(memory_retain::MemoryRetainTool),
Box::new(memory_edit::MemoryEditTool),
];
all_tools.push(Box::new(crate::mcp::McpTool::new(mcp_manager.clone())));
for def in mcp_manager.direct_tools_from_cache() {
all_tools.push(Box::new(crate::mcp::McpDirectTool::new(
mcp_manager.clone(),
def,
)));
}
registry.set_mcp_manager(mcp_manager);
all_tools.push(Box::new(context7::Context7ResolveLibraryIdTool::new()));
all_tools.push(Box::new(context7::Context7QueryDocsTool::new()));
all_tools.push(Box::new(generate_image::GenerateImageTool::new()));
all_tools.push(Box::new(commit::CommitTool::unconfigured()));
for tool in all_tools {
if tool.essential() || !disabled.contains(tool.name()) {
if tool.name() == "get_search_results" && disabled.contains("web_search") {
continue;
}
registry.register_arc(Arc::from(tool));
}
}
registry
}
pub fn extend_from(&self, other: &ToolRegistry) {
for name in other.names() {
if let Some(tool) = other.get(&name) {
self.register_arc(tool);
}
}
}
pub fn with_selected_tools(cwd: PathBuf, names: &[&str]) -> Self {
let full = Self::with_builtins_cwd(cwd, &[]);
let registry = Self::new();
let set: std::collections::HashSet<&str> = names.iter().copied().collect();
for name in full.names() {
if set.contains(name.as_str())
&& let Some(tool) = full.get(&name)
{
registry.register_arc(tool);
}
}
registry
}
}