Skip to main content

matrixcode_core/tools/
mod.rs

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;  // 代理工具模块
13pub mod read;
14pub mod registry;  // 工具注册中心
15pub 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
24// Re-export proxy types for convenience
25pub 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/// Context for tool definition generation
39/// Used to customize tool descriptions based on available features
40#[derive(Debug, Clone, Default)]
41pub struct ToolContext {
42    /// Whether CodeGraph tools are available
43    pub codegraph_available: bool,
44}
45
46/// Type alias for boxed tool
47pub 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    /// 是否为优先工具。true 时会在描述前添加 "[优先]" 提示,
55    /// 让 LLM 更倾向选择此工具。默认 false。
56    #[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    /// 获取发送给 LLM 的描述(带优先标记)
73    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    /// Get tool definition (must implement)
85    fn definition(&self) -> ToolDefinition;
86    
87    /// Get tool definition with context (for dynamic descriptions)
88    /// 
89    /// Default implementation calls definition(). Override this method
90    /// if you need context-aware descriptions (e.g., different text
91    /// when CodeGraph is available).
92    fn definition_with_context(&self, _ctx: &ToolContext) -> ToolDefinition {
93        self.definition()
94    }
95    
96    async fn execute(&self, params: Value) -> Result<String>;
97
98    /// Risk level of this tool. Defaults to Safe (read-only).
99    /// Override in tools that modify state.
100    fn risk_level(&self) -> RiskLevel {
101        RiskLevel::Safe
102    }
103}
104
105/// Default toolset without any skill integration. Kept for callers
106/// (and the existing tests) that don't care about skills.
107pub fn all_tools() -> Vec<Box<dyn Tool>> {
108    all_tools_with_skills(Arc::new(Vec::new()))
109}
110
111/// Base toolset without workflow tools (to avoid duplicates).
112fn 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
140/// Build the toolset with skill support but without provider.
141pub fn all_tools_with_skills(skills: Arc<Vec<Skill>>) -> Vec<Box<dyn Tool>> {
142    let mut tools = base_tools(skills);
143    // Add workflow tools without provider
144    tools.extend(workflow::workflow_tools());
145    tools
146}
147
148/// Build toolset with Provider for AI-powered tools.
149pub 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    // Add AI-powered workflow tools (with provider)
155    tools.extend(workflow::workflow_tools_with_provider(provider));
156    tools
157}
158
159/// Generate tools description for system prompt
160pub fn generate_tools_prompt() -> String {
161    generate_tools_prompt_with_path(None)
162}
163
164/// Generate tools description with optional CodeGraph support
165pub fn generate_tools_prompt_with_path(project_path: Option<&PathBuf>) -> String {
166    // Build tool context based on CodeGraph availability
167    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    // Add CodeGraph tools only if initialized (CLI installed + .codegraph exists)
176    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    // Add workflow tools
183    tools.extend(workflow::workflow_tools());
184
185    // 🎯 分类显示:优先工具 + 其他工具
186    let mut priority_tools = Vec::new();
187    let mut normal_tools = Vec::new();
188    
189    for tool in tools {
190        // Use definition_with_context for dynamic descriptions
191        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    // 优先工具(完整描述,包含适用场景)
202    if !priority_tools.is_empty() {
203        lines.push("\n【优先工具 - 必须优先考虑】".to_string());
204        for def in priority_tools {
205            // 使用 description_for_llm() 自动添加 [优先] 标记
206            let full_desc = def.description_for_llm();
207            // 优先工具保留完整描述(最多150字符)
208            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    // 其他工具(简要描述)
218    if !normal_tools.is_empty() {
219        lines.push("\n【其他工具】".to_string());
220        for def in normal_tools {
221            // 其他工具保持简要描述(前60字符)
222            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        // Verify workflow tools are present
247        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        // Verify workflow tools appear in prompt
257        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        // CodeGraph tools are only included when:
268        // 1. CodeGraph CLI is installed
269        // 2. Project has .codegraph directory
270        // So we check based on actual conditions
271        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            // When conditions not met, codegraph tools should NOT appear
276            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        // Verify codegraph tools NOT in prompt without path
285        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        // Without CodeGraph - should suggest using grep for definitions
293        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        // With CodeGraph - should recommend code_search
305        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        // Without CodeGraph
322        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        // With CodeGraph
330        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        // Without CodeGraph
343        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        // With CodeGraph
351        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        // Check based on actual CodeGraph availability
365        if codegraph::should_inject_codegraph_tools(&path) {
366            // When CodeGraph is available, grep should mention code_search
367            assert!(
368                prompt.contains("code_search") || prompt.contains("grep"),
369                "Prompt should contain grep tool"
370            );
371        }
372        
373        // Both grep and search should always be present
374        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
380/// Build toolset with Arc Provider (preferred method)
381pub 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
388/// Build toolset with Box Provider (for CLI compatibility - safe implementation)
389/// Uses clone_arc to safely convert Box to Arc without unsafe code.
390pub 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    // Safe conversion: clone_arc creates a new Arc without unsafe pointer manipulation
395    let arc_provider = boxed_provider.clone_arc();
396    all_tools_with_provider(skills, arc_provider)
397}
398
399/// Build toolset with project path for CodeGraph integration.
400pub 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    // Add CodeGraph tools
406    tools.extend(codegraph::codegraph_tools(&project_path));
407    // Add workflow tools
408    tools.extend(workflow::workflow_tools());
409    tools
410}
411
412/// Build full toolset with provider and project path.
413pub 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    // Add CodeGraph tools only if initialized (CLI installed + .codegraph exists)
420    if codegraph::should_inject_codegraph_tools(&project_path) {
421        tools.extend(codegraph::codegraph_tools(&project_path));
422    }
423    // Add AI-powered workflow tools
424    tools.extend(workflow::workflow_tools_with_provider(provider));
425    tools
426}