vtcode-core 0.136.3

Core library for VT Code - a Rust-based terminal coding agent
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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
//! Production ToolExecutor: Cache + Middleware + Patterns integrated.
//!
//! Drop-in replacement for tool execution with full observability.
//! Stats tracking uses lock-free `AtomicU64` counters to avoid contention.

use crate::config::constants::execution;
use crate::tools::lru_cache::LruCache;
use crate::tools::pattern_detection::{PatternDetector, ToolEvent};
use crate::tools::tool_middleware::{MiddlewareChain, MiddlewareResult, ToolRequest, ToolResponse};
use crate::tools::{UnifiedErrorKind, UnifiedToolError};
use serde_json::Value;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use tokio::time::timeout;
use tracing::warn;

static REQUEST_ID: AtomicU64 = AtomicU64::new(1);

fn next_request_id() -> String {
    let id = REQUEST_ID.fetch_add(1, Ordering::Relaxed);
    format!("req-{id}")
}

/// Cache-line padded atomic counter for stats updated by concurrent tool runs.
///
/// Padding keeps independent counters from sharing a cache line, avoiding the
/// false-sharing slowdown that can otherwise appear under high parallelism.
#[repr(align(64))]
struct PaddedAtomicU64(AtomicU64);

impl PaddedAtomicU64 {
    fn new(value: u64) -> Self {
        Self(AtomicU64::new(value))
    }

    #[inline]
    fn fetch_add(&self, value: u64, ordering: Ordering) -> u64 {
        self.0.fetch_add(value, ordering)
    }

    #[inline]
    fn load(&self, ordering: Ordering) -> u64 {
        self.0.load(ordering)
    }
}

/// Thread-safe snapshot of executor state.
#[derive(Clone, Debug)]
pub struct ExecutorStats {
    pub total_calls: u64,
    pub successful_calls: u64,
    pub failed_calls: u64,
    pub cache_hits: u64,
    pub cache_misses: u64,
    pub avg_duration_ms: u64,
    pub patterns_detected: usize,
}

/// Lock-free atomic counters for executor stats.
///
/// Avoids RwLock contention on the hot path. Duration tracking uses
/// total + count so the average is computed accurately on read.
struct AtomicStats {
    total_calls: PaddedAtomicU64,
    successful_calls: PaddedAtomicU64,
    failed_calls: PaddedAtomicU64,
    cache_hits: PaddedAtomicU64,
    cache_misses: PaddedAtomicU64,
    total_duration_ms: PaddedAtomicU64,
    duration_count: PaddedAtomicU64,
}

impl AtomicStats {
    fn new() -> Self {
        Self {
            total_calls: PaddedAtomicU64::new(0),
            successful_calls: PaddedAtomicU64::new(0),
            failed_calls: PaddedAtomicU64::new(0),
            cache_hits: PaddedAtomicU64::new(0),
            cache_misses: PaddedAtomicU64::new(0),
            total_duration_ms: PaddedAtomicU64::new(0),
            duration_count: PaddedAtomicU64::new(0),
        }
    }

    #[inline]
    fn record_success(&self, duration_ms: u64) {
        self.successful_calls.fetch_add(1, Ordering::Relaxed);
        self.total_duration_ms.fetch_add(duration_ms, Ordering::Relaxed);
        self.duration_count.fetch_add(1, Ordering::Relaxed);
    }

    #[inline]
    fn record_failure(&self, duration_ms: u64) {
        self.failed_calls.fetch_add(1, Ordering::Relaxed);
        self.total_duration_ms.fetch_add(duration_ms, Ordering::Relaxed);
        self.duration_count.fetch_add(1, Ordering::Relaxed);
    }

