Skip to main content

genai_rs/
streaming.rs

1//! Streaming types for automatic function calling.
2//!
3//! This module contains types for streaming responses with automatic function execution.
4//! The [`AutoFunctionStreamChunk`] enum provides events for tracking the progress of
5//! an interaction that may involve multiple function call rounds. Events are wrapped
6//! in [`AutoFunctionStreamEvent`] to include `event_id` for stream resume support.
7//!
8//! # Example
9//!
10//! ```no_run
11//! use futures_util::StreamExt;
12//! use genai_rs::{Client, AutoFunctionStreamChunk};
13//!
14//! # async fn example() -> Result<(), genai_rs::GenaiError> {
15//! let client = Client::new("your-api-key".to_string());
16//!
17//! let mut stream = client
18//!     .interaction()
19//!     .with_model("gemini-3-flash-preview")
20//!     .with_text("What's the weather in London?")
21//!     .create_stream_with_auto_functions();
22//!
23//! let mut last_event_id = None;
24//! while let Some(result) = stream.next().await {
25//!     let event = result?;
26//!     // Track event_id for potential resume
27//!     if event.event_id.is_some() {
28//!         last_event_id = event.event_id.clone();
29//!     }
30//!
31//!     match &event.chunk {
32//!         AutoFunctionStreamChunk::Delta(delta) => {
33//!             if let Some(t) = delta.as_text() {
34//!                 print!("{}", t);
35//!             }
36//!         }
37//!         AutoFunctionStreamChunk::ExecutingFunctions { pending_calls, .. } => {
38//!             println!("[Executing: {:?}]", pending_calls.iter().map(|c| &c.name).collect::<Vec<_>>());
39//!         }
40//!         AutoFunctionStreamChunk::FunctionResults(results) => {
41//!             println!("[Got {} results]", results.len());
42//!         }
43//!         AutoFunctionStreamChunk::Complete(_response) => {
44//!             println!("[Done]");
45//!         }
46//!         _ => {} // Handle unknown future variants
47//!     }
48//! }
49//! # Ok(())
50//! # }
51//! ```
52
53use std::time::Duration;
54
55use crate::{InteractionResponse, StepDelta};
56use serde::{Deserialize, Serialize};
57
58/// A function call that is about to be executed.
59///
60/// This represents a function call detected during streaming but not yet executed.
61/// It contains the call metadata (name, ID, args) but not the result, which will
62/// be available in [`FunctionExecutionResult`] after execution completes.
63///
64/// # Example
65///
66/// ```no_run
67/// # use genai_rs::PendingFunctionCall;
68/// # let call: PendingFunctionCall = todo!();
69/// println!("About to execute: {}({})", call.name, call.args);
70/// println!("  Call ID: {}", call.call_id);
71/// ```
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
73#[non_exhaustive]
74pub struct PendingFunctionCall {
75    /// Name of the function to be called
76    pub name: String,
77    /// The call_id from the API (used to match results)
78    pub call_id: String,
79    /// The arguments to pass to the function
80    pub args: serde_json::Value,
81}
82
83impl PendingFunctionCall {
84    /// Creates a new pending function call.
85    #[must_use]
86    pub fn new(
87        name: impl Into<String>,
88        call_id: impl Into<String>,
89        args: serde_json::Value,
90    ) -> Self {
91        Self {
92            name: name.into(),
93            call_id: call_id.into(),
94            args,
95        }
96    }
97}
98
99/// A chunk from streaming with automatic function calling.
100///
101/// This enum represents the different events that can occur during a streaming
102/// interaction with automatic function execution. The stream yields deltas as
103/// content arrives, signals when functions are being executed, and completes
104/// when the model returns a response without function calls.
105///
106/// # Forward Compatibility
107///
108/// This enum uses `#[non_exhaustive]` to allow adding new event types in future
109/// versions without breaking existing code. Always include a wildcard arm in
110/// match statements. Unknown variants are preserved with their data for debugging.
111///
112/// # Serialization
113///
114/// This enum implements `Serialize` and `Deserialize` for logging, persistence,
115/// and replay of streaming events.
116#[derive(Clone, Debug)]
117#[non_exhaustive]
118pub enum AutoFunctionStreamChunk {
119    /// Incremental step payload from the model (text fragments,
120    /// `arguments_delta` function-argument fragments, thought summaries and
121    /// signatures, tool call/result deltas, ...)
122    Delta(StepDelta),
123
124    /// Function calls detected, about to execute.
125    ///
126    /// This event is yielded when the model requests function calls and
127    /// before the functions are executed. The `pending_calls` field contains
128    /// the function calls that are about to be executed.
129    ///
130    /// **Note**: In streaming mode, function calls arrive incrementally via
131    /// `Delta` chunks. The `pending_calls` list is built from accumulated deltas
132    /// and will always be populated, even though `response.function_calls()`
133    /// may be empty.
134    ExecutingFunctions {
135        /// The response from the API (may have empty `function_calls()` in streaming mode)
136        response: InteractionResponse,
137        /// The function calls that are about to be executed (always populated)
138        pending_calls: Vec<PendingFunctionCall>,
139    },
140
141    /// Function execution completed with results.
142    ///
143    /// This event is yielded after all functions in a batch have been executed,
144    /// before sending results back to the model for the next iteration.
145    FunctionResults(Vec<FunctionExecutionResult>),
146
147    /// Final complete response (no more function calls).
148    ///
149    /// This is the last event in the stream, yielded when the model returns
150    /// a response that doesn't request any function calls.
151    Complete(InteractionResponse),
152
153    /// Maximum function call loops reached.
154    ///
155    /// This event is yielded when the auto-function loop has reached the maximum
156    /// number of iterations (set via `with_max_function_call_loops()`) without
157    /// the model returning a response without function calls.
158    ///
159    /// The response contains the last response from the model, which likely still
160    /// contains pending function calls. Use [`AutoFunctionResultAccumulator`] to
161    /// collect all function execution results from prior `FunctionResults` chunks.
162    ///
163    /// This allows debugging why the model is stuck in a loop while preserving
164    /// all partial results.
165    MaxLoopsReached(InteractionResponse),
166
167    /// Unknown event type (for forward compatibility).
168    ///
169    /// This variant is used when deserializing JSON that contains an unrecognized
170    /// `chunk_type`. This allows the library to gracefully handle new event types
171    /// added by the API in future versions without failing deserialization.
172    ///
173    /// The `chunk_type` field contains the unrecognized type string, and `data`
174    /// contains the full JSON data for inspection or debugging.
175    Unknown {
176        /// The unrecognized chunk type from the API
177        chunk_type: String,
178        /// The raw JSON data, preserved for debugging and roundtrip serialization
179        data: serde_json::Value,
180    },
181}
182
183impl AutoFunctionStreamChunk {
184    /// Check if this is an unknown chunk type.
185    #[must_use]
186    pub const fn is_unknown(&self) -> bool {
187        matches!(self, Self::Unknown { .. })
188    }
189
190    /// Check if this chunk is a Delta variant.
191    #[must_use]
192    pub const fn is_delta(&self) -> bool {
193        matches!(self, Self::Delta(_))
194    }
195
196    /// Check if this chunk is a Complete variant.
197    #[must_use]
198    pub const fn is_complete(&self) -> bool {
199        matches!(self, Self::Complete(_))
200    }
201
202    /// Returns the chunk type name if this is an unknown chunk type.
203    ///
204    /// Returns `None` for known chunk types.
205    #[must_use]
206    pub fn unknown_chunk_type(&self) -> Option<&str> {
207        match self {
208            Self::Unknown { chunk_type, .. } => Some(chunk_type),
209            _ => None,
210        }
211    }
212
213    /// Returns the raw JSON data if this is an unknown chunk type.
214    ///
215    /// Returns `None` for known chunk types.
216    #[must_use]
217    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
218        match self {
219            Self::Unknown { data, .. } => Some(data),
220            _ => None,
221        }
222    }
223}
224
225impl Serialize for AutoFunctionStreamChunk {
226    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
227    where
228        S: serde::Serializer,
229    {
230        use serde::ser::SerializeMap;
231
232        match self {
233            Self::Delta(content) => {
234                let mut map = serializer.serialize_map(None)?;
235                map.serialize_entry("chunk_type", "delta")?;
236                map.serialize_entry("data", content)?;
237                map.end()
238            }
239            Self::ExecutingFunctions {
240                response,
241                pending_calls,
242            } => {
243                let mut map = serializer.serialize_map(None)?;
244                map.serialize_entry("chunk_type", "executing_functions")?;
245                // Serialize as nested object with both fields
246                let data = serde_json::json!({
247                    "response": response,
248                    "pending_calls": pending_calls,
249                });
250                map.serialize_entry("data", &data)?;
251                map.end()
252            }
253            Self::FunctionResults(results) => {
254                let mut map = serializer.serialize_map(None)?;
255                map.serialize_entry("chunk_type", "function_results")?;
256                map.serialize_entry("data", results)?;
257                map.end()
258            }
259            Self::Complete(response) => {
260                let mut map = serializer.serialize_map(None)?;
261                map.serialize_entry("chunk_type", "complete")?;
262                map.serialize_entry("data", response)?;
263                map.end()
264            }
265            Self::MaxLoopsReached(response) => {
266                let mut map = serializer.serialize_map(None)?;
267                map.serialize_entry("chunk_type", "max_loops_reached")?;
268                map.serialize_entry("data", response)?;
269                map.end()
270            }
271            Self::Unknown { chunk_type, data } => {
272                let mut map = serializer.serialize_map(None)?;
273                map.serialize_entry("chunk_type", chunk_type)?;
274                if !data.is_null() {
275                    map.serialize_entry("data", data)?;
276                }
277                map.end()
278            }
279        }
280    }
281}
282
283impl<'de> Deserialize<'de> for AutoFunctionStreamChunk {
284    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
285    where
286        D: serde::Deserializer<'de>,
287    {
288        let value = serde_json::Value::deserialize(deserializer)?;
289
290        // Helper to extract "data" field with consistent warning for missing data
291        fn extract_data_field(value: &serde_json::Value, variant_name: &str) -> serde_json::Value {
292            match value.get("data").cloned() {
293                Some(d) => d,
294                None => {
295                    tracing::warn!(
296                        "AutoFunctionStreamChunk::{} is missing the 'data' field. \
297                         This may indicate a malformed API response.",
298                        variant_name
299                    );
300                    serde_json::Value::Null
301                }
302            }
303        }
304
305        let chunk_type = match value.get("chunk_type") {
306            Some(serde_json::Value::String(s)) => s.as_str(),
307            Some(other) => {
308                tracing::warn!(
309                    "AutoFunctionStreamChunk received non-string chunk_type: {}. \
310                     This may indicate a malformed API response.",
311                    other
312                );
313                "<non-string chunk_type>"
314            }
315            None => {
316                tracing::warn!(
317                    "AutoFunctionStreamChunk is missing required chunk_type field. \
318                     This may indicate a malformed API response."
319                );
320                "<missing chunk_type>"
321            }
322        };
323
324        match chunk_type {
325            "delta" => {
326                let data = extract_data_field(&value, "Delta");
327                let delta: StepDelta = serde_json::from_value(data).map_err(|e| {
328                    serde::de::Error::custom(format!(
329                        "Failed to deserialize AutoFunctionStreamChunk::Delta data: {}",
330                        e
331                    ))
332                })?;
333                Ok(Self::Delta(delta))
334            }
335            "executing_functions" => {
336                let data = extract_data_field(&value, "ExecutingFunctions");
337
338                let response = serde_json::from_value(
339                    data.get("response")
340                        .cloned()
341                        .unwrap_or(serde_json::Value::Null),
342                )
343                .map_err(|e| {
344                    serde::de::Error::custom(format!(
345                        "Failed to deserialize ExecutingFunctions response: {}",
346                        e
347                    ))
348                })?;
349                let pending_calls = serde_json::from_value(
350                    data.get("pending_calls")
351                        .cloned()
352                        .unwrap_or(serde_json::json!([])),
353                )
354                .map_err(|e| {
355                    serde::de::Error::custom(format!(
356                        "Failed to deserialize ExecutingFunctions pending_calls: {}",
357                        e
358                    ))
359                })?;
360
361                Ok(Self::ExecutingFunctions {
362                    response,
363                    pending_calls,
364                })
365            }
366            "function_results" => {
367                let data = extract_data_field(&value, "FunctionResults");
368                let results: Vec<FunctionExecutionResult> =
369                    serde_json::from_value(data).map_err(|e| {
370                        serde::de::Error::custom(format!(
371                            "Failed to deserialize AutoFunctionStreamChunk::FunctionResults data: {}",
372                            e
373                        ))
374                    })?;
375                Ok(Self::FunctionResults(results))
376            }
377            "complete" => {
378                let data = extract_data_field(&value, "Complete");
379                let response: InteractionResponse = serde_json::from_value(data).map_err(|e| {
380                    serde::de::Error::custom(format!(
381                        "Failed to deserialize AutoFunctionStreamChunk::Complete data: {}",
382                        e
383                    ))
384                })?;
385                Ok(Self::Complete(response))
386            }
387            "max_loops_reached" => {
388                let data = extract_data_field(&value, "MaxLoopsReached");
389                let response: InteractionResponse = serde_json::from_value(data).map_err(|e| {
390                    serde::de::Error::custom(format!(
391                        "Failed to deserialize AutoFunctionStreamChunk::MaxLoopsReached data: {}",
392                        e
393                    ))
394                })?;
395                Ok(Self::MaxLoopsReached(response))
396            }
397            other => {
398                tracing::warn!(
399                    "Encountered unknown AutoFunctionStreamChunk type '{}'. \
400                     This may indicate a new API feature. \
401                     The chunk will be preserved in the Unknown variant.",
402                    other
403                );
404                let data = value
405                    .get("data")
406                    .cloned()
407                    .unwrap_or(serde_json::Value::Null);
408                Ok(Self::Unknown {
409                    chunk_type: other.to_string(),
410                    data,
411                })
412            }
413        }
414    }
415}
416
417/// Streaming event with position metadata for auto-function stream resumption.
418///
419/// This wrapper pairs an [`AutoFunctionStreamChunk`] with its `event_id`,
420/// enabling stream resumption after network interruptions or reconnects.
421///
422/// # Stream Resumption
423///
424/// Save the `event_id` from each event. If the connection drops, you can resume
425/// the stream from the last received event by calling `get_interaction_stream()`
426/// with the saved `event_id`.
427///
428/// **Note**: The auto-function streaming loop is client-side. If interrupted during
429/// function execution, you may need to restart the full loop rather than resuming.
430/// However, the underlying API stream can be resumed.
431///
432/// # Example
433///
434/// ```no_run
435/// use futures_util::StreamExt;
436/// use genai_rs::{Client, AutoFunctionStreamEvent, AutoFunctionStreamChunk};
437///
438/// # async fn example() -> Result<(), genai_rs::GenaiError> {
439/// let client = Client::new("your-api-key".to_string());
440///
441/// let mut stream = client
442///     .interaction()
443///     .with_model("gemini-3-flash-preview")
444///     .with_text("What's the weather in London?")
445///     .create_stream_with_auto_functions();
446///
447/// let mut last_event_id: Option<String> = None;
448///
449/// while let Some(event) = stream.next().await {
450///     let event = event?;
451///
452///     // Save event_id for potential resume
453///     if let Some(id) = &event.event_id {
454///         last_event_id = Some(id.clone());
455///     }
456///
457///     match &event.chunk {
458///         AutoFunctionStreamChunk::Delta(delta) => {
459///             if let Some(text) = delta.as_text() {
460///                 print!("{}", text);
461///             }
462///         }
463///         AutoFunctionStreamChunk::Complete(_) => {
464///             println!("\n[Done]");
465///         }
466///         _ => {}
467///     }
468/// }
469/// # Ok(())
470/// # }
471/// ```
472#[derive(Clone, Debug)]
473#[non_exhaustive]
474pub struct AutoFunctionStreamEvent {
475    /// The auto-function stream chunk content
476    pub chunk: AutoFunctionStreamChunk,
477    /// Event ID for stream resumption.
478    ///
479    /// Pass this to `last_event_id` when resuming a stream to continue from
480    /// this position. This is the `event_id` from the underlying API stream.
481    ///
482    /// May be `None` for client-generated events (like `ExecutingFunctions`
483    /// and `FunctionResults`) that don't come from the API stream.
484    pub event_id: Option<String>,
485}
486
487impl AutoFunctionStreamEvent {
488    /// Creates a new auto-function stream event.
489    #[must_use]
490    pub fn new(chunk: AutoFunctionStreamChunk, event_id: Option<String>) -> Self {
491        Self { chunk, event_id }
492    }
493
494    /// Check if the inner chunk is a Delta variant.
495    #[must_use]
496    pub const fn is_delta(&self) -> bool {
497        self.chunk.is_delta()
498    }
499
500    /// Check if the inner chunk is a Complete variant.
501    #[must_use]
502    pub const fn is_complete(&self) -> bool {
503        self.chunk.is_complete()
504    }
505
506    /// Check if the inner chunk is an Unknown variant.
507    #[must_use]
508    pub const fn is_unknown(&self) -> bool {
509        self.chunk.is_unknown()
510    }
511
512    /// Returns the unrecognized chunk type if this is an Unknown variant.
513    #[must_use]
514    pub fn unknown_chunk_type(&self) -> Option<&str> {
515        self.chunk.unknown_chunk_type()
516    }
517
518    /// Returns the preserved JSON data if this is an Unknown variant.
519    #[must_use]
520    pub fn unknown_data(&self) -> Option<&serde_json::Value> {
521        self.chunk.unknown_data()
522    }
523}
524
525impl Serialize for AutoFunctionStreamEvent {
526    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
527    where
528        S: serde::Serializer,
529    {
530        use serde::ser::SerializeMap;
531
532        let mut map = serializer.serialize_map(None)?;
533        map.serialize_entry("chunk", &self.chunk)?;
534        if let Some(id) = &self.event_id {
535            map.serialize_entry("event_id", id)?;
536        }
537        map.end()
538    }
539}
540
541impl<'de> Deserialize<'de> for AutoFunctionStreamEvent {
542    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
543    where
544        D: serde::Deserializer<'de>,
545    {
546        let value = serde_json::Value::deserialize(deserializer)?;
547
548        let chunk = match value.get("chunk") {
549            Some(chunk_value) => {
550                serde_json::from_value(chunk_value.clone()).map_err(serde::de::Error::custom)?
551            }
552            None => {
553                return Err(serde::de::Error::missing_field("chunk"));
554            }
555        };
556
557        let event_id = value
558            .get("event_id")
559            .and_then(|v| v.as_str())
560            .map(String::from);
561
562        Ok(Self { chunk, event_id })
563    }
564}
565
566/// Result of executing a function locally.
567///
568/// This represents the output from a function that was executed by the library
569/// during automatic function calling. It contains the function name, the call ID
570/// (used to match with the original request), the arguments that were passed,
571/// and the result value.
572///
573/// # Example
574///
575/// ```no_run
576/// # use genai_rs::FunctionExecutionResult;
577/// # let result: FunctionExecutionResult = todo!();
578/// println!("Function {} called with: {}", result.name, result.args);
579/// println!("  Returned: {}", result.result);
580/// println!("  Call ID: {}, Duration: {:?}", result.call_id, result.duration);
581/// ```
582#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
583#[non_exhaustive]
584pub struct FunctionExecutionResult {
585    /// Name of the function that was called
586    pub name: String,
587    /// The call_id from the FunctionCall this result responds to
588    pub call_id: String,
589    /// The arguments passed to the function
590    pub args: serde_json::Value,
591    /// The result returned by the function
592    pub result: serde_json::Value,
593    /// How long the function took to execute
594    #[serde(with = "duration_millis")]
595    pub duration: Duration,
596}
597
598impl FunctionExecutionResult {
599    /// Creates a new function execution result.
600    #[must_use]
601    pub fn new(
602        name: impl Into<String>,
603        call_id: impl Into<String>,
604        args: serde_json::Value,
605        result: serde_json::Value,
606        duration: Duration,
607    ) -> Self {
608        Self {
609            name: name.into(),
610            call_id: call_id.into(),
611            args,
612            result,
613            duration,
614        }
615    }
616
617    /// Returns true if this execution resulted in an error.
618    ///
619    /// Errors occur when:
620    /// - The function was not found in the registry or tool service
621    /// - The function execution failed (panicked or returned an error)
622    ///
623    /// # Example
624    ///
625    /// ```ignore
626    /// let result = client.interaction()
627    ///     .with_text("What's the weather?")
628    ///     .create_with_auto_functions()
629    ///     .await?;
630    ///
631    /// for execution in &result.executions {
632    ///     if execution.is_error() {
633    ///         eprintln!("Function {} failed: {:?}", execution.name, execution.result);
634    ///     }
635    /// }
636    /// ```
637    #[must_use]
638    pub fn is_error(&self) -> bool {
639        self.result.get("error").is_some()
640    }
641
642    /// Returns true if this execution succeeded (no error).
643    #[must_use]
644    pub fn is_success(&self) -> bool {
645        !self.is_error()
646    }
647
648    /// Returns the error message if this execution failed, None otherwise.
649    #[must_use]
650    pub fn error_message(&self) -> Option<&str> {
651        self.result.get("error").and_then(|v| v.as_str())
652    }
653}
654
655/// Serialize Duration as milliseconds for JSON compatibility
656mod duration_millis {
657    use serde::{Deserialize, Deserializer, Serialize, Serializer};
658    use std::time::Duration;
659
660    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
661    where
662        S: Serializer,
663    {
664        duration.as_millis().serialize(serializer)
665    }
666
667    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
668    where
669        D: Deserializer<'de>,
670    {
671        let millis = u64::deserialize(deserializer)?;
672        Ok(Duration::from_millis(millis))
673    }
674}
675
676/// Result from `create_with_auto_functions()` containing the final response
677/// and a history of all function executions.
678///
679/// This type provides visibility into which functions were called during
680/// automatic function execution, useful for debugging, logging, and evaluation.
681///
682/// # Memory Considerations
683///
684/// The `executions` vector accumulates all [`FunctionExecutionResult`] records
685/// from each iteration of the auto-function loop. For typical use cases (1-5
686/// iterations with 1-3 functions each), this is negligible.
687///
688/// For edge cases with many function calls or large result payloads, consider:
689///
690/// - **Limit iterations**: Use [`crate::InteractionBuilder::with_max_function_call_loops()`]
691///   to cap the maximum number of iterations (default: 5)
692/// - **Extract and drop**: Extract only the data you need, then drop the result
693/// - **Manual control**: For fine-grained memory management, implement function
694///   calling manually using [`crate::InteractionBuilder::add_functions()`] and
695///   [`crate::InteractionBuilder::create()`] instead of the auto-function helpers
696///
697/// Each `FunctionExecutionResult` contains the function name, call ID, result
698/// value (as `serde_json::Value`), and execution duration. Memory usage scales
699/// primarily with the size of function result payloads.
700///
701/// # Example
702///
703/// ```no_run
704/// # use genai_rs::{Client, AutoFunctionResult};
705/// # async fn example() -> Result<(), genai_rs::GenaiError> {
706/// # let client = Client::new("key".to_string());
707/// let result = client
708///     .interaction()
709///     .with_model("gemini-3-flash-preview")
710///     .with_text("What's the weather in London?")
711///     .create_with_auto_functions()
712///     .await?;
713///
714/// // Access the final response
715/// if let Some(text) = result.response.as_text() {
716///     println!("Answer: {}", text);
717/// }
718///
719/// // Access execution history
720/// for exec in &result.executions {
721///     println!("Called {} -> {}", exec.name, exec.result);
722/// }
723/// # Ok(())
724/// # }
725/// ```
726#[derive(Clone, Debug, Serialize, Deserialize)]
727#[non_exhaustive]
728pub struct AutoFunctionResult {
729    /// The final response from the model (after all function calls completed)
730    pub response: InteractionResponse,
731    /// All functions that were executed during the auto-function loop
732    pub executions: Vec<FunctionExecutionResult>,
733    /// Whether the auto-function loop was terminated due to reaching the maximum
734    /// number of iterations (set via `with_max_function_call_loops()`).
735    ///
736    /// When `true`, the `response` contains the last response from the model before
737    /// hitting the limit, which likely still contains pending function calls.
738    /// The `executions` vector contains all functions that were successfully executed
739    /// before the limit was reached.
740    ///
741    /// This allows debugging why the model is stuck in a loop and preserves
742    /// partial results that may still be useful.
743    #[serde(default)]
744    pub reached_max_loops: bool,
745}
746
747impl AutoFunctionResult {
748    /// Returns true if all function executions succeeded (no errors).
749    ///
750    /// This is useful for detecting missing function implementations or
751    /// function execution failures that would otherwise be silently
752    /// sent to the model as error results.
753    ///
754    /// # Example
755    ///
756    /// ```ignore
757    /// let result = client.interaction()
758    ///     .with_text("What's the weather?")
759    ///     .add_functions(vec![get_weather_function()])
760    ///     .create_with_auto_functions()
761    ///     .await?;
762    ///
763    /// assert!(result.all_executions_succeeded(),
764    ///     "Function executions failed: {:?}",
765    ///     result.failed_executions());
766    /// ```
767    #[must_use]
768    pub fn all_executions_succeeded(&self) -> bool {
769        self.executions.iter().all(|e| e.is_success())
770    }
771
772    /// Returns all executions that failed (had errors).
773    #[must_use]
774    pub fn failed_executions(&self) -> Vec<&FunctionExecutionResult> {
775        self.executions.iter().filter(|e| e.is_error()).collect()
776    }
777}
778
779/// Accumulator for building [`AutoFunctionResult`] from a stream of [`AutoFunctionStreamChunk`].
780///
781/// This helper collects all function execution results and the final response from
782/// a streaming auto-function interaction, producing the same result type as the
783/// non-streaming `create_with_auto_functions()` method.
784///
785/// # Example
786///
787/// ```no_run
788/// use futures_util::StreamExt;
789/// use genai_rs::{Client, AutoFunctionStreamChunk, AutoFunctionResultAccumulator};
790///
791/// # async fn example() -> Result<(), genai_rs::GenaiError> {
792/// let client = Client::new("your-api-key".to_string());
793///
794/// let mut stream = client
795///     .interaction()
796///     .with_model("gemini-3-flash-preview")
797///     .with_text("What's the weather in London?")
798///     .create_stream_with_auto_functions();
799///
800/// let mut accumulator = AutoFunctionResultAccumulator::new();
801///
802/// while let Some(event) = stream.next().await {
803///     let event = event?;
804///
805///     // Process deltas for UI updates
806///     if let AutoFunctionStreamChunk::Delta(delta) = &event.chunk {
807///         if let Some(text) = delta.as_text() {
808///             print!("{}", text);
809///         }
810///     }
811///
812///     // Feed all chunks to the accumulator
813///     if let Some(result) = accumulator.push(event.chunk) {
814///         // Stream is complete, we have the full result
815///         println!("\n\nExecuted {} functions", result.executions.len());
816///         for exec in &result.executions {
817///             println!("  {} took {:?}", exec.name, exec.duration);
818///         }
819///     }
820/// }
821/// # Ok(())
822/// # }
823/// ```
824#[derive(Clone, Debug, Default)]
825pub struct AutoFunctionResultAccumulator {
826    executions: Vec<FunctionExecutionResult>,
827}
828
829impl AutoFunctionResultAccumulator {
830    /// Creates a new empty accumulator.
831    #[must_use]
832    pub fn new() -> Self {
833        Self::default()
834    }
835
836    /// Feeds a chunk to the accumulator.
837    ///
838    /// Returns `Some(AutoFunctionResult)` when the stream ends, either:
839    /// - With a `Complete` chunk (normal completion, `reached_max_loops: false`)
840    /// - With a `MaxLoopsReached` chunk (limit hit, `reached_max_loops: true`)
841    ///
842    /// Returns `None` for all other chunk types.
843    ///
844    /// The accumulator collects all `FunctionResults` chunks and combines them
845    /// with the final response.
846    #[must_use]
847    #[allow(unreachable_patterns)] // Handle future variants from #[non_exhaustive] enum
848    pub fn push(&mut self, chunk: AutoFunctionStreamChunk) -> Option<AutoFunctionResult> {
849        match chunk {
850            AutoFunctionStreamChunk::FunctionResults(results) => {
851                self.executions.extend(results);
852                None
853            }
854            AutoFunctionStreamChunk::Complete(response) => Some(AutoFunctionResult {
855                response,
856                executions: std::mem::take(&mut self.executions),
857                reached_max_loops: false,
858            }),
859            AutoFunctionStreamChunk::MaxLoopsReached(response) => Some(AutoFunctionResult {
860                response,
861                executions: std::mem::take(&mut self.executions),
862                reached_max_loops: true,
863            }),
864            AutoFunctionStreamChunk::Delta(_)
865            | AutoFunctionStreamChunk::ExecutingFunctions { .. } => None,
866            // Handle future variants gracefully
867            _ => None,
868        }
869    }
870
871    /// Returns the accumulated executions so far.
872    ///
873    /// Useful for checking progress without consuming the accumulator.
874    #[must_use]
875    pub fn executions(&self) -> &[FunctionExecutionResult] {
876        &self.executions
877    }
878
879    /// Resets the accumulator to its initial empty state.
880    pub fn reset(&mut self) {
881        self.executions.clear();
882    }
883}
884
885#[cfg(test)]
886mod tests {
887    use super::*;
888    use serde_json::json;
889
890    #[test]
891    fn test_function_execution_result() {
892        let result = FunctionExecutionResult::new(
893            "get_weather",
894            "call-123",
895            json!({"city": "Seattle"}),
896            json!({"temp": 20, "unit": "celsius"}),
897            Duration::from_millis(42),
898        );
899
900        assert_eq!(result.name, "get_weather");
901        assert_eq!(result.call_id, "call-123");
902        assert_eq!(result.args, json!({"city": "Seattle"}));
903        assert_eq!(result.result, json!({"temp": 20, "unit": "celsius"}));
904        assert_eq!(result.duration, Duration::from_millis(42));
905    }
906
907    #[test]
908    fn test_auto_function_stream_chunk_variants() {
909        // Test that Delta and FunctionResults variants can be created
910        let _delta = AutoFunctionStreamChunk::Delta(StepDelta::Text {
911            text: "Hello".to_string(),
912        });
913
914        let _results = AutoFunctionStreamChunk::FunctionResults(vec![FunctionExecutionResult {
915            name: "test".to_string(),
916            call_id: "1".to_string(),
917            args: json!({}),
918            result: json!({"ok": true}),
919            duration: Duration::from_millis(10),
920        }]);
921
922        let _complete = AutoFunctionStreamChunk::Complete(InteractionResponse::default());
923    }
924
925    #[test]
926    fn test_function_execution_result_serialization() {
927        let result = FunctionExecutionResult::new(
928            "get_weather",
929            "call-456",
930            json!({"city": "Miami"}),
931            json!({"temp": 22, "conditions": "sunny"}),
932            Duration::from_millis(150),
933        );
934
935        let json_str = serde_json::to_string(&result).expect("Serialization should succeed");
936
937        // Verify key fields are present in serialized output
938        assert!(
939            json_str.contains("get_weather"),
940            "Should contain function name"
941        );
942        assert!(json_str.contains("call-456"), "Should contain call_id");
943        assert!(json_str.contains("sunny"), "Should contain result data");
944
945        // Verify full roundtrip
946        let deserialized: FunctionExecutionResult =
947            serde_json::from_str(&json_str).expect("Deserialization should succeed");
948        assert_eq!(deserialized, result);
949    }
950
951    #[test]
952    fn test_auto_function_stream_chunk_serialization_roundtrip() {
953        // Test Delta variant roundtrip (payload is the StepDelta wire form)
954        let delta = AutoFunctionStreamChunk::Delta(StepDelta::Text {
955            text: "Hello, world!".to_string(),
956        });
957
958        let json_str = serde_json::to_string(&delta).expect("Serialization should succeed");
959        assert!(json_str.contains("chunk_type"), "Should contain tag field");
960        assert!(json_str.contains("delta"), "Should contain variant name");
961        assert!(json_str.contains("Hello, world!"), "Should contain text");
962
963        // The delta payload uses the StepDelta wire form: {"type":"text","text":"..."}
964        let value: serde_json::Value = serde_json::from_str(&json_str).unwrap();
965        assert_eq!(value["data"]["type"], "text");
966        assert_eq!(value["data"]["text"], "Hello, world!");
967
968        let deserialized: AutoFunctionStreamChunk =
969            serde_json::from_str(&json_str).expect("Deserialization should succeed");
970
971        match deserialized {
972            AutoFunctionStreamChunk::Delta(delta) => {
973                assert_eq!(delta.as_text(), Some("Hello, world!"));
974            }
975            _ => panic!("Expected Delta variant"),
976        }
977
978        // Test FunctionResults variant roundtrip
979        let results = AutoFunctionStreamChunk::FunctionResults(vec![
980            FunctionExecutionResult::new(
981                "get_weather",
982                "call-1",
983                json!({"city": "Tokyo"}),
984                json!({"temp": 20}),
985                Duration::from_millis(50),
986            ),
987            FunctionExecutionResult::new(
988                "get_time",
989                "call-2",
990                json!({"timezone": "UTC"}),
991                json!({"time": "14:30"}),
992                Duration::from_millis(30),
993            ),
994        ]);
995
996        let json_str = serde_json::to_string(&results).expect("Serialization should succeed");
997        let deserialized: AutoFunctionStreamChunk =
998            serde_json::from_str(&json_str).expect("Deserialization should succeed");
999
1000        match deserialized {
1001            AutoFunctionStreamChunk::FunctionResults(execs) => {
1002                assert_eq!(execs.len(), 2);
1003                assert_eq!(execs[0].name, "get_weather");
1004                assert_eq!(execs[1].name, "get_time");
1005            }
1006            _ => panic!("Expected FunctionResults variant"),
1007        }
1008
1009        // Test Unknown variant handling (forward compatibility)
1010        let unknown_json = r#"{"chunk_type": "future_event_type", "data": {"key": "value"}}"#;
1011        let deserialized: AutoFunctionStreamChunk =
1012            serde_json::from_str(unknown_json).expect("Should deserialize unknown variant");
1013
1014        // Verify it's an Unknown variant with data preserved
1015        assert!(deserialized.is_unknown());
1016        assert_eq!(deserialized.unknown_chunk_type(), Some("future_event_type"));
1017        let data = deserialized.unknown_data().expect("Should have data");
1018        assert_eq!(data["key"], "value");
1019
1020        // Verify roundtrip serialization
1021        let reserialized = serde_json::to_string(&deserialized).expect("Should serialize");
1022        assert!(reserialized.contains("future_event_type"));
1023        assert!(reserialized.contains("value"));
1024    }
1025
1026    #[test]
1027    fn test_auto_function_stream_chunk_unknown_without_data() {
1028        // Test unknown chunk type without data field
1029        let unknown_json = r#"{"chunk_type": "no_data_chunk"}"#;
1030        let deserialized: AutoFunctionStreamChunk =
1031            serde_json::from_str(unknown_json).expect("Should deserialize unknown variant");
1032
1033        assert!(deserialized.is_unknown());
1034        assert_eq!(deserialized.unknown_chunk_type(), Some("no_data_chunk"));
1035
1036        // Data should be null when not provided
1037        let data = deserialized.unknown_data().expect("Should have data field");
1038        assert!(data.is_null());
1039    }
1040
1041    #[test]
1042    fn test_auto_function_result_roundtrip() {
1043        use crate::InteractionStatus;
1044
1045        // Create a realistic AutoFunctionResult with multiple executions
1046        let result = AutoFunctionResult {
1047            response: crate::InteractionResponse {
1048                id: Some("interaction-abc123".to_string()),
1049                model: Some("gemini-3-flash-preview".to_string()),
1050                steps: vec![
1051                    crate::Step::model_text("Based on the weather data:"),
1052                    crate::Step::model_text("Paris is 18°C and London is 15°C."),
1053                ],
1054                status: InteractionStatus::Completed,
1055                usage: Some(crate::UsageMetadata {
1056                    total_input_tokens: Some(50),
1057                    total_output_tokens: Some(30),
1058                    total_tokens: Some(80),
1059                    ..Default::default()
1060                }),
1061                previous_interaction_id: Some("prev-interaction-xyz".to_string()),
1062                ..Default::default()
1063            },
1064            executions: vec![
1065                FunctionExecutionResult::new(
1066                    "get_weather",
1067                    "call-001",
1068                    json!({"city": "Paris"}),
1069                    json!({"city": "Paris", "temp": 18, "unit": "celsius"}),
1070                    Duration::from_millis(120),
1071                ),
1072                FunctionExecutionResult::new(
1073                    "get_weather",
1074                    "call-002",
1075                    json!({"city": "London"}),
1076                    json!({"city": "London", "temp": 15, "unit": "celsius"}),
1077                    Duration::from_millis(95),
1078                ),
1079            ],
1080            reached_max_loops: false,
1081        };
1082
1083        // Serialize
1084        let json_str = serde_json::to_string(&result).expect("Serialization should succeed");
1085
1086        // Verify key data is present in JSON
1087        assert!(
1088            json_str.contains("interaction-abc123"),
1089            "Should contain interaction ID"
1090        );
1091        assert!(
1092            json_str.contains("gemini-3-flash-preview"),
1093            "Should contain model name"
1094        );
1095        assert!(
1096            json_str.contains("get_weather"),
1097            "Should contain function name"
1098        );
1099        assert!(
1100            json_str.contains("call-001"),
1101            "Should contain first call_id"
1102        );
1103        assert!(
1104            json_str.contains("call-002"),
1105            "Should contain second call_id"
1106        );
1107        assert!(json_str.contains("Paris"), "Should contain Paris");
1108        assert!(json_str.contains("London"), "Should contain London");
1109        assert!(
1110            json_str.contains("prev-interaction-xyz"),
1111            "Should contain previous interaction ID"
1112        );
1113
1114        // Deserialize
1115        let deserialized: AutoFunctionResult =
1116            serde_json::from_str(&json_str).expect("Deserialization should succeed");
1117
1118        // Verify response fields
1119        assert_eq!(
1120            deserialized.response.id.as_deref(),
1121            Some("interaction-abc123")
1122        );
1123        assert_eq!(
1124            deserialized.response.model,
1125            Some("gemini-3-flash-preview".to_string())
1126        );
1127        assert_eq!(deserialized.response.status, InteractionStatus::Completed);
1128        assert_eq!(
1129            deserialized.response.previous_interaction_id,
1130            Some("prev-interaction-xyz".to_string())
1131        );
1132
1133        // Verify usage metadata
1134        let usage = deserialized.response.usage.expect("Should have usage");
1135        assert_eq!(usage.total_input_tokens, Some(50));
1136        assert_eq!(usage.total_output_tokens, Some(30));
1137        assert_eq!(usage.total_tokens, Some(80));
1138
1139        // Verify executions
1140        assert_eq!(deserialized.executions.len(), 2);
1141        assert_eq!(deserialized.executions[0].name, "get_weather");
1142        assert_eq!(deserialized.executions[0].call_id, "call-001");
1143        assert_eq!(deserialized.executions[0].result["city"], "Paris");
1144        assert_eq!(deserialized.executions[1].name, "get_weather");
1145        assert_eq!(deserialized.executions[1].call_id, "call-002");
1146        assert_eq!(deserialized.executions[1].result["city"], "London");
1147
1148        // Verify reached_max_loops
1149        assert!(!deserialized.reached_max_loops);
1150    }
1151
1152    #[test]
1153    fn test_auto_function_result_reached_max_loops() {
1154        use crate::InteractionStatus;
1155
1156        // Create an AutoFunctionResult with reached_max_loops: true
1157        let result = AutoFunctionResult {
1158            response: crate::InteractionResponse {
1159                id: Some("interaction-stuck".to_string()),
1160                model: Some("gemini-3-flash-preview".to_string()),
1161                steps: vec![crate::Step::function_call(
1162                    "call-stuck",
1163                    "get_weather",
1164                    json!({"city": "Tokyo"}),
1165                )],
1166                status: InteractionStatus::Completed,
1167                ..Default::default()
1168            },
1169            executions: vec![FunctionExecutionResult::new(
1170                "get_weather",
1171                "call-1",
1172                json!({"city": "Berlin"}),
1173                json!({"temp": 25}),
1174                Duration::from_millis(50),
1175            )],
1176            reached_max_loops: true,
1177        };
1178
1179        // Serialize
1180        let json_str = serde_json::to_string(&result).expect("Serialization should succeed");
1181        assert!(
1182            json_str.contains("reached_max_loops"),
1183            "Should contain reached_max_loops field"
1184        );
1185        assert!(json_str.contains("true"), "Should contain true value");
1186
1187        // Deserialize
1188        let deserialized: AutoFunctionResult =
1189            serde_json::from_str(&json_str).expect("Deserialization should succeed");
1190        assert!(deserialized.reached_max_loops);
1191        assert_eq!(deserialized.executions.len(), 1);
1192    }
1193
1194    #[test]
1195    fn test_auto_function_result_backwards_compatibility() {
1196        // Test that JSON without reached_max_loops (from older versions) still deserializes
1197        let legacy_json = r#"{
1198            "response": {
1199                "id": "interaction-old",
1200                "model": "gemini-3-flash-preview",
1201                "steps": [],
1202                "status": "completed"
1203            },
1204            "executions": []
1205        }"#;
1206
1207        let deserialized: AutoFunctionResult =
1208            serde_json::from_str(legacy_json).expect("Should deserialize legacy JSON");
1209
1210        // reached_max_loops should default to false
1211        assert!(
1212            !deserialized.reached_max_loops,
1213            "Missing field should default to false"
1214        );
1215    }
1216
1217    #[test]
1218    fn test_max_loops_reached_chunk_roundtrip() {
1219        use crate::InteractionStatus;
1220
1221        // Create a MaxLoopsReached chunk
1222        let response = crate::InteractionResponse {
1223            id: Some("interaction-max-loops".to_string()),
1224            model: Some("gemini-3-flash-preview".to_string()),
1225            steps: vec![crate::Step::function_call(
1226                "call-pending",
1227                "stuck_function",
1228                json!({}),
1229            )],
1230            status: InteractionStatus::Completed,
1231            ..Default::default()
1232        };
1233
1234        let chunk = AutoFunctionStreamChunk::MaxLoopsReached(response);
1235
1236        // Serialize
1237        let json_str = serde_json::to_string(&chunk).expect("Serialization should succeed");
1238        assert!(
1239            json_str.contains("max_loops_reached"),
1240            "Should contain chunk_type"
1241        );
1242        assert!(
1243            json_str.contains("interaction-max-loops"),
1244            "Should contain response data"
1245        );
1246
1247        // Deserialize
1248        let deserialized: AutoFunctionStreamChunk =
1249            serde_json::from_str(&json_str).expect("Deserialization should succeed");
1250
1251        match deserialized {
1252            AutoFunctionStreamChunk::MaxLoopsReached(resp) => {
1253                assert_eq!(resp.id.as_deref(), Some("interaction-max-loops"));
1254                assert_eq!(resp.function_calls().len(), 1);
1255                assert_eq!(resp.function_calls()[0].name, "stuck_function");
1256            }
1257            other => panic!("Expected MaxLoopsReached, got {:?}", other),
1258        }
1259    }
1260
1261    #[test]
1262    fn test_accumulator_handles_max_loops_reached() {
1263        use crate::InteractionStatus;
1264
1265        let mut accumulator = AutoFunctionResultAccumulator::new();
1266
1267        // Simulate function results being yielded
1268        let results = AutoFunctionStreamChunk::FunctionResults(vec![FunctionExecutionResult::new(
1269            "test_func",
1270            "call-1",
1271            json!({}),
1272            json!({"ok": true}),
1273            Duration::from_millis(10),
1274        )]);
1275
1276        assert!(
1277            accumulator.push(results).is_none(),
1278            "Should not complete yet"
1279        );
1280        assert_eq!(accumulator.executions().len(), 1);
1281
1282        // Simulate MaxLoopsReached being yielded
1283        let response = crate::InteractionResponse {
1284            id: Some("max-loops-response".to_string()),
1285            model: Some("gemini-3-flash-preview".to_string()),
1286            status: InteractionStatus::Completed,
1287            ..Default::default()
1288        };
1289
1290        let max_loops_chunk = AutoFunctionStreamChunk::MaxLoopsReached(response);
1291        let result = accumulator.push(max_loops_chunk);
1292
1293        assert!(result.is_some(), "Should complete on MaxLoopsReached");
1294        let result = result.unwrap();
1295        assert!(
1296            result.reached_max_loops,
1297            "Should have reached_max_loops: true"
1298        );
1299        assert_eq!(result.executions.len(), 1);
1300        assert_eq!(result.response.id.as_deref(), Some("max-loops-response"));
1301    }
1302
1303    #[test]
1304    fn test_auto_function_stream_event_with_event_id_roundtrip() {
1305        let event = AutoFunctionStreamEvent::new(
1306            AutoFunctionStreamChunk::Delta(StepDelta::Text {
1307                text: "Hello from auto-function".to_string(),
1308            }),
1309            Some("evt_auto_abc123".to_string()),
1310        );
1311
1312        // Test helper methods
1313        assert!(event.is_delta());
1314        assert!(!event.is_complete());
1315        assert!(!event.is_unknown());
1316
1317        let json = serde_json::to_string(&event).expect("Serialization should succeed");
1318        assert!(json.contains("evt_auto_abc123"), "Should have event_id");
1319        assert!(
1320            json.contains("Hello from auto-function"),
1321            "Should have content"
1322        );
1323
1324        let deserialized: AutoFunctionStreamEvent =
1325            serde_json::from_str(&json).expect("Deserialization should succeed");
1326        assert_eq!(deserialized.event_id.as_deref(), Some("evt_auto_abc123"));
1327        assert!(deserialized.is_delta());
1328    }
1329
1330    #[test]
1331    fn test_auto_function_stream_event_without_event_id() {
1332        // Client-generated events like FunctionResults don't have event_id
1333        let event = AutoFunctionStreamEvent::new(
1334            AutoFunctionStreamChunk::FunctionResults(vec![FunctionExecutionResult::new(
1335                "weather",
1336                "call-123",
1337                json!({"city": "Denver"}),
1338                json!({"temp": 72}),
1339                Duration::from_millis(50),
1340            )]),
1341            None,
1342        );
1343
1344        assert!(!event.is_delta());
1345        assert!(!event.is_complete());
1346        assert!(event.event_id.is_none());
1347
1348        let json = serde_json::to_string(&event).expect("Serialization should succeed");
1349        assert!(!json.contains("event_id"), "Should not have event_id field");
1350        assert!(json.contains("weather"), "Should have function name");
1351
1352        let deserialized: AutoFunctionStreamEvent =
1353            serde_json::from_str(&json).expect("Deserialization should succeed");
1354        assert!(deserialized.event_id.is_none());
1355    }
1356
1357    #[test]
1358    fn test_auto_function_stream_event_with_empty_event_id() {
1359        // Edge case: empty string event_id should still serialize/deserialize
1360        let event = AutoFunctionStreamEvent::new(
1361            AutoFunctionStreamChunk::Delta(StepDelta::Text {
1362                text: "Test".to_string(),
1363            }),
1364            Some(String::new()),
1365        );
1366
1367        let json = serde_json::to_string(&event).expect("Serialization should succeed");
1368        assert!(
1369            json.contains(r#""event_id":"""#),
1370            "Should have empty event_id"
1371        );
1372
1373        let deserialized: AutoFunctionStreamEvent =
1374            serde_json::from_str(&json).expect("Deserialization should succeed");
1375        assert_eq!(deserialized.event_id.as_deref(), Some(""));
1376    }
1377
1378    #[test]
1379    fn test_pending_function_call() {
1380        let call = PendingFunctionCall::new("get_weather", "call-123", json!({"city": "Seattle"}));
1381
1382        assert_eq!(call.name, "get_weather");
1383        assert_eq!(call.call_id, "call-123");
1384        assert_eq!(call.args, json!({"city": "Seattle"}));
1385    }
1386
1387    #[test]
1388    fn test_pending_function_call_serialization_roundtrip() {
1389        let call = PendingFunctionCall::new("test_func", "id-456", json!({"key": "value"}));
1390
1391        let json_str = serde_json::to_string(&call).expect("Serialization should succeed");
1392        assert!(json_str.contains("test_func"));
1393        assert!(json_str.contains("id-456"));
1394
1395        let deserialized: PendingFunctionCall =
1396            serde_json::from_str(&json_str).expect("Deserialization should succeed");
1397        assert_eq!(deserialized, call);
1398    }
1399
1400    #[test]
1401    fn test_executing_functions_new_format_roundtrip() {
1402        use crate::InteractionStatus;
1403
1404        let chunk = AutoFunctionStreamChunk::ExecutingFunctions {
1405            response: crate::InteractionResponse {
1406                id: Some("interaction-new".to_string()),
1407                model: Some("gemini-3-flash-preview".to_string()),
1408                status: InteractionStatus::Completed,
1409                ..Default::default()
1410            },
1411            pending_calls: vec![
1412                PendingFunctionCall::new("func1", "call-1", json!({"a": 1})),
1413                PendingFunctionCall::new("func2", "call-2", json!({"b": 2})),
1414            ],
1415        };
1416
1417        let json_str = serde_json::to_string(&chunk).expect("Serialization should succeed");
1418        assert!(json_str.contains("pending_calls"));
1419        assert!(json_str.contains("func1"));
1420        assert!(json_str.contains("func2"));
1421
1422        let deserialized: AutoFunctionStreamChunk =
1423            serde_json::from_str(&json_str).expect("Deserialization should succeed");
1424
1425        match deserialized {
1426            AutoFunctionStreamChunk::ExecutingFunctions {
1427                response,
1428                pending_calls,
1429            } => {
1430                assert_eq!(response.id.as_deref(), Some("interaction-new"));
1431                assert_eq!(pending_calls.len(), 2);
1432                assert_eq!(pending_calls[0].name, "func1");
1433                assert_eq!(pending_calls[1].name, "func2");
1434            }
1435            _ => panic!("Expected ExecutingFunctions variant"),
1436        }
1437    }
1438}