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
37#[cfg(feature = "graph-memory")]
40const RELATIONSHIP_PATTERNS: &[&str] = &[
41 "related to",
42 "relates to",
43 "connection between",
44 "relationship",
45 "opinion on",
46 "thinks about",
47 "preference for",
48 "history of",
49 "know about",
50];
51
52fn starts_with_question(words: &[&str]) -> bool {
53 words
54 .first()
55 .is_some_and(|w| QUESTION_WORDS.iter().any(|qw| w.eq_ignore_ascii_case(qw)))
56}
57
58fn is_pure_snake_case(word: &str) -> bool {
61 if word.is_empty() {
62 return false;
63 }
64 let has_underscore = word.contains('_');
65 if !has_underscore {
66 return false;
67 }
68 word.chars()
69 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
70 && !word.chars().all(|c| c.is_ascii_digit() || c == '_')
71}
72
73impl MemoryRouter for HeuristicRouter {
74 fn route(&self, query: &str) -> MemoryRoute {
75 let words: Vec<&str> = query.split_whitespace().collect();
76 let word_count = words.len();
77
78 #[cfg(feature = "graph-memory")]
80 {
81 let lower = query.to_ascii_lowercase();
82 let has_relationship = RELATIONSHIP_PATTERNS.iter().any(|p| lower.contains(p));
83 if has_relationship {
84 return MemoryRoute::Graph;
85 }
86 }
87
88 let has_structural_code_pattern = query.contains('/') || query.contains("::");
91
92 let has_snake_case = words.iter().any(|w| is_pure_snake_case(w));
95 let question = starts_with_question(&words);
96
97 if has_structural_code_pattern && !question {
98 return MemoryRoute::Keyword;
99 }
100
101 if has_snake_case && !question {
102 return MemoryRoute::Keyword;
103 }
104
105 if word_count <= 3 && !question {
107 return MemoryRoute::Keyword;
108 }
109
110 if question || word_count >= 6 {
112 return MemoryRoute::Semantic;
113 }
114
115 MemoryRoute::Hybrid
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 fn route(q: &str) -> MemoryRoute {
125 HeuristicRouter.route(q)
126 }
127
128 #[test]
129 fn rust_path_routes_keyword() {
130 assert_eq!(route("zeph_memory::recall"), MemoryRoute::Keyword);
131 }
132
133 #[test]
134 fn file_path_routes_keyword() {
135 assert_eq!(
136 route("crates/zeph-core/src/agent/mod.rs"),
137 MemoryRoute::Keyword
138 );
139 }
140
141 #[test]
142 fn pure_snake_case_routes_keyword() {
143 assert_eq!(route("memory_limit"), MemoryRoute::Keyword);
144 assert_eq!(route("error_handling"), MemoryRoute::Keyword);
145 }
146
147 #[test]
148 fn question_with_snake_case_routes_semantic() {
149 assert_eq!(
151 route("what is the memory_limit setting"),
152 MemoryRoute::Semantic
153 );
154 assert_eq!(route("how does error_handling work"), MemoryRoute::Semantic);
155 }
156
157 #[test]
158 fn short_query_routes_keyword() {
159 assert_eq!(route("context compaction"), MemoryRoute::Keyword);
160 assert_eq!(route("qdrant"), MemoryRoute::Keyword);
161 }
162
163 #[test]
164 fn question_routes_semantic() {
165 assert_eq!(
166 route("what is the purpose of semantic memory"),
167 MemoryRoute::Semantic
168 );
169 assert_eq!(route("how does the agent loop work"), MemoryRoute::Semantic);
170 assert_eq!(route("why does compaction fail"), MemoryRoute::Semantic);
171 assert_eq!(route("explain context compression"), MemoryRoute::Semantic);
172 }
173
174 #[test]
175 fn long_natural_query_routes_semantic() {
176 assert_eq!(
177 route("the agent keeps running out of context during long conversations"),
178 MemoryRoute::Semantic
179 );
180 }
181
182 #[test]
183 fn medium_non_question_routes_hybrid() {
184 assert_eq!(route("context window token budget"), MemoryRoute::Hybrid);
186 }
187
188 #[test]
189 fn empty_query_routes_keyword() {
190 assert_eq!(route(""), MemoryRoute::Keyword);
192 }
193
194 #[test]
195 fn question_word_only_routes_semantic() {
196 assert_eq!(route("what"), MemoryRoute::Semantic);
201 }
202
203 #[test]
204 fn camel_case_does_not_route_keyword_without_pattern() {
205 assert_eq!(
208 route("SemanticMemory configuration and options"),
209 MemoryRoute::Hybrid
210 );
211 }
212
213 #[cfg(feature = "graph-memory")]
214 #[test]
215 fn relationship_query_routes_graph() {
216 assert_eq!(
217 route("what is user's opinion on neovim"),
218 MemoryRoute::Graph
219 );
220 assert_eq!(
221 route("show the relationship between Alice and Bob"),
222 MemoryRoute::Graph
223 );
224 }
225
226 #[cfg(feature = "graph-memory")]
227 #[test]
228 fn relationship_query_related_to_routes_graph() {
229 assert_eq!(
230 route("how is Rust related to this project"),
231 MemoryRoute::Graph
232 );
233 assert_eq!(
234 route("how does this relates to the config"),
235 MemoryRoute::Graph
236 );
237 }
238
239 #[cfg(feature = "graph-memory")]
240 #[test]
241 fn relationship_know_about_routes_graph() {
242 assert_eq!(route("what do I know about neovim"), MemoryRoute::Graph);
243 }
244
245 #[cfg(feature = "graph-memory")]
246 #[test]
247 fn translate_does_not_route_graph() {
248 assert_ne!(route("translate this code to Python"), MemoryRoute::Graph);
251 }
252
253 #[test]
254 fn non_relationship_stays_semantic() {
255 assert_eq!(
256 route("find similar code patterns in the codebase"),
257 MemoryRoute::Semantic
258 );
259 }
260
261 #[test]
262 fn short_keyword_unchanged() {
263 assert_eq!(route("qdrant"), MemoryRoute::Keyword);
264 }
265}