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_filter_resolver:
287 Option<Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>>,
288 pub disabled_tools: Option<BTreeSet<String>>,
290 pub disabled_skill_ids: Option<BTreeSet<String>>,
292 pub selected_skill_ids: Option<Vec<String>>,
293 pub selected_skill_mode: Option<String>,
294 pub image_fallback: Option<ImageFallbackConfig>,
295 pub gold_config: Option<GoldConfig>,
296 pub guardian_config: Option<GuardianConfig>,
298 pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
301 pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
304 pub app_data_dir: Option<std::path::PathBuf>,
306}
307
308pub struct ExecuteRequestBuilder {
324 initial_message: String,
325 event_tx: mpsc::Sender<AgentEvent>,
326 cancel_token: CancellationToken,
327
328 tools: Option<Arc<dyn ToolExecutor>>,
329 provider_override: Option<Arc<dyn LLMProvider>>,
330 model: Option<String>,
334 provider_name: Option<String>,
335 provider_type: Option<String>,
336 fast_model: Option<String>,
337 fast_model_provider: Option<Arc<dyn LLMProvider>>,
338 background_model: Option<String>,
339 background_model_provider: Option<Arc<dyn LLMProvider>>,
340 summarization_model: Option<String>,
341 summarization_model_provider: Option<Arc<dyn LLMProvider>>,
342 reasoning_effort: Option<ReasoningEffort>,
343 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
344 disabled_filter_resolver:
345 Option<Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>>,
346 disabled_tools: Option<BTreeSet<String>>,
347 disabled_skill_ids: Option<BTreeSet<String>>,
348 selected_skill_ids: Option<Vec<String>>,
349 selected_skill_mode: Option<String>,
350 image_fallback: Option<ImageFallbackConfig>,
351 gold_config: Option<GoldConfig>,
352 guardian_config: Option<GuardianConfig>,
353 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
354 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
355 app_data_dir: Option<std::path::PathBuf>,
356}
357
358impl ExecuteRequestBuilder {
359 pub fn new(
362 initial_message: impl Into<String>,
363 event_tx: mpsc::Sender<AgentEvent>,
364 cancel_token: CancellationToken,
365 ) -> Self {
366 Self {
367 initial_message: initial_message.into(),
368 event_tx,
369 cancel_token,
370 tools: None,
371 provider_override: None,
372 model: None,
373 provider_name: None,
374 provider_type: None,
375 fast_model: None,
376 fast_model_provider: None,
377 background_model: None,
378 background_model_provider: None,
379 summarization_model: None,
380 summarization_model_provider: None,
381 reasoning_effort: None,
382 auxiliary_model_resolver: None,
383 disabled_filter_resolver: None,
384 disabled_tools: None,
385 disabled_skill_ids: None,
386 selected_skill_ids: None,
387 selected_skill_mode: None,
388 image_fallback: None,
389 gold_config: None,
390 guardian_config: None,
391 guardian_spawner: None,
392 bash_resume_hook: None,
393 app_data_dir: None,
394 }
395 }
396
397 pub fn tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
399 self.tools = Some(v);
400 self
401 }
402
403 pub fn provider_override(mut self, v: Arc<dyn LLMProvider>) -> Self {
405 self.provider_override = Some(v);
406 self
407 }
408
409 pub fn model_roster(mut self, roster: ModelRoster) -> Self {
415 self.fast_model = roster.fast_model();
416 self.fast_model_provider = roster.fast_model_provider();
417 self.background_model = roster.background_model();
418 self.background_model_provider = roster.background_model_provider();
419 self.summarization_model = roster.summarization_model();
420 self.summarization_model_provider = roster.summarization_model_provider();
421 self.model = roster.model;
422 self.provider_name = roster.provider_name;
423 self.provider_type = roster.provider_type;
424 self
425 }
426
427 pub fn model(mut self, v: impl Into<String>) -> Self {
429 self.model = Some(v.into());
430 self
431 }
432
433 pub fn provider_name(mut self, v: impl Into<String>) -> Self {
435 self.provider_name = Some(v.into());
436 self
437 }
438
439 pub fn provider_type(mut self, v: impl Into<String>) -> Self {
441 self.provider_type = Some(v.into());
442 self
443 }
444
445 pub fn fast_model(mut self, v: impl Into<String>) -> Self {
447 self.fast_model = Some(v.into());
448 self
449 }
450
451 pub fn fast_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
453 self.fast_model_provider = Some(v);
454 self
455 }
456
457 pub fn background_model(mut self, v: impl Into<String>) -> Self {
459 self.background_model = Some(v.into());
460 self
461 }
462
463 pub fn background_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
465 self.background_model_provider = Some(v);
466 self
467 }
468
469 pub fn summarization_model(mut self, v: impl Into<String>) -> Self {
471 self.summarization_model = Some(v.into());
472 self
473 }
474
475 pub fn summarization_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
477 self.summarization_model_provider = Some(v);
478 self
479 }
480
481 pub fn reasoning_effort(mut self, v: ReasoningEffort) -> Self {
483 self.reasoning_effort = Some(v);
484 self
485 }
486
487 pub fn auxiliary_model_resolver(
489 mut self,
490 v: Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>,
491 ) -> Self {
492 self.auxiliary_model_resolver = Some(v);
493 self
494 }
495
496 pub fn disabled_filter_resolver(
498 mut self,
499 v: Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>,
500 ) -> Self {
501 self.disabled_filter_resolver = Some(v);
502 self
503 }
504
505 pub fn disabled_tools(mut self, v: BTreeSet<String>) -> Self {
507 self.disabled_tools = Some(v);
508 self
509 }
510
511 pub fn disabled_skill_ids(mut self, v: BTreeSet<String>) -> Self {
513 self.disabled_skill_ids = Some(v);
514 self
515 }
516
517 pub fn selected_skill_ids(mut self, v: Vec<String>) -> Self {
519 self.selected_skill_ids = Some(v);
520 self
521 }
522
523 pub fn selected_skill_mode(mut self, v: impl Into<String>) -> Self {
525 self.selected_skill_mode = Some(v.into());
526 self
527 }
528
529 pub fn image_fallback(mut self, v: ImageFallbackConfig) -> Self {
531 self.image_fallback = Some(v);
532 self
533 }
534
535 pub(crate) fn gold_config(mut self, v: Option<GoldConfig>) -> Self {
542 self.gold_config = v;
543 self
544 }
545
546 pub(crate) fn guardian_config(mut self, v: Option<GuardianConfig>) -> Self {
549 self.guardian_config = v;
550 self
551 }
552
553 pub(crate) fn guardian_spawner(mut self, v: Option<Arc<dyn GuardianSpawner>>) -> Self {
556 self.guardian_spawner = v;
557 self
558 }
559
560 pub(crate) fn bash_resume_hook(mut self, v: Option<Arc<dyn BashResumeHook>>) -> Self {
563 self.bash_resume_hook = v;
564 self
565 }
566
567 pub fn app_data_dir(mut self, v: std::path::PathBuf) -> Self {
569 self.app_data_dir = Some(v);
570 self
571 }
572
573 pub fn build(self) -> ExecuteRequest {
578 let model_roster = ModelRoster {
579 model: self.model,
580 provider_name: self.provider_name,
581 provider_type: self.provider_type,
582 fast: RoleModel::from_parts(self.fast_model, self.fast_model_provider),
583 background: RoleModel::from_parts(
584 self.background_model,
585 self.background_model_provider,
586 ),
587 summarization: RoleModel::from_parts(
588 self.summarization_model,
589 self.summarization_model_provider,
590 ),
591 };
592 ExecuteRequest {
593 initial_message: self.initial_message,
594 event_tx: self.event_tx,
595 cancel_token: self.cancel_token,
596 tools: self.tools,
597 provider_override: self.provider_override,
598 model_roster,
599 reasoning_effort: self.reasoning_effort,
600 auxiliary_model_resolver: self.auxiliary_model_resolver,
601 disabled_filter_resolver: self.disabled_filter_resolver,
602 disabled_tools: self.disabled_tools,
603 disabled_skill_ids: self.disabled_skill_ids,
604 selected_skill_ids: self.selected_skill_ids,
605 selected_skill_mode: self.selected_skill_mode,
606 image_fallback: self.image_fallback,
607 gold_config: self.gold_config,
608 guardian_config: self.guardian_config,
609 guardian_spawner: self.guardian_spawner,
610 bash_resume_hook: self.bash_resume_hook,
611 app_data_dir: self.app_data_dir,
612 }
613 }
614}
615
616fn extract_system_prompt(session: &Session) -> Option<String> {
622 session
623 .messages
624 .iter()
625 .find(|m| matches!(m.role, Role::System))
626 .map(|m| m.content.clone())
627}
628
629impl AgentRuntime {
634 pub async fn execute(
639 &self,
640 session: &mut Session,
641 req: ExecuteRequest,
642 ) -> crate::runtime::runner::Result<()> {
643 let system_prompt = extract_system_prompt(session);
644 let config = self.config.read().await;
645 let ExecuteRequest {
646 initial_message,
647 event_tx,
648 cancel_token,
649 tools,
650 provider_override,
651 model_roster,
652 reasoning_effort,
653 auxiliary_model_resolver,
654 disabled_filter_resolver,
655 disabled_tools,
656 disabled_skill_ids,
657 selected_skill_ids,
658 selected_skill_mode,
659 image_fallback,
660 gold_config,
661 guardian_config,
662 guardian_spawner,
663 bash_resume_hook,
664 app_data_dir,
665 } = req;
666 let tools = tools.unwrap_or_else(|| self.default_tools.clone());
667 let llm = provider_override.unwrap_or_else(|| self.provider.clone());
668
669 let fast_model = model_roster.fast_model();
674 let fast_model_provider = model_roster.fast_model_provider();
675 let background_model = model_roster.background_model();
676 let background_model_provider = model_roster.background_model_provider();
677 let summarization_model = model_roster.summarization_model();
678 let summarization_model_provider = model_roster.summarization_model_provider();
679 let ModelRoster {
680 model,
681 provider_name,
682 provider_type,
683 ..
684 } = model_roster;
685
686 let loop_config = AgentLoopConfig {
687 max_rounds: 200,
688 system_prompt,
689 legacy_model_limits: config.extra.get("model_limits").cloned(),
692 disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
693 selected_skill_ids,
694 selected_skill_mode,
695 skill_manager: Some(self.skill_manager.clone()),
696 skip_initial_user_message: true,
697 storage: Some(self.storage.clone()),
698 persistence: Some(self.persistence.clone()),
699 attachment_reader: Some(self.attachment_reader.clone()),
700 metrics_collector: Some(self.metrics_collector.clone()),
701 model_name: model,
702 fast_model_name: fast_model.or_else(|| config.get_fast_model()),
703 fast_model_provider,
704 background_model_name: background_model
705 .or_else(|| config.get_memory_background_model()),
706 planning_model_name: config
707 .defaults
708 .as_ref()
709 .and_then(|d| d.planning.as_ref())
710 .map(|r| r.model.clone()),
711 search_model_name: config
712 .defaults
713 .as_ref()
714 .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
715 .map(|r| r.model.clone()),
716 compression_instructions: None,
717 summarization_model_name: summarization_model
718 .or_else(|| config.get_task_summary_model()),
719 background_model_provider,
720 summarization_model_provider,
721 provider_name: Some(provider_name.unwrap_or_else(|| config.provider.clone())),
722 provider_type,
723 reasoning_effort,
724 auxiliary_model_resolver,
725 disabled_filter_resolver,
726 disabled_tools: {
727 let mut merged = config.disabled_tool_names();
728 if let Some(dt) = disabled_tools {
729 merged.extend(dt);
730 }
731 merged
732 },
733 image_fallback,
734 app_data_dir,
735 prompt_memory_flags: config
736 .memory
737 .as_ref()
738 .map(PromptMemoryFlags::from)
739 .unwrap_or_default(),
740 features_dynamic_model_routing: config.features.dynamic_model_routing,
741 permission_mode: session
742 .agent_runtime_state
743 .as_ref()
744 .and_then(|state| state.plan_mode.as_ref())
745 .map(|_| PermissionMode::Plan),
746 gold_config,
747 guardian_config,
748 guardian_spawner,
749 bash_resume_hook,
750 mcp_tool_guidance: tools.tool_guidance(),
754 ..Default::default()
755 };
756
757 drop(config);
758
759 run_agent_loop_with_config(
760 session,
761 initial_message,
762 event_tx,
763 llm,
764 tools,
765 cancel_token,
766 loop_config,
767 )
768 .await
769 }
770}