Skip to main content

vtcode_core/tools/
cached_executor.rs

1//! Production ToolExecutor: Cache + Middleware + Patterns integrated.
2//!
3//! Drop-in replacement for tool execution with full observability.
4//! Stats tracking uses lock-free `AtomicU64` counters to avoid contention.
5
6use crate::config::constants::execution;
7use crate::tools::lru_cache::LruCache;
8use crate::tools::pattern_detection::{PatternDetector, ToolEvent};
9use crate::tools::tool_middleware::{MiddlewareChain, MiddlewareResult, ToolRequest, ToolResponse};
10use crate::tools::{UnifiedErrorKind, UnifiedToolError};
11use serde_json::Value;
12use std::collections::hash_map::DefaultHasher;
13use std::hash::Hasher;
14use std::sync::Arc;
15use std::sync::RwLock;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::time::Duration;
18use tokio::time::timeout;
19use tracing::warn;
20
21static REQUEST_ID: AtomicU64 = AtomicU64::new(1);
22
23fn next_request_id() -> String {
24    let id = REQUEST_ID.fetch_add(1, Ordering::Relaxed);
25    format!("req-{id}")
26}
27
28/// Cache-line padded atomic counter for stats updated by concurrent tool runs.
29///
30/// Padding keeps independent counters from sharing a cache line, avoiding the
31/// false-sharing slowdown that can otherwise appear under high parallelism.
32#[repr(align(64))]
33struct PaddedAtomicU64(AtomicU64);
34
35impl PaddedAtomicU64 {
36    fn new(value: u64) -> Self {
37        Self(AtomicU64::new(value))
38    }
39
40    #[inline]
41    fn fetch_add(&self, value: u64, ordering: Ordering) -> u64 {
42        self.0.fetch_add(value, ordering)
43    }
44
45    #[inline]
46    fn load(&self, ordering: Ordering) -> u64 {
47        self.0.load(ordering)
48    }
49}
50
51/// Thread-safe snapshot of executor state.
52#[derive(Clone, Debug)]
53pub struct ExecutorStats {
54    pub total_calls: u64,
55    pub successful_calls: u64,
56    pub failed_calls: u64,
57    pub cache_hits: u64,
58    pub cache_misses: u64,
59    pub avg_duration_ms: u64,
60    pub patterns_detected: usize,
61}
62
63/// Lock-free atomic counters for executor stats.
64///
65/// Avoids RwLock contention on the hot path. Duration tracking uses
66/// total + count so the average is computed accurately on read.
67struct AtomicStats {
68    total_calls: PaddedAtomicU64,
69    successful_calls: PaddedAtomicU64,
70    failed_calls: PaddedAtomicU64,
71    cache_hits: PaddedAtomicU64,
72    cache_misses: PaddedAtomicU64,
73    total_duration_ms: PaddedAtomicU64,
74    duration_count: PaddedAtomicU64,
75}
76
77impl AtomicStats {
78    fn new() -> Self {
79        Self {
80            total_calls: PaddedAtomicU64::new(0),
81            successful_calls: PaddedAtomicU64::new(0),
82            failed_calls: PaddedAtomicU64::new(0),
83            cache_hits: PaddedAtomicU64::new(0),
84            cache_misses: PaddedAtomicU64::new(0),
85            total_duration_ms: PaddedAtomicU64::new(0),
86            duration_count: PaddedAtomicU64::new(0),
87        }
88    }
89
90    #[inline]
91    fn record_success(&self, duration_ms: u64) {
92        self.successful_calls.fetch_add(1, Ordering::Relaxed);
93        self.total_duration_ms.fetch_add(duration_ms, Ordering::Relaxed);
94        self.duration_count.fetch_add(1, Ordering::Relaxed);
95    }
96
97    #[inline]
98    fn record_failure(&self, duration_ms: u64) {
99        self.failed_calls.fetch_add(1, Ordering::Relaxed);
100        self.total_duration_ms.fetch_add(duration_ms, Ordering::Relaxed);
101        self.duration_count.fetch_add(1, Ordering::Relaxed);
102    }
103
104    fn snapshot(&self) -> ExecutorStats {
105        let count = self.duration_count.load(Ordering::Relaxed);
106        let total = self.total_duration_ms.load(Ordering::Relaxed);
107        let avg = total.checked_div(count).unwrap_or(0);
108        ExecutorStats {
109            total_calls: self.total_calls.load(Ordering::Relaxed),
110            successful_calls: self.successful_calls.load(Ordering::Relaxed),
111            failed_calls: self.failed_calls.load(Ordering::Relaxed),
112            cache_hits: self.cache_hits.load(Ordering::Relaxed),
113            cache_misses: self.cache_misses.load(Ordering::Relaxed),
114            avg_duration_ms: avg,
115            patterns_detected: 0, // filled in by caller
116        }
117    }
118}
119
120/// Production tool executor with cache, middleware, and pattern detection.
121pub struct CachedToolExecutor {
122    /// Response cache (key = "tool_name:args_json")
123    cache: Arc<LruCache<Value>>,
124    /// Composable middleware chain
125    middleware: MiddlewareChain,
126    /// Pattern detector for workflow analysis.
127    ///
128    /// Uses `std::sync::RwLock` instead of `tokio::sync::RwLock` because:
129    /// 1. Critical sections are very short (just calling methods on PatternDetector)
130    /// 2. No async operations occur inside the lock
131    /// 3. `std::sync::RwLock` has lower overhead for short critical sections
132    ///
133    /// This is safe because the lock is never held across await points.
134    /// If critical sections become longer or need async operations, migrate to
135    /// `tokio::sync::RwLock`.
136    patterns: Arc<RwLock<PatternDetector>>,
137    /// Lock-free stats tracking
138    stats: Arc<AtomicStats>,
139}
140
141impl CachedToolExecutor {
142    /// Create a new executor with default settings.
143    ///
144    /// - Cache capacity: 1000 entries
145    /// - Cache TTL: 1 hour
146    /// - Pattern window: 3-tool sequences
147    pub fn new() -> Self {
148        Self::with_config(1000, Duration::from_secs(3600), 3)
149    }
150
151    /// Create executor with custom cache and pattern settings.
152    pub fn with_config(cache_capacity: usize, cache_ttl: Duration, pattern_window: usize) -> Self {
153        let cache = Arc::new(LruCache::<Value>::new(cache_capacity, cache_ttl));
154        let middleware = MiddlewareChain::new();
155        let patterns = Arc::new(RwLock::new(PatternDetector::new(pattern_window)));
156        let stats = Arc::new(AtomicStats::new());
157
158        Self { cache, middleware, patterns, stats }
159    }
160
161    /// Add middleware to the chain.
162    pub fn with_middleware(mut self, mw: Arc<dyn crate::tools::tool_middleware::Middleware>) -> Self {
163        self.middleware = self.middleware.push(mw);
164        self
165    }
166
167    /// Execute a tool with full caching and observability.
168    pub async fn execute(&self, tool_name: &str, args: Value) -> MiddlewareResult<Value> {
169        // Delegate to the shared-version and convert to an owned Value
170        let r = self.execute_shared_owned(tool_name, args).await?;
171        Ok((*r).clone())
172    }
173
174    /// Execute a tool but return a shared (Arc) response to avoid clones.
175    /// Accepts a shared `Arc<Value>` to avoid cloning arg contents when caller
176    /// already holds a shared reference.
177    pub async fn execute_shared(&self, tool_name: &str, args: Arc<Value>) -> MiddlewareResult<Arc<Value>> {
178        let start = std::time::Instant::now();
179        let cache_key = make_cache_key(tool_name, &args);
180
181        // Atomic increment — no lock needed
182        self.stats.total_calls.fetch_add(1, Ordering::Relaxed);
183
184        // Create request — reuse the shared `args` (already an Arc)
185        let owned_args = Arc::clone(&args);
186        let req = ToolRequest {
187            id: next_request_id(),
188            tool_name: tool_name.into(),
189            args: (*owned_args).clone(),
190            metadata: Some(Default::default()),
191        };
192
193        // Before hooks
194        if let Err(err) = self.middleware.before_execute(&req).await {
195            self.record_error(tool_name, start.elapsed(), &req, &err).await;
196            return Err(err);
197        }
198
199        // Check cache
200        if let Some(result) = self.cache.get(&cache_key).await {
201            let duration_ms = start.elapsed().as_millis() as u64;
202            self.stats.record_success(duration_ms);
203            self.stats.cache_hits.fetch_add(1, Ordering::Relaxed);
204
205            let res = ToolResponse {
206                id: req.id.clone(),
207                success: true,
208                result: Some((*result).clone()),
209                error: None,
210                duration_ms: Some(duration_ms),
211                cache_hit: Some(true),
212            };
213            if let Err(err) = self.middleware.after_execute(&req, &res).await {
214                self.record_error(tool_name, start.elapsed(), &req, &err).await;
215                return Err(err);
216            }
217
218            // Record pattern
219            self.record_pattern(tool_name, true, duration_ms).await;
220
221            return Ok(Arc::clone(&result));
222        }
223
224        // Atomic increment — no lock needed
225        self.stats.cache_misses.fetch_add(1, Ordering::Relaxed);
226
227        // Execute tool (caller provides actual execution)
228        let timeout_secs = execution::DEFAULT_TIMEOUT_SECS;
229        let result = match timeout(
230            Duration::from_secs(timeout_secs),
231            self.execute_tool_internal(tool_name, &owned_args),
232        )
233        .await
234        {
235            Ok(result) => match result {
236                Ok(result) => result,
237                Err(err) => {
238                    self.record_error(tool_name, start.elapsed(), &req, &err).await;
239                    return Err(err);
240                }
241            },
242            Err(_) => {
243                let err = UnifiedToolError::new(
244                    UnifiedErrorKind::Timeout,
245                    format!("Tool '{tool_name}' timed out after {timeout_secs} seconds"),
246                )
247                .with_tool_name(tool_name);
248                self.record_error(tool_name, start.elapsed(), &req, &err).await;
249                return Err(err);
250            }
251        };
252
253        let duration_ms = start.elapsed().as_millis() as u64;
254
255        // Wrap result in Arc once, then clone Arc for cache and response
256        let arc_res = Arc::new(result);
257
258        // Cache result (Arc clone is cheap - pass Arc directly into cache)
259        self.cache.insert_arc(cache_key, Arc::clone(&arc_res)).await;
260
261        // Atomic stats update — no lock needed
262        self.stats.record_success(duration_ms);
263
264        let res = ToolResponse {
265            id: req.id.clone(),
266            success: true,
267            result: Some((*arc_res).clone()),
268            error: None,
269            duration_ms: Some(duration_ms),
270            cache_hit: Some(false),
271        };
272
273        // After hooks
274        if let Err(err) = self.middleware.after_execute(&req, &res).await {
275            self.record_error(tool_name, start.elapsed(), &req, &err).await;
276            return Err(err);
277        }
278
279        // Record pattern
280        self.record_pattern(tool_name, true, duration_ms).await;
281
282        Ok(arc_res)
283    }
284
285    /// Backwards-compatible wrapper for callers that still pass an owned Value.
286    pub async fn execute_shared_owned(&self, tool_name: &str, args: Value) -> MiddlewareResult<Arc<Value>> {
287        let arg = Arc::new(args);
288        self.execute_shared(tool_name, arg).await
289    }
290
291    /// Execute tool (override this for real tool execution)
292    async fn execute_tool_internal(&self, _tool_name: &str, _args: &Value) -> MiddlewareResult<Value> {
293        // Default: return placeholder result
294        // In real usage, this would call ToolRegistry
295        Ok(serde_json::json!({"status": "ok"}))
296    }
297
298    async fn record_error(&self, tool_name: &str, elapsed: Duration, req: &ToolRequest, err: &UnifiedToolError) {
299        self.stats.record_failure(elapsed.as_millis() as u64);
300
301        let _ = self.middleware.on_error(req, err).await;
302        self.record_pattern(tool_name, false, elapsed.as_millis() as u64).await;
303    }
304
305    /// Record event in pattern detector
306    async fn record_pattern(&self, tool_name: &str, success: bool, duration_ms: u64) {
307        let mut patterns = self.patterns.write().unwrap_or_else(|poisoned| {
308            warn!("pattern detector lock poisoned; recovering");
309            poisoned.into_inner()
310        });
311        patterns.record_event(ToolEvent {
312            tool_name: tool_name.to_string(),
313            success,
314            duration_ms,
315            timestamp: std::time::Instant::now(),
316        });
317    }
318
319    /// Get current executor statistics.
320    pub async fn stats(&self) -> ExecutorStats {
321        let mut stats = self.stats.snapshot();
322        let patterns = self.patterns.read().unwrap_or_else(|poisoned| {
323            warn!("pattern detector lock poisoned; recovering");
324            poisoned.into_inner()
325        });
326        stats.patterns_detected = patterns.patterns().len();
327        stats
328    }
329
330    /// Get cache statistics.
331    pub async fn cache_stats(&self) -> crate::tools::lru_cache::CacheStats {
332        self.cache.stats().await
333    }
334
335    /// Get detected workflow patterns.
336    pub async fn patterns(&self) -> Vec<crate::tools::pattern_detection::DetectedPattern> {
337        let patterns = self.patterns.read().unwrap_or_else(|poisoned| {
338            warn!("pattern detector lock poisoned; recovering");
339            poisoned.into_inner()
340        });
341        patterns.patterns().to_vec()
342    }
343
344    /// Get ML-ready feature vector from patterns.
345    pub async fn feature_vector(&self) -> Vec<f64> {
346        let patterns = self.patterns.read().unwrap_or_else(|poisoned| {
347            warn!("pattern detector lock poisoned; recovering");
348            poisoned.into_inner()
349        });
350        patterns.feature_vector()
351    }
352
353    /// Clear cache
354    pub async fn clear_cache(&self) {
355        self.cache.clear().await;
356    }
357
358    /// Clear patterns
359    pub async fn clear_patterns(&self) {
360        let mut patterns = self.patterns.write().unwrap_or_else(|poisoned| {
361            warn!("pattern detector lock poisoned; recovering");
362            poisoned.into_inner()
363        });
364        patterns.reset();
365    }
366
367    /// Print execution report
368    pub async fn report(&self) {
369        let stats = self.stats().await;
370        let cache_stats = self.cache_stats().await;
371        let patterns = self.patterns().await;
372
373        println!("\n=== ToolExecutor Report ===\n");
374
375        println!("Execution Statistics:");
376        println!("  Total calls:      {}", stats.total_calls);
377        println!("  Successful:       {}", stats.successful_calls);
378        println!("  Failed:           {}", stats.failed_calls);
379        println!("  Avg duration:     {}ms", stats.avg_duration_ms);
380
381        println!("\nCache Performance:");
382        println!("  Hits:             {}", cache_stats.hits);
383        println!("  Misses:           {}", cache_stats.misses);
384        println!("  Hit rate:         {:.1}%", cache_stats.hit_rate());
385        println!("  Evictions:        {}", cache_stats.evictions);
386        println!("  Expirations:      {}", cache_stats.expirations);
387
388        println!("\nWorkflow Patterns ({} detected):", patterns.len());
389        for (i, pattern) in patterns.iter().take(5).enumerate() {
390            println!("  {}. {:?}", i + 1, pattern.sequence);
391            println!("     Frequency: {}, Confidence: {:.1}%", pattern.frequency, pattern.confidence * 100.0);
392        }
393
394        println!("\n");
395    }
396}
397
398// Helper for stable cache key generation — uses a small 64-bit hash of the
399// serialized JSON arguments to avoid storing large argument strings as cache
400// keys while still differentiating distinct argument payloads.
401#[inline]
402fn make_cache_key(tool_name: &str, args: &Value) -> String {
403    use std::hash::Hash;
404    let mut hasher = DefaultHasher::new();
405    // Hash the tool name first for better distribution.
406    tool_name.hash(&mut hasher);
407    // Use compact JSON serialization for hashing.
408    if let Ok(bytes) = serde_json::to_vec(args) {
409        hasher.write(&bytes);
410    }
411    let h = hasher.finish();
412    format!("{tool_name}:{h:x}")
413}
414
415// test above added to the test module further below
416
417impl Default for CachedToolExecutor {
418    fn default() -> Self {
419        Self::new()
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426    use crate::tools::tool_middleware::{LoggingMiddleware, MetricsMiddleware, MiddlewareResult};
427    use crate::tools::{UnifiedErrorKind, UnifiedToolError};
428    use async_trait::async_trait;
429    use std::sync::Arc;
430
431    struct FailingMiddleware;
432
433    #[async_trait]
434    impl crate::tools::tool_middleware::Middleware for FailingMiddleware {
435        async fn before_execute(&self, _req: &ToolRequest) -> MiddlewareResult<()> {
436            Err(UnifiedToolError::new(UnifiedErrorKind::ExecutionFailed, "middleware rejected request"))
437        }
438    }
439
440    #[tokio::test]
441    async fn test_executor_basic() -> anyhow::Result<()> {
442        let executor = CachedToolExecutor::new();
443
444        // First call - miss
445        let result = executor.execute("test_tool", serde_json::json!({"arg": 1})).await?;
446        assert_eq!(result, serde_json::json!({"status": "ok"}));
447
448        let stats = executor.stats().await;
449        assert_eq!(stats.total_calls, 1);
450        assert_eq!(stats.cache_misses, 1);
451
452        Ok(())
453    }
454
455    #[tokio::test]
456    async fn test_executor_cache_hit() -> anyhow::Result<()> {
457        let executor = CachedToolExecutor::new();
458
459        // Two identical calls
460        executor.execute("test_tool", serde_json::json!({"arg": 1})).await?;
461        executor.execute("test_tool", serde_json::json!({"arg": 1})).await?;
462
463        let stats = executor.stats().await;
464        assert_eq!(stats.total_calls, 2);
465        assert_eq!(stats.successful_calls, 2);
466        assert_eq!(stats.cache_hits, 1);
467        assert_eq!(stats.cache_misses, 1);
468
469        Ok(())
470    }
471
472    #[tokio::test]
473    async fn cache_hit_after_repeat_call() {
474        let exec = CachedToolExecutor::with_config(10, Duration::from_secs(60), 3);
475        let args = serde_json::json!({"x": 1});
476
477        // First call -> cache miss
478        let _first = exec.execute("test_tool", args.clone()).await.unwrap();
479
480        // Second call with same args -> should use cache
481        let _second = exec.execute("test_tool", args.clone()).await.unwrap();
482
483        // Check stats for cache hit/miss
484        let stats = exec.stats().await;
485        assert!(stats.cache_hits >= 1);
486        assert!(stats.cache_misses >= 1);
487    }
488
489    #[tokio::test]
490    async fn test_executor_with_middleware() -> anyhow::Result<()> {
491        let executor = CachedToolExecutor::new().with_middleware(LoggingMiddleware::new("test"));
492
493        executor.execute("test_tool", serde_json::json!({})).await?;
494
495        let stats = executor.stats().await;
496        assert_eq!(stats.total_calls, 1);
497
498        Ok(())
499    }
500
501    #[tokio::test]
502    async fn test_executor_patterns() -> anyhow::Result<()> {
503        let executor = CachedToolExecutor::new();
504
505        // Record a repeating A -> B pattern with enough events
506        // to trigger pattern analysis (ANALYZE_INTERVAL = 10).
507        for _ in 0..6 {
508            executor.execute("tool_a", serde_json::json!({})).await?;
509            executor.execute("tool_b", serde_json::json!({})).await?;
510        }
511
512        let patterns = executor.patterns().await;
513        assert!(!patterns.is_empty());
514
515        Ok(())
516    }
517
518    #[tokio::test]
519    async fn test_executor_clear() -> anyhow::Result<()> {
520        let executor = CachedToolExecutor::new();
521
522        executor.execute("test", serde_json::json!({})).await?;
523
524        let stats_before = executor.stats().await;
525        assert_eq!(stats_before.total_calls, 1);
526
527        executor.clear_cache().await;
528        executor.clear_patterns().await;
529
530        let cache_stats = executor.cache_stats().await;
531        assert_eq!(cache_stats.hits + cache_stats.misses, 0);
532
533        Ok(())
534    }
535
536    #[tokio::test]
537    async fn test_executor_reports_typed_error_to_middleware_metrics() {
538        let metrics = MetricsMiddleware::new();
539        let executor = CachedToolExecutor::new()
540            .with_middleware(metrics.clone())
541            .with_middleware(Arc::new(FailingMiddleware));
542
543        let err = executor.execute("test", serde_json::json!({})).await;
544        assert!(err.is_err());
545
546        let stats = executor.stats().await;
547        assert_eq!(stats.failed_calls, 1);
548
549        let snapshot = metrics.snapshot().await;
550        assert_eq!(snapshot.total_calls, 1);
551        assert_eq!(snapshot.failed_calls, 1);
552    }
553}