Skip to main content

rustant_core/gateway/
events.rs

1//! Gateway event types and message protocol.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Events emitted by the gateway to connected clients.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "type")]
10pub enum GatewayEvent {
11    /// A client has connected.
12    Connected { connection_id: Uuid },
13    /// A client has disconnected.
14    Disconnected { connection_id: Uuid },
15    /// A new task was submitted.
16    TaskSubmitted { task_id: Uuid, description: String },
17    /// Progress update on a running task.
18    TaskProgress {
19        task_id: Uuid,
20        progress: f32,
21        message: String,
22    },
23    /// A task has completed.
24    TaskCompleted {
25        task_id: Uuid,
26        success: bool,
27        summary: String,
28    },
29    /// An assistant message (full).
30    AssistantMessage { content: String },
31    /// A single token from a streaming response.
32    StreamToken { token: String },
33    /// A tool is being executed.
34    ToolExecution {
35        tool_name: String,
36        status: ToolStatus,
37    },
38    /// An error occurred.
39    Error { code: String, message: String },
40    /// A channel message was received.
41    ChannelMessageReceived {
42        channel_type: String,
43        message: String,
44    },
45    /// A task was dispatched to a node.
46    NodeTaskDispatched { node_id: String, task_name: String },
47    /// An agent was spawned.
48    AgentSpawned { agent_id: String, name: String },
49    /// An agent was terminated.
50    AgentTerminated { agent_id: String },
51    /// Metrics update for dashboard monitoring.
52    MetricsUpdate {
53        active_connections: usize,
54        active_sessions: usize,
55        total_tool_calls: u64,
56        total_llm_requests: u64,
57        uptime_secs: u64,
58    },
59    /// An approval request awaiting user decision.
60    ApprovalRequest {
61        approval_id: Uuid,
62        tool_name: String,
63        description: String,
64        risk_level: String,
65    },
66    /// A config snapshot was requested or changed.
67    ConfigSnapshot { config_json: String },
68}
69
70/// Status of a tool execution.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub enum ToolStatus {
73    Started,
74    Running,
75    Completed,
76    Failed,
77}
78
79/// Messages sent from clients to the gateway.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(tag = "type")]
82pub enum ClientMessage {
83    /// Authenticate with a token.
84    Authenticate { token: String },
85    /// Submit a new task to the agent.
86    SubmitTask { description: String },
87    /// Cancel a running task.
88    CancelTask { task_id: Uuid },
89    /// Request the current status.
90    GetStatus,
91    /// Keep-alive ping.
92    Ping { timestamp: DateTime<Utc> },
93    /// List connected channels.
94    ListChannels,
95    /// List registered nodes.
96    ListNodes,
97    /// Request current metrics for the dashboard.
98    GetMetrics,
99    /// Request current configuration snapshot.
100    GetConfig,
101    /// Submit an approval decision.
102    ApprovalDecision {
103        approval_id: Uuid,
104        approved: bool,
105        reason: Option<String>,
106    },
107}
108
109/// Messages sent from the gateway to clients.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(tag = "type")]
112pub enum ServerMessage {
113    /// Authentication succeeded.
114    Authenticated { connection_id: Uuid },
115    /// Authentication failed.
116    AuthFailed { reason: String },
117    /// A gateway event.
118    Event { event: GatewayEvent },
119    /// Response to a GetStatus request.
120    StatusResponse {
121        connected_clients: usize,
122        active_tasks: usize,
123        uptime_secs: u64,
124    },
125    /// Pong response to a Ping.
126    Pong { timestamp: DateTime<Utc> },
127    /// Channel status listing.
128    ChannelStatus { channels: Vec<(String, String)> },
129    /// Node status listing.
130    NodeStatus { nodes: Vec<(String, String)> },
131    /// Metrics snapshot for dashboard.
132    MetricsResponse {
133        active_connections: usize,
134        active_sessions: usize,
135        total_tool_calls: u64,
136        total_llm_requests: u64,
137        uptime_secs: u64,
138    },
139    /// Configuration snapshot.
140    ConfigResponse { config_json: String },
141    /// Approval decision acknowledgment.
142    ApprovalAck { approval_id: Uuid, accepted: bool },
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_gateway_event_serialization() {
151        let event = GatewayEvent::Connected {
152            connection_id: Uuid::new_v4(),
153        };
154        let json = serde_json::to_string(&event).unwrap();
155        assert!(json.contains("Connected"));
156
157        let restored: GatewayEvent = serde_json::from_str(&json).unwrap();
158        match restored {
159            GatewayEvent::Connected { connection_id } => {
160                assert!(!connection_id.is_nil());
161            }
162            _ => panic!("Wrong variant"),
163        }
164    }
165
166    #[test]
167    fn test_client_message_serialization() {
168        let msg = ClientMessage::Authenticate {
169            token: "secret".into(),
170        };
171        let json = serde_json::to_string(&msg).unwrap();
172        let restored: ClientMessage = serde_json::from_str(&json).unwrap();
173        match restored {
174            ClientMessage::Authenticate { token } => assert_eq!(token, "secret"),
175            _ => panic!("Wrong variant"),
176        }
177    }
178
179    #[test]
180    fn test_server_message_serialization() {
181        let msg = ServerMessage::StatusResponse {
182            connected_clients: 3,
183            active_tasks: 1,
184            uptime_secs: 3600,
185        };
186        let json = serde_json::to_string(&msg).unwrap();
187        let restored: ServerMessage = serde_json::from_str(&json).unwrap();
188        match restored {
189            ServerMessage::StatusResponse {
190                connected_clients,
191                active_tasks,
192                uptime_secs,
193            } => {
194                assert_eq!(connected_clients, 3);
195                assert_eq!(active_tasks, 1);
196                assert_eq!(uptime_secs, 3600);
197            }
198            _ => panic!("Wrong variant"),
199        }
200    }
201
202    #[test]
203    fn test_all_event_variants_serialize() {
204        let events: Vec<GatewayEvent> = vec![
205            GatewayEvent::Connected {
206                connection_id: Uuid::new_v4(),
207            },
208            GatewayEvent::Disconnected {
209                connection_id: Uuid::new_v4(),
210            },
211            GatewayEvent::TaskSubmitted {
212                task_id: Uuid::new_v4(),
213                description: "test".into(),
214            },
215            GatewayEvent::TaskProgress {
216                task_id: Uuid::new_v4(),
217                progress: 0.5,
218                message: "halfway".into(),
219            },
220            GatewayEvent::TaskCompleted {
221                task_id: Uuid::new_v4(),
222                success: true,
223                summary: "done".into(),
224            },
225            GatewayEvent::AssistantMessage {
226                content: "hello".into(),
227            },
228            GatewayEvent::StreamToken {
229                token: "tok".into(),
230            },
231            GatewayEvent::ToolExecution {
232                tool_name: "read_file".into(),
233                status: ToolStatus::Started,
234            },
235            GatewayEvent::Error {
236                code: "E001".into(),
237                message: "bad".into(),
238            },
239            GatewayEvent::ChannelMessageReceived {
240                channel_type: "telegram".into(),
241                message: "hello".into(),
242            },
243            GatewayEvent::NodeTaskDispatched {
244                node_id: "n1".into(),
245                task_name: "shell".into(),
246            },
247            GatewayEvent::AgentSpawned {
248                agent_id: "a1".into(),
249                name: "helper".into(),
250            },
251            GatewayEvent::AgentTerminated {
252                agent_id: "a1".into(),
253            },
254            GatewayEvent::MetricsUpdate {
255                active_connections: 5,
256                active_sessions: 2,
257                total_tool_calls: 100,
258                total_llm_requests: 50,
259                uptime_secs: 3600,
260            },
261            GatewayEvent::ApprovalRequest {
262                approval_id: Uuid::new_v4(),
263                tool_name: "shell_exec".into(),
264                description: "Run rm -rf".into(),
265                risk_level: "high".into(),
266            },
267            GatewayEvent::ConfigSnapshot {
268                config_json: "{}".into(),
269            },
270        ];
271
272        for event in &events {
273            let json = serde_json::to_string(event).unwrap();
274            let _: GatewayEvent = serde_json::from_str(&json).unwrap();
275        }
276        assert_eq!(events.len(), 16);
277    }
278
279    #[test]
280    fn test_tool_status_serialization() {
281        let statuses = vec![
282            ToolStatus::Started,
283            ToolStatus::Running,
284            ToolStatus::Completed,
285            ToolStatus::Failed,
286        ];
287        for status in statuses {
288            let json = serde_json::to_string(&status).unwrap();
289            let _: ToolStatus = serde_json::from_str(&json).unwrap();
290        }
291    }
292
293    #[test]
294    fn test_ping_pong_messages() {
295        let now = Utc::now();
296        let ping = ClientMessage::Ping { timestamp: now };
297        let json = serde_json::to_string(&ping).unwrap();
298        let restored: ClientMessage = serde_json::from_str(&json).unwrap();
299        match restored {
300            ClientMessage::Ping { timestamp } => {
301                assert_eq!(timestamp, now);
302            }
303            _ => panic!("Wrong variant"),
304        }
305
306        let pong = ServerMessage::Pong { timestamp: now };
307        let json = serde_json::to_string(&pong).unwrap();
308        let _: ServerMessage = serde_json::from_str(&json).unwrap();
309    }
310
311    #[test]
312    fn test_gateway_event_channel_message_received() {
313        let event = GatewayEvent::ChannelMessageReceived {
314            channel_type: "slack".into(),
315            message: "hello world".into(),
316        };
317        let json = serde_json::to_string(&event).unwrap();
318        assert!(json.contains("slack"));
319        let restored: GatewayEvent = serde_json::from_str(&json).unwrap();
320        match restored {
321            GatewayEvent::ChannelMessageReceived {
322                channel_type,
323                message,
324            } => {
325                assert_eq!(channel_type, "slack");
326                assert_eq!(message, "hello world");
327            }
328            _ => panic!("Wrong variant"),
329        }
330    }
331
332    #[test]
333    fn test_gateway_event_node_task_dispatched() {
334        let event = GatewayEvent::NodeTaskDispatched {
335            node_id: "node-1".into(),
336            task_name: "shell".into(),
337        };
338        let json = serde_json::to_string(&event).unwrap();
339        let restored: GatewayEvent = serde_json::from_str(&json).unwrap();
340        match restored {
341            GatewayEvent::NodeTaskDispatched { node_id, task_name } => {
342                assert_eq!(node_id, "node-1");
343                assert_eq!(task_name, "shell");
344            }
345            _ => panic!("Wrong variant"),
346        }
347    }
348
349    #[test]
350    fn test_gateway_event_agent_spawned_terminated() {
351        let spawned = GatewayEvent::AgentSpawned {
352            agent_id: "a1".into(),
353            name: "helper".into(),
354        };
355        let terminated = GatewayEvent::AgentTerminated {
356            agent_id: "a1".into(),
357        };
358        let json1 = serde_json::to_string(&spawned).unwrap();
359        let json2 = serde_json::to_string(&terminated).unwrap();
360        let _: GatewayEvent = serde_json::from_str(&json1).unwrap();
361        let _: GatewayEvent = serde_json::from_str(&json2).unwrap();
362    }
363
364    #[test]
365    fn test_server_message_channel_status() {
366        let msg = ServerMessage::ChannelStatus {
367            channels: vec![
368                ("telegram".into(), "connected".into()),
369                ("slack".into(), "disconnected".into()),
370            ],
371        };
372        let json = serde_json::to_string(&msg).unwrap();
373        let restored: ServerMessage = serde_json::from_str(&json).unwrap();
374        match restored {
375            ServerMessage::ChannelStatus { channels } => {
376                assert_eq!(channels.len(), 2);
377                assert_eq!(channels[0].0, "telegram");
378            }
379            _ => panic!("Wrong variant"),
380        }
381    }
382
383    #[test]
384    fn test_server_message_node_status() {
385        let msg = ServerMessage::NodeStatus {
386            nodes: vec![("macos-local".into(), "healthy".into())],
387        };
388        let json = serde_json::to_string(&msg).unwrap();
389        let restored: ServerMessage = serde_json::from_str(&json).unwrap();
390        match restored {
391            ServerMessage::NodeStatus { nodes } => {
392                assert_eq!(nodes.len(), 1);
393                assert_eq!(nodes[0].1, "healthy");
394            }
395            _ => panic!("Wrong variant"),
396        }
397    }
398
399    #[test]
400    fn test_client_message_list_channels_nodes() {
401        let lc = ClientMessage::ListChannels;
402        let ln = ClientMessage::ListNodes;
403        let json1 = serde_json::to_string(&lc).unwrap();
404        let json2 = serde_json::to_string(&ln).unwrap();
405        let _: ClientMessage = serde_json::from_str(&json1).unwrap();
406        let _: ClientMessage = serde_json::from_str(&json2).unwrap();
407    }
408}