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
68 let result = tokio::select! {
76 biased;
77 _ = cancel.cancelled() => return Err(AgentError::Cancelled),
78 result = crate::runtime::runner::tool_execution::execute_round_tool_calls(
79 tool_calls,
80 &frame,
81 session,
82 &mut runtime_state,
83 task_context,
84 config
85 .summarization_model_name
86 .as_deref()
87 .or(config.background_model_name.as_deref()),
88 config
89 .summarization_model_provider
90 .as_ref()
91 .or(config.background_model_provider.as_ref()),
92 tool_schemas,
93 ) => result?,
94 };
95 if !config.hook_runner.is_empty() {
96 session.agent_runtime_state = Some(runtime_state);
97 }
98
99 Ok(ToolRoundResult {
100 awaiting_clarification: result.awaiting_clarification,
101 should_break: false,
102 tool_calls_count: tool_calls.len(),
103 })
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use bamboo_agent_core::tools::{FunctionCall, ToolError, ToolExecutionContext, ToolResult};
111 use bamboo_agent_core::Message;
112 use bamboo_llm::provider::LLMStream;
113
114 struct PanicProvider;
117 #[async_trait]
118 impl bamboo_llm::LLMProvider for PanicProvider {
119 async fn chat_stream(
120 &self,
121 _messages: &[Message],
122 _tools: &[ToolSchema],
123 _max_output_tokens: Option<u32>,
124 _model: &str,
125 ) -> bamboo_llm::provider::Result<LLMStream> {
126 panic!("LLM must not be invoked when the run is already cancelled");
127 }
128 }
129
130 struct PanicExecutor;
131 #[async_trait]
132 impl ToolExecutor for PanicExecutor {
133 async fn execute(&self, _call: &ToolCall) -> Result<ToolResult, ToolError> {
134 panic!("tools must not run when the run is already cancelled");
135 }
136 async fn execute_with_context(
137 &self,
138 call: &ToolCall,
139 _ctx: ToolExecutionContext<'_>,
140 ) -> Result<ToolResult, ToolError> {
141 self.execute(call).await
142 }
143 fn list_tools(&self) -> Vec<ToolSchema> {
144 Vec::new()
145 }
146 }
147
148 #[tokio::test]
149 async fn execute_tool_calls_short_circuits_to_cancelled_when_token_already_fired() {
150 let mgr = DefaultToolManager::new(Arc::new(PanicExecutor), Arc::new(PanicProvider));
151 let (event_tx, _rx) = mpsc::channel(8);
152 let mut session = Session::new("s1", "model");
153 let config = AgentLoopConfig::default();
154 let mut task_context = None;
155 let tool_calls = vec![ToolCall {
158 id: "c1".to_string(),
159 tool_type: "function".to_string(),
160 function: FunctionCall {
161 name: "anything".to_string(),
162 arguments: "{}".to_string(),
163 },
164 }];
165
166 let cancel = CancellationToken::new();
167 cancel.cancel(); let result = mgr
170 .execute_tool_calls(
171 &tool_calls,
172 &event_tx,
173 None,
174 "s1",
175 "r1",
176 0,
177 &mut session,
178 &config,
179 &mut task_context,
180 &[],
181 &cancel,
182 )
183 .await;
184
185 assert!(
186 matches!(result, Err(AgentError::Cancelled)),
187 "a pre-cancelled token returns Cancelled before touching tools/LLM; got {result:?}"
188 );
189 }
190}