Skip to main content

adk_core/
event.rs

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
9// State scope prefixes
10pub const KEY_PREFIX_APP: &str = "app:";
11pub const KEY_PREFIX_TEMP: &str = "temp:";
12pub const KEY_PREFIX_USER: &str = "user:";
13
14/// Event represents a single interaction in a conversation.
15/// This struct embeds LlmResponse to match ADK-Go's design pattern.
16#[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    /// The LLM response containing content and metadata.
24    /// Access content via `event.llm_response.content`.
25    #[serde(flatten)]
26    pub llm_response: LlmResponse,
27    pub actions: EventActions,
28    /// IDs of long-running tools associated with this event.
29    #[serde(default)]
30    pub long_running_tool_ids: Vec<String>,
31    /// LLM request data for UI display (JSON string)
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub llm_request: Option<String>,
34    /// Provider-specific metadata (e.g., GCP Vertex, Azure OpenAI).
35    /// Keeps the core Event struct provider-agnostic.
36    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
37    pub provider_metadata: HashMap<String, String>,
38}
39
40/// Metadata for a compacted (summarized) event.
41/// When context compaction is enabled, older events are summarized into a single
42/// compacted event containing this metadata.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct EventCompaction {
45    /// Timestamp of the earliest event that was compacted.
46    pub start_timestamp: DateTime<Utc>,
47    /// Timestamp of the latest event that was compacted.
48    pub end_timestamp: DateTime<Utc>,
49    /// The summarized content replacing the original events.
50    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    /// Present when this event is a compaction summary replacing older events.
65    #[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    /// Create an event with a specific ID.
86    /// Use this for streaming events where all chunks should share the same event ID.
87    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    /// Convenience method to access content directly.
103    pub fn content(&self) -> Option<&Content> {
104        self.llm_response.content.as_ref()
105    }
106
107    /// Convenience method to set content directly.
108    pub fn set_content(&mut self, content: Content) {
109        self.llm_response.content = Some(content);
110    }
111
112    /// Returns whether the event is the final response of an agent.
113    ///
114    /// An event is considered final if:
115    /// - It has skip_summarization set, OR
116    /// - It has long_running_tool_ids (indicating async operations), OR
117    /// - It has no function calls, no function responses, is not partial,
118    ///   and has no trailing code execution results.
119    ///
120    /// Note: When multiple agents participate in one invocation, there could be
121    /// multiple events with is_final_response() as true, for each participating agent.
122    pub fn is_final_response(&self) -> bool {
123        // If skip_summarization is set or we have long-running tools, it's final
124        if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
125            return true;
126        }
127
128        // Check content for function calls/responses
129        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    /// Returns true if the event content contains function calls.
138    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    /// Returns true if the event content contains function responses.
150    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    /// Returns true if the event has a trailing code execution result.
162    #[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                // FunctionResponse as the last part indicates a code execution result
167                // that the model still needs to process.
168                return matches!(last_part, crate::Part::FunctionResponse { .. });
169            }
170        }
171        false
172    }
173
174    /// Extracts function call IDs from this event's content.
175    /// Used to identify which function calls are associated with long-running tools.
176    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                    // Use the actual call ID when available (OpenAI-style),
182                    // fall back to name for providers that don't emit IDs (Gemini).
183                    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        // No content, no function calls -> final
223        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        // Text only, no function calls -> final
234        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        // Has function call -> NOT final (need to execute it)
250        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        // Has function response -> NOT final (model needs to respond)
267        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        // Partial response -> NOT final
279        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        // Even with function response, skip_summarization makes it final
297        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        // Has long_running_tool_ids -> final (async operation started)
314        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        // Should use actual call IDs, not function names
342        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, // Gemini-style: no explicit ID
355                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        // Text followed by a function response as the last part —
373        // has_trailing_code_execution_result should catch this even though
374        // has_function_responses also catches it.
375        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        // Trailing function response -> NOT final
390        assert!(!event.is_final_response());
391    }
392
393    #[test]
394    fn test_is_final_response_text_after_function_response() {
395        // Function response followed by text — the trailing part is text,
396        // so has_trailing_code_execution_result is false, but
397        // has_function_responses is still true.
398        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        // Still has function responses -> NOT final
413        assert!(!event.is_final_response());
414    }
415}