Skip to main content

bamboo_agent/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::agent::core::composition::{CompositionExecutor, ExecutionContext, ToolExpr};
12use crate::agent::core::tools::{ToolCall, ToolResult, ToolSchema};
13
14use super::result_handler::parse_tool_args;
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
96/// Executes a tool call with composition support
97///
98/// This function provides a unified interface for tool execution that supports
99/// both composition-based workflows and direct tool execution.
100///
101/// # Execution Strategy
102///
103/// 1. If a `composition_executor` is provided, attempts to execute as a composition
104/// 2. If composition execution fails with `NotFound`, falls back to direct execution
105/// 3. Other composition errors are propagated immediately
106///
107/// # Arguments
108///
109/// * `tool_call` - The tool call to execute
110/// * `tools` - Direct tool executor (fallback)
111/// * `composition_executor` - Optional composition-based executor
112///
113/// # Returns
114///
115/// The tool execution result or an error
116///
117/// # Example
118///
119/// ```ignore
120/// use bamboo_agent::agent::core::tools::executor::execute_tool_call;
121///
122/// let result = execute_tool_call(
123///     &tool_call,
124///     &registry,
125///     Some(composition_executor),
126/// ).await?;
127/// ```
128pub async fn execute_tool_call(
129    tool_call: &ToolCall,
130    tools: &dyn ToolExecutor,
131    composition_executor: Option<Arc<CompositionExecutor>>,
132) -> Result<ToolResult> {
133    execute_tool_call_with_context(
134        tool_call,
135        tools,
136        composition_executor,
137        ToolExecutionContext::none(&tool_call.id),
138    )
139    .await
140}
141
142/// Like [`execute_tool_call`], but provides a context to support streaming tools.
143pub async fn execute_tool_call_with_context(
144    tool_call: &ToolCall,
145    tools: &dyn ToolExecutor,
146    composition_executor: Option<Arc<CompositionExecutor>>,
147    ctx: ToolExecutionContext<'_>,
148) -> Result<ToolResult> {
149    if let Some(executor) = composition_executor {
150        let args = parse_tool_args(&tool_call.function.arguments)?;
151        let expr = ToolExpr::call(tool_call.function.name.clone(), args);
152        let mut exec_ctx = ExecutionContext::new();
153
154        match executor.execute(&expr, &mut exec_ctx).await {
155            Ok(result) => return Ok(result),
156            Err(ToolError::NotFound(_)) => {}
157            Err(error) => return Err(error),
158        }
159    }
160
161    tools.execute_with_context(tool_call, ctx).await
162}
163
164#[cfg(test)]
165mod tests {
166    use std::collections::HashMap;
167
168    use async_trait::async_trait;
169    use serde_json::json;
170
171    use crate::agent::core::tools::{FunctionCall, Tool, ToolRegistry};
172
173    use super::*;
174
175    struct StaticExecutor {
176        results: HashMap<String, ToolResult>,
177    }
178
179    #[async_trait]
180    impl ToolExecutor for StaticExecutor {
181        async fn execute(&self, call: &ToolCall) -> Result<ToolResult> {
182            self.results
183                .get(&call.function.name)
184                .cloned()
185                .ok_or_else(|| ToolError::NotFound(call.function.name.clone()))
186        }
187
188        fn list_tools(&self) -> Vec<ToolSchema> {
189            Vec::new()
190        }
191    }
192
193    struct RegistryTool;
194
195    #[async_trait]
196    impl Tool for RegistryTool {
197        fn name(&self) -> &str {
198            "registry_tool"
199        }
200
201        fn description(&self) -> &str {
202            "registry tool"
203        }
204
205        fn parameters_schema(&self) -> serde_json::Value {
206            json!({
207                "type": "object",
208                "properties": {}
209            })
210        }
211
212        async fn execute(
213            &self,
214            _args: serde_json::Value,
215        ) -> std::result::Result<ToolResult, ToolError> {
216            Ok(ToolResult {
217                success: true,
218                result: "from-composition".to_string(),
219                display_preference: None,
220            })
221        }
222    }
223
224    fn make_tool_call(name: &str) -> ToolCall {
225        ToolCall {
226            id: "call_1".to_string(),
227            tool_type: "function".to_string(),
228            function: FunctionCall {
229                name: name.to_string(),
230                arguments: "{}".to_string(),
231            },
232        }
233    }
234
235    #[tokio::test]
236    async fn execute_tool_call_falls_back_when_composition_misses_tool() {
237        let mut results = HashMap::new();
238        results.insert(
239            "fallback_tool".to_string(),
240            ToolResult {
241                success: true,
242                result: "from-fallback".to_string(),
243                display_preference: None,
244            },
245        );
246
247        let tools = StaticExecutor { results };
248        let composition_executor =
249            Arc::new(CompositionExecutor::new(Arc::new(ToolRegistry::new())));
250        let tool_call = make_tool_call("fallback_tool");
251
252        let result = execute_tool_call(&tool_call, &tools, Some(composition_executor))
253            .await
254            .expect("fallback execution should succeed");
255
256        assert_eq!(result.result, "from-fallback");
257    }
258
259    #[tokio::test]
260    async fn execute_tool_call_uses_composition_when_available() {
261        let registry = Arc::new(ToolRegistry::new());
262        registry.register(RegistryTool).expect("register tool");
263
264        let tools = StaticExecutor {
265            results: HashMap::new(),
266        };
267        let composition_executor = Arc::new(CompositionExecutor::new(registry));
268        let tool_call = make_tool_call("registry_tool");
269
270        let result = execute_tool_call(&tool_call, &tools, Some(composition_executor))
271            .await
272            .expect("composition execution should succeed");
273
274        assert_eq!(result.result, "from-composition");
275    }
276}