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    /// Tool-start notifications and provider-native continuation pauses are not final.
365    ///
366    /// An event is considered final if:
367    /// - It has skip_summarization set, OR
368    /// - It has long_running_tool_ids (indicating async operations), OR
369    /// - It has no function calls, no function responses, is not partial,
370    ///   and has no trailing code execution results.
371    ///
372    /// Note: When multiple agents participate in one invocation, there could be
373    /// multiple events with is_final_response() as true, for each participating agent.
374    pub fn is_final_response(&self) -> bool {
375        if self.provider_metadata.contains_key("adk_tool_started")
376            || self
377                .llm_response
378                .provider_metadata
379                .as_ref()
380                .and_then(|metadata| metadata.get("continue_turn"))
381                .and_then(serde_json::Value::as_bool)
382                == Some(true)
383        {
384            return false;
385        }
386
387        // If skip_summarization is set or we have long-running tools, it's final
388        if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
389            return true;
390        }
391
392        // Check content for function calls/responses
393        let has_function_calls = self.has_function_calls();
394        let has_function_responses = self.has_function_responses();
395        let is_partial = self.llm_response.partial;
396        let has_trailing_code_result = self.has_trailing_code_execution_result();
397
398        !has_function_calls && !has_function_responses && !is_partial && !has_trailing_code_result
399    }
400
401    /// Returns true if the event content contains function calls.
402    fn has_function_calls(&self) -> bool {
403        if let Some(content) = &self.llm_response.content {
404            for part in &content.parts {
405                if matches!(part, crate::Part::FunctionCall { .. }) {
406                    return true;
407                }
408            }
409        }
410        false
411    }
412
413    /// Returns true if the event content contains function responses.
414    fn has_function_responses(&self) -> bool {
415        if let Some(content) = &self.llm_response.content {
416            for part in &content.parts {
417                if matches!(part, crate::Part::FunctionResponse { .. }) {
418                    return true;
419                }
420            }
421        }
422        false
423    }
424
425    /// Returns true if the event has a trailing code execution result.
426    #[allow(clippy::match_like_matches_macro)]
427    fn has_trailing_code_execution_result(&self) -> bool {
428        if let Some(content) = &self.llm_response.content
429            && let Some(last_part) = content.parts.last()
430        {
431            // FunctionResponse as the last part indicates a code execution result
432            // that the model still needs to process.
433            return matches!(last_part, crate::Part::FunctionResponse { .. });
434        }
435        false
436    }
437
438    /// Extracts function call IDs from this event's content.
439    /// Used to identify which function calls are associated with long-running tools.
440    pub fn function_call_ids(&self) -> Vec<String> {
441        let mut ids = Vec::new();
442        if let Some(content) = &self.llm_response.content {
443            for part in &content.parts {
444                if let crate::Part::FunctionCall { name, id, .. } = part {
445                    // Use the actual call ID when available (OpenAI-style),
446                    // fall back to name for providers that don't emit IDs (Gemini).
447                    ids.push(id.as_deref().unwrap_or(name).to_string());
448                }
449            }
450        }
451        ids
452    }
453}
454
455/// Whether an event is visible from a given conversation branch.
456///
457/// An event is visible when its branch equals `invocation_branch` or is an
458/// *ancestor* of it. Siblings and descendants are excluded, which is what keeps
459/// concurrent `ParallelAgent` branches from seeing each other's output while
460/// still letting each one see the conversation that led to the fan-out.
461///
462/// An empty branch on either side means "unscoped" and matches everything, so
463/// events written without a branch stay globally visible and callers that never
464/// set a branch are unaffected.
465///
466/// Branch segments are delimited by `.`, and the prefix test requires that
467/// delimiter explicitly: without it `agent_0` would match `agent_00`. This
468/// mirrors ADK Python's `_is_event_belongs_to_branch` and ADK Go's
469/// `eventBelongsToBranch`.
470///
471/// # Example
472///
473/// ```
474/// use adk_core::event_belongs_to_branch;
475///
476/// // Own branch and ancestors are visible.
477/// assert!(event_belongs_to_branch("parent.parallel.a", "parent.parallel.a"));
478/// assert!(event_belongs_to_branch("parent.parallel.a", "parent"));
479///
480/// // A sibling branch is not.
481/// assert!(!event_belongs_to_branch("parent.parallel.a", "parent.parallel.b"));
482///
483/// // Unscoped events remain visible.
484/// assert!(event_belongs_to_branch("parent.parallel.a", ""));
485/// ```
486pub fn event_belongs_to_branch(invocation_branch: &str, event_branch: &str) -> bool {
487    if invocation_branch.is_empty() || event_branch.is_empty() {
488        return true;
489    }
490    if event_branch == invocation_branch {
491        return true;
492    }
493    // Require the delimiter so `agent_0` does not match `agent_00`.
494    invocation_branch.starts_with(event_branch)
495        && invocation_branch.as_bytes().get(event_branch.len()) == Some(&b'.')
496}
497
498#[cfg(test)]
499mod branch_visibility_tests {
500    use super::event_belongs_to_branch;
501
502    #[test]
503    fn own_branch_is_visible() {
504        assert!(event_belongs_to_branch("root.parallel.a", "root.parallel.a"));
505    }
506
507    #[test]
508    fn ancestor_branches_are_visible() {
509        assert!(event_belongs_to_branch("root.parallel.a", "root"));
510        assert!(event_belongs_to_branch("root.parallel.a", "root.parallel"));
511    }
512
513    #[test]
514    fn sibling_branches_are_hidden() {
515        assert!(!event_belongs_to_branch("root.parallel.a", "root.parallel.b"));
516        assert!(!event_belongs_to_branch("root.parallel.b", "root.parallel.a"));
517    }
518
519    #[test]
520    fn descendant_branches_are_hidden() {
521        // A parent must not see what a nested child produced in its own branch.
522        assert!(!event_belongs_to_branch("root", "root.parallel.a"));
523    }
524
525    #[test]
526    fn empty_branch_on_either_side_matches() {
527        assert!(event_belongs_to_branch("", "root.parallel.a"));
528        assert!(event_belongs_to_branch("root.parallel.a", ""));
529        assert!(event_belongs_to_branch("", ""));
530    }
531
532    #[test]
533    fn prefix_match_requires_the_delimiter() {
534        // The bug the delimiter guards against: `agent_0` vs `agent_00`.
535        assert!(!event_belongs_to_branch("root.agent_00", "root.agent_0"));
536        assert!(event_belongs_to_branch("root.agent_0.child", "root.agent_0"));
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    use crate::Part;
544
545    #[test]
546    fn test_event_creation() {
547        let event = Event::new("inv-123");
548        assert_eq!(event.invocation_id, "inv-123");
549        assert!(!event.id.is_empty());
550    }
551
552    #[test]
553    fn test_event_actions_default() {
554        let actions = EventActions::default();
555        assert!(actions.state_delta.is_empty());
556        assert!(!actions.skip_summarization);
557        assert!(actions.tool_confirmation.is_none());
558        assert!(actions.tool_confirmation_decision.is_none());
559    }
560
561    #[test]
562    fn test_state_prefixes() {
563        assert_eq!(KEY_PREFIX_APP, "app:");
564        assert_eq!(KEY_PREFIX_TEMP, "temp:");
565        assert_eq!(KEY_PREFIX_USER, "user:");
566    }
567
568    #[test]
569    fn test_is_final_response_no_content() {
570        let event = Event::new("inv-123");
571        // No content, no function calls -> final
572        assert!(event.is_final_response());
573    }
574
575    #[test]
576    fn tool_start_is_not_final() {
577        let mut event = Event::new("inv-123");
578        event.provider_metadata.insert(
579            "adk_tool_started".into(),
580            serde_json::json!({"id": "call-1", "name": "search"}).to_string(),
581        );
582        assert!(!event.llm_response.partial);
583        assert!(!event.is_final_response());
584    }
585
586    #[test]
587    fn provider_continuation_is_not_final() {
588        for content in [None, Some(Content::new("model").with_text("Searching"))] {
589            let mut event = Event::new("inv-123");
590            event.llm_response.content = content;
591            event.llm_response.provider_metadata = Some(serde_json::json!({"continue_turn": true}));
592            assert!(!event.is_final_response());
593            event.llm_response.provider_metadata =
594                Some(serde_json::json!({"continue_turn": false}));
595            assert!(event.is_final_response());
596        }
597    }
598
599    #[test]
600    fn test_is_final_response_text_only() {
601        let mut event = Event::new("inv-123");
602        event.llm_response.content = Some(Content {
603            role: "model".to_string(),
604            parts: vec![Part::Text { text: "Hello!".to_string() }],
605        });
606        // Text only, no function calls -> final
607        assert!(event.is_final_response());
608    }
609
610    #[test]
611    fn test_is_final_response_with_function_call() {
612        let mut event = Event::new("inv-123");
613        event.llm_response.content = Some(Content {
614            role: "model".to_string(),
615            parts: vec![Part::FunctionCall {
616                name: "get_weather".to_string(),
617                args: serde_json::json!({"city": "NYC"}),
618                id: Some("call_123".to_string()),
619                thought_signature: None,
620            }],
621        });
622        // Has function call -> NOT final (need to execute it)
623        assert!(!event.is_final_response());
624    }
625
626    #[test]
627    fn test_is_final_response_with_function_response() {
628        let mut event = Event::new("inv-123");
629        event.llm_response.content = Some(Content {
630            role: "function".to_string(),
631            parts: vec![Part::FunctionResponse {
632                function_response: crate::FunctionResponseData::new(
633                    "get_weather",
634                    serde_json::json!({"temp": 72}),
635                ),
636                id: Some("call_123".to_string()),
637                annotations: None,
638            }],
639        });
640        // Has function response -> NOT final (model needs to respond)
641        assert!(!event.is_final_response());
642    }
643
644    #[test]
645    fn test_is_final_response_partial() {
646        let mut event = Event::new("inv-123");
647        event.llm_response.partial = true;
648        event.llm_response.content = Some(Content {
649            role: "model".to_string(),
650            parts: vec![Part::Text { text: "Hello...".to_string() }],
651        });
652        // Partial response -> NOT final
653        assert!(!event.is_final_response());
654    }
655
656    #[test]
657    fn test_is_final_response_skip_summarization() {
658        let mut event = Event::new("inv-123");
659        event.actions.skip_summarization = true;
660        event.llm_response.content = Some(Content {
661            role: "function".to_string(),
662            parts: vec![Part::FunctionResponse {
663                function_response: crate::FunctionResponseData::new(
664                    "tool",
665                    serde_json::json!({"result": "done"}),
666                ),
667                id: Some("call_tool".to_string()),
668                annotations: None,
669            }],
670        });
671        // Even with function response, skip_summarization makes it final
672        assert!(event.is_final_response());
673    }
674
675    #[test]
676    fn test_is_final_response_long_running_tool_ids() {
677        let mut event = Event::new("inv-123");
678        event.long_running_tool_ids = vec!["process_video".to_string()];
679        event.llm_response.content = Some(Content {
680            role: "model".to_string(),
681            parts: vec![Part::FunctionCall {
682                name: "process_video".to_string(),
683                args: serde_json::json!({"file": "video.mp4"}),
684                id: Some("call_process".to_string()),
685                thought_signature: None,
686            }],
687        });
688        // Has long_running_tool_ids -> final (async operation started)
689        assert!(event.is_final_response());
690    }
691
692    #[test]
693    fn test_function_call_ids() {
694        let mut event = Event::new("inv-123");
695        event.llm_response.content = Some(Content {
696            role: "model".to_string(),
697            parts: vec![
698                Part::FunctionCall {
699                    name: "get_weather".to_string(),
700                    args: serde_json::json!({}),
701                    id: Some("call_1".to_string()),
702                    thought_signature: None,
703                },
704                Part::Text { text: "I'll check the weather".to_string() },
705                Part::FunctionCall {
706                    name: "get_time".to_string(),
707                    args: serde_json::json!({}),
708                    id: Some("call_2".to_string()),
709                    thought_signature: None,
710                },
711            ],
712        });
713
714        let ids = event.function_call_ids();
715        assert_eq!(ids.len(), 2);
716        // Should use actual call IDs, not function names
717        assert!(ids.contains(&"call_1".to_string()));
718        assert!(ids.contains(&"call_2".to_string()));
719    }
720
721    #[test]
722    fn test_function_call_ids_falls_back_to_name() {
723        let mut event = Event::new("inv-123");
724        event.llm_response.content = Some(Content {
725            role: "model".to_string(),
726            parts: vec![Part::FunctionCall {
727                name: "get_weather".to_string(),
728                args: serde_json::json!({}),
729                id: None, // Gemini-style: no explicit ID
730                thought_signature: None,
731            }],
732        });
733
734        let ids = event.function_call_ids();
735        assert_eq!(ids, vec!["get_weather".to_string()]);
736    }
737
738    #[test]
739    fn test_function_call_ids_empty() {
740        let event = Event::new("inv-123");
741        let ids = event.function_call_ids();
742        assert!(ids.is_empty());
743    }
744
745    #[test]
746    fn test_is_final_response_trailing_function_response() {
747        // Text followed by a function response as the last part —
748        // has_trailing_code_execution_result should catch this even though
749        // has_function_responses also catches it.
750        let mut event = Event::new("inv-123");
751        event.llm_response.content = Some(Content {
752            role: "model".to_string(),
753            parts: vec![
754                Part::Text { text: "Running code...".to_string() },
755                Part::FunctionResponse {
756                    function_response: crate::FunctionResponseData::new(
757                        "code_exec",
758                        serde_json::json!({"output": "42"}),
759                    ),
760                    id: Some("call_exec".to_string()),
761                    annotations: None,
762                },
763            ],
764        });
765        // Trailing function response -> NOT final
766        assert!(!event.is_final_response());
767    }
768
769    #[test]
770    fn test_event_roundtrip_with_both_provider_metadata() {
771        let mut event = Event::new("inv-1");
772        event.provider_metadata.insert("adk.tool_progress.stream".into(), "stdout".into());
773        event.provider_metadata.insert("adk.tool_progress.call_id".into(), "call-7".into());
774        event.llm_response.provider_metadata = Some(serde_json::json!({"response_id": "resp-xyz"}));
775
776        let json = serde_json::to_string(&event).expect("serialize");
777        // Round-trip must succeed — regression test for the duplicate
778        // `provider_metadata` flatten collision with LlmResponse.
779        let back: Event = serde_json::from_str(&json)
780            .expect("round-trip must succeed without duplicate field error");
781
782        assert_eq!(
783            back.provider_metadata.get("adk.tool_progress.stream").map(String::as_str),
784            Some("stdout"),
785        );
786        assert_eq!(
787            back.provider_metadata.get("adk.tool_progress.call_id").map(String::as_str),
788            Some("call-7"),
789        );
790        assert_eq!(
791            back.llm_response.provider_metadata,
792            Some(serde_json::json!({"response_id": "resp-xyz"})),
793        );
794    }
795
796    #[test]
797    fn test_is_final_response_text_after_function_response() {
798        // Function response followed by text — the trailing part is text,
799        // so has_trailing_code_execution_result is false, but
800        // has_function_responses is still true.
801        let mut event = Event::new("inv-123");
802        event.llm_response.content = Some(Content {
803            role: "model".to_string(),
804            parts: vec![
805                Part::FunctionResponse {
806                    function_response: crate::FunctionResponseData::new(
807                        "tool",
808                        serde_json::json!({}),
809                    ),
810                    id: Some("call_1".to_string()),
811                    annotations: None,
812                },
813                Part::Text { text: "Done".to_string() },
814            ],
815        });
816        // Still has function responses -> NOT final
817        assert!(!event.is_final_response());
818    }
819}