Skip to main content

punch_types/
event.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use crate::fighter::FighterId;
6use crate::gorilla::GorillaId;
7use crate::troop::TroopId;
8
9/// Events emitted by the Punch system.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case", tag = "kind")]
12pub enum PunchEvent {
13    /// A new Fighter has been spawned.
14    FighterSpawned { fighter_id: FighterId, name: String },
15    /// A Fighter sent or received a message.
16    FighterMessage {
17        fighter_id: FighterId,
18        bout_id: Uuid,
19        role: String,
20        content_preview: String,
21    },
22    /// A Gorilla has been unleashed (activated).
23    GorillaUnleashed { gorilla_id: GorillaId, name: String },
24    /// A Gorilla has been paused.
25    GorillaPaused {
26        gorilla_id: GorillaId,
27        reason: String,
28    },
29    /// A tool (move) was executed.
30    ToolExecuted {
31        agent_id: String,
32        tool_name: String,
33        success: bool,
34        duration_ms: u64,
35    },
36    /// A bout (session/conversation) has started.
37    BoutStarted {
38        bout_id: Uuid,
39        fighter_id: FighterId,
40    },
41    /// A bout has ended.
42    BoutEnded {
43        bout_id: Uuid,
44        fighter_id: FighterId,
45        messages_exchanged: u64,
46    },
47    /// A combo (workflow) has been triggered.
48    ComboTriggered {
49        combo_name: String,
50        triggered_by: String,
51    },
52    /// A troop has been formed.
53    TroopFormed {
54        troop_id: TroopId,
55        name: String,
56        member_count: usize,
57    },
58    /// A troop has been disbanded.
59    TroopDisbanded { troop_id: TroopId, name: String },
60    /// An MCP server has been started and initialized.
61    McpServerStarted { server_name: String },
62    /// An MCP server has been shut down.
63    McpServerStopped { server_name: String },
64    /// A heartbeat task was proactively executed by the scheduler.
65    HeartbeatExecuted {
66        fighter_id: FighterId,
67        task_description: String,
68    },
69    /// An error occurred in the system.
70    Error { source: String, message: String },
71}
72
73/// A timestamped event with metadata.
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct EventPayload {
76    /// Unique identifier for this event instance.
77    pub id: Uuid,
78    /// When the event occurred.
79    pub timestamp: DateTime<Utc>,
80    /// The event data.
81    pub event: PunchEvent,
82    /// Optional correlation ID for tracing related events.
83    pub correlation_id: Option<Uuid>,
84}
85
86impl EventPayload {
87    /// Create a new `EventPayload` with the current timestamp.
88    pub fn new(event: PunchEvent) -> Self {
89        Self {
90            id: Uuid::new_v4(),
91            timestamp: Utc::now(),
92            event,
93            correlation_id: None,
94        }
95    }
96
97    /// Attach a correlation ID for event tracing.
98    pub fn with_correlation(mut self, correlation_id: Uuid) -> Self {
99        self.correlation_id = Some(correlation_id);
100        self
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_fighter_spawned_serde() {
110        let event = PunchEvent::FighterSpawned {
111            fighter_id: FighterId(Uuid::nil()),
112            name: "TestFighter".to_string(),
113        };
114        let json = serde_json::to_string(&event).expect("serialize");
115        assert!(json.contains("\"kind\":\"fighter_spawned\""));
116        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
117        match deser {
118            PunchEvent::FighterSpawned { name, .. } => assert_eq!(name, "TestFighter"),
119            _ => panic!("wrong variant"),
120        }
121    }
122
123    #[test]
124    fn test_fighter_message_serde() {
125        let event = PunchEvent::FighterMessage {
126            fighter_id: FighterId(Uuid::nil()),
127            bout_id: Uuid::nil(),
128            role: "user".to_string(),
129            content_preview: "Hello".to_string(),
130        };
131        let json = serde_json::to_string(&event).expect("serialize");
132        assert!(json.contains("\"kind\":\"fighter_message\""));
133        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
134        match deser {
135            PunchEvent::FighterMessage {
136                content_preview, ..
137            } => assert_eq!(content_preview, "Hello"),
138            _ => panic!("wrong variant"),
139        }
140    }
141
142    #[test]
143    fn test_gorilla_unleashed_serde() {
144        let event = PunchEvent::GorillaUnleashed {
145            gorilla_id: GorillaId(Uuid::nil()),
146            name: "AlphaGorilla".to_string(),
147        };
148        let json = serde_json::to_string(&event).expect("serialize");
149        assert!(json.contains("\"kind\":\"gorilla_unleashed\""));
150        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
151        match deser {
152            PunchEvent::GorillaUnleashed { name, .. } => assert_eq!(name, "AlphaGorilla"),
153            _ => panic!("wrong variant"),
154        }
155    }
156
157    #[test]
158    fn test_tool_executed_serde() {
159        let event = PunchEvent::ToolExecuted {
160            agent_id: "agent-1".to_string(),
161            tool_name: "web_fetch".to_string(),
162            success: true,
163            duration_ms: 150,
164        };
165        let json = serde_json::to_string(&event).expect("serialize");
166        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
167        match deser {
168            PunchEvent::ToolExecuted {
169                success,
170                duration_ms,
171                ..
172            } => {
173                assert!(success);
174                assert_eq!(duration_ms, 150);
175            }
176            _ => panic!("wrong variant"),
177        }
178    }
179
180    #[test]
181    fn test_bout_started_serde() {
182        let event = PunchEvent::BoutStarted {
183            bout_id: Uuid::nil(),
184            fighter_id: FighterId(Uuid::nil()),
185        };
186        let json = serde_json::to_string(&event).expect("serialize");
187        assert!(json.contains("\"kind\":\"bout_started\""));
188    }
189
190    #[test]
191    fn test_bout_ended_serde() {
192        let event = PunchEvent::BoutEnded {
193            bout_id: Uuid::nil(),
194            fighter_id: FighterId(Uuid::nil()),
195            messages_exchanged: 42,
196        };
197        let json = serde_json::to_string(&event).expect("serialize");
198        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
199        match deser {
200            PunchEvent::BoutEnded {
201                messages_exchanged, ..
202            } => assert_eq!(messages_exchanged, 42),
203            _ => panic!("wrong variant"),
204        }
205    }
206
207    #[test]
208    fn test_combo_triggered_serde() {
209        let event = PunchEvent::ComboTriggered {
210            combo_name: "deploy-pipeline".to_string(),
211            triggered_by: "admin".to_string(),
212        };
213        let json = serde_json::to_string(&event).expect("serialize");
214        assert!(json.contains("\"kind\":\"combo_triggered\""));
215    }
216
217    #[test]
218    fn test_error_event_serde() {
219        let event = PunchEvent::Error {
220            source: "kernel".to_string(),
221            message: "out of memory".to_string(),
222        };
223        let json = serde_json::to_string(&event).expect("serialize");
224        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
225        match deser {
226            PunchEvent::Error { source, message } => {
227                assert_eq!(source, "kernel");
228                assert_eq!(message, "out of memory");
229            }
230            _ => panic!("wrong variant"),
231        }
232    }
233
234    #[test]
235    fn test_event_payload_new() {
236        let event = PunchEvent::FighterSpawned {
237            fighter_id: FighterId(Uuid::nil()),
238            name: "Test".to_string(),
239        };
240        let payload = EventPayload::new(event);
241        assert!(payload.correlation_id.is_none());
242        assert!(payload.timestamp <= Utc::now());
243    }
244
245    #[test]
246    fn test_event_payload_with_correlation() {
247        let event = PunchEvent::Error {
248            source: "test".to_string(),
249            message: "msg".to_string(),
250        };
251        let corr_id = Uuid::new_v4();
252        let payload = EventPayload::new(event).with_correlation(corr_id);
253        assert_eq!(payload.correlation_id, Some(corr_id));
254    }
255
256    #[test]
257    fn test_event_payload_serde_roundtrip() {
258        let event = PunchEvent::ToolExecuted {
259            agent_id: "a1".to_string(),
260            tool_name: "read_file".to_string(),
261            success: false,
262            duration_ms: 0,
263        };
264        let payload = EventPayload::new(event);
265        let json = serde_json::to_string(&payload).expect("serialize");
266        let deser: EventPayload = serde_json::from_str(&json).expect("deserialize");
267        assert_eq!(deser.id, payload.id);
268    }
269
270    #[test]
271    fn test_gorilla_paused_serde() {
272        let event = PunchEvent::GorillaPaused {
273            gorilla_id: GorillaId(Uuid::nil()),
274            reason: "rate limited".to_string(),
275        };
276        let json = serde_json::to_string(&event).expect("serialize");
277        assert!(json.contains("\"kind\":\"gorilla_paused\""));
278        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
279        match deser {
280            PunchEvent::GorillaPaused { reason, .. } => assert_eq!(reason, "rate limited"),
281            _ => panic!("wrong variant"),
282        }
283    }
284
285    #[test]
286    fn test_event_clone() {
287        let event = PunchEvent::FighterSpawned {
288            fighter_id: FighterId(Uuid::nil()),
289            name: "Clone".to_string(),
290        };
291        let cloned = event.clone();
292        let json1 = serde_json::to_string(&event).unwrap();
293        let json2 = serde_json::to_string(&cloned).unwrap();
294        assert_eq!(json1, json2);
295    }
296}