pub mod ask;
pub mod bash;
pub mod browser;
pub mod codegraph;
pub mod edit;
pub mod glob;
pub mod grep;
pub mod ls;
pub mod monitor;
pub mod multi_edit;
pub mod plan_mode;
pub mod read;
pub mod registry; pub mod search;
pub mod skill;
pub mod task;
pub mod todo_write;
pub mod toolproxy; pub mod webfetch;
pub mod websearch;
pub mod workflow;
pub mod write;
pub use toolproxy::{
ProxyMetadata, ProxyTool, ProxyToolDef, ProxyToolExecutor, ProxyToolRequest, ProxyToolResponse,
};
use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use crate::approval::RiskLevel;
use crate::skills::Skill;
use std::path::PathBuf;
#[derive(Debug, Clone, Default)]
pub struct ToolContext {
pub codegraph_available: bool,
}
pub type BoxedTool = Box<dyn Tool>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub parameters: Value,
#[serde(default)]
pub is_priority: bool,
}
impl Default for ToolDefinition {
fn default() -> Self {
Self {
name: String::new(),
description: String::new(),
parameters: json!({"type": "object"}),
is_priority: false,
}
}
}
impl ToolDefinition {
pub fn description_for_llm(&self) -> String {
if self.is_priority {
format!("[优先] {}", self.description)
} else {
self.description.clone()
}
}
}
#[async_trait]
pub trait Tool: Send + Sync {
fn definition(&self) -> ToolDefinition;
fn definition_with_context(&self, _ctx: &ToolContext) -> ToolDefinition {
self.definition()
}
async fn execute(&self, params: Value) -> Result<String>;
fn risk_level(&self) -> RiskLevel {
RiskLevel::Safe
}
}
pub fn all_tools() -> Vec<Box<dyn Tool>> {
all_tools_with_skills(Arc::new(Vec::new()))
}
fn base_tools(skills: Arc<Vec<Skill>>) -> Vec<Box<dyn Tool>> {
vec![
Box::new(ask::AskTool),
Box::new(read::ReadTool),
Box::new(write::WriteTool),
Box::new(edit::EditTool),
Box::new(multi_edit::MultiEditTool),
Box::new(search::SearchTool),
Box::new(grep::GrepTool),
Box::new(glob::GlobTool),
Box::new(ls::LsTool),
Box::new(bash::BashTool),
Box::new(browser::BrowserOpenTool),
Box::new(todo_write::TodoWriteTool),
Box::new(websearch::WebSearchTool::new()),
Box::new(webfetch::WebFetchTool),
Box::new(skill::SkillTool::new(skills)),
Box::new(task::TaskTool),
Box::new(task::TaskCreateTool),
Box::new(task::TaskGetTool),
Box::new(task::TaskListTool),
Box::new(task::TaskStopTool),
Box::new(plan_mode::EnterPlanModeTool),
Box::new(plan_mode::ExitPlanModeTool),
Box::new(monitor::MonitorTool),
]
}
pub fn all_tools_with_skills(skills: Arc<Vec<Skill>>) -> Vec<Box<dyn Tool>> {
let mut tools = base_tools(skills);
tools.extend(workflow::workflow_tools());
tools
}
pub fn all_tools_with_provider(
skills: Arc<Vec<Skill>>,
provider: Arc<dyn crate::providers::Provider>,
) -> Vec<Box<dyn Tool>> {
let mut tools = base_tools(skills);
tools.extend(workflow::workflow_tools_with_provider(provider));
tools
}
pub fn generate_tools_prompt() -> String {
generate_tools_prompt_with_path(None)
}
pub fn generate_tools_prompt_with_path(project_path: Option<&PathBuf>) -> String {
let ctx = ToolContext {
codegraph_available: project_path
.map(|p| codegraph::should_inject_codegraph_tools(p))
.unwrap_or(false),
};
let mut tools = base_tools(Arc::new(Vec::new()));
if ctx.codegraph_available {
if let Some(path) = project_path {
tools.extend(codegraph::codegraph_tools_with_auto_detect(path));
}
}
tools.extend(workflow::workflow_tools());
let mut priority_tools = Vec::new();
let mut normal_tools = Vec::new();
for tool in tools {
let def = tool.definition_with_context(&ctx);
if def.is_priority {
priority_tools.push(def);
} else {
normal_tools.push(def);
}
}
let mut lines = vec!["可用工具:".to_string()];
if !priority_tools.is_empty() {
lines.push("\n【优先工具 - 必须优先考虑】".to_string());
for def in priority_tools {
let full_desc = def.description_for_llm();
let desc = full_desc.split('\n').next().unwrap_or(&full_desc);
if desc.len() > 150 {
lines.push(format!(
" {}: {}...",
def.name,
desc.chars().take(147).collect::<String>()
));
} else {
lines.push(format!(" {}: {}", def.name, desc));
}
}
}
if !normal_tools.is_empty() {
lines.push("\n【其他工具】".to_string());
for def in normal_tools {
let desc = def
.description
.split('.')
.next()
.or_else(|| def.description.split('\n').next())
.unwrap_or(&def.description);
if desc.len() > 60 {
lines.push(format!(
" {}: {}...",
def.name,
desc.chars().take(57).collect::<String>()
));
} else {
lines.push(format!(" {}: {}", def.name, desc));
}
}
}
lines.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_all_tools_includes_workflow_tools() {
let tools = all_tools();
let tool_names: Vec<String> = tools.iter().map(|t| t.definition().name).collect();
assert!(
tool_names.contains(&"workflow_discover".to_string()),
"workflow_discover should be in tools"
);
assert!(
tool_names.contains(&"workflow_run".to_string()),
"workflow_run should be in tools"
);
assert!(
tool_names.contains(&"workflow_match".to_string()),
"workflow_match should be in tools"
);
}
#[test]
fn test_generate_tools_prompt_includes_workflow() {
let prompt = generate_tools_prompt();
assert!(
prompt.contains("workflow_discover"),
"prompt should mention workflow_discover"
);
assert!(
prompt.contains("workflow_run"),
"prompt should mention workflow_run"
);
assert!(
prompt.contains("workflow_match"),
"prompt should mention workflow_match"
);
}
#[test]
fn test_generate_tools_prompt_with_path_includes_codegraph() {
let path = PathBuf::from(".");
let prompt = generate_tools_prompt_with_path(Some(&path));
if codegraph::should_inject_codegraph_tools(&path) {
assert!(
prompt.contains("code_search"),
"prompt should mention code_search when conditions met"
);
assert!(
prompt.contains("code_callers"),
"prompt should mention code_callers when conditions met"
);
} else {
assert!(
!prompt.contains("code_search"),
"prompt should NOT mention code_search without .codegraph"
);
}
}
#[test]
fn test_generate_tools_prompt_without_path_excludes_codegraph() {
let prompt = generate_tools_prompt();
assert!(
!prompt.contains("code_search"),
"prompt should NOT mention code_search without path"
);
}
#[test]
fn test_tool_context_affects_grep_description() {
use crate::tools::grep::GrepTool;
let ctx_no_codegraph = ToolContext {
codegraph_available: false,
};
let def_no_cg = GrepTool.definition_with_context(&ctx_no_codegraph);
assert!(
def_no_cg.description.contains("用 grep 搜索"),
"Without CodeGraph, grep should suggest using grep for definitions"
);
assert!(
!def_no_cg.description.contains("code_search"),
"Without CodeGraph, grep description should not mention code_search"
);
let ctx_with_codegraph = ToolContext {
codegraph_available: true,
};
let def_with_cg = GrepTool.definition_with_context(&ctx_with_codegraph);
assert!(
def_with_cg.description.contains("code_search"),
"With CodeGraph, grep should recommend code_search"
);
assert!(
def_with_cg.description.contains("快10-100倍"),
"With CodeGraph, grep should mention speed advantage"
);
}
#[test]
fn test_tool_context_affects_search_description() {
use crate::tools::search::SearchTool;
let ctx_no_codegraph = ToolContext {
codegraph_available: false,
};
let def_no_cg = SearchTool.definition_with_context(&ctx_no_codegraph);
assert!(
def_no_cg.description.contains("search 的适用场景"),
"Without CodeGraph, search should show its own applicable scenarios"
);
let ctx_with_codegraph = ToolContext {
codegraph_available: true,
};
let def_with_cg = SearchTool.definition_with_context(&ctx_with_codegraph);
assert!(
def_with_cg.description.contains("优先使用 code_search"),
"With CodeGraph, search should mention code_search priority"
);
}
#[test]
fn test_tool_context_affects_glob_description() {
use crate::tools::glob::GlobTool;
let ctx_no_codegraph = ToolContext {
codegraph_available: false,
};
let def_no_cg = GlobTool.definition_with_context(&ctx_no_codegraph);
assert!(
def_no_cg.description.contains("glob 的适用场景"),
"Without CodeGraph, glob should show its own applicable scenarios"
);
let ctx_with_codegraph = ToolContext {
codegraph_available: true,
};
let def_with_cg = GlobTool.definition_with_context(&ctx_with_codegraph);
assert!(
def_with_cg.description.contains("优先使用 code_files"),
"With CodeGraph, glob should mention code_files priority"
);
}
#[test]
fn test_generate_tools_prompt_dynamic_descriptions() {
let path = PathBuf::from(".");
let prompt = generate_tools_prompt_with_path(Some(&path));
if codegraph::should_inject_codegraph_tools(&path) {
assert!(
prompt.contains("code_search") || prompt.contains("grep"),
"Prompt should contain grep tool"
);
}
assert!(prompt.contains("grep"), "Prompt should contain grep tool");
assert!(
prompt.contains("search"),
"Prompt should contain search tool"
);
assert!(prompt.contains("glob"), "Prompt should contain glob tool");
}
}
pub fn all_tools_with_arc_provider(
skills: Arc<Vec<Skill>>,
provider: Arc<dyn crate::providers::Provider>,
) -> Vec<Box<dyn Tool>> {
all_tools_with_provider(skills, provider)
}
pub fn all_tools_with_box_provider(
skills: Arc<Vec<Skill>>,
boxed_provider: Box<dyn crate::providers::Provider>,
) -> Vec<Box<dyn Tool>> {
let arc_provider = boxed_provider.clone_arc();
all_tools_with_provider(skills, arc_provider)
}
pub fn all_tools_with_project_path(
skills: Arc<Vec<Skill>>,
project_path: PathBuf,
) -> Vec<Box<dyn Tool>> {
let mut tools = base_tools(skills);
tools.extend(codegraph::codegraph_tools(&project_path));
tools.extend(workflow::workflow_tools());
tools
}
pub fn all_tools_full(
skills: Arc<Vec<Skill>>,
provider: Arc<dyn crate::providers::Provider>,
project_path: PathBuf,
) -> Vec<Box<dyn Tool>> {
let mut tools = base_tools(skills);
if codegraph::should_inject_codegraph_tools(&project_path) {
tools.extend(codegraph::codegraph_tools(&project_path));
}
tools.extend(workflow::workflow_tools_with_provider(provider));
tools
}