Skip to main content

ToolExecutor

Trait ToolExecutor 

Source
pub trait ToolExecutor: Send + Sync {
    // 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 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 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§

Source

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,

Executes a tool call

§Arguments
  • call - The tool call to execute (contains tool name and arguments)
§Returns

The tool execution result or an error

Source

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§

Source

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).

Source

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.

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, 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 THIS ToolOutcome is 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 (a Completed result tagged display_preference = "request_permissions" that the engine turns into a clarification pause). Returning it here — rather than collapsing it to an Err — 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.

Source

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.

Source

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.

Source

fn call_mutability(&self, call: &ToolCall) -> ToolMutability

Returns mutability metadata for a specific tool call when available. Defaults to name-based classification.

Source

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.

Source

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.

Source

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

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,

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 + args the 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_forced so the active mode/bypass can’t suppress the prompt;
  • a ConfirmationRequired first tries the cross-process ApprovalProxy (a subagent worker forwarding to its host), then the interactive human sink (returning the synthesized approval pause as Ok(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.

Source§

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, BuiltinToolExecutor: 'async_trait,

Source§

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, BuiltinToolExecutor: 'async_trait,

Source§

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, BuiltinToolExecutor: 'async_trait,

Source§

fn list_tools(&self) -> Vec<ToolSchema>

Source§

fn tool_mutability(&self, tool_name: &str) -> ToolMutability

Source§

fn call_mutability(&self, call: &ToolCall) -> ToolMutability

Source§

fn tool_concurrency_safe(&self, tool_name: &str) -> bool

Source§

fn call_concurrency_safe(&self, call: &ToolCall) -> bool

Source§

fn call_parallel_classification( &self, call: &ToolCall, ) -> (ToolMutability, bool)

Source§

impl ToolExecutor for McpProxyExecutor

Source§

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, McpProxyExecutor: 'async_trait,

Source§

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, McpProxyExecutor: 'async_trait,

Source§

fn list_tools(&self) -> Vec<ToolSchema>

Implementors§