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    /// An error occurred in the system.
65    Error { source: String, message: String },
66}
67
68/// A timestamped event with metadata.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct EventPayload {
71    /// Unique identifier for this event instance.
72    pub id: Uuid,
73    /// When the event occurred.
74    pub timestamp: DateTime<Utc>,
75    /// The event data.
76    pub event: PunchEvent,
77    /// Optional correlation ID for tracing related events.
78    pub correlation_id: Option<Uuid>,
79}
80
81impl EventPayload {
82    /// Create a new `EventPayload` with the current timestamp.
83    pub fn new(event: PunchEvent) -> Self {
84        Self {
85            id: Uuid::new_v4(),
86            timestamp: Utc::now(),
87            event,
88            correlation_id: None,
89        }
90    }
91
92    /// Attach a correlation ID for event tracing.
93    pub fn with_correlation(mut self, correlation_id: Uuid) -> Self {
94        self.correlation_id = Some(correlation_id);
95        self
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_fighter_spawned_serde() {
105        let event = PunchEvent::FighterSpawned {
106            fighter_id: FighterId(Uuid::nil()),
107            name: "TestFighter".to_string(),
108        };
109        let json = serde_json::to_string(&event).expect("serialize");
110        assert!(json.contains("\"kind\":\"fighter_spawned\""));
111        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
112        match deser {
113            PunchEvent::FighterSpawned { name, .. } => assert_eq!(name, "TestFighter"),
114            _ => panic!("wrong variant"),
115        }
116    }
117
118    #[test]
119    fn test_fighter_message_serde() {
120        let event = PunchEvent::FighterMessage {
121            fighter_id: FighterId(Uuid::nil()),
122            bout_id: Uuid::nil(),
123            role: "user".to_string(),
124            content_preview: "Hello".to_string(),
125        };
126        let json = serde_json::to_string(&event).expect("serialize");
127        assert!(json.contains("\"kind\":\"fighter_message\""));
128        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
129        match deser {
130            PunchEvent::FighterMessage {
131                content_preview, ..
132            } => assert_eq!(content_preview, "Hello"),
133            _ => panic!("wrong variant"),
134        }
135    }
136
137    #[test]
138    fn test_gorilla_unleashed_serde() {
139        let event = PunchEvent::GorillaUnleashed {
140            gorilla_id: GorillaId(Uuid::nil()),
141            name: "AlphaGorilla".to_string(),
142        };
143        let json = serde_json::to_string(&event).expect("serialize");
144        assert!(json.contains("\"kind\":\"gorilla_unleashed\""));
145        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
146        match deser {
147            PunchEvent::GorillaUnleashed { name, .. } => assert_eq!(name, "AlphaGorilla"),
148            _ => panic!("wrong variant"),
149        }
150    }
151
152    #[test]
153    fn test_tool_executed_serde() {
154        let event = PunchEvent::ToolExecuted {
155            agent_id: "agent-1".to_string(),
156            tool_name: "web_fetch".to_string(),
157            success: true,
158            duration_ms: 150,
159        };
160        let json = serde_json::to_string(&event).expect("serialize");
161        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
162        match deser {
163            PunchEvent::ToolExecuted {
164                success,
165                duration_ms,
166                ..
167            } => {
168                assert!(success);
169                assert_eq!(duration_ms, 150);
170            }
171            _ => panic!("wrong variant"),
172        }
173    }
174
175    #[test]
176    fn test_bout_started_serde() {
177        let event = PunchEvent::BoutStarted {
178            bout_id: Uuid::nil(),
179            fighter_id: FighterId(Uuid::nil()),
180        };
181        let json = serde_json::to_string(&event).expect("serialize");
182        assert!(json.contains("\"kind\":\"bout_started\""));
183    }
184
185    #[test]
186    fn test_bout_ended_serde() {
187        let event = PunchEvent::BoutEnded {
188            bout_id: Uuid::nil(),
189            fighter_id: FighterId(Uuid::nil()),
190            messages_exchanged: 42,
191        };
192        let json = serde_json::to_string(&event).expect("serialize");
193        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
194        match deser {
195            PunchEvent::BoutEnded {
196                messages_exchanged, ..
197            } => assert_eq!(messages_exchanged, 42),
198            _ => panic!("wrong variant"),
199        }
200    }
201
202    #[test]
203    fn test_combo_triggered_serde() {
204        let event = PunchEvent::ComboTriggered {
205            combo_name: "deploy-pipeline".to_string(),
206            triggered_by: "admin".to_string(),
207        };
208        let json = serde_json::to_string(&event).expect("serialize");
209        assert!(json.contains("\"kind\":\"combo_triggered\""));
210    }
211
212    #[test]
213    fn test_error_event_serde() {
214        let event = PunchEvent::Error {
215            source: "kernel".to_string(),
216            message: "out of memory".to_string(),
217        };
218        let json = serde_json::to_string(&event).expect("serialize");
219        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
220        match deser {
221            PunchEvent::Error { source, message } => {
222                assert_eq!(source, "kernel");
223                assert_eq!(message, "out of memory");
224            }
225            _ => panic!("wrong variant"),
226        }
227    }
228
229    #[test]
230    fn test_event_payload_new() {
231        let event = PunchEvent::FighterSpawned {
232            fighter_id: FighterId(Uuid::nil()),
233            name: "Test".to_string(),
234        };
235        let payload = EventPayload::new(event);
236        assert!(payload.correlation_id.is_none());
237        assert!(payload.timestamp <= Utc::now());
238    }
239
240    #[test]
241    fn test_event_payload_with_correlation() {
242        let event = PunchEvent::Error {
243            source: "test".to_string(),
244            message: "msg".to_string(),
245        };
246        let corr_id = Uuid::new_v4();
247        let payload = EventPayload::new(event).with_correlation(corr_id);
248        assert_eq!(payload.correlation_id, Some(corr_id));
249    }
250
251    #[test]
252    fn test_event_payload_serde_roundtrip() {
253        let event = PunchEvent::ToolExecuted {
254            agent_id: "a1".to_string(),
255            tool_name: "read_file".to_string(),
256            success: false,
257            duration_ms: 0,
258        };
259        let payload = EventPayload::new(event);
260        let json = serde_json::to_string(&payload).expect("serialize");
261        let deser: EventPayload = serde_json::from_str(&json).expect("deserialize");
262        assert_eq!(deser.id, payload.id);
263    }
264
265    #[test]
266    fn test_gorilla_paused_serde() {
267        let event = PunchEvent::GorillaPaused {
268            gorilla_id: GorillaId(Uuid::nil()),
269            reason: "rate limited".to_string(),
270        };
271        let json = serde_json::to_string(&event).expect("serialize");
272        assert!(json.contains("\"kind\":\"gorilla_paused\""));
273        let deser: PunchEvent = serde_json::from_str(&json).expect("deserialize");
274        match deser {
275            PunchEvent::GorillaPaused { reason, .. } => assert_eq!(reason, "rate limited"),
276            _ => panic!("wrong variant"),
277        }
278    }
279
280    #[test]
281    fn test_event_clone() {
282        let event = PunchEvent::FighterSpawned {
283            fighter_id: FighterId(Uuid::nil()),
284            name: "Clone".to_string(),
285        };
286        let cloned = event.clone();
287        let json1 = serde_json::to_string(&event).unwrap();
288        let json2 = serde_json::to_string(&cloned).unwrap();
289        assert_eq!(json1, json2);
290    }
291}