Skip to main content

oxicode_sdk/
message_bus.rs

1//! Inter-agent message bus for multi-agent communication.
2//!
3//! Provides a broadcast-based message bus that agents can use to
4//! communicate with each other in an oxicode environment.
5//!
6//! # Lag Handling
7//!
8//! The underlying `tokio::sync::broadcast` channel has a fixed capacity.
9//! Slow consumers will have old messages automatically dropped. Use
10//! [`MessageBus::subscribe_lag_aware`] to receive a [`LagAwareReceiver`]
11//! that logs a warning when messages are skipped due to lagging.
12
13use serde::{Deserialize, Serialize};
14use tokio::sync::broadcast;
15
16/// A message sent between agents.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct InterAgentMessage {
19    /// Sender agent ID.
20    pub from: String,
21    /// Recipient agent ID. `None` means broadcast to all subscribers.
22    pub to: Option<String>,
23    /// Message type (e.g. "task_complete", "delegation", "status").
24    pub message_type: String,
25    /// Message payload (arbitrary JSON).
26    pub payload: serde_json::Value,
27    /// Unix timestamp in milliseconds.
28    pub timestamp_ms: u64,
29}
30
31impl InterAgentMessage {
32    /// Create a new directed message.
33    pub fn direct(
34        from: impl Into<String>,
35        to: impl Into<String>,
36        message_type: impl Into<String>,
37        payload: serde_json::Value,
38    ) -> Self {
39        Self {
40            from: from.into(),
41            to: Some(to.into()),
42            message_type: message_type.into(),
43            payload,
44            timestamp_ms: std::time::SystemTime::now()
45                .duration_since(std::time::UNIX_EPOCH)
46                .unwrap_or_default()
47                .as_millis() as u64,
48        }
49    }
50
51    /// Create a broadcast message.
52    pub fn broadcast(
53        from: impl Into<String>,
54        message_type: impl Into<String>,
55        payload: serde_json::Value,
56    ) -> Self {
57        Self {
58            from: from.into(),
59            to: None,
60            message_type: message_type.into(),
61            payload,
62            timestamp_ms: std::time::SystemTime::now()
63                .duration_since(std::time::UNIX_EPOCH)
64                .unwrap_or_default()
65                .as_millis() as u64,
66        }
67    }
68
69    /// Check if this message is intended for the given agent.
70    pub fn is_for(&self, agent_id: &str) -> bool {
71        self.to.as_deref() == Some(agent_id) || self.to.is_none()
72    }
73}
74
75/// Result of a publish operation on the message bus.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum PublishResult {
78    /// Message was delivered to `n` active subscribers.
79    Delivered {
80        /// Number of subscribers that received the message.
81        n: usize,
82    },
83    /// Message was dropped because there were no active subscribers.
84    NoSubscribers,
85}
86
87impl PublishResult {
88    /// Returns the number of subscribers that received the message, or 0 if
89    /// there were no subscribers.
90    pub fn delivered_count(&self) -> usize {
91        match self {
92            PublishResult::Delivered { n } => *n,
93            PublishResult::NoSubscribers => 0,
94        }
95    }
96}
97
98/// Broadcast-based message bus for inter-agent communication.
99///
100/// Agents subscribe to the bus and receive messages addressed to them
101/// or broadcast messages. Thread-safe and async-compatible.
102#[derive(Clone)]
103pub struct MessageBus {
104    sender: broadcast::Sender<InterAgentMessage>,
105    capacity: usize,
106}
107
108impl MessageBus {
109    /// Create a new message bus with the given channel capacity.
110    pub fn new(capacity: usize) -> Self {
111        let (tx, _rx) = broadcast::channel(capacity);
112        Self {
113            sender: tx,
114            capacity,
115        }
116    }
117
118    /// Publish a message to the bus.
119    ///
120    /// Returns a [`PublishResult`] indicating how many receivers received the
121    /// message, or whether the message was dropped due to no subscribers.
122    /// A warning is logged when messages are dropped.
123    pub fn publish(&self, msg: InterAgentMessage) -> PublishResult {
124        match self.sender.send(msg) {
125            Ok(n) => PublishResult::Delivered { n },
126            Err(broadcast::error::SendError(msg)) => {
127                tracing::warn!(
128                    from = %msg.from,
129                    message_type = %msg.message_type,
130                    "MessageBus publish dropped message: no active subscribers"
131                );
132                PublishResult::NoSubscribers
133            }
134        }
135    }
136
137    /// Subscribe to all messages on the bus.
138    ///
139    /// **Warning**: The raw broadcast receiver will silently drop messages if
140    /// the receiver lags behind. Consider using [`subscribe_lag_aware`] instead.
141    ///
142    /// [`subscribe_lag_aware`]: MessageBus::subscribe_lag_aware
143    pub fn subscribe(&self) -> broadcast::Receiver<InterAgentMessage> {
144        self.sender.subscribe()
145    }
146
147    /// Subscribe with automatic lag handling.
148    ///
149    /// Returns a [`LagAwareReceiver`] that logs a warning when messages are
150    /// skipped due to the receiver falling behind.
151    pub fn subscribe_lag_aware(&self) -> LagAwareReceiver {
152        LagAwareReceiver {
153            inner: self.sender.subscribe(),
154            total_skipped: std::sync::atomic::AtomicU64::new(0),
155        }
156    }
157
158    /// Get the number of active subscribers.
159    pub fn subscriber_count(&self) -> usize {
160        self.sender.receiver_count()
161    }
162
163    /// Get the configured capacity.
164    pub fn capacity(&self) -> usize {
165        self.capacity
166    }
167}
168
169/// A broadcast receiver that logs warnings when messages are dropped due to
170/// lagging instead of silently losing them.
171///
172/// Obtained via [`MessageBus::subscribe_lag_aware`].
173pub struct LagAwareReceiver {
174    inner: broadcast::Receiver<InterAgentMessage>,
175    total_skipped: std::sync::atomic::AtomicU64,
176}
177
178impl LagAwareReceiver {
179    /// Receive the next message.
180    ///
181    /// If the receiver has fallen behind and messages were skipped, a warning
182    /// is logged and the next available message is returned.
183    ///
184    /// Returns `None` if all senders have been dropped (channel closed).
185    pub async fn recv(&mut self) -> Option<InterAgentMessage> {
186        loop {
187            match self.inner.recv().await {
188                Ok(msg) => return Some(msg),
189                Err(broadcast::error::RecvError::Lagged(n)) => {
190                    let prev = self
191                        .total_skipped
192                        .fetch_add(n, std::sync::atomic::Ordering::Relaxed);
193                    tracing::warn!(
194                        skipped_now = n,
195                        total_skipped = prev + n,
196                        "MessageBus receiver lagged — messages were dropped"
197                    );
198                    continue;
199                }
200                Err(broadcast::error::RecvError::Closed) => return None,
201            }
202        }
203    }
204
205    /// Try to receive a message without waiting.
206    ///
207    /// Returns the message if available, or an indication of why no message
208    /// is available.
209    pub fn try_recv(&mut self) -> Result<InterAgentMessage, broadcast::error::TryRecvError> {
210        loop {
211            match self.inner.try_recv() {
212                Ok(msg) => return Ok(msg),
213                Err(broadcast::error::TryRecvError::Lagged(n)) => {
214                    let prev = self
215                        .total_skipped
216                        .fetch_add(n, std::sync::atomic::Ordering::Relaxed);
217                    tracing::warn!(
218                        skipped_now = n,
219                        total_skipped = prev + n,
220                        "MessageBus receiver lagged — messages were dropped"
221                    );
222                    continue;
223                }
224                Err(e) => return Err(e),
225            }
226        }
227    }
228
229    /// Returns the total number of messages skipped due to lagging since this
230    /// receiver was created.
231    pub fn total_skipped(&self) -> u64 {
232        self.total_skipped
233            .load(std::sync::atomic::Ordering::Relaxed)
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use serde_json::json;
241
242    #[test]
243    fn test_direct_message() {
244        let msg = InterAgentMessage::direct(
245            "agent-1",
246            "agent-2",
247            "task_complete",
248            json!({"result": "ok"}),
249        );
250        assert_eq!(msg.from, "agent-1");
251        assert_eq!(msg.to, Some("agent-2".to_string()));
252        assert!(msg.is_for("agent-2"));
253        assert!(!msg.is_for("agent-1"));
254        assert!(!msg.is_for("agent-3"));
255    }
256
257    #[test]
258    fn test_broadcast_message() {
259        let msg =
260            InterAgentMessage::broadcast("agent-1", "status_update", json!({"status": "idle"}));
261        assert_eq!(msg.from, "agent-1");
262        assert!(msg.to.is_none());
263        assert!(msg.is_for("agent-2"));
264        assert!(msg.is_for("agent-3"));
265    }
266
267    #[tokio::test]
268    async fn test_message_bus_pub_sub() {
269        let bus = MessageBus::new(16);
270        let mut rx = bus.subscribe();
271
272        let msg = InterAgentMessage::broadcast("agent-1", "ping", json!("pong"));
273        let result = bus.publish(msg.clone());
274        assert_eq!(result.delivered_count(), 1);
275
276        let received = rx.try_recv().expect("should receive message");
277        assert_eq!(received.from, "agent-1");
278        assert_eq!(received.message_type, "ping");
279    }
280
281    #[tokio::test]
282    async fn test_message_bus_multiple_subscribers() {
283        let bus = MessageBus::new(16);
284        let mut rx1 = bus.subscribe();
285        let mut rx2 = bus.subscribe();
286
287        assert_eq!(bus.subscriber_count(), 2);
288
289        let msg = InterAgentMessage::broadcast("coordinator", "start", json!({}));
290        let result = bus.publish(msg);
291        assert_eq!(result, PublishResult::Delivered { n: 2 });
292
293        assert!(rx1.try_recv().is_ok());
294        assert!(rx2.try_recv().is_ok());
295    }
296
297    #[test]
298    fn test_message_bus_no_subscribers() {
299        let bus = MessageBus::new(16);
300        // No subscribers — publish should return NoSubscribers.
301        let msg = InterAgentMessage::broadcast("agent-1", "ping", json!("pong"));
302        let result = bus.publish(msg);
303        assert_eq!(result, PublishResult::NoSubscribers);
304    }
305
306    #[test]
307    fn test_message_serialization() {
308        let msg = InterAgentMessage::direct("a", "b", "test", json!({"key": "value"}));
309        let json_str = serde_json::to_string(&msg).unwrap();
310        let deserialized: InterAgentMessage = serde_json::from_str(&json_str).unwrap();
311        assert_eq!(deserialized.from, "a");
312        assert_eq!(deserialized.to, Some("b".to_string()));
313    }
314
315    #[tokio::test]
316    async fn test_lag_aware_receiver() {
317        let bus = MessageBus::new(2);
318        let mut rx = bus.subscribe_lag_aware();
319
320        // Publish 5 messages to a capacity-2 channel.
321        for i in 0..5 {
322            bus.publish(InterAgentMessage::broadcast("sender", "test", json!(i)));
323        }
324
325        // LagAwareReceiver should still return available messages after logging lag.
326        let mut received = Vec::new();
327        for _ in 0..3 {
328            match rx.try_recv() {
329                Ok(msg) => received.push(msg),
330                Err(_) => break,
331            }
332        }
333
334        // We should get at least some messages (the most recent ones).
335        assert!(!received.is_empty());
336        // Some messages were skipped due to lagging.
337        assert!(rx.total_skipped() > 0);
338    }
339
340    #[tokio::test]
341    async fn test_lag_aware_receiver_recv() {
342        let bus = MessageBus::new(4);
343        let mut rx = bus.subscribe_lag_aware();
344
345        bus.publish(InterAgentMessage::broadcast("a", "ping", json!(1)));
346        bus.publish(InterAgentMessage::broadcast("a", "pong", json!(2)));
347
348        let msg = rx.recv().await.expect("should receive");
349        assert_eq!(msg.message_type, "ping");
350        let msg = rx.recv().await.expect("should receive");
351        assert_eq!(msg.message_type, "pong");
352    }
353
354    #[test]
355    fn test_publish_result_delivered_count() {
356        assert_eq!(PublishResult::Delivered { n: 3 }.delivered_count(), 3);
357        assert_eq!(PublishResult::NoSubscribers.delivered_count(), 0);
358    }
359}