Skip to main content

lc_agents/
cache.rs

1// lc-agents/src/cache.rs
2//! LLM result cache (P2-1)
3//!
4//! `plan()`'s LLM calls in the agent loop are keyed by `(input +
5//! intermediate steps + executor namespace)`; under deterministic prompts the
6//! same input reuses the previous `AgentOutput`, skipping the LLM round trip.
7//! Tool execution results (observations) enter the key; tools themselves are
8//! not cached.
9
10use std::collections::{HashMap, VecDeque};
11use std::sync::Mutex;
12
13/// LLM result cache abstraction.
14///
15/// Values are carried as strings (internally a serialized `AgentOutput`); the
16/// implementation can be swapped for disk / Redis / cross-process sharing as
17/// long as `get`/`put` semantics stay consistent.
18pub trait ResponseCache: Send + Sync {
19    /// Returns the serialized result for `key`, if cached.
20    fn get(&self, key: &str) -> Option<String>;
21    /// Writes a cache entry.
22    fn put(&self, key: String, value: String);
23    /// Clears the cache.
24    fn clear(&self);
25}
26
27/// Bounded in-memory cache.
28///
29/// When entries exceed `max_entries`, the oldest are evicted FIFO to keep the
30/// deterministic cache from growing without bound.
31#[derive(Default)]
32pub struct MemoryCache {
33    inner: Mutex<CacheInner>,
34}
35
36struct CacheInner {
37    map: HashMap<String, String>,
38    order: VecDeque<String>,
39    max_entries: usize,
40}
41
42impl Default for CacheInner {
43    fn default() -> Self {
44        Self {
45            map: HashMap::new(),
46            order: VecDeque::new(),
47            max_entries: 256,
48        }
49    }
50}
51
52impl MemoryCache {
53    /// Creates an in-memory cache with default capacity (256 entries).
54    pub fn new() -> Self {
55        Self::default()
56    }
57
58    /// Sets the maximum entry count (at least 1).
59    pub fn with_capacity(max_entries: usize) -> Self {
60        Self {
61            inner: Mutex::new(CacheInner {
62                max_entries: max_entries.max(1),
63                ..Default::default()
64            }),
65        }
66    }
67}
68
69impl ResponseCache for MemoryCache {
70    fn get(&self, key: &str) -> Option<String> {
71        self.inner
72            .lock()
73            .ok()
74            .and_then(|inner| inner.map.get(key).cloned())
75    }
76
77    fn put(&self, key: String, value: String) {
78        if let Ok(mut inner) = self.inner.lock() {
79            if inner.map.insert(key.clone(), value).is_none() {
80                inner.order.push_back(key);
81            }
82            while inner.order.len() > inner.max_entries {
83                if let Some(oldest) = inner.order.pop_front() {
84                    inner.map.remove(&oldest);
85                }
86            }
87        }
88    }
89
90    fn clear(&self) {
91        if let Ok(mut inner) = self.inner.lock() {
92            inner.map.clear();
93            inner.order.clear();
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_cache_put_get() {
104        let cache = MemoryCache::new();
105        assert!(cache.get("k").is_none());
106        cache.put("k".to_string(), "v".to_string());
107        assert_eq!(cache.get("k").as_deref(), Some("v"));
108    }
109
110    #[test]
111    fn test_cache_evicts_oldest() {
112        let cache = MemoryCache::with_capacity(2);
113        cache.put("a".to_string(), "1".to_string());
114        cache.put("b".to_string(), "2".to_string());
115        cache.put("c".to_string(), "3".to_string());
116        assert!(cache.get("a").is_none(), "最旧条目应被淘汰");
117        assert_eq!(cache.get("b").as_deref(), Some("2"));
118        assert_eq!(cache.get("c").as_deref(), Some("3"));
119    }
120
121    #[test]
122    fn test_cache_clear() {
123        let cache = MemoryCache::new();
124        cache.put("a".to_string(), "1".to_string());
125        cache.clear();
126        assert!(cache.get("a").is_none());
127    }
128}