1use lellm_core::TokenUsage;
29use lellm_graph::StreamChunk;
30use lellm_provider::ProviderEvent;
31
32#[derive(Debug, Clone)]
39pub enum AgentStreamEvent {
40 TextDelta { text: String },
42 ThinkingDelta {
44 thinking: String,
45 redacted: Option<String>,
46 },
47 ToolResult {
49 call_id: String,
50 tool_name: String,
51 content: String,
52 is_error: bool,
53 },
54 Usage(TokenUsage),
56 ResponseComplete,
58}
59
60impl AgentStreamEvent {
61 pub fn to_chunk(&self) -> Option<StreamChunk> {
69 match self {
70 AgentStreamEvent::TextDelta { text } => Some(StreamChunk::TextDelta(text.clone())),
71 AgentStreamEvent::ThinkingDelta { thinking, redacted } => {
72 Some(StreamChunk::ThinkingDelta {
73 text: thinking.clone(),
74 redacted: redacted.clone(),
75 })
76 }
77 AgentStreamEvent::ToolResult { .. } => None,
78 AgentStreamEvent::Usage(_) => None,
79 AgentStreamEvent::ResponseComplete => None,
80 }
81 }
82}
83
84pub fn transform(event: &ProviderEvent) -> Vec<AgentStreamEvent> {
91 match event {
92 ProviderEvent::Token { token } => {
93 vec![AgentStreamEvent::TextDelta {
94 text: token.clone(),
95 }]
96 }
97 ProviderEvent::ThinkingDelta { thinking, redacted } => {
98 vec![AgentStreamEvent::ThinkingDelta {
99 thinking: thinking.clone(),
100 redacted: redacted.clone(),
101 }]
102 }
103 ProviderEvent::ResponseComplete { tool_calls, usage } => {
104 let mut events = Vec::new();
105 let _ = tool_calls; if let Some(u) = usage {
109 events.push(AgentStreamEvent::Usage(u.clone()));
110 }
111 events.push(AgentStreamEvent::ResponseComplete);
112 events
113 }
114 ProviderEvent::Start { .. } => vec![], }
116}
117
118#[derive(Debug)]
125pub enum TranslationResult {
126 Emit(StreamChunk),
128 EmitWithText { chunk: StreamChunk, delta: String },
130 EmitWithThinking {
132 chunk: StreamChunk,
133 delta: String,
134 redacted: Option<String>,
135 },
136 Usage(TokenUsage),
138 Finished,
140 Ignore,
142}
143
144pub fn translate_provider_event(event: &ProviderEvent) -> TranslationResult {
149 for agent_event in transform(event) {
151 match agent_event {
152 AgentStreamEvent::TextDelta { text } => {
153 return TranslationResult::EmitWithText {
154 chunk: StreamChunk::TextDelta(text.clone()),
155 delta: text,
156 };
157 }
158 AgentStreamEvent::ThinkingDelta { thinking, redacted } => {
159 return TranslationResult::EmitWithThinking {
160 chunk: StreamChunk::ThinkingDelta {
161 text: thinking.clone(),
162 redacted: redacted.clone(),
163 },
164 delta: thinking,
165 redacted,
166 };
167 }
168 AgentStreamEvent::Usage(u) => return TranslationResult::Usage(u),
169 AgentStreamEvent::ResponseComplete => return TranslationResult::Finished,
170 AgentStreamEvent::ToolResult { .. } => {}
171 }
172 }
173 TranslationResult::Ignore
174}
175
176#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
185 fn test_transform_token() {
186 let event = ProviderEvent::Token {
187 token: "hello".into(),
188 };
189 let result = transform(&event);
190 assert_eq!(result.len(), 1);
191 match &result[0] {
192 AgentStreamEvent::TextDelta { text } => assert_eq!(text, "hello"),
193 other => panic!("expected TextDelta, got {:?}", other),
194 }
195 }
196
197 #[test]
198 fn test_transform_thinking_with_redacted() {
199 let event = ProviderEvent::ThinkingDelta {
200 thinking: "visible".into(),
201 redacted: Some("sensitive".into()),
202 };
203 let result = transform(&event);
204 assert_eq!(result.len(), 1);
205 match &result[0] {
206 AgentStreamEvent::ThinkingDelta { thinking, redacted } => {
207 assert_eq!(thinking, "visible");
208 assert_eq!(redacted, &Some("sensitive".into()));
209 }
210 other => panic!("expected ThinkingDelta, got {:?}", other),
211 }
212 }
213
214 #[test]
215 fn test_transform_thinking_without_redacted() {
216 let event = ProviderEvent::ThinkingDelta {
217 thinking: "thinking".into(),
218 redacted: None,
219 };
220 let result = transform(&event);
221 assert_eq!(result.len(), 1);
222 match &result[0] {
223 AgentStreamEvent::ThinkingDelta { thinking, redacted } => {
224 assert_eq!(thinking, "thinking");
225 assert!(redacted.is_none());
226 }
227 other => panic!("expected ThinkingDelta, got {:?}", other),
228 }
229 }
230
231 #[test]
232 fn test_transform_response_complete_with_usage() {
233 let event = ProviderEvent::ResponseComplete {
234 tool_calls: vec![],
235 usage: Some(TokenUsage::default()),
236 };
237 let result = transform(&event);
238 assert_eq!(result.len(), 2);
239 assert!(matches!(result[0], AgentStreamEvent::Usage(_)));
240 assert!(matches!(result[1], AgentStreamEvent::ResponseComplete));
241 }
242
243 #[test]
244 fn test_transform_response_complete_without_usage() {
245 let event = ProviderEvent::ResponseComplete {
246 tool_calls: vec![],
247 usage: None,
248 };
249 let result = transform(&event);
250 assert_eq!(result.len(), 1);
251 assert!(matches!(result[0], AgentStreamEvent::ResponseComplete));
252 }
253
254 #[test]
255 fn test_transform_start_ignored() {
256 let event = ProviderEvent::Start {
257 model: "test".into(),
258 };
259 let result = transform(&event);
260 assert!(result.is_empty());
261 }
262
263 #[test]
266 fn test_to_chunk_text_delta() {
267 let event = AgentStreamEvent::TextDelta {
268 text: "hello".into(),
269 };
270 let chunk = event.to_chunk();
271 assert!(chunk.is_some());
272 match chunk.unwrap() {
273 StreamChunk::TextDelta(t) => assert_eq!(t, "hello"),
274 other => panic!("expected TextDelta, got {:?}", other),
275 }
276 }
277
278 #[test]
279 fn test_to_chunk_thinking_delta() {
280 let event = AgentStreamEvent::ThinkingDelta {
281 thinking: "think".into(),
282 redacted: Some("secret".into()),
283 };
284 let chunk = event.to_chunk();
285 assert!(chunk.is_some());
286 match chunk.unwrap() {
287 StreamChunk::ThinkingDelta { text, redacted } => {
288 assert_eq!(text, "think");
289 assert_eq!(redacted, Some("secret".into()));
290 }
291 other => panic!("expected ThinkingDelta, got {:?}", other),
292 }
293 }
294
295 #[test]
296 fn test_to_chunk_usage_none() {
297 let event = AgentStreamEvent::Usage(TokenUsage::default());
298 assert!(event.to_chunk().is_none());
299 }
300
301 #[test]
302 fn test_to_chunk_response_complete_none() {
303 let event = AgentStreamEvent::ResponseComplete;
304 assert!(event.to_chunk().is_none());
305 }
306
307 #[test]
310 fn test_translate_provider_event_compat() {
311 let event = ProviderEvent::Token {
312 token: "hello".into(),
313 };
314 match translate_provider_event(&event) {
315 TranslationResult::EmitWithText {
316 chunk: StreamChunk::TextDelta(t),
317 delta,
318 } => {
319 assert_eq!(t, "hello");
320 assert_eq!(delta, "hello");
321 }
322 other => panic!("expected EmitWithText(TextDelta), got {:?}", other),
323 }
324 }
325
326 #[test]
327 fn test_ignore_protocol_events() {
328 let event = ProviderEvent::Start {
329 model: "test".into(),
330 };
331 assert!(matches!(
332 translate_provider_event(&event),
333 TranslationResult::Ignore
334 ));
335 }
336}