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 has_snake_case && !question {
100 return MemoryRoute::Keyword;
101 }
102
103 if word_count <= 3 && !question {
105 return MemoryRoute::Keyword;
106 }
107
108 if question || word_count >= 6 {
110 return MemoryRoute::Semantic;
111 }
112
113 MemoryRoute::Hybrid
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 fn route(q: &str) -> MemoryRoute {
123 HeuristicRouter.route(q)
124 }
125
126 #[test]
127 fn rust_path_routes_keyword() {
128 assert_eq!(route("zeph_memory::recall"), MemoryRoute::Keyword);
129 }
130
131 #[test]
132 fn file_path_routes_keyword() {
133 assert_eq!(
134 route("crates/zeph-core/src/agent/mod.rs"),
135 MemoryRoute::Keyword
136 );
137 }
138
139 #[test]
140 fn pure_snake_case_routes_keyword() {
141 assert_eq!(route("memory_limit"), MemoryRoute::Keyword);
142 assert_eq!(route("error_handling"), MemoryRoute::Keyword);
143 }
144
145 #[test]
146 fn question_with_snake_case_routes_semantic() {
147 assert_eq!(
149 route("what is the memory_limit setting"),
150 MemoryRoute::Semantic
151 );
152 assert_eq!(route("how does error_handling work"), MemoryRoute::Semantic);
153 }
154
155 #[test]
156 fn short_query_routes_keyword() {
157 assert_eq!(route("context compaction"), MemoryRoute::Keyword);
158 assert_eq!(route("qdrant"), MemoryRoute::Keyword);
159 }
160
161 #[test]
162 fn question_routes_semantic() {
163 assert_eq!(
164 route("what is the purpose of semantic memory"),
165 MemoryRoute::Semantic
166 );
167 assert_eq!(route("how does the agent loop work"), MemoryRoute::Semantic);
168 assert_eq!(route("why does compaction fail"), MemoryRoute::Semantic);
169 assert_eq!(route("explain context compression"), MemoryRoute::Semantic);
170 }
171
172 #[test]
173 fn long_natural_query_routes_semantic() {
174 assert_eq!(
175 route("the agent keeps running out of context during long conversations"),
176 MemoryRoute::Semantic
177 );
178 }
179
180 #[test]
181 fn medium_non_question_routes_hybrid() {
182 assert_eq!(route("context window token budget"), MemoryRoute::Hybrid);
184 }
185
186 #[test]
187 fn empty_query_routes_keyword() {
188 assert_eq!(route(""), MemoryRoute::Keyword);
190 }
191
192 #[test]
193 fn question_word_only_routes_semantic() {
194 assert_eq!(route("what"), MemoryRoute::Semantic);
199 }
200
201 #[test]
202 fn camel_case_does_not_route_keyword_without_pattern() {
203 assert_eq!(
206 route("SemanticMemory configuration and options"),
207 MemoryRoute::Hybrid
208 );
209 }
210
211 #[test]
212 fn relationship_query_routes_graph() {
213 assert_eq!(
214 route("what is user's opinion on neovim"),
215 MemoryRoute::Graph
216 );
217 assert_eq!(
218 route("show the relationship between Alice and Bob"),
219 MemoryRoute::Graph
220 );
221 }
222
223 #[test]
224 fn relationship_query_related_to_routes_graph() {
225 assert_eq!(
226 route("how is Rust related to this project"),
227 MemoryRoute::Graph
228 );
229 assert_eq!(
230 route("how does this relates to the config"),
231 MemoryRoute::Graph
232 );
233 }
234
235 #[test]
236 fn relationship_know_about_routes_graph() {
237 assert_eq!(route("what do I know about neovim"), MemoryRoute::Graph);
238 }
239
240 #[test]
241 fn translate_does_not_route_graph() {
242 assert_ne!(route("translate this code to Python"), MemoryRoute::Graph);
245 }
246
247 #[test]
248 fn non_relationship_stays_semantic() {
249 assert_eq!(
250 route("find similar code patterns in the codebase"),
251 MemoryRoute::Semantic
252 );
253 }
254
255 #[test]
256 fn short_keyword_unchanged() {
257 assert_eq!(route("qdrant"), MemoryRoute::Keyword);
258 }
259}