1use crate::context::{ToolConfirmationDecision, ToolConfirmationRequest};
2use crate::model::LlmResponse;
3use crate::types::Content;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use uuid::Uuid;
8
9pub const KEY_PREFIX_APP: &str = "app:";
12pub const KEY_PREFIX_TEMP: &str = "temp:";
14pub const KEY_PREFIX_USER: &str = "user:";
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Event {
21 pub id: String,
23 pub timestamp: DateTime<Utc>,
25 pub invocation_id: String,
27 pub branch: String,
29 pub author: String,
31 #[serde(flatten)]
34 pub llm_response: LlmResponse,
35 pub actions: EventActions,
37 #[serde(default)]
39 pub long_running_tool_ids: Vec<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub llm_request: Option<String>,
43 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
46 pub provider_metadata: HashMap<String, String>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct EventCompaction {
54 pub start_timestamp: DateTime<Utc>,
56 pub end_timestamp: DateTime<Utc>,
58 pub compacted_content: Content,
60}
61
62#[derive(Debug, Clone, Default, Serialize, Deserialize)]
64pub struct EventActions {
65 pub state_delta: HashMap<String, serde_json::Value>,
67 pub artifact_delta: HashMap<String, i64>,
69 pub skip_summarization: bool,
71 pub transfer_to_agent: Option<String>,
73 pub escalate: bool,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub tool_confirmation: Option<ToolConfirmationRequest>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub tool_confirmation_decision: Option<ToolConfirmationDecision>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub compaction: Option<EventCompaction>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub route: Option<Vec<String>>,
89}
90
91impl Event {
92 pub fn new(invocation_id: impl Into<String>) -> Self {
94 Self {
95 id: Uuid::new_v4().to_string(),
96 timestamp: Utc::now(),
97 invocation_id: invocation_id.into(),
98 branch: String::new(),
99 author: String::new(),
100 llm_response: LlmResponse::default(),
101 actions: EventActions::default(),
102 long_running_tool_ids: Vec::new(),
103 llm_request: None,
104 provider_metadata: HashMap::new(),
105 }
106 }
107
108 pub fn with_id(id: impl Into<String>, invocation_id: impl Into<String>) -> Self {
111 Self {
112 id: id.into(),
113 timestamp: Utc::now(),
114 invocation_id: invocation_id.into(),
115 branch: String::new(),
116 author: String::new(),
117 llm_response: LlmResponse::default(),
118 actions: EventActions::default(),
119 long_running_tool_ids: Vec::new(),
120 llm_request: None,
121 provider_metadata: HashMap::new(),
122 }
123 }
124
125 pub fn content(&self) -> Option<&Content> {
127 self.llm_response.content.as_ref()
128 }
129
130 pub fn set_content(&mut self, content: Content) {
132 self.llm_response.content = Some(content);
133 }
134
135 pub fn interaction_id(&self) -> Option<&str> {
153 self.llm_response.interaction_id.as_deref()
154 }
155
156 pub fn is_final_response(&self) -> bool {
167 if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
169 return true;
170 }
171
172 let has_function_calls = self.has_function_calls();
174 let has_function_responses = self.has_function_responses();
175 let is_partial = self.llm_response.partial;
176 let has_trailing_code_result = self.has_trailing_code_execution_result();
177
178 !has_function_calls && !has_function_responses && !is_partial && !has_trailing_code_result
179 }
180
181 fn has_function_calls(&self) -> bool {
183 if let Some(content) = &self.llm_response.content {
184 for part in &content.parts {
185 if matches!(part, crate::Part::FunctionCall { .. }) {
186 return true;
187 }
188 }
189 }
190 false
191 }
192
193 fn has_function_responses(&self) -> bool {
195 if let Some(content) = &self.llm_response.content {
196 for part in &content.parts {
197 if matches!(part, crate::Part::FunctionResponse { .. }) {
198 return true;
199 }
200 }
201 }
202 false
203 }
204
205 #[allow(clippy::match_like_matches_macro)]
207 fn has_trailing_code_execution_result(&self) -> bool {
208 if let Some(content) = &self.llm_response.content
209 && let Some(last_part) = content.parts.last()
210 {
211 return matches!(last_part, crate::Part::FunctionResponse { .. });
214 }
215 false
216 }
217
218 pub fn function_call_ids(&self) -> Vec<String> {
221 let mut ids = Vec::new();
222 if let Some(content) = &self.llm_response.content {
223 for part in &content.parts {
224 if let crate::Part::FunctionCall { name, id, .. } = part {
225 ids.push(id.as_deref().unwrap_or(name).to_string());
228 }
229 }
230 }
231 ids
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::Part;
239
240 #[test]
241 fn test_event_creation() {
242 let event = Event::new("inv-123");
243 assert_eq!(event.invocation_id, "inv-123");
244 assert!(!event.id.is_empty());
245 }
246
247 #[test]
248 fn test_event_actions_default() {
249 let actions = EventActions::default();
250 assert!(actions.state_delta.is_empty());
251 assert!(!actions.skip_summarization);
252 assert!(actions.tool_confirmation.is_none());
253 assert!(actions.tool_confirmation_decision.is_none());
254 }
255
256 #[test]
257 fn test_state_prefixes() {
258 assert_eq!(KEY_PREFIX_APP, "app:");
259 assert_eq!(KEY_PREFIX_TEMP, "temp:");
260 assert_eq!(KEY_PREFIX_USER, "user:");
261 }
262
263 #[test]
264 fn test_is_final_response_no_content() {
265 let event = Event::new("inv-123");
266 assert!(event.is_final_response());
268 }
269
270 #[test]
271 fn test_is_final_response_text_only() {
272 let mut event = Event::new("inv-123");
273 event.llm_response.content = Some(Content {
274 role: "model".to_string(),
275 parts: vec![Part::Text { text: "Hello!".to_string() }],
276 });
277 assert!(event.is_final_response());
279 }
280
281 #[test]
282 fn test_is_final_response_with_function_call() {
283 let mut event = Event::new("inv-123");
284 event.llm_response.content = Some(Content {
285 role: "model".to_string(),
286 parts: vec![Part::FunctionCall {
287 name: "get_weather".to_string(),
288 args: serde_json::json!({"city": "NYC"}),
289 id: Some("call_123".to_string()),
290 thought_signature: None,
291 }],
292 });
293 assert!(!event.is_final_response());
295 }
296
297 #[test]
298 fn test_is_final_response_with_function_response() {
299 let mut event = Event::new("inv-123");
300 event.llm_response.content = Some(Content {
301 role: "function".to_string(),
302 parts: vec![Part::FunctionResponse {
303 function_response: crate::FunctionResponseData::new(
304 "get_weather",
305 serde_json::json!({"temp": 72}),
306 ),
307 id: Some("call_123".to_string()),
308 }],
309 });
310 assert!(!event.is_final_response());
312 }
313
314 #[test]
315 fn test_is_final_response_partial() {
316 let mut event = Event::new("inv-123");
317 event.llm_response.partial = true;
318 event.llm_response.content = Some(Content {
319 role: "model".to_string(),
320 parts: vec![Part::Text { text: "Hello...".to_string() }],
321 });
322 assert!(!event.is_final_response());
324 }
325
326 #[test]
327 fn test_is_final_response_skip_summarization() {
328 let mut event = Event::new("inv-123");
329 event.actions.skip_summarization = true;
330 event.llm_response.content = Some(Content {
331 role: "function".to_string(),
332 parts: vec![Part::FunctionResponse {
333 function_response: crate::FunctionResponseData::new(
334 "tool",
335 serde_json::json!({"result": "done"}),
336 ),
337 id: Some("call_tool".to_string()),
338 }],
339 });
340 assert!(event.is_final_response());
342 }
343
344 #[test]
345 fn test_is_final_response_long_running_tool_ids() {
346 let mut event = Event::new("inv-123");
347 event.long_running_tool_ids = vec!["process_video".to_string()];
348 event.llm_response.content = Some(Content {
349 role: "model".to_string(),
350 parts: vec![Part::FunctionCall {
351 name: "process_video".to_string(),
352 args: serde_json::json!({"file": "video.mp4"}),
353 id: Some("call_process".to_string()),
354 thought_signature: None,
355 }],
356 });
357 assert!(event.is_final_response());
359 }
360
361 #[test]
362 fn test_function_call_ids() {
363 let mut event = Event::new("inv-123");
364 event.llm_response.content = Some(Content {
365 role: "model".to_string(),
366 parts: vec![
367 Part::FunctionCall {
368 name: "get_weather".to_string(),
369 args: serde_json::json!({}),
370 id: Some("call_1".to_string()),
371 thought_signature: None,
372 },
373 Part::Text { text: "I'll check the weather".to_string() },
374 Part::FunctionCall {
375 name: "get_time".to_string(),
376 args: serde_json::json!({}),
377 id: Some("call_2".to_string()),
378 thought_signature: None,
379 },
380 ],
381 });
382
383 let ids = event.function_call_ids();
384 assert_eq!(ids.len(), 2);
385 assert!(ids.contains(&"call_1".to_string()));
387 assert!(ids.contains(&"call_2".to_string()));
388 }
389
390 #[test]
391 fn test_function_call_ids_falls_back_to_name() {
392 let mut event = Event::new("inv-123");
393 event.llm_response.content = Some(Content {
394 role: "model".to_string(),
395 parts: vec![Part::FunctionCall {
396 name: "get_weather".to_string(),
397 args: serde_json::json!({}),
398 id: None, thought_signature: None,
400 }],
401 });
402
403 let ids = event.function_call_ids();
404 assert_eq!(ids, vec!["get_weather".to_string()]);
405 }
406
407 #[test]
408 fn test_function_call_ids_empty() {
409 let event = Event::new("inv-123");
410 let ids = event.function_call_ids();
411 assert!(ids.is_empty());
412 }
413
414 #[test]
415 fn test_is_final_response_trailing_function_response() {
416 let mut event = Event::new("inv-123");
420 event.llm_response.content = Some(Content {
421 role: "model".to_string(),
422 parts: vec![
423 Part::Text { text: "Running code...".to_string() },
424 Part::FunctionResponse {
425 function_response: crate::FunctionResponseData::new(
426 "code_exec",
427 serde_json::json!({"output": "42"}),
428 ),
429 id: Some("call_exec".to_string()),
430 },
431 ],
432 });
433 assert!(!event.is_final_response());
435 }
436
437 #[test]
438 fn test_is_final_response_text_after_function_response() {
439 let mut event = Event::new("inv-123");
443 event.llm_response.content = Some(Content {
444 role: "model".to_string(),
445 parts: vec![
446 Part::FunctionResponse {
447 function_response: crate::FunctionResponseData::new(
448 "tool",
449 serde_json::json!({}),
450 ),
451 id: Some("call_1".to_string()),
452 },
453 Part::Text { text: "Done".to_string() },
454 ],
455 });
456 assert!(!event.is_final_response());
458 }
459}