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}