use std::sync::Arc;
use anyhow::{Result, bail};
use futures::future::BoxFuture;
use serde_json::Value;
pub trait MemorySlot: Send + Sync {
fn store<'a>(
&'a self,
scope: &'a str,
content: &'a str,
metadata: Value,
) -> BoxFuture<'a, Result<String>>;
fn recall<'a>(
&'a self,
scope: &'a str,
query: &'a str,
top_k: usize,
) -> BoxFuture<'a, Result<Vec<MemoryItem>>>;
fn forget<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<()>>;
}
pub trait ContextEngineSlot: Send + Sync {
fn prune<'a>(
&'a self,
messages: &'a mut Vec<Value>,
budget_tokens: u32,
) -> BoxFuture<'a, Result<()>>;
}
#[derive(Debug, Clone)]
pub struct MemoryItem {
pub id: String,
pub content: String,
pub score: f32,
pub metadata: Value,
}
#[derive(Default)]
pub struct SlotRegistry {
pub memory: Option<Arc<dyn MemorySlot>>,
pub context_engine: Option<Arc<dyn ContextEngineSlot>>,
}
impl SlotRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn set_memory(&mut self, plugin: Arc<dyn MemorySlot>, plugin_name: &str) -> Result<()> {
if self.memory.is_some() {
bail!("memory slot already occupied; cannot register plugin `{plugin_name}`");
}
self.memory = Some(plugin);
tracing::info!(plugin = plugin_name, "memory slot registered");
Ok(())
}
pub fn set_context_engine(
&mut self,
plugin: Arc<dyn ContextEngineSlot>,
plugin_name: &str,
) -> Result<()> {
if self.context_engine.is_some() {
bail!("context_engine slot already occupied; cannot register `{plugin_name}`");
}
self.context_engine = Some(plugin);
tracing::info!(plugin = plugin_name, "context_engine slot registered");
Ok(())
}
pub fn has_memory(&self) -> bool {
self.memory.is_some()
}
pub fn has_context_engine(&self) -> bool {
self.context_engine.is_some()
}
}
pub struct MemoryStoreSlot {
inner: Arc<tokio::sync::Mutex<crate::agent::memory::MemoryStore>>,
}
impl MemoryStoreSlot {
pub fn new(store: Arc<tokio::sync::Mutex<crate::agent::memory::MemoryStore>>) -> Self {
Self { inner: store }
}
}
impl MemorySlot for MemoryStoreSlot {
fn store<'a>(
&'a self,
scope: &'a str,
content: &'a str,
_metadata: Value,
) -> BoxFuture<'a, Result<String>> {
Box::pin(async move {
let id = uuid::Uuid::new_v4().to_string();
let doc = crate::agent::memory::MemoryDoc {
id: id.clone(),
scope: scope.to_owned(),
kind: "note".to_owned(),
text: content.to_owned(),
vector: vec![],
created_at: 0,
accessed_at: 0,
access_count: 0,
importance: 0.5,
tier: Default::default(),
abstract_text: None,
overview_text: None,
tags: vec![],
pinned: false,
};
self.inner.lock().await.add(doc).await?;
Ok(id)
})
}
fn recall<'a>(
&'a self,
scope: &'a str,
query: &'a str,
top_k: usize,
) -> BoxFuture<'a, Result<Vec<MemoryItem>>> {
Box::pin(async move {
let mut store = self.inner.lock().await;
let docs = store.search(query, Some(scope), top_k).await?;
Ok(docs
.into_iter()
.map(|d| MemoryItem {
id: d.id,
content: d.text,
score: 1.0,
metadata: Value::Null,
})
.collect())
})
}
fn forget<'a>(&'a self, id: &'a str) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { self.inner.lock().await.delete(id).await })
}
}
#[cfg(test)]
mod tests {
use super::*;
struct DummyMemory;
impl MemorySlot for DummyMemory {
fn store<'a>(
&'a self,
_scope: &'a str,
_content: &'a str,
_meta: Value,
) -> BoxFuture<'a, Result<String>> {
Box::pin(async move { Ok("id-1".to_owned()) })
}
fn recall<'a>(
&'a self,
_scope: &'a str,
_query: &'a str,
_k: usize,
) -> BoxFuture<'a, Result<Vec<MemoryItem>>> {
Box::pin(async move { Ok(vec![]) })
}
fn forget<'a>(&'a self, _id: &'a str) -> BoxFuture<'a, Result<()>> {
Box::pin(async move { Ok(()) })
}
}
#[test]
fn register_memory_slot() {
let mut reg = SlotRegistry::new();
assert!(!reg.has_memory());
reg.set_memory(Arc::new(DummyMemory), "dummy")
.expect("register");
assert!(reg.has_memory());
}
#[test]
fn double_register_memory_slot_fails() {
let mut reg = SlotRegistry::new();
reg.set_memory(Arc::new(DummyMemory), "first")
.expect("first");
assert!(reg.set_memory(Arc::new(DummyMemory), "second").is_err());
}
}