coyote-ai 0.7.2

An all-in-one, batteries included LLM CLI Tool
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
    pub from: String,
    pub to: String,
    pub payload: EnvelopePayload,
    pub timestamp: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum EnvelopePayload {
    Text { content: String },
    TaskCompleted { task_id: String, summary: String },
    ShutdownRequest { reason: String },
    ShutdownApproved,
}

#[derive(Debug, Default)]
pub struct Inbox {
    messages: parking_lot::Mutex<Vec<Envelope>>,
}

impl Inbox {
    pub fn new() -> Self {
        Self {
            messages: parking_lot::Mutex::new(Vec::new()),
        }
    }

    pub fn deliver(&self, envelope: Envelope) {
        self.messages.lock().push(envelope);
    }

    pub fn drain(&self) -> Vec<Envelope> {
        let mut msgs = {
            let mut guard = self.messages.lock();
            std::mem::take(&mut *guard)
        };

        msgs.sort_by_key(|e| match &e.payload {
            EnvelopePayload::ShutdownRequest { .. } => 0,
            EnvelopePayload::ShutdownApproved => 0,
            EnvelopePayload::TaskCompleted { .. } => 1,
            EnvelopePayload::Text { .. } => 2,
        });

        msgs
    }
}

impl Clone for Inbox {
    fn clone(&self) -> Self {
        let messages = self.messages.lock().clone();
        Self {
            messages: parking_lot::Mutex::new(messages),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::Utc;

    fn text_envelope(from: &str, to: &str, content: &str) -> Envelope {
        Envelope {
            from: from.to_string(),
            to: to.to_string(),
            payload: EnvelopePayload::Text {
                content: content.to_string(),
            },
            timestamp: Utc::now(),
        }
    }

    fn task_completed_envelope(from: &str, to: &str) -> Envelope {
        Envelope {
            from: from.to_string(),
            to: to.to_string(),
            payload: EnvelopePayload::TaskCompleted {
                task_id: "t1".into(),
                summary: "done".into(),
            },
            timestamp: Utc::now(),
        }
    }

    fn shutdown_request_envelope(from: &str, to: &str) -> Envelope {
        Envelope {
            from: from.to_string(),
            to: to.to_string(),
            payload: EnvelopePayload::ShutdownRequest {
                reason: "all done".into(),
            },
            timestamp: Utc::now(),
        }
    }

    #[test]
    fn inbox_new_is_empty() {
        let inbox = Inbox::new();
        assert!(inbox.drain().is_empty());
    }

    #[test]
    fn inbox_default_is_empty() {
        let inbox = Inbox::default();
        assert!(inbox.drain().is_empty());
    }

    #[test]
    fn deliver_and_drain() {
        let inbox = Inbox::new();
        inbox.deliver(text_envelope("a", "b", "hello"));
        let msgs = inbox.drain();
        assert_eq!(msgs.len(), 1);
        assert_eq!(msgs[0].from, "a");
    }

    #[test]
    fn drain_empties_inbox() {
        let inbox = Inbox::new();
        inbox.deliver(text_envelope("a", "b", "hello"));
        inbox.drain();
        assert!(inbox.drain().is_empty());
    }

    #[test]
    fn drain_orders_shutdown_before_task_before_text() {
        let inbox = Inbox::new();
        inbox.deliver(text_envelope("a", "b", "msg"));
        inbox.deliver(task_completed_envelope("a", "b"));
        inbox.deliver(shutdown_request_envelope("a", "b"));

        let msgs = inbox.drain();
        assert_eq!(msgs.len(), 3);
        assert!(matches!(
            msgs[0].payload,
            EnvelopePayload::ShutdownRequest { .. }
        ));
        assert!(matches!(
            msgs[1].payload,
            EnvelopePayload::TaskCompleted { .. }
        ));
        assert!(matches!(msgs[2].payload, EnvelopePayload::Text { .. }));
    }

    #[test]
    fn clone_preserves_messages() {
        let inbox = Inbox::new();
        inbox.deliver(text_envelope("a", "b", "hello"));
        let cloned = inbox.clone();
        let msgs = cloned.drain();
        assert_eq!(msgs.len(), 1);
    }

    #[test]
    fn clone_is_independent() {
        let inbox = Inbox::new();
        inbox.deliver(text_envelope("a", "b", "hello"));
        let cloned = inbox.clone();
        inbox.deliver(text_envelope("a", "b", "second"));
        let original_msgs = inbox.drain();
        let cloned_msgs = cloned.drain();
        assert_eq!(original_msgs.len(), 2);
        assert_eq!(cloned_msgs.len(), 1);
    }

    #[test]
    fn multiple_deliveries() {
        let inbox = Inbox::new();
        for i in 0..5 {
            inbox.deliver(text_envelope("a", "b", &format!("msg {i}")));
        }
        assert_eq!(inbox.drain().len(), 5);
    }
}