bamboo_engine/runtime/managers/adapters/
tool.rs1use std::sync::Arc;
2
3use async_trait::async_trait;
4use bamboo_agent_core::tools::{ToolCall, ToolExecutor, ToolSchema};
5use bamboo_agent_core::{AgentError, AgentEvent, Session};
6use bamboo_llm::LLMProvider;
7use bamboo_metrics::MetricsCollector;
8use tokio::sync::mpsc;
9use tokio_util::sync::CancellationToken;
10
11use crate::runtime::config::AgentLoopConfig;
12use crate::runtime::managers::tool::{ToolManager, ToolRoundResult};
13use crate::runtime::task_context::TaskLoopContext;
14
15pub struct DefaultToolManager {
17 tools: Arc<dyn ToolExecutor>,
18 llm: Arc<dyn LLMProvider>,
19}
20
21impl DefaultToolManager {
22 pub fn new(tools: Arc<dyn ToolExecutor>, llm: Arc<dyn LLMProvider>) -> Self {
23 Self { tools, llm }
24 }
25}
26
27#[async_trait]
28impl ToolManager for DefaultToolManager {
29 fn resolve_tool_schemas(&self, config: &AgentLoopConfig, session: &Session) -> Vec<ToolSchema> {
30 crate::runtime::runner::session_setup::tool_schemas::resolve_available_tool_schemas_for_session(
31 config,
32 self.tools.as_ref(),
33 session,
34 )
35 }
36
37 #[allow(clippy::too_many_arguments)]
38 async fn execute_tool_calls(
39 &self,
40 tool_calls: &[ToolCall],
41 event_tx: &mpsc::Sender<AgentEvent>,
42 metrics_collector: Option<&MetricsCollector>,
43 session_id: &str,
44 round_id: &str,
45 round: usize,
46 session: &mut Session,
47 config: &AgentLoopConfig,
48 task_context: &mut Option<TaskLoopContext>,
49 tool_schemas: &[ToolSchema],
50 cancel: &CancellationToken,
51 ) -> Result<ToolRoundResult, AgentError> {
52 let frame = crate::runtime::runner::round_frame::RoundFrame {
53 session_id,
54 round_id,
55 turn: round,
56 debug_enabled: false,
57 event_tx,
58 metrics_collector,
59 config,
60 llm: &self.llm,
61 tools: &self.tools,
62 };
63 let mut runtime_state = session
64 .agent_runtime_state
65 .clone()
66 .unwrap_or_else(|| bamboo_domain::AgentRuntimeState::new(session_id));
67 let effective_callable_set =
68 crate::runtime::runner::tool_execution::legacy_effective_callable_set(tool_schemas);
69
70 let result = tokio::select! {
78 biased;
79 _ = cancel.cancelled() => return Err(AgentError::Cancelled),
80 result = crate::runtime::runner::tool_execution::execute_round_tool_calls(
81 crate::runtime::runner::tool_execution::RoundToolExecution {
82 tool_calls,
83 frame: &frame,
84 session,
85 runtime_state: &mut runtime_state,
86 task_context,
87 compression_model_name: config
88 .summarization_model_name
89 .as_deref()
90 .or(config.background_model_name.as_deref()),
91 compression_model_provider: config
92 .summarization_model_provider
93 .as_ref()
94 .or(config.background_model_provider.as_ref()),
95 tool_schemas,
96 effective_callable_set: &effective_callable_set,
97 },
98 ) => result?,
99 };
100 if !config.hook_runner.is_empty() {
101 session.agent_runtime_state = Some(runtime_state);
102 }
103
104 Ok(ToolRoundResult {
105 awaiting_clarification: result.awaiting_clarification,
106 should_break: false,
107 tool_calls_count: tool_calls.len(),
108 })
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use bamboo_agent_core::tools::{FunctionCall, ToolError, ToolExecutionContext, ToolResult};
116 use bamboo_agent_core::Message;
117 use bamboo_llm::provider::LLMStream;
118
119 struct PanicProvider;
122 #[async_trait]
123 impl bamboo_llm::LLMProvider for PanicProvider {
124 async fn chat_stream(
125 &self,
126 _messages: &[Message],
127 _tools: &[ToolSchema],
128 _max_output_tokens: Option<u32>,
129 _model: &str,
130 ) -> bamboo_llm::provider::Result<LLMStream> {
131 panic!("LLM must not be invoked when the run is already cancelled");
132 }
133 }
134
135 struct PanicExecutor;
136 #[async_trait]
137 impl ToolExecutor for PanicExecutor {
138 async fn execute(&self, _call: &ToolCall) -> Result<ToolResult, ToolError> {
139 panic!("tools must not run when the run is already cancelled");
140 }
141 async fn execute_with_context(
142 &self,
143 call: &ToolCall,
144 _ctx: ToolExecutionContext<'_>,
145 ) -> Result<ToolResult, ToolError> {
146 self.execute(call).await
147 }
148 fn list_tools(&self) -> Vec<ToolSchema> {
149 Vec::new()
150 }
151 }
152
153 #[tokio::test]
154 async fn execute_tool_calls_short_circuits_to_cancelled_when_token_already_fired() {
155 let mgr = DefaultToolManager::new(Arc::new(PanicExecutor), Arc::new(PanicProvider));
156 let (event_tx, _rx) = mpsc::channel(8);
157 let mut session = Session::new("s1", "model");
158 let config = AgentLoopConfig::default();
159 let mut task_context = None;
160 let tool_calls = vec![ToolCall {
163 id: "c1".to_string(),
164 tool_type: "function".to_string(),
165 function: FunctionCall {
166 name: "anything".to_string(),
167 arguments: "{}".to_string(),
168 },
169 }];
170
171 let cancel = CancellationToken::new();
172 cancel.cancel(); let result = mgr
175 .execute_tool_calls(
176 &tool_calls,
177 &event_tx,
178 None,
179 "s1",
180 "r1",
181 0,
182 &mut session,
183 &config,
184 &mut task_context,
185 &[],
186 &cancel,
187 )
188 .await;
189
190 assert!(
191 matches!(result, Err(AgentError::Cancelled)),
192 "a pre-cancelled token returns Cancelled before touching tools/LLM; got {result:?}"
193 );
194 }
195}