adk_core/
event.rs

1use crate::model::LlmResponse;
2use crate::types::Content;
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use uuid::Uuid;
7
8// State scope prefixes
9pub const KEY_PREFIX_APP: &str = "app:";
10pub const KEY_PREFIX_TEMP: &str = "temp:";
11pub const KEY_PREFIX_USER: &str = "user:";
12
13/// Event represents a single interaction in a conversation.
14/// This struct embeds LlmResponse to match ADK-Go's design pattern.
15#[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    /// The LLM response containing content and metadata.
23    /// Access content via `event.llm_response.content`.
24    #[serde(flatten)]
25    pub llm_response: LlmResponse,
26    pub actions: EventActions,
27    /// IDs of long-running tools associated with this event.
28    #[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    /// Convenience method to access content directly.
56    pub fn content(&self) -> Option<&Content> {
57        self.llm_response.content.as_ref()
58    }
59
60    /// Convenience method to set content directly.
61    pub fn set_content(&mut self, content: Content) {
62        self.llm_response.content = Some(content);
63    }
64
65    /// Returns whether the event is the final response of an agent.
66    ///
67    /// An event is considered final if:
68    /// - It has skip_summarization set, OR
69    /// - It has long_running_tool_ids (indicating async operations), OR
70    /// - It has no function calls, no function responses, is not partial,
71    ///   and has no trailing code execution results.
72    ///
73    /// Note: When multiple agents participate in one invocation, there could be
74    /// multiple events with is_final_response() as true, for each participating agent.
75    pub fn is_final_response(&self) -> bool {
76        // If skip_summarization is set or we have long-running tools, it's final
77        if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
78            return true;
79        }
80
81        // Check content for function calls/responses
82        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    /// Returns true if the event content contains function calls.
91    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    /// Returns true if the event content contains function responses.
103    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    /// Returns true if the event has a trailing code execution result.
115    /// Note: CodeExecutionResult is not yet implemented in the Part enum,
116    /// so this always returns false for now.
117    #[allow(clippy::match_like_matches_macro)]
118    fn has_trailing_code_execution_result(&self) -> bool {
119        // TODO: Implement when CodeExecutionResult is added to Part enum
120        // if let Some(content) = &self.llm_response.content {
121        //     if let Some(last_part) = content.parts.last() {
122        //         return matches!(last_part, crate::Part::CodeExecutionResult { .. });
123        //     }
124        // }
125        false
126    }
127
128    /// Extracts function call IDs from this event's content.
129    /// Used to identify which function calls are associated with long-running tools.
130    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                    // Use name as ID since we don't have explicit IDs in our Part enum
136                    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        // No content, no function calls -> final
174        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        // Text only, no function calls -> final
185        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        // Has function call -> NOT final (need to execute it)
200        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        // Has function response -> NOT final (model needs to respond)
215        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        // Partial response -> NOT final
227        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        // Even with function response, skip_summarization makes it final
243        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        // Has long_running_tool_ids -> final (async operation started)
259        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}