use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, mpsc};
use tracing::{debug, warn};
use clawft_types::agent_bus::{AgentBusError, InterAgentMessage};
pub(crate) const DEFAULT_INBOX_CAPACITY: usize = 256;
pub struct AgentInbox {
agent_id: String,
rx: mpsc::Receiver<InterAgentMessage>,
}
impl AgentInbox {
pub fn agent_id(&self) -> &str {
&self.agent_id
}
pub async fn recv(&mut self) -> Option<InterAgentMessage> {
loop {
match self.rx.recv().await {
Some(msg) => {
if msg.is_expired() {
warn!(
msg_id = %msg.id,
from = %msg.from_agent,
to = %msg.to_agent,
"dropping expired inter-agent message"
);
continue;
}
return Some(msg);
}
None => return None,
}
}
}
pub fn try_recv(&mut self) -> Option<InterAgentMessage> {
loop {
match self.rx.try_recv() {
Ok(msg) => {
if msg.is_expired() {
warn!(
msg_id = %msg.id,
from = %msg.from_agent,
to = %msg.to_agent,
"dropping expired inter-agent message"
);
continue;
}
return Some(msg);
}
Err(_) => return None,
}
}
}
}
pub struct AgentBus {
inboxes: Arc<Mutex<HashMap<String, mpsc::Sender<InterAgentMessage>>>>,
inbox_capacity: usize,
}
impl AgentBus {
pub fn new() -> Self {
Self {
inboxes: Arc::new(Mutex::new(HashMap::new())),
inbox_capacity: DEFAULT_INBOX_CAPACITY,
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
inboxes: Arc::new(Mutex::new(HashMap::new())),
inbox_capacity: capacity,
}
}
pub fn inbox_capacity(&self) -> usize {
self.inbox_capacity
}
pub async fn register_agent(&self, agent_id: &str) -> AgentInbox {
let (tx, rx) = mpsc::channel(self.inbox_capacity);
let mut inboxes = self.inboxes.lock().await;
inboxes.insert(agent_id.to_string(), tx);
debug!(agent_id = %agent_id, "agent registered on bus");
AgentInbox {
agent_id: agent_id.to_string(),
rx,
}
}
pub async fn unregister_agent(&self, agent_id: &str) {
let mut inboxes = self.inboxes.lock().await;
if inboxes.remove(agent_id).is_some() {
debug!(agent_id = %agent_id, "agent unregistered from bus");
}
}
pub async fn send(&self, msg: InterAgentMessage) -> Result<(), AgentBusError> {
if msg.is_expired() {
warn!(
msg_id = %msg.id,
from = %msg.from_agent,
to = %msg.to_agent,
"dropping expired message before delivery"
);
return Err(AgentBusError::MessageExpired { ttl: msg.ttl });
}
let inboxes = self.inboxes.lock().await;
let tx = inboxes
.get(&msg.to_agent)
.ok_or_else(|| AgentBusError::AgentNotFound(msg.to_agent.clone()))?;
tx.try_send(msg).map_err(|e| match e {
mpsc::error::TrySendError::Full(msg) => AgentBusError::InboxFull(msg.to_agent.clone()),
mpsc::error::TrySendError::Closed(msg) => {
AgentBusError::AgentNotFound(msg.to_agent.clone())
}
})
}
pub async fn is_registered(&self, agent_id: &str) -> bool {
let inboxes = self.inboxes.lock().await;
inboxes.contains_key(agent_id)
}
pub async fn registered_agents(&self) -> Vec<String> {
let inboxes = self.inboxes.lock().await;
inboxes.keys().cloned().collect()
}
}
impl Default for AgentBus {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::time::Duration;
#[tokio::test]
async fn register_and_send() {
let bus = AgentBus::new();
let mut inbox = bus.register_agent("agent-a").await;
let msg = InterAgentMessage::new(
"agent-b",
"agent-a",
"do work",
json!({}),
Duration::from_secs(60),
);
bus.send(msg).await.unwrap();
let received = inbox.recv().await.unwrap();
assert_eq!(received.from_agent, "agent-b");
assert_eq!(received.to_agent, "agent-a");
assert_eq!(received.task, "do work");
}
#[tokio::test]
async fn send_to_unregistered_agent_fails() {
let bus = AgentBus::new();
let msg = InterAgentMessage::new(
"sender",
"nonexistent",
"task",
json!({}),
Duration::from_secs(60),
);
let result = bus.send(msg).await;
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
AgentBusError::AgentNotFound(_)
));
}
#[tokio::test]
async fn agent_isolation() {
let bus = AgentBus::new();
let mut inbox_a = bus.register_agent("agent-a").await;
let mut inbox_b = bus.register_agent("agent-b").await;
let msg = InterAgentMessage::new(
"sender",
"agent-a",
"for a only",
json!({}),
Duration::from_secs(60),
);
bus.send(msg).await.unwrap();
let received = inbox_a.try_recv();
assert!(received.is_some());
assert_eq!(received.unwrap().task, "for a only");
let received_b = inbox_b.try_recv();
assert!(received_b.is_none());
}
#[tokio::test]
async fn unregister_agent() {
let bus = AgentBus::new();
let _inbox = bus.register_agent("agent-a").await;
assert!(bus.is_registered("agent-a").await);
bus.unregister_agent("agent-a").await;
assert!(!bus.is_registered("agent-a").await);
}
#[tokio::test]
async fn inbox_full_backpressure() {
let bus = AgentBus::with_capacity(2);
let _inbox = bus.register_agent("agent-a").await;
for i in 0..2 {
let msg = InterAgentMessage::new(
"sender",
"agent-a",
format!("task-{i}"),
json!({}),
Duration::from_secs(60),
);
bus.send(msg).await.unwrap();
}
let msg = InterAgentMessage::new(
"sender",
"agent-a",
"overflow",
json!({}),
Duration::from_secs(60),
);
let result = bus.send(msg).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AgentBusError::InboxFull(_)));
}
#[tokio::test]
async fn registered_agents_list() {
let bus = AgentBus::new();
bus.register_agent("a").await;
bus.register_agent("b").await;
let agents = bus.registered_agents().await;
assert_eq!(agents.len(), 2);
assert!(agents.contains(&"a".to_string()));
assert!(agents.contains(&"b".to_string()));
}
#[test]
fn agent_bus_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<AgentBus>();
}
#[test]
fn with_capacity_exposes_capacity() {
let bus = AgentBus::with_capacity(32);
assert_eq!(bus.inbox_capacity(), 32);
}
}