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