    fn snapshot(&self) -> ExecutorStats {
        let count = self.duration_count.load(Ordering::Relaxed);
        let total = self.total_duration_ms.load(Ordering::Relaxed);
        let avg = if count > 0 { total / count } else { 0 };
        ExecutorStats {
            total_calls: self.total_calls.load(Ordering::Relaxed),
            successful_calls: self.successful_calls.load(Ordering::Relaxed),
            failed_calls: self.failed_calls.load(Ordering::Relaxed),
            cache_hits: self.cache_hits.load(Ordering::Relaxed),
            cache_misses: self.cache_misses.load(Ordering::Relaxed),
            avg_duration_ms: avg,
            patterns_detected: 0, // filled in by caller
        }
    }
}

/// Production tool executor with cache, middleware, and pattern detection.
pub struct CachedToolExecutor {
    /// Response cache (key = "tool_name:args_json")
    cache: Arc<LruCache<Value>>,
    /// Composable middleware chain
    middleware: MiddlewareChain,
    /// Pattern detector for workflow analysis.
    ///
    /// Uses `std::sync::RwLock` instead of `tokio::sync::RwLock` because:
    /// 1. Critical sections are very short (just calling methods on PatternDetector)
    /// 2. No async operations occur inside the lock
    /// 3. `std::sync::RwLock` has lower overhead for short critical sections
    ///
    /// This is safe because the lock is never held across await points.
    /// If critical sections become longer or need async operations, migrate to
    /// `tokio::sync::RwLock`.
    patterns: Arc<RwLock<PatternDetector>>,
    /// Lock-free stats tracking
    stats: Arc<AtomicStats>,
}

impl CachedToolExecutor {
    /// Create a new executor with default settings.
    ///
    /// - Cache capacity: 1000 entries
    /// - Cache TTL: 1 hour
    /// - Pattern window: 3-tool sequences
    pub fn new() -> Self {
        Self::with_config(1000, Duration::from_secs(3600), 3)
    }

    /// Create executor with custom cache and pattern settings.
    pub fn with_config(cache_capacity: usize, cache_ttl: Duration, pattern_window: usize) -> Self {
        let cache = Arc::new(LruCache::<Value>::new(cache_capacity, cache_ttl));
        let middleware = MiddlewareChain::new();
        let patterns = Arc::new(RwLock::new(PatternDetector::new(pattern_window)));
        let stats = Arc::new(AtomicStats::new());

        Self { cache, middleware, patterns, stats }
    }

    /// Add middleware to the chain.
    pub fn with_middleware(mut self, mw: Arc<dyn crate::tools::tool_middleware::Middleware>) -> Self {
        self.middleware = self.middleware.push(mw);
        self
    }

    /// Execute a tool with full caching and observability.
    pub async fn execute(&self, tool_name: &str, args: Value) -> MiddlewareResult<Value> {
        // Delegate to the shared-version and convert to an owned Value
        let r = self.execute_shared_owned(tool_name, args).await?;
        Ok((*r).clone())
    }

