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 read;
13pub mod registry; pub mod search;
15pub mod skill;
16pub mod task;
17pub mod todo_write;
18pub mod toolproxy; pub mod webfetch;
20pub mod websearch;
21pub mod workflow;
22pub mod write;
23
24pub use toolproxy::{
26 ProxyMetadata, ProxyTool, ProxyToolDef, ProxyToolExecutor, ProxyToolRequest, ProxyToolResponse,
27};
28
29use std::sync::Arc;
30
31use anyhow::Result;
32use async_trait::async_trait;
33use serde::{Deserialize, Serialize};
34use serde_json::{Value, json};
35
36use crate::approval::RiskLevel;
37use crate::skills::Skill;
38use std::path::PathBuf;
39
40#[derive(Debug, Clone, Default)]
43pub struct ToolContext {
44 pub codegraph_available: bool,
46}
47
48pub type BoxedTool = Box<dyn Tool>;
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ToolDefinition {
53 pub name: String,
54 pub description: String,
55 pub parameters: Value,
56 #[serde(default)]
59 pub is_priority: bool,
60}
61
62impl Default for ToolDefinition {
63 fn default() -> Self {
64 Self {
65 name: String::new(),
66 description: String::new(),
67 parameters: json!({"type": "object"}),
68 is_priority: false,
69 }
70 }
71}
72
73impl ToolDefinition {
74 pub fn description_for_llm(&self) -> String {
76 if self.is_priority {
77 format!("[优先] {}", self.description)
78 } else {
79 self.description.clone()
80 }
81 }
82}
83
84#[async_trait]
85pub trait Tool: Send + Sync {
86 fn definition(&self) -> ToolDefinition;
88
89 fn definition_with_context(&self, _ctx: &ToolContext) -> ToolDefinition {
95 self.definition()
96 }
97
98 async fn execute(&self, params: Value) -> Result<String>;
99
100 fn risk_level(&self) -> RiskLevel {
103 RiskLevel::Safe
104 }
105}
106
107pub fn all_tools() -> Vec<Box<dyn Tool>> {
110 all_tools_with_skills(Arc::new(Vec::new()))
111}
112
113fn base_tools(skills: Arc<Vec<Skill>>) -> Vec<Box<dyn Tool>> {
115 vec![
116 Box::new(ask::AskTool),
117 Box::new(read::ReadTool),
118 Box::new(write::WriteTool),
119 Box::new(edit::EditTool),
120 Box::new(multi_edit::MultiEditTool),
121 Box::new(search::SearchTool),
122 Box::new(grep::GrepTool),
123 Box::new(glob::GlobTool),
124 Box::new(ls::LsTool),
125 Box::new(bash::BashTool),
126 Box::new(browser::BrowserOpenTool),
127 Box::new(todo_write::TodoWriteTool),
128 Box::new(websearch::WebSearchTool::new()),
129 Box::new(webfetch::WebFetchTool),
130 Box::new(skill::SkillTool::new(skills)),
131 Box::new(task::TaskTool),
132 Box::new(task::TaskCreateTool),
133 Box::new(task::TaskGetTool),
134 Box::new(task::TaskListTool),
135 Box::new(task::TaskStopTool),
136 Box::new(plan_mode::EnterPlanModeTool),
137 Box::new(plan_mode::ExitPlanModeTool),
138 Box::new(monitor::MonitorTool),
139 ]
140}
141
142pub fn all_tools_with_skills(skills: Arc<Vec<Skill>>) -> Vec<Box<dyn Tool>> {
144 let mut tools = base_tools(skills);
145 tools.extend(workflow::workflow_tools());
147 tools
148}
149
150pub fn all_tools_with_provider(
152 skills: Arc<Vec<Skill>>,
153 provider: Arc<dyn crate::providers::Provider>,
154) -> Vec<Box<dyn Tool>> {
155 let mut tools = base_tools(skills);
156 tools.extend(workflow::workflow_tools_with_provider(provider));
158 tools
159}
160
161pub fn generate_tools_prompt() -> String {
163 generate_tools_prompt_with_path(None)
164}
165
166pub fn generate_tools_prompt_with_path(project_path: Option<&PathBuf>) -> String {
168 let ctx = ToolContext {
170 codegraph_available: project_path
171 .map(|p| codegraph::should_inject_codegraph_tools(p))
172 .unwrap_or(false),
173 };
174
175 let mut tools = base_tools(Arc::new(Vec::new()));
176
177 if ctx.codegraph_available {
179 if let Some(path) = project_path {
180 tools.extend(codegraph::codegraph_tools_with_auto_detect(path));
181 }
182 }
183
184 tools.extend(workflow::workflow_tools());
186
187 let mut priority_tools = Vec::new();
189 let mut normal_tools = Vec::new();
190
191 for tool in tools {
192 let def = tool.definition_with_context(&ctx);
194 if def.is_priority {
195 priority_tools.push(def);
196 } else {
197 normal_tools.push(def);
198 }
199 }
200
201 let mut lines = vec!["可用工具:".to_string()];
202
203 if !priority_tools.is_empty() {
205 lines.push("\n【优先工具 - 必须优先考虑】".to_string());
206 for def in priority_tools {
207 let full_desc = def.description_for_llm();
209 let desc = full_desc.split('\n').next().unwrap_or(&full_desc);
211 if desc.len() > 150 {
212 lines.push(format!(
213 " {}: {}...",
214 def.name,
215 desc.chars().take(147).collect::<String>()
216 ));
217 } else {
218 lines.push(format!(" {}: {}", def.name, desc));
219 }
220 }
221 }
222
223 if !normal_tools.is_empty() {
225 lines.push("\n【其他工具】".to_string());
226 for def in normal_tools {
227 let desc = def
229 .description
230 .split('.')
231 .next()
232 .or_else(|| def.description.split('\n').next())
233 .unwrap_or(&def.description);
234 if desc.len() > 60 {
235 lines.push(format!(
236 " {}: {}...",
237 def.name,
238 desc.chars().take(57).collect::<String>()
239 ));
240 } else {
241 lines.push(format!(" {}: {}", def.name, desc));
242 }
243 }
244 }
245
246 lines.join("\n")
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use std::path::PathBuf;
253
254 #[test]
255 fn test_all_tools_includes_workflow_tools() {
256 let tools = all_tools();
257 let tool_names: Vec<String> = tools.iter().map(|t| t.definition().name).collect();
258
259 assert!(
261 tool_names.contains(&"workflow_discover".to_string()),
262 "workflow_discover should be in tools"
263 );
264 assert!(
265 tool_names.contains(&"workflow_run".to_string()),
266 "workflow_run should be in tools"
267 );
268 assert!(
269 tool_names.contains(&"workflow_match".to_string()),
270 "workflow_match should be in tools"
271 );
272 }
273
274 #[test]
275 fn test_generate_tools_prompt_includes_workflow() {
276 let prompt = generate_tools_prompt();
277
278 assert!(
280 prompt.contains("workflow_discover"),
281 "prompt should mention workflow_discover"
282 );
283 assert!(
284 prompt.contains("workflow_run"),
285 "prompt should mention workflow_run"
286 );
287 assert!(
288 prompt.contains("workflow_match"),
289 "prompt should mention workflow_match"
290 );
291 }
292
293 #[test]
294 fn test_generate_tools_prompt_with_path_includes_codegraph() {
295 let path = PathBuf::from(".");
296 let prompt = generate_tools_prompt_with_path(Some(&path));
297
298 if codegraph::should_inject_codegraph_tools(&path) {
303 assert!(
304 prompt.contains("code_search"),
305 "prompt should mention code_search when conditions met"
306 );
307 assert!(
308 prompt.contains("code_callers"),
309 "prompt should mention code_callers when conditions met"
310 );
311 } else {
312 assert!(
314 !prompt.contains("code_search"),
315 "prompt should NOT mention code_search without .codegraph"
316 );
317 }
318 }
319
320 #[test]
321 fn test_generate_tools_prompt_without_path_excludes_codegraph() {
322 let prompt = generate_tools_prompt();
323
324 assert!(
326 !prompt.contains("code_search"),
327 "prompt should NOT mention code_search without path"
328 );
329 }
330
331 #[test]
332 fn test_tool_context_affects_grep_description() {
333 use crate::tools::grep::GrepTool;
334
335 let ctx_no_codegraph = ToolContext {
337 codegraph_available: false,
338 };
339 let def_no_cg = GrepTool.definition_with_context(&ctx_no_codegraph);
340 assert!(
341 def_no_cg.description.contains("用 grep 搜索"),
342 "Without CodeGraph, grep should suggest using grep for definitions"
343 );
344 assert!(
345 !def_no_cg.description.contains("code_search"),
346 "Without CodeGraph, grep description should not mention code_search"
347 );
348
349 let ctx_with_codegraph = ToolContext {
351 codegraph_available: true,
352 };
353 let def_with_cg = GrepTool.definition_with_context(&ctx_with_codegraph);
354 assert!(
355 def_with_cg.description.contains("code_search"),
356 "With CodeGraph, grep should recommend code_search"
357 );
358 assert!(
359 def_with_cg.description.contains("快10-100倍"),
360 "With CodeGraph, grep should mention speed advantage"
361 );
362 }
363
364 #[test]
365 fn test_tool_context_affects_search_description() {
366 use crate::tools::search::SearchTool;
367
368 let ctx_no_codegraph = ToolContext {
370 codegraph_available: false,
371 };
372 let def_no_cg = SearchTool.definition_with_context(&ctx_no_codegraph);
373 assert!(
374 def_no_cg.description.contains("search 的适用场景"),
375 "Without CodeGraph, search should show its own applicable scenarios"
376 );
377
378 let ctx_with_codegraph = ToolContext {
380 codegraph_available: true,
381 };
382 let def_with_cg = SearchTool.definition_with_context(&ctx_with_codegraph);
383 assert!(
384 def_with_cg.description.contains("优先使用 code_search"),
385 "With CodeGraph, search should mention code_search priority"
386 );
387 }
388
389 #[test]
390 fn test_tool_context_affects_glob_description() {
391 use crate::tools::glob::GlobTool;
392
393 let ctx_no_codegraph = ToolContext {
395 codegraph_available: false,
396 };
397 let def_no_cg = GlobTool.definition_with_context(&ctx_no_codegraph);
398 assert!(
399 def_no_cg.description.contains("glob 的适用场景"),
400 "Without CodeGraph, glob should show its own applicable scenarios"
401 );
402
403 let ctx_with_codegraph = ToolContext {
405 codegraph_available: true,
406 };
407 let def_with_cg = GlobTool.definition_with_context(&ctx_with_codegraph);
408 assert!(
409 def_with_cg.description.contains("优先使用 code_files"),
410 "With CodeGraph, glob should mention code_files priority"
411 );
412 }
413
414 #[test]
415 fn test_generate_tools_prompt_dynamic_descriptions() {
416 let path = PathBuf::from(".");
417 let prompt = generate_tools_prompt_with_path(Some(&path));
418
419 if codegraph::should_inject_codegraph_tools(&path) {
421 assert!(
423 prompt.contains("code_search") || prompt.contains("grep"),
424 "Prompt should contain grep tool"
425 );
426 }
427
428 assert!(prompt.contains("grep"), "Prompt should contain grep tool");
430 assert!(
431 prompt.contains("search"),
432 "Prompt should contain search tool"
433 );
434 assert!(prompt.contains("glob"), "Prompt should contain glob tool");
435 }
436}
437
438pub fn all_tools_with_arc_provider(
440 skills: Arc<Vec<Skill>>,
441 provider: Arc<dyn crate::providers::Provider>,
442) -> Vec<Box<dyn Tool>> {
443 all_tools_with_provider(skills, provider)
444}
445
446pub fn all_tools_with_box_provider(
449 skills: Arc<Vec<Skill>>,
450 boxed_provider: Box<dyn crate::providers::Provider>,
451) -> Vec<Box<dyn Tool>> {
452 let arc_provider = boxed_provider.clone_arc();
454 all_tools_with_provider(skills, arc_provider)
455}
456
457pub fn all_tools_with_project_path(
459 skills: Arc<Vec<Skill>>,
460 project_path: PathBuf,
461) -> Vec<Box<dyn Tool>> {
462 let mut tools = base_tools(skills);
463 tools.extend(codegraph::codegraph_tools(&project_path));
465 tools.extend(workflow::workflow_tools());
467 tools
468}
469
470pub fn all_tools_full(
472 skills: Arc<Vec<Skill>>,
473 provider: Arc<dyn crate::providers::Provider>,
474 project_path: PathBuf,
475) -> Vec<Box<dyn Tool>> {
476 let mut tools = base_tools(skills);
477 if codegraph::should_inject_codegraph_tools(&project_path) {
479 tools.extend(codegraph::codegraph_tools(&project_path));
480 }
481 tools.extend(workflow::workflow_tools_with_provider(provider));
483 tools
484}