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 read_history; // 读取历史追踪
14pub mod registry; // 工具注册中心
15pub mod search;
16pub mod skill;
17pub mod subagent_executor; // 子代理执行器
18pub mod task;
19pub mod todo_write;
20pub mod toolproxy; // 代理工具模块
21pub mod webfetch;
22pub mod websearch;
23pub mod workflow;
24pub mod write;
25
26// Re-export read history types
27pub use read_history::{MustReadFirstError, ReadHistoryTracker};
28
29// Re-export proxy types for convenience
30pub 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/// Context for tool definition generation
46/// Used to customize tool descriptions based on available features
47#[derive(Debug, Clone, Default)]
48pub struct ToolContext {
49    /// Whether CodeGraph tools are available
50    pub codegraph_available: bool,
51}
52
53/// Type alias for boxed tool
54pub 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    /// 是否为优先工具。true 时会在描述前添加 "[优先]" 提示,
62    /// 让 LLM 更倾向选择此工具。默认 false。
63    #[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    /// 获取发送给 LLM 的描述(带优先标记)
80    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    /// Get tool definition (must implement)
92    fn definition(&self) -> ToolDefinition;
93
94    /// Get tool definition with context (for dynamic descriptions)
95    ///
96    /// Default implementation calls definition(). Override this method
97    /// if you need context-aware descriptions (e.g., different text
98    /// when CodeGraph is available).
99    fn definition_with_context(&self, _ctx: &ToolContext) -> ToolDefinition {
100        self.definition()
101    }
102
103    async fn execute(&self, params: Value) -> Result<String>;
104
105    /// Risk level of this tool. Defaults to Safe (read-only).
106    /// Override in tools that modify state.
107    fn risk_level(&self) -> RiskLevel {
108        RiskLevel::Safe
109    }
110}
111
112/// Default toolset without any skill integration. Kept for callers
113/// (and the existing tests) that don't care about skills.
114pub fn all_tools() -> Vec<Box<dyn Tool>> {
115    all_tools_with_skills(Arc::new(Vec::new()))
116}
117
118/// Base toolset without workflow tools (to avoid duplicates).
119fn 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
147/// Build the toolset with skill support but without provider.
148pub fn all_tools_with_skills(skills: Arc<Vec<Skill>>) -> Vec<Box<dyn Tool>> {
149    let mut tools = base_tools(skills);
150    // Add workflow tools without provider
151    tools.extend(workflow::workflow_tools());
152    tools
153}
154
155/// Build toolset with Provider for AI-powered tools.
156pub 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    // Add AI-powered workflow tools (with provider)
162    tools.extend(workflow::workflow_tools_with_provider(provider));
163    tools
164}
165
166/// Generate tools description for system prompt
167pub fn generate_tools_prompt() -> String {
168    generate_tools_prompt_with_path_and_lsp(None, None)
169}
170
171/// Generate tools description with optional CodeGraph support
172pub fn generate_tools_prompt_with_path(project_path: Option<&PathBuf>) -> String {
173    generate_tools_prompt_with_path_and_lsp(project_path, None)
174}
175
176/// Generate tools description with optional CodeGraph and LSP support
177pub fn generate_tools_prompt_with_path_and_lsp(
178    project_path: Option<&PathBuf>,
179    lsp_registry: Option<Arc<crate::lsp::LspClientRegistry>>,
180) -> String {
181    // Build tool context based on CodeGraph availability
182    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    // Add CodeGraph tools only if initialized (CLI installed + .codegraph exists)
191    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    // Add LSP tools if registry is provided
198    if let Some(registry) = lsp_registry {
199        tools.extend(crate::lsp::tools::lsp_tools(registry));
200    }
201
202    // Add workflow tools
203    tools.extend(workflow::workflow_tools());
204
205    // 🎯 分类显示:优先工具 + 其他工具
206    let mut priority_tools = Vec::new();
207    let mut normal_tools = Vec::new();
208
209    for tool in tools {
210        // Use definition_with_context for dynamic descriptions
211        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    // 优先工具(完整描述,包含适用场景)
222    if !priority_tools.is_empty() {
223        lines.push("\n【优先工具 - 必须优先考虑】".to_string());
224        for def in priority_tools {
225            // 使用 description_for_llm() 自动添加 [优先] 标记
226            let full_desc = def.description_for_llm();
227            // 优先工具保留完整描述(最多150字符)
228            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    // 其他工具(简要描述)
242    if !normal_tools.is_empty() {
243        lines.push("\n【其他工具】".to_string());
244        for def in normal_tools {
245            // 其他工具保持简要描述(前60字符)
246            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        // Verify workflow tools are present
278        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        // Verify workflow tools appear in prompt
297        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        // CodeGraph tools are only included when:
317        // 1. CodeGraph CLI is installed
318        // 2. Project has .codegraph directory
319        // So we check based on actual conditions
320        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            // When conditions not met, codegraph tools should NOT appear
331            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        // Verify codegraph tools NOT in prompt without path
343        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        // Without CodeGraph - should suggest using grep for definitions
354        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        // With CodeGraph - should recommend code_search
368        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        // Without CodeGraph
387        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        // With CodeGraph
397        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        // Without CodeGraph
412        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        // With CodeGraph
422        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        // Check based on actual CodeGraph availability
438        if codegraph::should_inject_codegraph_tools(&path) {
439            // When CodeGraph is available, grep should mention code_search
440            assert!(
441                prompt.contains("code_search") || prompt.contains("grep"),
442                "Prompt should contain grep tool"
443            );
444        }
445
446        // Both grep and search should always be present
447        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
456/// Build toolset with Arc Provider (preferred method)
457pub 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
464/// Build toolset with Box Provider (for CLI compatibility - safe implementation)
465/// Uses clone_arc to safely convert Box to Arc without unsafe code.
466pub 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    // Safe conversion: clone_arc creates a new Arc without unsafe pointer manipulation
471    let arc_provider = boxed_provider.clone_arc();
472    all_tools_with_provider(skills, arc_provider)
473}
474
475/// Build toolset with project path for CodeGraph integration.
476pub 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
483/// Build toolset with project path and optional LSP registry.
484pub 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    // Add CodeGraph tools
491    tools.extend(codegraph::codegraph_tools(&project_path));
492    // Add LSP tools if registry is provided
493    if let Some(registry) = lsp_registry {
494        tools.extend(crate::lsp::tools::lsp_tools(registry));
495    }
496    // Add workflow tools
497    tools.extend(workflow::workflow_tools());
498    tools
499}
500
501/// Build full toolset with provider and project path.
502pub 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
510/// Build full toolset with provider, project path, and optional LSP registry.
511pub 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    // Add CodeGraph tools only if initialized (CLI installed + .codegraph exists)
519    if codegraph::should_inject_codegraph_tools(&project_path) {
520        tools.extend(codegraph::codegraph_tools(&project_path));
521    }
522    // Add LSP tools if registry is provided
523    if let Some(registry) = lsp_registry {
524        tools.extend(crate::lsp::tools::lsp_tools(registry));
525    }
526    // Add AI-powered workflow tools
527    tools.extend(workflow::workflow_tools_with_provider(provider));
528    tools
529}