    /// Execute a tool but return a shared (Arc) response to avoid clones.
    /// Accepts a shared `Arc<Value>` to avoid cloning arg contents when caller
    /// already holds a shared reference.
    pub async fn execute_shared(&self, tool_name: &str, args: Arc<Value>) -> MiddlewareResult<Arc<Value>> {
        let start = std::time::Instant::now();
        let cache_key = make_cache_key(tool_name, &args);

        // Atomic increment — no lock needed
        self.stats.total_calls.fetch_add(1, Ordering::Relaxed);

        // Create request — reuse the shared `args` (already an Arc)
        let owned_args = Arc::clone(&args);
        let req = ToolRequest {
            id: next_request_id(),
            tool_name: tool_name.into(),
            args: (*owned_args).clone(),
            metadata: Some(Default::default()),
        };

        // Before hooks
        if let Err(err) = self.middleware.before_execute(&req).await {
            self.record_error(tool_name, start.elapsed(), &req, &err).await;
            return Err(err);
        }

        // Check cache
        if let Some(result) = self.cache.get(&cache_key).await {
            let duration_ms = start.elapsed().as_millis() as u64;
            self.stats.record_success(duration_ms);
            self.stats.cache_hits.fetch_add(1, Ordering::Relaxed);

            let res = ToolResponse {
                id: req.id.clone(),
                success: true,
                result: Some((*result).clone()),
                error: None,
                duration_ms: Some(duration_ms),
                cache_hit: Some(true),
            };
            if let Err(err) = self.middleware.after_execute(&req, &res).await {
                self.record_error(tool_name, start.elapsed(), &req, &err).await;
                return Err(err);
            }

            // Record pattern
            self.record_pattern(tool_name, true, duration_ms).await;

            return Ok(Arc::clone(&result));
        }

        // Atomic increment — no lock needed
        self.stats.cache_misses.fetch_add(1, Ordering::Relaxed);

        // Execute tool (caller provides actual execution)
        let timeout_secs = execution::DEFAULT_TIMEOUT_SECS;
        let result = match timeout(
            Duration::from_secs(timeout_secs),
            self.execute_tool_internal(tool_name, &owned_args),
        )
        .await
        {
            Ok(result) => match result {
                Ok(result) => result,
                Err(err) => {
                    self.record_error(tool_name, start.elapsed(), &req, &err).await;
                    return Err(err);
                }
            },
            Err(_) => {
                let err = UnifiedToolError::new(
                    UnifiedErrorKind::Timeout,
                    format!("Tool '{tool_name}' timed out after {timeout_secs} seconds"),
                )
                .with_tool_name(tool_name);
                self.record_error(tool_name, start.elapsed(), &req, &err).await;
                return Err(err);
            }
        };

        let duration_ms = start.elapsed().as_millis() as u64;

        // Wrap result in Arc once, then clone Arc for cache and response
        let arc_res = Arc::new(result);

        // Cache result (Arc clone is cheap - pass Arc directly into cache)
        self.cache.insert_arc(cache_key, Arc::clone(&arc_res)).await;

        // Atomic stats update — no lock needed
        self.stats.record_success(duration_ms);

        let res = ToolResponse {
            id: req.id.clone(),
            success: true,
            result: Some((*arc_res).clone()),
            error: None,
            duration_ms: Some(duration_ms),
            cache_hit: Some(false),
        };

        // After hooks
        if let Err(err) = self.middleware.after_execute(&req, &res).await {
            self.record_error(tool_name, start.elapsed(), &req, &err).await;
            return Err(err);
        }

        // Record pattern
        self.record_pattern(tool_name, true, duration_ms).await;

        Ok(arc_res)
    }

    /// Backwards-compatible wrapper for callers that still pass an owned Value.
    pub async fn execute_shared_owned(&self, tool_name: &str, args: Value) -> MiddlewareResult<Arc<Value>> {
        let arg = Arc::new(args);
        self.execute_shared(tool_name, arg).await
    }

    /// Execute tool (override this for real tool execution)
    async fn execute_tool_internal(&self, _tool_name: &str, _args: &Value) -> MiddlewareResult<Value> {
        // Default: return placeholder result
        // In real usage, this would call ToolRegistry
        Ok(serde_json::json!({"status": "ok"}))
    }

    async fn record_error(&self, tool_name: &str, elapsed: Duration, req: &ToolRequest, err: &UnifiedToolError) {
        self.stats.record_failure(elapsed.as_millis() as u64);

        let _ = self.middleware.on_error(req, err).await;
        self.record_pattern(tool_name, false, elapsed.as_millis() as u64).await;
    }

    /// Record event in pattern detector
    async fn record_pattern(&self, tool_name: &str, success: bool, duration_ms: u64) {
        let mut patterns = self.patterns.write().unwrap_or_else(|poisoned| {
            warn!("pattern detector lock poisoned; recovering");
            poisoned.into_inner()
        });
        patterns.record_event(ToolEvent {
            tool_name: tool_name.to_string(),
            success,
            duration_ms,
            timestamp: std::time::Instant::now(),
        });
    }

