use std::path::Path;
use std::sync::Arc;
use crate::MemberAgentEventSink;
use crate::identity_first::agent_memory::{AgentMemoryConfig, AgentMemoryProvider};
use crate::memory::capabilities::{MemoryPanelStore, StewardStore};
use crate::memory::distiller::{
DistillerConfig, DistillerEngine, DistillerProfile, DistillerTriggers, FactoryDistillerHandle,
HnswDiscardSource, SessionStoreTranscriptSource,
};
use crate::memory::events::MemoryEventSink;
use crate::memory::sqlite_store::SqliteAgentMemoryStore;
use crate::memory::steward::{
FactoryStewardHandle, MemoryConflictBridge, MemoryGatingBridge, MobPurposeSource,
SessionStoreEvidenceResolver, StewardConfig, StewardEngine, StewardProfile, StewardTriggers,
};
use crate::memory::taint::{SessionTaintTracker, TaintLlmWriteGate};
#[derive(Debug, Clone, Default)]
pub struct MemoryEnginesConfig {
pub distiller: DistillerConfig,
pub steward: StewardConfig,
}
#[derive(Default)]
pub struct MemoryStackSeams {
pub persistent_state: Option<std::path::PathBuf>,
pub transcript_store: Option<Arc<dyn meerkat::SessionStore>>,
pub event_sink: Option<Arc<dyn MemoryEventSink>>,
pub mob_purpose: Option<Arc<dyn MobPurposeSource>>,
pub steward_gating: Option<Arc<dyn MemoryGatingBridge>>,
pub steward_conflicts: Option<Arc<dyn MemoryConflictBridge>>,
}
pub struct AgentMemoryStack {
pub provider: Arc<dyn AgentMemoryProvider>,
pub steward_store: Option<Arc<dyn StewardStore>>,
pub panel: Option<Arc<dyn MemoryPanelStore>>,
pub taint: SessionTaintTracker,
pub distiller: Option<Arc<DistillerEngine>>,
pub steward: Option<Arc<StewardEngine>>,
pub sinks: Vec<Arc<dyn MemberAgentEventSink>>,
}
pub fn build_sqlite_memory_stack(
memory_dir: &Path,
config: &AgentMemoryConfig,
engines: &MemoryEnginesConfig,
seams: MemoryStackSeams,
) -> Result<AgentMemoryStack, String> {
let store = SqliteAgentMemoryStore::open(memory_dir)
.map_err(|e| format!("failed to open agent memory store: {e}"))?;
attach_memory_engines(Arc::new(store), config, engines, seams)
}
pub fn attach_memory_engines(
provider: Arc<dyn AgentMemoryProvider>,
config: &AgentMemoryConfig,
engines: &MemoryEnginesConfig,
seams: MemoryStackSeams,
) -> Result<AgentMemoryStack, String> {
let MemoryStackSeams {
persistent_state,
transcript_store,
event_sink,
mob_purpose,
steward_gating,
steward_conflicts,
} = seams;
let persistent_state = persistent_state.as_deref();
let event_sink =
event_sink.ok_or_else(|| "agent memory stack requires an event sink".to_string())?;
let taintable = provider.as_taintable().ok_or_else(|| {
"agent memory stack requires a provider with firewall controls (TaintableStore)".to_string()
})?;
let taint = SessionTaintTracker::new(config.content_trust.clone());
taintable.set_llm_write_gate(Arc::new(TaintLlmWriteGate::new(
Some(taint.clone()),
config.llm_writes,
)));
taintable.set_event_sink(event_sink.clone());
taint.set_event_sink(event_sink.clone());
let mut sinks: Vec<Arc<dyn MemberAgentEventSink>> = vec![Arc::new(taint.clone())];
let realm = config.realm.clone();
let state_for_engines = |what: &str| -> Result<&Path, String> {
persistent_state.ok_or_else(|| format!("agent memory {what} requires persistent_state"))
};
let transcripts_for_engines = |what: &str| -> Result<Arc<dyn meerkat::SessionStore>, String> {
transcript_store
.clone()
.ok_or_else(|| format!("agent memory {what} requires a session transcript store"))
};
let distiller = if engines.distiller.enabled {
let state = state_for_engines("distiller")?;
let transcripts = transcripts_for_engines("distiller")?;
let tombstones = provider.as_tombstone_source().ok_or_else(|| {
"agent memory distiller requires a provider with tombstone reads (TombstoneSource)"
.to_string()
})?;
let mut profile = DistillerProfile::embedded_default();
if let Some(model) = engines.distiller.model.as_deref() {
profile = profile
.with_model_override(model)
.map_err(|e| format!("agent memory distiller: {e}"))?;
}
let handle =
FactoryDistillerHandle::new(state, meerkat::Config::default(), &realm, &profile);
let engine = Arc::new(DistillerEngine::new(
profile,
engines.distiller.clone(),
Arc::new(handle),
provider.clone(),
tombstones,
Arc::new(SessionStoreTranscriptSource::new(transcripts)),
Some(Arc::new(HnswDiscardSource::new(state.join("memory")))),
Some(taint.clone()),
realm.clone(),
));
engine.set_event_sink(event_sink.clone());
sinks.push(Arc::new(DistillerTriggers::new(engine.clone())));
Some(engine)
} else {
None
};
let steward_store = provider.as_steward_store();
let steward = if engines.steward.enabled {
let store = steward_store.clone().ok_or_else(|| {
"agent memory steward requires a provider with the steward surface (StewardStore)"
.to_string()
})?;
let state = state_for_engines("steward")?;
let transcripts = transcripts_for_engines("steward")?;
let mut profile = StewardProfile::embedded_default();
if let Some(model) = engines.steward.model.as_deref() {
profile = profile
.with_model_override(model)
.map_err(|e| format!("agent memory steward: {e}"))?;
}
let transcripts_source: Arc<dyn crate::memory::distiller::TranscriptSource> =
Arc::new(SessionStoreTranscriptSource::new(transcripts));
taintable.set_evidence_resolver(Arc::new(SessionStoreEvidenceResolver::new(
transcripts_source.clone(),
tokio::runtime::Handle::current(),
)));
let handle = FactoryStewardHandle::new(
state.to_path_buf(),
meerkat::Config::default(),
realm.clone(),
&profile,
);
let mut engine = StewardEngine::new(
profile,
engines.steward.clone(),
Arc::new(handle),
store,
transcripts_source,
realm,
)
.with_events(event_sink.clone())
.with_operator_routing(
config.operator_scope
== crate::identity_first::agent_memory::AgentMemoryOperatorScope::Provisional,
);
if let Some(purpose) = mob_purpose {
engine = engine.with_mob_context(purpose);
}
if let Some(gating) = steward_gating {
engine = engine.with_gating(gating);
}
if let Some(conflicts) = steward_conflicts {
engine = engine.with_conflicts(conflicts);
}
let engine = Arc::new(engine);
sinks.push(Arc::new(StewardTriggers::new(engine.clone())));
Some(engine)
} else {
None
};
Ok(AgentMemoryStack {
steward_store,
panel: provider.as_memory_panel_store(),
provider,
taint,
distiller,
steward,
sinks,
})
}