use async_trait::async_trait;
use crate::domain::{A2AError, ContextId, Conversation, Digest};
#[async_trait]
pub trait AsyncConversationStore: Send + Sync {
async fn load(
&self,
context_id: &ContextId,
caller: Option<&str>,
limit: Option<u32>,
) -> Result<Conversation, A2AError>;
async fn compact(
&self,
context_id: &ContextId,
caller: Option<&str>,
digest: Digest,
) -> Result<(), A2AError>;
}
#[async_trait]
pub trait AsyncConversationStoreExt: AsyncConversationStore {
async fn load_recent(
&self,
context_id: &ContextId,
caller: Option<&str>,
keep: u32,
) -> Result<Conversation, A2AError> {
self.load(context_id, caller, Some(keep)).await
}
async fn compact_through(
&self,
context_id: &ContextId,
caller: Option<&str>,
conversation: &Conversation,
summary: String,
model: String,
) -> Result<(), A2AError> {
let digest = Digest {
covers_through: conversation.watermark(),
summary,
replaced_messages: conversation.tail.len() as u32,
model,
};
self.compact(context_id, caller, digest).await
}
}
impl<T: AsyncConversationStore + ?Sized> AsyncConversationStoreExt for T {}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoConversationMemory;
#[async_trait]
impl AsyncConversationStore for NoConversationMemory {
async fn load(
&self,
_context_id: &ContextId,
_caller: Option<&str>,
_limit: Option<u32>,
) -> Result<Conversation, A2AError> {
Ok(Conversation::default())
}
async fn compact(
&self,
_context_id: &ContextId,
_caller: Option<&str>,
_digest: Digest,
) -> Result<(), A2AError> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[tokio::test]
async fn the_no_memory_store_reports_an_empty_conversation() {
let store = NoConversationMemory;
let context = ContextId::from_str("ctx-1").unwrap();
let conversation = store.load(&context, None, None).await.unwrap();
assert!(conversation.is_empty());
store
.compact_through(
&context,
None,
&conversation,
"nothing happened".to_string(),
"test".to_string(),
)
.await
.unwrap();
}
}