Skip to main content

simple_agent_type/
cache.rs

1//! Cache trait for response caching.
2//!
3//! Provides an abstract interface for caching LLM responses.
4
5use crate::error::Result;
6use async_trait::async_trait;
7use std::time::Duration;
8
9/// Trait for caching LLM responses.
10///
11/// Implementations can use various backends:
12/// - In-memory (HashMap, LRU)
13/// - Redis
14/// - Disk-based
15/// - Distributed caches
16///
17/// # Example Implementation
18///
19/// ```rust
20/// use simple_agent_type::cache::Cache;
21/// use simple_agent_type::error::Result;
22/// use async_trait::async_trait;
23/// use std::collections::HashMap;
24/// use std::sync::{Arc, Mutex};
25/// use std::time::Duration;
26///
27/// struct InMemoryCache {
28///     store: Arc<Mutex<HashMap<String, Vec<u8>>>>,
29/// }
30///
31/// #[async_trait]
32/// impl Cache for InMemoryCache {
33///     async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
34///         let store = self.store.lock().unwrap();
35///         Ok(store.get(key).cloned())
36///     }
37///
38///     async fn set(&self, key: &str, value: Vec<u8>, _ttl: Duration) -> Result<()> {
39///         let mut store = self.store.lock().unwrap();
40///         store.insert(key.to_string(), value);
41///         Ok(())
42///     }
43///
44///     async fn delete(&self, key: &str) -> Result<()> {
45///         let mut store = self.store.lock().unwrap();
46///         store.remove(key);
47///         Ok(())
48///     }
49///
50///     async fn clear(&self) -> Result<()> {
51///         let mut store = self.store.lock().unwrap();
52///         store.clear();
53///         Ok(())
54///     }
55/// }
56///
57/// let cache = InMemoryCache {
58///     store: Arc::new(Mutex::new(HashMap::new())),
59/// };
60///
61/// let rt = tokio::runtime::Runtime::new().unwrap();
62/// rt.block_on(async {
63///     cache
64///         .set("request:abc123", b"ok".to_vec(), Duration::from_secs(60))
65///         .await
66///         .unwrap();
67///     let value = cache.get("request:abc123").await.unwrap();
68///     assert_eq!(value, Some(b"ok".to_vec()));
69/// });
70/// ```
71#[async_trait]
72pub trait Cache: Send + Sync {
73    /// Get a value from the cache.
74    ///
75    /// Returns `Ok(None)` if the key doesn't exist or has expired.
76    ///
77    /// # Arguments
78    /// - `key`: Cache key
79    ///
80    /// # Example
81    /// ```rust
82    /// use simple_agent_type::cache::Cache;
83    /// use async_trait::async_trait;
84    /// use std::collections::HashMap;
85    /// use std::sync::{Arc, Mutex};
86    /// use std::time::Duration;
87    ///
88    /// # struct InMemoryCache {
89    /// #     store: Arc<Mutex<HashMap<String, Vec<u8>>>>,
90    /// # }
91    /// # #[async_trait]
92    /// # impl Cache for InMemoryCache {
93    /// #     async fn get(&self, key: &str) -> simple_agent_type::error::Result<Option<Vec<u8>>> {
94    /// #         let store = self.store.lock().unwrap();
95    /// #         Ok(store.get(key).cloned())
96    /// #     }
97    /// #     async fn set(&self, key: &str, value: Vec<u8>, _ttl: Duration) -> simple_agent_type::error::Result<()> {
98    /// #         let mut store = self.store.lock().unwrap();
99    /// #         store.insert(key.to_string(), value);
100    /// #         Ok(())
101    /// #     }
102    /// #     async fn delete(&self, key: &str) -> simple_agent_type::error::Result<()> {
103    /// #         let mut store = self.store.lock().unwrap();
104    /// #         store.remove(key);
105    /// #         Ok(())
106    /// #     }
107    /// #     async fn clear(&self) -> simple_agent_type::error::Result<()> {
108    /// #         let mut store = self.store.lock().unwrap();
109    /// #         store.clear();
110    /// #         Ok(())
111    /// #     }
112    /// # }
113    /// # let cache = InMemoryCache {
114    /// #     store: Arc::new(Mutex::new(HashMap::new())),
115    /// # };
116    /// # let rt = tokio::runtime::Runtime::new().unwrap();
117    /// # rt.block_on(async {
118    /// cache
119    ///     .set("request:abc123", b"ok".to_vec(), Duration::from_secs(60))
120    ///     .await
121    ///     .unwrap();
122    /// let value = cache.get("request:abc123").await.unwrap();
123    /// assert_eq!(value, Some(b"ok".to_vec()));
124    /// # });
125    /// ```
126    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
127
128    /// Set a value in the cache with TTL.
129    ///
130    /// # Arguments
131    /// - `key`: Cache key
132    /// - `value`: Serialized value (typically JSON bytes)
133    /// - `ttl`: Time-to-live (expiration duration)
134    ///
135    /// # Example
136    /// ```rust
137    /// use simple_agent_type::cache::Cache;
138    /// use async_trait::async_trait;
139    /// use std::collections::HashMap;
140    /// use std::sync::{Arc, Mutex};
141    /// use std::time::Duration;
142    ///
143    /// # struct InMemoryCache {
144    /// #     store: Arc<Mutex<HashMap<String, Vec<u8>>>>,
145    /// # }
146    /// # #[async_trait]
147    /// # impl Cache for InMemoryCache {
148    /// #     async fn get(&self, key: &str) -> simple_agent_type::error::Result<Option<Vec<u8>>> {
149    /// #         let store = self.store.lock().unwrap();
150    /// #         Ok(store.get(key).cloned())
151    /// #     }
152    /// #     async fn set(&self, key: &str, value: Vec<u8>, _ttl: Duration) -> simple_agent_type::error::Result<()> {
153    /// #         let mut store = self.store.lock().unwrap();
154    /// #         store.insert(key.to_string(), value);
155    /// #         Ok(())
156    /// #     }
157    /// #     async fn delete(&self, key: &str) -> simple_agent_type::error::Result<()> {
158    /// #         let mut store = self.store.lock().unwrap();
159    /// #         store.remove(key);
160    /// #         Ok(())
161    /// #     }
162    /// #     async fn clear(&self) -> simple_agent_type::error::Result<()> {
163    /// #         let mut store = self.store.lock().unwrap();
164    /// #         store.clear();
165    /// #         Ok(())
166    /// #     }
167    /// # }
168    /// # let cache = InMemoryCache {
169    /// #     store: Arc::new(Mutex::new(HashMap::new())),
170    /// # };
171    /// # let rt = tokio::runtime::Runtime::new().unwrap();
172    /// # rt.block_on(async {
173    /// cache
174    ///     .set("request:abc123", b"payload".to_vec(), Duration::from_secs(3600))
175    ///     .await
176    ///     .unwrap();
177    /// # });
178    /// ```
179    async fn set(&self, key: &str, value: Vec<u8>, ttl: Duration) -> Result<()>;
180
181    /// Delete a value from the cache.
182    ///
183    /// # Arguments
184    /// - `key`: Cache key
185    ///
186    /// # Example
187    /// ```rust
188    /// use simple_agent_type::cache::Cache;
189    /// use async_trait::async_trait;
190    /// use std::collections::HashMap;
191    /// use std::sync::{Arc, Mutex};
192    /// use std::time::Duration;
193    ///
194    /// # struct InMemoryCache {
195    /// #     store: Arc<Mutex<HashMap<String, Vec<u8>>>>,
196    /// # }
197    /// # #[async_trait]
198    /// # impl Cache for InMemoryCache {
199    /// #     async fn get(&self, key: &str) -> simple_agent_type::error::Result<Option<Vec<u8>>> {
200    /// #         let store = self.store.lock().unwrap();
201    /// #         Ok(store.get(key).cloned())
202    /// #     }
203    /// #     async fn set(&self, key: &str, value: Vec<u8>, _ttl: Duration) -> simple_agent_type::error::Result<()> {
204    /// #         let mut store = self.store.lock().unwrap();
205    /// #         store.insert(key.to_string(), value);
206    /// #         Ok(())
207    /// #     }
208    /// #     async fn delete(&self, key: &str) -> simple_agent_type::error::Result<()> {
209    /// #         let mut store = self.store.lock().unwrap();
210    /// #         store.remove(key);
211    /// #         Ok(())
212    /// #     }
213    /// #     async fn clear(&self) -> simple_agent_type::error::Result<()> {
214    /// #         let mut store = self.store.lock().unwrap();
215    /// #         store.clear();
216    /// #         Ok(())
217    /// #     }
218    /// # }
219    /// # let cache = InMemoryCache {
220    /// #     store: Arc::new(Mutex::new(HashMap::new())),
221    /// # };
222    /// # let rt = tokio::runtime::Runtime::new().unwrap();
223    /// # rt.block_on(async {
224    /// cache
225    ///     .set("request:abc123", b"payload".to_vec(), Duration::from_secs(60))
226    ///     .await
227    ///     .unwrap();
228    /// cache.delete("request:abc123").await.unwrap();
229    /// # });
230    /// ```
231    async fn delete(&self, key: &str) -> Result<()>;
232
233    /// Clear all values from the cache.
234    ///
235    /// # Warning
236    /// This is a destructive operation. Use with caution.
237    ///
238    /// # Example
239    /// ```rust
240    /// use simple_agent_type::cache::Cache;
241    /// use async_trait::async_trait;
242    /// use std::collections::HashMap;
243    /// use std::sync::{Arc, Mutex};
244    /// use std::time::Duration;
245    ///
246    /// # struct InMemoryCache {
247    /// #     store: Arc<Mutex<HashMap<String, Vec<u8>>>>,
248    /// # }
249    /// # #[async_trait]
250    /// # impl Cache for InMemoryCache {
251    /// #     async fn get(&self, key: &str) -> simple_agent_type::error::Result<Option<Vec<u8>>> {
252    /// #         let store = self.store.lock().unwrap();
253    /// #         Ok(store.get(key).cloned())
254    /// #     }
255    /// #     async fn set(&self, key: &str, value: Vec<u8>, _ttl: Duration) -> simple_agent_type::error::Result<()> {
256    /// #         let mut store = self.store.lock().unwrap();
257    /// #         store.insert(key.to_string(), value);
258    /// #         Ok(())
259    /// #     }
260    /// #     async fn delete(&self, key: &str) -> simple_agent_type::error::Result<()> {
261    /// #         let mut store = self.store.lock().unwrap();
262    /// #         store.remove(key);
263    /// #         Ok(())
264    /// #     }
265    /// #     async fn clear(&self) -> simple_agent_type::error::Result<()> {
266    /// #         let mut store = self.store.lock().unwrap();
267    /// #         store.clear();
268    /// #         Ok(())
269    /// #     }
270    /// # }
271    /// # let cache = InMemoryCache {
272    /// #     store: Arc::new(Mutex::new(HashMap::new())),
273    /// # };
274    /// # let rt = tokio::runtime::Runtime::new().unwrap();
275    /// # rt.block_on(async {
276    /// cache
277    ///     .set("request:abc123", b"payload".to_vec(), Duration::from_secs(60))
278    ///     .await
279    ///     .unwrap();
280    /// cache.clear().await.unwrap();
281    /// # });
282    /// ```
283    async fn clear(&self) -> Result<()>;
284
285    /// Check if caching is enabled.
286    ///
287    /// This allows for a "no-op" cache implementation that always
288    /// returns false, disabling caching without changing call sites.
289    fn is_enabled(&self) -> bool {
290        true
291    }
292
293    /// Get the cache name/type.
294    ///
295    /// Used for logging and debugging.
296    fn name(&self) -> &str {
297        "cache"
298    }
299}
300
301/// Cache key builder for standardized key generation.
302///
303/// Generates deterministic cache keys from requests.
304pub struct CacheKey;
305
306impl CacheKey {
307    /// Generate a cache key from a request.
308    ///
309    /// Uses blake3 for cryptographically secure and deterministic hashing.
310    ///
311    /// # Example
312    /// ```
313    /// use simple_agent_type::cache::CacheKey;
314    ///
315    /// let key = CacheKey::from_parts("openai", "gpt-4", "user:Hello");
316    /// assert!(key.starts_with("openai:"));
317    /// ```
318    pub fn from_parts(provider: &str, model: &str, content: &str) -> String {
319        let mut hasher = blake3::Hasher::new();
320        hasher.update(provider.as_bytes());
321        hasher.update(model.as_bytes());
322        hasher.update(content.as_bytes());
323        let hash = hasher.finalize();
324        format!("{}:{}:{}", provider, model, hash.to_hex())
325    }
326
327    /// Generate a cache key with custom namespace.
328    pub fn with_namespace(namespace: &str, key: &str) -> String {
329        format!("{}:{}", namespace, key)
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn test_cache_key_from_parts() {
339        let key1 = CacheKey::from_parts("openai", "gpt-4", "Hello");
340        let key2 = CacheKey::from_parts("openai", "gpt-4", "Hello");
341        let key3 = CacheKey::from_parts("openai", "gpt-4", "Goodbye");
342
343        // Same inputs produce same key
344        assert_eq!(key1, key2);
345
346        // Different inputs produce different keys
347        assert_ne!(key1, key3);
348
349        // Keys contain provider and model
350        assert!(key1.starts_with("openai:"));
351        assert!(key1.contains("gpt-4"));
352    }
353
354    #[test]
355    fn test_cache_key_with_namespace() {
356        let key = CacheKey::with_namespace("responses", "abc123");
357        assert_eq!(key, "responses:abc123");
358    }
359
360    #[test]
361    fn test_cache_key_deterministic() {
362        // Keys should be deterministic across runs
363        let key1 = CacheKey::from_parts("test", "model", "content");
364        let key2 = CacheKey::from_parts("test", "model", "content");
365        assert_eq!(key1, key2);
366    }
367
368    // Test that Cache trait is object-safe
369    #[test]
370    fn test_cache_object_safety() {
371        fn _assert_object_safe(_: &dyn Cache) {}
372    }
373
374    #[test]
375    fn test_cache_key_blake3_deterministic() {
376        // Verify blake3 produces deterministic hashes
377        let key1 = CacheKey::from_parts("openai", "gpt-4", "Hello, world!");
378        let key2 = CacheKey::from_parts("openai", "gpt-4", "Hello, world!");
379        assert_eq!(key1, key2, "Blake3 hashing should be deterministic");
380    }
381
382    #[test]
383    fn test_cache_key_blake3_collision_resistance() {
384        // Verify different inputs produce different hashes
385        let key1 = CacheKey::from_parts("openai", "gpt-4", "Hello");
386        let key2 = CacheKey::from_parts("openai", "gpt-4", "Hello!");
387        let key3 = CacheKey::from_parts("openai", "gpt-3.5", "Hello");
388        let key4 = CacheKey::from_parts("anthropic", "gpt-4", "Hello");
389
390        assert_ne!(
391            key1, key2,
392            "Different content should produce different hashes"
393        );
394        assert_ne!(
395            key1, key3,
396            "Different models should produce different hashes"
397        );
398        assert_ne!(
399            key1, key4,
400            "Different providers should produce different hashes"
401        );
402    }
403
404    #[test]
405    fn test_cache_key_blake3_format() {
406        // Verify the hash format is correct (provider:model:hex_hash)
407        let key = CacheKey::from_parts("openai", "gpt-4", "test");
408        let parts: Vec<&str> = key.split(':').collect();
409
410        assert_eq!(parts.len(), 3, "Key should have 3 parts");
411        assert_eq!(parts[0], "openai", "First part should be provider");
412        assert_eq!(parts[1], "gpt-4", "Second part should be model");
413        assert_eq!(
414            parts[2].len(),
415            64,
416            "Blake3 hash should be 64 hex characters"
417        );
418        assert!(
419            parts[2].chars().all(|c| c.is_ascii_hexdigit()),
420            "Hash should be valid hex"
421        );
422    }
423}