bamboo_agent/agent/core/tools/
executor.rs1use 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#[derive(Error, Debug, Clone)]
19pub enum ToolError {
20 #[error("Tool not found: {0}")]
22 NotFound(String),
23
24 #[error("Execution failed: {0}")]
26 Execution(String),
27
28 #[error("Invalid arguments: {0}")]
30 InvalidArguments(String),
31}
32
33pub type Result<T> = std::result::Result<T, ToolError>;
35
36#[async_trait]
66pub trait ToolExecutor: Send + Sync {
67 async fn execute(&self, call: &ToolCall) -> Result<ToolResult>;
77
78 async fn execute_with_context(
83 &self,
84 call: &ToolCall,
85 _ctx: ToolExecutionContext<'_>,
86 ) -> Result<ToolResult> {
87 self.execute(call).await
88 }
89
90 fn list_tools(&self) -> Vec<ToolSchema>;
94}
95
96pub 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
142pub 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}