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