Skip to main content

matrixcode_core/tools/
mod.rs

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