use std::sync::Arc;
use crate::agent::tools;
use crate::agent::tools::ToolCache;
use crate::agent::tools::background::BackgroundStore;
use crate::agent::tools::plan::PlanSwitchSender;
use crate::agent::tools::question::QuestionSender;
use crate::cli::Cli;
use crate::config::Config;
#[cfg(feature = "mcp")]
use crate::extras::mcp::McpClientManager;
use crate::permission::ask::AskSender;
use crate::permission::checker::PermCheck;
use crate::provider::AnyModel;
use crate::sandbox::Sandbox;
#[cfg(feature = "semantic")]
use crate::semantic::SemanticManager;
use crate::skill::{self, Skill};
use super::build_session_search_tool;
#[cfg(any(feature = "mcp", feature = "plugin"))]
fn shadows_builtin(name: &str, source: &str) -> bool {
if tools::BUILTIN_TOOL_NAMES.contains(&name) {
eprintln!(
"warning: {source} exports tool '{name}' which collides with a dirge built-in; skipping it",
);
true
} else {
false
}
}
#[cfg(feature = "mcp")]
pub async fn wrap_mcp_tools(
mcp_tools: Vec<crate::extras::mcp::tool::McpTool>,
) -> Vec<Arc<dyn crate::agent::agent_loop::LoopTool>> {
use crate::agent::agent_loop::RigToolAdapter;
let mut out: Vec<Arc<dyn crate::agent::agent_loop::LoopTool>> = Vec::new();
for mcp_tool in mcp_tools {
let name = mcp_tool.definition.name.to_string();
if shadows_builtin(&name, &format!("MCP server '{}'", mcp_tool.server_name)) {
continue;
}
let adapter = RigToolAdapter::new(Box::new(mcp_tool)).await;
out.push(Arc::new(adapter));
}
out
}
pub struct DynamicToolSearch {
pub filter: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
pub registry: std::sync::Arc<std::sync::Mutex<Vec<tools::ToolMeta>>>,
}
pub(crate) async fn register_memory_tool(
tools: &mut Vec<std::sync::Arc<dyn crate::agent::agent_loop::LoopTool>>,
memory_store: Option<std::sync::Arc<dyn crate::extras::memory_provider::MemoryProvider>>,
global_store: Option<std::sync::Arc<dyn crate::extras::memory_provider::MemoryProvider>>,
permission: Option<PermCheck>,
ask_tx: Option<AskSender>,
) {
use crate::agent::agent_loop::{RigToolAdapter, types::ToolExecutionMode};
match memory_store {
Some(store) => {
let tool = tools::MemoryTool::new(store, permission, ask_tx).with_global(global_store);
let adapter = RigToolAdapter::new(Box::new(tool))
.await
.with_execution_mode(ToolExecutionMode::Sequential);
tools.push(std::sync::Arc::new(adapter));
}
None => {
tracing::warn!(
target: "dirge::memory",
"memory store unavailable — running this session without the memory tool",
);
}
}
}
pub(crate) async fn register_spec_tool(
tools: &mut Vec<std::sync::Arc<dyn crate::agent::agent_loop::LoopTool>>,
memory_store: Option<std::sync::Arc<dyn crate::extras::memory_provider::MemoryProvider>>,
permission: Option<PermCheck>,
ask_tx: Option<AskSender>,
) {
use crate::agent::agent_loop::{RigToolAdapter, types::ToolExecutionMode};
let paths = std::env::current_dir()
.map(|c| crate::extras::dirge_paths::ProjectPaths::new(&c))
.unwrap_or_else(|_| {
crate::extras::dirge_paths::ProjectPaths::new(std::path::Path::new("."))
});
match crate::extras::spec_db::SpecStore::open(&paths) {
Ok(store) => {
let tool = tools::SpecTool::new(std::sync::Arc::new(store), permission, ask_tx)
.with_memory(memory_store);
let adapter = RigToolAdapter::new(Box::new(tool))
.await
.with_execution_mode(ToolExecutionMode::Sequential);
tools.push(std::sync::Arc::new(adapter));
}
Err(e) => {
tracing::warn!(
target: "dirge::spec",
error = %e,
"spec store unavailable — running this session without the spec tool",
);
}
}
}
#[allow(clippy::too_many_arguments)]
pub async fn build_loop_tools(
cache: ToolCache,
permission: Option<PermCheck>,
ask_tx: Option<AskSender>,
question_tx: Option<QuestionSender>,
plan_tx: Option<PlanSwitchSender>,
bg_store: Option<BackgroundStore>,
#[cfg(feature = "lsp")] lsp_manager: Option<std::sync::Arc<crate::lsp::manager::LspManager>>,
sandbox: Sandbox,
parent_model: Option<AnyModel>,
#[cfg(feature = "mcp")] mcp_manager: Option<&McpClientManager>,
#[cfg(feature = "semantic")] semantic_manager: Option<&SemanticManager>,
cli: &Cli,
cfg: &Config,
session_id: Option<String>,
) -> (
Vec<std::sync::Arc<dyn crate::agent::agent_loop::LoopTool>>,
Option<DynamicToolSearch>,
) {
use crate::agent::agent_loop::types::ToolExecutionMode;
use crate::agent::agent_loop::{LoopTool, RigToolAdapter};
if cli.resolve_no_tools(cfg) {
return (Vec::new(), None);
}
let cwd = std::env::current_dir().unwrap_or_else(|_| ".".into());
let paths = crate::extras::dirge_paths::ProjectPaths::new(&cwd);
let skill_mgr = crate::extras::skills::manager::SkillManager::new(&paths);
let usage_store = crate::extras::skills::usage::UsageStore::load(&paths).ok();
let skills: Arc<[Skill]> = Arc::from(
tokio::task::spawn_blocking(move || skill::discover_skills(&cwd))
.await
.unwrap_or_default(),
);
let memory_store: Option<Arc<dyn crate::extras::memory_provider::MemoryProvider>> =
if let Ok(c) = std::env::current_dir() {
let paths = crate::extras::dirge_paths::ProjectPaths::new(&c);
tokio::task::spawn_blocking(move || {
crate::extras::memory_db::SqliteMemoryStore::load(&paths)
.ok()
.map(|s| {
let arc: Arc<dyn crate::extras::memory_provider::MemoryProvider> =
Arc::new(s);
arc
})
})
.await
.unwrap_or_default()
} else {
None
};
async fn wrap<T>(inner: T, mode: Option<ToolExecutionMode>) -> Arc<dyn LoopTool>
where
T: rig::tool::ToolDyn + 'static,
{
let adapter = RigToolAdapter::new(Box::new(inner)).await;
let adapter = match mode {
Some(m) => adapter.with_execution_mode(m),
None => adapter,
};
Arc::new(adapter)
}
let mut tools: Vec<Arc<dyn LoopTool>> = Vec::new();
tools.push(
wrap(
tools::ReadTool::with_cache(
permission.clone(),
ask_tx.clone(),
cache.clone(),
#[cfg(feature = "lsp")]
lsp_manager.clone(),
),
None,
)
.await,
);
#[cfg(feature = "semantic")]
tools.push(
wrap(
tools::ReadMinifiedTool::with_cache(
permission.clone(),
ask_tx.clone(),
cache.clone(),
#[cfg(feature = "lsp")]
lsp_manager.clone(),
),
None,
)
.await,
);
tools.push(
wrap(
tools::WriteTool::with_cache(
permission.clone(),
ask_tx.clone(),
cache.clone(),
#[cfg(feature = "lsp")]
lsp_manager.clone(),
),
Some(ToolExecutionMode::Sequential),
)
.await,
);
tools.push(
wrap(
tools::EditTool::with_cache(
permission.clone(),
ask_tx.clone(),
cache.clone(),
#[cfg(feature = "lsp")]
lsp_manager.clone(),
),
Some(ToolExecutionMode::Sequential),
)
.await,
);
tools.push(
wrap(
tools::EditLinesTool::with_cache(
permission.clone(),
ask_tx.clone(),
cache.clone(),
#[cfg(feature = "lsp")]
lsp_manager.clone(),
),
Some(ToolExecutionMode::Sequential),
)
.await,
);
#[cfg(feature = "semantic")]
tools.push(
wrap(
tools::EditMinifiedTool::with_cache(
permission.clone(),
ask_tx.clone(),
cache.clone(),
#[cfg(feature = "lsp")]
lsp_manager.clone(),
),
Some(ToolExecutionMode::Sequential),
)
.await,
);
tools.push(
wrap(
tools::BashTool::with_cache(
permission.clone(),
ask_tx.clone(),
sandbox.clone(),
cache.clone(),
)
.with_shell_store(Some(tools::bg_shell::global())),
Some(ToolExecutionMode::Sequential),
)
.await,
);
tools.push(wrap(tools::BashOutputTool::new(tools::bg_shell::global()), None).await);
tools.push(wrap(tools::KillShellTool::new(tools::bg_shell::global()), None).await);
tools.push(
wrap(
tools::GrepTool::with_cache(permission.clone(), ask_tx.clone(), cache.clone()),
None,
)
.await,
);
tools.push(
wrap(
tools::FindFilesTool::with_cache(permission.clone(), ask_tx.clone(), cache.clone()),
None,
)
.await,
);
tools.push(
wrap(
tools::GlobTool::with_cache(permission.clone(), ask_tx.clone(), cache.clone()),
None,
)
.await,
);
tools.push(
wrap(
tools::ListDirTool::with_cache(permission.clone(), ask_tx.clone(), cache.clone()),
None,
)
.await,
);
tools.push(
wrap(
tools::RepoOverviewTool::with_cache(permission.clone(), ask_tx.clone(), cache.clone()),
None,
)
.await,
);
tools.push(
wrap(
tools::WriteTodoList::new(permission.clone(), ask_tx.clone()),
Some(ToolExecutionMode::Sequential),
)
.await,
);
let session_db_path = std::env::current_dir()
.map(|c| crate::extras::dirge_paths::ProjectPaths::new(&c).session_db_path())
.unwrap_or_else(|_| std::path::PathBuf::from(".dirge/sessions/state.db"));
tools.push(
wrap(
build_session_search_tool(
session_db_path,
session_id.clone(),
permission.clone(),
ask_tx.clone(),
),
None,
)
.await,
);
tools.push(
wrap(
tools::SkillTool::new(
Arc::clone(&skills),
skill_mgr,
usage_store.clone(),
permission.clone(),
ask_tx.clone(),
),
Some(ToolExecutionMode::Sequential),
)
.await,
);
let global_store: Option<std::sync::Arc<dyn crate::extras::memory_provider::MemoryProvider>> =
crate::extras::memory_db::SqliteMemoryStore::load_global()
.ok()
.map(|s| std::sync::Arc::new(s) as _);
register_memory_tool(
&mut tools,
memory_store.clone(),
global_store,
permission.clone(),
ask_tx.clone(),
)
.await;
register_spec_tool(
&mut tools,
memory_store.clone(),
permission.clone(),
ask_tx.clone(),
)
.await;
tools.push(
wrap(
tools::ApplyPatchTool::with_cache(permission.clone(), ask_tx.clone(), cache.clone()),
Some(ToolExecutionMode::Sequential),
)
.await,
);
if let Some(tx) = question_tx {
tools.push(
wrap(
tools::QuestionTool::new(tx).with_permission(permission.clone(), ask_tx.clone()),
Some(ToolExecutionMode::Sequential),
)
.await,
);
}
if let Some(tx) = plan_tx {
tools.push(
wrap(
tools::PlanEnterTool::new(tx.clone()).with_permission(permission.clone()),
Some(ToolExecutionMode::Sequential),
)
.await,
);
tools.push(
wrap(
tools::PlanExitTool::new(tx).with_permission(permission.clone()),
Some(ToolExecutionMode::Sequential),
)
.await,
);
}
let websearch_enabled = crate::config::websearch_enabled(cfg);
let webfetch_enabled = crate::config::webfetch_enabled(cfg);
if websearch_enabled {
let key = crate::config::exa_api_key();
tools.push(
wrap(
tools::WebSearchTool::new(permission.clone(), ask_tx.clone(), key),
None,
)
.await,
);
}
if webfetch_enabled {
tools.push(
wrap(
tools::WebFetchTool::new(permission.clone(), ask_tx.clone()),
None,
)
.await,
);
}
if let (Some(pm), Some(store)) = (parent_model, bg_store) {
tools.push(
wrap(
tools::TaskTool::new(permission.clone(), ask_tx.clone(), pm, store.clone()),
Some(ToolExecutionMode::Sequential),
)
.await,
);
tools.push(
wrap(
tools::TaskStatusTool::new(store)
.with_permission(permission.clone(), ask_tx.clone()),
None,
)
.await,
);
}
#[cfg(feature = "lsp")]
if let Some(manager) = &lsp_manager {
tools.push(
wrap(
tools::LspTool::new(permission.clone(), ask_tx.clone(), manager.clone()),
None,
)
.await,
);
}
#[cfg(feature = "dap")]
{
#[cfg(feature = "lsp")]
let dap_tool = if let Some(lsp) = lsp_manager.clone() {
tools::DebugTool::new_with_lsp(permission.clone(), ask_tx.clone(), lsp)
} else {
tools::DebugTool::new(permission.clone(), ask_tx.clone())
};
#[cfg(not(feature = "lsp"))]
let dap_tool = tools::DebugTool::new(permission.clone(), ask_tx.clone());
tools.push(wrap(dap_tool, Some(ToolExecutionMode::Sequential)).await);
}
#[cfg(feature = "mcp")]
if let Some(manager) = &mcp_manager {
let mcp_tools = manager
.collect_tools(permission.clone(), ask_tx.clone())
.await;
for mcp_tool in mcp_tools {
let name = mcp_tool.definition.name.to_string();
if shadows_builtin(&name, &format!("MCP server '{}'", mcp_tool.server_name)) {
continue;
}
tools.push(wrap(mcp_tool, None).await);
}
}
#[cfg(feature = "semantic")]
if let Some(manager) = &semantic_manager {
let sem_tools = manager.tools(permission.clone(), ask_tx.clone());
for sem_tool in sem_tools {
let adapter = RigToolAdapter::new(sem_tool).await;
tools.push(Arc::new(adapter));
}
}
#[cfg(feature = "plugin")]
if let Some(pm_arc) = crate::plugin::hook::global() {
let metas: Vec<crate::plugin::PluginToolMeta> = match pm_arc.lock() {
Ok(mut guard) => guard.list_plugin_tools(),
Err(_) => Vec::new(),
};
for meta in metas {
if shadows_builtin(&meta.name, "plugin") {
continue;
}
if let Some(adapter) = crate::plugin::extension::JanetLoopTool::from_meta(
meta,
pm_arc.clone(),
permission.clone(),
ask_tx.clone(),
) {
tools.push(Arc::new(adapter));
}
}
}
let tool_def_filter = if cfg.resolve_dynamic_tool_search() {
let registry_vec: Vec<tools::ToolMeta> = tools
.iter()
.map(|t| tools::tool_search::meta_from_loop_tool(t.as_ref()))
.collect();
let registry = std::sync::Arc::new(std::sync::Mutex::new(registry_vec));
let filter: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>> =
std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));
let search_tool = tools::ToolSearchTool::new(registry.clone(), filter.clone());
tools.push(Arc::new(search_tool));
Some(DynamicToolSearch { filter, registry })
} else {
None
};
(tools, tool_def_filter)
}
#[cfg(all(test, any(feature = "mcp", feature = "plugin")))]
mod tests {
use super::shadows_builtin;
#[test]
fn shadows_builtin_rejects_only_builtins() {
assert!(shadows_builtin("read", "MCP server 'x'"));
assert!(shadows_builtin("bash", "plugin"));
assert!(!shadows_builtin("totally_custom_tool", "plugin"));
assert!(!shadows_builtin("acme_search", "MCP server 'acme'"));
}
}