Skip to main content

cersei_tools/
remote_trigger.rs

1//! RemoteTrigger tool: fire cross-session events.
2
3use super::*;
4use serde::Deserialize;
5
6/// Global event registry for cross-session triggers.
7static TRIGGER_REGISTRY: once_cell::sync::Lazy<dashmap::DashMap<String, Vec<TriggerEvent>>> =
8    once_cell::sync::Lazy::new(dashmap::DashMap::new);
9
10#[derive(Debug, Clone, serde::Serialize)]
11pub struct TriggerEvent {
12    pub source_session: String,
13    pub target_session: String,
14    pub event_type: String,
15    pub payload: serde_json::Value,
16    pub timestamp: String,
17}
18
19/// Drain pending trigger events for a session.
20pub fn drain_triggers(session_id: &str) -> Vec<TriggerEvent> {
21    TRIGGER_REGISTRY
22        .remove(session_id)
23        .map(|(_, v)| v)
24        .unwrap_or_default()
25}
26
27pub struct RemoteTriggerTool;
28
29#[async_trait]
30impl Tool for RemoteTriggerTool {
31    fn name(&self) -> &str {
32        "RemoteTrigger"
33    }
34    fn description(&self) -> &str {
35        "Send an event to another session or agent."
36    }
37    fn permission_level(&self) -> PermissionLevel {
38        PermissionLevel::Execute
39    }
40    fn category(&self) -> ToolCategory {
41        ToolCategory::Orchestration
42    }
43
44    fn input_schema(&self) -> Value {
45        serde_json::json!({
46            "type": "object",
47            "properties": {
48                "target_session": { "type": "string", "description": "Target session ID" },
49                "event_type": { "type": "string", "description": "Event type identifier" },
50                "payload": { "description": "Event payload (any JSON)" }
51            },
52            "required": ["target_session", "event_type"]
53        })
54    }
55
56    async fn execute(&self, input: Value, ctx: &ToolContext) -> ToolResult {
57        #[derive(Deserialize)]
58        struct Input {
59            target_session: String,
60            event_type: String,
61            payload: Option<Value>,
62        }
63
64        let input: Input = match serde_json::from_value(input) {
65            Ok(i) => i,
66            Err(e) => return ToolResult::error(format!("Invalid input: {}", e)),
67        };
68
69        let event = TriggerEvent {
70            source_session: ctx.session_id.clone(),
71            target_session: input.target_session.clone(),
72            event_type: input.event_type.clone(),
73            payload: input.payload.unwrap_or(Value::Null),
74            timestamp: chrono::Utc::now().to_rfc3339(),
75        };
76
77        TRIGGER_REGISTRY
78            .entry(input.target_session.clone())
79            .or_default()
80            .push(event);
81
82        ToolResult::success(format!(
83            "Trigger '{}' sent to session '{}'",
84            input.event_type, input.target_session
85        ))
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::permissions::AllowAll;
93    use std::sync::Arc;
94
95    fn test_ctx() -> ToolContext {
96        ToolContext {
97            working_dir: std::env::temp_dir(),
98            session_id: "sender".into(),
99            permissions: Arc::new(AllowAll),
100            cost_tracker: Arc::new(CostTracker::new()),
101            mcp_manager: None,
102            extensions: Extensions::default(),
103        }
104    }
105
106    #[tokio::test]
107    async fn test_trigger_send_receive() {
108        let tool = RemoteTriggerTool;
109        let result = tool
110            .execute(
111                serde_json::json!({
112                    "target_session": "receiver",
113                    "event_type": "tests_complete",
114                    "payload": {"passed": 42}
115                }),
116                &test_ctx(),
117            )
118            .await;
119
120        assert!(!result.is_error);
121        assert!(result.content.contains("sent"));
122
123        let events = drain_triggers("receiver");
124        assert_eq!(events.len(), 1);
125        assert_eq!(events[0].event_type, "tests_complete");
126        assert_eq!(events[0].source_session, "sender");
127    }
128}