Skip to main content

vtcode_commons/
tool_types.rs

1#![expect(
2    clippy::cast_possible_truncation,
3    reason = "Quality scores are normalized to the documented 0..=100 display range before narrowing."
4)]
5
6//! Shared runtime types for the VT Code tool system.
7//!
8//! This module provides types shared between the LLM and tools subsystems,
9//! breaking the circular dependency that would otherwise exist between them.
10//!
11//! # Overview
12//!
13//! The key types here are:
14//! - [`CompactStr`] - stack-allocated string for short tool names
15//! - [`EnhancedToolResult`] - tool result with quality metadata
16//! - [`ResultMetadata`] - quality/confidence scoring for tool results
17//! - [`tool_names`] - tool name constants used across subsystems
18
19// ---------------------------------------------------------------------------
20// CompactStr type alias
21// ---------------------------------------------------------------------------
22
23/// Compact inline string -- stack-allocated for strings up to 24 bytes.
24/// Drop-in replacement for `String` with zero heap allocation for short strings.
25pub type CompactStr = compact_str::CompactString;
26
27// ---------------------------------------------------------------------------
28// Tool name constants (canonical names used across subsystems)
29// ---------------------------------------------------------------------------
30
31/// Canonical tool name constants used by both LLM and tools subsystems.
32/// These match the values defined in `vtcode-config::constants::tools`.
33pub mod tool_names {
34    /// Advanced bounded source search tool
35    pub const CODE_SEARCH: &str = "code_search";
36    /// Shell command execution tool
37    pub const EXEC_COMMAND: &str = "exec_command";
38}
39
40/// Use direct tool name without alias resolution.
41/// Alias resolution is now handled by the tool registry inventory
42/// which maintains a mapping of aliases to canonical tool names.
43pub const fn canonical_tool_name(name: &str) -> &str {
44    name
45}
46
47// ---------------------------------------------------------------------------
48// Operational constants shared across subsystems
49// ---------------------------------------------------------------------------
50
51/// Standard error patterns used for error detection across tools
52pub const ERROR_DETECTION_PATTERNS: &[&str] = &[
53    "error",
54    "failed",
55    "exception",
56    "permission denied",
57    "not found",
58    "no such file",
59    "cannot",
60    "could not",
61    "panic",
62    "crash",
63    "unhandled",
64    "fatal",
65    "timeout",
66    "connection refused",
67    "access denied",
68    "stack trace",
69    "traceback",
70    "abort",
71    "terminate",
72];
73
74/// Network-related error patterns for more specific error detection
75pub const NETWORK_ERROR_PATTERNS: &[&str] = &["connection", "timeout", "network", "http", "ssl", "tls", "dns", "proxy"];
76
77/// Default capacity hints for common collections
78pub const DEFAULT_VEC_CAPACITY: usize = 32;
79pub const DEFAULT_HASHMAP_CAPACITY: usize = 16;
80pub const DEFAULT_STRING_CAPACITY: usize = 256;
81
82/// Context optimization constants following AGENTS.md guidelines
83pub const MAX_SEARCH_RESULTS: usize = 5;
84pub const MAX_LIST_ITEMS_SUMMARY: usize = 5;
85pub const OVERFLOW_INDICATOR_PREFIX: &str = "[+]";
86pub const OVERFLOW_INDICATOR_SUFFIX: &str = "more items]";
87
88/// Common tool operation limits
89pub const MAX_FILE_SIZE_FOR_PROCESSING: usize = 100 * 1024 * 1024; // 100MB
90pub const MAX_CONTEXT_LINES: usize = 20;
91pub const MAX_OUTPUT_TOKENS: usize = 4000;
92
93/// Reusable empty JSON object schema `{"type": "object"}` for tool parameter definitions.
94/// Used by tools that accept no parameters or only optional parameters.
95pub fn empty_object_schema() -> Value {
96    serde_json::json!({"type": "object"})
97}
98
99// ---------------------------------------------------------------------------
100// Tool result metadata types
101// ---------------------------------------------------------------------------
102
103use hashbrown::HashMap;
104use serde::{Deserialize, Serialize};
105use serde_json::Value;
106use std::fmt;
107use std::time::SystemTime;
108
109/// Result completeness level
110#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
111pub enum ResultCompleteness {
112    /// Full result with no truncation
113    Complete,
114    /// Partial result (more data exists but not shown)
115    Partial,
116    /// Result truncated due to size limits
117    Truncated,
118    /// Empty result (no matches)
119    Empty,
120}
121
122impl ResultCompleteness {
123    /// Deprecated: prefer using the `Display` impl; `ToString` is derived from Display.
124    fn to_static_str(self) -> &'static str {
125        match self {
126            Self::Complete => "complete",
127            Self::Partial => "partial",
128            Self::Truncated => "truncated",
129            Self::Empty => "empty",
130        }
131    }
132}
133
134impl fmt::Display for ResultCompleteness {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        f.write_str(self.to_static_str())
137    }
138}
139
140/// Quality metadata for tool results
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ResultMetadata {
143    /// Confidence that result is correct (0.0-1.0)
144    #[serde(default = "default_confidence")]
145    pub confidence: f32,
146
147    /// Relevance to current task (0.0-1.0)
148    #[serde(default = "default_relevance")]
149    pub relevance: f32,
150
151    /// Result completeness level
152    pub completeness: ResultCompleteness,
153
154    /// Count of matches/results
155    #[serde(default)]
156    pub result_count: usize,
157
158    /// Likelihood of false positives (0.0-1.0)
159    #[serde(default)]
160    pub false_positive_likelihood: f32,
161
162    /// Detected content types (code, docs, config, binary, etc.)
163    #[serde(default)]
164    pub content_types: Vec<String>,
165
166    /// Tool-specific metrics (lines matched, execution time, etc.)
167    #[serde(default)]
168    pub tool_metrics: HashMap<String, Value>,
169}
170
171fn default_confidence() -> f32 {
172    0.5
173}
174
175fn default_relevance() -> f32 {
176    0.5
177}
178
179impl Default for ResultMetadata {
180    fn default() -> Self {
181        Self {
182            confidence: 0.5,
183            relevance: 0.5,
184            completeness: ResultCompleteness::Complete,
185            result_count: 0,
186            false_positive_likelihood: 0.1,
187            content_types: vec![],
188            tool_metrics: HashMap::new(),
189        }
190    }
191}
192
193impl ResultMetadata {
194    /// Overall quality score (0.0-1.0)
195    #[inline]
196    pub fn quality_score(&self) -> f32 {
197        let weighted = (self.confidence * 0.4) + (self.relevance * 0.4) + (self.false_positive_likelihood * -0.2);
198        weighted.clamp(0.0, 1.0)
199    }
200
201    /// Create metadata for a successful tool execution
202    #[inline]
203    pub fn success(confidence: f32, relevance: f32) -> Self {
204        Self {
205            confidence: confidence.clamp(0.0, 1.0),
206            relevance: relevance.clamp(0.0, 1.0),
207            completeness: ResultCompleteness::Complete,
208            result_count: 1,
209            false_positive_likelihood: 0.05,
210            ..Default::default()
211        }
212    }
213
214    /// Create metadata for empty results
215    #[inline]
216    pub fn empty() -> Self {
217        Self {
218            completeness: ResultCompleteness::Empty,
219            result_count: 0,
220            confidence: 1.0, // High confidence in "no results"
221            ..Default::default()
222        }
223    }
224
225    /// Create metadata for error/inconclusive results
226    pub fn error() -> Self {
227        Self {
228            confidence: 0.2,
229            completeness: ResultCompleteness::Empty,
230            ..Default::default()
231        }
232    }
233
234    /// Merge with another metadata (for combining results)
235    pub fn merge(&mut self, other: &ResultMetadata) {
236        self.result_count += other.result_count;
237        self.confidence = (self.confidence + other.confidence) / 2.0;
238        self.relevance = (self.relevance + other.relevance) / 2.0;
239
240        // Merge content types
241        for ct in &other.content_types {
242            if !self.content_types.contains(ct) {
243                self.content_types.push(ct.clone());
244            }
245        }
246
247        // Merge tool metrics - use extend to avoid double clone
248        self.tool_metrics
249            .extend(other.tool_metrics.iter().map(|(k, v)| (k.clone(), v.clone())));
250    }
251}
252
253/// Enhanced tool result with metadata
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct EnhancedToolResult {
256    /// The actual tool result
257    value: Value,
258
259    /// Quality metadata
260    pub metadata: ResultMetadata,
261
262    /// When result was produced
263    timestamp: u64,
264
265    /// Tool name that produced this
266    tool_name: CompactStr,
267
268    /// Whether this was from cache
269    #[serde(default)]
270    from_cache: bool,
271}
272
273impl EnhancedToolResult {
274    pub fn new(value: Value, metadata: ResultMetadata, tool_name: impl Into<CompactStr>) -> Self {
275        Self {
276            value,
277            metadata,
278            timestamp: SystemTime::now()
279                .duration_since(SystemTime::UNIX_EPOCH)
280                .unwrap_or_default()
281                .as_secs(),
282            tool_name: tool_name.into(),
283            from_cache: false,
284        }
285    }
286
287    pub fn from_cache(value: Value, metadata: ResultMetadata, tool_name: impl Into<CompactStr>) -> Self {
288        Self {
289            value,
290            metadata,
291            timestamp: SystemTime::now()
292                .duration_since(SystemTime::UNIX_EPOCH)
293                .unwrap_or_default()
294                .as_secs(),
295            tool_name: tool_name.into(),
296            from_cache: true,
297        }
298    }
299
300    /// Whether this result is useful enough to include
301    #[inline]
302    pub fn is_useful(&self) -> bool {
303        self.metadata.quality_score() > 0.3
304    }
305
306    /// Whether this result is high quality
307    #[inline]
308    pub fn is_high_quality(&self) -> bool {
309        self.metadata.quality_score() > 0.7
310    }
311
312    /// Convert to a message-friendly format
313    #[allow(
314        clippy::cast_sign_loss,
315        reason = "Intentional compatibility, platform, or test-only suppression."
316    )] // quality_score is always 0.0-1.0
317    pub fn to_summary(&self) -> String {
318        let quality = ((self.metadata.quality_score() * 100.0).round().max(0.0) as u32).min(100);
319        match self.metadata.completeness {
320            ResultCompleteness::Complete => {
321                format!("{} found {} results (confidence: {}%)", self.tool_name, self.metadata.result_count, quality)
322            }
323            ResultCompleteness::Partial => {
324                format!(
325                    "{} found {} results (truncated, confidence: {}%)",
326                    self.tool_name, self.metadata.result_count, quality
327                )
328            }
329            ResultCompleteness::Empty => {
330                format!("{} found no results", self.tool_name)
331            }
332            ResultCompleteness::Truncated => {
333                format!("{} found results (truncated due to size, confidence: {}%)", self.tool_name, quality)
334            }
335        }
336    }
337}
338
339/// Trait for scoring tool results
340pub trait ResultScorer {
341    /// Score a tool result and return metadata
342    fn score(&self, result: &Value) -> ResultMetadata;
343
344    /// Tool name this scorer handles
345    fn tool_name(&self) -> &str;
346}