Skip to main content

cersei_tools/
send_message.rs

1//! SendMessage tool: inter-agent message passing.
2
3use super::*;
4use serde::Deserialize;
5
6/// Global inbox registry keyed by session_id.
7static INBOX_REGISTRY: once_cell::sync::Lazy<dashmap::DashMap<String, Vec<InboxMessage>>> =
8    once_cell::sync::Lazy::new(dashmap::DashMap::new);
9
10#[derive(Debug, Clone, serde::Serialize)]
11pub struct InboxMessage {
12    pub from: String,
13    pub content: String,
14    pub timestamp: String,
15}
16
17/// Drain all pending messages for a session.
18pub fn drain_inbox(session_id: &str) -> Vec<InboxMessage> {
19    INBOX_REGISTRY
20        .remove(session_id)
21        .map(|(_, v)| v)
22        .unwrap_or_default()
23}
24
25/// Peek at pending messages without consuming them.
26pub fn peek_inbox(session_id: &str) -> Vec<InboxMessage> {
27    INBOX_REGISTRY
28        .get(session_id)
29        .map(|v| v.clone())
30        .unwrap_or_default()
31}
32
33pub struct SendMessageTool;
34
35#[async_trait]
36impl Tool for SendMessageTool {
37    fn name(&self) -> &str {
38        "SendMessage"
39    }
40    fn description(&self) -> &str {
41        "Send a message to another agent or session by ID."
42    }
43    fn permission_level(&self) -> PermissionLevel {
44        PermissionLevel::None
45    }
46    fn category(&self) -> ToolCategory {
47        ToolCategory::Orchestration
48    }
49
50    fn input_schema(&self) -> Value {
51        serde_json::json!({
52            "type": "object",
53            "properties": {
54                "to": { "type": "string", "description": "Target session/agent ID" },
55                "content": { "type": "string", "description": "Message content" }
56            },
57            "required": ["to", "content"]
58        })
59    }
60
61    async fn execute(&self, input: Value, ctx: &ToolContext) -> ToolResult {
62        #[derive(Deserialize)]
63        struct Input {
64            to: String,
65            content: String,
66        }
67
68        let input: Input = match serde_json::from_value(input) {
69            Ok(i) => i,
70            Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
71        };
72
73        let msg = InboxMessage {
74            from: ctx.session_id.clone(),
75            content: input.content.clone(),
76            timestamp: chrono::Utc::now().to_rfc3339(),
77        };
78
79        INBOX_REGISTRY
80            .entry(input.to.clone())
81            .or_default()
82            .push(msg);
83
84        ToolResult::success(format!("Message sent to '{}'", input.to))
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::permissions::AllowAll;
92
93    fn test_ctx() -> ToolContext {
94        ToolContext {
95            working_dir: std::env::temp_dir(),
96            session_id: "agent-a".into(),
97            permissions: Arc::new(AllowAll),
98            cost_tracker: Arc::new(CostTracker::new()),
99            mcp_manager: None,
100            extensions: Extensions::default(),
101        }
102    }
103
104    #[tokio::test]
105    async fn test_send_and_receive() {
106        let tool = SendMessageTool;
107        tool.execute(
108            serde_json::json!({"to": "agent-b", "content": "Hello B!"}),
109            &test_ctx(),
110        )
111        .await;
112
113        let msgs = peek_inbox("agent-b");
114        assert_eq!(msgs.len(), 1);
115        assert_eq!(msgs[0].from, "agent-a");
116        assert_eq!(msgs[0].content, "Hello B!");
117
118        let drained = drain_inbox("agent-b");
119        assert_eq!(drained.len(), 1);
120        assert!(peek_inbox("agent-b").is_empty());
121    }
122}