1pub mod ask;
2pub mod bash;
3pub mod browser;
4pub mod codegraph;
5pub mod edit;
6pub mod glob;
7pub mod grep;
8pub mod ls;
9pub mod monitor;
10pub mod multi_edit;
11pub mod plan_mode;
12pub mod toolproxy; pub mod read;
14pub mod registry; pub mod search;
16pub mod skill;
17pub mod task;
18pub mod todo_write;
19pub mod webfetch;
20pub mod websearch;
21pub mod workflow;
22pub mod write;
23
24pub use toolproxy::{ProxyToolExecutor, ProxyToolDef, ProxyMetadata, ProxyTool, ProxyToolResponse, ProxyToolRequest};
26
27use std::sync::Arc;
28
29use anyhow::Result;
30use async_trait::async_trait;
31use serde::{Deserialize, Serialize};
32use serde_json::{Value, json};
33
34use crate::approval::RiskLevel;
35use crate::skills::Skill;
36use std::path::PathBuf;
37
38#[derive(Debug, Clone, Default)]
41pub struct ToolContext {
42 pub codegraph_available: bool,
44}
45
46pub type BoxedTool = Box<dyn Tool>;
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ToolDefinition {
51 pub name: String,
52 pub description: String,
53 pub parameters: Value,
54 #[serde(default)]
57 pub is_priority: bool,
58}
59
60impl Default for ToolDefinition {
61 fn default() -> Self {
62 Self {
63 name: String::new(),
64 description: String::new(),
65 parameters: json!({"type": "object"}),
66 is_priority: false,
67 }
68 }
69}
70
71impl ToolDefinition {
72 pub fn description_for_llm(&self) -> String {
74 if self.is_priority {
75 format!("[优先] {}", self.description)
76 } else {
77 self.description.clone()
78 }
79 }
80}
81
82#[async_trait]
83pub trait Tool: Send + Sync {
84 fn definition(&self) -> ToolDefinition;
86
87 fn definition_with_context(&self, _ctx: &ToolContext) -> ToolDefinition {
93 self.definition()
94 }
95
96 async fn execute(&self, params: Value) -> Result<String>;
97
98 fn risk_level(&self) -> RiskLevel {
101 RiskLevel::Safe
102 }
103}
104
105pub fn all_tools() -> Vec<Box<dyn Tool>> {
108 all_tools_with_skills(Arc::new(Vec::new()))
109}
110
111fn base_tools(skills: Arc<Vec<Skill>>) -> Vec<Box<dyn Tool>> {
113 vec![
114 Box::new(ask::AskTool),
115 Box::new(read::ReadTool),
116 Box::new(write::WriteTool),
117 Box::new(edit::EditTool),
118 Box::new(multi_edit::MultiEditTool),
119 Box::new(search::SearchTool),
120 Box::new(grep::GrepTool),
121 Box::new(glob::GlobTool),
122 Box::new(ls::LsTool),
123 Box::new(bash::BashTool),
124 Box::new(browser::BrowserOpenTool),
125 Box::new(todo_write::TodoWriteTool),
126 Box::new(websearch::WebSearchTool::new()),
127 Box::new(webfetch::WebFetchTool),
128 Box::new(skill::SkillTool::new(skills)),
129 Box::new(task::TaskTool),
130 Box::new(task::TaskCreateTool),
131 Box::new(task::TaskGetTool),
132 Box::new(task::TaskListTool),
133 Box::new(task::TaskStopTool),
134 Box::new(plan_mode::EnterPlanModeTool),
135 Box::new(plan_mode::ExitPlanModeTool),
136 Box::new(monitor::MonitorTool),
137 ]
138}
139
140pub fn all_tools_with_skills(skills: Arc<Vec<Skill>>) -> Vec<Box<dyn Tool>> {
142 let mut tools = base_tools(skills);
143 tools.extend(workflow::workflow_tools());
145 tools
146}
147
148pub fn all_tools_with_provider(
150 skills: Arc<Vec<Skill>>,
151 provider: Arc<dyn crate::providers::Provider>,
152) -> Vec<Box<dyn Tool>> {
153 let mut tools = base_tools(skills);
154 tools.extend(workflow::workflow_tools_with_provider(provider));
156 tools
157}
158
159pub fn generate_tools_prompt() -> String {
161 generate_tools_prompt_with_path(None)
162}
163
164pub fn generate_tools_prompt_with_path(project_path: Option<&PathBuf>) -> String {
166 let ctx = ToolContext {
168 codegraph_available: project_path
169 .map(|p| codegraph::should_inject_codegraph_tools(p))
170 .unwrap_or(false),
171 };
172
173 let mut tools = base_tools(Arc::new(Vec::new()));
174
175 if ctx.codegraph_available {
177 if let Some(path) = project_path {
178 tools.extend(codegraph::codegraph_tools_with_auto_detect(path));
179 }
180 }
181
182 tools.extend(workflow::workflow_tools());
184
185 let mut priority_tools = Vec::new();
187 let mut normal_tools = Vec::new();
188
189 for tool in tools {
190 let def = tool.definition_with_context(&ctx);
192 if def.is_priority {
193 priority_tools.push(def);
194 } else {
195 normal_tools.push(def);
196 }
197 }
198
199 let mut lines = vec!["可用工具:".to_string()];
200
201 if !priority_tools.is_empty() {
203 lines.push("\n【优先工具 - 必须优先考虑】".to_string());
204 for def in priority_tools {
205 let full_desc = def.description_for_llm();
207 let desc = full_desc.split('\n').next().unwrap_or(&full_desc);
209 if desc.len() > 150 {
210 lines.push(format!(" {}: {}...", def.name, desc.chars().take(147).collect::<String>()));
211 } else {
212 lines.push(format!(" {}: {}", def.name, desc));
213 }
214 }
215 }
216
217 if !normal_tools.is_empty() {
219 lines.push("\n【其他工具】".to_string());
220 for def in normal_tools {
221 let desc = def.description.split('.').next()
223 .or_else(|| def.description.split('\n').next())
224 .unwrap_or(&def.description);
225 if desc.len() > 60 {
226 lines.push(format!(" {}: {}...", def.name, desc.chars().take(57).collect::<String>()));
227 } else {
228 lines.push(format!(" {}: {}", def.name, desc));
229 }
230 }
231 }
232
233 lines.join("\n")
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use std::path::PathBuf;
240
241 #[test]
242 fn test_all_tools_includes_workflow_tools() {
243 let tools = all_tools();
244 let tool_names: Vec<String> = tools.iter().map(|t| t.definition().name).collect();
245
246 assert!(tool_names.contains(&"workflow_discover".to_string()), "workflow_discover should be in tools");
248 assert!(tool_names.contains(&"workflow_run".to_string()), "workflow_run should be in tools");
249 assert!(tool_names.contains(&"workflow_match".to_string()), "workflow_match should be in tools");
250 }
251
252 #[test]
253 fn test_generate_tools_prompt_includes_workflow() {
254 let prompt = generate_tools_prompt();
255
256 assert!(prompt.contains("workflow_discover"), "prompt should mention workflow_discover");
258 assert!(prompt.contains("workflow_run"), "prompt should mention workflow_run");
259 assert!(prompt.contains("workflow_match"), "prompt should mention workflow_match");
260 }
261
262 #[test]
263 fn test_generate_tools_prompt_with_path_includes_codegraph() {
264 let path = PathBuf::from(".");
265 let prompt = generate_tools_prompt_with_path(Some(&path));
266
267 if codegraph::should_inject_codegraph_tools(&path) {
272 assert!(prompt.contains("code_search"), "prompt should mention code_search when conditions met");
273 assert!(prompt.contains("code_callers"), "prompt should mention code_callers when conditions met");
274 } else {
275 assert!(!prompt.contains("code_search"), "prompt should NOT mention code_search without .codegraph");
277 }
278 }
279
280 #[test]
281 fn test_generate_tools_prompt_without_path_excludes_codegraph() {
282 let prompt = generate_tools_prompt();
283
284 assert!(!prompt.contains("code_search"), "prompt should NOT mention code_search without path");
286 }
287
288 #[test]
289 fn test_tool_context_affects_grep_description() {
290 use crate::tools::grep::GrepTool;
291
292 let ctx_no_codegraph = ToolContext { codegraph_available: false };
294 let def_no_cg = GrepTool.definition_with_context(&ctx_no_codegraph);
295 assert!(
296 def_no_cg.description.contains("用 grep 搜索"),
297 "Without CodeGraph, grep should suggest using grep for definitions"
298 );
299 assert!(
300 !def_no_cg.description.contains("code_search"),
301 "Without CodeGraph, grep description should not mention code_search"
302 );
303
304 let ctx_with_codegraph = ToolContext { codegraph_available: true };
306 let def_with_cg = GrepTool.definition_with_context(&ctx_with_codegraph);
307 assert!(
308 def_with_cg.description.contains("code_search"),
309 "With CodeGraph, grep should recommend code_search"
310 );
311 assert!(
312 def_with_cg.description.contains("快10-100倍"),
313 "With CodeGraph, grep should mention speed advantage"
314 );
315 }
316
317 #[test]
318 fn test_tool_context_affects_search_description() {
319 use crate::tools::search::SearchTool;
320
321 let ctx_no_codegraph = ToolContext { codegraph_available: false };
323 let def_no_cg = SearchTool.definition_with_context(&ctx_no_codegraph);
324 assert!(
325 def_no_cg.description.contains("search 的适用场景"),
326 "Without CodeGraph, search should show its own applicable scenarios"
327 );
328
329 let ctx_with_codegraph = ToolContext { codegraph_available: true };
331 let def_with_cg = SearchTool.definition_with_context(&ctx_with_codegraph);
332 assert!(
333 def_with_cg.description.contains("优先使用 code_search"),
334 "With CodeGraph, search should mention code_search priority"
335 );
336 }
337
338 #[test]
339 fn test_tool_context_affects_glob_description() {
340 use crate::tools::glob::GlobTool;
341
342 let ctx_no_codegraph = ToolContext { codegraph_available: false };
344 let def_no_cg = GlobTool.definition_with_context(&ctx_no_codegraph);
345 assert!(
346 def_no_cg.description.contains("glob 的适用场景"),
347 "Without CodeGraph, glob should show its own applicable scenarios"
348 );
349
350 let ctx_with_codegraph = ToolContext { codegraph_available: true };
352 let def_with_cg = GlobTool.definition_with_context(&ctx_with_codegraph);
353 assert!(
354 def_with_cg.description.contains("优先使用 code_files"),
355 "With CodeGraph, glob should mention code_files priority"
356 );
357 }
358
359 #[test]
360 fn test_generate_tools_prompt_dynamic_descriptions() {
361 let path = PathBuf::from(".");
362 let prompt = generate_tools_prompt_with_path(Some(&path));
363
364 if codegraph::should_inject_codegraph_tools(&path) {
366 assert!(
368 prompt.contains("code_search") || prompt.contains("grep"),
369 "Prompt should contain grep tool"
370 );
371 }
372
373 assert!(prompt.contains("grep"), "Prompt should contain grep tool");
375 assert!(prompt.contains("search"), "Prompt should contain search tool");
376 assert!(prompt.contains("glob"), "Prompt should contain glob tool");
377 }
378}
379
380pub fn all_tools_with_arc_provider(
382 skills: Arc<Vec<Skill>>,
383 provider: Arc<dyn crate::providers::Provider>,
384) -> Vec<Box<dyn Tool>> {
385 all_tools_with_provider(skills, provider)
386}
387
388pub fn all_tools_with_box_provider(
391 skills: Arc<Vec<Skill>>,
392 boxed_provider: Box<dyn crate::providers::Provider>,
393) -> Vec<Box<dyn Tool>> {
394 let arc_provider = boxed_provider.clone_arc();
396 all_tools_with_provider(skills, arc_provider)
397}
398
399pub fn all_tools_with_project_path(
401 skills: Arc<Vec<Skill>>,
402 project_path: PathBuf,
403) -> Vec<Box<dyn Tool>> {
404 let mut tools = base_tools(skills);
405 tools.extend(codegraph::codegraph_tools(&project_path));
407 tools.extend(workflow::workflow_tools());
409 tools
410}
411
412pub fn all_tools_full(
414 skills: Arc<Vec<Skill>>,
415 provider: Arc<dyn crate::providers::Provider>,
416 project_path: PathBuf,
417) -> Vec<Box<dyn Tool>> {
418 let mut tools = base_tools(skills);
419 if codegraph::should_inject_codegraph_tools(&project_path) {
421 tools.extend(codegraph::codegraph_tools(&project_path));
422 }
423 tools.extend(workflow::workflow_tools_with_provider(provider));
425 tools
426}