1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum MemoryKind {
12 User,
14 #[serde(rename = "feedback")]
16 BehaviorPreference,
17 Project,
19 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37pub struct MemoryMetadata {
38 pub name: String,
40
41 pub description: String,
43
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub kind: Option<MemoryKind>,
47
48 #[serde(default)]
50 pub created_at: u64,
51
52 #[serde(default)]
54 pub updated_at: u64,
55
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub session_id: Option<String>,
59
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct MemoryWriteRequest {
65 pub metadata: MemoryMetadata,
66 pub content: String,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct MemoryQuery {
72 pub current_context: String,
74
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
77 pub active_tools: Vec<String>,
78
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
81 pub already_surfaced: Vec<String>,
82
83 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct MemoryRetrieval {
104 pub selected_memory_ids: Vec<String>,
106
107 pub selection_rationale: String,
109}
110
111#[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#[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 pub fn validate(&self, request: &MemoryWriteRequest) -> Result<(), MemoryValidationError> {
134 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 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 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 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
177pub fn validate_memory_write(request: &MemoryWriteRequest) -> Result<(), MemoryValidationError> {
179 MemoryValidation::default().validate(request)
180}
181
182#[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 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 pub fn clamp_top_k(&self, requested: usize) -> usize {
233 requested.min(self.retrieval_top_k)
234 }
235}
236
237impl 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 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 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}