pub trait ToolExecutor: Send + Sync {
Show 15 methods
// Required methods
fn execute<'life0, 'life1, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
) -> Pin<Box<dyn Future<Output = Result<ToolResult, ToolError>> + Send + 'async_trait>>
where 'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait;
fn list_tools(&self) -> Vec<ToolSchema>;
// Provided methods
fn execute_with_context<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
_ctx: ToolExecutionContext<'life2>,
) -> Pin<Box<dyn Future<Output = Result<ToolResult, ToolError>> + Send + 'async_trait>>
where 'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: 'async_trait { ... }
fn execute_with_context_outcome<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
ctx: ToolExecutionContext<'life2>,
) -> Pin<Box<dyn Future<Output = Result<ToolOutcome, ToolError>> + Send + 'async_trait>>
where 'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: 'async_trait { ... }
fn execute_exact_with_context_outcome<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
execution_name: &'life2 str,
ctx: ToolExecutionContext<'life3>,
) -> Pin<Box<dyn Future<Output = Result<ToolOutcome, ToolError>> + Send + 'async_trait>>
where 'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
Self: 'async_trait { ... }
fn check_permissions_for<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
_call: &'life1 ToolCall,
_ctx: &'life2 ToolExecutionContext<'life3>,
) -> Pin<Box<dyn Future<Output = Result<Option<ToolOutcome>, ToolError>> + Send + 'async_trait>>
where 'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
Self: 'async_trait { ... }
fn check_permissions_for_exact<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
execution_name: &'life2 str,
ctx: &'life3 ToolExecutionContext<'life4>,
) -> Pin<Box<dyn Future<Output = Result<Option<ToolOutcome>, ToolError>> + Send + 'async_trait>>
where 'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
Self: 'async_trait { ... }
fn check_permissions_for_resolved<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
execution_name: &'life2 str,
args: &'life3 Value,
ctx: &'life4 ToolExecutionContext<'life5>,
) -> Pin<Box<dyn Future<Output = Result<Option<ToolOutcome>, ToolError>> + Send + 'async_trait>>
where 'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
'life5: 'async_trait,
Self: 'async_trait { ... }
fn owns_exact_tool(&self, tool_name: &str) -> bool { ... }
fn tool_guidance(&self) -> Option<String> { ... }
fn tool_mutability(&self, tool_name: &str) -> ToolMutability { ... }
fn call_mutability(&self, call: &ToolCall) -> ToolMutability { ... }
fn tool_concurrency_safe(&self, tool_name: &str) -> bool { ... }
fn call_concurrency_safe(&self, call: &ToolCall) -> bool { ... }
fn call_parallel_classification(
&self,
call: &ToolCall,
) -> (ToolMutability, bool) { ... }
}Expand description
Trait for tool execution backends
This trait defines the interface for executing tool calls and listing available tools. Implementations can wrap tool registries, provide mock tools for testing, or implement custom execution logic.
§Example
use bamboo_agent::agent::core::tools::executor::ToolExecutor;
struct MyExecutor {
tools: HashMap<String, Box<dyn Tool>>,
}
#[async_trait]
impl ToolExecutor for MyExecutor {
async fn execute(&self, call: &ToolCall) -> Result<ToolResult> {
let tool = self.tools.get(&call.function.name)
.ok_or_else(|| ToolError::NotFound(call.function.name.clone()))?;
let args = parse_tool_args(&call.function.arguments)?;
tool.execute(args).await
}
fn list_tools(&self) -> Vec<ToolSchema> {
self.tools.values().map(|t| t.schema()).collect()
}
}Required Methods§
Sourcefn execute<'life0, 'life1, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
) -> Pin<Box<dyn Future<Output = Result<ToolResult, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait,
fn execute<'life0, 'life1, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
) -> Pin<Box<dyn Future<Output = Result<ToolResult, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
Self: 'async_trait,
Sourcefn list_tools(&self) -> Vec<ToolSchema>
fn list_tools(&self) -> Vec<ToolSchema>
Lists all available tools and their schemas
Returns schemas for all tools that can be executed via this executor
Provided Methods§
Sourcefn execute_with_context<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
_ctx: ToolExecutionContext<'life2>,
) -> Pin<Box<dyn Future<Output = Result<ToolResult, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: 'async_trait,
fn execute_with_context<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
_ctx: ToolExecutionContext<'life2>,
) -> Pin<Box<dyn Future<Output = Result<ToolResult, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: 'async_trait,
Executes a tool call with streaming-capable context.
Default implementation falls back to execute() for executors that don’t
support streaming (e.g. remote MCP tools).
Sourcefn execute_with_context_outcome<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
ctx: ToolExecutionContext<'life2>,
) -> Pin<Box<dyn Future<Output = Result<ToolOutcome, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: 'async_trait,
fn execute_with_context_outcome<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
ctx: ToolExecutionContext<'life2>,
) -> Pin<Box<dyn Future<Output = Result<ToolOutcome, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Self: 'async_trait,
Outcome-aware dispatch: returns the tool’s ToolOutcome rather than the
collapsed ToolResult, so the agent loop can branch on
Completed/Running/NeedsHuman directly instead of sniffing markers on
a result. The default collapses via the execute_with_context path
(always Completed), so executors that never surface Running/NeedsHuman
(composition, MCP, tests) need no override; the built-in + overlay
executors override this to return the real outcome.
Sourcefn execute_exact_with_context_outcome<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
execution_name: &'life2 str,
ctx: ToolExecutionContext<'life3>,
) -> Pin<Box<dyn Future<Output = Result<ToolOutcome, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
Self: 'async_trait,
fn execute_exact_with_context_outcome<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
execution_name: &'life2 str,
ctx: ToolExecutionContext<'life3>,
) -> Pin<Box<dyn Future<Output = Result<ToolOutcome, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
Self: 'async_trait,
Execute a call through an already-selected exact owner.
execution_name is the complete, case-sensitive identity returned by
the shared reference resolver. The original call is retained so a
concrete executor can apply compatibility argument handling based on the
caller’s spelling without re-running ownership selection. The default
rewrites only the function name and delegates to the outcome-aware path.
Sourcefn check_permissions_for<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
_call: &'life1 ToolCall,
_ctx: &'life2 ToolExecutionContext<'life3>,
) -> Pin<Box<dyn Future<Output = Result<Option<ToolOutcome>, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
Self: 'async_trait,
fn check_permissions_for<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
_call: &'life1 ToolCall,
_ctx: &'life2 ToolExecutionContext<'life3>,
) -> Pin<Box<dyn Future<Output = Result<Option<ToolOutcome>, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
Self: 'async_trait,
Permission gate for a tool call, exposed on the executor trait so the check is part of the executor chain.
An overlay/wrapping executor runs its own tool directly (it does NOT route
that call through the inner executor’s execute path), so before this
method existed the inner executor’s permission check was silently skipped
for those tools (issue #341). By putting the check on the trait, a wrapper
can call the inner executor’s real check before invoking its own tool.
Returns:
Ok(None)— the call is permitted; the caller proceeds to run the tool.Ok(Some(outcome))— the permission layer intercepts the call and THISToolOutcomeis the tool call’s result without the tool running. It carries the interactive “awaiting approval” pause the built-in executor synthesizes for a human event sink (aCompletedresult taggeddisplay_preference = "request_permissions"that the engine turns into a clarification pause). Returning it here — rather than collapsing it to anErr— is what preserves the built-in path’s exact pause-and-ask behavior when the check is routed through this method.Err(_)— the call is denied / fails closed.
The default returns Ok(None) (no gate), so executors that enforce no
permissions are unaffected. The built-in executor overrides this with the
real check; overlay/wrapping executors delegate to their inner executor’s
implementation so the gate can’t be dropped by stacking a wrapper.
Sourcefn check_permissions_for_exact<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
execution_name: &'life2 str,
ctx: &'life3 ToolExecutionContext<'life4>,
) -> Pin<Box<dyn Future<Output = Result<Option<ToolOutcome>, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
Self: 'async_trait,
fn check_permissions_for_exact<'life0, 'life1, 'life2, 'life3, 'life4, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
execution_name: &'life2 str,
ctx: &'life3 ToolExecutionContext<'life4>,
) -> Pin<Box<dyn Future<Output = Result<Option<ToolOutcome>, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
Self: 'async_trait,
Permission gate for an already-selected exact owner identity.
Routing wrappers must use this seam after ownership resolution instead
of handing the original reference back to
check_permissions_for, where a concrete
executor may resolve it differently. The default preserves compatibility
by pinning only the function name and delegating to the existing gate;
argument bytes and the caller’s pre-parsed context remain unchanged.
Sourcefn check_permissions_for_resolved<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
execution_name: &'life2 str,
args: &'life3 Value,
ctx: &'life4 ToolExecutionContext<'life5>,
) -> Pin<Box<dyn Future<Output = Result<Option<ToolOutcome>, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
'life5: 'async_trait,
Self: 'async_trait,
fn check_permissions_for_resolved<'life0, 'life1, 'life2, 'life3, 'life4, 'life5, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
execution_name: &'life2 str,
args: &'life3 Value,
ctx: &'life4 ToolExecutionContext<'life5>,
) -> Pin<Box<dyn Future<Output = Result<Option<ToolOutcome>, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
'life4: 'async_trait,
'life5: 'async_trait,
Self: 'async_trait,
Permission gate for a call whose exact execution identity and effective arguments have already been resolved by a wrapping executor.
Routing wrappers use this seam after selecting an exact owner so the
permission layer observes the same identity and arguments that the tool
will receive. The default rebuilds both the call and its pre-parsed-args
context before delegating to
check_permissions_for, preserving source
compatibility for executors without a specialized permission layer.
Sourcefn owns_exact_tool(&self, tool_name: &str) -> bool
fn owns_exact_tool(&self, tool_name: &str) -> bool
Returns whether this executor owns tool_name as a complete, exact,
case-sensitive execution identity.
This deliberately performs no namespace stripping or alias resolution. Wrappers use it to select an owner before applying compatibility fallbacks. Registry/index-backed production executors should override the source-compatible default to avoid cloning parameter schemas per query.
Sourcefn tool_guidance(&self) -> Option<String>
fn tool_guidance(&self) -> Option<String>
Server-level usage guidance to surface in the system prompt for whatever
this executor currently exposes — e.g. the instructions an MCP server
returns from initialize. Because it is derived from the live executor,
the text is naturally scoped to what is actually connected/loaded for the
run (a disconnected server contributes nothing).
Returns None when there is no guidance to add (the default for executors
that have none). The string, when present, is appended to the tool-guide
section of the prompt.
Sourcefn tool_mutability(&self, tool_name: &str) -> ToolMutability
fn tool_mutability(&self, tool_name: &str) -> ToolMutability
Returns mutability metadata for a tool name when available. Executors that can inspect concrete tools should override this.
Sourcefn call_mutability(&self, call: &ToolCall) -> ToolMutability
fn call_mutability(&self, call: &ToolCall) -> ToolMutability
Returns mutability metadata for a specific tool call when available. Defaults to name-based classification.
Sourcefn tool_concurrency_safe(&self, tool_name: &str) -> bool
fn tool_concurrency_safe(&self, tool_name: &str) -> bool
Returns whether a tool can safely execute in parallel with other read-only tools. Executors that can inspect concrete tools should override this. Fallback keeps current behavior for known read-only tools.
Sourcefn call_concurrency_safe(&self, call: &ToolCall) -> bool
fn call_concurrency_safe(&self, call: &ToolCall) -> bool
Returns whether a specific tool call can safely run in parallel. Defaults to the tool-name level classification.
Sourcefn call_parallel_classification(
&self,
call: &ToolCall,
) -> (ToolMutability, bool)
fn call_parallel_classification( &self, call: &ToolCall, ) -> (ToolMutability, bool)
Classify a tool call’s mutability AND concurrency-safety together,
returning (mutability, concurrency_safe).
The default delegates to call_mutability and
call_concurrency_safe — i.e. the exact
prior behavior, so every executor that doesn’t override this is
unchanged. Concrete executors that parse the call’s arguments inside BOTH
of those methods (e.g. BuiltinToolExecutor) override this to parse the
arguments a single time while returning the identical pair. The combined
result lets callers that need both (parallel scheduling) avoid a
redundant argument parse per tool call.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".
Implementations on Foreign Types§
Source§impl ToolExecutor for BuiltinToolExecutor
impl ToolExecutor for BuiltinToolExecutor
Source§fn check_permissions_for<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
ctx: &'life2 ToolExecutionContext<'life3>,
) -> Pin<Box<dyn Future<Output = Result<Option<ToolOutcome>, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
BuiltinToolExecutor: 'async_trait,
fn check_permissions_for<'life0, 'life1, 'life2, 'life3, 'async_trait>(
&'life0 self,
call: &'life1 ToolCall,
ctx: &'life2 ToolExecutionContext<'life3>,
) -> Pin<Box<dyn Future<Output = Result<Option<ToolOutcome>, ToolError>> + Send + 'async_trait>>where
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
'life3: 'async_trait,
BuiltinToolExecutor: 'async_trait,
The real permission gate for built-in tools, extracted from the execute
path so it is reusable by wrapping executors (issue #341). The behavior is
byte-for-byte the same block that used to run inline in
execute_with_context_outcome:
- resolves the SAME
tool_name+argsthe execute path runs with (so the check sees exactly what the tool will run with); - “always ask” rules (
requires_forced_confirmation) force a confirmation even under bypass; everything else is skipped when the session is in bypass-permissions mode; - forced confirmations route through
check_or_request_forcedso the active mode/bypass can’t suppress the prompt; - a
ConfirmationRequiredfirst tries the cross-processApprovalProxy(a subagent worker forwarding to its host), then the interactive human sink (returning the synthesized approval pause asOk(Some(..))), then fails closed; - deny fails closed.
The only mechanical difference from the old inline block: the interactive
pause is returned as Ok(Some(outcome)) and a clean pass returns
Ok(None), so the caller decides whether to run the tool. The fallback
arg-parse warning is intentionally NOT re-logged here — the execute path
already logs it once for this call.