Skip to main content

bamboo_agent_core/tools/
mod.rs

1//! Tool execution system for Bamboo agents.
2//!
3//! This module provides a comprehensive framework for defining, registering, and executing
4//! tools that can be used by AI agents to interact with external systems.
5//!
6//! # Architecture
7//!
8//! The tools system is built around several key components:
9//!
10//! - **accumulator**: Accumulates partial tool calls from streaming responses
11//! - **agentic**: Agentic tool execution with multi-step capabilities
12//! - **executor**: Core tool execution logic
13//! - **registry**: Tool registration and lookup
14//! - **result_handler**: Processes tool results and handles agentic support
15//! - **smart_code_review**: Specialized tool for intelligent code review
16//! - **types**: Core type definitions for tools
17//!
18//! # Key Concepts
19//!
20//! ## Tool Registry
21//!
22//! Tools are registered in a central [`ToolRegistry`] that maps tool names to their
23//! implementations. The registry supports:
24//!
25//! - Dynamic tool registration
26//! - Tool name normalization
27//! - Global singleton access via [`global_registry`]
28//!
29//! ## Tool Execution
30//!
31//! Tools implement the [`ToolExecutor`] trait and can be executed via [`execute_tool_call`].
32//! The execution flow:
33//!
34//! 1. Parse tool arguments from JSON
35//! 2. Execute the tool logic
36//! 3. Return a [`ToolResult`] with success/failure status
37//!
38//! ## Agentic Tools
39//!
40//! Some tools support "agentic" behavior, allowing multi-step execution:
41//!
42//! - [`AgenticTool`]: Marker trait for agentic tools
43//! - [`AgenticContext`]: Context for agentic execution
44//! - [`AgenticToolResult`]: Extended result type with sub-actions
45//!
46//! # Example
47//!
48//! ```rust,ignore
49//! use async_trait::async_trait;
50//! use bamboo_agent::agent::core::tools::{
51//!     execute_tool_call, FunctionCall, ToolCall, ToolError, ToolExecutor, ToolResult, ToolSchema,
52//! };
53//!
54//! struct NoopExecutor;
55//!
56//! #[async_trait]
57//! impl ToolExecutor for NoopExecutor {
58//!     async fn execute(&self, call: &ToolCall) -> Result<ToolResult, ToolError> {
59//!         Err(ToolError::NotFound(call.function.name.clone()))
60//!     }
61//!
62//!     fn list_tools(&self) -> Vec<ToolSchema> {
63//!         Vec::new()
64//!     }
65//! }
66//!
67//! #[tokio::main]
68//! async fn main() {
69//!     // Execute a tool call (this example uses a no-op executor).
70//!     let call = ToolCall {
71//!         id: "call-1".to_string(),
72//!         tool_type: "function".to_string(),
73//!         function: FunctionCall {
74//!             name: "read_file".to_string(),
75//!             arguments: r#"{\"path\":\"/tmp/test.txt\"}"#.to_string(),
76//!         },
77//!     };
78//!
79//!     let _ = execute_tool_call(&call, &NoopExecutor, None).await;
80//! }
81//! ```
82//!
83//! # Re-exports
84//!
85//! Key types and functions re-exported for convenience:
86//!
87//! - Accumulator: [`ToolCallAccumulator`], [`PartialToolCall`], [`finalize_tool_calls`]
88//! - Agentic: [`AgenticTool`], [`AgenticContext`], [`AgenticToolResult`], [`ToolGoal`]
89//! - Executor: [`ToolExecutor`], [`execute_tool_call`], [`ToolError`]
90//! - Registry: [`ToolRegistry`], [`Tool`], [`global_registry`]
91//! - Types: [`ToolCall`], [`ToolResult`], [`ToolSchema`]
92
93pub mod accumulator;
94pub mod agentic;
95pub mod bash_completion;
96pub mod context;
97pub mod execution_contract;
98pub mod executor;
99pub mod input_guard;
100mod output_cap_observation;
101pub mod pending_operation;
102pub mod registry;
103pub mod result_handler;
104pub mod smart_code_review;
105mod suspension;
106pub mod tool_runtime;
107pub mod types;
108
109pub use accumulator::{
110    finalize_tool_calls, update_partial_tool_call, update_partial_tool_call_indexed,
111    PartialToolCall, ToolCallAccumulator,
112};
113pub use agentic::{
114    convert_from_standard_result, convert_to_standard_result, AgenticContext, AgenticTool,
115    AgenticToolExecutor, AgenticToolResult, Interaction, InteractionRole, ToolGoal,
116};
117pub use bash_completion::{BashCompletionInfo, BashCompletionSink};
118pub use context::{
119    with_executing_session_caller, ExecutingSupervisorObservation, ToolExecutionContext,
120    ToolExecutionSessionFlags,
121};
122pub use execution_contract::{
123    retained_tool_execution_for, retained_tool_execution_for_call, retained_tool_owner_lookup,
124    with_retained_tool_execution, RetainedToolExecution, FRAMEWORK_ROOT_SPAWN_CONTRACT_V1,
125};
126mod framework_session;
127pub use executor::{execute_tool_call, execute_tool_call_with_context, ToolError, ToolExecutor};
128pub use framework_session::{
129    FrameworkSessionToolGuard, FrameworkSessionToolKind, HostFrameworkSessionTool,
130};
131pub use output_cap_observation::{observed_tool_output_cap, scope_tool_output_cap};
132pub use pending_operation::{PendingToolOperation, PENDING_TOOL_OPERATION_METADATA_KEY};
133pub use registry::{
134    global_registry, normalize_tool_name, RegistryError, SharedTool, Tool, ToolRegistry,
135};
136pub use result_handler::{
137    execute_sub_actions, handle_tool_result_with_agentic_support,
138    handle_tool_result_with_agentic_support_and_persistence, is_waiting_for_children_control,
139    parse_tool_args, parse_tool_args_best_effort, send_clarification_request,
140    tool_result_requires_suspension, tool_start_arguments_for_display, try_parse_agentic_result,
141    ToolHandlingOutcome, MAX_SUB_ACTIONS,
142};
143pub use smart_code_review::SmartCodeReviewTool;
144pub use suspension::{
145    capture_suspension_restriction, current_tool_suspension_scope, observe_tool_suspension,
146    suspension_allowed, suspension_running_violated, with_suspension_disabled,
147    with_suspension_disabled_observed, SuspensionRestriction, ToolSuspensionObservation,
148};
149pub use tool_runtime::{
150    AsyncToolCompletionInfo, AsyncToolCompletionSink, AsyncWaitKind, RunningCompletion,
151    RunningHandle, ToolClass, ToolCtx, ToolOutcome, ToolResultFuture,
152};
153pub use types::{FunctionCall, FunctionSchema, ToolCall, ToolResult, ToolResultImage, ToolSchema};
154
155/// Classification of a tool call for approval purposes.
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum ToolMutability {
158    ReadOnly,
159    Mutating,
160}
161
162/// Read-only tools that don't require user approval.
163const READ_ONLY_TOOLS: &[&str] = &[
164    "Read",
165    "GetFileInfo",
166    "Glob",
167    "Grep",
168    "WebFetch",
169    "WebSearch",
170    "ViewImage",
171    "Workspace",
172    "BashOutput",
173    "session_note",
174    "memory_note",
175    "session_history",
176    "session_history_current",
177    "bamboo_diagnostics",
178    "recall",
179    "session_inspector",
180    "archive_context",
181    "compact_context",
182    "Sleep",
183    // The goal self-report tool records a status only; the durable goal-state
184    // mutation happens in the engine, and it touches no user-facing state, so it
185    // never needs approval.
186    "update_goal",
187    // `notify` (an overlay tool, like `compact_context`/`session_inspector`
188    // above) fires an outbound OS popup / push side effect but mutates
189    // nothing in the session or workspace and produces no result the model
190    // reads back — safe both to run in the concurrent read-only batch
191    // (`ToolExecutor::tool_concurrency_safe`, which name-matches here since
192    // overlay tools aren't in a `BuiltinToolExecutor` registry for their own
193    // `Tool::classify()` to be consulted) and to allow through the Plan Mode
194    // gate (surfacing a heads-up is exactly the "needs attention" case Plan
195    // Mode shouldn't block).
196    "notify",
197];
198
199/// Classify a tool call as read-only or mutating.
200pub fn classify_tool(tool_name: &str) -> ToolMutability {
201    if READ_ONLY_TOOLS
202        .iter()
203        .any(|&t| t.eq_ignore_ascii_case(tool_name))
204    {
205        ToolMutability::ReadOnly
206    } else {
207        ToolMutability::Mutating
208    }
209}
210
211/// Shared Plan-mode authorization gate used by agent loops and direct tool
212/// dispatch. Unknown tools fail closed as mutating; only read-only tools and
213/// the bounded pause/clarification set are admitted.
214pub fn plan_mode_allows_tool(tool_name: &str) -> bool {
215    classify_tool(tool_name) == ToolMutability::ReadOnly
216        || [
217            "EnterPlanMode",
218            "ExitPlanMode",
219            "request_permissions",
220            "archive_context",
221            "compact_context",
222            // This mixed-action tool checks Plan posture itself: target
223            // discovery is read-only, while send/reply remain forbidden.
224            "session_message",
225        ]
226        .iter()
227        .any(|allowed| allowed.eq_ignore_ascii_case(tool_name))
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn diagnostics_is_read_only_in_shared_plan_gate() {
236        assert_eq!(
237            classify_tool("bamboo_diagnostics"),
238            ToolMutability::ReadOnly
239        );
240        assert!(plan_mode_allows_tool("bamboo_diagnostics"));
241        assert!(plan_mode_allows_tool("BAMBOO_DIAGNOSTICS"));
242        assert!(!plan_mode_allows_tool("bamboo_diagnostics_repair"));
243    }
244
245    #[test]
246    fn classify_compact_context_as_read_only() {
247        assert_eq!(classify_tool("compact_context"), ToolMutability::ReadOnly);
248    }
249
250    #[test]
251    fn classify_archive_context_as_read_only() {
252        assert_eq!(classify_tool("archive_context"), ToolMutability::ReadOnly);
253        assert!(plan_mode_allows_tool("ARCHIVE_CONTEXT"));
254    }
255
256    #[test]
257    fn classify_compact_context_case_insensitive() {
258        assert_eq!(classify_tool("Compact_Context"), ToolMutability::ReadOnly);
259        assert_eq!(classify_tool("COMPACT_CONTEXT"), ToolMutability::ReadOnly);
260    }
261
262    #[test]
263    fn classify_write_as_mutating() {
264        assert_eq!(classify_tool("Write"), ToolMutability::Mutating);
265    }
266
267    #[test]
268    fn session_message_plan_entry_keeps_mutating_name_classification() {
269        assert!(plan_mode_allows_tool("session_message"));
270        assert_eq!(classify_tool("session_message"), ToolMutability::Mutating);
271        assert!(!plan_mode_allows_tool("session_control"));
272    }
273
274    #[test]
275    fn classify_unknown_as_mutating() {
276        assert_eq!(
277            classify_tool("totally_unknown_tool"),
278            ToolMutability::Mutating
279        );
280    }
281
282    #[test]
283    fn classify_all_read_only_tools() {
284        for name in READ_ONLY_TOOLS {
285            assert_eq!(
286                classify_tool(name),
287                ToolMutability::ReadOnly,
288                "{name} should be classified as read-only"
289            );
290        }
291    }
292}