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-level `provider_metadata` key marking a tool-progress event and naming
18/// its output stream (e.g. `"stdout"`, `"stderr"`). Present only on events
19/// produced by [`ToolContext::emit_progress`](crate::ToolContext::emit_progress).
20pub const TOOL_PROGRESS_STREAM_KEY: &str = "adk.tool_progress.stream";
21
22/// Event-level `provider_metadata` key carrying the originating tool's
23/// function-call id on a tool-progress event.
24pub const TOOL_PROGRESS_CALL_ID_KEY: &str = "adk.tool_progress.call_id";
25
26/// Event represents a single interaction in a conversation.
27/// This struct embeds LlmResponse to match ADK-Go's design pattern.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Event {
30    /// Unique identifier for this event.
31    pub id: String,
32    /// When this event was created.
33    pub timestamp: DateTime<Utc>,
34    /// The invocation that produced this event.
35    pub invocation_id: String,
36    /// The conversation branch this event belongs to.
37    pub branch: String,
38    /// The agent or role that authored this event.
39    pub author: String,
40    /// The LLM response containing content and metadata.
41    /// Access content via `event.llm_response.content`.
42    #[serde(flatten)]
43    pub llm_response: LlmResponse,
44    /// Actions to apply (state changes, transfers, confirmations).
45    pub actions: EventActions,
46    /// IDs of long-running tools associated with this event.
47    #[serde(default)]
48    pub long_running_tool_ids: Vec<String>,
49    /// LLM request data for UI display (JSON string)
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub llm_request: Option<String>,
52    /// Provider-specific metadata (e.g., GCP Vertex, Azure OpenAI).
53    /// Keeps the core Event struct provider-agnostic.
54    /// Serialized as `"event_metadata"` to avoid collision with
55    /// [`LlmResponse::provider_metadata`](crate::LlmResponse) when flattened.
56    #[serde(default, skip_serializing_if = "HashMap::is_empty", rename = "event_metadata")]
57    pub provider_metadata: HashMap<String, String>,
58}
59
60/// Metadata for a compacted (summarized) event.
61/// When context compaction is enabled, older events are summarized into a single
62/// compacted event containing this metadata.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct EventCompaction {
65    /// Timestamp of the earliest event that was compacted.
66    pub start_timestamp: DateTime<Utc>,
67    /// Timestamp of the latest event that was compacted.
68    pub end_timestamp: DateTime<Utc>,
69    /// The summarized content replacing the original events.
70    pub compacted_content: Content,
71}
72
73/// Actions to apply as side effects of an event.
74#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub struct EventActions {
76    /// State key-value changes to apply.
77    pub state_delta: HashMap<String, serde_json::Value>,
78    /// Artifact version changes.
79    pub artifact_delta: HashMap<String, i64>,
80    /// Whether to skip summarization for this event.
81    pub skip_summarization: bool,
82    /// Agent name to transfer control to.
83    pub transfer_to_agent: Option<String>,
84    /// Whether to escalate to a human operator.
85    pub escalate: bool,
86    /// Tool confirmation request awaiting human approval.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub tool_confirmation: Option<ToolConfirmationRequest>,
89    /// Decision for a pending tool confirmation.
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub tool_confirmation_decision: Option<ToolConfirmationDecision>,
92    /// Present when this event is a compaction summary replacing older events.
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub compaction: Option<EventCompaction>,
95    /// Target node names for dynamic route dispatch in graph workflows.
96    /// When non-empty, the graph executor routes to these nodes instead of
97    /// following static edges.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub route: Option<Vec<String>>,
100}
101
102/// A typed, borrowed view of a single tool call carried by an [`Event`].
103///
104/// Produced by [`Event::tool_calls`]. Lets UI/event consumers render the tool a
105/// model requested without matching on [`Part::FunctionCall`](crate::Part::FunctionCall)
106/// internals.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct ToolCallView<'a> {
109    /// Provider-assigned call id (OpenAI-style). `None` for providers that omit
110    /// it (e.g. Gemini); fall back to [`name`](Self::name) for correlation.
111    pub call_id: Option<&'a str>,
112    /// The tool/function name the model requested.
113    pub name: &'a str,
114    /// The call arguments as raw JSON.
115    pub args: &'a serde_json::Value,
116}
117
118/// A typed, borrowed view of a single tool result carried by an [`Event`].
119///
120/// Produced by [`Event::tool_results`]. Surfaces a completed tool's output
121/// generically so any tool — streaming or not — can be rendered from the event
122/// stream without walking [`Part::FunctionResponse`](crate::Part::FunctionResponse)
123/// internals.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub struct ToolResultView<'a> {
126    /// Provider-assigned call id (OpenAI-style), correlating this result with
127    /// its originating [`ToolCallView`] and progress chunks. `None` for
128    /// providers that omit it (e.g. Gemini); fall back to [`name`](Self::name).
129    pub call_id: Option<&'a str>,
130    /// The tool/function name that produced this result.
131    pub name: &'a str,
132    /// The tool's JSON response payload.
133    pub response: &'a serde_json::Value,
134}
135
136impl Event {
137    /// Creates a new event with a generated UUID and current timestamp.
138    pub fn new(invocation_id: impl Into<String>) -> Self {
139        Self {
140            id: Uuid::new_v4().to_string(),
141            timestamp: Utc::now(),
142            invocation_id: invocation_id.into(),
143            branch: String::new(),
144            author: String::new(),
145            llm_response: LlmResponse::default(),
146            actions: EventActions::default(),
147            long_running_tool_ids: Vec::new(),
148            llm_request: None,
149            provider_metadata: HashMap::new(),
150        }
151    }
152
153    /// Create an event with a specific ID.
154    /// Use this for streaming events where all chunks should share the same event ID.
155    pub fn with_id(id: impl Into<String>, invocation_id: impl Into<String>) -> Self {
156        Self {
157            id: id.into(),
158            timestamp: Utc::now(),
159            invocation_id: invocation_id.into(),
160            branch: String::new(),
161            author: String::new(),
162            llm_response: LlmResponse::default(),
163            actions: EventActions::default(),
164            long_running_tool_ids: Vec::new(),
165            llm_request: None,
166            provider_metadata: HashMap::new(),
167        }
168    }
169
170    /// Creates a streaming tool-progress event.
171    ///
172    /// Tools emit these via [`ToolContext::emit_progress`](crate::ToolContext::emit_progress)
173    /// to push intermediate stdout/stderr to the client *while the tool is still
174    /// running*. The event carries the chunk as partial text content (role
175    /// `"tool"`) and is tagged with [`TOOL_PROGRESS_STREAM_KEY`] /
176    /// [`TOOL_PROGRESS_CALL_ID_KEY`] so consumers can distinguish it from a
177    /// final tool result and route it to the right terminal widget.
178    ///
179    /// # Example
180    ///
181    /// ```
182    /// use adk_core::Event;
183    ///
184    /// let event = Event::tool_progress("inv-1", "agent", "call-7", "stdout", "compiling...\n");
185    /// assert_eq!(event.tool_progress_stream(), Some("stdout"));
186    /// assert!(event.llm_response.partial);
187    /// ```
188    pub fn tool_progress(
189        invocation_id: impl Into<String>,
190        author: impl Into<String>,
191        function_call_id: impl Into<String>,
192        stream: impl Into<String>,
193        chunk: impl Into<String>,
194    ) -> Self {
195        let mut event = Event::new(invocation_id);
196        event.author = author.into();
197        event.llm_response.content = Some(Content {
198            role: "tool".to_string(),
199            parts: vec![crate::types::Part::Text { text: chunk.into() }],
200        });
201        // Partial so downstream aggregation/persistence treats it as a streaming
202        // chunk, never as the agent's final response.
203        event.llm_response.partial = true;
204        event.provider_metadata.insert(TOOL_PROGRESS_STREAM_KEY.to_string(), stream.into());
205        event
206            .provider_metadata
207            .insert(TOOL_PROGRESS_CALL_ID_KEY.to_string(), function_call_id.into());
208        event
209    }
210
211    /// Returns the progress stream name (`"stdout"`, `"stderr"`, …) if this is a
212    /// tool-progress event produced by [`ToolContext::emit_progress`](crate::ToolContext::emit_progress),
213    /// otherwise `None`.
214    pub fn tool_progress_stream(&self) -> Option<&str> {
215        self.provider_metadata.get(TOOL_PROGRESS_STREAM_KEY).map(String::as_str)
216    }
217
218    /// Returns the tool calls carried by this event, as a typed, render-ready view.
219    ///
220    /// A UI consuming the agent's `EventStream` can call this on every event to
221    /// detect when the model requested one or more tools, without matching on
222    /// [`Part::FunctionCall`](crate::Part::FunctionCall) internals. Pair it with
223    /// [`tool_results`](Self::tool_results) and [`tool_progress_stream`](Self::tool_progress_stream)
224    /// to render a complete tool lifecycle (call → live progress → result).
225    ///
226    /// Returns an empty vector for events that contain no tool calls.
227    ///
228    /// # Correlation
229    ///
230    /// Use [`ToolCallView::call_id`] to correlate a call with its progress chunks
231    /// and final result. For providers that omit call ids (e.g. Gemini), fall
232    /// back to [`ToolCallView::name`].
233    ///
234    /// # Example
235    ///
236    /// ```
237    /// use adk_core::{Content, Event, Part};
238    ///
239    /// let mut event = Event::new("inv-1");
240    /// event.llm_response.content = Some(Content {
241    ///     role: "model".to_string(),
242    ///     parts: vec![Part::FunctionCall {
243    ///         name: "bash".to_string(),
244    ///         args: serde_json::json!({ "command": "ls" }),
245    ///         id: Some("call_1".to_string()),
246    ///         thought_signature: None,
247    ///     }],
248    /// });
249    ///
250    /// let calls = event.tool_calls();
251    /// assert_eq!(calls.len(), 1);
252    /// assert_eq!(calls[0].name, "bash");
253    /// assert_eq!(calls[0].call_id, Some("call_1"));
254    /// ```
255    pub fn tool_calls(&self) -> Vec<ToolCallView<'_>> {
256        let Some(content) = &self.llm_response.content else {
257            return Vec::new();
258        };
259        content
260            .parts
261            .iter()
262            .filter_map(|part| match part {
263                crate::types::Part::FunctionCall { name, args, id, .. } => {
264                    Some(ToolCallView { call_id: id.as_deref(), name, args })
265                }
266                _ => None,
267            })
268            .collect()
269    }
270
271    /// Returns the tool results carried by this event, as a typed, render-ready view.
272    ///
273    /// After a tool executes, the agent yields its result on the same
274    /// `EventStream` as everything else, as a `function`-role event holding a
275    /// [`Part::FunctionResponse`](crate::Part::FunctionResponse). This accessor
276    /// surfaces those results generically so a UI can render the output of *any*
277    /// tool — streaming or not — without walking part internals.
278    ///
279    /// Returns an empty vector for events that contain no tool results.
280    ///
281    /// # Correlation
282    ///
283    /// Use [`ToolResultView::call_id`] to attach a result to the originating
284    /// [`tool_calls`](Self::tool_calls) entry and its progress chunks. For
285    /// providers that omit call ids, fall back to [`ToolResultView::name`].
286    ///
287    /// # Example
288    ///
289    /// ```
290    /// use adk_core::{Content, Event, FunctionResponseData, Part};
291    ///
292    /// let mut event = Event::new("inv-1");
293    /// event.llm_response.content = Some(Content {
294    ///     role: "function".to_string(),
295    ///     parts: vec![Part::FunctionResponse {
296    ///         function_response: FunctionResponseData::new(
297    ///             "bash",
298    ///             serde_json::json!({ "stdout": "ok\n", "exit_code": 0 }),
299    ///         ),
300    ///         id: Some("call_1".to_string()),
301    ///         annotations: None,
302    ///     }],
303    /// });
304    ///
305    /// let results = event.tool_results();
306    /// assert_eq!(results.len(), 1);
307    /// assert_eq!(results[0].name, "bash");
308    /// assert_eq!(results[0].call_id, Some("call_1"));
309    /// assert_eq!(results[0].response["exit_code"], 0);
310    /// ```
311    pub fn tool_results(&self) -> Vec<ToolResultView<'_>> {
312        let Some(content) = &self.llm_response.content else {
313            return Vec::new();
314        };
315        content
316            .parts
317            .iter()
318            .filter_map(|part| match part {
319                crate::types::Part::FunctionResponse { function_response, id, .. } => {
320                    Some(ToolResultView {
321                        call_id: id.as_deref(),
322                        name: &function_response.name,
323                        response: &function_response.response,
324                    })
325                }
326                _ => None,
327            })
328            .collect()
329    }
330
331    /// Convenience method to access content directly.
332    pub fn content(&self) -> Option<&Content> {
333        self.llm_response.content.as_ref()
334    }
335
336    /// Convenience method to set content directly.
337    pub fn set_content(&mut self, content: Content) {
338        self.llm_response.content = Some(content);
339    }
340
341    /// Returns the Interactions API interaction id for this event, if present.
342    ///
343    /// Reads the id from the flattened [`LlmResponse`], mirroring ADK-Python's
344    /// `event.interaction_id`. Returns `None` for events produced by the
345    /// generateContent transport and non-Gemini providers.
346    ///
347    /// # Example
348    ///
349    /// ```
350    /// use adk_core::Event;
351    ///
352    /// let mut event = Event::new("inv-123");
353    /// assert_eq!(event.interaction_id(), None);
354    ///
355    /// event.llm_response.interaction_id = Some("v1_abc".to_string());
356    /// assert_eq!(event.interaction_id(), Some("v1_abc"));
357    /// ```
358    pub fn interaction_id(&self) -> Option<&str> {
359        self.llm_response.interaction_id.as_deref()
360    }
361
362    /// Returns whether the event is the final response of an agent.
363    ///
364    /// An event is considered final if:
365    /// - It has skip_summarization set, OR
366    /// - It has long_running_tool_ids (indicating async operations), OR
367    /// - It has no function calls, no function responses, is not partial,
368    ///   and has no trailing code execution results.
369    ///
370    /// Note: When multiple agents participate in one invocation, there could be
371    /// multiple events with is_final_response() as true, for each participating agent.
372    pub fn is_final_response(&self) -> bool {
373        // If skip_summarization is set or we have long-running tools, it's final
374        if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
375            return true;
376        }
377
378        // Check content for function calls/responses
379        let has_function_calls = self.has_function_calls();
380        let has_function_responses = self.has_function_responses();
381        let is_partial = self.llm_response.partial;
382        let has_trailing_code_result = self.has_trailing_code_execution_result();
383
384        !has_function_calls && !has_function_responses && !is_partial && !has_trailing_code_result
385    }
386
387    /// Returns true if the event content contains function calls.
388    fn has_function_calls(&self) -> bool {
389        if let Some(content) = &self.llm_response.content {
390            for part in &content.parts {
391                if matches!(part, crate::Part::FunctionCall { .. }) {
392                    return true;
393                }
394            }
395        }
396        false
397    }
398
399    /// Returns true if the event content contains function responses.
400    fn has_function_responses(&self) -> bool {
401        if let Some(content) = &self.llm_response.content {
402            for part in &content.parts {
403                if matches!(part, crate::Part::FunctionResponse { .. }) {
404                    return true;
405                }
406            }
407        }
408        false
409    }
410
411    /// Returns true if the event has a trailing code execution result.
412    #[allow(clippy::match_like_matches_macro)]
413    fn has_trailing_code_execution_result(&self) -> bool {
414        if let Some(content) = &self.llm_response.content
415            && let Some(last_part) = content.parts.last()
416        {
417            // FunctionResponse as the last part indicates a code execution result
418            // that the model still needs to process.
419            return matches!(last_part, crate::Part::FunctionResponse { .. });
420        }
421        false
422    }
423
424    /// Extracts function call IDs from this event's content.
425    /// Used to identify which function calls are associated with long-running tools.
426    pub fn function_call_ids(&self) -> Vec<String> {
427        let mut ids = Vec::new();
428        if let Some(content) = &self.llm_response.content {
429            for part in &content.parts {
430                if let crate::Part::FunctionCall { name, id, .. } = part {
431                    // Use the actual call ID when available (OpenAI-style),
432                    // fall back to name for providers that don't emit IDs (Gemini).
433                    ids.push(id.as_deref().unwrap_or(name).to_string());
434                }
435            }
436        }
437        ids
438    }
439}
440
441/// Whether an event is visible from a given conversation branch.
442///
443/// An event is visible when its branch equals `invocation_branch` or is an
444/// *ancestor* of it. Siblings and descendants are excluded, which is what keeps
445/// concurrent `ParallelAgent` branches from seeing each other's output while
446/// still letting each one see the conversation that led to the fan-out.
447///
448/// An empty branch on either side means "unscoped" and matches everything, so
449/// events written without a branch stay globally visible and callers that never
450/// set a branch are unaffected.
451///
452/// Branch segments are delimited by `.`, and the prefix test requires that
453/// delimiter explicitly: without it `agent_0` would match `agent_00`. This
454/// mirrors ADK Python's `_is_event_belongs_to_branch` and ADK Go's
455/// `eventBelongsToBranch`.
456///
457/// # Example
458///
459/// ```
460/// use adk_core::event_belongs_to_branch;
461///
462/// // Own branch and ancestors are visible.
463/// assert!(event_belongs_to_branch("parent.parallel.a", "parent.parallel.a"));
464/// assert!(event_belongs_to_branch("parent.parallel.a", "parent"));
465///
466/// // A sibling branch is not.
467/// assert!(!event_belongs_to_branch("parent.parallel.a", "parent.parallel.b"));
468///
469/// // Unscoped events remain visible.
470/// assert!(event_belongs_to_branch("parent.parallel.a", ""));
471/// ```
472pub fn event_belongs_to_branch(invocation_branch: &str, event_branch: &str) -> bool {
473    if invocation_branch.is_empty() || event_branch.is_empty() {
474        return true;
475    }
476    if event_branch == invocation_branch {
477        return true;
478    }
479    // Require the delimiter so `agent_0` does not match `agent_00`.
480    invocation_branch.starts_with(event_branch)
481        && invocation_branch.as_bytes().get(event_branch.len()) == Some(&b'.')
482}
483
484#[cfg(test)]
485mod branch_visibility_tests {
486    use super::event_belongs_to_branch;
487
488    #[test]
489    fn own_branch_is_visible() {
490        assert!(event_belongs_to_branch("root.parallel.a", "root.parallel.a"));
491    }
492
493    #[test]
494    fn ancestor_branches_are_visible() {
495        assert!(event_belongs_to_branch("root.parallel.a", "root"));
496        assert!(event_belongs_to_branch("root.parallel.a", "root.parallel"));
497    }
498
499    #[test]
500    fn sibling_branches_are_hidden() {
501        assert!(!event_belongs_to_branch("root.parallel.a", "root.parallel.b"));
502        assert!(!event_belongs_to_branch("root.parallel.b", "root.parallel.a"));
503    }
504
505    #[test]
506    fn descendant_branches_are_hidden() {
507        // A parent must not see what a nested child produced in its own branch.
508        assert!(!event_belongs_to_branch("root", "root.parallel.a"));
509    }
510
511    #[test]
512    fn empty_branch_on_either_side_matches() {
513        assert!(event_belongs_to_branch("", "root.parallel.a"));
514        assert!(event_belongs_to_branch("root.parallel.a", ""));
515        assert!(event_belongs_to_branch("", ""));
516    }
517
518    #[test]
519    fn prefix_match_requires_the_delimiter() {
520        // The bug the delimiter guards against: `agent_0` vs `agent_00`.
521        assert!(!event_belongs_to_branch("root.agent_00", "root.agent_0"));
522        assert!(event_belongs_to_branch("root.agent_0.child", "root.agent_0"));
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529    use crate::Part;
530
531    #[test]
532    fn test_event_creation() {
533        let event = Event::new("inv-123");
534        assert_eq!(event.invocation_id, "inv-123");
535        assert!(!event.id.is_empty());
536    }
537
538    #[test]
539    fn test_event_actions_default() {
540        let actions = EventActions::default();
541        assert!(actions.state_delta.is_empty());
542        assert!(!actions.skip_summarization);
543        assert!(actions.tool_confirmation.is_none());
544        assert!(actions.tool_confirmation_decision.is_none());
545    }
546
547    #[test]
548    fn test_state_prefixes() {
549        assert_eq!(KEY_PREFIX_APP, "app:");
550        assert_eq!(KEY_PREFIX_TEMP, "temp:");
551        assert_eq!(KEY_PREFIX_USER, "user:");
552    }
553
554    #[test]
555    fn test_is_final_response_no_content() {
556        let event = Event::new("inv-123");
557        // No content, no function calls -> final
558        assert!(event.is_final_response());
559    }
560
561    #[test]
562    fn test_is_final_response_text_only() {
563        let mut event = Event::new("inv-123");
564        event.llm_response.content = Some(Content {
565            role: "model".to_string(),
566            parts: vec![Part::Text { text: "Hello!".to_string() }],
567        });
568        // Text only, no function calls -> final
569        assert!(event.is_final_response());
570    }
571
572    #[test]
573    fn test_is_final_response_with_function_call() {
574        let mut event = Event::new("inv-123");
575        event.llm_response.content = Some(Content {
576            role: "model".to_string(),
577            parts: vec![Part::FunctionCall {
578                name: "get_weather".to_string(),
579                args: serde_json::json!({"city": "NYC"}),
580                id: Some("call_123".to_string()),
581                thought_signature: None,
582            }],
583        });
584        // Has function call -> NOT final (need to execute it)
585        assert!(!event.is_final_response());
586    }
587
588    #[test]
589    fn test_is_final_response_with_function_response() {
590        let mut event = Event::new("inv-123");
591        event.llm_response.content = Some(Content {
592            role: "function".to_string(),
593            parts: vec![Part::FunctionResponse {
594                function_response: crate::FunctionResponseData::new(
595                    "get_weather",
596                    serde_json::json!({"temp": 72}),
597                ),
598                id: Some("call_123".to_string()),
599                annotations: None,
600            }],
601        });
602        // Has function response -> NOT final (model needs to respond)
603        assert!(!event.is_final_response());
604    }
605
606    #[test]
607    fn test_is_final_response_partial() {
608        let mut event = Event::new("inv-123");
609        event.llm_response.partial = true;
610        event.llm_response.content = Some(Content {
611            role: "model".to_string(),
612            parts: vec![Part::Text { text: "Hello...".to_string() }],
613        });
614        // Partial response -> NOT final
615        assert!(!event.is_final_response());
616    }
617
618    #[test]
619    fn test_is_final_response_skip_summarization() {
620        let mut event = Event::new("inv-123");
621        event.actions.skip_summarization = true;
622        event.llm_response.content = Some(Content {
623            role: "function".to_string(),
624            parts: vec![Part::FunctionResponse {
625                function_response: crate::FunctionResponseData::new(
626                    "tool",
627                    serde_json::json!({"result": "done"}),
628                ),
629                id: Some("call_tool".to_string()),
630                annotations: None,
631            }],
632        });
633        // Even with function response, skip_summarization makes it final
634        assert!(event.is_final_response());
635    }
636
637    #[test]
638    fn test_is_final_response_long_running_tool_ids() {
639        let mut event = Event::new("inv-123");
640        event.long_running_tool_ids = vec!["process_video".to_string()];
641        event.llm_response.content = Some(Content {
642            role: "model".to_string(),
643            parts: vec![Part::FunctionCall {
644                name: "process_video".to_string(),
645                args: serde_json::json!({"file": "video.mp4"}),
646                id: Some("call_process".to_string()),
647                thought_signature: None,
648            }],
649        });
650        // Has long_running_tool_ids -> final (async operation started)
651        assert!(event.is_final_response());
652    }
653
654    #[test]
655    fn test_function_call_ids() {
656        let mut event = Event::new("inv-123");
657        event.llm_response.content = Some(Content {
658            role: "model".to_string(),
659            parts: vec![
660                Part::FunctionCall {
661                    name: "get_weather".to_string(),
662                    args: serde_json::json!({}),
663                    id: Some("call_1".to_string()),
664                    thought_signature: None,
665                },
666                Part::Text { text: "I'll check the weather".to_string() },
667                Part::FunctionCall {
668                    name: "get_time".to_string(),
669                    args: serde_json::json!({}),
670                    id: Some("call_2".to_string()),
671                    thought_signature: None,
672                },
673            ],
674        });
675
676        let ids = event.function_call_ids();
677        assert_eq!(ids.len(), 2);
678        // Should use actual call IDs, not function names
679        assert!(ids.contains(&"call_1".to_string()));
680        assert!(ids.contains(&"call_2".to_string()));
681    }
682
683    #[test]
684    fn test_function_call_ids_falls_back_to_name() {
685        let mut event = Event::new("inv-123");
686        event.llm_response.content = Some(Content {
687            role: "model".to_string(),
688            parts: vec![Part::FunctionCall {
689                name: "get_weather".to_string(),
690                args: serde_json::json!({}),
691                id: None, // Gemini-style: no explicit ID
692                thought_signature: None,
693            }],
694        });
695
696        let ids = event.function_call_ids();
697        assert_eq!(ids, vec!["get_weather".to_string()]);
698    }
699
700    #[test]
701    fn test_function_call_ids_empty() {
702        let event = Event::new("inv-123");
703        let ids = event.function_call_ids();
704        assert!(ids.is_empty());
705    }
706
707    #[test]
708    fn test_is_final_response_trailing_function_response() {
709        // Text followed by a function response as the last part —
710        // has_trailing_code_execution_result should catch this even though
711        // has_function_responses also catches it.
712        let mut event = Event::new("inv-123");
713        event.llm_response.content = Some(Content {
714            role: "model".to_string(),
715            parts: vec![
716                Part::Text { text: "Running code...".to_string() },
717                Part::FunctionResponse {
718                    function_response: crate::FunctionResponseData::new(
719                        "code_exec",
720                        serde_json::json!({"output": "42"}),
721                    ),
722                    id: Some("call_exec".to_string()),
723                    annotations: None,
724                },
725            ],
726        });
727        // Trailing function response -> NOT final
728        assert!(!event.is_final_response());
729    }
730
731    #[test]
732    fn test_event_roundtrip_with_both_provider_metadata() {
733        let mut event = Event::new("inv-1");
734        event.provider_metadata.insert("adk.tool_progress.stream".into(), "stdout".into());
735        event.provider_metadata.insert("adk.tool_progress.call_id".into(), "call-7".into());
736        event.llm_response.provider_metadata = Some(serde_json::json!({"response_id": "resp-xyz"}));
737
738        let json = serde_json::to_string(&event).expect("serialize");
739        // Round-trip must succeed — regression test for the duplicate
740        // `provider_metadata` flatten collision with LlmResponse.
741        let back: Event = serde_json::from_str(&json)
742            .expect("round-trip must succeed without duplicate field error");
743
744        assert_eq!(
745            back.provider_metadata.get("adk.tool_progress.stream").map(String::as_str),
746            Some("stdout"),
747        );
748        assert_eq!(
749            back.provider_metadata.get("adk.tool_progress.call_id").map(String::as_str),
750            Some("call-7"),
751        );
752        assert_eq!(
753            back.llm_response.provider_metadata,
754            Some(serde_json::json!({"response_id": "resp-xyz"})),
755        );
756    }
757
758    #[test]
759    fn test_is_final_response_text_after_function_response() {
760        // Function response followed by text — the trailing part is text,
761        // so has_trailing_code_execution_result is false, but
762        // has_function_responses is still true.
763        let mut event = Event::new("inv-123");
764        event.llm_response.content = Some(Content {
765            role: "model".to_string(),
766            parts: vec![
767                Part::FunctionResponse {
768                    function_response: crate::FunctionResponseData::new(
769                        "tool",
770                        serde_json::json!({}),
771                    ),
772                    id: Some("call_1".to_string()),
773                    annotations: None,
774                },
775                Part::Text { text: "Done".to_string() },
776            ],
777        });
778        // Still has function responses -> NOT final
779        assert!(!event.is_final_response());
780    }
781}