koda_core/
sub_agent_cache.rs1use std::collections::HashMap;
20use std::hash::{Hash, Hasher};
21use std::sync::{Arc, Mutex};
22
23type CacheKey = (String, u64);
25
26#[derive(Clone, Debug)]
30pub struct SubAgentCache {
31 inner: Arc<Mutex<CacheInner>>,
32}
33
34#[derive(Debug)]
35struct CacheInner {
36 entries: HashMap<CacheKey, CachedResult>,
37 generation: u64,
40}
41
42#[derive(Debug, Clone)]
43struct CachedResult {
44 response: String,
45 generation: u64,
46}
47
48impl SubAgentCache {
49 pub fn new() -> Self {
51 Self {
52 inner: Arc::new(Mutex::new(CacheInner {
53 entries: HashMap::new(),
54 generation: 0,
55 })),
56 }
57 }
58
59 pub fn get(&self, agent_name: &str, prompt: &str) -> Option<String> {
64 let key = make_key(agent_name, prompt);
65 let inner = self.inner.lock().ok()?;
66 let entry = inner.entries.get(&key)?;
67 if entry.generation == inner.generation {
68 Some(entry.response.clone())
69 } else {
70 None
71 }
72 }
73
74 pub fn put(&self, agent_name: &str, prompt: &str, response: &str) {
76 let key = make_key(agent_name, prompt);
77 if let Ok(mut inner) = self.inner.lock() {
78 let current_gen = inner.generation;
79 inner.entries.insert(
80 key,
81 CachedResult {
82 response: response.to_string(),
83 generation: current_gen,
84 },
85 );
86 }
87 }
88
89 pub fn invalidate(&self) {
94 if let Ok(mut inner) = self.inner.lock() {
95 inner.generation += 1;
96 }
97 }
98
99 pub fn len(&self) -> usize {
101 self.inner
102 .lock()
103 .map(|inner| inner.entries.len())
104 .unwrap_or(0)
105 }
106
107 pub fn is_empty(&self) -> bool {
109 self.len() == 0
110 }
111}
112
113impl Default for SubAgentCache {
114 fn default() -> Self {
115 Self::new()
116 }
117}
118
119fn make_key(agent_name: &str, prompt: &str) -> CacheKey {
121 let mut hasher = std::collections::hash_map::DefaultHasher::new();
122 prompt.hash(&mut hasher);
123 (agent_name.to_string(), hasher.finish())
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn cache_hit_after_put() {
132 let cache = SubAgentCache::new();
133 cache.put("reviewer", "review this code", "looks good!");
134 assert_eq!(
135 cache.get("reviewer", "review this code"),
136 Some("looks good!".to_string())
137 );
138 }
139
140 #[test]
141 fn cache_miss_different_prompt() {
142 let cache = SubAgentCache::new();
143 cache.put("reviewer", "review this code", "looks good!");
144 assert_eq!(cache.get("reviewer", "review OTHER code"), None);
145 }
146
147 #[test]
148 fn cache_miss_different_agent() {
149 let cache = SubAgentCache::new();
150 cache.put("reviewer", "review this", "looks good!");
151 assert_eq!(cache.get("testgen", "review this"), None);
152 }
153
154 #[test]
155 fn invalidation_clears_stale_entries() {
156 let cache = SubAgentCache::new();
157 cache.put("reviewer", "prompt", "result");
158 assert!(cache.get("reviewer", "prompt").is_some());
159
160 cache.invalidate();
161 assert_eq!(cache.get("reviewer", "prompt"), None);
162 }
163
164 #[test]
165 fn entries_after_invalidation_are_fresh() {
166 let cache = SubAgentCache::new();
167 cache.put("reviewer", "old prompt", "old result");
168 cache.invalidate();
169 cache.put("reviewer", "new prompt", "new result");
170
171 assert_eq!(cache.get("reviewer", "old prompt"), None);
173 assert_eq!(
175 cache.get("reviewer", "new prompt"),
176 Some("new result".to_string())
177 );
178 }
179
180 #[test]
181 fn len_tracks_entries() {
182 let cache = SubAgentCache::new();
183 assert!(cache.is_empty());
184 cache.put("a", "p1", "r1");
185 cache.put("b", "p2", "r2");
186 assert_eq!(cache.len(), 2);
187 }
188
189 #[test]
190 fn shared_across_clones() {
191 let cache = SubAgentCache::new();
192 let clone = cache.clone();
193 cache.put("agent", "prompt", "result");
194 assert_eq!(clone.get("agent", "prompt"), Some("result".to_string()));
195 }
196}