Skip to main content

matrixcode_core/tools/
mod.rs

1pub mod ask;
2pub mod bash;
3pub mod browser;
4pub mod codegraph;
5pub mod code_quality_hook; // 代码质量验证 Hook
6pub mod edit;
7pub mod glob;
8pub mod grep;
9pub mod ls;
10pub mod monitor;
11pub mod multi_edit;
12pub mod plan_mode;
13pub mod read;
14pub mod read_history; // 读取历史追踪
15pub mod registry; // 工具注册中心
16pub mod search;
17pub mod skill;
18pub mod subagent_executor; // 子代理执行器
19pub mod task;
20pub mod todo_write;
21pub mod tool_hooks; // 工具执行 Hook 系统
22pub mod toolproxy; // 代理工具模块
23pub mod verify; // 代码验证
24pub mod webfetch;
25pub mod websearch;
26pub mod workflow;
27pub mod write;
28
29// Re-export read history types
30pub use read_history::{MustReadFirstError, ReadHistoryTracker};
31
32// Re-export proxy types for convenience
33pub use toolproxy::{
34    ProxyMetadata, ProxyTool, ProxyToolDef, ProxyToolExecutor, ProxyToolRequest, ProxyToolResponse,
35};
36
37use std::sync::Arc;
38
39use anyhow::Result;
40use async_trait::async_trait;
41use serde::{Deserialize, Serialize};
42use serde_json::{Value, json};
43
44use crate::approval::RiskLevel;
45use crate::skills::Skill;
46use std::path::PathBuf;
47
48/// Context for tool definition generation
49/// Used to customize tool descriptions based on available features
50#[derive(Debug, Clone, Default)]
51pub struct ToolContext {
52    /// Whether CodeGraph tools are available
53    pub codegraph_available: bool,
54}
55
56/// Type alias for boxed tool
57pub type BoxedTool = Box<dyn Tool>;
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ToolDefinition {
61    pub name: String,
62    pub description: String,
63    pub parameters: Value,
64    /// 是否为优先工具。true 时会在描述前添加 "[优先]" 提示,
65    /// 让 LLM 更倾向选择此工具。默认 false。
66    #[serde(default)]
67    pub is_priority: bool,
68}
69
70impl Default for ToolDefinition {
71    fn default() -> Self {
72        Self {
73            name: String::new(),
74            description: String::new(),
75            parameters: json!({"type": "object"}),
76            is_priority: false,
77        }
78    }
79}
80
81impl ToolDefinition {
82    /// 获取发送给 LLM 的描述(带优先标记)
83    pub fn description_for_llm(&self) -> String {
84        if self.is_priority {
85            format!("[优先] {}", self.description)
86        } else {
87            self.description.clone()
88        }
89    }
90}
91
92#[async_trait]
93pub trait Tool: Send + Sync {
94    /// Get tool definition (must implement)
95    fn definition(&self) -> ToolDefinition;
96
97    /// Get tool definition with context (for dynamic descriptions)
98    ///
99    /// Default implementation calls definition(). Override this method
100    /// if you need context-aware descriptions (e.g., different text
101    /// when CodeGraph is available).
102    fn definition_with_context(&self, _ctx: &ToolContext) -> ToolDefinition {
103        self.definition()
104    }
105
106    async fn execute(&self, params: Value) -> Result<String>;
107
108    /// Risk level of this tool. Defaults to Safe (read-only).
109    /// Override in tools that modify state.
110    fn risk_level(&self) -> RiskLevel {
111        RiskLevel::Safe
112    }
113}
114
115/// Default toolset without any skill integration. Kept for callers
116/// (and the existing tests) that don't care about skills.
117pub fn all_tools() -> Vec<Box<dyn Tool>> {
118    all_tools_with_skills(Arc::new(Vec::new()))
119}
120
121/// Base toolset without workflow tools (to avoid duplicates).
122fn base_tools(skills: Arc<Vec<Skill>>) -> Vec<Box<dyn Tool>> {
123    vec![
124        Box::new(ask::AskTool),
125        Box::new(read::ReadTool),
126        Box::new(write::WriteTool::new()),
127        Box::new(edit::EditTool),
128        Box::new(multi_edit::MultiEditTool),
129        Box::new(search::SearchTool),
130        Box::new(grep::GrepTool),
131        Box::new(glob::GlobTool),
132        Box::new(ls::LsTool),
133        Box::new(bash::BashTool),
134        Box::new(browser::BrowserOpenTool),
135        Box::new(todo_write::TodoWriteTool),
136        Box::new(websearch::WebSearchTool::new()),
137        Box::new(webfetch::WebFetchTool),
138        Box::new(skill::SkillTool::new(skills)),
139        Box::new(task::TaskTool),
140        Box::new(task::TaskCreateTool),
141        Box::new(task::TaskGetTool),
142        Box::new(task::TaskListTool),
143        Box::new(task::TaskStopTool),
144        Box::new(plan_mode::EnterPlanModeTool),
145        Box::new(plan_mode::ExitPlanModeTool),
146        Box::new(monitor::MonitorTool),
147    ]
148}
149
150/// Build the toolset with skill support but without provider.
151pub fn all_tools_with_skills(skills: Arc<Vec<Skill>>) -> Vec<Box<dyn Tool>> {
152    let mut tools = base_tools(skills);
153    // Add workflow tools without provider
154    tools.extend(workflow::workflow_tools());
155    tools
156}
157
158/// Build toolset with Provider for AI-powered tools.
159pub fn all_tools_with_provider(
160    skills: Arc<Vec<Skill>>,
161    provider: Arc<dyn crate::providers::Provider>,
162) -> Vec<Box<dyn Tool>> {
163    let mut tools = base_tools(skills);
164    // Add AI-powered workflow tools (with provider)
165    tools.extend(workflow::workflow_tools_with_provider(provider));
166    tools
167}
168
169/// Generate tools description for system prompt
170pub fn generate_tools_prompt() -> String {
171    generate_tools_prompt_with_path_and_lsp(None, None)
172}
173
174/// Generate tools description with optional CodeGraph support
175pub fn generate_tools_prompt_with_path(project_path: Option<&PathBuf>) -> String {
176    generate_tools_prompt_with_path_and_lsp(project_path, None)
177}
178
179/// Generate tools description with optional CodeGraph and LSP support
180pub fn generate_tools_prompt_with_path_and_lsp(
181    project_path: Option<&PathBuf>,
182    lsp_registry: Option<Arc<crate::lsp::LspClientRegistry>>,
183) -> String {
184    // Build tool context based on CodeGraph availability
185    let ctx = ToolContext {
186        codegraph_available: project_path
187            .map(|p| codegraph::should_inject_codegraph_tools(p))
188            .unwrap_or(false),
189    };
190
191    let mut tools = base_tools(Arc::new(Vec::new()));
192
193    // Add CodeGraph tools only if initialized (CLI installed + .codegraph exists)
194    if ctx.codegraph_available {
195        if let Some(path) = project_path {
196            tools.extend(codegraph::codegraph_tools_with_auto_detect(path));
197        }
198    }
199
200    // Add LSP tools if registry is provided
201    if let Some(registry) = lsp_registry {
202        tools.extend(crate::lsp::tools::lsp_tools(registry));
203    }
204
205    // Add workflow tools
206    tools.extend(workflow::workflow_tools());
207
208    // 🎯 分类显示:优先工具 + 其他工具
209    let mut priority_tools = Vec::new();
210    let mut normal_tools = Vec::new();
211
212    for tool in tools {
213        // Use definition_with_context for dynamic descriptions
214        let def = tool.definition_with_context(&ctx);
215        if def.is_priority {
216            priority_tools.push(def);
217        } else {
218            normal_tools.push(def);
219        }
220    }
221
222    let mut lines = vec!["可用工具:".to_string()];
223
224    // 优先工具(完整描述,包含适用场景)
225    if !priority_tools.is_empty() {
226        lines.push("\n【优先工具 - 必须优先考虑】".to_string());
227        for def in priority_tools {
228            // 使用 description_for_llm() 自动添加 [优先] 标记
229            let full_desc = def.description_for_llm();
230            // 优先工具保留完整描述(最多150字符)
231            let desc = full_desc.split('\n').next().unwrap_or(&full_desc);
232            if desc.len() > 150 {
233                lines.push(format!(
234                    "  {}: {}...",
235                    def.name,
236                    desc.chars().take(147).collect::<String>()
237                ));
238            } else {
239                lines.push(format!("  {}: {}", def.name, desc));
240            }
241        }
242    }
243
244    // 其他工具(简要描述)
245    if !normal_tools.is_empty() {
246        lines.push("\n【其他工具】".to_string());
247        for def in normal_tools {
248            // 其他工具保持简要描述(前60字符)
249            let desc = def
250                .description
251                .split('.')
252                .next()
253                .or_else(|| def.description.split('\n').next())
254                .unwrap_or(&def.description);
255            if desc.len() > 60 {
256                lines.push(format!(
257                    "  {}: {}...",
258                    def.name,
259                    desc.chars().take(57).collect::<String>()
260                ));
261            } else {
262                lines.push(format!("  {}: {}", def.name, desc));
263            }
264        }
265    }
266
267    lines.join("\n")
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273    use std::path::PathBuf;
274
275    #[test]
276    fn test_all_tools_includes_workflow_tools() {
277        let tools = all_tools();
278        let tool_names: Vec<String> = tools.iter().map(|t| t.definition().name).collect();
279
280        // Verify workflow tools are present
281        assert!(
282            tool_names.contains(&"workflow_discover".to_string()),
283            "workflow_discover should be in tools"
284        );
285        assert!(
286            tool_names.contains(&"workflow_run".to_string()),
287            "workflow_run should be in tools"
288        );
289        assert!(
290            tool_names.contains(&"workflow_match".to_string()),
291            "workflow_match should be in tools"
292        );
293    }
294
295    #[test]
296    fn test_generate_tools_prompt_includes_workflow() {
297        let prompt = generate_tools_prompt();
298
299        // Verify workflow tools appear in prompt
300        assert!(
301            prompt.contains("workflow_discover"),
302            "prompt should mention workflow_discover"
303        );
304        assert!(
305            prompt.contains("workflow_run"),
306            "prompt should mention workflow_run"
307        );
308        assert!(
309            prompt.contains("workflow_match"),
310            "prompt should mention workflow_match"
311        );
312    }
313
314    #[test]
315    fn test_generate_tools_prompt_with_path_includes_codegraph() {
316        let path = PathBuf::from(".");
317        let prompt = generate_tools_prompt_with_path(Some(&path));
318
319        // CodeGraph tools are only included when:
320        // 1. CodeGraph CLI is installed
321        // 2. Project has .codegraph directory
322        // So we check based on actual conditions
323        if codegraph::should_inject_codegraph_tools(&path) {
324            assert!(
325                prompt.contains("code_search"),
326                "prompt should mention code_search when conditions met"
327            );
328            assert!(
329                prompt.contains("code_callers"),
330                "prompt should mention code_callers when conditions met"
331            );
332        } else {
333            // When conditions not met, codegraph tools should NOT appear
334            assert!(
335                !prompt.contains("code_search"),
336                "prompt should NOT mention code_search without .codegraph"
337            );
338        }
339    }
340
341    #[test]
342    fn test_generate_tools_prompt_without_path_excludes_codegraph() {
343        let prompt = generate_tools_prompt();
344
345        // Verify codegraph tools NOT in prompt without path
346        assert!(
347            !prompt.contains("code_search"),
348            "prompt should NOT mention code_search without path"
349        );
350    }
351
352    #[test]
353    fn test_tool_context_affects_grep_description() {
354        use crate::tools::grep::GrepTool;
355
356        // Without CodeGraph - should suggest using grep for definitions
357        let ctx_no_codegraph = ToolContext {
358            codegraph_available: false,
359        };
360        let def_no_cg = GrepTool.definition_with_context(&ctx_no_codegraph);
361        assert!(
362            def_no_cg.description.contains("用 grep 搜索"),
363            "Without CodeGraph, grep should suggest using grep for definitions"
364        );
365        assert!(
366            !def_no_cg.description.contains("code_search"),
367            "Without CodeGraph, grep description should not mention code_search"
368        );
369
370        // With CodeGraph - should recommend code_search
371        let ctx_with_codegraph = ToolContext {
372            codegraph_available: true,
373        };
374        let def_with_cg = GrepTool.definition_with_context(&ctx_with_codegraph);
375        assert!(
376            def_with_cg.description.contains("code_search"),
377            "With CodeGraph, grep should recommend code_search"
378        );
379        assert!(
380            def_with_cg.description.contains("快10-100倍"),
381            "With CodeGraph, grep should mention speed advantage"
382        );
383    }
384
385    #[test]
386    fn test_tool_context_affects_search_description() {
387        use crate::tools::search::SearchTool;
388
389        // Without CodeGraph
390        let ctx_no_codegraph = ToolContext {
391            codegraph_available: false,
392        };
393        let def_no_cg = SearchTool.definition_with_context(&ctx_no_codegraph);
394        assert!(
395            def_no_cg.description.contains("search 的适用场景"),
396            "Without CodeGraph, search should show its own applicable scenarios"
397        );
398
399        // With CodeGraph
400        let ctx_with_codegraph = ToolContext {
401            codegraph_available: true,
402        };
403        let def_with_cg = SearchTool.definition_with_context(&ctx_with_codegraph);
404        assert!(
405            def_with_cg.description.contains("优先使用 code_search"),
406            "With CodeGraph, search should mention code_search priority"
407        );
408    }
409
410    #[test]
411    fn test_tool_context_affects_glob_description() {
412        use crate::tools::glob::GlobTool;
413
414        // Without CodeGraph
415        let ctx_no_codegraph = ToolContext {
416            codegraph_available: false,
417        };
418        let def_no_cg = GlobTool.definition_with_context(&ctx_no_codegraph);
419        assert!(
420            def_no_cg.description.contains("glob 的适用场景"),
421            "Without CodeGraph, glob should show its own applicable scenarios"
422        );
423
424        // With CodeGraph
425        let ctx_with_codegraph = ToolContext {
426            codegraph_available: true,
427        };
428        let def_with_cg = GlobTool.definition_with_context(&ctx_with_codegraph);
429        assert!(
430            def_with_cg.description.contains("优先使用 code_files"),
431            "With CodeGraph, glob should mention code_files priority"
432        );
433    }
434
435    #[test]
436    fn test_generate_tools_prompt_dynamic_descriptions() {
437        let path = PathBuf::from(".");
438        let prompt = generate_tools_prompt_with_path(Some(&path));
439
440        // Check based on actual CodeGraph availability
441        if codegraph::should_inject_codegraph_tools(&path) {
442            // When CodeGraph is available, grep should mention code_search
443            assert!(
444                prompt.contains("code_search") || prompt.contains("grep"),
445                "Prompt should contain grep tool"
446            );
447        }
448
449        // Both grep and search should always be present
450        assert!(prompt.contains("grep"), "Prompt should contain grep tool");
451        assert!(
452            prompt.contains("search"),
453            "Prompt should contain search tool"
454        );
455        assert!(prompt.contains("glob"), "Prompt should contain glob tool");
456    }
457}
458
459/// Build toolset with Arc Provider (preferred method)
460pub fn all_tools_with_arc_provider(
461    skills: Arc<Vec<Skill>>,
462    provider: Arc<dyn crate::providers::Provider>,
463) -> Vec<Box<dyn Tool>> {
464    all_tools_with_provider(skills, provider)
465}
466
467/// Build toolset with Box Provider (for CLI compatibility - safe implementation)
468/// Uses clone_arc to safely convert Box to Arc without unsafe code.
469pub fn all_tools_with_box_provider(
470    skills: Arc<Vec<Skill>>,
471    boxed_provider: Box<dyn crate::providers::Provider>,
472) -> Vec<Box<dyn Tool>> {
473    // Safe conversion: clone_arc creates a new Arc without unsafe pointer manipulation
474    let arc_provider = boxed_provider.clone_arc();
475    all_tools_with_provider(skills, arc_provider)
476}
477
478/// Build toolset with project path for CodeGraph integration.
479pub fn all_tools_with_project_path(
480    skills: Arc<Vec<Skill>>,
481    project_path: PathBuf,
482) -> Vec<Box<dyn Tool>> {
483    all_tools_with_project_path_and_lsp(skills, project_path, None)
484}
485
486/// Build toolset with project path and optional LSP registry.
487pub fn all_tools_with_project_path_and_lsp(
488    skills: Arc<Vec<Skill>>,
489    project_path: PathBuf,
490    lsp_registry: Option<Arc<crate::lsp::LspClientRegistry>>,
491) -> Vec<Box<dyn Tool>> {
492    let mut tools = base_tools(skills);
493    // Add CodeGraph tools
494    tools.extend(codegraph::codegraph_tools(&project_path));
495    // Add LSP tools if registry is provided
496    if let Some(registry) = lsp_registry {
497        tools.extend(crate::lsp::tools::lsp_tools(registry));
498    }
499    // Add workflow tools
500    tools.extend(workflow::workflow_tools());
501    tools
502}
503
504/// Build full toolset with provider and project path.
505pub fn all_tools_full(
506    skills: Arc<Vec<Skill>>,
507    provider: Arc<dyn crate::providers::Provider>,
508    project_path: PathBuf,
509) -> Vec<Box<dyn Tool>> {
510    all_tools_full_with_lsp(skills, provider, project_path, None)
511}
512
513/// Build full toolset with provider, project path, and optional LSP registry.
514pub fn all_tools_full_with_lsp(
515    skills: Arc<Vec<Skill>>,
516    provider: Arc<dyn crate::providers::Provider>,
517    project_path: PathBuf,
518    lsp_registry: Option<Arc<crate::lsp::LspClientRegistry>>,
519) -> Vec<Box<dyn Tool>> {
520    let mut tools = base_tools(skills);
521    // Add CodeGraph tools only if initialized (CLI installed + .codegraph exists)
522    if codegraph::should_inject_codegraph_tools(&project_path) {
523        tools.extend(codegraph::codegraph_tools(&project_path));
524    }
525    // Add LSP tools if registry is provided
526    if let Some(registry) = lsp_registry {
527        tools.extend(crate::lsp::tools::lsp_tools(registry));
528    }
529    // Add AI-powered workflow tools
530    tools.extend(workflow::workflow_tools_with_provider(provider));
531    tools
532}