use async_trait::async_trait;
use klieo_core::error::MemoryError;
use klieo_core::ids::RunId;
use klieo_core::memory::{Episode, EpisodicMemory, RunFilter, RunSummary};
use std::sync::Arc;
use tokio::sync::broadcast;
const CHANNEL_CAPACITY: usize = 256;
#[non_exhaustive]
pub struct ProjectableEpisodic {
inner: Arc<dyn EpisodicMemory>,
tx: broadcast::Sender<(RunId, Episode)>,
}
impl ProjectableEpisodic {
pub fn new(inner: Arc<dyn EpisodicMemory>) -> (Self, broadcast::Receiver<(RunId, Episode)>) {
let (tx, rx) = broadcast::channel(CHANNEL_CAPACITY);
(Self { inner, tx }, rx)
}
pub fn subscribe(&self) -> broadcast::Receiver<(RunId, Episode)> {
self.tx.subscribe()
}
}
#[async_trait]
impl EpisodicMemory for ProjectableEpisodic {
async fn record(&self, run: RunId, event: Episode) -> Result<(), MemoryError> {
self.inner.record(run, event.clone()).await?;
let _ = self.tx.send((run, event));
Ok(())
}
async fn replay(&self, run: RunId) -> Result<Vec<Episode>, MemoryError> {
self.inner.replay(run).await
}
async fn list_runs(&self, filter: RunFilter) -> Result<Vec<RunSummary>, MemoryError> {
self.inner.list_runs(filter).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use klieo_core::test_utils::InMemoryEpisodic;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::broadcast::error::TryRecvError;
struct FailingEpisodic {
record_calls: AtomicUsize,
}
impl FailingEpisodic {
fn new() -> Self {
Self {
record_calls: AtomicUsize::new(0),
}
}
}
#[async_trait]
impl EpisodicMemory for FailingEpisodic {
async fn record(&self, _run: RunId, _event: Episode) -> Result<(), MemoryError> {
self.record_calls.fetch_add(1, Ordering::SeqCst);
Err(MemoryError::Store("simulated persistence failure".into()))
}
async fn replay(&self, _run: RunId) -> Result<Vec<Episode>, MemoryError> {
Ok(Vec::new())
}
async fn list_runs(&self, _filter: RunFilter) -> Result<Vec<RunSummary>, MemoryError> {
Ok(Vec::new())
}
}
#[tokio::test]
async fn record_delegates_to_inner() {
let inner: Arc<dyn EpisodicMemory> = Arc::new(InMemoryEpisodic::default());
let (wrapper, _rx) = ProjectableEpisodic::new(Arc::clone(&inner));
let run = RunId::new();
wrapper
.record(
run,
Episode::Started {
agent: "alpha".into(),
},
)
.await
.expect("record succeeds");
let stored = inner.replay(run).await.expect("replay");
assert_eq!(stored.len(), 1, "inner should have one recorded episode");
assert!(matches!(&stored[0], Episode::Started { agent } if agent == "alpha"));
}
#[tokio::test]
async fn broadcasts_each_record() {
let inner: Arc<dyn EpisodicMemory> = Arc::new(InMemoryEpisodic::default());
let (wrapper, mut rx) = ProjectableEpisodic::new(inner);
let run = RunId::new();
wrapper
.record(
run,
Episode::Started {
agent: "alpha".into(),
},
)
.await
.unwrap();
wrapper.record(run, Episode::Completed).await.unwrap();
let (got_run, got_ep) = rx.recv().await.expect("first broadcast");
assert_eq!(got_run, run);
assert!(matches!(got_ep, Episode::Started { agent } if agent == "alpha"));
let (got_run, got_ep) = rx.recv().await.expect("second broadcast");
assert_eq!(got_run, run);
assert!(matches!(got_ep, Episode::Completed));
}
#[tokio::test]
async fn record_inner_failure_does_not_broadcast() {
let failing = Arc::new(FailingEpisodic::new());
let (wrapper, mut rx) =
ProjectableEpisodic::new(Arc::clone(&failing) as Arc<dyn EpisodicMemory>);
let run = RunId::new();
let err = wrapper
.record(
run,
Episode::Started {
agent: "alpha".into(),
},
)
.await
.expect_err("inner failure must propagate");
assert!(matches!(err, MemoryError::Store(_)));
assert_eq!(
failing.record_calls.load(Ordering::SeqCst),
1,
"inner must be called exactly once"
);
match rx.try_recv() {
Err(TryRecvError::Empty) => {}
other => panic!("expected Empty channel after failed record, got {other:?}"),
}
}
#[tokio::test]
async fn replay_delegates_to_inner() {
let inner = Arc::new(InMemoryEpisodic::default());
let run = RunId::new();
inner
.record(
run,
Episode::Started {
agent: "alpha".into(),
},
)
.await
.unwrap();
let (wrapper, _rx) =
ProjectableEpisodic::new(Arc::clone(&inner) as Arc<dyn EpisodicMemory>);
let episodes = wrapper.replay(run).await.expect("replay through wrapper");
assert_eq!(episodes.len(), 1);
assert!(matches!(&episodes[0], Episode::Started { agent } if agent == "alpha"));
}
#[tokio::test]
async fn replay_propagates_inner_error() {
struct FailingReplay;
#[async_trait]
impl EpisodicMemory for FailingReplay {
async fn record(&self, _run: RunId, _event: Episode) -> Result<(), MemoryError> {
Ok(())
}
async fn replay(&self, _run: RunId) -> Result<Vec<Episode>, MemoryError> {
Err(MemoryError::Store("simulated replay failure".into()))
}
async fn list_runs(&self, _filter: RunFilter) -> Result<Vec<RunSummary>, MemoryError> {
Ok(Vec::new())
}
}
let (wrapper, _rx) =
ProjectableEpisodic::new(Arc::new(FailingReplay) as Arc<dyn EpisodicMemory>);
let err = wrapper
.replay(RunId::new())
.await
.expect_err("inner replay error must propagate");
assert!(matches!(err, MemoryError::Store(s) if s.contains("simulated replay failure")));
}
#[tokio::test]
async fn list_runs_delegates_to_inner() {
let inner = Arc::new(InMemoryEpisodic::default());
let run = RunId::new();
inner.record(run, Episode::Completed).await.unwrap();
let (wrapper, _rx) =
ProjectableEpisodic::new(Arc::clone(&inner) as Arc<dyn EpisodicMemory>);
let runs = wrapper
.list_runs(RunFilter::default())
.await
.expect("list_runs through wrapper");
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].run_id, run);
}
#[tokio::test]
async fn subscribe_receives_events_published_after_subscription() {
let inner: Arc<dyn EpisodicMemory> = Arc::new(InMemoryEpisodic::default());
let (wrapper, _rx_initial) = ProjectableEpisodic::new(inner);
let mut rx_second = wrapper.subscribe();
let run = RunId::new();
wrapper
.record(
run,
Episode::Started {
agent: "alpha".into(),
},
)
.await
.unwrap();
let (got_run, got_ep) = rx_second.recv().await.expect("second subscriber receives");
assert_eq!(got_run, run);
assert!(matches!(got_ep, Episode::Started { agent } if agent == "alpha"));
}
}