1use crate::model::LlmResponse;
2use crate::types::Content;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use uuid::Uuid;
7
8pub const KEY_PREFIX_APP: &str = "app:";
10pub const KEY_PREFIX_TEMP: &str = "temp:";
11pub const KEY_PREFIX_USER: &str = "user:";
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Event {
17 pub id: String,
18 pub timestamp: DateTime<Utc>,
19 pub invocation_id: String,
20 pub branch: String,
21 pub author: String,
22 #[serde(flatten)]
25 pub llm_response: LlmResponse,
26 pub actions: EventActions,
27 #[serde(default)]
29 pub long_running_tool_ids: Vec<String>,
30}
31
32#[derive(Debug, Clone, Default, Serialize, Deserialize)]
33pub struct EventActions {
34 pub state_delta: HashMap<String, serde_json::Value>,
35 pub artifact_delta: HashMap<String, i64>,
36 pub skip_summarization: bool,
37 pub transfer_to_agent: Option<String>,
38 pub escalate: bool,
39}
40
41impl Event {
42 pub fn new(invocation_id: impl Into<String>) -> Self {
43 Self {
44 id: Uuid::new_v4().to_string(),
45 timestamp: Utc::now(),
46 invocation_id: invocation_id.into(),
47 branch: String::new(),
48 author: String::new(),
49 llm_response: LlmResponse::default(),
50 actions: EventActions::default(),
51 long_running_tool_ids: Vec::new(),
52 }
53 }
54
55 pub fn content(&self) -> Option<&Content> {
57 self.llm_response.content.as_ref()
58 }
59
60 pub fn set_content(&mut self, content: Content) {
62 self.llm_response.content = Some(content);
63 }
64
65 pub fn is_final_response(&self) -> bool {
76 if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
78 return true;
79 }
80
81 let has_function_calls = self.has_function_calls();
83 let has_function_responses = self.has_function_responses();
84 let is_partial = self.llm_response.partial;
85 let has_trailing_code_result = self.has_trailing_code_execution_result();
86
87 !has_function_calls && !has_function_responses && !is_partial && !has_trailing_code_result
88 }
89
90 fn has_function_calls(&self) -> bool {
92 if let Some(content) = &self.llm_response.content {
93 for part in &content.parts {
94 if matches!(part, crate::Part::FunctionCall { .. }) {
95 return true;
96 }
97 }
98 }
99 false
100 }
101
102 fn has_function_responses(&self) -> bool {
104 if let Some(content) = &self.llm_response.content {
105 for part in &content.parts {
106 if matches!(part, crate::Part::FunctionResponse { .. }) {
107 return true;
108 }
109 }
110 }
111 false
112 }
113
114 #[allow(clippy::match_like_matches_macro)]
118 fn has_trailing_code_execution_result(&self) -> bool {
119 false
126 }
127
128 pub fn function_call_ids(&self) -> Vec<String> {
131 let mut ids = Vec::new();
132 if let Some(content) = &self.llm_response.content {
133 for part in &content.parts {
134 if let crate::Part::FunctionCall { name, .. } = part {
135 ids.push(name.clone());
137 }
138 }
139 }
140 ids
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use crate::Part;
148
149 #[test]
150 fn test_event_creation() {
151 let event = Event::new("inv-123");
152 assert_eq!(event.invocation_id, "inv-123");
153 assert!(!event.id.is_empty());
154 }
155
156 #[test]
157 fn test_event_actions_default() {
158 let actions = EventActions::default();
159 assert!(actions.state_delta.is_empty());
160 assert!(!actions.skip_summarization);
161 }
162
163 #[test]
164 fn test_state_prefixes() {
165 assert_eq!(KEY_PREFIX_APP, "app:");
166 assert_eq!(KEY_PREFIX_TEMP, "temp:");
167 assert_eq!(KEY_PREFIX_USER, "user:");
168 }
169
170 #[test]
171 fn test_is_final_response_no_content() {
172 let event = Event::new("inv-123");
173 assert!(event.is_final_response());
175 }
176
177 #[test]
178 fn test_is_final_response_text_only() {
179 let mut event = Event::new("inv-123");
180 event.llm_response.content = Some(Content {
181 role: "model".to_string(),
182 parts: vec![Part::Text { text: "Hello!".to_string() }],
183 });
184 assert!(event.is_final_response());
186 }
187
188 #[test]
189 fn test_is_final_response_with_function_call() {
190 let mut event = Event::new("inv-123");
191 event.llm_response.content = Some(Content {
192 role: "model".to_string(),
193 parts: vec![Part::FunctionCall {
194 name: "get_weather".to_string(),
195 args: serde_json::json!({"city": "NYC"}),
196 id: Some("call_123".to_string()),
197 }],
198 });
199 assert!(!event.is_final_response());
201 }
202
203 #[test]
204 fn test_is_final_response_with_function_response() {
205 let mut event = Event::new("inv-123");
206 event.llm_response.content = Some(Content {
207 role: "function".to_string(),
208 parts: vec![Part::FunctionResponse {
209 name: "get_weather".to_string(),
210 response: serde_json::json!({"temp": 72}),
211 id: Some("call_123".to_string()),
212 }],
213 });
214 assert!(!event.is_final_response());
216 }
217
218 #[test]
219 fn test_is_final_response_partial() {
220 let mut event = Event::new("inv-123");
221 event.llm_response.partial = true;
222 event.llm_response.content = Some(Content {
223 role: "model".to_string(),
224 parts: vec![Part::Text { text: "Hello...".to_string() }],
225 });
226 assert!(!event.is_final_response());
228 }
229
230 #[test]
231 fn test_is_final_response_skip_summarization() {
232 let mut event = Event::new("inv-123");
233 event.actions.skip_summarization = true;
234 event.llm_response.content = Some(Content {
235 role: "function".to_string(),
236 parts: vec![Part::FunctionResponse {
237 name: "tool".to_string(),
238 response: serde_json::json!({"result": "done"}),
239 id: Some("call_tool".to_string()),
240 }],
241 });
242 assert!(event.is_final_response());
244 }
245
246 #[test]
247 fn test_is_final_response_long_running_tool_ids() {
248 let mut event = Event::new("inv-123");
249 event.long_running_tool_ids = vec!["process_video".to_string()];
250 event.llm_response.content = Some(Content {
251 role: "model".to_string(),
252 parts: vec![Part::FunctionCall {
253 name: "process_video".to_string(),
254 args: serde_json::json!({"file": "video.mp4"}),
255 id: Some("call_process".to_string()),
256 }],
257 });
258 assert!(event.is_final_response());
260 }
261
262 #[test]
263 fn test_function_call_ids() {
264 let mut event = Event::new("inv-123");
265 event.llm_response.content = Some(Content {
266 role: "model".to_string(),
267 parts: vec![
268 Part::FunctionCall {
269 name: "get_weather".to_string(),
270 args: serde_json::json!({}),
271 id: Some("call_1".to_string()),
272 },
273 Part::Text { text: "I'll check the weather".to_string() },
274 Part::FunctionCall {
275 name: "get_time".to_string(),
276 args: serde_json::json!({}),
277 id: Some("call_2".to_string()),
278 },
279 ],
280 });
281
282 let ids = event.function_call_ids();
283 assert_eq!(ids.len(), 2);
284 assert!(ids.contains(&"get_weather".to_string()));
285 assert!(ids.contains(&"get_time".to_string()));
286 }
287
288 #[test]
289 fn test_function_call_ids_empty() {
290 let event = Event::new("inv-123");
291 let ids = event.function_call_ids();
292 assert!(ids.is_empty());
293 }
294}