1use std::collections::BTreeSet;
10use std::sync::Arc;
11
12use tokio::sync::{mpsc, RwLock};
13use tokio_util::sync::CancellationToken;
14
15use bamboo_agent_core::storage::{AttachmentReader, Storage};
16use bamboo_agent_core::tools::ToolExecutor;
17use bamboo_agent_core::{AgentEvent, Role, Session};
18use bamboo_config::PermissionMode;
19use bamboo_domain::ReasoningEffort;
20use bamboo_llm::Config;
21use bamboo_llm::LLMProvider;
22use bamboo_metrics::MetricsCollector;
23use bamboo_skills::SkillManager;
24
25use crate::runtime::config::{
26 AgentLoopConfig, AuxiliaryModelConfig, BashResumeHook, GoldConfig, GuardianConfig,
27 GuardianSpawner, ImageFallbackConfig, PromptMemoryFlags,
28};
29use crate::runtime::hooks::HookRunner;
30use crate::runtime::managers::{
31 LifecycleManager, LlmManager, MemoryManager, PromptManager, ToolManager,
32};
33use crate::runtime::model_roster::{ModelRoster, RoleModel};
34use crate::runtime::runner::run_agent_loop_with_config;
35use bamboo_domain::RuntimeSessionPersistence;
36
37#[derive(Clone)]
46pub struct AgentRuntime {
47 pub storage: Arc<dyn Storage>,
48 pub persistence: Arc<dyn RuntimeSessionPersistence>,
49 pub attachment_reader: Arc<dyn AttachmentReader>,
50 pub skill_manager: Arc<SkillManager>,
51 pub metrics_collector: MetricsCollector,
52 pub config: Arc<RwLock<Config>>,
53
54 pub provider: Arc<dyn LLMProvider>,
56
57 pub default_tools: Arc<dyn ToolExecutor>,
61
62 pub prompt_manager: Option<Arc<dyn PromptManager>>,
65 pub memory_manager: Option<Arc<dyn MemoryManager>>,
67 pub tool_manager: Option<Arc<dyn ToolManager>>,
69 pub llm_manager: Option<Arc<dyn LlmManager>>,
71 pub lifecycle_manager: Option<Arc<dyn LifecycleManager>>,
73 pub hook_runner: Option<HookRunner>,
75}
76
77pub struct AgentRuntimeBuilder {
92 storage: Option<Arc<dyn Storage>>,
93 persistence: Option<Arc<dyn RuntimeSessionPersistence>>,
94 attachment_reader: Option<Arc<dyn AttachmentReader>>,
95 skill_manager: Option<Arc<SkillManager>>,
96 metrics_collector: Option<MetricsCollector>,
97 config: Option<Arc<RwLock<Config>>>,
98 provider: Option<Arc<dyn LLMProvider>>,
99 default_tools: Option<Arc<dyn ToolExecutor>>,
100 prompt_manager: Option<Arc<dyn PromptManager>>,
101 memory_manager: Option<Arc<dyn MemoryManager>>,
102 tool_manager: Option<Arc<dyn ToolManager>>,
103 llm_manager: Option<Arc<dyn LlmManager>>,
104 lifecycle_manager: Option<Arc<dyn LifecycleManager>>,
105 hook_runner: Option<HookRunner>,
106}
107
108impl AgentRuntimeBuilder {
109 pub fn new() -> Self {
110 Self {
111 storage: None,
112 persistence: None,
113 attachment_reader: None,
114 skill_manager: None,
115 metrics_collector: None,
116 config: None,
117 provider: None,
118 default_tools: None,
119 prompt_manager: None,
120 memory_manager: None,
121 tool_manager: None,
122 llm_manager: None,
123 lifecycle_manager: None,
124 hook_runner: None,
125 }
126 }
127
128 pub fn storage(mut self, v: Arc<dyn Storage>) -> Self {
129 self.storage = Some(v);
130 self
131 }
132
133 pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
134 self.persistence = Some(v);
135 self
136 }
137
138 pub fn attachment_reader(mut self, v: Arc<dyn AttachmentReader>) -> Self {
139 self.attachment_reader = Some(v);
140 self
141 }
142
143 pub fn skill_manager(mut self, v: Arc<SkillManager>) -> Self {
144 self.skill_manager = Some(v);
145 self
146 }
147
148 pub fn metrics_collector(mut self, v: MetricsCollector) -> Self {
149 self.metrics_collector = Some(v);
150 self
151 }
152
153 pub fn config(mut self, v: Arc<RwLock<Config>>) -> Self {
154 self.config = Some(v);
155 self
156 }
157
158 pub fn provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
159 self.provider = Some(v);
160 self
161 }
162
163 pub fn default_tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
164 self.default_tools = Some(v);
165 self
166 }
167
168 pub fn prompt_manager(mut self, v: Arc<dyn PromptManager>) -> Self {
169 self.prompt_manager = Some(v);
170 self
171 }
172
173 pub fn memory_manager(mut self, v: Arc<dyn MemoryManager>) -> Self {
174 self.memory_manager = Some(v);
175 self
176 }
177
178 pub fn tool_manager(mut self, v: Arc<dyn ToolManager>) -> Self {
179 self.tool_manager = Some(v);
180 self
181 }
182
183 pub fn llm_manager(mut self, v: Arc<dyn LlmManager>) -> Self {
184 self.llm_manager = Some(v);
185 self
186 }
187
188 pub fn lifecycle_manager(mut self, v: Arc<dyn LifecycleManager>) -> Self {
189 self.lifecycle_manager = Some(v);
190 self
191 }
192
193 pub fn hook_runner(mut self, v: HookRunner) -> Self {
194 self.hook_runner = Some(v);
195 self
196 }
197
198 pub fn build(self) -> Result<AgentRuntime, &'static str> {
199 Ok(AgentRuntime {
200 storage: self.storage.ok_or_else(|| format_missing("storage"))?,
201 persistence: self
202 .persistence
203 .ok_or_else(|| format_missing("persistence"))?,
204 attachment_reader: self
205 .attachment_reader
206 .ok_or_else(|| format_missing("attachment_reader"))?,
207 skill_manager: self
208 .skill_manager
209 .ok_or_else(|| format_missing("skill_manager"))?,
210 metrics_collector: self
211 .metrics_collector
212 .ok_or_else(|| format_missing("metrics_collector"))?,
213 config: self.config.ok_or_else(|| format_missing("config"))?,
214 provider: self.provider.ok_or_else(|| format_missing("provider"))?,
215 default_tools: self
216 .default_tools
217 .ok_or_else(|| format_missing("default_tools"))?,
218 prompt_manager: None,
219 memory_manager: None,
220 tool_manager: None,
221 llm_manager: None,
222 lifecycle_manager: None,
223 hook_runner: None,
224 })
225 }
226}
227
228fn format_missing(field: &str) -> &'static str {
229 match field {
232 "storage" => "AgentRuntimeBuilder: missing storage",
233 "persistence" => "AgentRuntimeBuilder: missing persistence",
234 "attachment_reader" => "AgentRuntimeBuilder: missing attachment_reader",
235 "skill_manager" => "AgentRuntimeBuilder: missing skill_manager",
236 "metrics_collector" => "AgentRuntimeBuilder: missing metrics_collector",
237 "config" => "AgentRuntimeBuilder: missing config",
238 "provider" => "AgentRuntimeBuilder: missing provider",
239 "default_tools" => "AgentRuntimeBuilder: missing default_tools",
240 _ => "AgentRuntimeBuilder: missing required field",
241 }
242}
243
244impl Default for AgentRuntimeBuilder {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250pub struct ExecuteRequest {
260 pub initial_message: String,
262 pub event_tx: mpsc::Sender<AgentEvent>,
263 pub cancel_token: CancellationToken,
264
265 pub tools: Option<Arc<dyn ToolExecutor>>,
269 pub provider_override: Option<Arc<dyn LLMProvider>>,
272
273 pub model_roster: ModelRoster,
280 pub reasoning_effort: Option<ReasoningEffort>,
281 pub auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
284 pub disabled_tools: Option<BTreeSet<String>>,
286 pub disabled_skill_ids: Option<BTreeSet<String>>,
288 pub selected_skill_ids: Option<Vec<String>>,
289 pub selected_skill_mode: Option<String>,
290 pub image_fallback: Option<ImageFallbackConfig>,
291 pub gold_config: Option<GoldConfig>,
292 pub guardian_config: Option<GuardianConfig>,
294 pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
297 pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
300 pub app_data_dir: Option<std::path::PathBuf>,
302}
303
304pub struct ExecuteRequestBuilder {
320 initial_message: String,
321 event_tx: mpsc::Sender<AgentEvent>,
322 cancel_token: CancellationToken,
323
324 tools: Option<Arc<dyn ToolExecutor>>,
325 provider_override: Option<Arc<dyn LLMProvider>>,
326 model: Option<String>,
330 provider_name: Option<String>,
331 provider_type: Option<String>,
332 fast_model: Option<String>,
333 fast_model_provider: Option<Arc<dyn LLMProvider>>,
334 background_model: Option<String>,
335 background_model_provider: Option<Arc<dyn LLMProvider>>,
336 summarization_model: Option<String>,
337 summarization_model_provider: Option<Arc<dyn LLMProvider>>,
338 reasoning_effort: Option<ReasoningEffort>,
339 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
340 disabled_tools: Option<BTreeSet<String>>,
341 disabled_skill_ids: Option<BTreeSet<String>>,
342 selected_skill_ids: Option<Vec<String>>,
343 selected_skill_mode: Option<String>,
344 image_fallback: Option<ImageFallbackConfig>,
345 gold_config: Option<GoldConfig>,
346 guardian_config: Option<GuardianConfig>,
347 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
348 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
349 app_data_dir: Option<std::path::PathBuf>,
350}
351
352impl ExecuteRequestBuilder {
353 pub fn new(
356 initial_message: impl Into<String>,
357 event_tx: mpsc::Sender<AgentEvent>,
358 cancel_token: CancellationToken,
359 ) -> Self {
360 Self {
361 initial_message: initial_message.into(),
362 event_tx,
363 cancel_token,
364 tools: None,
365 provider_override: None,
366 model: None,
367 provider_name: None,
368 provider_type: None,
369 fast_model: None,
370 fast_model_provider: None,
371 background_model: None,
372 background_model_provider: None,
373 summarization_model: None,
374 summarization_model_provider: None,
375 reasoning_effort: None,
376 auxiliary_model_resolver: None,
377 disabled_tools: None,
378 disabled_skill_ids: None,
379 selected_skill_ids: None,
380 selected_skill_mode: None,
381 image_fallback: None,
382 gold_config: None,
383 guardian_config: None,
384 guardian_spawner: None,
385 bash_resume_hook: None,
386 app_data_dir: None,
387 }
388 }
389
390 pub fn tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
392 self.tools = Some(v);
393 self
394 }
395
396 pub fn provider_override(mut self, v: Arc<dyn LLMProvider>) -> Self {
398 self.provider_override = Some(v);
399 self
400 }
401
402 pub fn model_roster(mut self, roster: ModelRoster) -> Self {
408 self.fast_model = roster.fast_model();
409 self.fast_model_provider = roster.fast_model_provider();
410 self.background_model = roster.background_model();
411 self.background_model_provider = roster.background_model_provider();
412 self.summarization_model = roster.summarization_model();
413 self.summarization_model_provider = roster.summarization_model_provider();
414 self.model = roster.model;
415 self.provider_name = roster.provider_name;
416 self.provider_type = roster.provider_type;
417 self
418 }
419
420 pub fn model(mut self, v: impl Into<String>) -> Self {
422 self.model = Some(v.into());
423 self
424 }
425
426 pub fn provider_name(mut self, v: impl Into<String>) -> Self {
428 self.provider_name = Some(v.into());
429 self
430 }
431
432 pub fn provider_type(mut self, v: impl Into<String>) -> Self {
434 self.provider_type = Some(v.into());
435 self
436 }
437
438 pub fn fast_model(mut self, v: impl Into<String>) -> Self {
440 self.fast_model = Some(v.into());
441 self
442 }
443
444 pub fn fast_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
446 self.fast_model_provider = Some(v);
447 self
448 }
449
450 pub fn background_model(mut self, v: impl Into<String>) -> Self {
452 self.background_model = Some(v.into());
453 self
454 }
455
456 pub fn background_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
458 self.background_model_provider = Some(v);
459 self
460 }
461
462 pub fn summarization_model(mut self, v: impl Into<String>) -> Self {
464 self.summarization_model = Some(v.into());
465 self
466 }
467
468 pub fn summarization_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
470 self.summarization_model_provider = Some(v);
471 self
472 }
473
474 pub fn reasoning_effort(mut self, v: ReasoningEffort) -> Self {
476 self.reasoning_effort = Some(v);
477 self
478 }
479
480 pub fn auxiliary_model_resolver(
482 mut self,
483 v: Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>,
484 ) -> Self {
485 self.auxiliary_model_resolver = Some(v);
486 self
487 }
488
489 pub fn disabled_tools(mut self, v: BTreeSet<String>) -> Self {
491 self.disabled_tools = Some(v);
492 self
493 }
494
495 pub fn disabled_skill_ids(mut self, v: BTreeSet<String>) -> Self {
497 self.disabled_skill_ids = Some(v);
498 self
499 }
500
501 pub fn selected_skill_ids(mut self, v: Vec<String>) -> Self {
503 self.selected_skill_ids = Some(v);
504 self
505 }
506
507 pub fn selected_skill_mode(mut self, v: impl Into<String>) -> Self {
509 self.selected_skill_mode = Some(v.into());
510 self
511 }
512
513 pub fn image_fallback(mut self, v: ImageFallbackConfig) -> Self {
515 self.image_fallback = Some(v);
516 self
517 }
518
519 pub(crate) fn gold_config(mut self, v: Option<GoldConfig>) -> Self {
526 self.gold_config = v;
527 self
528 }
529
530 pub(crate) fn guardian_config(mut self, v: Option<GuardianConfig>) -> Self {
533 self.guardian_config = v;
534 self
535 }
536
537 pub(crate) fn guardian_spawner(mut self, v: Option<Arc<dyn GuardianSpawner>>) -> Self {
540 self.guardian_spawner = v;
541 self
542 }
543
544 pub(crate) fn bash_resume_hook(mut self, v: Option<Arc<dyn BashResumeHook>>) -> Self {
547 self.bash_resume_hook = v;
548 self
549 }
550
551 pub fn app_data_dir(mut self, v: std::path::PathBuf) -> Self {
553 self.app_data_dir = Some(v);
554 self
555 }
556
557 pub fn build(self) -> ExecuteRequest {
562 let model_roster = ModelRoster {
563 model: self.model,
564 provider_name: self.provider_name,
565 provider_type: self.provider_type,
566 fast: RoleModel::from_parts(self.fast_model, self.fast_model_provider),
567 background: RoleModel::from_parts(
568 self.background_model,
569 self.background_model_provider,
570 ),
571 summarization: RoleModel::from_parts(
572 self.summarization_model,
573 self.summarization_model_provider,
574 ),
575 };
576 ExecuteRequest {
577 initial_message: self.initial_message,
578 event_tx: self.event_tx,
579 cancel_token: self.cancel_token,
580 tools: self.tools,
581 provider_override: self.provider_override,
582 model_roster,
583 reasoning_effort: self.reasoning_effort,
584 auxiliary_model_resolver: self.auxiliary_model_resolver,
585 disabled_tools: self.disabled_tools,
586 disabled_skill_ids: self.disabled_skill_ids,
587 selected_skill_ids: self.selected_skill_ids,
588 selected_skill_mode: self.selected_skill_mode,
589 image_fallback: self.image_fallback,
590 gold_config: self.gold_config,
591 guardian_config: self.guardian_config,
592 guardian_spawner: self.guardian_spawner,
593 bash_resume_hook: self.bash_resume_hook,
594 app_data_dir: self.app_data_dir,
595 }
596 }
597}
598
599fn extract_system_prompt(session: &Session) -> Option<String> {
605 session
606 .messages
607 .iter()
608 .find(|m| matches!(m.role, Role::System))
609 .map(|m| m.content.clone())
610}
611
612impl AgentRuntime {
617 pub async fn execute(
622 &self,
623 session: &mut Session,
624 req: ExecuteRequest,
625 ) -> crate::runtime::runner::Result<()> {
626 let system_prompt = extract_system_prompt(session);
627 let config = self.config.read().await;
628 let ExecuteRequest {
629 initial_message,
630 event_tx,
631 cancel_token,
632 tools,
633 provider_override,
634 model_roster,
635 reasoning_effort,
636 auxiliary_model_resolver,
637 disabled_tools,
638 disabled_skill_ids,
639 selected_skill_ids,
640 selected_skill_mode,
641 image_fallback,
642 gold_config,
643 guardian_config,
644 guardian_spawner,
645 bash_resume_hook,
646 app_data_dir,
647 } = req;
648 let tools = tools.unwrap_or_else(|| self.default_tools.clone());
649 let llm = provider_override.unwrap_or_else(|| self.provider.clone());
650
651 let fast_model = model_roster.fast_model();
656 let fast_model_provider = model_roster.fast_model_provider();
657 let background_model = model_roster.background_model();
658 let background_model_provider = model_roster.background_model_provider();
659 let summarization_model = model_roster.summarization_model();
660 let summarization_model_provider = model_roster.summarization_model_provider();
661 let ModelRoster {
662 model,
663 provider_name,
664 provider_type,
665 ..
666 } = model_roster;
667
668 let loop_config = AgentLoopConfig {
669 max_rounds: 200,
670 system_prompt,
671 disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
672 selected_skill_ids,
673 selected_skill_mode,
674 skill_manager: Some(self.skill_manager.clone()),
675 skip_initial_user_message: true,
676 storage: Some(self.storage.clone()),
677 persistence: Some(self.persistence.clone()),
678 attachment_reader: Some(self.attachment_reader.clone()),
679 metrics_collector: Some(self.metrics_collector.clone()),
680 model_name: model,
681 fast_model_name: fast_model.or_else(|| config.get_fast_model()),
682 fast_model_provider,
683 background_model_name: background_model
684 .or_else(|| config.get_memory_background_model()),
685 planning_model_name: config
686 .defaults
687 .as_ref()
688 .and_then(|d| d.planning.as_ref())
689 .map(|r| r.model.clone()),
690 search_model_name: config
691 .defaults
692 .as_ref()
693 .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
694 .map(|r| r.model.clone()),
695 compression_instructions: None,
696 summarization_model_name: summarization_model
697 .or_else(|| config.get_task_summary_model()),
698 background_model_provider,
699 summarization_model_provider,
700 provider_name: Some(provider_name.unwrap_or_else(|| config.provider.clone())),
701 provider_type,
702 reasoning_effort,
703 auxiliary_model_resolver,
704 disabled_tools: {
705 let mut merged = config.disabled_tool_names();
706 if let Some(dt) = disabled_tools {
707 merged.extend(dt);
708 }
709 merged
710 },
711 image_fallback,
712 app_data_dir,
713 prompt_memory_flags: config
714 .memory
715 .as_ref()
716 .map(PromptMemoryFlags::from)
717 .unwrap_or_default(),
718 features_dynamic_model_routing: config.features.dynamic_model_routing,
719 permission_mode: session
720 .agent_runtime_state
721 .as_ref()
722 .and_then(|state| state.plan_mode.as_ref())
723 .map(|_| PermissionMode::Plan),
724 gold_config,
725 guardian_config,
726 guardian_spawner,
727 bash_resume_hook,
728 mcp_tool_guidance: tools.tool_guidance(),
732 ..Default::default()
733 };
734
735 drop(config);
736
737 run_agent_loop_with_config(
738 session,
739 initial_message,
740 event_tx,
741 llm,
742 tools,
743 cancel_token,
744 loop_config,
745 )
746 .await
747 }
748}