    /// Get current executor statistics.
    pub async fn stats(&self) -> ExecutorStats {
        let mut stats = self.stats.snapshot();
        let patterns = self.patterns.read().unwrap_or_else(|poisoned| {
            warn!("pattern detector lock poisoned; recovering");
            poisoned.into_inner()
        });
        stats.patterns_detected = patterns.patterns().len();
        stats
    }

    /// Get cache statistics.
    pub async fn cache_stats(&self) -> crate::tools::lru_cache::CacheStats {
        self.cache.stats().await
    }

    /// Get detected workflow patterns.
    pub async fn patterns(&self) -> Vec<crate::tools::pattern_detection::DetectedPattern> {
        let patterns = self.patterns.read().unwrap_or_else(|poisoned| {
            warn!("pattern detector lock poisoned; recovering");
            poisoned.into_inner()
        });
        patterns.patterns().to_vec()
    }

    /// Get ML-ready feature vector from patterns.
    pub async fn feature_vector(&self) -> Vec<f64> {
        let patterns = self.patterns.read().unwrap_or_else(|poisoned| {
            warn!("pattern detector lock poisoned; recovering");
            poisoned.into_inner()
        });
        patterns.feature_vector()
    }

    /// Clear cache
    pub async fn clear_cache(&self) {
        self.cache.clear().await;
    }

    /// Clear patterns
    pub async fn clear_patterns(&self) {
        let mut patterns = self.patterns.write().unwrap_or_else(|poisoned| {
            warn!("pattern detector lock poisoned; recovering");
            poisoned.into_inner()
        });
        patterns.reset();
    }

    /// Print execution report
    pub async fn report(&self) {
        let stats = self.stats().await;
        let cache_stats = self.cache_stats().await;
        let patterns = self.patterns().await;

        println!("\n=== ToolExecutor Report ===\n");

        println!("Execution Statistics:");
        println!("  Total calls:      {}", stats.total_calls);
        println!("  Successful:       {}", stats.successful_calls);
        println!("  Failed:           {}", stats.failed_calls);
        println!("  Avg duration:     {}ms", stats.avg_duration_ms);

        println!("\nCache Performance:");
        println!("  Hits:             {}", cache_stats.hits);
        println!("  Misses:           {}", cache_stats.misses);
        println!("  Hit rate:         {:.1}%", cache_stats.hit_rate());
        println!("  Evictions:        {}", cache_stats.evictions);
        println!("  Expirations:      {}", cache_stats.expirations);

        println!("\nWorkflow Patterns ({} detected):", patterns.len());
        for (i, pattern) in patterns.iter().take(5).enumerate() {
            println!("  {}. {:?}", i + 1, pattern.sequence);
            println!("     Frequency: {}, Confidence: {:.1}%", pattern.frequency, pattern.confidence * 100.0);
        }

        println!("\n");
    }
}

// Helper for stable cache key generation — uses a small 64-bit hash of the
// serialized JSON arguments to avoid storing large argument strings as cache
// keys while still differentiating distinct argument payloads.
#[inline]
fn make_cache_key(tool_name: &str, args: &Value) -> String {
    use std::hash::Hash;
    let mut hasher = DefaultHasher::new();
    // Hash the tool name first for better distribution.
    tool_name.hash(&mut hasher);
    // Use compact JSON serialization for hashing.
    if let Ok(bytes) = serde_json::to_vec(args) {
        hasher.write(&bytes);
    }
    let h = hasher.finish();
    format!("{tool_name}:{h:x}")
}

// test above added to the test module further below

impl Default for CachedToolExecutor {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tools::tool_middleware::{LoggingMiddleware, MetricsMiddleware, MiddlewareResult};
    use crate::tools::{UnifiedErrorKind, UnifiedToolError};
    use async_trait::async_trait;
    use std::sync::Arc;

    struct FailingMiddleware;

