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