Skip to main content

vtcode_core/tools/
traits.rs

1//! Core traits for the composable tool system
2
3use anyhow::Result;
4use async_trait::async_trait;
5use serde_json::Value;
6use std::borrow::Cow;
7use std::path::PathBuf;
8use std::sync::Arc;
9use vtcode_commons::serde_helpers::json_to_string_pretty;
10
11use crate::tool_policy::ToolPolicy;
12use crate::tools::result::ToolResult as SplitToolResult;
13
14/// Core trait for all agent tools
15#[async_trait]
16pub trait Tool: Send + Sync {
17    /// Execute the tool with given arguments
18    ///
19    /// Returns a JSON Value for backward compatibility.
20    /// For new tools, consider implementing `execute_dual()` instead.
21    async fn execute(&self, args: Value) -> Result<Value>;
22
23    /// Execute with dual-channel output (LLM summary + UI content)
24    ///
25    /// This method enables significant token savings by separating:
26    /// - `llm_content`: Concise summary sent to LLM context (token-optimized)
27    /// - `ui_content`: Rich output displayed to user (full details)
28    ///
29    /// Default implementation wraps single-channel `execute()` result for backward compatibility.
30    /// Tools can override this to provide optimized dual output.
31    ///
32    /// # Example
33    /// ```rust,no_run
34    /// use vtcode_core::tools::result::ToolResult as SplitToolResult;
35    /// use serde_json::Value;
36    /// use anyhow::Result;
37    ///
38    /// async fn execute_dual(&self, args: Value) -> Result<SplitToolResult> {
39    ///     let full_output = "127 matches across 2,500 tokens...";
40    ///     let summary = "Found 127 matches in 15 files. Key: src/tools/grep.rs (3)";
41    ///     Ok(SplitToolResult::new(self.name(), summary, full_output))
42    /// }
43    /// ```
44    async fn execute_dual(&self, args: Value) -> Result<SplitToolResult> {
45        // Default: wrap single-channel result for backward compatibility
46        let result = self.execute(args).await?;
47
48        // Convert JSON Value to string for dual output
49        let content = if result.is_string() {
50            result.as_str().unwrap_or("").to_string()
51        } else {
52            json_to_string_pretty(&result)
53        };
54
55        Ok(SplitToolResult::simple(self.name(), content))
56    }
57
58    /// Get the tool's name
59    fn name(&self) -> &str;
60
61    /// Get the tool's description
62    fn description(&self) -> &str;
63
64    /// Validate arguments before execution
65    fn validate_args(&self, _args: &Value) -> Result<()> {
66        // Default implementation - tools can override for specific validation
67        Ok(())
68    }
69
70    /// Optional JSON schema for the tool's parameters, if available.
71    fn parameter_schema(&self) -> Option<Value> {
72        None
73    }
74
75    /// Optional JSON schema for the tool's configuration, if available.
76    fn config_schema(&self) -> Option<Value> {
77        None
78    }
79
80    /// Optional JSON schema describing state persisted by the tool, if any.
81    fn state_schema(&self) -> Option<Value> {
82        None
83    }
84
85    /// Optional prompt path metadata (e.g., for loading companion prompts).
86    fn prompt_path(&self) -> Option<Cow<'static, str>> {
87        None
88    }
89
90    /// Default execution policy for this tool.
91    fn default_permission(&self) -> ToolPolicy {
92        ToolPolicy::Prompt
93    }
94
95    /// Optional allowlist patterns the tool considers pre-approved.
96    fn allow_patterns(&self) -> Option<&'static [&'static str]> {
97        None
98    }
99
100    /// Optional denylist patterns the tool considers blocked.
101    fn deny_patterns(&self) -> Option<&'static [&'static str]> {
102        None
103    }
104
105    // ──────────────────────────────────────────────────────────────
106    // Codex-inspired methods for execution policy and parallel safety
107    // ──────────────────────────────────────────────────────────────
108
109    /// Whether this tool mutates state (files, environment, etc).
110    ///
111    /// Mutating tools require more careful policy evaluation and typically
112    /// cannot be run in parallel with other tools that touch the same resources.
113    ///
114    /// Default: true (conservative — assume mutation unless overridden).
115    ///
116    /// Per the Rust Patterns guide (Ch 7 — "accept the weakest bound your API
117    /// needs"), read-only tools SHOULD override this to return `false`. The
118    /// conservative default exists because accidentally treating a mutating tool
119    /// as read-only is worse than the reverse.
120    fn is_mutating(&self) -> bool {
121        true
122    }
123
124    /// Whether this tool is safe to run in parallel with other tools.
125    ///
126    /// Non-mutating read-only tools can often run in parallel.
127    /// Mutating tools should generally return false.
128    ///
129    /// Default: opposite of is_mutating()
130    fn is_parallel_safe(&self) -> bool {
131        !self.is_mutating()
132    }
133
134    /// Get the kind/category of this tool for matching against policies.
135    ///
136    /// Used by ExecPolicyManager to apply category-level rules.
137    /// Common kinds: "shell", "file", "search", "network", "system"
138    fn kind(&self) -> &'static str {
139        "unknown"
140    }
141
142    /// Check if this tool matches a given kind pattern.
143    ///
144    /// Supports exact matches and wildcard patterns.
145    fn matches_kind(&self, pattern: &str) -> bool {
146        if pattern == "*" {
147            return true;
148        }
149        if let Some(prefix) = pattern.strip_suffix('*') {
150            return self.kind().starts_with(prefix);
151        }
152        self.kind() == pattern
153    }
154
155    /// Resources this tool might access (paths, URLs, etc).
156    ///
157    /// Used for conflict detection in parallel execution planning.
158    fn resource_hints(&self, _args: &Value) -> Vec<String> {
159        Vec::new()
160    }
161
162    /// Estimated execution cost (1-10 scale).
163    ///
164    /// Used for scheduling and resource management.
165    /// 1 = instant, 5 = moderate, 10 = expensive/long-running
166    fn execution_cost(&self) -> u8 {
167        5
168    }
169
170    /// Resolve a path relative to workspace root and validate it is within bounds
171    async fn resolve_and_validate_path(
172        &self,
173        workspace_root: &std::path::Path,
174        path: &str,
175    ) -> Result<PathBuf> {
176        crate::tools::validation::unified_path::validate_and_resolve_path(workspace_root, path)
177            .await
178    }
179}
180
181/// Trait for tools that operate on files
182#[async_trait]
183pub trait FileTool: Tool {
184    /// Get the workspace root
185    fn workspace_root(&self) -> &PathBuf;
186
187    /// Check if a path should be excluded
188    async fn should_exclude(&self, path: &std::path::Path) -> bool;
189}
190
191/// Trait for tools that support multiple execution modes
192#[async_trait]
193pub trait ModeTool: Tool {
194    /// Get supported modes
195    fn supported_modes(&self) -> Vec<&'static str>;
196
197    /// Execute with specific mode
198    async fn execute_mode(&self, mode: &str, args: Value) -> Result<Value>;
199}
200
201/// Trait for caching tool results
202#[async_trait]
203pub trait CacheableTool: Tool {
204    /// Generate cache key for given arguments.
205    ///
206    /// Default implementation combines the tool name with a hash of the
207    /// serialized arguments. The key is stable within a single process
208    /// run but may differ across restarts (uses `DefaultHasher`). Override
209    /// for tools that need a custom key strategy.
210    fn cache_key(&self, args: &Value) -> String {
211        use std::collections::hash_map::DefaultHasher;
212        use std::hash::{Hash, Hasher};
213
214        let mut hasher = DefaultHasher::new();
215        // Hash the canonical string representation of the JSON args.
216        let args_str = serde_json::to_string(args).unwrap_or_default();
217        args_str.hash(&mut hasher);
218        format!("{}:{:016x}", self.name(), hasher.finish())
219    }
220
221    /// Check if result should be cached
222    fn should_cache(&self, _args: &Value) -> bool {
223        true // Default: cache everything
224    }
225
226    /// Get cache TTL in seconds
227    fn cache_ttl(&self) -> u64 {
228        300 // Default: 5 minutes
229    }
230}
231
232/// Main tool executor that coordinates all tools
233#[async_trait]
234pub trait ToolExecutor: Send + Sync {
235    /// Execute a tool by name
236    async fn execute_tool(&self, name: &str, args: Value) -> Result<Value>;
237
238    /// Execute a tool with a reference to arguments to avoid cloning when caller
239    /// already holds a reference.
240    async fn execute_tool_ref(&self, name: &str, args: &Value) -> Result<Value> {
241        self.execute_tool(name, args.clone()).await
242    }
243
244    /// Execute a tool and return a shared result (Arc) to avoid cloning results
245    /// for callers that want to keep a shared reference.
246    async fn execute_shared(&self, name: &str, args: Arc<Value>) -> Result<Arc<Value>> {
247        let res = self
248            .execute_tool(
249                name,
250                Arc::try_unwrap(args).unwrap_or_else(|arc| (*arc).clone()),
251            )
252            .await?;
253        Ok(Arc::new(res))
254    }
255
256    /// List available tools
257    async fn available_tools(&self) -> Vec<String>;
258
259    /// Check if a tool exists
260    async fn has_tool(&self, name: &str) -> bool;
261
262    /// Execute multiple tools in batch.
263    ///
264    /// The default implementation runs them sequentially.
265    /// Implementors can override this to provide parallel execution.
266    async fn execute_batch(&self, calls: Vec<(String, Value)>) -> Vec<Result<Value>> {
267        let futures = calls
268            .into_iter()
269            .map(|(name, args)| async move { self.execute_tool(&name, args).await });
270        futures::future::join_all(futures).await
271    }
272}