use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum MemoryKind {
#[default]
Episodic,
Semantic,
Working,
Reflection,
ToolCall,
Procedural,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Visibility {
#[default]
Shared,
Private,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryRecord {
pub id: Uuid,
pub kind: MemoryKind,
pub text: String,
pub created_at: i64,
pub importance: f32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<i64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sources: Vec<Uuid>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entities: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default)]
pub visibility: Visibility,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub embedding: Option<Vec<f32>>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct RememberRequest {
pub text: String,
#[serde(default)]
pub kind: MemoryKind,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub agent_id: Option<String>,
#[serde(default)]
pub metadata: Option<serde_json::Value>,
#[serde(default)]
pub importance: Option<f32>,
#[serde(default)]
pub ttl_seconds: Option<u64>,
#[serde(default)]
pub visibility: Option<Visibility>,
#[serde(default)]
pub embedding: Option<Vec<f32>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum RecallStrategy {
#[default]
Balanced,
Semantic,
Lexical,
Recent,
Relational,
}
impl RecallStrategy {
pub fn weights(self) -> (f32, f32, f32, f32) {
match self {
RecallStrategy::Balanced => (1.0, 1.0, 0.7, 0.3),
RecallStrategy::Semantic => (1.0, 0.3, 0.5, 0.1),
RecallStrategy::Lexical => (0.3, 1.0, 0.5, 0.1),
RecallStrategy::Recent => (0.5, 0.5, 0.3, 1.2),
RecallStrategy::Relational => (0.4, 0.4, 1.2, 0.2),
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct MemoryFilter {
#[serde(default)]
pub kinds: Vec<MemoryKind>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub agent_id: Option<String>,
#[serde(default)]
pub since: Option<i64>,
#[serde(default)]
pub until: Option<i64>,
}
impl MemoryFilter {
pub fn is_empty(&self) -> bool {
self.kinds.is_empty()
&& self.tags.is_empty()
&& self.session_id.is_none()
&& self.agent_id.is_none()
&& self.since.is_none()
&& self.until.is_none()
}
pub fn matches(&self, rec: &MemoryRecord) -> bool {
if !self.kinds.is_empty() && !self.kinds.contains(&rec.kind) {
return false;
}
if let Some(s) = &self.session_id {
if rec.session_id.as_deref() != Some(s.as_str()) {
return false;
}
}
if let Some(a) = &self.agent_id {
if rec.agent_id.as_deref() != Some(a.as_str()) {
return false;
}
}
if !self.tags.is_empty() && !self.tags.iter().all(|t| rec.tags.contains(t)) {
return false;
}
if let Some(since) = self.since {
if rec.created_at < since {
return false;
}
}
if let Some(until) = self.until {
if rec.created_at > until {
return false;
}
}
true
}
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct RecallRequest {
pub query: String,
#[serde(default)]
pub top_k: Option<usize>,
#[serde(default)]
pub strategy: RecallStrategy,
#[serde(default)]
pub filter: MemoryFilter,
#[serde(default)]
pub query_embedding: Option<Vec<f32>>,
#[serde(default)]
pub rerank: Option<bool>,
#[serde(default)]
pub as_agent: Option<String>,
#[serde(default)]
pub ef_search: Option<usize>,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct RecallSignals {
pub vector: f32,
pub lexical: f32,
pub graph: f32,
pub recency: f32,
pub importance: f32,
pub rerank: f32,
}
#[derive(Debug, Clone, Serialize)]
pub struct RecallHit {
pub record: MemoryRecord,
pub score: f32,
pub signals: RecallSignals,
}
#[derive(Debug, Clone, Serialize)]
pub struct EngineStats {
pub total_memories: usize,
pub vector_indexed: usize,
pub lexical_indexed: usize,
pub embedding_dim: usize,
pub vector_dim: Option<usize>,
pub entities: usize,
pub quantized: bool,
pub wal_tail_ops: usize,
}
#[derive(Debug, Clone)]
pub struct LifecycleConfig {
pub working_ttl_secs: u64,
pub consolidate_after_secs: u64,
pub min_batch: usize,
pub max_batch: usize,
}
impl Default for LifecycleConfig {
fn default() -> Self {
Self {
working_ttl_secs: 24 * 3600,
consolidate_after_secs: 7 * 24 * 3600,
min_batch: 4,
max_batch: 12,
}
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct LifecycleReport {
pub expired_swept: usize,
pub batches_consolidated: usize,
pub summaries: Vec<Uuid>,
pub checkpointed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Snapshot {
pub created_at: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub records: Vec<MemoryRecord>,
}
pub fn now_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}