macro_rules! check {
($cond:expr, $($arg:tt)+) => {
if !($cond) {
return Err(anyhow::Error::new($crate::core::memory_api::MemoryError::Invalid(
format!($($arg)+),
)));
}
};
}
macro_rules! invalid {
($($arg:tt)+) => {
return Err(anyhow::Error::new($crate::core::memory_api::MemoryError::Invalid(
format!($($arg)+),
)))
};
}
#[cfg(feature = "conversation-memory")]
mod conversation;
mod index;
mod json;
mod maintenance;
mod policy;
mod recall;
mod stamp;
mod types;
mod write;
use crate::core::sync::{Mutex, RwLock};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::core::HmsCore;
use index::Index;
#[cfg(feature = "conversation-memory")]
pub use conversation::*;
pub use json::OPERATIONS;
pub use policy::*;
pub use types::*;
#[derive(Debug, thiserror::Error)]
pub enum MemoryError {
#[error("{0}")]
Invalid(String),
#[error("{0}")]
NotFound(String),
#[error("{0}")]
Conflict(String),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum IdConflict {
#[default]
Reject,
Replace,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", default, deny_unknown_fields)]
pub struct MemoryConfig {
pub default_k: u32,
pub default_history: HistoryMode,
pub supersession: SupersessionSpec,
pub recency: RecencySpec,
pub historical_weight: f64,
pub forgetting: ForgettingSpec,
pub consolidation: ConsolidationSpec,
pub on_id_conflict: IdConflict,
}
impl Default for MemoryConfig {
fn default() -> Self {
Self {
default_k: 10,
default_history: HistoryMode::Current,
supersession: SupersessionSpec::Explicit,
recency: RecencySpec::None,
historical_weight: 1.0,
forgetting: ForgettingSpec::KeepAll,
consolidation: ConsolidationSpec::None,
on_id_conflict: IdConflict::Reject,
}
}
}
impl MemoryConfig {
pub fn validate(&self) -> Result<()> {
check!(
(1..=1000).contains(&self.default_k),
"defaultK must be 1..=1000"
);
check!(
self.historical_weight.is_finite() && self.historical_weight >= 0.0,
"historicalWeight must be finite and nonnegative"
);
if let RecencySpec::Exponential {
half_life_ms,
floor,
..
} = self.recency
{
check!(half_life_ms > 0, "recency halfLifeMs must be positive");
check!(
(0.0..=1.0).contains(&floor),
"recency floor must be in [0, 1]"
);
}
if let ForgettingSpec::MaxFacts { limit } = self.forgetting {
check!(limit > 0, "forgetting limit must be positive");
}
Ok(())
}
}
pub struct Memory {
core: Arc<HmsCore>,
config: MemoryConfig,
supersession: Arc<dyn SupersessionPolicy>,
supersession_active: bool,
recency: Arc<dyn RecencyPolicy>,
forgetting: Arc<dyn ForgettingPolicy>,
consolidation: Arc<dyn ConsolidationPolicy>,
entities: Option<Arc<dyn EntityExtractor>>,
observer: Option<Arc<dyn MemoryObserver>>,
clock: Arc<dyn Clock>,
index: RwLock<Index>,
writer: Mutex<()>,
}
impl Memory {
pub fn new(core: Arc<HmsCore>) -> Self {
Self::with_config(core, MemoryConfig::default()).expect("the default config is valid")
}
pub fn with_config(core: Arc<HmsCore>, config: MemoryConfig) -> Result<Self> {
config.validate()?;
let supersession_active = config.supersession != SupersessionSpec::Explicit;
let supersession: Arc<dyn SupersessionPolicy> = match config.supersession {
SupersessionSpec::Explicit => Arc::new(ExplicitOnly),
SupersessionSpec::LatestBySubject => Arc::new(LatestBySubject),
};
let recency: Arc<dyn RecencyPolicy> = match config.recency {
RecencySpec::None => Arc::new(NoRecency),
RecencySpec::Exponential {
half_life_ms,
basis,
floor,
} => Arc::new(ExponentialRecency {
half_life_ms,
basis,
floor,
}),
};
let forgetting: Arc<dyn ForgettingPolicy> = match config.forgetting {
ForgettingSpec::KeepAll => Arc::new(KeepAll),
ForgettingSpec::ExpiredFor { grace_ms } => Arc::new(ExpiredFor { grace_ms }),
ForgettingSpec::SupersededFor { age_ms } => Arc::new(SupersededFor { age_ms }),
ForgettingSpec::MaxFacts { limit } => Arc::new(MaxFacts { limit }),
};
let consolidation: Arc<dyn ConsolidationPolicy> = match config.consolidation {
ConsolidationSpec::None => Arc::new(NoConsolidation),
ConsolidationSpec::CollapseDuplicates => Arc::new(CollapseDuplicates),
};
Ok(Self {
core,
config,
supersession,
supersession_active,
recency,
forgetting,
consolidation,
entities: None,
observer: None,
clock: Arc::new(SystemClock),
index: RwLock::new(Index::default()),
writer: Mutex::new(()),
})
}
pub fn with_supersession(mut self, policy: Arc<dyn SupersessionPolicy>) -> Self {
self.supersession = policy;
self.supersession_active = true;
self
}
pub fn with_recency(mut self, policy: Arc<dyn RecencyPolicy>) -> Self {
self.recency = policy;
self
}
pub fn with_forgetting(mut self, policy: Arc<dyn ForgettingPolicy>) -> Self {
self.forgetting = policy;
self
}
pub fn with_consolidation(mut self, policy: Arc<dyn ConsolidationPolicy>) -> Self {
self.consolidation = policy;
self
}
pub fn with_entity_extractor(mut self, extractor: Arc<dyn EntityExtractor>) -> Self {
self.entities = Some(extractor);
self
}
pub fn with_observer(mut self, observer: Arc<dyn MemoryObserver>) -> Self {
self.observer = Some(observer);
self
}
pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Self {
self.clock = clock;
self
}
pub fn config(&self) -> &MemoryConfig {
&self.config
}
pub fn core(&self) -> &Arc<HmsCore> {
&self.core
}
fn ensure_fresh(&self) {
let revision = self.core.revision();
if self.index.read().revision == Some(revision) {
return;
}
let mut index = self.index.write();
if index.revision == Some(self.core.revision()) {
return;
}
*index = Index::rebuild(&self.core);
}
fn settle(&self, before: u64, commits: u64, apply: impl FnOnce(&mut Index) -> bool) {
let after = self.core.revision();
let mut index = self.index.write();
index.revision =
(index.revision == Some(before) && after == before + commits && apply(&mut index))
.then_some(after);
}
}
#[cfg(test)]
mod tests;