use std::sync::Arc;
use molo::memory::{CharTokenCounter, Memory, SummarizeStrategy, TokenCounter, WindowMemory};
use molo::{ContentBlock, FakeProvider, FakeReply, Message};
async fn count_tokens(messages: &[Message]) -> Result<usize, molo::memory::MemoryError> {
let counter = CharTokenCounter;
let mut total = 0usize;
for message in messages {
match message {
Message::System(s) => total += counter.count(s).await?,
Message::User(blocks) => {
for block in blocks {
match block {
ContentBlock::Text(t) => total += counter.count(t).await?,
ContentBlock::Image(_) | ContentBlock::Wire(_) => {}
}
}
}
Message::Assistant {
content,
reasoning,
tool_calls,
} => {
total += counter.count(content).await?;
if let Some(r) = reasoning {
total += counter.count(r).await?;
}
for tc in tool_calls {
total += counter.count(&tc.arguments).await?;
}
}
Message::ToolResult { content, .. } => total += counter.count(content).await?,
}
}
Ok(total)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let fake = Arc::new(FakeProvider::new([
FakeReply::Text("A and B have been answered".into()),
FakeReply::Text("A and B have been answered, and C has been answered too".into()),
]));
let mut memory =
WindowMemory::new(30).with_strategy(Arc::new(SummarizeStrategy::new(fake.clone())));
for i in 1..=4 {
memory
.record(Message::user(format!("question for round {i}")))
.await?;
memory
.record(Message::assistant(format!("reply for round {i}")))
.await?;
}
let full_tokens = count_tokens(&[
Message::user("question for round 1"),
Message::assistant("reply for round 1"),
])
.await?;
println!(
"0. recorded 4 rounds of chat: 8 messages ≈ {} tokens (budget 30) → over budget; the first fetch triggers compression",
full_tokens * 4
);
let context = memory.context().await?;
let tokens = count_tokens(&context).await?;
println!(
"1. first compression: 8 messages → {} ≈ {tokens} tokens (summary + most recent round)",
context.len()
);
for message in &context {
println!(" {message:?}");
}
assert_eq!(context.len(), 3);
assert!(matches!(context[0], Message::System(_)));
assert_eq!(context[1], Message::user("question for round 4"));
let again = memory.context().await?;
let again_tokens = count_tokens(&again).await?;
println!(
"2. fetched {} again, under budget ≈ {again_tokens} tokens (materialized, zero recompute — the summarizer model is not called again)",
again.len()
);
assert_eq!(again, context);
assert_eq!(
fake.requests().len(),
1,
"after compression, under-budget fetches must not call the summarizer model again"
);
for i in 5..=6 {
memory
.record(Message::user(format!("question for round {i}")))
.await?;
memory
.record(Message::assistant(format!("reply for round {i}")))
.await?;
}
let context = memory.context().await?;
let tokens = count_tokens(&context).await?;
println!(
"3. compressed again after appending rounds 5 and 6: {} ≈ {tokens} tokens (old summary merged into the new one)",
context.len()
);
for message in &context {
println!(" {message:?}");
}
assert_eq!(context.len(), 3);
assert_eq!(context[1], Message::user("question for round 6"));
assert_eq!(
fake.requests().len(),
2,
"each compression calls the summarizer model once"
);
let requests = fake.requests();
let Message::User(blocks) = &requests[1].messages[1] else {
unreachable!("summary input is a user message");
};
let ContentBlock::Text(text) = &blocks[0] else {
panic!("expected a text block");
};
assert!(
text.contains("A and B have been answered"),
"incremental compression must carry the previous summary"
);
println!(
"4. the summarizer model received 2 requests in total; the second request's input contains the previous summary (incremental merge)"
);
Ok(())
}