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    /// Camel case version for UI compatibility
21    #[serde(rename = "invocationId")]
22    pub invocation_id_camel: String,
23    pub branch: String,
24    pub author: String,
25    /// The LLM response containing content and metadata.
26    /// Access content via `event.llm_response.content`.
27    #[serde(flatten)]
28    pub llm_response: LlmResponse,
29    pub actions: EventActions,
30    /// IDs of long-running tools associated with this event.
31    #[serde(default)]
32    pub long_running_tool_ids: Vec<String>,
33    /// LLM request data for UI display (JSON string)
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub llm_request: Option<String>,
36    /// GCP Vertex Agent LLM request (for UI compatibility)
37    #[serde(rename = "gcp.vertex.agent.llm_request", skip_serializing_if = "Option::is_none")]
38    pub gcp_llm_request: Option<String>,
39    /// GCP Vertex Agent LLM response (for UI compatibility)
40    #[serde(rename = "gcp.vertex.agent.llm_response", skip_serializing_if = "Option::is_none")]
41    pub gcp_llm_response: Option<String>,
42}
43
44#[derive(Debug, Clone, Default, Serialize, Deserialize)]
45pub struct EventActions {
46    pub state_delta: HashMap<String, serde_json::Value>,
47    pub artifact_delta: HashMap<String, i64>,
48    pub skip_summarization: bool,
49    pub transfer_to_agent: Option<String>,
50    pub escalate: bool,
51}
52
53impl Event {
54    pub fn new(invocation_id: impl Into<String>) -> Self {
55        let invocation_id = invocation_id.into();
56        Self {
57            id: Uuid::new_v4().to_string(),
58            timestamp: Utc::now(),
59            invocation_id: invocation_id.clone(),
60            invocation_id_camel: invocation_id,
61            branch: String::new(),
62            author: String::new(),
63            llm_response: LlmResponse::default(),
64            actions: EventActions::default(),
65            long_running_tool_ids: Vec::new(),
66            llm_request: None,
67            gcp_llm_request: None,
68            gcp_llm_response: None,
69        }
70    }
71
72    /// Create an event with a specific ID.
73    /// Use this for streaming events where all chunks should share the same event ID.
74    pub fn with_id(id: impl Into<String>, invocation_id: impl Into<String>) -> Self {
75        let invocation_id = invocation_id.into();
76        Self {
77            id: id.into(),
78            timestamp: Utc::now(),
79            invocation_id: invocation_id.clone(),
80            invocation_id_camel: invocation_id,
81            branch: String::new(),
82            author: String::new(),
83            llm_response: LlmResponse::default(),
84            actions: EventActions::default(),
85            long_running_tool_ids: Vec::new(),
86            llm_request: None,
87            gcp_llm_request: None,
88            gcp_llm_response: None,
89        }
90    }
91
92    /// Convenience method to access content directly.
93    pub fn content(&self) -> Option<&Content> {
94        self.llm_response.content.as_ref()
95    }
96
97    /// Convenience method to set content directly.
98    pub fn set_content(&mut self, content: Content) {
99        self.llm_response.content = Some(content);
100    }
101
102    /// Returns whether the event is the final response of an agent.
103    ///
104    /// An event is considered final if:
105    /// - It has skip_summarization set, OR
106    /// - It has long_running_tool_ids (indicating async operations), OR
107    /// - It has no function calls, no function responses, is not partial,
108    ///   and has no trailing code execution results.
109    ///
110    /// Note: When multiple agents participate in one invocation, there could be
111    /// multiple events with is_final_response() as true, for each participating agent.
112    pub fn is_final_response(&self) -> bool {
113        // If skip_summarization is set or we have long-running tools, it's final
114        if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
115            return true;
116        }
117
118        // Check content for function calls/responses
119        let has_function_calls = self.has_function_calls();
120        let has_function_responses = self.has_function_responses();
121        let is_partial = self.llm_response.partial;
122        let has_trailing_code_result = self.has_trailing_code_execution_result();
123
124        !has_function_calls && !has_function_responses && !is_partial && !has_trailing_code_result
125    }
126
127    /// Returns true if the event content contains function calls.
128    fn has_function_calls(&self) -> bool {
129        if let Some(content) = &self.llm_response.content {
130            for part in &content.parts {
131                if matches!(part, crate::Part::FunctionCall { .. }) {
132                    return true;
133                }
134            }
135        }
136        false
137    }
138
139    /// Returns true if the event content contains function responses.
140    fn has_function_responses(&self) -> bool {
141        if let Some(content) = &self.llm_response.content {
142            for part in &content.parts {
143                if matches!(part, crate::Part::FunctionResponse { .. }) {
144                    return true;
145                }
146            }
147        }
148        false
149    }
150
151    /// Returns true if the event has a trailing code execution result.
152    /// Note: CodeExecutionResult is not yet implemented in the Part enum,
153    /// so this always returns false for now.
154    #[allow(clippy::match_like_matches_macro)]
155    fn has_trailing_code_execution_result(&self) -> bool {
156        // TODO: Implement when CodeExecutionResult is added to Part enum
157        // if let Some(content) = &self.llm_response.content {
158        //     if let Some(last_part) = content.parts.last() {
159        //         return matches!(last_part, crate::Part::CodeExecutionResult { .. });
160        //     }
161        // }
162        false
163    }
164
165    /// Extracts function call IDs from this event's content.
166    /// Used to identify which function calls are associated with long-running tools.
167    pub fn function_call_ids(&self) -> Vec<String> {
168        let mut ids = Vec::new();
169        if let Some(content) = &self.llm_response.content {
170            for part in &content.parts {
171                if let crate::Part::FunctionCall { name, .. } = part {
172                    // Use name as ID since we don't have explicit IDs in our Part enum
173                    ids.push(name.clone());
174                }
175            }
176        }
177        ids
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::Part;
185
186    #[test]
187    fn test_event_creation() {
188        let event = Event::new("inv-123");
189        assert_eq!(event.invocation_id, "inv-123");
190        assert!(!event.id.is_empty());
191    }
192
193    #[test]
194    fn test_event_actions_default() {
195        let actions = EventActions::default();
196        assert!(actions.state_delta.is_empty());
197        assert!(!actions.skip_summarization);
198    }
199
200    #[test]
201    fn test_state_prefixes() {
202        assert_eq!(KEY_PREFIX_APP, "app:");
203        assert_eq!(KEY_PREFIX_TEMP, "temp:");
204        assert_eq!(KEY_PREFIX_USER, "user:");
205    }
206
207    #[test]
208    fn test_is_final_response_no_content() {
209        let event = Event::new("inv-123");
210        // No content, no function calls -> final
211        assert!(event.is_final_response());
212    }
213
214    #[test]
215    fn test_is_final_response_text_only() {
216        let mut event = Event::new("inv-123");
217        event.llm_response.content = Some(Content {
218            role: "model".to_string(),
219            parts: vec![Part::Text { text: "Hello!".to_string() }],
220        });
221        // Text only, no function calls -> final
222        assert!(event.is_final_response());
223    }
224
225    #[test]
226    fn test_is_final_response_with_function_call() {
227        let mut event = Event::new("inv-123");
228        event.llm_response.content = Some(Content {
229            role: "model".to_string(),
230            parts: vec![Part::FunctionCall {
231                name: "get_weather".to_string(),
232                args: serde_json::json!({"city": "NYC"}),
233                id: Some("call_123".to_string()),
234            }],
235        });
236        // Has function call -> NOT final (need to execute it)
237        assert!(!event.is_final_response());
238    }
239
240    #[test]
241    fn test_is_final_response_with_function_response() {
242        let mut event = Event::new("inv-123");
243        event.llm_response.content = Some(Content {
244            role: "function".to_string(),
245            parts: vec![Part::FunctionResponse {
246                function_response: crate::FunctionResponseData {
247                    name: "get_weather".to_string(),
248                    response: serde_json::json!({"temp": 72}),
249                },
250                id: Some("call_123".to_string()),
251            }],
252        });
253        // Has function response -> NOT final (model needs to respond)
254        assert!(!event.is_final_response());
255    }
256
257    #[test]
258    fn test_is_final_response_partial() {
259        let mut event = Event::new("inv-123");
260        event.llm_response.partial = true;
261        event.llm_response.content = Some(Content {
262            role: "model".to_string(),
263            parts: vec![Part::Text { text: "Hello...".to_string() }],
264        });
265        // Partial response -> NOT final
266        assert!(!event.is_final_response());
267    }
268
269    #[test]
270    fn test_is_final_response_skip_summarization() {
271        let mut event = Event::new("inv-123");
272        event.actions.skip_summarization = true;
273        event.llm_response.content = Some(Content {
274            role: "function".to_string(),
275            parts: vec![Part::FunctionResponse {
276                function_response: crate::FunctionResponseData {
277                    name: "tool".to_string(),
278                    response: serde_json::json!({"result": "done"}),
279                },
280                id: Some("call_tool".to_string()),
281            }],
282        });
283        // Even with function response, skip_summarization makes it final
284        assert!(event.is_final_response());
285    }
286
287    #[test]
288    fn test_is_final_response_long_running_tool_ids() {
289        let mut event = Event::new("inv-123");
290        event.long_running_tool_ids = vec!["process_video".to_string()];
291        event.llm_response.content = Some(Content {
292            role: "model".to_string(),
293            parts: vec![Part::FunctionCall {
294                name: "process_video".to_string(),
295                args: serde_json::json!({"file": "video.mp4"}),
296                id: Some("call_process".to_string()),
297            }],
298        });
299        // Has long_running_tool_ids -> final (async operation started)
300        assert!(event.is_final_response());
301    }
302
303    #[test]
304    fn test_function_call_ids() {
305        let mut event = Event::new("inv-123");
306        event.llm_response.content = Some(Content {
307            role: "model".to_string(),
308            parts: vec![
309                Part::FunctionCall {
310                    name: "get_weather".to_string(),
311                    args: serde_json::json!({}),
312                    id: Some("call_1".to_string()),
313                },
314                Part::Text { text: "I'll check the weather".to_string() },
315                Part::FunctionCall {
316                    name: "get_time".to_string(),
317                    args: serde_json::json!({}),
318                    id: Some("call_2".to_string()),
319                },
320            ],
321        });
322
323        let ids = event.function_call_ids();
324        assert_eq!(ids.len(), 2);
325        assert!(ids.contains(&"get_weather".to_string()));
326        assert!(ids.contains(&"get_time".to_string()));
327    }
328
329    #[test]
330    fn test_function_call_ids_empty() {
331        let event = Event::new("inv-123");
332        let ids = event.function_call_ids();
333        assert!(ids.is_empty());
334    }
335}