1use crate::cache::{CacheKey as UnifiedCacheKey, DEFAULT_CACHE_TTL, EvictionPolicy, UnifiedCache};
9use serde_json::Value;
10use std::collections::hash_map::DefaultHasher;
11use std::hash::{Hash, Hasher};
12use std::sync::Arc;
13
14#[derive(Debug, Clone, Eq, PartialEq, Hash)]
16pub struct ToolCacheKey {
17 pub tool: String,
19 pub params_hash: u64,
21 pub target_path: String,
23}
24
25impl UnifiedCacheKey for ToolCacheKey {
26 fn to_cache_key(&self) -> String {
27 format!("{}:{}:{}", self.tool, self.params_hash, self.target_path)
28 }
29}
30
31impl ToolCacheKey {
32 #[inline]
34 pub fn new(tool: &str, params: &str, target_path: &str) -> Self {
35 let mut hasher = DefaultHasher::new();
36 params.hash(&mut hasher);
37 let params_hash = hasher.finish();
38
39 ToolCacheKey {
40 tool: tool.to_string(),
41 params_hash,
42 target_path: target_path.to_string(),
43 }
44 }
45
46 #[inline]
49 pub fn from_json(tool: &str, params: &Value, target_path: &str) -> Self {
50 let mut hasher = DefaultHasher::new();
51 if let Ok(bytes) = serde_json::to_vec(params) {
54 bytes.hash(&mut hasher);
55 } else {
56 params.to_string().hash(&mut hasher);
57 }
58 let params_hash = hasher.finish();
59 ToolCacheKey {
60 tool: tool.to_string(),
61 params_hash,
62 target_path: target_path.to_string(),
63 }
64 }
65}
66
67pub struct ToolResultCache {
69 inner: UnifiedCache<ToolCacheKey, String>,
70}
71
72impl ToolResultCache {
73 pub fn new(capacity: usize) -> Self {
75 Self {
76 inner: UnifiedCache::new(capacity, DEFAULT_CACHE_TTL, EvictionPolicy::Lru),
77 }
78 }
79
80 fn insert_owned(&mut self, key: ToolCacheKey, output: String) {
81 let size_bytes = size_of_val(&output) as u64;
82 self.inner.insert(key, output, size_bytes);
83 }
84
85 pub fn insert(&mut self, key: ToolCacheKey, output: String) {
87 self.insert_owned(key, output);
88 }
89
90 pub fn insert_arc(&mut self, key: ToolCacheKey, output: Arc<String>) {
93 self.insert_owned(key, (*output).clone());
94 }
95
96 pub fn get(&self, key: &ToolCacheKey) -> Option<Arc<String>> {
98 self.inner.get(key)
99 }
100
101 pub fn get_owned(&self, key: &ToolCacheKey) -> Option<String> {
103 self.inner.get_owned(key)
104 }
105
106 pub fn invalidate_for_path(&mut self, path: &str) {
115 self.inner
116 .remove_where(|key| key.target_path.starts_with(path));
117 }
118
119 pub fn invalidate_key(&mut self, key: &ToolCacheKey) {
121 self.inner.remove(key);
122 }
123
124 pub fn invalidate_for_paths<I, S>(&mut self, paths: I)
129 where
130 I: IntoIterator<Item = S>,
131 S: AsRef<str>,
132 {
133 let path_prefixes: Vec<String> = paths
134 .into_iter()
135 .map(|path| path.as_ref().trim().to_string())
136 .filter(|path| !path.is_empty())
137 .collect();
138 if path_prefixes.is_empty() {
139 return;
140 }
141
142 self.inner.remove_where(|key| {
143 path_prefixes
144 .iter()
145 .any(|prefix| key.target_path.starts_with(prefix))
146 });
147 }
148
149 pub fn clear(&mut self) {
151 self.inner.clear();
152 }
153
154 pub fn check_pressure_and_evict(&mut self) {
159 if self.inner.total_memory_bytes() > 50 * 1024 * 1024 {
160 self.inner.evict_under_pressure(30); }
162 }
163
164 pub fn stats(&self) -> crate::cache::CacheStats {
166 self.inner.stats()
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use crate::config::constants::tools;
174
175 #[test]
176 fn creates_cache_key() {
177 let key = ToolCacheKey::new(tools::UNIFIED_SEARCH, "pattern=test", "/workspace");
178 assert_eq!(key.tool, tools::UNIFIED_SEARCH);
179 assert_eq!(key.target_path, "/workspace");
180 }
181
182 #[test]
183 fn from_json_and_new_equivalence() {
184 let params = serde_json::json!({"a": 1, "b": [1,2,3]});
185 let params_str = serde_json::to_string(¶ms).unwrap();
186 let k1 = ToolCacheKey::new("tool", ¶ms_str, "/workspace");
187 let k2 = ToolCacheKey::from_json("tool", ¶ms, "/workspace");
188 assert_eq!(k1.tool, k2.tool);
189 assert_eq!(k1.target_path, k2.target_path);
190 assert_ne!(k1.params_hash, 0);
191 assert_ne!(k2.params_hash, 0);
192 }
193
194 #[test]
195 fn caches_and_retrieves_result() {
196 let mut cache = ToolResultCache::new(10);
197 let key = ToolCacheKey::new(tools::UNIFIED_SEARCH, "pattern=test", "/workspace");
198 let output = "line 1\nline 2".to_string();
199
200 cache.insert_arc(key.clone(), Arc::new(output.clone()));
201 assert_eq!(cache.get(&key).as_ref(), Some(&Arc::new(output)));
202 }
203
204 #[test]
205 fn returns_none_for_missing_key() {
206 let cache = ToolResultCache::new(10);
207 let key = ToolCacheKey::new(tools::UNIFIED_SEARCH, "pattern=test", "/workspace");
208 assert!(cache.get(&key).is_none());
209 }
210
211 #[test]
212 fn evicts_least_recently_used() {
213 let mut cache = ToolResultCache::new(3);
214
215 let key1 = ToolCacheKey::new("tool", "p1", "/a");
216 let key2 = ToolCacheKey::new("tool", "p2", "/b");
217 let key3 = ToolCacheKey::new("tool", "p3", "/c");
218 let key4 = ToolCacheKey::new("tool", "p4", "/d");
219
220 cache.insert(key1.clone(), "out1".to_string());
221 cache.insert(key2.clone(), "out2".to_string());
222 cache.insert(key3.clone(), "out3".to_string());
223
224 cache.insert(key4.clone(), "out4".to_string());
226
227 assert!(cache.get(&key1).is_none());
228 assert_eq!(cache.get(&key2).unwrap().as_ref(), "out2");
229 }
230
231 #[test]
232 fn invalidates_by_path() {
233 let mut cache = ToolResultCache::new(10);
234
235 let key1 = ToolCacheKey::new("tool", "p1", "/workspace/file1.rs");
236 let key2 = ToolCacheKey::new("tool", "p2", "/workspace/file2.rs");
237 let key3 = ToolCacheKey::new("tool", "p3", "/other/file3.rs");
238
239 cache.insert(key1.clone(), "out1".to_string());
240 cache.insert(key2.clone(), "out2".to_string());
241 cache.insert(key3.clone(), "out3".to_string());
242
243 cache.invalidate_for_path("/workspace/file1.rs");
244
245 assert!(cache.get(&key1).is_none());
246 assert_eq!(cache.get(&key2).unwrap().as_ref(), "out2");
247 assert_eq!(cache.get(&key3).unwrap().as_ref(), "out3");
248 }
249
250 #[test]
251 fn invalidates_exact_key_only() {
252 let mut cache = ToolResultCache::new(10);
253
254 let key1 = ToolCacheKey::new("tool", "p1", "/workspace/file.rs");
255 let key2 = ToolCacheKey::new("tool", "p2", "/workspace/file.rs");
256
257 cache.insert(key1.clone(), "out1".to_string());
258 cache.insert(key2.clone(), "out2".to_string());
259
260 cache.invalidate_key(&key1);
261
262 assert!(cache.get(&key1).is_none());
263 assert_eq!(cache.get(&key2).unwrap().as_ref(), "out2");
264 }
265
266 #[test]
267 fn invalidates_multiple_paths() {
268 let mut cache = ToolResultCache::new(10);
269
270 let key1 = ToolCacheKey::new("tool", "p1", "/workspace/file1.rs");
271 let key2 = ToolCacheKey::new("tool", "p2", "/workspace/file2.rs");
272 let key3 = ToolCacheKey::new("tool", "p3", "/workspace/file3.rs");
273
274 cache.insert(key1.clone(), "out1".to_string());
275 cache.insert(key2.clone(), "out2".to_string());
276 cache.insert(key3.clone(), "out3".to_string());
277
278 cache.invalidate_for_paths(["/workspace/file1.rs", "/workspace/file3.rs"]);
279
280 assert!(cache.get(&key1).is_none());
281 assert!(cache.get(&key3).is_none());
282 assert_eq!(cache.get(&key2).unwrap().as_ref(), "out2");
283 }
284
285 #[test]
286 fn tracks_access_count() {
287 let mut cache = ToolResultCache::new(10);
288 let key = ToolCacheKey::new("tool", "p1", "/a");
289
290 cache.insert(key.clone(), "output".to_string());
291 let initial_stats = cache.stats();
292
293 cache.get(&key);
294 cache.get(&key);
295
296 let final_stats = cache.stats();
297 assert!(final_stats.hits > initial_stats.hits);
298 }
299
300 #[test]
301 fn clears_cache() {
302 let mut cache = ToolResultCache::new(10);
303 let key = ToolCacheKey::new("tool", "p1", "/a");
304
305 cache.insert(key.clone(), "output".to_string());
306 assert_eq!(cache.stats().current_size, 1);
307
308 cache.clear();
309 assert_eq!(cache.stats().current_size, 0);
310 assert!(cache.get(&key).is_none());
311 }
312
313 #[test]
314 fn computes_stats() {
315 let mut cache = ToolResultCache::new(10);
316
317 let key1 = ToolCacheKey::new("tool", "p1", "/a");
318 let key2 = ToolCacheKey::new("tool", "p2", "/b");
319
320 cache.insert(key1.clone(), "out1".to_string());
321 cache.insert(key2.clone(), "out2".to_string());
322 cache.get(&key1);
323 cache.get(&key2);
324 cache.get(&key1);
325
326 let stats = cache.stats();
327 assert_eq!(stats.current_size, 2);
328 assert_eq!(stats.max_size, 10);
329 assert_eq!(stats.hits, 3);
330 assert_eq!(stats.misses, 0); }
332
333 #[test]
334 fn insert_arc_and_get_arc() {
335 let mut cache = ToolResultCache::new(10);
336 let key = ToolCacheKey::new("tool", "p1", "/a");
337 let arc = Arc::new("output".to_string());
338 cache.insert_arc(key.clone(), Arc::clone(&arc));
339 assert_eq!(cache.get(&key).unwrap(), arc);
340 }
341
342 #[test]
343 fn test_granular_cache_invalidation() {
344 let mut cache = ToolResultCache::new(100);
346
347 let key1 = ToolCacheKey::new("grep", "pattern=test", "/workspace/src/main.rs");
348 let key2 = ToolCacheKey::new("grep", "pattern=test", "/workspace/src/lib.rs");
349 let key3 = ToolCacheKey::new("list", "recursive=true", "/workspace/src/");
350
351 cache.insert(key1.clone(), "result1".to_string());
352 cache.insert(key2.clone(), "result2".to_string());
353 cache.insert(key3.clone(), "result3".to_string());
354
355 assert_eq!(cache.stats().current_size, 3);
356
357 cache.invalidate_for_path("/workspace/src/main.rs");
359
360 assert!(cache.get(&key1).is_none(), "Key1 should be removed");
361 assert!(
362 cache.get(&key2).is_some(),
363 "Key2 should still exist (different file)"
364 );
365 assert!(
366 cache.get(&key3).is_some(),
367 "Key3 should still exist (different tool)"
368 );
369 assert_eq!(cache.stats().current_size, 2);
370 }
371
372 #[test]
373 fn test_invalidate_prefix_removes_only_matched() {
374 let mut cache = ToolResultCache::new(100);
376
377 let key1 = ToolCacheKey::new("grep", "p1", "/workspace/a");
378 let key2 = ToolCacheKey::new("grep", "p2", "/workspace/b");
379 let key3 = ToolCacheKey::new("grep", "p3", "/other/c");
380
381 cache.insert(key1.clone(), "1".to_string());
382 cache.insert(key2.clone(), "2".to_string());
383 cache.insert(key3.clone(), "3".to_string());
384
385 cache.invalidate_for_path("/workspace");
387
388 assert!(cache.get(&key1).is_none());
390 assert!(cache.get(&key2).is_none());
391 assert!(cache.get(&key3).is_some());
393 }
394
395 #[test]
396 fn test_cache_hit_ratio_preserved_after_selective_invalidation() {
397 let mut cache = ToolResultCache::new(100);
399
400 for i in 0..10 {
402 let key = ToolCacheKey::new("tool", "params", &format!("/file_{}", i));
403 cache.insert(key, format!("result_{}", i));
404 }
405
406 let stats_before = cache.stats();
407 assert_eq!(stats_before.current_size, 10);
408
409 for i in 0..5 {
411 let key = ToolCacheKey::new("tool", "params", &format!("/file_{}", i));
412 let _ = cache.get(&key);
413 }
414
415 let stats_mid = cache.stats();
416 let hits_before_invalidation = stats_mid.hits;
417
418 cache.invalidate_for_path("/file_0");
420
421 for i in 1..5 {
423 let key = ToolCacheKey::new("tool", "params", &format!("/file_{}", i));
424 assert!(
425 cache.get(&key).is_some(),
426 "Cache for /file_{} should still be valid",
427 i
428 );
429 }
430
431 let stats_after = cache.stats();
432 assert_eq!(stats_after.current_size, 9);
434 assert!(stats_after.hits > hits_before_invalidation);
436 }
437}