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:";
11pub const KEY_PREFIX_TEMP: &str = "temp:";
12pub const KEY_PREFIX_USER: &str = "user:";
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Event {
18 pub id: String,
19 pub timestamp: DateTime<Utc>,
20 pub invocation_id: String,
21 pub branch: String,
22 pub author: String,
23 #[serde(flatten)]
26 pub llm_response: LlmResponse,
27 pub actions: EventActions,
28 #[serde(default)]
30 pub long_running_tool_ids: Vec<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub llm_request: Option<String>,
34 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
37 pub provider_metadata: HashMap<String, String>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct EventCompaction {
45 pub start_timestamp: DateTime<Utc>,
47 pub end_timestamp: DateTime<Utc>,
49 pub compacted_content: Content,
51}
52
53#[derive(Debug, Clone, Default, Serialize, Deserialize)]
54pub struct EventActions {
55 pub state_delta: HashMap<String, serde_json::Value>,
56 pub artifact_delta: HashMap<String, i64>,
57 pub skip_summarization: bool,
58 pub transfer_to_agent: Option<String>,
59 pub escalate: bool,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub tool_confirmation: Option<ToolConfirmationRequest>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub tool_confirmation_decision: Option<ToolConfirmationDecision>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub compaction: Option<EventCompaction>,
67}
68
69impl Event {
70 pub fn new(invocation_id: impl Into<String>) -> Self {
71 Self {
72 id: Uuid::new_v4().to_string(),
73 timestamp: Utc::now(),
74 invocation_id: invocation_id.into(),
75 branch: String::new(),
76 author: String::new(),
77 llm_response: LlmResponse::default(),
78 actions: EventActions::default(),
79 long_running_tool_ids: Vec::new(),
80 llm_request: None,
81 provider_metadata: HashMap::new(),
82 }
83 }
84
85 pub fn with_id(id: impl Into<String>, invocation_id: impl Into<String>) -> Self {
88 Self {
89 id: id.into(),
90 timestamp: Utc::now(),
91 invocation_id: invocation_id.into(),
92 branch: String::new(),
93 author: String::new(),
94 llm_response: LlmResponse::default(),
95 actions: EventActions::default(),
96 long_running_tool_ids: Vec::new(),
97 llm_request: None,
98 provider_metadata: HashMap::new(),
99 }
100 }
101
102 pub fn content(&self) -> Option<&Content> {
104 self.llm_response.content.as_ref()
105 }
106
107 pub fn set_content(&mut self, content: Content) {
109 self.llm_response.content = Some(content);
110 }
111
112 pub fn is_final_response(&self) -> bool {
123 if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
125 return true;
126 }
127
128 let has_function_calls = self.has_function_calls();
130 let has_function_responses = self.has_function_responses();
131 let is_partial = self.llm_response.partial;
132 let has_trailing_code_result = self.has_trailing_code_execution_result();
133
134 !has_function_calls && !has_function_responses && !is_partial && !has_trailing_code_result
135 }
136
137 fn has_function_calls(&self) -> bool {
139 if let Some(content) = &self.llm_response.content {
140 for part in &content.parts {
141 if matches!(part, crate::Part::FunctionCall { .. }) {
142 return true;
143 }
144 }
145 }
146 false
147 }
148
149 fn has_function_responses(&self) -> bool {
151 if let Some(content) = &self.llm_response.content {
152 for part in &content.parts {
153 if matches!(part, crate::Part::FunctionResponse { .. }) {
154 return true;
155 }
156 }
157 }
158 false
159 }
160
161 #[allow(clippy::match_like_matches_macro)]
163 fn has_trailing_code_execution_result(&self) -> bool {
164 if let Some(content) = &self.llm_response.content {
165 if let Some(last_part) = content.parts.last() {
166 return matches!(last_part, crate::Part::FunctionResponse { .. });
169 }
170 }
171 false
172 }
173
174 pub fn function_call_ids(&self) -> Vec<String> {
177 let mut ids = Vec::new();
178 if let Some(content) = &self.llm_response.content {
179 for part in &content.parts {
180 if let crate::Part::FunctionCall { name, id, .. } = part {
181 ids.push(id.as_deref().unwrap_or(name).to_string());
184 }
185 }
186 }
187 ids
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::Part;
195
196 #[test]
197 fn test_event_creation() {
198 let event = Event::new("inv-123");
199 assert_eq!(event.invocation_id, "inv-123");
200 assert!(!event.id.is_empty());
201 }
202
203 #[test]
204 fn test_event_actions_default() {
205 let actions = EventActions::default();
206 assert!(actions.state_delta.is_empty());
207 assert!(!actions.skip_summarization);
208 assert!(actions.tool_confirmation.is_none());
209 assert!(actions.tool_confirmation_decision.is_none());
210 }
211
212 #[test]
213 fn test_state_prefixes() {
214 assert_eq!(KEY_PREFIX_APP, "app:");
215 assert_eq!(KEY_PREFIX_TEMP, "temp:");
216 assert_eq!(KEY_PREFIX_USER, "user:");
217 }
218
219 #[test]
220 fn test_is_final_response_no_content() {
221 let event = Event::new("inv-123");
222 assert!(event.is_final_response());
224 }
225
226 #[test]
227 fn test_is_final_response_text_only() {
228 let mut event = Event::new("inv-123");
229 event.llm_response.content = Some(Content {
230 role: "model".to_string(),
231 parts: vec![Part::Text { text: "Hello!".to_string() }],
232 });
233 assert!(event.is_final_response());
235 }
236
237 #[test]
238 fn test_is_final_response_with_function_call() {
239 let mut event = Event::new("inv-123");
240 event.llm_response.content = Some(Content {
241 role: "model".to_string(),
242 parts: vec![Part::FunctionCall {
243 name: "get_weather".to_string(),
244 args: serde_json::json!({"city": "NYC"}),
245 id: Some("call_123".to_string()),
246 }],
247 });
248 assert!(!event.is_final_response());
250 }
251
252 #[test]
253 fn test_is_final_response_with_function_response() {
254 let mut event = Event::new("inv-123");
255 event.llm_response.content = Some(Content {
256 role: "function".to_string(),
257 parts: vec![Part::FunctionResponse {
258 function_response: crate::FunctionResponseData {
259 name: "get_weather".to_string(),
260 response: serde_json::json!({"temp": 72}),
261 },
262 id: Some("call_123".to_string()),
263 }],
264 });
265 assert!(!event.is_final_response());
267 }
268
269 #[test]
270 fn test_is_final_response_partial() {
271 let mut event = Event::new("inv-123");
272 event.llm_response.partial = true;
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_skip_summarization() {
283 let mut event = Event::new("inv-123");
284 event.actions.skip_summarization = true;
285 event.llm_response.content = Some(Content {
286 role: "function".to_string(),
287 parts: vec![Part::FunctionResponse {
288 function_response: crate::FunctionResponseData {
289 name: "tool".to_string(),
290 response: serde_json::json!({"result": "done"}),
291 },
292 id: Some("call_tool".to_string()),
293 }],
294 });
295 assert!(event.is_final_response());
297 }
298
299 #[test]
300 fn test_is_final_response_long_running_tool_ids() {
301 let mut event = Event::new("inv-123");
302 event.long_running_tool_ids = vec!["process_video".to_string()];
303 event.llm_response.content = Some(Content {
304 role: "model".to_string(),
305 parts: vec![Part::FunctionCall {
306 name: "process_video".to_string(),
307 args: serde_json::json!({"file": "video.mp4"}),
308 id: Some("call_process".to_string()),
309 }],
310 });
311 assert!(event.is_final_response());
313 }
314
315 #[test]
316 fn test_function_call_ids() {
317 let mut event = Event::new("inv-123");
318 event.llm_response.content = Some(Content {
319 role: "model".to_string(),
320 parts: vec![
321 Part::FunctionCall {
322 name: "get_weather".to_string(),
323 args: serde_json::json!({}),
324 id: Some("call_1".to_string()),
325 },
326 Part::Text { text: "I'll check the weather".to_string() },
327 Part::FunctionCall {
328 name: "get_time".to_string(),
329 args: serde_json::json!({}),
330 id: Some("call_2".to_string()),
331 },
332 ],
333 });
334
335 let ids = event.function_call_ids();
336 assert_eq!(ids.len(), 2);
337 assert!(ids.contains(&"call_1".to_string()));
339 assert!(ids.contains(&"call_2".to_string()));
340 }
341
342 #[test]
343 fn test_function_call_ids_falls_back_to_name() {
344 let mut event = Event::new("inv-123");
345 event.llm_response.content = Some(Content {
346 role: "model".to_string(),
347 parts: vec![Part::FunctionCall {
348 name: "get_weather".to_string(),
349 args: serde_json::json!({}),
350 id: None, }],
352 });
353
354 let ids = event.function_call_ids();
355 assert_eq!(ids, vec!["get_weather".to_string()]);
356 }
357
358 #[test]
359 fn test_function_call_ids_empty() {
360 let event = Event::new("inv-123");
361 let ids = event.function_call_ids();
362 assert!(ids.is_empty());
363 }
364
365 #[test]
366 fn test_is_final_response_trailing_function_response() {
367 let mut event = Event::new("inv-123");
371 event.llm_response.content = Some(Content {
372 role: "model".to_string(),
373 parts: vec![
374 Part::Text { text: "Running code...".to_string() },
375 Part::FunctionResponse {
376 function_response: crate::FunctionResponseData {
377 name: "code_exec".to_string(),
378 response: serde_json::json!({"output": "42"}),
379 },
380 id: Some("call_exec".to_string()),
381 },
382 ],
383 });
384 assert!(!event.is_final_response());
386 }
387
388 #[test]
389 fn test_is_final_response_text_after_function_response() {
390 let mut event = Event::new("inv-123");
394 event.llm_response.content = Some(Content {
395 role: "model".to_string(),
396 parts: vec![
397 Part::FunctionResponse {
398 function_response: crate::FunctionResponseData {
399 name: "tool".to_string(),
400 response: serde_json::json!({}),
401 },
402 id: Some("call_1".to_string()),
403 },
404 Part::Text { text: "Done".to_string() },
405 ],
406 });
407 assert!(!event.is_final_response());
409 }
410}