use anyhow::Result;
use async_trait::async_trait;
use crate::agents::Generator;
#[async_trait]
pub trait MemoryStrategy: Send + Sync {
async fn generate_rewrite(&self, prompt: &str) -> Option<String>;
async fn clear(&self) -> Result<()> {
Ok(())
}
fn name(&self) -> &'static str;
}
pub struct RewriteMemory {
agent: Box<dyn Generator>,
}
impl RewriteMemory {
pub fn new(agent: Box<dyn Generator>) -> Self {
Self { agent }
}
pub fn agent(&self) -> &dyn Generator {
&*self.agent
}
}
#[async_trait]
impl MemoryStrategy for RewriteMemory {
async fn generate_rewrite(&self, prompt: &str) -> Option<String> {
self.agent.generate(prompt).await.ok()
}
async fn clear(&self) -> Result<()> {
self.agent.clear_memory().await
}
fn name(&self) -> &'static str {
"rewrite"
}
}
pub struct TranscriptMemory;
#[async_trait]
impl MemoryStrategy for TranscriptMemory {
async fn generate_rewrite(&self, _prompt: &str) -> Option<String> {
None
}
fn name(&self) -> &'static str {
"transcript"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn transcript_never_rewrites() {
let strat = TranscriptMemory;
assert!(strat.generate_rewrite("any prompt").await.is_none());
}
#[tokio::test]
async fn transcript_clear_is_noop() {
let strat = TranscriptMemory;
assert!(strat.clear().await.is_ok());
}
#[test]
fn transcript_name() {
assert_eq!(TranscriptMemory.name(), "transcript");
}
#[test]
fn rewrite_memory_name() {
struct DummyGen;
#[async_trait]
impl Generator for DummyGen {
async fn generate_stream(
&self,
_prompt: &str,
_on_token: &(dyn Fn(String) + Sync),
) -> Result<()> {
Ok(())
}
fn backend_name(&self) -> &'static str {
"dummy"
}
fn model_name(&self) -> &str {
"dummy"
}
}
let strat = RewriteMemory::new(Box::new(DummyGen));
assert_eq!(strat.name(), "rewrite");
}
#[tokio::test]
async fn rewrite_memory_delegates_generate() {
struct EchoGen;
#[async_trait]
impl Generator for EchoGen {
async fn generate_stream(
&self,
prompt: &str,
on_token: &(dyn Fn(String) + Sync),
) -> Result<()> {
on_token(prompt.to_string());
Ok(())
}
fn backend_name(&self) -> &'static str {
"echo"
}
fn model_name(&self) -> &str {
"echo"
}
}
let strat = RewriteMemory::new(Box::new(EchoGen));
let result = strat.generate_rewrite("hello").await;
assert_eq!(result, Some("hello".to_string()));
}
struct LastTurnOnly {
agent: Box<dyn Generator>,
}
#[async_trait]
impl MemoryStrategy for LastTurnOnly {
async fn generate_rewrite(&self, prompt: &str) -> Option<String> {
if let Some((memory_part, rest)) = prompt.split_once("\n\n") {
let lines: Vec<&str> = memory_part.lines().collect();
let mut tail = Vec::new();
for line in lines.iter().rev() {
if line.starts_with("User: ") || line.starts_with("Assistant: ") {
tail.push(*line);
if tail.len() >= 2 {
break;
}
}
}
tail.reverse();
let trimmed = format!("Conversation:\n{}\n\n{}", tail.join("\n"), rest);
self.agent.generate(&trimmed).await.ok()
} else {
None
}
}
fn name(&self) -> &'static str {
"last-turn"
}
}
#[tokio::test]
async fn last_turn_only_trims_memory() {
struct EchoGen;
#[async_trait]
impl Generator for EchoGen {
async fn generate_stream(
&self,
prompt: &str,
on_token: &(dyn Fn(String) + Sync),
) -> Result<()> {
on_token(prompt.to_string());
Ok(())
}
fn backend_name(&self) -> &'static str {
"echo"
}
fn model_name(&self) -> &str {
"echo"
}
}
let strat = LastTurnOnly {
agent: Box::new(EchoGen),
};
let prompt = concat!(
"Conversation:\n",
"User: What is RAG?\n",
"Assistant: Retrieval-Augmented Generation.\n",
"User: Tell me more\n",
"Assistant: It combines vector search with LLMs.\n",
"User: Summarize that\n",
"Assistant: RAG retrieves docs, then generates answers.\n",
"\n",
"You are a query rewriter. Produce a single search query.\n",
"\n",
"Latest question: can you elaborate?\n",
);
let result = strat.generate_rewrite(prompt).await;
let trimmed = result.expect("should produce a trimmed prompt");
assert!(
trimmed.contains("User: Summarize that"),
"should contain the last user turn: {}",
trimmed
);
assert!(
trimmed.contains("Assistant: RAG retrieves docs"),
"should contain the last assistant turn: {}",
trimmed
);
assert!(
!trimmed.contains("What is RAG?"),
"should NOT contain earlier turns: {}",
trimmed
);
assert!(
!trimmed.contains("Tell me more"),
"should NOT contain earlier turns: {}",
trimmed
);
assert!(
trimmed.contains("can you elaborate?"),
"should contain the system rewrite prompt: {}",
trimmed
);
}
}