Skip to main content

oxios_memory/memory/
root_index.rs

1#![allow(missing_docs)]
2//! ROOT index — the "table of contents" for all agent knowledge.
3//!
4//! Provides O(1) topic lookup so agents can quickly understand what they know.
5//! Automatically maintained by the Dream process; users never interact with it.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10use crate::memory::types::{MemoryType, ProtectionLevel};
11
12// ---------------------------------------------------------------------------
13// RootIndex
14// ---------------------------------------------------------------------------
15
16/// ROOT index — the "table of contents" for all agent knowledge.
17///
18/// Agents use this to understand what they know at a glance (O(1) lookup).
19/// Dream automatically rebuilds this on every run.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct RootIndex {
22    /// Index version (incremented on each dream).
23    pub version: u64,
24    /// Last update timestamp.
25    pub updated_at: DateTime<Utc>,
26    /// Active context entries (recent ~7 days).
27    pub active_context: Vec<RootEntry>,
28    /// Recent patterns observed across sessions.
29    pub recent_patterns: Vec<String>,
30    /// Historical summary (monthly breakdowns).
31    pub historical_summary: Vec<HistoricalPeriod>,
32    /// Topic index — all known topics with type and freshness.
33    pub topics: Vec<TopicEntry>,
34}
35
36impl Default for RootIndex {
37    fn default() -> Self {
38        Self {
39            version: 0,
40            updated_at: Utc::now(),
41            active_context: Vec::new(),
42            recent_patterns: Vec::new(),
43            historical_summary: Vec::new(),
44            topics: Vec::new(),
45        }
46    }
47}
48
49impl RootIndex {
50    /// Create a new empty ROOT index.
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Estimate token count for this index (4 chars ≈ 1 token).
56    pub fn estimated_tokens(&self) -> usize {
57        let total_chars: usize = self
58            .active_context
59            .iter()
60            .map(|e| e.topic.len() + e.reference.len())
61            .chain(self.recent_patterns.iter().map(|p| p.len()))
62            .chain(
63                self.topics
64                    .iter()
65                    .map(|t| t.name.len() + t.description.len()),
66            )
67            .sum();
68        total_chars / 4
69    }
70
71    /// Check if a topic matches a query string.
72    pub fn topic_matches_query(&self, topic: &TopicEntry, query: &str) -> bool {
73        let query_lower = query.to_lowercase();
74        topic.name.to_lowercase().contains(&query_lower)
75            || topic.description.to_lowercase().contains(&query_lower)
76            || topic.category.to_lowercase().contains(&query_lower)
77    }
78}
79
80// ---------------------------------------------------------------------------
81// Supporting types
82// ---------------------------------------------------------------------------
83
84/// A single entry in the ROOT index's active context.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct RootEntry {
87    /// Topic name.
88    pub topic: String,
89    /// Memory type.
90    pub memory_type: MemoryType,
91    /// Protection level.
92    pub protection: ProtectionLevel,
93    /// Age in days.
94    pub age_days: u32,
95    /// Reference (memory entry ID or file path).
96    pub reference: String,
97}
98
99/// A historical period summary.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct HistoricalPeriod {
102    /// Period label (e.g., "2026-05").
103    pub period: String,
104    /// Summary of activities in this period.
105    pub summary: String,
106}
107
108/// A topic entry in the ROOT index.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct TopicEntry {
111    /// Topic name.
112    pub name: String,
113    /// Category (e.g., "project", "preference", "decision").
114    pub category: String,
115    /// Age in days.
116    pub age_days: u32,
117    /// Brief description.
118    pub description: String,
119    /// Reference (memory entry ID).
120    pub reference: String,
121}
122
123// ---------------------------------------------------------------------------
124// Tests
125// ---------------------------------------------------------------------------
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn test_root_index_default() {
133        let idx = RootIndex::default();
134        assert_eq!(idx.version, 0);
135        assert!(idx.active_context.is_empty());
136        assert!(idx.topics.is_empty());
137    }
138
139    #[test]
140    fn test_estimated_tokens() {
141        let mut idx = RootIndex::new();
142        idx.topics.push(TopicEntry {
143            name: "Rust async runtime".to_string(),
144            category: "project".to_string(),
145            age_days: 5,
146            description: "Using Tokio for async".to_string(),
147            reference: "fact-123".to_string(),
148        });
149        let tokens = idx.estimated_tokens();
150        assert!(tokens > 0, "Should have some estimated tokens");
151    }
152
153    #[test]
154    fn test_topic_matches_query() {
155        let idx = RootIndex::new();
156        let topic = TopicEntry {
157            name: "Memory consolidation".to_string(),
158            category: "architecture".to_string(),
159            age_days: 3,
160            description: "RFC-008 tiered memory system".to_string(),
161            reference: "dec-456".to_string(),
162        };
163        assert!(idx.topic_matches_query(&topic, "memory"));
164        assert!(idx.topic_matches_query(&topic, "consolidation"));
165        assert!(idx.topic_matches_query(&topic, "architecture"));
166        assert!(!idx.topic_matches_query(&topic, "deployment"));
167    }
168}