1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum MemoryRoute {
7 Keyword,
9 Semantic,
11 Hybrid,
13 Graph,
16}
17
18pub trait MemoryRouter: Send + Sync {
20 fn route(&self, query: &str) -> MemoryRoute;
22}
23
24pub struct HeuristicRouter;
32
33const QUESTION_WORDS: &[&str] = &[
34 "what", "how", "why", "when", "where", "who", "which", "explain", "describe",
35];
36
37const RELATIONSHIP_PATTERNS: &[&str] = &[
40 "related to",
41 "relates to",
42 "connection between",
43 "relationship",
44 "opinion on",
45 "thinks about",
46 "preference for",
47 "history of",
48 "know about",
49];
50
51fn starts_with_question(words: &[&str]) -> bool {
52 words
53 .first()
54 .is_some_and(|w| QUESTION_WORDS.iter().any(|qw| w.eq_ignore_ascii_case(qw)))
55}
56
57fn is_pure_snake_case(word: &str) -> bool {
60 if word.is_empty() {
61 return false;
62 }
63 let has_underscore = word.contains('_');
64 if !has_underscore {
65 return false;
66 }
67 word.chars()
68 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
69 && !word.chars().all(|c| c.is_ascii_digit() || c == '_')
70}
71
72impl MemoryRouter for HeuristicRouter {
73 fn route(&self, query: &str) -> MemoryRoute {
74 let words: Vec<&str> = query.split_whitespace().collect();
75 let word_count = words.len();
76
77 {
79 let lower = query.to_ascii_lowercase();
80 let has_relationship = RELATIONSHIP_PATTERNS.iter().any(|p| lower.contains(p));
81 if has_relationship {
82 return MemoryRoute::Graph;
83 }
84 }
85
86 let has_structural_code_pattern = query.contains('/') || query.contains("::");
89
90 let has_snake_case = words.iter().any(|w| is_pure_snake_case(w));
93 let question = starts_with_question(&words);
94
95 if has_structural_code_pattern && !question {
96 return MemoryRoute::Keyword;
97 }
98
99 if question || word_count >= 6 {
101 return MemoryRoute::Semantic;
102 }
103
104 if word_count <= 3 && !question {
106 return MemoryRoute::Keyword;
107 }
108
109 if has_snake_case {
111 return MemoryRoute::Keyword;
112 }
113
114 MemoryRoute::Hybrid
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 fn route(q: &str) -> MemoryRoute {
124 HeuristicRouter.route(q)
125 }
126
127 #[test]
128 fn rust_path_routes_keyword() {
129 assert_eq!(route("zeph_memory::recall"), MemoryRoute::Keyword);
130 }
131
132 #[test]
133 fn file_path_routes_keyword() {
134 assert_eq!(
135 route("crates/zeph-core/src/agent/mod.rs"),
136 MemoryRoute::Keyword
137 );
138 }
139
140 #[test]
141 fn pure_snake_case_routes_keyword() {
142 assert_eq!(route("memory_limit"), MemoryRoute::Keyword);
143 assert_eq!(route("error_handling"), MemoryRoute::Keyword);
144 }
145
146 #[test]
147 fn question_with_snake_case_routes_semantic() {
148 assert_eq!(
150 route("what is the memory_limit setting"),
151 MemoryRoute::Semantic
152 );
153 assert_eq!(route("how does error_handling work"), MemoryRoute::Semantic);
154 }
155
156 #[test]
157 fn short_query_routes_keyword() {
158 assert_eq!(route("context compaction"), MemoryRoute::Keyword);
159 assert_eq!(route("qdrant"), MemoryRoute::Keyword);
160 }
161
162 #[test]
163 fn question_routes_semantic() {
164 assert_eq!(
165 route("what is the purpose of semantic memory"),
166 MemoryRoute::Semantic
167 );
168 assert_eq!(route("how does the agent loop work"), MemoryRoute::Semantic);
169 assert_eq!(route("why does compaction fail"), MemoryRoute::Semantic);
170 assert_eq!(route("explain context compression"), MemoryRoute::Semantic);
171 }
172
173 #[test]
174 fn long_natural_query_routes_semantic() {
175 assert_eq!(
176 route("the agent keeps running out of context during long conversations"),
177 MemoryRoute::Semantic
178 );
179 }
180
181 #[test]
182 fn medium_non_question_routes_hybrid() {
183 assert_eq!(route("context window token budget"), MemoryRoute::Hybrid);
185 }
186
187 #[test]
188 fn empty_query_routes_keyword() {
189 assert_eq!(route(""), MemoryRoute::Keyword);
191 }
192
193 #[test]
194 fn question_word_only_routes_semantic() {
195 assert_eq!(route("what"), MemoryRoute::Semantic);
200 }
201
202 #[test]
203 fn camel_case_does_not_route_keyword_without_pattern() {
204 assert_eq!(
207 route("SemanticMemory configuration and options"),
208 MemoryRoute::Hybrid
209 );
210 }
211
212 #[test]
213 fn relationship_query_routes_graph() {
214 assert_eq!(
215 route("what is user's opinion on neovim"),
216 MemoryRoute::Graph
217 );
218 assert_eq!(
219 route("show the relationship between Alice and Bob"),
220 MemoryRoute::Graph
221 );
222 }
223
224 #[test]
225 fn relationship_query_related_to_routes_graph() {
226 assert_eq!(
227 route("how is Rust related to this project"),
228 MemoryRoute::Graph
229 );
230 assert_eq!(
231 route("how does this relates to the config"),
232 MemoryRoute::Graph
233 );
234 }
235
236 #[test]
237 fn relationship_know_about_routes_graph() {
238 assert_eq!(route("what do I know about neovim"), MemoryRoute::Graph);
239 }
240
241 #[test]
242 fn translate_does_not_route_graph() {
243 assert_ne!(route("translate this code to Python"), MemoryRoute::Graph);
246 }
247
248 #[test]
249 fn non_relationship_stays_semantic() {
250 assert_eq!(
251 route("find similar code patterns in the codebase"),
252 MemoryRoute::Semantic
253 );
254 }
255
256 #[test]
257 fn short_keyword_unchanged() {
258 assert_eq!(route("qdrant"), MemoryRoute::Keyword);
259 }
260
261 #[test]
263 fn long_nl_with_snake_case_routes_semantic() {
264 assert_eq!(
265 route("Use memory_search to find information about Rust ownership"),
266 MemoryRoute::Semantic
267 );
268 }
269
270 #[test]
271 fn short_snake_case_only_routes_keyword() {
272 assert_eq!(route("memory_search"), MemoryRoute::Keyword);
273 }
274
275 #[test]
276 fn question_with_snake_case_short_routes_semantic() {
277 assert_eq!(
278 route("What does memory_search return?"),
279 MemoryRoute::Semantic
280 );
281 }
282}