use std::path::Path;
use std::sync::Arc;
use hirn_core::embed::{Embedder, EntityExtractor, ExtractedEntity};
use hirn_core::episodic::{EntityRef, EpisodicRecord};
use hirn_core::timestamp::Timestamp;
use hirn_core::types::{AgentId, EventType, Namespace};
use hirn_core::{HirnConfig, HirnError, HirnResult};
use hirn_engine::HirnDB;
use hirn_engine::ProviderRegistry;
use hirn_engine::activation::ActivationMode;
use hirn_engine::ql::QueryResult;
use hirn_engine::ql::context::{ContextConfig, ContextFormat, ThinkResult};
use hirn_engine::recall::{LayerFilter, RecallResult};
use hirn_engine::scoring::ScoringWeights;
use hirn_storage::{HirnDb, HirnDbConfig};
pub const MAX_TOKEN_BUDGET: usize = 128_000;
pub struct HirnMemory {
db: HirnDB,
entity_extractor: Arc<dyn EntityExtractor>,
agent_id: AgentId,
}
impl HirnMemory {
pub async fn open(path: impl AsRef<Path>) -> HirnResult<Self> {
let path = path.as_ref();
let mut config = HirnConfig::builder().db_path(path).build()?;
config.admission_enabled = true;
let brain_dir = path.parent().unwrap_or(path);
let toml_path = brain_dir.join("hirn.toml");
if toml_path.is_file() {
config = load_toml_over_defaults(&toml_path, config)?;
}
Self::open_with_config(config).await
}
pub async fn open_with_config(mut config: HirnConfig) -> HirnResult<Self> {
let registry = if config.allow_pseudo_embedder_fallback {
ProviderRegistry::from_env()
} else {
ProviderRegistry::from_env_strict()
};
let embedder: Arc<dyn Embedder> = registry
.embedder()
.ok_or_else(|| HirnError::InvalidConfig {
field: "allow_pseudo_embedder_fallback".to_string(),
value: config.allow_pseudo_embedder_fallback.to_string(),
reason: "HirnMemory requires a configured embedder from environment or explicit allow_pseudo_embedder_fallback = true for dev/test mode".to_string(),
})?;
config.embedding_dimensions = hirn_core::EmbeddingDimension::new(
u32::try_from(embedder.dimensions()).map_err(|_| HirnError::InvalidConfig {
field: "embedding_dimensions".to_string(),
value: embedder.dimensions().to_string(),
reason: "embedder reported dimension exceeds u32::MAX".to_string(),
})?,
)?;
let lance_path = config
.db_path
.parent()
.unwrap_or(config.db_path.as_path())
.join("lance");
let storage_config = HirnDbConfig::local(lance_path.to_string_lossy());
let hirn_storage = HirnDb::open(storage_config)
.await
.map_err(|e| HirnError::storage(e.to_string()))?;
let storage: Arc<dyn hirn_storage::PhysicalStore> = hirn_storage.store_arc();
let mut db = HirnDB::open_with_config(config, storage).await?;
db.set_embedder(embedder);
if let Some(tokenizer) = registry.tokenizer() {
db.set_tokenizer(tokenizer);
}
db.setup_default_admission_pipeline();
let agent_id = AgentId::new("hirn_memory")?;
db.register_agent(&agent_id, "HirnMemory default agent")
.await?;
let entity_extractor: Arc<dyn EntityExtractor> =
Arc::new(hirn_provider::RegexEntityExtractor::new());
Ok(Self {
db,
entity_extractor,
agent_id,
})
}
pub async fn remember(&self, text: &str) -> HirnResult<crate::MemoryId> {
let embedding = self.db.embed_text(text).await?;
let entities = self
.entity_extractor
.extract_entities(text, &[])
.await
.unwrap_or_default();
let entity_refs = extracted_to_refs(&entities);
let record = EpisodicRecord::builder()
.content(text)
.embedding(embedding)
.event_type(EventType::Observation)
.agent_id(self.agent_id.clone())
.entities(entity_refs)
.build()?;
self.db.episodic().remember(record).await
}
pub async fn think(&self, query: &str, budget: usize) -> HirnResult<ThinkResult> {
if budget > MAX_TOKEN_BUDGET {
return Err(HirnError::InvalidInput(format!(
"token budget {budget} exceeds the maximum allowed value of {MAX_TOKEN_BUDGET}"
)));
}
let embedding = self.db.embed_text(query).await?;
self.db
.recall_view()
.think(embedding)
.budget(budget)
.execute()
.await
}
pub async fn recall(&self, query: &str, limit: usize) -> HirnResult<Vec<RecallResult>> {
let embedding = self.db.embed_text(query).await?;
self.db
.recall_view()
.query(embedding)
.limit(limit)
.execute()
.await
}
pub async fn query(&self, hirnql: &str) -> HirnResult<QueryResult> {
self.db.ql().execute(hirnql).await
}
#[must_use]
pub fn db(&self) -> &HirnDB {
&self.db
}
pub fn db_mut(&mut self) -> &mut HirnDB {
&mut self.db
}
pub fn recall_builder(&self, about: &str) -> MemoryRecallBuilder<'_> {
MemoryRecallBuilder::new(self, about.to_owned())
}
pub fn think_builder(&self, about: &str) -> MemoryThinkBuilder<'_> {
MemoryThinkBuilder::new(self, about.to_owned())
}
}
#[must_use]
pub struct MemoryRecallBuilder<'a> {
memory: &'a HirnMemory,
about: String,
limit: usize,
threshold: Option<f32>,
layer_filter: LayerFilter,
namespace: Option<Namespace>,
after: Option<Timestamp>,
before: Option<Timestamp>,
weights: Option<ScoringWeights>,
activation_mode: ActivationMode,
activation_depth: Option<usize>,
hybrid: bool,
agent_id: Option<String>,
}
impl<'a> MemoryRecallBuilder<'a> {
fn new(memory: &'a HirnMemory, about: String) -> Self {
Self {
memory,
about,
limit: 10,
threshold: None,
layer_filter: LayerFilter::default(),
namespace: None,
after: None,
before: None,
weights: None,
activation_mode: ActivationMode::None,
activation_depth: None,
hybrid: false,
agent_id: None,
}
}
pub fn limit(mut self, k: usize) -> Self {
self.limit = k;
self
}
pub fn threshold(mut self, min: f32) -> Self {
self.threshold = Some(min);
self
}
pub fn episodic_only(mut self) -> Self {
self.layer_filter = LayerFilter::EpisodicOnly;
self
}
pub fn semantic_only(mut self) -> Self {
self.layer_filter = LayerFilter::SemanticOnly;
self
}
pub fn procedural_only(mut self) -> Self {
self.layer_filter = LayerFilter::ProceduralOnly;
self
}
pub fn namespace(mut self, ns: Namespace) -> Self {
self.namespace = Some(ns);
self
}
pub fn after(mut self, ts: Timestamp) -> Self {
self.after = Some(ts);
self
}
pub fn before(mut self, ts: Timestamp) -> Self {
self.before = Some(ts);
self
}
pub fn weights(mut self, w: ScoringWeights) -> Self {
self.weights = Some(w);
self
}
pub fn activation(mut self, mode: ActivationMode) -> Self {
self.activation_mode = mode;
self
}
pub fn depth(mut self, d: usize) -> Self {
self.activation_depth = Some(d);
self
}
pub fn hybrid(mut self, enable: bool) -> Self {
self.hybrid = enable;
self
}
pub fn agent_id(mut self, id: impl Into<String>) -> Self {
self.agent_id = Some(id.into());
self
}
pub fn between(mut self, start: Timestamp, end: Timestamp) -> Self {
self.after = Some(start);
self.before = Some(end);
self
}
pub async fn execute(self) -> HirnResult<Vec<RecallResult>> {
let embedding = self.memory.db.embed_text(&self.about).await?;
let mut builder = self
.memory
.db
.recall_view()
.query(embedding)
.limit(self.limit)
.activation(self.activation_mode)
.query_text(self.about);
if let Some(t) = self.threshold {
builder = builder.threshold(t);
}
if let Some(ns) = self.namespace {
builder = builder.namespace(ns);
}
if let Some(ts) = self.after {
builder = builder.after(ts);
}
if let Some(ts) = self.before {
builder = builder.before(ts);
}
if let Some(w) = self.weights {
builder = builder.weights(w);
}
if let Some(d) = self.activation_depth {
builder = builder.depth(d);
}
if self.hybrid {
builder = builder.hybrid(true);
}
if let Some(id) = self.agent_id {
builder = builder.agent_id(id);
}
match self.layer_filter {
LayerFilter::EpisodicOnly => builder = builder.episodic_only(),
LayerFilter::SemanticOnly => builder = builder.semantic_only(),
LayerFilter::ProceduralOnly => builder = builder.procedural_only(),
LayerFilter::All => {}
}
builder.execute().await
}
}
#[must_use]
pub struct MemoryThinkBuilder<'a> {
memory: &'a HirnMemory,
about: String,
budget: Option<usize>,
limit: usize,
layer_filter: LayerFilter,
namespace: Option<Namespace>,
after: Option<Timestamp>,
before: Option<Timestamp>,
weights: Option<ScoringWeights>,
activation_mode: ActivationMode,
activation_depth: Option<usize>,
format: Option<ContextFormat>,
context_config: Option<ContextConfig>,
}
impl<'a> MemoryThinkBuilder<'a> {
fn new(memory: &'a HirnMemory, about: String) -> Self {
Self {
memory,
about,
budget: None,
limit: 50,
layer_filter: LayerFilter::default(),
namespace: None,
after: None,
before: None,
weights: None,
activation_mode: ActivationMode::None,
activation_depth: None,
format: None,
context_config: None,
}
}
pub fn budget(mut self, tokens: usize) -> Self {
self.budget = Some(tokens);
self
}
pub fn limit(mut self, k: usize) -> Self {
self.limit = k;
self
}
pub fn episodic_only(mut self) -> Self {
self.layer_filter = LayerFilter::EpisodicOnly;
self
}
pub fn semantic_only(mut self) -> Self {
self.layer_filter = LayerFilter::SemanticOnly;
self
}
pub fn namespace(mut self, ns: Namespace) -> Self {
self.namespace = Some(ns);
self
}
pub fn after(mut self, ts: Timestamp) -> Self {
self.after = Some(ts);
self
}
pub fn before(mut self, ts: Timestamp) -> Self {
self.before = Some(ts);
self
}
pub fn activation(mut self, mode: ActivationMode) -> Self {
self.activation_mode = mode;
self
}
pub fn depth(mut self, d: usize) -> Self {
self.activation_depth = Some(d);
self
}
pub fn weights(mut self, w: ScoringWeights) -> Self {
self.weights = Some(w);
self
}
pub fn format(mut self, fmt: ContextFormat) -> Self {
self.format = Some(fmt);
self
}
pub fn context_config(mut self, config: ContextConfig) -> Self {
self.context_config = Some(config);
self
}
pub fn between(mut self, start: Timestamp, end: Timestamp) -> Self {
self.after = Some(start);
self.before = Some(end);
self
}
pub async fn execute(self) -> HirnResult<ThinkResult> {
let embedding = self.memory.db.embed_text(&self.about).await?;
let mut builder = self
.memory
.db
.recall_view()
.think(embedding)
.limit(self.limit)
.activation(self.activation_mode);
if let Some(b) = self.budget {
builder = builder.budget(b);
}
if let Some(ns) = self.namespace {
builder = builder.namespace(ns);
}
if let Some(ts) = self.after {
builder = builder.after(ts);
}
if let Some(ts) = self.before {
builder = builder.before(ts);
}
if let Some(d) = self.activation_depth {
builder = builder.depth(d);
}
if let Some(w) = self.weights {
builder = builder.weights(w);
}
if let Some(fmt) = self.format {
builder = builder.format(fmt);
}
if let Some(cfg) = self.context_config {
builder = builder.context_config(cfg);
}
match self.layer_filter {
LayerFilter::EpisodicOnly => builder = builder.episodic_only(),
LayerFilter::SemanticOnly => builder = builder.semantic_only(),
LayerFilter::ProceduralOnly | LayerFilter::All => {}
}
builder.execute().await
}
}
fn extracted_to_refs(entities: &[ExtractedEntity]) -> Vec<EntityRef> {
entities
.iter()
.map(|e| EntityRef {
name: e.name.clone(),
role: e.entity_type.clone(),
entity_id: None,
})
.collect()
}
fn load_toml_over_defaults(toml_path: &Path, base: HirnConfig) -> HirnResult<HirnConfig> {
let content = std::fs::read_to_string(toml_path).map_err(|e| {
HirnError::InvalidInput(format!("failed to read {}: {e}", toml_path.display()))
})?;
let mut base_table: toml::Table = toml::from_str(
&toml::to_string(&base)
.map_err(|e| HirnError::InvalidInput(format!("config serialization error: {e}")))?,
)
.map_err(|e| HirnError::InvalidInput(format!("config round-trip error: {e}")))?;
let file_table: toml::Table = toml::from_str(&content).map_err(|e| {
HirnError::InvalidInput(format!("invalid hirn.toml at {}: {e}", toml_path.display()))
})?;
for (key, value) in file_table {
base_table.insert(key, value);
}
let merged: HirnConfig = base_table.try_into().map_err(|e: toml::de::Error| {
HirnError::InvalidInput(format!("invalid hirn.toml config: {e}"))
})?;
Ok(merged)
}