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 }],
197 });
198 assert!(!event.is_final_response());
200 }
201
202 #[test]
203 fn test_is_final_response_with_function_response() {
204 let mut event = Event::new("inv-123");
205 event.llm_response.content = Some(Content {
206 role: "function".to_string(),
207 parts: vec![Part::FunctionResponse {
208 name: "get_weather".to_string(),
209 response: serde_json::json!({"temp": 72}),
210 }],
211 });
212 assert!(!event.is_final_response());
214 }
215
216 #[test]
217 fn test_is_final_response_partial() {
218 let mut event = Event::new("inv-123");
219 event.llm_response.partial = true;
220 event.llm_response.content = Some(Content {
221 role: "model".to_string(),
222 parts: vec![Part::Text { text: "Hello...".to_string() }],
223 });
224 assert!(!event.is_final_response());
226 }
227
228 #[test]
229 fn test_is_final_response_skip_summarization() {
230 let mut event = Event::new("inv-123");
231 event.actions.skip_summarization = true;
232 event.llm_response.content = Some(Content {
233 role: "function".to_string(),
234 parts: vec![Part::FunctionResponse {
235 name: "tool".to_string(),
236 response: serde_json::json!({"result": "done"}),
237 }],
238 });
239 assert!(event.is_final_response());
241 }
242
243 #[test]
244 fn test_is_final_response_long_running_tool_ids() {
245 let mut event = Event::new("inv-123");
246 event.long_running_tool_ids = vec!["process_video".to_string()];
247 event.llm_response.content = Some(Content {
248 role: "model".to_string(),
249 parts: vec![Part::FunctionCall {
250 name: "process_video".to_string(),
251 args: serde_json::json!({"file": "video.mp4"}),
252 }],
253 });
254 assert!(event.is_final_response());
256 }
257
258 #[test]
259 fn test_function_call_ids() {
260 let mut event = Event::new("inv-123");
261 event.llm_response.content = Some(Content {
262 role: "model".to_string(),
263 parts: vec![
264 Part::FunctionCall { name: "get_weather".to_string(), args: serde_json::json!({}) },
265 Part::Text { text: "I'll check the weather".to_string() },
266 Part::FunctionCall { name: "get_time".to_string(), args: serde_json::json!({}) },
267 ],
268 });
269
270 let ids = event.function_call_ids();
271 assert_eq!(ids.len(), 2);
272 assert!(ids.contains(&"get_weather".to_string()));
273 assert!(ids.contains(&"get_time".to_string()));
274 }
275
276 #[test]
277 fn test_function_call_ids_empty() {
278 let event = Event::new("inv-123");
279 let ids = event.function_call_ids();
280 assert!(ids.is_empty());
281 }
282}