Skip to main content

apollo/memory/
recall.rs

1//! Recall gate — inject file/KV snippets when the user references past context.
2
3use 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}