1use std::path::Path;
4use std::sync::Arc;
5
6use crate::memory::search::memory_search;
7use crate::memory::MemoryBackend;
8
9const RECALL_PATTERNS: &[&str] = &[
10 "remember",
11 "last time",
12 "we discussed",
13 "as before",
14 "earlier",
15 "you said",
16 "what did we",
17 "previously",
18 "again",
19];
20
21pub fn should_recall_gate(text: &str) -> bool {
22 let lower = text.to_lowercase();
23 RECALL_PATTERNS.iter().any(|p| lower.contains(p))
24}
25
26pub async fn build_supporting_recall(
27 workspace: &Path,
28 memory: &Arc<dyn MemoryBackend>,
29 user_text: &str,
30 limit: usize,
31) -> Option<String> {
32 if !should_recall_gate(user_text) {
33 return None;
34 }
35
36 let mut bullets: Vec<String> = Vec::new();
37
38 for hit in memory_search(workspace, user_text, limit)
39 .into_iter()
40 .take(limit)
41 {
42 bullets.push(format!(
43 "- [{}:{}] {}",
44 hit.path, hit.line_number, hit.snippet
45 ));
46 }
47
48 if bullets.len() < limit {
49 if let Ok(kv) = memory.search("notes", user_text, limit).await {
50 for entry in kv.into_iter().take(limit.saturating_sub(bullets.len())) {
51 bullets.push(format!("- [kv:{}] {}", entry.key, entry.value));
52 }
53 }
54 }
55
56 if bullets.is_empty() {
57 return None;
58 }
59
60 let body = bullets.join("\n");
61 Some(format!("<supporting_recall>\n{body}\n</supporting_recall>"))
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn gate_triggers() {
70 assert!(should_recall_gate("Do you remember the deploy?"));
71 assert!(!should_recall_gate("deploy now"));
72 }
73}