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
10/// Key prefix for application-scoped state (persists across sessions).
11pub const KEY_PREFIX_APP: &str = "app:";
12/// Key prefix for temporary state (cleared each turn).
13pub const KEY_PREFIX_TEMP: &str = "temp:";
14/// Key prefix for user-scoped state (persists across sessions).
15pub const KEY_PREFIX_USER: &str = "user:";
16
17/// Event represents a single interaction in a conversation.
18/// This struct embeds LlmResponse to match ADK-Go's design pattern.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Event {
21    /// Unique identifier for this event.
22    pub id: String,
23    /// When this event was created.
24    pub timestamp: DateTime<Utc>,
25    /// The invocation that produced this event.
26    pub invocation_id: String,
27    /// The conversation branch this event belongs to.
28    pub branch: String,
29    /// The agent or role that authored this event.
30    pub author: String,
31    /// The LLM response containing content and metadata.
32    /// Access content via `event.llm_response.content`.
33    #[serde(flatten)]
34    pub llm_response: LlmResponse,
35    /// Actions to apply (state changes, transfers, confirmations).
36    pub actions: EventActions,
37    /// IDs of long-running tools associated with this event.
38    #[serde(default)]
39    pub long_running_tool_ids: Vec<String>,
40    /// LLM request data for UI display (JSON string)
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub llm_request: Option<String>,
43    /// Provider-specific metadata (e.g., GCP Vertex, Azure OpenAI).
44    /// Keeps the core Event struct provider-agnostic.
45    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
46    pub provider_metadata: HashMap<String, String>,
47}
48
49/// Metadata for a compacted (summarized) event.
50/// When context compaction is enabled, older events are summarized into a single
51/// compacted event containing this metadata.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct EventCompaction {
54    /// Timestamp of the earliest event that was compacted.
55    pub start_timestamp: DateTime<Utc>,
56    /// Timestamp of the latest event that was compacted.
57    pub end_timestamp: DateTime<Utc>,
58    /// The summarized content replacing the original events.
59    pub compacted_content: Content,
60}
61
62/// Actions to apply as side effects of an event.
63#[derive(Debug, Clone, Default, Serialize, Deserialize)]
64pub struct EventActions {
65    /// State key-value changes to apply.
66    pub state_delta: HashMap<String, serde_json::Value>,
67    /// Artifact version changes.
68    pub artifact_delta: HashMap<String, i64>,
69    /// Whether to skip summarization for this event.
70    pub skip_summarization: bool,
71    /// Agent name to transfer control to.
72    pub transfer_to_agent: Option<String>,
73    /// Whether to escalate to a human operator.
74    pub escalate: bool,
75    /// Tool confirmation request awaiting human approval.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub tool_confirmation: Option<ToolConfirmationRequest>,
78    /// Decision for a pending tool confirmation.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub tool_confirmation_decision: Option<ToolConfirmationDecision>,
81    /// Present when this event is a compaction summary replacing older events.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub compaction: Option<EventCompaction>,
84    /// Target node names for dynamic route dispatch in graph workflows.
85    /// When non-empty, the graph executor routes to these nodes instead of
86    /// following static edges.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub route: Option<Vec<String>>,
89}
90
91impl Event {
92    /// Creates a new event with a generated UUID and current timestamp.
93    pub fn new(invocation_id: impl Into<String>) -> Self {
94        Self {
95            id: Uuid::new_v4().to_string(),
96            timestamp: Utc::now(),
97            invocation_id: invocation_id.into(),
98            branch: String::new(),
99            author: String::new(),
100            llm_response: LlmResponse::default(),
101            actions: EventActions::default(),
102            long_running_tool_ids: Vec::new(),
103            llm_request: None,
104            provider_metadata: HashMap::new(),
105        }
106    }
107
108    /// Create an event with a specific ID.
109    /// Use this for streaming events where all chunks should share the same event ID.
110    pub fn with_id(id: impl Into<String>, invocation_id: impl Into<String>) -> Self {
111        Self {
112            id: id.into(),
113            timestamp: Utc::now(),
114            invocation_id: invocation_id.into(),
115            branch: String::new(),
116            author: String::new(),
117            llm_response: LlmResponse::default(),
118            actions: EventActions::default(),
119            long_running_tool_ids: Vec::new(),
120            llm_request: None,
121            provider_metadata: HashMap::new(),
122        }
123    }
124
125    /// Convenience method to access content directly.
126    pub fn content(&self) -> Option<&Content> {
127        self.llm_response.content.as_ref()
128    }
129
130    /// Convenience method to set content directly.
131    pub fn set_content(&mut self, content: Content) {
132        self.llm_response.content = Some(content);
133    }
134
135    /// Returns the Interactions API interaction id for this event, if present.
136    ///
137    /// Reads the id from the flattened [`LlmResponse`], mirroring ADK-Python's
138    /// `event.interaction_id`. Returns `None` for events produced by the
139    /// generateContent transport and non-Gemini providers.
140    ///
141    /// # Example
142    ///
143    /// ```
144    /// use adk_core::Event;
145    ///
146    /// let mut event = Event::new("inv-123");
147    /// assert_eq!(event.interaction_id(), None);
148    ///
149    /// event.llm_response.interaction_id = Some("v1_abc".to_string());
150    /// assert_eq!(event.interaction_id(), Some("v1_abc"));
151    /// ```
152    pub fn interaction_id(&self) -> Option<&str> {
153        self.llm_response.interaction_id.as_deref()
154    }
155
156    /// Returns whether the event is the final response of an agent.
157    ///
158    /// An event is considered final if:
159    /// - It has skip_summarization set, OR
160    /// - It has long_running_tool_ids (indicating async operations), OR
161    /// - It has no function calls, no function responses, is not partial,
162    ///   and has no trailing code execution results.
163    ///
164    /// Note: When multiple agents participate in one invocation, there could be
165    /// multiple events with is_final_response() as true, for each participating agent.
166    pub fn is_final_response(&self) -> bool {
167        // If skip_summarization is set or we have long-running tools, it's final
168        if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
169            return true;
170        }
171
172        // Check content for function calls/responses
173        let has_function_calls = self.has_function_calls();
174        let has_function_responses = self.has_function_responses();
175        let is_partial = self.llm_response.partial;
176        let has_trailing_code_result = self.has_trailing_code_execution_result();
177
178        !has_function_calls && !has_function_responses && !is_partial && !has_trailing_code_result
179    }
180
181    /// Returns true if the event content contains function calls.
182    fn has_function_calls(&self) -> bool {
183        if let Some(content) = &self.llm_response.content {
184            for part in &content.parts {
185                if matches!(part, crate::Part::FunctionCall { .. }) {
186                    return true;
187                }
188            }
189        }
190        false
191    }
192
193    /// Returns true if the event content contains function responses.
194    fn has_function_responses(&self) -> bool {
195        if let Some(content) = &self.llm_response.content {
196            for part in &content.parts {
197                if matches!(part, crate::Part::FunctionResponse { .. }) {
198                    return true;
199                }
200            }
201        }
202        false
203    }
204
205    /// Returns true if the event has a trailing code execution result.
206    #[allow(clippy::match_like_matches_macro)]
207    fn has_trailing_code_execution_result(&self) -> bool {
208        if let Some(content) = &self.llm_response.content
209            && let Some(last_part) = content.parts.last()
210        {
211            // FunctionResponse as the last part indicates a code execution result
212            // that the model still needs to process.
213            return matches!(last_part, crate::Part::FunctionResponse { .. });
214        }
215        false
216    }
217
218    /// Extracts function call IDs from this event's content.
219    /// Used to identify which function calls are associated with long-running tools.
220    pub fn function_call_ids(&self) -> Vec<String> {
221        let mut ids = Vec::new();
222        if let Some(content) = &self.llm_response.content {
223            for part in &content.parts {
224                if let crate::Part::FunctionCall { name, id, .. } = part {
225                    // Use the actual call ID when available (OpenAI-style),
226                    // fall back to name for providers that don't emit IDs (Gemini).
227                    ids.push(id.as_deref().unwrap_or(name).to_string());
228                }
229            }
230        }
231        ids
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use crate::Part;
239
240    #[test]
241    fn test_event_creation() {
242        let event = Event::new("inv-123");
243        assert_eq!(event.invocation_id, "inv-123");
244        assert!(!event.id.is_empty());
245    }
246
247    #[test]
248    fn test_event_actions_default() {
249        let actions = EventActions::default();
250        assert!(actions.state_delta.is_empty());
251        assert!(!actions.skip_summarization);
252        assert!(actions.tool_confirmation.is_none());
253        assert!(actions.tool_confirmation_decision.is_none());
254    }
255
256    #[test]
257    fn test_state_prefixes() {
258        assert_eq!(KEY_PREFIX_APP, "app:");
259        assert_eq!(KEY_PREFIX_TEMP, "temp:");
260        assert_eq!(KEY_PREFIX_USER, "user:");
261    }
262
263    #[test]
264    fn test_is_final_response_no_content() {
265        let event = Event::new("inv-123");
266        // No content, no function calls -> final
267        assert!(event.is_final_response());
268    }
269
270    #[test]
271    fn test_is_final_response_text_only() {
272        let mut event = Event::new("inv-123");
273        event.llm_response.content = Some(Content {
274            role: "model".to_string(),
275            parts: vec![Part::Text { text: "Hello!".to_string() }],
276        });
277        // Text only, no function calls -> final
278        assert!(event.is_final_response());
279    }
280
281    #[test]
282    fn test_is_final_response_with_function_call() {
283        let mut event = Event::new("inv-123");
284        event.llm_response.content = Some(Content {
285            role: "model".to_string(),
286            parts: vec![Part::FunctionCall {
287                name: "get_weather".to_string(),
288                args: serde_json::json!({"city": "NYC"}),
289                id: Some("call_123".to_string()),
290                thought_signature: None,
291            }],
292        });
293        // Has function call -> NOT final (need to execute it)
294        assert!(!event.is_final_response());
295    }
296
297    #[test]
298    fn test_is_final_response_with_function_response() {
299        let mut event = Event::new("inv-123");
300        event.llm_response.content = Some(Content {
301            role: "function".to_string(),
302            parts: vec![Part::FunctionResponse {
303                function_response: crate::FunctionResponseData::new(
304                    "get_weather",
305                    serde_json::json!({"temp": 72}),
306                ),
307                id: Some("call_123".to_string()),
308            }],
309        });
310        // Has function response -> NOT final (model needs to respond)
311        assert!(!event.is_final_response());
312    }
313
314    #[test]
315    fn test_is_final_response_partial() {
316        let mut event = Event::new("inv-123");
317        event.llm_response.partial = true;
318        event.llm_response.content = Some(Content {
319            role: "model".to_string(),
320            parts: vec![Part::Text { text: "Hello...".to_string() }],
321        });
322        // Partial response -> NOT final
323        assert!(!event.is_final_response());
324    }
325
326    #[test]
327    fn test_is_final_response_skip_summarization() {
328        let mut event = Event::new("inv-123");
329        event.actions.skip_summarization = true;
330        event.llm_response.content = Some(Content {
331            role: "function".to_string(),
332            parts: vec![Part::FunctionResponse {
333                function_response: crate::FunctionResponseData::new(
334                    "tool",
335                    serde_json::json!({"result": "done"}),
336                ),
337                id: Some("call_tool".to_string()),
338            }],
339        });
340        // Even with function response, skip_summarization makes it final
341        assert!(event.is_final_response());
342    }
343
344    #[test]
345    fn test_is_final_response_long_running_tool_ids() {
346        let mut event = Event::new("inv-123");
347        event.long_running_tool_ids = vec!["process_video".to_string()];
348        event.llm_response.content = Some(Content {
349            role: "model".to_string(),
350            parts: vec![Part::FunctionCall {
351                name: "process_video".to_string(),
352                args: serde_json::json!({"file": "video.mp4"}),
353                id: Some("call_process".to_string()),
354                thought_signature: None,
355            }],
356        });
357        // Has long_running_tool_ids -> final (async operation started)
358        assert!(event.is_final_response());
359    }
360
361    #[test]
362    fn test_function_call_ids() {
363        let mut event = Event::new("inv-123");
364        event.llm_response.content = Some(Content {
365            role: "model".to_string(),
366            parts: vec![
367                Part::FunctionCall {
368                    name: "get_weather".to_string(),
369                    args: serde_json::json!({}),
370                    id: Some("call_1".to_string()),
371                    thought_signature: None,
372                },
373                Part::Text { text: "I'll check the weather".to_string() },
374                Part::FunctionCall {
375                    name: "get_time".to_string(),
376                    args: serde_json::json!({}),
377                    id: Some("call_2".to_string()),
378                    thought_signature: None,
379                },
380            ],
381        });
382
383        let ids = event.function_call_ids();
384        assert_eq!(ids.len(), 2);
385        // Should use actual call IDs, not function names
386        assert!(ids.contains(&"call_1".to_string()));
387        assert!(ids.contains(&"call_2".to_string()));
388    }
389
390    #[test]
391    fn test_function_call_ids_falls_back_to_name() {
392        let mut event = Event::new("inv-123");
393        event.llm_response.content = Some(Content {
394            role: "model".to_string(),
395            parts: vec![Part::FunctionCall {
396                name: "get_weather".to_string(),
397                args: serde_json::json!({}),
398                id: None, // Gemini-style: no explicit ID
399                thought_signature: None,
400            }],
401        });
402
403        let ids = event.function_call_ids();
404        assert_eq!(ids, vec!["get_weather".to_string()]);
405    }
406
407    #[test]
408    fn test_function_call_ids_empty() {
409        let event = Event::new("inv-123");
410        let ids = event.function_call_ids();
411        assert!(ids.is_empty());
412    }
413
414    #[test]
415    fn test_is_final_response_trailing_function_response() {
416        // Text followed by a function response as the last part —
417        // has_trailing_code_execution_result should catch this even though
418        // has_function_responses also catches it.
419        let mut event = Event::new("inv-123");
420        event.llm_response.content = Some(Content {
421            role: "model".to_string(),
422            parts: vec![
423                Part::Text { text: "Running code...".to_string() },
424                Part::FunctionResponse {
425                    function_response: crate::FunctionResponseData::new(
426                        "code_exec",
427                        serde_json::json!({"output": "42"}),
428                    ),
429                    id: Some("call_exec".to_string()),
430                },
431            ],
432        });
433        // Trailing function response -> NOT final
434        assert!(!event.is_final_response());
435    }
436
437    #[test]
438    fn test_is_final_response_text_after_function_response() {
439        // Function response followed by text — the trailing part is text,
440        // so has_trailing_code_execution_result is false, but
441        // has_function_responses is still true.
442        let mut event = Event::new("inv-123");
443        event.llm_response.content = Some(Content {
444            role: "model".to_string(),
445            parts: vec![
446                Part::FunctionResponse {
447                    function_response: crate::FunctionResponseData::new(
448                        "tool",
449                        serde_json::json!({}),
450                    ),
451                    id: Some("call_1".to_string()),
452                },
453                Part::Text { text: "Done".to_string() },
454            ],
455        });
456        // Still has function responses -> NOT final
457        assert!(!event.is_final_response());
458    }
459}