diff --git a/memoria/crates/memoria-api/src/models.rs b/memoria/crates/memoria-api/src/models.rs index c6b94da8..7a595039 100644 --- a/memoria/crates/memoria-api/src/models.rs +++ b/memoria/crates/memoria-api/src/models.rs @@ -429,7 +429,7 @@ pub enum PickSelector { // ── Helpers ─────────────────────────────────────────────────────────────────── pub fn parse_memory_type(s: &str) -> Result { - MemoryType::from_str(s).map_err(|e| e.to_string()) + Ok(s.parse::().unwrap()) } pub fn parse_trust_tier(s: &str) -> Result { diff --git a/memoria/crates/memoria-api/src/routes/governance.rs b/memoria/crates/memoria-api/src/routes/governance.rs index 4df63189..120fa766 100644 --- a/memoria/crates/memoria-api/src/routes/governance.rs +++ b/memoria/crates/memoria-api/src/routes/governance.rs @@ -240,8 +240,7 @@ pub async fn reflect( continue; } let mt_str = item["type"].as_str().unwrap_or("semantic"); - let mt = memoria_core::MemoryType::from_str(mt_str) - .unwrap_or(memoria_core::MemoryType::Semantic); + let mt = mt_str.parse::().unwrap(); let _ = state .service .store_memory( @@ -431,5 +430,3 @@ pub async fn get_entities( "entities": entities.iter().map(|(n, t)| json!({"name": n, "entity_type": t})).collect::>() }))) } - -use std::str::FromStr; diff --git a/memoria/crates/memoria-core/src/types.rs b/memoria/crates/memoria-core/src/types.rs index 95511d29..b3b0c9bf 100644 --- a/memoria/crates/memoria-core/src/types.rs +++ b/memoria/crates/memoria-core/src/types.rs @@ -2,9 +2,16 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; -/// Memory type — must have exactly 6 variants matching Python implementation. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] +/// Memory type classification. +/// +/// The built-in variants cover standard agent memory categories. +/// `Custom(String)` allows downstream applications to define their own +/// domain-specific types (e.g. `brand_theme`, `layout_catalog`) without +/// requiring changes to Memoria itself. +/// +/// Custom types are stored as-is in the database `memory_type VARCHAR(64)` +/// column and participate in retrieval filtering just like built-in types. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum MemoryType { Semantic, Working, @@ -12,6 +19,28 @@ pub enum MemoryType { Profile, ToolResult, Procedural, + /// Application-defined memory type. The inner string is stored verbatim. + Custom(String), +} + +impl MemoryType { + /// Returns `true` for the six built-in variants, `false` for `Custom`. + pub fn is_builtin(&self) -> bool { + !matches!(self, MemoryType::Custom(_)) + } +} + +impl Serialize for MemoryType { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for MemoryType { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Ok(s.parse().expect("MemoryType::from_str is infallible")) + } } impl MemoryType { @@ -41,23 +70,24 @@ impl std::fmt::Display for MemoryType { MemoryType::Profile => "profile", MemoryType::ToolResult => "tool_result", MemoryType::Procedural => "procedural", + MemoryType::Custom(name) => name.as_str(), }; write!(f, "{s}") } } impl std::str::FromStr for MemoryType { - type Err = crate::MemoriaError; + type Err = std::convert::Infallible; fn from_str(s: &str) -> Result { - match s { - "semantic" => Ok(MemoryType::Semantic), - "working" => Ok(MemoryType::Working), - "episodic" => Ok(MemoryType::Episodic), - "profile" => Ok(MemoryType::Profile), - "tool_result" => Ok(MemoryType::ToolResult), - "procedural" => Ok(MemoryType::Procedural), - other => Err(crate::MemoriaError::InvalidMemoryType(other.to_string())), - } + Ok(match s { + "semantic" => MemoryType::Semantic, + "working" => MemoryType::Working, + "episodic" => MemoryType::Episodic, + "profile" => MemoryType::Profile, + "tool_result" => MemoryType::ToolResult, + "procedural" => MemoryType::Procedural, + other => MemoryType::Custom(other.to_string()), + }) } } @@ -207,7 +237,7 @@ mod tests { } #[test] - fn test_memory_type_roundtrip() { + fn test_builtin_types_roundtrip() { for (s, expected) in [ ("semantic", MemoryType::Semantic), ("working", MemoryType::Working), @@ -219,9 +249,27 @@ mod tests { let parsed: MemoryType = s.parse().unwrap(); assert_eq!(parsed, expected); assert_eq!(parsed.to_string(), s); + assert!(parsed.is_builtin()); } } + #[test] + fn test_custom_type_roundtrip() { + let parsed: MemoryType = "brand_theme".parse().unwrap(); + assert_eq!(parsed, MemoryType::Custom("brand_theme".to_string())); + assert_eq!(parsed.to_string(), "brand_theme"); + assert!(!parsed.is_builtin()); + } + + #[test] + fn test_custom_type_serde_roundtrip() { + let mt = MemoryType::Custom("layout_catalog".to_string()); + let json = serde_json::to_string(&mt).unwrap(); + assert_eq!(json, "\"layout_catalog\""); + let back: MemoryType = serde_json::from_str(&json).unwrap(); + assert_eq!(back, mt); + } + #[test] fn test_trust_tier_roundtrip() { for (s, expected) in [ diff --git a/memoria/crates/memoria-mcp/src/tools.rs b/memoria/crates/memoria-mcp/src/tools.rs index b4d06e4d..745855b6 100644 --- a/memoria/crates/memoria-mcp/src/tools.rs +++ b/memoria/crates/memoria-mcp/src/tools.rs @@ -292,7 +292,7 @@ pub async fn call( .map(TrustTier::from_str) .transpose() .map_err(|e| anyhow::anyhow!("{e}"))?; - let mt = MemoryType::from_str(memory_type).unwrap_or(MemoryType::Semantic); + let mt = memory_type.parse::().unwrap(); let m = match service .store_memory( user_id, @@ -772,7 +772,7 @@ pub async fn call( continue; } let mt_str = item["type"].as_str().unwrap_or("semantic"); - let mt = MemoryType::from_str(mt_str).unwrap_or(MemoryType::Semantic); + let mt = mt_str.parse::().unwrap(); let confidence = item["confidence"].as_f64().unwrap_or(0.5) as f32; // Store as T4 (unverified insight from reflection) let _ = service diff --git a/memoria/crates/memoria-storage/src/store.rs b/memoria/crates/memoria-storage/src/store.rs index c805d0bc..e7e3afd6 100644 --- a/memoria/crates/memoria-storage/src/store.rs +++ b/memoria/crates/memoria-storage/src/store.rs @@ -1126,7 +1126,7 @@ impl SqlMemoryStore { memory_id VARCHAR(64) PRIMARY KEY, user_id VARCHAR(64) NOT NULL, author_id VARCHAR(64) DEFAULT NULL, - memory_type VARCHAR(20) NOT NULL, + memory_type VARCHAR(64) NOT NULL, content TEXT NOT NULL, embedding vecf32({dim}), session_id VARCHAR(64), @@ -5767,7 +5767,7 @@ fn row_to_memory_base(row: &sqlx::mysql::MySqlRow) -> Result, _>("author_id") .unwrap_or(None), - memory_type: MemoryType::from_str(&memory_type_str)?, + memory_type: memory_type_str.parse::().unwrap(), content: row.try_get("content").map_err(db_err)?, initial_confidence: row .try_get::("initial_confidence")