    #[async_trait]
    impl crate::tools::tool_middleware::Middleware for FailingMiddleware {
        async fn before_execute(&self, _req: &ToolRequest) -> MiddlewareResult<()> {
            Err(UnifiedToolError::new(UnifiedErrorKind::ExecutionFailed, "middleware rejected request"))
        }
    }

    #[tokio::test]
    async fn test_executor_basic() -> anyhow::Result<()> {
        let executor = CachedToolExecutor::new();

        // First call - miss
        let result = executor.execute("test_tool", serde_json::json!({"arg": 1})).await?;
        assert_eq!(result, serde_json::json!({"status": "ok"}));

        let stats = executor.stats().await;
        assert_eq!(stats.total_calls, 1);
        assert_eq!(stats.cache_misses, 1);

        Ok(())
    }

    #[tokio::test]
    async fn test_executor_cache_hit() -> anyhow::Result<()> {
        let executor = CachedToolExecutor::new();

        // Two identical calls
        executor.execute("test_tool", serde_json::json!({"arg": 1})).await?;
        executor.execute("test_tool", serde_json::json!({"arg": 1})).await?;

        let stats = executor.stats().await;
        assert_eq!(stats.total_calls, 2);
        assert_eq!(stats.successful_calls, 2);
        assert_eq!(stats.cache_hits, 1);
        assert_eq!(stats.cache_misses, 1);

        Ok(())
    }

    #[tokio::test]
    async fn cache_hit_after_repeat_call() {
        let exec = CachedToolExecutor::with_config(10, Duration::from_secs(60), 3);
        let args = serde_json::json!({"x": 1});

        // First call -> cache miss
        let _first = exec.execute("test_tool", args.clone()).await.unwrap();

        // Second call with same args -> should use cache
        let _second = exec.execute("test_tool", args.clone()).await.unwrap();

        // Check stats for cache hit/miss
        let stats = exec.stats().await;
        assert!(stats.cache_hits >= 1);
        assert!(stats.cache_misses >= 1);
    }

    #[tokio::test]
    async fn test_executor_with_middleware() -> anyhow::Result<()> {
        let executor = CachedToolExecutor::new().with_middleware(LoggingMiddleware::new("test"));

        executor.execute("test_tool", serde_json::json!({})).await?;

        let stats = executor.stats().await;
        assert_eq!(stats.total_calls, 1);

        Ok(())
    }

    #[tokio::test]
    async fn test_executor_patterns() -> anyhow::Result<()> {
        let executor = CachedToolExecutor::new();

        // Record a repeating A -> B pattern with enough events
        // to trigger pattern analysis (ANALYZE_INTERVAL = 10).
        for _ in 0..6 {
            executor.execute("tool_a", serde_json::json!({})).await?;
            executor.execute("tool_b", serde_json::json!({})).await?;
        }

        let patterns = executor.patterns().await;
        assert!(!patterns.is_empty());

        Ok(())
    }

    #[tokio::test]
    async fn test_executor_clear() -> anyhow::Result<()> {
        let executor = CachedToolExecutor::new();

        executor.execute("test", serde_json::json!({})).await?;

        let stats_before = executor.stats().await;
        assert_eq!(stats_before.total_calls, 1);

        executor.clear_cache().await;
        executor.clear_patterns().await;

        let cache_stats = executor.cache_stats().await;
        assert_eq!(cache_stats.hits + cache_stats.misses, 0);

        Ok(())
    }

    #[tokio::test]
    async fn test_executor_reports_typed_error_to_middleware_metrics() {
        let metrics = MetricsMiddleware::new();
        let executor = CachedToolExecutor::new()
            .with_middleware(metrics.clone())
            .with_middleware(Arc::new(FailingMiddleware));

        let err = executor.execute("test", serde_json::json!({})).await;
        assert!(err.is_err());

        let stats = executor.stats().await;
        assert_eq!(stats.failed_calls, 1);

        let snapshot = metrics.snapshot().await;
        assert_eq!(snapshot.total_calls, 1);
        assert_eq!(snapshot.failed_calls, 1);
    }
}