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 read;
13pub mod registry; // 工具注册中心
14pub mod search;
15pub mod skill;
16pub mod task;
17pub mod todo_write;
18pub mod toolproxy; // 代理工具模块
19pub mod webfetch;
20pub mod websearch;
21pub mod workflow;
22pub mod write;
23
24// Re-export proxy types for convenience
25pub 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/// Context for tool definition generation
41/// Used to customize tool descriptions based on available features
42#[derive(Debug, Clone, Default)]
43pub struct ToolContext {
44    /// Whether CodeGraph tools are available
45    pub codegraph_available: bool,
46}
47
48/// Type alias for boxed tool
49pub 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    /// 是否为优先工具。true 时会在描述前添加 "[优先]" 提示,
57    /// 让 LLM 更倾向选择此工具。默认 false。
58    #[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    /// 获取发送给 LLM 的描述(带优先标记)
75    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    /// Get tool definition (must implement)
87    fn definition(&self) -> ToolDefinition;
88
89    /// Get tool definition with context (for dynamic descriptions)
90    ///
91    /// Default implementation calls definition(). Override this method
92    /// if you need context-aware descriptions (e.g., different text
93    /// when CodeGraph is available).
94    fn definition_with_context(&self, _ctx: &ToolContext) -> ToolDefinition {
95        self.definition()
96    }
97
98    async fn execute(&self, params: Value) -> Result<String>;
99
100    /// Risk level of this tool. Defaults to Safe (read-only).
101    /// Override in tools that modify state.
102    fn risk_level(&self) -> RiskLevel {
103        RiskLevel::Safe
104    }
105}
106
107/// Default toolset without any skill integration. Kept for callers
108/// (and the existing tests) that don't care about skills.
109pub fn all_tools() -> Vec<Box<dyn Tool>> {
110    all_tools_with_skills(Arc::new(Vec::new()))
111}
112
113/// Base toolset without workflow tools (to avoid duplicates).
114fn 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
142/// Build the toolset with skill support but without provider.
143pub fn all_tools_with_skills(skills: Arc<Vec<Skill>>) -> Vec<Box<dyn Tool>> {
144    let mut tools = base_tools(skills);
145    // Add workflow tools without provider
146    tools.extend(workflow::workflow_tools());
147    tools
148}
149
150/// Build toolset with Provider for AI-powered tools.
151pub 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    // Add AI-powered workflow tools (with provider)
157    tools.extend(workflow::workflow_tools_with_provider(provider));
158    tools
159}
160
161/// Generate tools description for system prompt
162pub fn generate_tools_prompt() -> String {
163    generate_tools_prompt_with_path(None)
164}
165
166/// Generate tools description with optional CodeGraph support
167pub fn generate_tools_prompt_with_path(project_path: Option<&PathBuf>) -> String {
168    // Build tool context based on CodeGraph availability
169    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    // Add CodeGraph tools only if initialized (CLI installed + .codegraph exists)
178    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    // Add workflow tools
185    tools.extend(workflow::workflow_tools());
186
187    // 🎯 分类显示:优先工具 + 其他工具
188    let mut priority_tools = Vec::new();
189    let mut normal_tools = Vec::new();
190
191    for tool in tools {
192        // Use definition_with_context for dynamic descriptions
193        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    // 优先工具(完整描述,包含适用场景)
204    if !priority_tools.is_empty() {
205        lines.push("\n【优先工具 - 必须优先考虑】".to_string());
206        for def in priority_tools {
207            // 使用 description_for_llm() 自动添加 [优先] 标记
208            let full_desc = def.description_for_llm();
209            // 优先工具保留完整描述(最多150字符)
210            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    // 其他工具(简要描述)
224    if !normal_tools.is_empty() {
225        lines.push("\n【其他工具】".to_string());
226        for def in normal_tools {
227            // 其他工具保持简要描述(前60字符)
228            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        // Verify workflow tools are present
260        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        // Verify workflow tools appear in prompt
279        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        // CodeGraph tools are only included when:
299        // 1. CodeGraph CLI is installed
300        // 2. Project has .codegraph directory
301        // So we check based on actual conditions
302        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            // When conditions not met, codegraph tools should NOT appear
313            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        // Verify codegraph tools NOT in prompt without path
325        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        // Without CodeGraph - should suggest using grep for definitions
336        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        // With CodeGraph - should recommend code_search
350        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        // Without CodeGraph
369        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        // With CodeGraph
379        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        // Without CodeGraph
394        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        // With CodeGraph
404        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        // Check based on actual CodeGraph availability
420        if codegraph::should_inject_codegraph_tools(&path) {
421            // When CodeGraph is available, grep should mention code_search
422            assert!(
423                prompt.contains("code_search") || prompt.contains("grep"),
424                "Prompt should contain grep tool"
425            );
426        }
427
428        // Both grep and search should always be present
429        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
438/// Build toolset with Arc Provider (preferred method)
439pub 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
446/// Build toolset with Box Provider (for CLI compatibility - safe implementation)
447/// Uses clone_arc to safely convert Box to Arc without unsafe code.
448pub 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    // Safe conversion: clone_arc creates a new Arc without unsafe pointer manipulation
453    let arc_provider = boxed_provider.clone_arc();
454    all_tools_with_provider(skills, arc_provider)
455}
456
457/// Build toolset with project path for CodeGraph integration.
458pub 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    // Add CodeGraph tools
464    tools.extend(codegraph::codegraph_tools(&project_path));
465    // Add workflow tools
466    tools.extend(workflow::workflow_tools());
467    tools
468}
469
470/// Build full toolset with provider and project path.
471pub 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    // Add CodeGraph tools only if initialized (CLI installed + .codegraph exists)
478    if codegraph::should_inject_codegraph_tools(&project_path) {
479        tools.extend(codegraph::codegraph_tools(&project_path));
480    }
481    // Add AI-powered workflow tools
482    tools.extend(workflow::workflow_tools_with_provider(provider));
483    tools
484}