Skip to main content

claw10_memory/
lib.rs

1#![allow(clippy::pedantic)]
2
3pub mod admission;
4
5use std::sync::Arc;
6
7use chrono::Utc;
8
9use claw10_domain::{
10    AgentId, EvidenceId, Memory, MemoryId, MemorySource, MemoryStatus, MemoryType, TaskId,
11};
12use claw10_store::{Store, StoreError, StoreExt};
13
14pub use admission::{AdmissionConfig, AdmissionPipeline, AdmissionResult};
15
16#[derive(Debug, thiserror::Error)]
17pub enum MemoryError {
18    #[error("memory not found: {0}")]
19    NotFound(String),
20    #[error("invalid memory status transition: {from} → {to}")]
21    InvalidTransition {
22        from: MemoryStatus,
23        to: MemoryStatus,
24    },
25    #[error("{0}")]
26    Other(String),
27}
28
29impl From<StoreError> for MemoryError {
30    fn from(e: StoreError) -> Self {
31        Self::Other(e.to_string())
32    }
33}
34
35/// Input for storing a new memory.
36pub struct StoreMemoryInput {
37    pub tenant_id: String,
38    pub scope: String,
39    pub memory_type: MemoryType,
40    pub content: String,
41    pub source_agent: AgentId,
42    pub source_task: TaskId,
43    pub evidence_id: Option<EvidenceId>,
44    pub confidence: f64,
45    pub classification: String,
46}
47
48const KEY_PREFIX: &str = "memory:";
49
50pub struct MemoryService {
51    store: Arc<dyn Store>,
52    admission: AdmissionPipeline,
53}
54
55impl MemoryService {
56    #[must_use]
57    pub fn new(store: Arc<dyn Store>) -> Self {
58        Self::with_config(store, AdmissionConfig::default())
59    }
60
61    #[must_use]
62    pub fn with_config(store: Arc<dyn Store>, config: AdmissionConfig) -> Self {
63        Self {
64            store,
65            admission: AdmissionPipeline::new(config),
66        }
67    }
68
69    /// Store a new memory.
70    ///
71    /// Runs the admission pipeline before persisting:
72    /// - memories that pass all checks are stored as `Active`
73    /// - memories that fail are stored as `Rejected`
74    pub async fn store(&self, input: StoreMemoryInput) -> Memory {
75        let now = Utc::now();
76        let mut memory = Memory {
77            id: MemoryId(uuid::Uuid::now_v7()),
78            tenant_id: input.tenant_id,
79            scope: input.scope,
80            memory_type: input.memory_type,
81            content: input.content,
82            source: MemorySource {
83                agent_id: input.source_agent,
84                task_id: input.source_task,
85                evidence_id: input.evidence_id,
86            },
87            confidence: input.confidence,
88            classification: input.classification,
89            status: MemoryStatus::Candidate,
90            verified_by: vec![],
91            created_at: now,
92            updated_at: now,
93        };
94
95        // Run admission pipeline against existing active memories for deduplication.
96        let existing = self
97            .query(MemoryQuery {
98                status: Some(MemoryStatus::Active),
99                ..Default::default()
100            })
101            .await
102            .unwrap_or_default();
103
104        memory.status = match self.admission.evaluate(&memory, &existing) {
105            AdmissionResult::Activated => MemoryStatus::Active,
106            AdmissionResult::Rejected { reason } => {
107                tracing::debug!("memory {} rejected: {}", memory.id.0, reason);
108                MemoryStatus::Rejected
109            }
110        };
111        memory.updated_at = Utc::now();
112
113        let key = format!("{KEY_PREFIX}{}", memory.id.0);
114        self.store
115            .set(&key, &memory)
116            .await
117            .expect("MemoryService::store: store set failed");
118        memory
119    }
120
121    /// Retrieve a memory by ID.
122    pub async fn get(&self, memory_id: &MemoryId) -> Result<Option<Memory>, MemoryError> {
123        let key = format!("{KEY_PREFIX}{}", memory_id.0);
124        Ok(self.store.get::<Memory>(&key).await?)
125    }
126
127    /// Update memory content.
128    ///
129    /// # Errors
130    /// Returns `MemoryError::NotFound` if the memory does not exist.
131    pub async fn update_content(
132        &self,
133        memory_id: &MemoryId,
134        new_content: String,
135    ) -> Result<(), MemoryError> {
136        let key = format!("{KEY_PREFIX}{}", memory_id.0);
137        let mut memory = self
138            .store
139            .get::<Memory>(&key)
140            .await?
141            .ok_or_else(|| MemoryError::NotFound(memory_id.0.to_string()))?;
142
143        memory.content = new_content;
144        memory.updated_at = Utc::now();
145
146        self.store.set(&key, &memory).await?;
147        Ok(())
148    }
149
150    /// Transition memory status.
151    ///
152    /// Valid transitions:
153    /// - Candidate → {Scanning, Rejected}
154    /// - Scanning → {Verified, Rejected}
155    /// - Verified → Active
156    /// - Active → {Expired, Rejected}
157    ///
158    /// # Errors
159    /// Returns `MemoryError::InvalidTransition` if the transition is not allowed.
160    pub async fn transition_status(
161        &self,
162        memory_id: &MemoryId,
163        new_status: MemoryStatus,
164    ) -> Result<(), MemoryError> {
165        let key = format!("{KEY_PREFIX}{}", memory_id.0);
166        let mut memory = self
167            .store
168            .get::<Memory>(&key)
169            .await?
170            .ok_or_else(|| MemoryError::NotFound(memory_id.0.to_string()))?;
171
172        if !Self::is_valid_transition(&memory.status, &new_status) {
173            return Err(MemoryError::InvalidTransition {
174                from: memory.status.clone(),
175                to: new_status,
176            });
177        }
178
179        memory.status = new_status;
180        memory.updated_at = Utc::now();
181
182        self.store.set(&key, &memory).await?;
183        Ok(())
184    }
185
186    /// Check if a status transition is valid.
187    #[must_use]
188    pub fn is_valid_transition(from: &MemoryStatus, to: &MemoryStatus) -> bool {
189        matches!(
190            (from, to),
191            (MemoryStatus::Candidate, MemoryStatus::Scanning)
192                | (MemoryStatus::Candidate, MemoryStatus::Rejected)
193                | (MemoryStatus::Scanning, MemoryStatus::Verified)
194                | (MemoryStatus::Scanning, MemoryStatus::Rejected)
195                | (MemoryStatus::Verified, MemoryStatus::Active)
196                | (MemoryStatus::Active, MemoryStatus::Expired)
197                | (MemoryStatus::Active, MemoryStatus::Rejected)
198        )
199    }
200
201    /// Verify a memory by an agent.
202    ///
203    /// # Errors
204    /// Returns `MemoryError::NotFound` if the memory does not exist.
205    pub async fn verify(
206        &self,
207        memory_id: &MemoryId,
208        verifier: AgentId,
209    ) -> Result<(), MemoryError> {
210        let key = format!("{KEY_PREFIX}{}", memory_id.0);
211        let mut memory = self
212            .store
213            .get::<Memory>(&key)
214            .await?
215            .ok_or_else(|| MemoryError::NotFound(memory_id.0.to_string()))?;
216
217        if !memory.verified_by.contains(&verifier) {
218            memory.verified_by.push(verifier);
219        }
220        memory.updated_at = Utc::now();
221
222        self.store.set(&key, &memory).await?;
223        Ok(())
224    }
225
226    /// Query memories by filter.
227    pub async fn query(&self, filter: MemoryQuery) -> Result<Vec<Memory>, MemoryError> {
228        let all: Vec<(String, Memory)> = self.store.scan_prefix(KEY_PREFIX).await?;
229        Ok(all
230            .into_iter()
231            .map(|(_, m)| m)
232            .filter(|m| {
233                if let Some(ref tenant) = filter.tenant_id && m.tenant_id != *tenant {
234                    return false;
235                }
236                if let Some(ref scope) = filter.scope && m.scope != *scope {
237                    return false;
238                }
239                if let Some(ref memory_type) = filter.memory_type && m.memory_type != *memory_type {
240                    return false;
241                }
242                if let Some(ref status) = filter.status && m.status != *status {
243                    return false;
244                }
245                if let Some(ref agent) = filter.source_agent && m.source.agent_id != *agent {
246                    return false;
247                }
248                if let Some(min_confidence) = filter.min_confidence && m.confidence < min_confidence
249                {
250                    return false;
251                }
252                true
253            })
254            .collect())
255    }
256
257    /// Delete a memory.
258    ///
259    /// # Errors
260    /// Returns `MemoryError::NotFound` if the memory does not exist.
261    pub async fn delete(&self, memory_id: &MemoryId) -> Result<(), MemoryError> {
262        let key = format!("{KEY_PREFIX}{}", memory_id.0);
263        if !self.store.exists(&key).await? {
264            return Err(MemoryError::NotFound(memory_id.0.to_string()));
265        }
266        self.store.delete(&key).await?;
267        Ok(())
268    }
269
270    /// Count memories by status.
271    pub async fn count_by_status(&self) -> Result<std::collections::HashMap<String, usize>, MemoryError> {
272        let all: Vec<(String, Memory)> = self.store.scan_prefix(KEY_PREFIX).await?;
273        let mut counts = std::collections::HashMap::new();
274        for (_, memory) in all {
275            *counts.entry(format!("{:?}", memory.status)).or_insert(0) += 1;
276        }
277        Ok(counts)
278    }
279}
280
281/// Filter for querying memories.
282#[derive(Debug, Default)]
283pub struct MemoryQuery {
284    pub tenant_id: Option<String>,
285    pub scope: Option<String>,
286    pub memory_type: Option<MemoryType>,
287    pub status: Option<MemoryStatus>,
288    pub source_agent: Option<AgentId>,
289    pub min_confidence: Option<f64>,
290}