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}