Skip to main content

bamboo_agent_core/tools/
executor.rs

1//! Tool execution infrastructure
2//!
3//! This module provides the trait and utilities for executing tool calls,
4//! with support for both direct tool execution and composition-based workflows.
5
6use std::sync::Arc;
7
8use async_trait::async_trait;
9use thiserror::Error;
10
11use crate::composition::{CompositionExecutor, ExecutionContext, ToolExpr};
12use crate::tools::{ToolCall, ToolResult, ToolSchema};
13
14use super::result_handler::parse_tool_args_best_effort;
15use super::ToolExecutionContext;
16
17/// Errors that can occur during tool execution
18#[derive(Error, Debug, Clone)]
19pub enum ToolError {
20    /// The requested tool was not found in the registry
21    #[error("Tool not found: {0}")]
22    NotFound(String),
23
24    /// Tool execution failed
25    #[error("Execution failed: {0}")]
26    Execution(String),
27
28    /// Invalid arguments provided to the tool
29    #[error("Invalid arguments: {0}")]
30    InvalidArguments(String),
31}
32
33/// Convenient result type for tool execution operations
34pub type Result<T> = std::result::Result<T, ToolError>;
35
36/// Trait for tool execution backends
37///
38/// This trait defines the interface for executing tool calls and listing
39/// available tools. Implementations can wrap tool registries, provide
40/// mock tools for testing, or implement custom execution logic.
41///
42/// # Example
43///
44/// ```ignore
45/// use bamboo_agent::agent::core::tools::executor::ToolExecutor;
46///
47/// struct MyExecutor {
48///     tools: HashMap<String, Box<dyn Tool>>,
49/// }
50///
51/// #[async_trait]
52/// impl ToolExecutor for MyExecutor {
53///     async fn execute(&self, call: &ToolCall) -> Result<ToolResult> {
54///         let tool = self.tools.get(&call.function.name)
55///             .ok_or_else(|| ToolError::NotFound(call.function.name.clone()))?;
56///         let args = parse_tool_args(&call.function.arguments)?;
57///         tool.execute(args).await
58///     }
59///
60///     fn list_tools(&self) -> Vec<ToolSchema> {
61///         self.tools.values().map(|t| t.schema()).collect()
62///     }
63/// }
64/// ```
65#[async_trait]
66pub trait ToolExecutor: Send + Sync {
67    /// Executes a tool call
68    ///
69    /// # Arguments
70    ///
71    /// * `call` - The tool call to execute (contains tool name and arguments)
72    ///
73    /// # Returns
74    ///
75    /// The tool execution result or an error
76    async fn execute(&self, call: &ToolCall) -> Result<ToolResult>;
77
78    /// Executes a tool call with streaming-capable context.
79    ///
80    /// Default implementation falls back to `execute()` for executors that don't
81    /// support streaming (e.g. remote MCP tools).
82    async fn execute_with_context(
83        &self,
84        call: &ToolCall,
85        _ctx: ToolExecutionContext<'_>,
86    ) -> Result<ToolResult> {
87        self.execute(call).await
88    }
89
90    /// Lists all available tools and their schemas
91    ///
92    /// Returns schemas for all tools that can be executed via this executor
93    fn list_tools(&self) -> Vec<ToolSchema>;
94
95    /// Returns mutability metadata for a tool name when available.
96    /// Executors that can inspect concrete tools should override this.
97    fn tool_mutability(&self, tool_name: &str) -> crate::tools::ToolMutability {
98        crate::tools::classify_tool(tool_name)
99    }
100
101    /// Returns mutability metadata for a specific tool call when available.
102    /// Defaults to name-based classification.
103    fn call_mutability(&self, call: &ToolCall) -> crate::tools::ToolMutability {
104        self.tool_mutability(call.function.name.trim())
105    }
106
107    /// Returns whether a tool can safely execute in parallel with other
108    /// read-only tools. Executors that can inspect concrete tools should
109    /// override this. Fallback keeps current behavior for known read-only tools.
110    fn tool_concurrency_safe(&self, tool_name: &str) -> bool {
111        self.tool_mutability(tool_name) == crate::tools::ToolMutability::ReadOnly
112    }
113
114    /// Returns whether a specific tool call can safely run in parallel.
115    /// Defaults to the tool-name level classification.
116    fn call_concurrency_safe(&self, call: &ToolCall) -> bool {
117        self.tool_concurrency_safe(call.function.name.trim())
118    }
119}
120
121/// Executes a tool call with composition support
122///
123/// This function provides a unified interface for tool execution that supports
124/// both composition-based workflows and direct tool execution.
125///
126/// # Execution Strategy
127///
128/// 1. If a `composition_executor` is provided, attempts to execute as a composition
129/// 2. If composition execution fails with `NotFound`, falls back to direct execution
130/// 3. Other composition errors are propagated immediately
131///
132/// # Arguments
133///
134/// * `tool_call` - The tool call to execute
135/// * `tools` - Direct tool executor (fallback)
136/// * `composition_executor` - Optional composition-based executor
137///
138/// # Returns
139///
140/// The tool execution result or an error
141///
142/// # Example
143///
144/// ```ignore
145/// use bamboo_agent::agent::core::tools::executor::execute_tool_call;
146///
147/// let result = execute_tool_call(
148///     &tool_call,
149///     &registry,
150///     Some(composition_executor),
151/// ).await?;
152/// ```
153pub async fn execute_tool_call(
154    tool_call: &ToolCall,
155    tools: &dyn ToolExecutor,
156    composition_executor: Option<Arc<CompositionExecutor>>,
157) -> Result<ToolResult> {
158    execute_tool_call_with_context(
159        tool_call,
160        tools,
161        composition_executor,
162        ToolExecutionContext::none(&tool_call.id),
163    )
164    .await
165}
166
167/// Like [`execute_tool_call`], but provides a context to support streaming tools.
168pub async fn execute_tool_call_with_context(
169    tool_call: &ToolCall,
170    tools: &dyn ToolExecutor,
171    composition_executor: Option<Arc<CompositionExecutor>>,
172    ctx: ToolExecutionContext<'_>,
173) -> Result<ToolResult> {
174    if let Some(executor) = composition_executor {
175        let args_raw = tool_call.function.arguments.trim();
176        let (args, parse_warning) = parse_tool_args_best_effort(&tool_call.function.arguments);
177        if let Some(warning) = parse_warning {
178            tracing::warn!(
179                "Composition executor tool args fallback applied: tool_call_id={}, tool_name={}, args_len={}, warning={}",
180                tool_call.id,
181                tool_call.function.name,
182                args_raw.len(),
183                warning
184            );
185        }
186        let expr = ToolExpr::call(tool_call.function.name.clone(), args);
187        let mut exec_ctx = ExecutionContext::new();
188
189        match executor.execute(&expr, &mut exec_ctx).await {
190            Ok(result) => return Ok(result),
191            Err(ToolError::NotFound(_)) => {}
192            Err(error) => return Err(error),
193        }
194    }
195
196    tools.execute_with_context(tool_call, ctx).await
197}
198
199#[cfg(test)]
200mod tests {
201    use std::collections::HashMap;
202
203    use async_trait::async_trait;
204    use serde_json::json;
205
206    use crate::tools::{FunctionCall, Tool, ToolRegistry};
207
208    use super::*;
209
210    struct StaticExecutor {
211        results: HashMap<String, ToolResult>,
212    }
213
214    #[async_trait]
215    impl ToolExecutor for StaticExecutor {
216        async fn execute(&self, call: &ToolCall) -> Result<ToolResult> {
217            self.results
218                .get(&call.function.name)
219                .cloned()
220                .ok_or_else(|| ToolError::NotFound(call.function.name.clone()))
221        }
222
223        fn list_tools(&self) -> Vec<ToolSchema> {
224            Vec::new()
225        }
226    }
227
228    struct RegistryTool;
229
230    #[async_trait]
231    impl Tool for RegistryTool {
232        fn name(&self) -> &str {
233            "registry_tool"
234        }
235
236        fn description(&self) -> &str {
237            "registry tool"
238        }
239
240        fn parameters_schema(&self) -> serde_json::Value {
241            json!({
242                "type": "object",
243                "properties": {}
244            })
245        }
246
247        async fn execute(
248            &self,
249            _args: serde_json::Value,
250        ) -> std::result::Result<ToolResult, ToolError> {
251            Ok(ToolResult {
252                success: true,
253                result: "from-composition".to_string(),
254                display_preference: None,
255            })
256        }
257    }
258
259    fn make_tool_call(name: &str) -> ToolCall {
260        ToolCall {
261            id: "call_1".to_string(),
262            tool_type: "function".to_string(),
263            function: FunctionCall {
264                name: name.to_string(),
265                arguments: "{}".to_string(),
266            },
267        }
268    }
269
270    #[tokio::test]
271    async fn execute_tool_call_falls_back_when_composition_misses_tool() {
272        let mut results = HashMap::new();
273        results.insert(
274            "fallback_tool".to_string(),
275            ToolResult {
276                success: true,
277                result: "from-fallback".to_string(),
278                display_preference: None,
279            },
280        );
281
282        let tools = StaticExecutor { results };
283        let composition_executor =
284            Arc::new(CompositionExecutor::new(Arc::new(ToolRegistry::new())));
285        let tool_call = make_tool_call("fallback_tool");
286
287        let result = execute_tool_call(&tool_call, &tools, Some(composition_executor))
288            .await
289            .expect("fallback execution should succeed");
290
291        assert_eq!(result.result, "from-fallback");
292    }
293
294    #[tokio::test]
295    async fn execute_tool_call_uses_composition_when_available() {
296        let registry = Arc::new(ToolRegistry::new());
297        registry.register(RegistryTool).expect("register tool");
298
299        let tools = StaticExecutor {
300            results: HashMap::new(),
301        };
302        let composition_executor = Arc::new(CompositionExecutor::new(registry));
303        let tool_call = make_tool_call("registry_tool");
304
305        let result = execute_tool_call(&tool_call, &tools, Some(composition_executor))
306            .await
307            .expect("composition execution should succeed");
308
309        assert_eq!(result.result, "from-composition");
310    }
311}