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}
85
86impl Event {
87 pub fn new(invocation_id: impl Into<String>) -> Self {
89 Self {
90 id: Uuid::new_v4().to_string(),
91 timestamp: Utc::now(),
92 invocation_id: invocation_id.into(),
93 branch: String::new(),
94 author: String::new(),
95 llm_response: LlmResponse::default(),
96 actions: EventActions::default(),
97 long_running_tool_ids: Vec::new(),
98 llm_request: None,
99 provider_metadata: HashMap::new(),
100 }
101 }
102
103 pub fn with_id(id: impl Into<String>, invocation_id: impl Into<String>) -> Self {
106 Self {
107 id: id.into(),
108 timestamp: Utc::now(),
109 invocation_id: invocation_id.into(),
110 branch: String::new(),
111 author: String::new(),
112 llm_response: LlmResponse::default(),
113 actions: EventActions::default(),
114 long_running_tool_ids: Vec::new(),
115 llm_request: None,
116 provider_metadata: HashMap::new(),
117 }
118 }
119
120 pub fn content(&self) -> Option<&Content> {
122 self.llm_response.content.as_ref()
123 }
124
125 pub fn set_content(&mut self, content: Content) {
127 self.llm_response.content = Some(content);
128 }
129
130 pub fn is_final_response(&self) -> bool {
141 if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
143 return true;
144 }
145
146 let has_function_calls = self.has_function_calls();
148 let has_function_responses = self.has_function_responses();
149 let is_partial = self.llm_response.partial;
150 let has_trailing_code_result = self.has_trailing_code_execution_result();
151
152 !has_function_calls && !has_function_responses && !is_partial && !has_trailing_code_result
153 }
154
155 fn has_function_calls(&self) -> bool {
157 if let Some(content) = &self.llm_response.content {
158 for part in &content.parts {
159 if matches!(part, crate::Part::FunctionCall { .. }) {
160 return true;
161 }
162 }
163 }
164 false
165 }
166
167 fn has_function_responses(&self) -> bool {
169 if let Some(content) = &self.llm_response.content {
170 for part in &content.parts {
171 if matches!(part, crate::Part::FunctionResponse { .. }) {
172 return true;
173 }
174 }
175 }
176 false
177 }
178
179 #[allow(clippy::match_like_matches_macro)]
181 fn has_trailing_code_execution_result(&self) -> bool {
182 if let Some(content) = &self.llm_response.content {
183 if let Some(last_part) = content.parts.last() {
184 return matches!(last_part, crate::Part::FunctionResponse { .. });
187 }
188 }
189 false
190 }
191
192 pub fn function_call_ids(&self) -> Vec<String> {
195 let mut ids = Vec::new();
196 if let Some(content) = &self.llm_response.content {
197 for part in &content.parts {
198 if let crate::Part::FunctionCall { name, id, .. } = part {
199 ids.push(id.as_deref().unwrap_or(name).to_string());
202 }
203 }
204 }
205 ids
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::Part;
213
214 #[test]
215 fn test_event_creation() {
216 let event = Event::new("inv-123");
217 assert_eq!(event.invocation_id, "inv-123");
218 assert!(!event.id.is_empty());
219 }
220
221 #[test]
222 fn test_event_actions_default() {
223 let actions = EventActions::default();
224 assert!(actions.state_delta.is_empty());
225 assert!(!actions.skip_summarization);
226 assert!(actions.tool_confirmation.is_none());
227 assert!(actions.tool_confirmation_decision.is_none());
228 }
229
230 #[test]
231 fn test_state_prefixes() {
232 assert_eq!(KEY_PREFIX_APP, "app:");
233 assert_eq!(KEY_PREFIX_TEMP, "temp:");
234 assert_eq!(KEY_PREFIX_USER, "user:");
235 }
236
237 #[test]
238 fn test_is_final_response_no_content() {
239 let event = Event::new("inv-123");
240 assert!(event.is_final_response());
242 }
243
244 #[test]
245 fn test_is_final_response_text_only() {
246 let mut event = Event::new("inv-123");
247 event.llm_response.content = Some(Content {
248 role: "model".to_string(),
249 parts: vec![Part::Text { text: "Hello!".to_string() }],
250 });
251 assert!(event.is_final_response());
253 }
254
255 #[test]
256 fn test_is_final_response_with_function_call() {
257 let mut event = Event::new("inv-123");
258 event.llm_response.content = Some(Content {
259 role: "model".to_string(),
260 parts: vec![Part::FunctionCall {
261 name: "get_weather".to_string(),
262 args: serde_json::json!({"city": "NYC"}),
263 id: Some("call_123".to_string()),
264 thought_signature: None,
265 }],
266 });
267 assert!(!event.is_final_response());
269 }
270
271 #[test]
272 fn test_is_final_response_with_function_response() {
273 let mut event = Event::new("inv-123");
274 event.llm_response.content = Some(Content {
275 role: "function".to_string(),
276 parts: vec![Part::FunctionResponse {
277 function_response: crate::FunctionResponseData::new(
278 "get_weather",
279 serde_json::json!({"temp": 72}),
280 ),
281 id: Some("call_123".to_string()),
282 }],
283 });
284 assert!(!event.is_final_response());
286 }
287
288 #[test]
289 fn test_is_final_response_partial() {
290 let mut event = Event::new("inv-123");
291 event.llm_response.partial = true;
292 event.llm_response.content = Some(Content {
293 role: "model".to_string(),
294 parts: vec![Part::Text { text: "Hello...".to_string() }],
295 });
296 assert!(!event.is_final_response());
298 }
299
300 #[test]
301 fn test_is_final_response_skip_summarization() {
302 let mut event = Event::new("inv-123");
303 event.actions.skip_summarization = true;
304 event.llm_response.content = Some(Content {
305 role: "function".to_string(),
306 parts: vec![Part::FunctionResponse {
307 function_response: crate::FunctionResponseData::new(
308 "tool",
309 serde_json::json!({"result": "done"}),
310 ),
311 id: Some("call_tool".to_string()),
312 }],
313 });
314 assert!(event.is_final_response());
316 }
317
318 #[test]
319 fn test_is_final_response_long_running_tool_ids() {
320 let mut event = Event::new("inv-123");
321 event.long_running_tool_ids = vec!["process_video".to_string()];
322 event.llm_response.content = Some(Content {
323 role: "model".to_string(),
324 parts: vec![Part::FunctionCall {
325 name: "process_video".to_string(),
326 args: serde_json::json!({"file": "video.mp4"}),
327 id: Some("call_process".to_string()),
328 thought_signature: None,
329 }],
330 });
331 assert!(event.is_final_response());
333 }
334
335 #[test]
336 fn test_function_call_ids() {
337 let mut event = Event::new("inv-123");
338 event.llm_response.content = Some(Content {
339 role: "model".to_string(),
340 parts: vec![
341 Part::FunctionCall {
342 name: "get_weather".to_string(),
343 args: serde_json::json!({}),
344 id: Some("call_1".to_string()),
345 thought_signature: None,
346 },
347 Part::Text { text: "I'll check the weather".to_string() },
348 Part::FunctionCall {
349 name: "get_time".to_string(),
350 args: serde_json::json!({}),
351 id: Some("call_2".to_string()),
352 thought_signature: None,
353 },
354 ],
355 });
356
357 let ids = event.function_call_ids();
358 assert_eq!(ids.len(), 2);
359 assert!(ids.contains(&"call_1".to_string()));
361 assert!(ids.contains(&"call_2".to_string()));
362 }
363
364 #[test]
365 fn test_function_call_ids_falls_back_to_name() {
366 let mut event = Event::new("inv-123");
367 event.llm_response.content = Some(Content {
368 role: "model".to_string(),
369 parts: vec![Part::FunctionCall {
370 name: "get_weather".to_string(),
371 args: serde_json::json!({}),
372 id: None, thought_signature: None,
374 }],
375 });
376
377 let ids = event.function_call_ids();
378 assert_eq!(ids, vec!["get_weather".to_string()]);
379 }
380
381 #[test]
382 fn test_function_call_ids_empty() {
383 let event = Event::new("inv-123");
384 let ids = event.function_call_ids();
385 assert!(ids.is_empty());
386 }
387
388 #[test]
389 fn test_is_final_response_trailing_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::Text { text: "Running code...".to_string() },
398 Part::FunctionResponse {
399 function_response: crate::FunctionResponseData::new(
400 "code_exec",
401 serde_json::json!({"output": "42"}),
402 ),
403 id: Some("call_exec".to_string()),
404 },
405 ],
406 });
407 assert!(!event.is_final_response());
409 }
410
411 #[test]
412 fn test_is_final_response_text_after_function_response() {
413 let mut event = Event::new("inv-123");
417 event.llm_response.content = Some(Content {
418 role: "model".to_string(),
419 parts: vec![
420 Part::FunctionResponse {
421 function_response: crate::FunctionResponseData::new(
422 "tool",
423 serde_json::json!({}),
424 ),
425 id: Some("call_1".to_string()),
426 },
427 Part::Text { text: "Done".to_string() },
428 ],
429 });
430 assert!(!event.is_final_response());
432 }
433}