Skip to main content

deepstrike_core/mm/
memory.rs

1//! Long-term memory management (Phase 7).
2//!
3//! Kernel defines memory types and validation rules; SDKs perform I/O and selection.
4//! No I/O in this module — pure classification and validation logic.
5
6use serde::{Deserialize, Serialize};
7
8/// Memory kind (4 types, mirroring Claude Code's taxonomy).
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum MemoryKind {
12    /// User profile: who they are, expertise level, role.
13    User,
14    /// Behavior preference: what they like/dislike, approved patterns.
15    #[serde(rename = "feedback")]
16    BehaviorPreference,
17    /// Project context: what's happening, milestones, phases.
18    Project,
19    /// External pointer: where to find things (tickets, docs).
20    Reference,
21}
22
23impl MemoryKind {
24    pub fn label(self) -> &'static str {
25        match self {
26            Self::User => "user",
27            Self::BehaviorPreference => "feedback",
28            Self::Project => "project",
29            Self::Reference => "reference",
30        }
31    }
32
33}
34
35/// Lightweight memory metadata (kernel stores, SDK provides full content).
36#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37pub struct MemoryMetadata {
38    /// Memory slug (unique identifier).
39    pub name: String,
40
41    /// One-line description (for index display).
42    pub description: String,
43
44    /// Memory kind (optional; kernel infers if omitted).
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub kind: Option<MemoryKind>,
47
48    /// Creation timestamp (for stale warnings).
49    #[serde(default)]
50    pub created_at: u64,
51
52    /// Last update timestamp.
53    #[serde(default)]
54    pub updated_at: u64,
55
56    /// Associated session ID (for provenance).
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub session_id: Option<String>,
59
60}
61
62/// Memory write request (SDK → kernel).
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct MemoryWriteRequest {
65    pub metadata: MemoryMetadata,
66    pub content: String,
67}
68
69/// Memory query request (kernel → SDK).
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct MemoryQuery {
72    /// Current context summary (for selection).
73    pub current_context: String,
74
75    /// Active tools (filter recentTools).
76    #[serde(default, skip_serializing_if = "Vec::is_empty")]
77    pub active_tools: Vec<String>,
78
79    /// Recently surfaced memory IDs (filter alreadySurfaced).
80    #[serde(default, skip_serializing_if = "Vec::is_empty")]
81    pub already_surfaced: Vec<String>,
82
83    /// Return count limit (default: 5).
84    #[serde(default = "default_top_k")]
85    pub top_k: usize,
86}
87
88fn default_top_k() -> usize { 5 }
89
90impl Default for MemoryQuery {
91    fn default() -> Self {
92        Self {
93            current_context: String::new(),
94            active_tools: Vec::new(),
95            already_surfaced: Vec::new(),
96            top_k: 5,
97        }
98    }
99}
100
101/// Memory retrieval response (SDK → kernel).
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct MemoryRetrieval {
104    /// Selected memory IDs.
105    pub selected_memory_ids: Vec<String>,
106
107    /// Selection rationale (for kernel logging).
108    pub selection_rationale: String,
109}
110
111/// Memory validation error.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(tag = "error_kind", rename_all = "snake_case")]
114pub enum MemoryValidationError {
115    MissingRequiredField { field: String },
116    ContentTooLarge { size: u32, limit: u32 },
117    ForbiddenPattern { pattern: String, reason: String },
118    InvalidKind { kind: String },
119    NameTooLong { length: usize, limit: usize },
120}
121
122/// Memory validation rules (kernel-enforced).
123#[derive(Debug, Clone)]
124pub struct MemoryValidation {
125    pub max_size_bytes: u32,
126    pub max_name_length: usize,
127    pub required_fields: Vec<String>,
128    pub forbidden_patterns: Vec<(String, &'static str)>,
129}
130
131impl MemoryValidation {
132    /// Validate a memory write request.
133    pub fn validate(&self, request: &MemoryWriteRequest) -> Result<(), MemoryValidationError> {
134        // Check required fields
135        for field in &self.required_fields {
136            match field.as_str() {
137                "name" if request.metadata.name.is_empty() => {
138                    return Err(MemoryValidationError::MissingRequiredField { field: "name".into() });
139                }
140                "description" if request.metadata.description.is_empty() => {
141                    return Err(MemoryValidationError::MissingRequiredField { field: "description".into() });
142                }
143                _ => {}
144            }
145        }
146
147        // Check name length
148        if request.metadata.name.len() > self.max_name_length {
149            return Err(MemoryValidationError::NameTooLong {
150                length: request.metadata.name.len(),
151                limit: self.max_name_length,
152            });
153        }
154
155        // Check content size
156        if request.content.len() > self.max_size_bytes as usize {
157            return Err(MemoryValidationError::ContentTooLarge {
158                size: request.content.len() as u32,
159                limit: self.max_size_bytes,
160            });
161        }
162
163        // Check forbidden patterns
164        for (pattern, reason) in &self.forbidden_patterns {
165            if request.content.contains(pattern) {
166                return Err(MemoryValidationError::ForbiddenPattern {
167                    pattern: pattern.clone(),
168                    reason: reason.to_string(),
169                });
170            }
171        }
172
173        Ok(())
174    }
175}
176
177/// Validate a memory write request with default validation rules.
178pub fn validate_memory_write(request: &MemoryWriteRequest) -> Result<(), MemoryValidationError> {
179    MemoryValidation::default().validate(request)
180}
181
182/// Declarative configuration for the kernel's long-term memory subsystem.
183///
184/// Installed via the `set_memory_policy` input event (opt-in). When no policy is installed the
185/// kernel preserves pre-policy behavior: every `write_memory` is validated with the default rules
186/// and `query_memory` uses the requested `top_k` verbatim. Installing a policy makes these knobs
187/// authoritative:
188/// - `validation_enabled = false` admits every write without validation.
189/// - `retrieval_top_k` is an upper bound: the emitted `requested_k` is `min(query.top_k, top_k)`.
190/// - `max_content_bytes` / `max_name_length` override the validation size limits when set.
191///
192/// `memory_path` and `stale_warning_days` are not enforced inside the kernel (the kernel performs
193/// no recall I/O); they are carried so the SDK consumes a single authoritative config.
194#[derive(Debug, Clone)]
195pub struct MemoryPolicy {
196    pub memory_path: String,
197    pub stale_warning_days: u32,
198    pub retrieval_top_k: usize,
199    pub validation_enabled: bool,
200    pub max_content_bytes: Option<u32>,
201    pub max_name_length: Option<usize>,
202}
203
204impl Default for MemoryPolicy {
205    fn default() -> Self {
206        Self {
207            memory_path: String::new(),
208            stale_warning_days: 2,
209            retrieval_top_k: 5,
210            validation_enabled: true,
211            max_content_bytes: None,
212            max_name_length: None,
213        }
214    }
215}
216
217impl MemoryPolicy {
218    /// Build the validation rules this policy implies, starting from the kernel defaults and
219    /// applying any size / name-length overrides.
220    pub fn validation(&self) -> MemoryValidation {
221        let mut v = MemoryValidation::default();
222        if let Some(bytes) = self.max_content_bytes {
223            v.max_size_bytes = bytes;
224        }
225        if let Some(len) = self.max_name_length {
226            v.max_name_length = len;
227        }
228        v
229    }
230
231    /// Clamp a requested retrieval count to this policy's `retrieval_top_k` upper bound.
232    pub fn clamp_top_k(&self, requested: usize) -> usize {
233        requested.min(self.retrieval_top_k)
234    }
235}
236
237/// Default validation rules (aligned with Claude Code's "what NOT to store").
238impl Default for MemoryValidation {
239    fn default() -> Self {
240        Self {
241            max_size_bytes: 10_000,
242            max_name_length: 100,
243            required_fields: vec!["name".into(), "description".into()],
244            // P13: no baked-in content heuristics — what belongs in memory is host/model
245            // judgment. Hosts configure forbidden prefixes via MemoryPolicy when they want them.
246            forbidden_patterns: Vec::new(),
247        }
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    #[test]
256    fn memory_kind_labels_correct() {
257        assert_eq!(MemoryKind::User.label(), "user");
258        assert_eq!(MemoryKind::BehaviorPreference.label(), "feedback");
259        assert_eq!(MemoryKind::Project.label(), "project");
260        assert_eq!(MemoryKind::Reference.label(), "reference");
261    }
262
263    #[test]
264    fn validation_passes_for_valid_request() {
265        let validation = MemoryValidation::default();
266        let request = MemoryWriteRequest {
267            metadata: MemoryMetadata {
268                name: "test-memory".into(),
269                description: "A valid memory".into(),
270                ..Default::default()
271            },
272            content: "This is fine".to_string(),
273        };
274        assert!(validation.validate(&request).is_ok());
275    }
276
277    #[test]
278    fn validation_rejects_missing_name() {
279        let validation = MemoryValidation::default();
280        let request = MemoryWriteRequest {
281            metadata: MemoryMetadata {
282                name: "".into(),
283                description: "Missing name".into(),
284                ..Default::default()
285            },
286            content: "content".to_string(),
287        };
288        assert!(matches!(
289            validation.validate(&request),
290            Err(MemoryValidationError::MissingRequiredField { field }) if field == "name"
291        ));
292    }
293
294    #[test]
295    fn validation_rejects_host_configured_forbidden_pattern() {
296        // P13: no baked-in content heuristics — the mechanism only bites when a HOST
297        // configures forbidden prefixes on its MemoryPolicy.
298        let mut validation = MemoryValidation::default();
299        assert!(validation.forbidden_patterns.is_empty(), "no defaults");
300        validation
301            .forbidden_patterns
302            .push(("TODO:".into(), "transient tasks do not belong in memory"));
303        let request = MemoryWriteRequest {
304            metadata: MemoryMetadata {
305                name: "bad-memory".into(),
306                description: "Contains forbidden pattern".into(),
307                ..Default::default()
308            },
309            content: "TODO: ship it".to_string(),
310        };
311        assert!(matches!(
312            validation.validate(&request),
313            Err(MemoryValidationError::ForbiddenPattern { .. })
314        ));
315    }
316
317    #[test]
318    fn validation_rejects_oversized_content() {
319        let validation = MemoryValidation::default();
320        let request = MemoryWriteRequest {
321            metadata: MemoryMetadata {
322                name: "huge-memory".into(),
323                description: "Too large".into(),
324                ..Default::default()
325            },
326            content: "x".repeat(20_000),
327        };
328        assert!(matches!(
329            validation.validate(&request),
330            Err(MemoryValidationError::ContentTooLarge { .. })
331        ));
332    }
333
334    #[test]
335    fn memory_query_defaults_top_k_to_5() {
336        let query = MemoryQuery {
337            current_context: "test".into(),
338            ..Default::default()
339        };
340        assert_eq!(query.top_k, 5);
341    }
342}