1use std::collections::BTreeSet;
10use std::sync::Arc;
11
12use tokio::sync::{mpsc, RwLock};
13use tokio_util::sync::CancellationToken;
14
15use crate::metrics::MetricsCollector;
16use crate::skills::SkillManager;
17use bamboo_agent_core::storage::{AttachmentReader, Storage};
18use bamboo_agent_core::tools::ToolExecutor;
19use bamboo_agent_core::{AgentEvent, Role, Session};
20use bamboo_domain::ReasoningEffort;
21use bamboo_infrastructure::config::PermissionMode;
22use bamboo_infrastructure::Config;
23use bamboo_infrastructure::LLMProvider;
24
25use crate::runtime::config::{AgentLoopConfig, ImageFallbackConfig, PromptMemoryFlags};
26use crate::runtime::hooks::HookRunner;
27use crate::runtime::managers::{
28 LifecycleManager, LlmManager, MemoryManager, PromptManager, ToolManager,
29};
30use crate::runtime::runner::run_agent_loop_with_config;
31
32#[derive(Clone)]
41pub struct AgentRuntime {
42 pub storage: Arc<dyn Storage>,
43 pub attachment_reader: Arc<dyn AttachmentReader>,
44 pub skill_manager: Arc<SkillManager>,
45 pub metrics_collector: MetricsCollector,
46 pub config: Arc<RwLock<Config>>,
47
48 pub provider: Arc<dyn LLMProvider>,
50
51 pub default_tools: Arc<dyn ToolExecutor>,
55
56 pub prompt_manager: Option<Arc<dyn PromptManager>>,
59 pub memory_manager: Option<Arc<dyn MemoryManager>>,
61 pub tool_manager: Option<Arc<dyn ToolManager>>,
63 pub llm_manager: Option<Arc<dyn LlmManager>>,
65 pub lifecycle_manager: Option<Arc<dyn LifecycleManager>>,
67 pub hook_runner: Option<HookRunner>,
69}
70
71pub struct AgentRuntimeBuilder {
86 storage: Option<Arc<dyn Storage>>,
87 attachment_reader: Option<Arc<dyn AttachmentReader>>,
88 skill_manager: Option<Arc<SkillManager>>,
89 metrics_collector: Option<MetricsCollector>,
90 config: Option<Arc<RwLock<Config>>>,
91 provider: Option<Arc<dyn LLMProvider>>,
92 default_tools: Option<Arc<dyn ToolExecutor>>,
93 prompt_manager: Option<Arc<dyn PromptManager>>,
94 memory_manager: Option<Arc<dyn MemoryManager>>,
95 tool_manager: Option<Arc<dyn ToolManager>>,
96 llm_manager: Option<Arc<dyn LlmManager>>,
97 lifecycle_manager: Option<Arc<dyn LifecycleManager>>,
98 hook_runner: Option<HookRunner>,
99}
100
101impl AgentRuntimeBuilder {
102 pub fn new() -> Self {
103 Self {
104 storage: None,
105 attachment_reader: None,
106 skill_manager: None,
107 metrics_collector: None,
108 config: None,
109 provider: None,
110 default_tools: None,
111 prompt_manager: None,
112 memory_manager: None,
113 tool_manager: None,
114 llm_manager: None,
115 lifecycle_manager: None,
116 hook_runner: None,
117 }
118 }
119
120 pub fn storage(mut self, v: Arc<dyn Storage>) -> Self {
121 self.storage = Some(v);
122 self
123 }
124
125 pub fn attachment_reader(mut self, v: Arc<dyn AttachmentReader>) -> Self {
126 self.attachment_reader = Some(v);
127 self
128 }
129
130 pub fn skill_manager(mut self, v: Arc<SkillManager>) -> Self {
131 self.skill_manager = Some(v);
132 self
133 }
134
135 pub fn metrics_collector(mut self, v: MetricsCollector) -> Self {
136 self.metrics_collector = Some(v);
137 self
138 }
139
140 pub fn config(mut self, v: Arc<RwLock<Config>>) -> Self {
141 self.config = Some(v);
142 self
143 }
144
145 pub fn provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
146 self.provider = Some(v);
147 self
148 }
149
150 pub fn default_tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
151 self.default_tools = Some(v);
152 self
153 }
154
155 pub fn prompt_manager(mut self, v: Arc<dyn PromptManager>) -> Self {
156 self.prompt_manager = Some(v);
157 self
158 }
159
160 pub fn memory_manager(mut self, v: Arc<dyn MemoryManager>) -> Self {
161 self.memory_manager = Some(v);
162 self
163 }
164
165 pub fn tool_manager(mut self, v: Arc<dyn ToolManager>) -> Self {
166 self.tool_manager = Some(v);
167 self
168 }
169
170 pub fn llm_manager(mut self, v: Arc<dyn LlmManager>) -> Self {
171 self.llm_manager = Some(v);
172 self
173 }
174
175 pub fn lifecycle_manager(mut self, v: Arc<dyn LifecycleManager>) -> Self {
176 self.lifecycle_manager = Some(v);
177 self
178 }
179
180 pub fn hook_runner(mut self, v: HookRunner) -> Self {
181 self.hook_runner = Some(v);
182 self
183 }
184
185 pub fn build(self) -> Result<AgentRuntime, &'static str> {
186 Ok(AgentRuntime {
187 storage: self.storage.ok_or_else(|| format_missing("storage"))?,
188 attachment_reader: self
189 .attachment_reader
190 .ok_or_else(|| format_missing("attachment_reader"))?,
191 skill_manager: self
192 .skill_manager
193 .ok_or_else(|| format_missing("skill_manager"))?,
194 metrics_collector: self
195 .metrics_collector
196 .ok_or_else(|| format_missing("metrics_collector"))?,
197 config: self.config.ok_or_else(|| format_missing("config"))?,
198 provider: self.provider.ok_or_else(|| format_missing("provider"))?,
199 default_tools: self
200 .default_tools
201 .ok_or_else(|| format_missing("default_tools"))?,
202 prompt_manager: None,
203 memory_manager: None,
204 tool_manager: None,
205 llm_manager: None,
206 lifecycle_manager: None,
207 hook_runner: None,
208 })
209 }
210}
211
212fn format_missing(field: &str) -> &'static str {
213 match field {
216 "storage" => "AgentRuntimeBuilder: missing storage",
217 "attachment_reader" => "AgentRuntimeBuilder: missing attachment_reader",
218 "skill_manager" => "AgentRuntimeBuilder: missing skill_manager",
219 "metrics_collector" => "AgentRuntimeBuilder: missing metrics_collector",
220 "config" => "AgentRuntimeBuilder: missing config",
221 "provider" => "AgentRuntimeBuilder: missing provider",
222 "default_tools" => "AgentRuntimeBuilder: missing default_tools",
223 _ => "AgentRuntimeBuilder: missing required field",
224 }
225}
226
227impl Default for AgentRuntimeBuilder {
228 fn default() -> Self {
229 Self::new()
230 }
231}
232
233pub struct ExecuteRequest {
243 pub initial_message: String,
245 pub event_tx: mpsc::Sender<AgentEvent>,
246 pub cancel_token: CancellationToken,
247
248 pub tools: Option<Arc<dyn ToolExecutor>>,
252 pub provider_override: Option<Arc<dyn LLMProvider>>,
255
256 pub model: Option<String>,
258 pub provider_name: Option<String>,
259 pub background_model: Option<String>,
261 pub background_model_provider: Option<Arc<dyn LLMProvider>>,
264 pub reasoning_effort: Option<ReasoningEffort>,
265 pub disabled_tools: Option<BTreeSet<String>>,
267 pub disabled_skill_ids: Option<BTreeSet<String>>,
269 pub selected_skill_ids: Option<Vec<String>>,
270 pub selected_skill_mode: Option<String>,
271 pub image_fallback: Option<ImageFallbackConfig>,
272}
273
274fn extract_system_prompt(session: &Session) -> Option<String> {
280 session
281 .messages
282 .iter()
283 .find(|m| matches!(m.role, Role::System))
284 .map(|m| m.content.clone())
285}
286
287impl AgentRuntime {
292 pub async fn execute(
297 &self,
298 session: &mut Session,
299 req: ExecuteRequest,
300 ) -> crate::runtime::runner::Result<()> {
301 let system_prompt = extract_system_prompt(session);
302 let config = self.config.read().await;
303 let ExecuteRequest {
304 initial_message,
305 event_tx,
306 cancel_token,
307 tools,
308 provider_override,
309 model,
310 provider_name,
311 background_model,
312 background_model_provider,
313 reasoning_effort,
314 disabled_tools,
315 disabled_skill_ids,
316 selected_skill_ids,
317 selected_skill_mode,
318 image_fallback,
319 } = req;
320 let tools = tools.unwrap_or_else(|| self.default_tools.clone());
321 let llm = provider_override.unwrap_or_else(|| self.provider.clone());
322
323 let loop_config = AgentLoopConfig {
324 max_rounds: 200,
325 system_prompt,
326 disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
327 selected_skill_ids,
328 selected_skill_mode,
329 skill_manager: Some(self.skill_manager.clone()),
330 skip_initial_user_message: true,
331 storage: Some(self.storage.clone()),
332 attachment_reader: Some(self.attachment_reader.clone()),
333 metrics_collector: Some(self.metrics_collector.clone()),
334 model_name: model,
335 fast_model_name: config.get_fast_model(),
336 background_model_name: background_model
337 .or_else(|| config.get_memory_background_model()),
338 background_model_provider,
339 planning_model_name: config
340 .defaults
341 .as_ref()
342 .and_then(|d| d.planning.as_ref())
343 .map(|r| r.model.clone()),
344 search_model_name: config
345 .defaults
346 .as_ref()
347 .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
348 .map(|r| r.model.clone()),
349 provider_name: Some(provider_name.unwrap_or_else(|| config.provider.clone())),
350 reasoning_effort,
351 disabled_tools: disabled_tools.unwrap_or_else(|| config.disabled_tool_names()),
352 image_fallback,
353 prompt_memory_flags: config
354 .memory
355 .as_ref()
356 .map(PromptMemoryFlags::from)
357 .unwrap_or_default(),
358 features_dynamic_model_routing: config.features.dynamic_model_routing,
359 permission_mode: session
360 .agent_runtime_state
361 .as_ref()
362 .and_then(|state| state.plan_mode.as_ref())
363 .map(|_| PermissionMode::Plan),
364 ..Default::default()
365 };
366
367 drop(config);
368
369 run_agent_loop_with_config(
370 session,
371 initial_message,
372 event_tx,
373 llm,
374 tools,
375 cancel_token,
376 loop_config,
377 )
378 .await
379 }
380}