Skip to main content

apollo/memory/
brief.rs

1//! Memory brief — time windows + open loops (Vellum-style, KV-backed).
2
3use std::sync::Arc;
4
5use crate::memory::MemoryBackend;
6
7pub const BRIEF_NS: &str = "brief";
8pub const OPEN_LOOPS_KEY: &str = "open_loops";
9pub const TIME_CONTEXTS_KEY: &str = "time_contexts";
10
11pub fn parse_bullet_list(value: &str) -> Vec<String> {
12    value
13        .lines()
14        .map(str::trim)
15        .filter(|l| !l.is_empty() && !l.starts_with('#'))
16        .map(|l| {
17            l.trim_start_matches("- ")
18                .trim_start_matches("* ")
19                .to_string()
20        })
21        .filter(|l| !l.is_empty())
22        .collect()
23}
24
25pub async fn load_brief(memory: &Arc<dyn MemoryBackend>) -> (Vec<String>, Vec<String>) {
26    let loops = memory
27        .recall(BRIEF_NS, OPEN_LOOPS_KEY)
28        .await
29        .ok()
30        .flatten()
31        .map(|e| parse_bullet_list(&e.value))
32        .unwrap_or_default();
33    let contexts = memory
34        .recall(BRIEF_NS, TIME_CONTEXTS_KEY)
35        .await
36        .ok()
37        .flatten()
38        .map(|e| parse_bullet_list(&e.value))
39        .unwrap_or_default();
40    (contexts, loops)
41}
42
43pub fn format_brief_xml(time_contexts: &[String], open_loops: &[String]) -> Option<String> {
44    if time_contexts.is_empty() && open_loops.is_empty() {
45        return None;
46    }
47    let mut out = String::from("<memory_brief>\n");
48    if !time_contexts.is_empty() {
49        out.push_str("  <time_contexts>\n");
50        for line in time_contexts {
51            out.push_str("    - ");
52            out.push_str(line);
53            out.push('\n');
54        }
55        out.push_str("  </time_contexts>\n");
56    }
57    if !open_loops.is_empty() {
58        out.push_str("  <open_loops>\n");
59        for line in open_loops {
60            out.push_str("    - ");
61            out.push_str(line);
62            out.push('\n');
63        }
64        out.push_str("  </open_loops>\n");
65    }
66    out.push_str("</memory_brief>");
67    Some(out)
68}
69
70#[cfg(test)]
71fn demo_brief() {
72    let xml = format_brief_xml(
73        &["this week: ship memory".into()],
74        &["follow up on graph vendor".into()],
75    )
76    .unwrap();
77    assert!(xml.contains("open_loops"));
78    assert!(xml.contains("graph vendor"));
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn parse_bullets() {
87        let v = "- a\n* b\n\n# c\n- d";
88        assert_eq!(parse_bullet_list(v), vec!["a", "b", "d"]);
89    }
90
91    #[test]
92    fn brief_xml() {
93        demo_brief();
94    }
95}