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            }],
247        });
248        // Has function call -> NOT final (need to execute it)
249        assert!(!event.is_final_response());
250    }
251
252    #[test]
253    fn test_is_final_response_with_function_response() {
254        let mut event = Event::new("inv-123");
255        event.llm_response.content = Some(Content {
256            role: "function".to_string(),
257            parts: vec![Part::FunctionResponse {
258                function_response: crate::FunctionResponseData {
259                    name: "get_weather".to_string(),
260                    response: serde_json::json!({"temp": 72}),
261                },
262                id: Some("call_123".to_string()),
263            }],
264        });
265        // Has function response -> NOT final (model needs to respond)
266        assert!(!event.is_final_response());
267    }
268
269    #[test]
270    fn test_is_final_response_partial() {
271        let mut event = Event::new("inv-123");
272        event.llm_response.partial = true;
273        event.llm_response.content = Some(Content {
274            role: "model".to_string(),
275            parts: vec![Part::Text { text: "Hello...".to_string() }],
276        });
277        // Partial response -> NOT final
278        assert!(!event.is_final_response());
279    }
280
281    #[test]
282    fn test_is_final_response_skip_summarization() {
283        let mut event = Event::new("inv-123");
284        event.actions.skip_summarization = true;
285        event.llm_response.content = Some(Content {
286            role: "function".to_string(),
287            parts: vec![Part::FunctionResponse {
288                function_response: crate::FunctionResponseData {
289                    name: "tool".to_string(),
290                    response: serde_json::json!({"result": "done"}),
291                },
292                id: Some("call_tool".to_string()),
293            }],
294        });
295        // Even with function response, skip_summarization makes it final
296        assert!(event.is_final_response());
297    }
298
299    #[test]
300    fn test_is_final_response_long_running_tool_ids() {
301        let mut event = Event::new("inv-123");
302        event.long_running_tool_ids = vec!["process_video".to_string()];
303        event.llm_response.content = Some(Content {
304            role: "model".to_string(),
305            parts: vec![Part::FunctionCall {
306                name: "process_video".to_string(),
307                args: serde_json::json!({"file": "video.mp4"}),
308                id: Some("call_process".to_string()),
309            }],
310        });
311        // Has long_running_tool_ids -> final (async operation started)
312        assert!(event.is_final_response());
313    }
314
315    #[test]
316    fn test_function_call_ids() {
317        let mut event = Event::new("inv-123");
318        event.llm_response.content = Some(Content {
319            role: "model".to_string(),
320            parts: vec![
321                Part::FunctionCall {
322                    name: "get_weather".to_string(),
323                    args: serde_json::json!({}),
324                    id: Some("call_1".to_string()),
325                },
326                Part::Text { text: "I'll check the weather".to_string() },
327                Part::FunctionCall {
328                    name: "get_time".to_string(),
329                    args: serde_json::json!({}),
330                    id: Some("call_2".to_string()),
331                },
332            ],
333        });
334
335        let ids = event.function_call_ids();
336        assert_eq!(ids.len(), 2);
337        // Should use actual call IDs, not function names
338        assert!(ids.contains(&"call_1".to_string()));
339        assert!(ids.contains(&"call_2".to_string()));
340    }
341
342    #[test]
343    fn test_function_call_ids_falls_back_to_name() {
344        let mut event = Event::new("inv-123");
345        event.llm_response.content = Some(Content {
346            role: "model".to_string(),
347            parts: vec![Part::FunctionCall {
348                name: "get_weather".to_string(),
349                args: serde_json::json!({}),
350                id: None, // Gemini-style: no explicit ID
351            }],
352        });
353
354        let ids = event.function_call_ids();
355        assert_eq!(ids, vec!["get_weather".to_string()]);
356    }
357
358    #[test]
359    fn test_function_call_ids_empty() {
360        let event = Event::new("inv-123");
361        let ids = event.function_call_ids();
362        assert!(ids.is_empty());
363    }
364
365    #[test]
366    fn test_is_final_response_trailing_function_response() {
367        // Text followed by a function response as the last part —
368        // has_trailing_code_execution_result should catch this even though
369        // has_function_responses also catches it.
370        let mut event = Event::new("inv-123");
371        event.llm_response.content = Some(Content {
372            role: "model".to_string(),
373            parts: vec![
374                Part::Text { text: "Running code...".to_string() },
375                Part::FunctionResponse {
376                    function_response: crate::FunctionResponseData {
377                        name: "code_exec".to_string(),
378                        response: serde_json::json!({"output": "42"}),
379                    },
380                    id: Some("call_exec".to_string()),
381                },
382            ],
383        });
384        // Trailing function response -> NOT final
385        assert!(!event.is_final_response());
386    }
387
388    #[test]
389    fn test_is_final_response_text_after_function_response() {
390        // Function response followed by text — the trailing part is text,
391        // so has_trailing_code_execution_result is false, but
392        // has_function_responses is still true.
393        let mut event = Event::new("inv-123");
394        event.llm_response.content = Some(Content {
395            role: "model".to_string(),
396            parts: vec![
397                Part::FunctionResponse {
398                    function_response: crate::FunctionResponseData {
399                        name: "tool".to_string(),
400                        response: serde_json::json!({}),
401                    },
402                    id: Some("call_1".to_string()),
403                },
404                Part::Text { text: "Done".to_string() },
405            ],
406        });
407        // Still has function responses -> NOT final
408        assert!(!event.is_final_response());
409    }
410}