Skip to main content

zai_rs/toolkits/
executor.rs

1//! Enhanced tool executor with type-safe builder pattern
2
3use std::{
4    sync::Arc,
5    time::{Duration, Instant},
6};
7
8use dashmap::DashMap;
9use serde::{Deserialize, Serialize};
10use tokio::{task::JoinSet, time::timeout};
11use tracing::warn;
12
13use super::{
14    cache::{CacheKey, ToolCallCache},
15    core::ToolHandler,
16};
17use crate::{
18    model::{
19        chat_base_response::ToolCallMessage,
20        chat_message_types::TextMessage,
21        tools::{Function, Tools},
22    },
23    toolkits::{
24        core::DynTool,
25        error::{ToolError, ToolResult, error_context},
26    },
27};
28
29/// Enhanced retry configuration with exponential backoff
30#[derive(Debug, Clone)]
31pub struct RetryConfig {
32    pub max_retries: u32,
33    pub initial_delay: Duration,
34    pub max_delay: Duration,
35    pub backoff_multiplier: f64,
36}
37
38impl Default for RetryConfig {
39    fn default() -> Self {
40        Self {
41            max_retries: 3,
42            initial_delay: Duration::from_millis(100),
43            max_delay: Duration::from_secs(30),
44            backoff_multiplier: 2.0,
45        }
46    }
47}
48
49impl RetryConfig {
50    pub fn calculate_delay(&self, attempt: u32) -> Duration {
51        if attempt == 0 {
52            return Duration::ZERO;
53        }
54
55        let delay_ms = self.initial_delay.as_millis() as f64
56            * self.backoff_multiplier.powi((attempt - 1) as i32);
57        let delay_ms = delay_ms.min(self.max_delay.as_millis() as f64) as u64;
58
59        Duration::from_millis(delay_ms)
60    }
61}
62
63/// Execution configuration with type-safe builder
64#[derive(Debug, Clone)]
65pub struct ExecutionConfig {
66    pub timeout: Option<Duration>,
67    pub retry_config: RetryConfig,
68    pub validate_parameters: bool,
69    pub enable_logging: bool,
70}
71
72impl Default for ExecutionConfig {
73    fn default() -> Self {
74        Self {
75            timeout: Some(Duration::from_secs(30)),
76            retry_config: RetryConfig::default(),
77            validate_parameters: true,
78            enable_logging: false,
79        }
80    }
81}
82
83/// Execution result with enhanced metadata
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ExecutionResult {
86    pub tool_name: String,
87    pub result: serde_json::Value,
88    pub duration: Duration,
89    pub success: bool,
90    pub error: Option<String>,
91    pub retries: u32,
92    pub timestamp: std::time::SystemTime,
93    pub metadata: std::collections::HashMap<String, serde_json::Value>,
94}
95
96impl ExecutionResult {
97    pub fn success(
98        tool_name: String,
99        result: serde_json::Value,
100        duration: Duration,
101        retries: u32,
102    ) -> Self {
103        Self {
104            tool_name,
105            result,
106            duration,
107            success: true,
108            error: None,
109            retries,
110            timestamp: std::time::SystemTime::now(),
111            metadata: std::collections::HashMap::new(),
112        }
113    }
114
115    pub fn failure(tool_name: String, error: String, duration: Duration, retries: u32) -> Self {
116        Self {
117            tool_name,
118            result: serde_json::Value::Null,
119            duration,
120            success: false,
121            error: Some(error),
122            retries,
123            timestamp: std::time::SystemTime::now(),
124            metadata: std::collections::HashMap::new(),
125        }
126    }
127
128    pub fn with_metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
129        self.metadata.insert(key.into(), value);
130        self
131    }
132}
133
134/// Enhanced tool executor with built-in registry and fluent API
135#[derive(Clone)]
136pub struct ToolExecutor {
137    tools: Arc<DashMap<String, Arc<dyn DynTool>>>,
138    config: ExecutionConfig,
139    cache: ToolCallCache,
140}
141
142impl std::fmt::Debug for ToolExecutor {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        let tool_count = self.tools.len();
145        let cache_enabled = self.cache.stats().total_entries > 0;
146        f.debug_struct("ToolExecutor")
147            .field("tool_count", &tool_count)
148            .field("config", &self.config)
149            .field("cache_enabled", &cache_enabled)
150            .finish()
151    }
152}
153
154impl Default for ToolExecutor {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160impl ToolExecutor {
161    /// Create a new executor with default config
162    pub fn new() -> Self {
163        Self {
164            tools: Arc::new(DashMap::new()),
165            config: ExecutionConfig::default(),
166            cache: ToolCallCache::new(),
167        }
168    }
169
170    /// Create an executor builder for fluent API
171    pub fn builder() -> ExecutorBuilder {
172        ExecutorBuilder::new()
173    }
174
175    /// Enable or disable tool call result caching
176    pub fn with_cache_enabled(mut self, enabled: bool) -> Self {
177        self.cache = self.cache.with_enabled(enabled);
178        self
179    }
180
181    /// Set cache TTL (time-to-live)
182    pub fn with_cache_ttl(mut self, ttl: Duration) -> Self {
183        self.cache = self.cache.with_ttl(ttl);
184        self
185    }
186
187    /// Set maximum cache size
188    pub fn with_cache_max_size(mut self, size: usize) -> Self {
189        self.cache = self.cache.with_max_size(size);
190        self
191    }
192
193    /// Clear the cache
194    pub fn clear_cache(&self) {
195        self.cache.clear();
196    }
197
198    /// Invalidate cache for a specific tool
199    pub fn invalidate_cache_for_tool(&self, tool_name: &str) {
200        self.cache.invalidate_tool(tool_name);
201    }
202
203    /// Get cache statistics
204    pub fn cache_stats(&self) -> super::cache::CacheStats {
205        self.cache.stats()
206    }
207
208    /// Chain-friendly: add a dynamic tool, returns error if already registered
209    pub fn add_dyn_tool(&self, tool: Box<dyn DynTool>) -> ToolResult<&Self> {
210        let name = tool.name().to_string();
211        if self.tools.contains_key(&name) {
212            return Err(ToolError::RegistrationError {
213                message: format!("Tool '{}' is already registered", name).into(),
214            });
215        }
216        self.tools.insert(name, Arc::from(tool));
217        Ok(self)
218    }
219
220    /// Chain-friendly: try to add a dynamic tool (ignores error)
221    pub fn try_add_dyn_tool(&self, tool: Box<dyn DynTool>) -> &Self {
222        let name = tool.name().to_string();
223        self.tools.entry(name).or_insert_with(|| Arc::from(tool));
224        self
225    }
226
227    /// Unregister a tool
228    pub fn unregister(&self, name: &str) -> ToolResult<()> {
229        if self.tools.remove(name).is_none() {
230            return Err(error_context().tool_not_found());
231        }
232        Ok(())
233    }
234
235    /// Get input schema for a tool
236    pub fn input_schema(&self, name: &str) -> Option<serde_json::Value> {
237        self.tools.get(name).map(|t| t.input_schema())
238    }
239
240    /// Check if tool exists
241    pub fn has_tool(&self, name: &str) -> bool {
242        self.tools.contains_key(name)
243    }
244
245    /// List tool names
246    pub fn tool_names(&self) -> Vec<String> {
247        self.tools.iter().map(|entry| entry.key().clone()).collect()
248    }
249
250    fn get_tool(&self, name: &str) -> Option<Arc<dyn DynTool>> {
251        self.tools.get(name).map(|t| Arc::clone(t.value()))
252    }
253
254    /// Execute a tool with detailed result and exponential backoff
255    #[tracing::instrument(skip(self, input))]
256    pub async fn execute(
257        &self,
258        tool_name: &str,
259        input: serde_json::Value,
260    ) -> ToolResult<ExecutionResult> {
261        let start_time = Instant::now();
262        let mut retries = 0;
263        let retry_config = &self.config.retry_config;
264
265        // Check cache first
266        let cache_key = CacheKey::new(tool_name.to_string(), input.clone());
267        if let Some(cached_result) = self.cache.get(&cache_key) {
268            let duration = start_time.elapsed();
269            return Ok(ExecutionResult::success(
270                tool_name.to_string(),
271                cached_result,
272                duration,
273                retries,
274            )
275            .with_metadata("cache_hit", serde_json::Value::Bool(true)));
276        }
277
278        loop {
279            match self.execute_once(tool_name, &input).await {
280                Ok(result) => {
281                    let duration = start_time.elapsed();
282                    // Cache the successful result
283                    self.cache.insert(cache_key, result.clone(), None);
284
285                    return Ok(ExecutionResult::success(
286                        tool_name.to_string(),
287                        result,
288                        duration,
289                        retries,
290                    )
291                    .with_metadata("cache_hit", serde_json::Value::Bool(false)));
292                },
293                Err(error) => {
294                    // Only retry on retryable errors (timeout, transient failures)
295                    if !error.is_retryable() {
296                        let duration = start_time.elapsed();
297                        return Ok(ExecutionResult::failure(
298                            tool_name.to_string(),
299                            error.to_string(),
300                            duration,
301                            retries,
302                        ));
303                    }
304
305                    if retries >= retry_config.max_retries {
306                        let duration = start_time.elapsed();
307                        return Ok(ExecutionResult::failure(
308                            tool_name.to_string(),
309                            error.to_string(),
310                            duration,
311                            retries,
312                        ));
313                    }
314
315                    retries += 1;
316
317                    warn!(attempt = retries, error = %error, "Tool execution failed, retrying");
318
319                    // Use exponential backoff
320                    let delay = retry_config.calculate_delay(retries);
321                    tokio::time::sleep(delay).await;
322                },
323            }
324        }
325    }
326
327    /// Execute a tool and return only the result
328    pub async fn execute_simple(
329        &self,
330        tool_name: &str,
331        input: serde_json::Value,
332    ) -> ToolResult<serde_json::Value> {
333        let result = self.execute(tool_name, input).await?;
334        if result.success {
335            Ok(result.result)
336        } else {
337            Err(error_context()
338                .with_tool(tool_name)
339                .execution_failed(result.error.unwrap_or_else(|| "Unknown error".to_string())))
340        }
341    }
342
343    /// Bulk load function specs from a directory of .json files and register
344    /// them with handlers.
345    ///
346    /// - Each file should contain either of the following shapes:
347    ///   1) {"name":..., "description":..., "parameters": {...}}
348    ///   2) {"type":"function", "function": {"name":..., "description":...,
349    ///      "parameters": {...}}}
350    /// - `handlers` maps function `name` -> handler closure
351    /// - `strict`: when true, missing handler for any spec will return error;
352    ///   when false, specs without handlers are skipped
353    ///
354    /// Returns the list of function names successfully registered.
355    pub fn add_functions_from_dir_with_registry(
356        &self,
357        dir: impl AsRef<std::path::Path>,
358        handlers: &std::collections::HashMap<String, ToolHandler>,
359        strict: bool,
360    ) -> ToolResult<Vec<String>> {
361        use std::fs;
362
363        use serde_json::Value;
364        let dir = dir.as_ref();
365        let mut added = Vec::new();
366        let read_dir = fs::read_dir(dir).map_err(|e| {
367            error_context().invalid_parameters(format!(
368                "Failed to read dir {}: {}",
369                dir.display(),
370                e
371            ))
372        })?;
373        for entry in read_dir {
374            let entry = match entry {
375                Ok(e) => e,
376                Err(e) => {
377                    return Err(
378                        error_context().invalid_parameters(format!("Dir entry error: {}", e))
379                    );
380                },
381            };
382            let path = entry.path();
383            if !path.is_file() {
384                continue;
385            }
386            if path.extension().and_then(|s| s.to_str()) != Some("json") {
387                continue;
388            }
389            let content = fs::read_to_string(&path).map_err(|e| {
390                error_context().invalid_parameters(format!(
391                    "Failed to read {}: {}",
392                    path.display(),
393                    e
394                ))
395            })?;
396            let spec: Value = serde_json::from_str(&content).map_err(|e| {
397                error_context().invalid_parameters(format!(
398                    "Invalid JSON in {}: {}",
399                    path.display(),
400                    e
401                ))
402            })?;
403
404            // Extract name/description/parameters from spec
405            let (name, description, parameters) =
406                crate::toolkits::core::parse_function_spec_details(&spec).map_err(|e| {
407                    error_context().invalid_parameters(format!(
408                        "Failed to parse spec {}: {}",
409                        path.display(),
410                        e
411                    ))
412                })?;
413
414            let handler = match handlers.get(&name) {
415                Some(h) => h.clone(),
416                None => {
417                    if strict {
418                        return Err(error_context().invalid_parameters(format!(
419                            "No handler registered for function '{}' (file {})",
420                            name,
421                            path.display()
422                        )));
423                    } else {
424                        // skip silently
425                        continue;
426                    }
427                },
428            };
429
430            // Build FunctionTool via existing builder path (will auto-complete schema
431            // defaults)
432            let mut builder =
433                crate::toolkits::core::FunctionTool::builder(name.clone(), description);
434            if let Some(p) = parameters {
435                builder = builder.schema(p);
436            }
437            let tool = builder
438                .handler(move |args| {
439                    let h = handler.clone();
440                    h(args)
441                })
442                .build()?;
443
444            self.add_dyn_tool(Box::new(tool))?;
445            added.push(name);
446        }
447        Ok(added)
448    }
449
450    /// Execute LLM tool_calls in parallel and return `TextMessage::tool`
451    /// messages.
452    ///
453    /// Behavior:
454    /// - Parses each ToolCallMessage's function.arguments (stringified JSON
455    ///   supported)
456    /// - Runs all tools concurrently using this executor
457    /// - Captures errors per-call and encodes them as JSON: { "error": {
458    ///   "type": "...", "message": "..." } }
459    /// - Preserves tool_call `id` by emitting TextMessage::tool_with_id when
460    ///   present
461    ///
462    /// Returns:
463    /// - `Vec<TextMessage>` ready to be appended to ChatCompletion as tool
464    ///   messages.
465    async fn execute_single_tool_call(&self, tc: &ToolCallMessage) -> TextMessage {
466        let id_opt = tc.id().map(|s| s.to_string());
467        let func_opt = tc.function();
468
469        if let Some(func) = func_opt {
470            let name = func.name().unwrap_or("").to_string();
471            let args_str = func.arguments().unwrap_or("{}");
472            let args_json: serde_json::Value = serde_json::from_str(args_str)
473                .unwrap_or_else(|_| serde_json::json!({ "_raw": args_str }));
474
475            let content_json = match self.execute_simple(&name, args_json).await {
476                Ok(v) => v,
477                Err(err) => serde_json::json!({
478                    "error": { "type": "execution_failed", "message": err.to_string() }
479                }),
480            };
481
482            let s = serde_json::to_string(&content_json).unwrap_or_else(|_| "{}".to_string());
483
484            if let Some(id) = id_opt {
485                TextMessage::tool_with_id(s, id)
486            } else {
487                TextMessage::tool(s)
488            }
489        } else {
490            let s = serde_json::json!({
491                "error": { "type": "missing_function", "message": "tool_call.function is missing" }
492            })
493            .to_string();
494
495            if let Some(id) = id_opt {
496                TextMessage::tool_with_id(s, id)
497            } else {
498                TextMessage::tool(s)
499            }
500        }
501    }
502
503    pub async fn execute_tool_calls_parallel(&self, calls: &[ToolCallMessage]) -> Vec<TextMessage> {
504        let mut set = JoinSet::new();
505
506        // Clone the calls to avoid borrowing issues
507        let calls_vec = calls.to_vec();
508        for tc in calls_vec {
509            let this = self.clone();
510            set.spawn(async move { this.execute_single_tool_call(&tc).await });
511        }
512
513        let mut messages = Vec::with_capacity(calls.len());
514        while let Some(res) = set.join_next().await {
515            if let Ok(msg) = res {
516                messages.push(msg);
517            }
518        }
519        messages
520    }
521
522    /// Execute LLM tool_calls in parallel with result ordering preserved
523    ///
524    /// This method guarantees that results are returned in the same order as
525    /// the input calls, which is important for maintaining conversation
526    /// context in LLM interactions.
527    ///
528    /// Behavior:
529    /// - Parses each ToolCallMessage's function.arguments (stringified JSON
530    ///   supported)
531    /// - Runs all tools concurrently using this executor
532    /// - Preserves the original order of tool calls in results
533    /// - Captures errors per-call and encodes them as JSON
534    /// - Preserves tool_call `id` by emitting TextMessage::tool_with_id when
535    ///   present
536    ///
537    /// Returns:
538    /// - Vec<TextMessage> in the same order as input calls, ready for
539    ///   ChatCompletion
540    pub async fn execute_tool_calls_ordered(&self, calls: &[ToolCallMessage]) -> Vec<TextMessage> {
541        use futures::future::join_all;
542
543        let calls_vec = calls.to_vec();
544        let futures: Vec<_> = calls_vec
545            .into_iter()
546            .map(|tc| {
547                let this = self.clone();
548                async move { this.execute_single_tool_call(&tc).await }
549            })
550            .collect();
551
552        join_all(futures).await
553    }
554
555    /// Export a single registered tool as Tools::Function (for LLM function
556    /// calling)
557    pub fn export_tool_as_function(&self, name: &str) -> Option<Tools> {
558        let tool = self.tools.get(name)?;
559        let meta = tool.metadata();
560        let schema = tool.input_schema();
561        let func = Function::new(meta.name.clone(), meta.description.clone(), schema);
562        Some(Tools::Function { function: func })
563    }
564
565    /// Export all registered tools as a Vec<Tools::Function>
566    pub fn export_all_tools_as_functions(&self) -> Vec<Tools> {
567        self.tools
568            .iter()
569            .map(|entry| {
570                let tool = entry.value();
571                let meta = tool.metadata();
572                let schema = tool.input_schema();
573                let func = Function::new(meta.name.clone(), meta.description.clone(), schema);
574                Tools::Function { function: func }
575            })
576            .collect()
577    }
578    /// Export all registered tools with a metadata filter as Tools::Function
579    pub fn export_tools_filtered<F>(&self, mut filter: F) -> Vec<Tools>
580    where
581        F: FnMut(&crate::toolkits::core::ToolMetadata) -> bool,
582    {
583        self.tools
584            .iter()
585            .filter(|entry| filter(entry.value().metadata()))
586            .map(|entry| {
587                let tool = entry.value();
588                let meta = tool.metadata();
589                let schema = tool.input_schema();
590                let func = Function::new(meta.name.clone(), meta.description.clone(), schema);
591                Tools::Function { function: func }
592            })
593            .collect()
594    }
595
596    #[tracing::instrument(skip(self, input))]
597    async fn execute_once(
598        &self,
599        tool_name: &str,
600        input: &serde_json::Value,
601    ) -> ToolResult<serde_json::Value> {
602        let tool = self
603            .get_tool(tool_name)
604            .ok_or_else(|| error_context().with_tool(tool_name).tool_not_found())?;
605        let execution_future = tool.execute_json(input.clone());
606
607        match self.config.timeout {
608            Some(timeout_duration) => match timeout(timeout_duration, execution_future).await {
609                Ok(result) => result,
610                Err(_) => Err(error_context()
611                    .with_tool(tool_name)
612                    .timeout_error(timeout_duration)),
613            },
614            None => execution_future.await,
615        }
616    }
617
618    /// Get the config
619    pub fn config(&self) -> &ExecutionConfig {
620        &self.config
621    }
622}
623
624/// Builder for creating tool executors with fluent API
625pub struct ExecutorBuilder {
626    config: ExecutionConfig,
627    cache_config: Option<CacheConfig>,
628}
629
630#[derive(Clone)]
631struct CacheConfig {
632    enabled: bool,
633    ttl: Duration,
634    max_size: usize,
635}
636
637impl Default for ExecutorBuilder {
638    fn default() -> Self {
639        Self::new()
640    }
641}
642
643impl ExecutorBuilder {
644    /// Create a new executor builder
645    pub fn new() -> Self {
646        Self {
647            config: ExecutionConfig::default(),
648            cache_config: None,
649        }
650    }
651
652    /// Set timeout for tool execution
653    pub fn timeout(mut self, timeout: Duration) -> Self {
654        self.config.timeout = Some(timeout);
655        self
656    }
657
658    /// Set maximum number of retries
659    pub fn retries(mut self, retries: u32) -> Self {
660        self.config.retry_config.max_retries = retries;
661        self
662    }
663
664    /// Enable or disable logging
665    pub fn logging(mut self, enabled: bool) -> Self {
666        self.config.enable_logging = enabled;
667        self
668    }
669
670    /// Enable tool call result caching
671    pub fn enable_cache(mut self) -> Self {
672        self.cache_config
673            .get_or_insert(CacheConfig {
674                enabled: true,
675                ttl: Duration::from_secs(300),
676                max_size: 1000,
677            })
678            .enabled = true;
679        self
680    }
681
682    /// Disable tool call result caching
683    pub fn disable_cache(mut self) -> Self {
684        self.cache_config
685            .get_or_insert(CacheConfig {
686                enabled: false,
687                ttl: Duration::from_secs(300),
688                max_size: 1000,
689            })
690            .enabled = false;
691        self
692    }
693
694    /// Set cache TTL
695    pub fn cache_ttl(mut self, ttl: Duration) -> Self {
696        let cfg = self.cache_config.get_or_insert(CacheConfig {
697            enabled: true,
698            ttl: Duration::from_secs(300),
699            max_size: 1000,
700        });
701        cfg.ttl = ttl;
702        self
703    }
704
705    /// Set maximum cache size
706    pub fn cache_max_size(mut self, size: usize) -> Self {
707        let cfg = self.cache_config.get_or_insert(CacheConfig {
708            enabled: true,
709            ttl: Duration::from_secs(300),
710            max_size: 1000,
711        });
712        cfg.max_size = size;
713        self
714    }
715
716    /// Build the final executor
717    pub fn build(self) -> ToolExecutor {
718        let cache = match self.cache_config {
719            Some(cfg) => ToolCallCache::new()
720                .with_enabled(cfg.enabled)
721                .with_ttl(cfg.ttl)
722                .with_max_size(cfg.max_size),
723            None => ToolCallCache::new(),
724        };
725
726        ToolExecutor {
727            tools: Arc::new(DashMap::new()),
728            config: self.config,
729            cache,
730        }
731    }
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737    use crate::toolkits::core::FunctionTool;
738
739    #[test]
740    fn test_retry_config_default() {
741        let config = RetryConfig::default();
742        assert_eq!(config.max_retries, 3);
743        assert_eq!(config.initial_delay, Duration::from_millis(100));
744        assert_eq!(config.max_delay, Duration::from_secs(30));
745        assert_eq!(config.backoff_multiplier, 2.0);
746    }
747
748    #[test]
749    fn test_retry_config_calculate_delay() {
750        let config = RetryConfig::default();
751
752        // First attempt should have zero delay
753        assert_eq!(config.calculate_delay(0), Duration::ZERO);
754
755        // Second attempt should have initial delay
756        assert_eq!(config.calculate_delay(1), Duration::from_millis(100));
757
758        // Third attempt should double (100 * 2)
759        assert_eq!(config.calculate_delay(2), Duration::from_millis(200));
760
761        // Fourth attempt should quadruple (100 * 2^2)
762        assert_eq!(config.calculate_delay(3), Duration::from_millis(400));
763
764        // Test with exponential growth that exceeds max_delay
765        let config = RetryConfig {
766            max_retries: 10,
767            initial_delay: Duration::from_millis(500),
768            max_delay: Duration::from_secs(1),
769            backoff_multiplier: 3.0,
770        };
771        // 500ms, then 1500ms (capped at 1000ms)
772        assert_eq!(config.calculate_delay(1), Duration::from_millis(500));
773        assert_eq!(config.calculate_delay(2), Duration::from_secs(1));
774        assert_eq!(config.calculate_delay(3), Duration::from_secs(1));
775    }
776
777    #[test]
778    fn test_execution_config_default() {
779        let config = ExecutionConfig::default();
780        assert_eq!(config.timeout, Some(Duration::from_secs(30)));
781        assert!(config.validate_parameters);
782        assert!(!config.enable_logging);
783        assert_eq!(config.retry_config.max_retries, 3);
784    }
785
786    #[test]
787    fn test_execution_result_success() {
788        let result = ExecutionResult::success(
789            "test_tool".to_string(),
790            serde_json::json!({"value": 42}),
791            Duration::from_millis(100),
792            2,
793        );
794
795        assert_eq!(result.tool_name, "test_tool");
796        assert_eq!(result.result, serde_json::json!({"value": 42}));
797        assert_eq!(result.duration, Duration::from_millis(100));
798        assert!(result.success);
799        assert!(result.error.is_none());
800        assert_eq!(result.retries, 2);
801        assert!(result.metadata.is_empty());
802    }
803
804    #[test]
805    fn test_execution_result_failure() {
806        let result = ExecutionResult::failure(
807            "test_tool".to_string(),
808            "Something went wrong".to_string(),
809            Duration::from_millis(50),
810            1,
811        );
812
813        assert_eq!(result.tool_name, "test_tool");
814        assert_eq!(result.result, serde_json::Value::Null);
815        assert_eq!(result.duration, Duration::from_millis(50));
816        assert!(!result.success);
817        assert_eq!(result.error, Some("Something went wrong".to_string()));
818        assert_eq!(result.retries, 1);
819        assert!(result.metadata.is_empty());
820    }
821
822    #[test]
823    fn test_execution_result_with_metadata() {
824        let mut result = ExecutionResult::success(
825            "test_tool".to_string(),
826            serde_json::json!({"value": 42}),
827            Duration::from_millis(100),
828            0,
829        );
830
831        result = result.with_metadata("key1", serde_json::json!("value1"));
832        result = result.with_metadata("key2", serde_json::json!({"nested": true}));
833
834        assert_eq!(result.metadata.len(), 2);
835        assert_eq!(
836            result.metadata.get("key1"),
837            Some(&serde_json::json!("value1"))
838        );
839        assert_eq!(
840            result.metadata.get("key2"),
841            Some(&serde_json::json!({"nested": true}))
842        );
843    }
844
845    #[test]
846    fn test_execution_result_serialization() {
847        let result = ExecutionResult::success(
848            "test_tool".to_string(),
849            serde_json::json!({"value": 42}),
850            Duration::from_millis(100),
851            0,
852        );
853
854        let json = serde_json::to_string(&result).unwrap();
855        assert!(json.contains("\"tool_name\":\"test_tool\""));
856        assert!(json.contains("\"success\":true"));
857        assert!(json.contains("\"value\":42"));
858    }
859
860    #[test]
861    fn test_tool_executor_default() {
862        let executor = ToolExecutor::new();
863        assert_eq!(executor.tool_names().len(), 0);
864        assert_eq!(executor.config.timeout, Some(Duration::from_secs(30)));
865    }
866
867    #[test]
868    fn test_tool_executor_register_and_unregister() {
869        let executor = ToolExecutor::new();
870
871        // Create a simple test tool
872        let tool = FunctionTool::builder("test_tool", "A test tool")
873            .handler(|_args| async move { Ok(serde_json::json!({"result": "success"})) })
874            .build()
875            .unwrap();
876
877        // Register the tool
878        executor.add_dyn_tool(Box::new(tool)).unwrap();
879        assert_eq!(executor.tool_names().len(), 1);
880        assert!(executor.has_tool("test_tool"));
881
882        // Unregister the tool
883        assert!(executor.unregister("test_tool").is_ok());
884        assert_eq!(executor.tool_names().len(), 0);
885        assert!(!executor.has_tool("test_tool"));
886    }
887
888    #[test]
889    fn test_tool_executor_duplicate_tool_returns_error() {
890        let executor = ToolExecutor::new();
891
892        let tool1 = FunctionTool::builder("duplicate_tool", "First tool")
893            .handler(|_args| async move { Ok(serde_json::json!({})) })
894            .build()
895            .unwrap();
896
897        let tool2 = FunctionTool::builder("duplicate_tool", "Second tool")
898            .handler(|_args| async move { Ok(serde_json::json!({})) })
899            .build()
900            .unwrap();
901
902        executor.add_dyn_tool(Box::new(tool1)).unwrap();
903
904        // Adding duplicate tool should return error
905        let result = executor.add_dyn_tool(Box::new(tool2));
906        assert!(result.is_err());
907    }
908
909    #[test]
910    fn test_tool_executor_try_add_dyn_tool() {
911        let executor = ToolExecutor::new();
912
913        let tool1 = FunctionTool::builder("test_tool", "First tool")
914            .handler(|_args| async move { Ok(serde_json::json!({})) })
915            .build()
916            .unwrap();
917
918        let tool2 = FunctionTool::builder("test_tool", "Second tool")
919            .handler(|_args| async move { Ok(serde_json::json!({})) })
920            .build()
921            .unwrap();
922
923        executor.try_add_dyn_tool(Box::new(tool1));
924        executor.try_add_dyn_tool(Box::new(tool2));
925
926        // Only one tool should be registered (second should be ignored)
927        assert_eq!(executor.tool_names().len(), 1);
928        assert!(executor.has_tool("test_tool"));
929    }
930
931    #[test]
932    fn test_tool_executor_unregister_nonexistent_tool() {
933        let executor = ToolExecutor::new();
934        let result = executor.unregister("nonexistent_tool");
935        assert!(result.is_err());
936    }
937
938    #[test]
939    fn test_tool_executor_input_schema() {
940        let executor = ToolExecutor::new();
941
942        let schema = serde_json::json!({
943            "type": "object",
944            "properties": {
945                "name": {"type": "string"}
946            }
947        });
948
949        let tool = FunctionTool::builder("test_tool", "A test tool")
950            .schema(schema.clone())
951            .handler(|_args| async move { Ok(serde_json::json!({})) })
952            .build()
953            .unwrap();
954
955        executor.add_dyn_tool(Box::new(tool)).unwrap();
956
957        let retrieved_schema = executor.input_schema("test_tool");
958        assert!(retrieved_schema.is_some());
959        let retrieved = retrieved_schema.unwrap();
960
961        // Check that schema contains expected properties
962        assert_eq!(retrieved["type"], "object");
963        assert_eq!(retrieved["properties"]["name"]["type"], "string");
964        // additionalProperties is automatically set by FunctionToolBuilder
965        assert_eq!(retrieved["additionalProperties"], false);
966    }
967
968    #[test]
969    fn test_tool_executor_input_schema_nonexistent() {
970        let executor = ToolExecutor::new();
971        let schema = executor.input_schema("nonexistent");
972        assert!(schema.is_none());
973    }
974
975    #[test]
976    fn test_tool_executor_tool_names() {
977        let executor = ToolExecutor::new();
978
979        let tool1 = FunctionTool::builder("tool1", "First tool")
980            .handler(|_args| async move { Ok(serde_json::json!({})) })
981            .build()
982            .unwrap();
983
984        let tool2 = FunctionTool::builder("tool2", "Second tool")
985            .handler(|_args| async move { Ok(serde_json::json!({})) })
986            .build()
987            .unwrap();
988
989        let tool3 = FunctionTool::builder("tool3", "Third tool")
990            .handler(|_args| async move { Ok(serde_json::json!({})) })
991            .build()
992            .unwrap();
993
994        executor.add_dyn_tool(Box::new(tool1)).unwrap();
995        executor.add_dyn_tool(Box::new(tool2)).unwrap();
996        executor.add_dyn_tool(Box::new(tool3)).unwrap();
997
998        let names = executor.tool_names();
999        assert_eq!(names.len(), 3);
1000        assert!(names.contains(&"tool1".to_string()));
1001        assert!(names.contains(&"tool2".to_string()));
1002        assert!(names.contains(&"tool3".to_string()));
1003    }
1004
1005    #[tokio::test]
1006    async fn test_tool_executor_execute_success() {
1007        let executor = ToolExecutor::new();
1008
1009        let tool = FunctionTool::builder("add_tool", "Add two numbers")
1010            .property("a", serde_json::json!({"type": "number"}))
1011            .property("b", serde_json::json!({"type": "number"}))
1012            .handler(|args| async move {
1013                let a = args.get("a").and_then(|v| v.as_i64()).unwrap_or(0);
1014                let b = args.get("b").and_then(|v| v.as_i64()).unwrap_or(0);
1015                Ok(serde_json::json!({"result": a + b}))
1016            })
1017            .build()
1018            .unwrap();
1019
1020        executor.add_dyn_tool(Box::new(tool)).unwrap();
1021
1022        let input = serde_json::json!({"a": 5, "b": 3});
1023        let result = executor.execute("add_tool", input).await.unwrap();
1024
1025        assert!(result.success);
1026        assert_eq!(result.tool_name, "add_tool");
1027        assert_eq!(result.result, serde_json::json!({"result": 8}));
1028        assert_eq!(result.retries, 0);
1029    }
1030
1031    #[tokio::test]
1032    async fn test_tool_executor_execute_failure() {
1033        let executor = ToolExecutor::new();
1034
1035        let tool = FunctionTool::builder("failing_tool", "Always fails")
1036            .handler(|_args| async move {
1037                Err(error_context()
1038                    .with_tool("failing_tool")
1039                    .execution_failed("Intentional failure"))
1040            })
1041            .build()
1042            .unwrap();
1043
1044        executor.add_dyn_tool(Box::new(tool)).unwrap();
1045
1046        let input = serde_json::json!({});
1047        let result = executor.execute("failing_tool", input).await.unwrap();
1048
1049        assert!(!result.success);
1050        assert_eq!(result.tool_name, "failing_tool");
1051        assert!(result.error.is_some());
1052    }
1053
1054    #[tokio::test]
1055    async fn test_tool_executor_execute_nonexistent_tool() {
1056        let executor = ToolExecutor::new();
1057        let input = serde_json::json!({});
1058        let result = executor.execute("nonexistent_tool", input).await.unwrap();
1059
1060        assert!(!result.success);
1061        assert!(result.error.is_some());
1062    }
1063
1064    #[tokio::test]
1065    async fn test_tool_executor_execute_simple_success() {
1066        let executor = ToolExecutor::new();
1067
1068        let tool = FunctionTool::builder("echo_tool", "Echo input")
1069            .property("message", serde_json::json!({"type": "string"}))
1070            .handler(|args| async move { Ok(args) })
1071            .build()
1072            .unwrap();
1073
1074        executor.add_dyn_tool(Box::new(tool)).unwrap();
1075
1076        let input = serde_json::json!({"message": "hello"});
1077        let result = executor.execute_simple("echo_tool", input).await.unwrap();
1078
1079        assert_eq!(result, serde_json::json!({"message": "hello"}));
1080    }
1081
1082    #[tokio::test]
1083    async fn test_tool_executor_execute_simple_failure() {
1084        let executor = ToolExecutor::new();
1085
1086        let tool = FunctionTool::builder("failing_tool", "Always fails")
1087            .handler(|_args| async move {
1088                Err(error_context()
1089                    .with_tool("failing_tool")
1090                    .execution_failed("Intentional failure"))
1091            })
1092            .build()
1093            .unwrap();
1094
1095        executor.add_dyn_tool(Box::new(tool)).unwrap();
1096
1097        let input = serde_json::json!({});
1098        let result = executor.execute_simple("failing_tool", input).await;
1099
1100        assert!(result.is_err());
1101    }
1102
1103    #[tokio::test]
1104    async fn test_tool_executor_timeout() {
1105        let executor = ToolExecutor::builder()
1106            .timeout(Duration::from_millis(100))
1107            .build();
1108
1109        let tool = FunctionTool::builder("slow_tool", "Slow tool")
1110            .handler(|_args| async move {
1111                tokio::time::sleep(Duration::from_secs(1)).await;
1112                Ok(serde_json::json!({"done": true}))
1113            })
1114            .build()
1115            .unwrap();
1116
1117        executor.add_dyn_tool(Box::new(tool)).unwrap();
1118
1119        let input = serde_json::json!({});
1120        let result = executor.execute("slow_tool", input).await.unwrap();
1121
1122        assert!(!result.success);
1123        assert!(result.error.is_some());
1124        assert!(result.error.unwrap().contains("Timeout"));
1125    }
1126
1127    #[tokio::test]
1128    async fn test_tool_executor_retry() {
1129        let executor = ToolExecutor::builder()
1130            .retries(2)
1131            .timeout(Duration::from_secs(30))
1132            .build();
1133
1134        let attempt_counter = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
1135        let counter_clone = attempt_counter.clone();
1136
1137        let tool = FunctionTool::builder("flaky_tool", "Flaky tool")
1138            .handler(move |_args| {
1139                let counter = counter_clone.clone();
1140                async move {
1141                    let attempts = counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1142                    if attempts < 2 {
1143                        Err(error_context()
1144                            .with_tool("flaky_tool")
1145                            .execution_failed("Temporary failure"))
1146                    } else {
1147                        Ok(serde_json::json!({"attempts": attempts + 1}))
1148                    }
1149                }
1150            })
1151            .build()
1152            .unwrap();
1153
1154        executor.add_dyn_tool(Box::new(tool)).unwrap();
1155
1156        let input = serde_json::json!({});
1157        let result = executor.execute("flaky_tool", input).await.unwrap();
1158
1159        assert!(result.success);
1160        assert_eq!(result.retries, 2);
1161    }
1162
1163    #[test]
1164    fn test_executor_builder_default() {
1165        let builder = ExecutorBuilder::new();
1166        assert_eq!(builder.config.timeout, Some(Duration::from_secs(30)));
1167        assert_eq!(builder.config.retry_config.max_retries, 3);
1168    }
1169
1170    #[test]
1171    fn test_executor_builder_timeout() {
1172        let builder = ExecutorBuilder::new().timeout(Duration::from_secs(60));
1173        assert_eq!(builder.config.timeout, Some(Duration::from_secs(60)));
1174    }
1175
1176    #[test]
1177    fn test_executor_builder_retries() {
1178        let builder = ExecutorBuilder::new().retries(5);
1179        assert_eq!(builder.config.retry_config.max_retries, 5);
1180    }
1181
1182    #[test]
1183    fn test_executor_builder_logging() {
1184        let builder = ExecutorBuilder::new().logging(true);
1185        assert!(builder.config.enable_logging);
1186    }
1187
1188    #[test]
1189    fn test_executor_builder_build() {
1190        let executor = ExecutorBuilder::new()
1191            .timeout(Duration::from_secs(60))
1192            .retries(5)
1193            .logging(true)
1194            .build();
1195
1196        assert_eq!(executor.config.timeout, Some(Duration::from_secs(60)));
1197        assert_eq!(executor.config.retry_config.max_retries, 5);
1198        assert!(executor.config.enable_logging);
1199    }
1200
1201    #[test]
1202    fn test_executor_builder_chainable() {
1203        let builder = ExecutorBuilder::new()
1204            .timeout(Duration::from_secs(45))
1205            .retries(3)
1206            .logging(false)
1207            .timeout(Duration::from_secs(50))
1208            .retries(4)
1209            .logging(true);
1210
1211        assert_eq!(builder.config.timeout, Some(Duration::from_secs(50)));
1212        assert_eq!(builder.config.retry_config.max_retries, 4);
1213        assert!(builder.config.enable_logging);
1214    }
1215
1216    #[test]
1217    fn test_export_tool_as_function() {
1218        let executor = ToolExecutor::new();
1219
1220        let tool = FunctionTool::builder("greet_tool", "Greet someone")
1221            .handler(|_args| async move { Ok(serde_json::json!({"greeting": "hello"})) })
1222            .build()
1223            .unwrap();
1224
1225        executor.add_dyn_tool(Box::new(tool)).unwrap();
1226
1227        let exported = executor.export_tool_as_function("greet_tool");
1228        assert!(exported.is_some());
1229
1230        if let Some(Tools::Function { function }) = exported {
1231            assert_eq!(function.name, "greet_tool");
1232            assert_eq!(function.description, "Greet someone");
1233            // Schema is auto-generated with default values
1234            assert!(function.parameters.is_some());
1235        } else {
1236            panic!("Expected Tools::Function");
1237        }
1238    }
1239
1240    #[test]
1241    fn test_export_tool_as_function_nonexistent() {
1242        let executor = ToolExecutor::new();
1243        let exported = executor.export_tool_as_function("nonexistent");
1244        assert!(exported.is_none());
1245    }
1246
1247    #[test]
1248    fn test_export_all_tools_as_functions() {
1249        let executor = ToolExecutor::new();
1250
1251        let tool1 = FunctionTool::builder("tool1", "First tool")
1252            .handler(|_args| async move { Ok(serde_json::json!({})) })
1253            .build()
1254            .unwrap();
1255
1256        let tool2 = FunctionTool::builder("tool2", "Second tool")
1257            .handler(|_args| async move { Ok(serde_json::json!({})) })
1258            .build()
1259            .unwrap();
1260
1261        executor.add_dyn_tool(Box::new(tool1)).unwrap();
1262        executor.add_dyn_tool(Box::new(tool2)).unwrap();
1263
1264        let exported = executor.export_all_tools_as_functions();
1265        assert_eq!(exported.len(), 2);
1266
1267        let names: Vec<_> = exported
1268            .iter()
1269            .filter_map(|t| match t {
1270                Tools::Function { function } => Some(function.name.clone()),
1271                _ => None,
1272            })
1273            .collect();
1274
1275        assert!(names.contains(&"tool1".to_string()));
1276        assert!(names.contains(&"tool2".to_string()));
1277    }
1278
1279    #[test]
1280    fn test_export_tools_filtered() {
1281        let executor = ToolExecutor::new();
1282
1283        let tool1 = FunctionTool::builder("math_tool", "Math operations")
1284            .metadata(|m| m.version("1.0.0"))
1285            .handler(|_args| async move { Ok(serde_json::json!({})) })
1286            .build()
1287            .unwrap();
1288
1289        let tool2 = FunctionTool::builder("text_tool", "Text operations")
1290            .metadata(|m| m.version("2.0.0"))
1291            .handler(|_args| async move { Ok(serde_json::json!({})) })
1292            .build()
1293            .unwrap();
1294
1295        executor.add_dyn_tool(Box::new(tool1)).unwrap();
1296        executor.add_dyn_tool(Box::new(tool2)).unwrap();
1297
1298        let exported = executor.export_tools_filtered(|meta| meta.version == "1.0.0");
1299        assert_eq!(exported.len(), 1);
1300
1301        if let Some(Tools::Function { function }) = exported.first() {
1302            assert_eq!(function.name, "math_tool");
1303        } else {
1304            panic!("Expected Tools::Function");
1305        }
1306    }
1307
1308    #[test]
1309    fn test_execution_result_metadata_serialization() {
1310        let result = ExecutionResult::success(
1311            "test_tool".to_string(),
1312            serde_json::json!({"value": 42}),
1313            Duration::from_millis(100),
1314            0,
1315        )
1316        .with_metadata("key1", serde_json::json!("value1"))
1317        .with_metadata("key2", serde_json::json!(123));
1318
1319        let json = serde_json::to_string(&result).unwrap();
1320        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1321
1322        assert_eq!(parsed["metadata"]["key1"], "value1");
1323        assert_eq!(parsed["metadata"]["key2"], 123);
1324    }
1325
1326    #[test]
1327    fn test_execution_result_timestamp() {
1328        let before = std::time::SystemTime::now();
1329        let result = ExecutionResult::success(
1330            "test_tool".to_string(),
1331            serde_json::json!({"value": 42}),
1332            Duration::from_millis(100),
1333            0,
1334        );
1335        let after = std::time::SystemTime::now();
1336
1337        assert!(result.timestamp >= before && result.timestamp <= after);
1338    }
1339
1340    #[tokio::test]
1341    async fn test_tool_executor_no_retry_for_non_retryable_error() {
1342        let executor = ToolExecutor::builder()
1343            .retries(3)
1344            .timeout(Duration::from_secs(30))
1345            .build();
1346
1347        let attempt_counter = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
1348        let counter_clone = attempt_counter.clone();
1349
1350        // ToolNotFound is not retryable, so it should fail immediately without retries
1351        let tool = FunctionTool::builder("not_found_tool", "Not found tool")
1352            .handler(move |_args| {
1353                let counter = counter_clone.clone();
1354                async move {
1355                    counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1356                    Err(error_context()
1357                        .with_tool("not_found_tool")
1358                        .invalid_parameters("Invalid parameters"))
1359                }
1360            })
1361            .build()
1362            .unwrap();
1363
1364        executor.add_dyn_tool(Box::new(tool)).unwrap();
1365
1366        let input = serde_json::json!({});
1367        let result = executor.execute("not_found_tool", input).await.unwrap();
1368
1369        assert!(!result.success);
1370        assert_eq!(result.retries, 0); // Should not have retried
1371        // Should have been called exactly once
1372        assert_eq!(attempt_counter.load(std::sync::atomic::Ordering::SeqCst), 1);
1373    }
1374
1375    #[tokio::test]
1376    async fn test_execute_tool_calls_ordered_preserves_order() {
1377        use crate::model::chat_base_response::{ToolCallMessage, ToolFunction};
1378
1379        let executor = ToolExecutor::new();
1380
1381        // Register two tools that return different results
1382        let tool1 = FunctionTool::builder("tool_a", "First tool")
1383            .property("n", serde_json::json!({"type": "number"}))
1384            .handler(|args| async move {
1385                let n = args.get("n").and_then(|v| v.as_i64()).unwrap_or(0);
1386                Ok(serde_json::json!({"tool": "a", "n": n}))
1387            })
1388            .build()
1389            .unwrap();
1390
1391        let tool2 = FunctionTool::builder("tool_b", "Second tool")
1392            .property("n", serde_json::json!({"type": "number"}))
1393            .handler(|args| async move {
1394                let n = args.get("n").and_then(|v| v.as_i64()).unwrap_or(0);
1395                Ok(serde_json::json!({"tool": "b", "n": n}))
1396            })
1397            .build()
1398            .unwrap();
1399
1400        executor.add_dyn_tool(Box::new(tool1)).unwrap();
1401        executor.add_dyn_tool(Box::new(tool2)).unwrap();
1402
1403        let calls = vec![
1404            ToolCallMessage {
1405                id: Some("call_1".to_string()),
1406                type_: Some("function".to_string()),
1407                function: Some(ToolFunction {
1408                    name: Some("tool_a".to_string()),
1409                    arguments: Some(r#"{"n": 1}"#.to_string()),
1410                }),
1411                mcp: None,
1412            },
1413            ToolCallMessage {
1414                id: Some("call_2".to_string()),
1415                type_: Some("function".to_string()),
1416                function: Some(ToolFunction {
1417                    name: Some("tool_b".to_string()),
1418                    arguments: Some(r#"{"n": 2}"#.to_string()),
1419                }),
1420                mcp: None,
1421            },
1422        ];
1423
1424        let results = executor.execute_tool_calls_ordered(&calls).await;
1425        assert_eq!(results.len(), 2);
1426        // Verify ordering: first result should be from tool_a, second from tool_b
1427        let first = &results[0];
1428        let first_content = match first {
1429            crate::model::chat_message_types::TextMessage::Tool { content, .. } => content.clone(),
1430            _ => panic!("Expected Tool message"),
1431        };
1432        let parsed1: serde_json::Value = serde_json::from_str(&first_content).unwrap();
1433        // Check both tools since order in the content identifies which tool ran
1434        assert!(parsed1.get("tool").is_some());
1435        assert!(parsed1["n"].as_i64() == Some(1));
1436
1437        let second = &results[1];
1438        let second_content = match second {
1439            crate::model::chat_message_types::TextMessage::Tool { content, .. } => content.clone(),
1440            _ => panic!("Expected Tool message"),
1441        };
1442        let parsed2: serde_json::Value = serde_json::from_str(&second_content).unwrap();
1443        assert!(parsed2.get("tool").is_some());
1444        assert!(parsed2["n"].as_i64() == Some(2));
1445    }
1446
1447    #[tokio::test]
1448    async fn test_execute_tool_calls_parallel_returns_all() {
1449        use crate::model::chat_base_response::{ToolCallMessage, ToolFunction};
1450
1451        let executor = ToolExecutor::new();
1452
1453        let tool1 = FunctionTool::builder("parallel_a", "First parallel tool")
1454            .property("n", serde_json::json!({"type": "number"}))
1455            .handler(|args| async move {
1456                let n = args.get("n").and_then(|v| v.as_i64()).unwrap_or(0);
1457                Ok(serde_json::json!({"tool": "a", "n": n}))
1458            })
1459            .build()
1460            .unwrap();
1461
1462        let tool2 = FunctionTool::builder("parallel_b", "Second parallel tool")
1463            .property("n", serde_json::json!({"type": "number"}))
1464            .handler(|args| async move {
1465                let n = args.get("n").and_then(|v| v.as_i64()).unwrap_or(0);
1466                Ok(serde_json::json!({"tool": "b", "n": n}))
1467            })
1468            .build()
1469            .unwrap();
1470
1471        executor.add_dyn_tool(Box::new(tool1)).unwrap();
1472        executor.add_dyn_tool(Box::new(tool2)).unwrap();
1473
1474        let calls = vec![
1475            ToolCallMessage {
1476                id: Some("call_1".to_string()),
1477                type_: Some("function".to_string()),
1478                function: Some(ToolFunction {
1479                    name: Some("parallel_a".to_string()),
1480                    arguments: Some(r#"{"n": 1}"#.to_string()),
1481                }),
1482                mcp: None,
1483            },
1484            ToolCallMessage {
1485                id: Some("call_2".to_string()),
1486                type_: Some("function".to_string()),
1487                function: Some(ToolFunction {
1488                    name: Some("parallel_b".to_string()),
1489                    arguments: Some(r#"{"n": 2}"#.to_string()),
1490                }),
1491                mcp: None,
1492            },
1493        ];
1494
1495        let results = executor.execute_tool_calls_parallel(&calls).await;
1496        assert_eq!(results.len(), 2);
1497    }
1498}