Skip to main content

everruns_core/
memory_store.rs

1// Memory store module — persistent cross-session memory for agents.
2// See specs/memory.md for design rationale.
3//
4// Decision: MemoryContentPart reuses same shape as message ContentPart (text + image)
5//   but is a separate type to exclude tool-call/tool-result variants.
6// Decision: Multiple stores per org, selected via capability config.
7// Decision: Capacity limits enforced on write (remember tool + REST API).
8
9use crate::typed_id::{MemoryId, MemoryStoreId, OrgId};
10use async_trait::async_trait;
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13
14#[cfg(feature = "openapi")]
15use utoipa::ToSchema;
16
17// ============================================================================
18// Memory Content Parts (multicontent for recall)
19// ============================================================================
20
21/// Content part for memory entries — same discriminated-union shape as message
22/// `ContentPart` but restricted to text and image variants.
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
24#[cfg_attr(feature = "openapi", derive(ToSchema))]
25#[serde(tag = "type", rename_all = "snake_case")]
26pub enum MemoryContentPart {
27    /// Text content
28    Text(MemoryTextPart),
29    /// Image content (inline base64)
30    Image(MemoryImagePart),
31}
32
33/// Text content within a memory.
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35#[cfg_attr(feature = "openapi", derive(ToSchema))]
36pub struct MemoryTextPart {
37    pub text: String,
38}
39
40/// Image content within a memory (base64-encoded).
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42#[cfg_attr(feature = "openapi", derive(ToSchema))]
43pub struct MemoryImagePart {
44    /// Base64-encoded image data
45    pub base64: String,
46    /// MIME type (e.g. "image/png")
47    pub media_type: String,
48}
49
50impl MemoryContentPart {
51    pub fn text(text: impl Into<String>) -> Self {
52        Self::Text(MemoryTextPart { text: text.into() })
53    }
54
55    pub fn image(base64: impl Into<String>, media_type: impl Into<String>) -> Self {
56        Self::Image(MemoryImagePart {
57            base64: base64.into(),
58            media_type: media_type.into(),
59        })
60    }
61
62    /// Estimated size in bytes (for capacity limit checks).
63    pub fn estimated_size(&self) -> usize {
64        match self {
65            Self::Text(t) => t.text.len(),
66            Self::Image(i) => i.base64.len(),
67        }
68    }
69}
70
71// ============================================================================
72// Memory Kind
73// ============================================================================
74
75/// Classification of a memory entry.
76#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
77#[cfg_attr(feature = "openapi", derive(ToSchema))]
78#[serde(rename_all = "snake_case")]
79pub enum MemoryKind {
80    #[default]
81    Fact,
82    Preference,
83    Correction,
84    Procedure,
85    Context,
86}
87
88impl std::fmt::Display for MemoryKind {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        match self {
91            Self::Fact => write!(f, "fact"),
92            Self::Preference => write!(f, "preference"),
93            Self::Correction => write!(f, "correction"),
94            Self::Procedure => write!(f, "procedure"),
95            Self::Context => write!(f, "context"),
96        }
97    }
98}
99
100// ============================================================================
101// Memory Entity
102// ============================================================================
103
104/// A single memory entry within a store.
105#[derive(Debug, Clone, Serialize, Deserialize)]
106#[cfg_attr(feature = "openapi", derive(ToSchema))]
107pub struct Memory {
108    pub id: MemoryId,
109    pub store_id: MemoryStoreId,
110    pub content: String,
111    pub content_parts: Vec<MemoryContentPart>,
112    pub kind: MemoryKind,
113    /// 1-10 importance score (higher = more important)
114    pub importance: u8,
115    pub tags: Vec<String>,
116    pub active: bool,
117    pub created_at: DateTime<Utc>,
118    pub updated_at: DateTime<Utc>,
119}
120
121// ============================================================================
122// Memory Store Entity
123// ============================================================================
124
125/// An org-scoped container for memories.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127#[cfg_attr(feature = "openapi", derive(ToSchema))]
128pub struct MemoryStoreEntity {
129    pub id: MemoryStoreId,
130    pub org_id: OrgId,
131    pub name: String,
132    pub is_default: bool,
133    pub created_at: DateTime<Utc>,
134}
135
136// ============================================================================
137// Capacity Limits
138// ============================================================================
139
140/// Capacity limits for memory operations.
141pub struct MemoryLimits;
142
143impl MemoryLimits {
144    /// Max characters in memory content field.
145    pub const MAX_CONTENT_LENGTH: usize = 2_000;
146    /// Max tags per memory.
147    pub const MAX_TAGS: usize = 10;
148    /// Max image content parts per memory.
149    pub const MAX_IMAGE_PARTS: usize = 4;
150    /// Max single image size (5 MB base64).
151    pub const MAX_IMAGE_SIZE: usize = 5 * 1024 * 1024;
152    /// Max total image data per memory (10 MB base64).
153    pub const MAX_TOTAL_IMAGE_SIZE: usize = 10 * 1024 * 1024;
154    /// Max active memories per store.
155    pub const MAX_MEMORIES_PER_STORE: usize = 10_000;
156    /// Max stores per org.
157    pub const MAX_STORES_PER_ORG: usize = 50;
158
159    /// Validate a memory before creation/update.
160    pub fn validate(
161        content: &str,
162        tags: &[String],
163        parts: &[MemoryContentPart],
164    ) -> Result<(), String> {
165        if content.len() > Self::MAX_CONTENT_LENGTH {
166            return Err(format!(
167                "Memory content exceeds {} character limit (got {})",
168                Self::MAX_CONTENT_LENGTH,
169                content.len()
170            ));
171        }
172        if tags.len() > Self::MAX_TAGS {
173            return Err(format!(
174                "Too many tags: max {}, got {}",
175                Self::MAX_TAGS,
176                tags.len()
177            ));
178        }
179
180        let mut image_count = 0;
181        let mut total_image_size = 0;
182        for part in parts {
183            if let MemoryContentPart::Image(img) = part {
184                image_count += 1;
185                let size = img.base64.len();
186                if size > Self::MAX_IMAGE_SIZE {
187                    return Err(format!(
188                        "Image exceeds {} byte limit (got {})",
189                        Self::MAX_IMAGE_SIZE,
190                        size
191                    ));
192                }
193                total_image_size += size;
194            }
195        }
196        if image_count > Self::MAX_IMAGE_PARTS {
197            return Err(format!(
198                "Too many images: max {}, got {}",
199                Self::MAX_IMAGE_PARTS,
200                image_count
201            ));
202        }
203        if total_image_size > Self::MAX_TOTAL_IMAGE_SIZE {
204            return Err(format!(
205                "Total image data exceeds {} byte limit (got {})",
206                Self::MAX_TOTAL_IMAGE_SIZE,
207                total_image_size
208            ));
209        }
210
211        Ok(())
212    }
213}
214
215// ============================================================================
216// Store Trait
217// ============================================================================
218
219/// Query parameters for recalling memories.
220#[derive(Debug, Default)]
221pub struct MemoryQuery {
222    pub store_id: Option<MemoryStoreId>,
223    pub query: Option<String>,
224    pub tags: Option<Vec<String>>,
225    pub kind: Option<MemoryKind>,
226    pub limit: usize,
227}
228
229/// Trait for persistent memory storage backends.
230#[async_trait]
231pub trait MemoryStoreBackend: Send + Sync {
232    /// Get or create the default store for an org.
233    async fn get_or_create_default_store(
234        &self,
235        org_id: OrgId,
236    ) -> crate::error::Result<MemoryStoreEntity>;
237
238    /// Get a store by ID.
239    async fn get_store(
240        &self,
241        store_id: MemoryStoreId,
242    ) -> crate::error::Result<Option<MemoryStoreEntity>>;
243
244    /// Create a memory in a store.
245    async fn create_memory(
246        &self,
247        store_id: MemoryStoreId,
248        content: String,
249        content_parts: Vec<MemoryContentPart>,
250        kind: MemoryKind,
251        importance: u8,
252        tags: Vec<String>,
253    ) -> crate::error::Result<Memory>;
254
255    /// Search/recall memories.
256    async fn recall(&self, query: MemoryQuery) -> crate::error::Result<(Vec<Memory>, usize)>;
257
258    /// Soft-delete (deactivate) a memory.
259    ///
260    /// `store_id` is required so implementations can enforce org-scoped
261    /// ownership: the memory must belong to the given store.
262    async fn forget(
263        &self,
264        store_id: MemoryStoreId,
265        memory_id: MemoryId,
266    ) -> crate::error::Result<bool>;
267
268    /// Count active memories in a store.
269    async fn count_active(&self, store_id: MemoryStoreId) -> crate::error::Result<usize>;
270}