use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Default)]
pub struct SwarmScratchpad {
entries: Arc<RwLock<HashMap<String, String>>>,
}
impl SwarmScratchpad {
pub fn new() -> Self {
Self {
entries: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn write(&self, role: &str, output: &str) {
let mut entries = self.entries.write().await;
entries.insert(role.to_string(), output.to_string());
}
pub async fn read(&self, role: &str) -> Option<String> {
let entries = self.entries.read().await;
entries.get(role).cloned()
}
pub async fn entries(&self) -> HashMap<String, String> {
let entries = self.entries.read().await;
entries.clone()
}
pub async fn is_empty(&self) -> bool {
let entries = self.entries.read().await;
entries.is_empty()
}
pub async fn format_for_prompt(&self) -> Option<String> {
let entries = self.entries.read().await;
if entries.is_empty() {
return None;
}
let mut lines = vec!["Previous agent outputs:".to_string()];
let mut sorted: Vec<_> = entries.iter().collect();
sorted.sort_by_key(|(k, _)| k.as_str());
for (role, output) in sorted {
let truncated = if output.len() > 2000 {
format!("{}... [truncated]", &output[..2000])
} else {
output.clone()
};
lines.push(format!("- {}: {}", role, truncated));
}
Some(lines.join("\n"))
}
pub async fn clear(&self) {
let mut entries = self.entries.write().await;
entries.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_scratchpad_new_is_empty() {
let sp = SwarmScratchpad::new();
assert!(sp.is_empty().await);
assert_eq!(sp.format_for_prompt().await, None);
}
#[tokio::test]
async fn test_scratchpad_write_read() {
let sp = SwarmScratchpad::new();
sp.write("researcher", "Found 3 results").await;
assert_eq!(
sp.read("researcher").await,
Some("Found 3 results".to_string())
);
assert_eq!(sp.read("writer").await, None);
assert!(!sp.is_empty().await);
}
#[tokio::test]
async fn test_scratchpad_overwrite() {
let sp = SwarmScratchpad::new();
sp.write("analyst", "First analysis").await;
sp.write("analyst", "Updated analysis").await;
assert_eq!(
sp.read("analyst").await,
Some("Updated analysis".to_string())
);
}
#[tokio::test]
async fn test_scratchpad_format_for_prompt() {
let sp = SwarmScratchpad::new();
sp.write("researcher", "Found data").await;
sp.write("writer", "Wrote summary").await;
let prompt = sp.format_for_prompt().await.unwrap();
assert!(prompt.starts_with("Previous agent outputs:"));
assert!(prompt.contains("- researcher: Found data"));
assert!(prompt.contains("- writer: Wrote summary"));
}
#[tokio::test]
async fn test_scratchpad_format_truncates_long_output() {
let sp = SwarmScratchpad::new();
let long_output = "x".repeat(3000);
sp.write("analyst", &long_output).await;
let prompt = sp.format_for_prompt().await.unwrap();
assert!(prompt.contains("[truncated]"));
assert!(prompt.len() < 3000 + 200); }
#[tokio::test]
async fn test_scratchpad_clear() {
let sp = SwarmScratchpad::new();
sp.write("researcher", "data").await;
assert!(!sp.is_empty().await);
sp.clear().await;
assert!(sp.is_empty().await);
}
#[tokio::test]
async fn test_scratchpad_entries_snapshot() {
let sp = SwarmScratchpad::new();
sp.write("a", "alpha").await;
sp.write("b", "beta").await;
let entries = sp.entries().await;
assert_eq!(entries.len(), 2);
assert_eq!(entries.get("a"), Some(&"alpha".to_string()));
assert_eq!(entries.get("b"), Some(&"beta".to_string()));
}
#[tokio::test]
async fn test_scratchpad_clone_shares_state() {
let sp = SwarmScratchpad::new();
let sp2 = sp.clone();
sp.write("role1", "data1").await;
assert_eq!(sp2.read("role1").await, Some("data1".to_string()));
}
}