Skip to main content

lago_api/sse/
lago.rs

1use lago_core::EventEnvelope;
2
3use super::format::{SseFormat, SseFrame};
4
5/// Native Lago SSE format. Events are sent as-is in their JSON envelope
6/// representation, preserving the full event structure.
7pub struct LagoFormat;
8
9impl SseFormat for LagoFormat {
10    fn format(&self, event: &EventEnvelope) -> Vec<SseFrame> {
11        let data = match serde_json::to_string(event) {
12            Ok(json) => json,
13            Err(e) => {
14                tracing::warn!(error = %e, "failed to serialize event envelope");
15                return Vec::new();
16            }
17        };
18
19        vec![SseFrame {
20            event: Some("event".to_string()),
21            data,
22            id: Some(event.seq.to_string()),
23        }]
24    }
25
26    fn done_frame(&self) -> Option<SseFrame> {
27        Some(SseFrame {
28            event: Some("done".to_string()),
29            data: r#"{"type":"done"}"#.to_string(),
30            id: None,
31        })
32    }
33
34    fn extra_headers(&self) -> Vec<(String, String)> {
35        Vec::new()
36    }
37
38    fn name(&self) -> &str {
39        "lago"
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use lago_core::event::EventPayload;
47    use lago_core::id::*;
48    use std::collections::HashMap;
49
50    fn make_envelope(payload: EventPayload, seq: u64) -> EventEnvelope {
51        EventEnvelope {
52            event_id: EventId::from_string("EVT001"),
53            session_id: SessionId::from_string("SESS001"),
54            branch_id: BranchId::from_string("main"),
55            run_id: None,
56            seq,
57            timestamp: 1_700_000_000_000_000,
58            parent_id: None,
59            payload,
60            metadata: HashMap::new(),
61        }
62    }
63
64    #[test]
65    fn lago_format_passes_through_all_events() {
66        let fmt = LagoFormat;
67
68        // Message events
69        let event = make_envelope(
70            EventPayload::Message {
71                role: "user".into(),
72                content: "hi".into(),
73                model: None,
74                token_usage: None,
75            },
76            1,
77        );
78        let frames = fmt.format(&event);
79        assert_eq!(frames.len(), 1);
80        assert_eq!(frames[0].event.as_deref(), Some("event"));
81        assert_eq!(frames[0].id.as_deref(), Some("1"));
82
83        // The data should be a valid JSON EventEnvelope
84        let back: EventEnvelope = serde_json::from_str(&frames[0].data).unwrap();
85        assert_eq!(back.event_id.as_str(), "EVT001");
86    }
87
88    #[test]
89    fn lago_format_includes_file_events() {
90        let fmt = LagoFormat;
91        let event = make_envelope(
92            EventPayload::FileDelete {
93                path: "/tmp/x".into(),
94            },
95            5,
96        );
97        let frames = fmt.format(&event);
98        assert_eq!(frames.len(), 1);
99        // Verify the full envelope is in the data
100        let back: EventEnvelope = serde_json::from_str(&frames[0].data).unwrap();
101        if let EventPayload::FileDelete { path } = &back.payload {
102            assert_eq!(path, "/tmp/x");
103        } else {
104            panic!("wrong payload");
105        }
106    }
107
108    #[test]
109    fn lago_done_frame() {
110        let fmt = LagoFormat;
111        let done = fmt.done_frame().unwrap();
112        assert_eq!(done.event.as_deref(), Some("done"));
113        assert_eq!(done.data, r#"{"type":"done"}"#);
114    }
115
116    #[test]
117    fn lago_no_extra_headers() {
118        assert!(LagoFormat.extra_headers().is_empty());
119    }
120
121    #[test]
122    fn name_is_lago() {
123        assert_eq!(LagoFormat.name(), "lago");
124    }
125}