1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum MemoryRoute {
7 Keyword,
9 Semantic,
11 Hybrid,
13}
14
15pub trait MemoryRouter: Send + Sync {
17 fn route(&self, query: &str) -> MemoryRoute;
19}
20
21pub struct HeuristicRouter;
29
30const QUESTION_WORDS: &[&str] = &[
31 "what", "how", "why", "when", "where", "who", "which", "explain", "describe",
32];
33
34fn starts_with_question(words: &[&str]) -> bool {
35 words
36 .first()
37 .is_some_and(|w| QUESTION_WORDS.iter().any(|qw| w.eq_ignore_ascii_case(qw)))
38}
39
40fn is_pure_snake_case(word: &str) -> bool {
43 if word.is_empty() {
44 return false;
45 }
46 let has_underscore = word.contains('_');
47 if !has_underscore {
48 return false;
49 }
50 word.chars()
51 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
52 && !word.chars().all(|c| c.is_ascii_digit() || c == '_')
53}
54
55impl MemoryRouter for HeuristicRouter {
56 fn route(&self, query: &str) -> MemoryRoute {
57 let words: Vec<&str> = query.split_whitespace().collect();
58 let word_count = words.len();
59
60 let has_structural_code_pattern = query.contains('/') || query.contains("::");
63
64 let has_snake_case = words.iter().any(|w| is_pure_snake_case(w));
67 let question = starts_with_question(&words);
68
69 if has_structural_code_pattern && !question {
70 return MemoryRoute::Keyword;
71 }
72
73 if has_snake_case && !question {
74 return MemoryRoute::Keyword;
75 }
76
77 if word_count <= 3 && !question {
79 return MemoryRoute::Keyword;
80 }
81
82 if question || word_count >= 6 {
84 return MemoryRoute::Semantic;
85 }
86
87 MemoryRoute::Hybrid
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 fn route(q: &str) -> MemoryRoute {
97 HeuristicRouter.route(q)
98 }
99
100 #[test]
101 fn rust_path_routes_keyword() {
102 assert_eq!(route("zeph_memory::recall"), MemoryRoute::Keyword);
103 }
104
105 #[test]
106 fn file_path_routes_keyword() {
107 assert_eq!(
108 route("crates/zeph-core/src/agent/mod.rs"),
109 MemoryRoute::Keyword
110 );
111 }
112
113 #[test]
114 fn pure_snake_case_routes_keyword() {
115 assert_eq!(route("memory_limit"), MemoryRoute::Keyword);
116 assert_eq!(route("error_handling"), MemoryRoute::Keyword);
117 }
118
119 #[test]
120 fn question_with_snake_case_routes_semantic() {
121 assert_eq!(
123 route("what is the memory_limit setting"),
124 MemoryRoute::Semantic
125 );
126 assert_eq!(route("how does error_handling work"), MemoryRoute::Semantic);
127 }
128
129 #[test]
130 fn short_query_routes_keyword() {
131 assert_eq!(route("context compaction"), MemoryRoute::Keyword);
132 assert_eq!(route("qdrant"), MemoryRoute::Keyword);
133 }
134
135 #[test]
136 fn question_routes_semantic() {
137 assert_eq!(
138 route("what is the purpose of semantic memory"),
139 MemoryRoute::Semantic
140 );
141 assert_eq!(route("how does the agent loop work"), MemoryRoute::Semantic);
142 assert_eq!(route("why does compaction fail"), MemoryRoute::Semantic);
143 assert_eq!(route("explain context compression"), MemoryRoute::Semantic);
144 }
145
146 #[test]
147 fn long_natural_query_routes_semantic() {
148 assert_eq!(
149 route("the agent keeps running out of context during long conversations"),
150 MemoryRoute::Semantic
151 );
152 }
153
154 #[test]
155 fn medium_non_question_routes_hybrid() {
156 assert_eq!(route("context window token budget"), MemoryRoute::Hybrid);
158 }
159
160 #[test]
161 fn empty_query_routes_keyword() {
162 assert_eq!(route(""), MemoryRoute::Keyword);
164 }
165
166 #[test]
167 fn question_word_only_routes_semantic() {
168 assert_eq!(route("what"), MemoryRoute::Semantic);
173 }
174
175 #[test]
176 fn camel_case_does_not_route_keyword_without_pattern() {
177 assert_eq!(
180 route("SemanticMemory configuration and options"),
181 MemoryRoute::Hybrid
182 );
183 }
184}