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 thought_signature: None,
247 }],
248 });
249 assert!(!event.is_final_response());
251 }
252
253 #[test]
254 fn test_is_final_response_with_function_response() {
255 let mut event = Event::new("inv-123");
256 event.llm_response.content = Some(Content {
257 role: "function".to_string(),
258 parts: vec![Part::FunctionResponse {
259 function_response: crate::FunctionResponseData {
260 name: "get_weather".to_string(),
261 response: serde_json::json!({"temp": 72}),
262 },
263 id: Some("call_123".to_string()),
264 }],
265 });
266 assert!(!event.is_final_response());
268 }
269
270 #[test]
271 fn test_is_final_response_partial() {
272 let mut event = Event::new("inv-123");
273 event.llm_response.partial = true;
274 event.llm_response.content = Some(Content {
275 role: "model".to_string(),
276 parts: vec![Part::Text { text: "Hello...".to_string() }],
277 });
278 assert!(!event.is_final_response());
280 }
281
282 #[test]
283 fn test_is_final_response_skip_summarization() {
284 let mut event = Event::new("inv-123");
285 event.actions.skip_summarization = true;
286 event.llm_response.content = Some(Content {
287 role: "function".to_string(),
288 parts: vec![Part::FunctionResponse {
289 function_response: crate::FunctionResponseData {
290 name: "tool".to_string(),
291 response: serde_json::json!({"result": "done"}),
292 },
293 id: Some("call_tool".to_string()),
294 }],
295 });
296 assert!(event.is_final_response());
298 }
299
300 #[test]
301 fn test_is_final_response_long_running_tool_ids() {
302 let mut event = Event::new("inv-123");
303 event.long_running_tool_ids = vec!["process_video".to_string()];
304 event.llm_response.content = Some(Content {
305 role: "model".to_string(),
306 parts: vec![Part::FunctionCall {
307 name: "process_video".to_string(),
308 args: serde_json::json!({"file": "video.mp4"}),
309 id: Some("call_process".to_string()),
310 thought_signature: None,
311 }],
312 });
313 assert!(event.is_final_response());
315 }
316
317 #[test]
318 fn test_function_call_ids() {
319 let mut event = Event::new("inv-123");
320 event.llm_response.content = Some(Content {
321 role: "model".to_string(),
322 parts: vec![
323 Part::FunctionCall {
324 name: "get_weather".to_string(),
325 args: serde_json::json!({}),
326 id: Some("call_1".to_string()),
327 thought_signature: None,
328 },
329 Part::Text { text: "I'll check the weather".to_string() },
330 Part::FunctionCall {
331 name: "get_time".to_string(),
332 args: serde_json::json!({}),
333 id: Some("call_2".to_string()),
334 thought_signature: None,
335 },
336 ],
337 });
338
339 let ids = event.function_call_ids();
340 assert_eq!(ids.len(), 2);
341 assert!(ids.contains(&"call_1".to_string()));
343 assert!(ids.contains(&"call_2".to_string()));
344 }
345
346 #[test]
347 fn test_function_call_ids_falls_back_to_name() {
348 let mut event = Event::new("inv-123");
349 event.llm_response.content = Some(Content {
350 role: "model".to_string(),
351 parts: vec![Part::FunctionCall {
352 name: "get_weather".to_string(),
353 args: serde_json::json!({}),
354 id: None, thought_signature: None,
356 }],
357 });
358
359 let ids = event.function_call_ids();
360 assert_eq!(ids, vec!["get_weather".to_string()]);
361 }
362
363 #[test]
364 fn test_function_call_ids_empty() {
365 let event = Event::new("inv-123");
366 let ids = event.function_call_ids();
367 assert!(ids.is_empty());
368 }
369
370 #[test]
371 fn test_is_final_response_trailing_function_response() {
372 let mut event = Event::new("inv-123");
376 event.llm_response.content = Some(Content {
377 role: "model".to_string(),
378 parts: vec![
379 Part::Text { text: "Running code...".to_string() },
380 Part::FunctionResponse {
381 function_response: crate::FunctionResponseData {
382 name: "code_exec".to_string(),
383 response: serde_json::json!({"output": "42"}),
384 },
385 id: Some("call_exec".to_string()),
386 },
387 ],
388 });
389 assert!(!event.is_final_response());
391 }
392
393 #[test]
394 fn test_is_final_response_text_after_function_response() {
395 let mut event = Event::new("inv-123");
399 event.llm_response.content = Some(Content {
400 role: "model".to_string(),
401 parts: vec![
402 Part::FunctionResponse {
403 function_response: crate::FunctionResponseData {
404 name: "tool".to_string(),
405 response: serde_json::json!({}),
406 },
407 id: Some("call_1".to_string()),
408 },
409 Part::Text { text: "Done".to_string() },
410 ],
411 });
412 assert!(!event.is_final_response());
414 }
415}