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, BashCompletionSink, BashResumeHook, GoldConfig,
27 GuardianConfig, GuardianSpawner, ImageFallbackConfig, PromptMemoryFlags,
28};
29use crate::runtime::model_roster::{ModelRoster, RoleModel};
30use crate::runtime::runner::run_agent_loop_with_config;
31use bamboo_domain::RuntimeSessionPersistence;
32
33#[derive(Clone)]
42pub struct AgentRuntime {
43 pub storage: Arc<dyn Storage>,
44 pub persistence: Arc<dyn RuntimeSessionPersistence>,
45 pub attachment_reader: Arc<dyn AttachmentReader>,
46 pub skill_manager: Arc<SkillManager>,
47 pub metrics_collector: MetricsCollector,
48 pub config: Arc<RwLock<Config>>,
49
50 pub provider: Arc<dyn LLMProvider>,
52
53 pub default_tools: Arc<dyn ToolExecutor>,
57}
58
59pub struct AgentRuntimeBuilder {
74 storage: Option<Arc<dyn Storage>>,
75 persistence: Option<Arc<dyn RuntimeSessionPersistence>>,
76 attachment_reader: Option<Arc<dyn AttachmentReader>>,
77 skill_manager: Option<Arc<SkillManager>>,
78 metrics_collector: Option<MetricsCollector>,
79 config: Option<Arc<RwLock<Config>>>,
80 provider: Option<Arc<dyn LLMProvider>>,
81 default_tools: Option<Arc<dyn ToolExecutor>>,
82}
83
84impl AgentRuntimeBuilder {
85 pub fn new() -> Self {
86 Self {
87 storage: None,
88 persistence: None,
89 attachment_reader: None,
90 skill_manager: None,
91 metrics_collector: None,
92 config: None,
93 provider: None,
94 default_tools: None,
95 }
96 }
97
98 pub fn storage(mut self, v: Arc<dyn Storage>) -> Self {
99 self.storage = Some(v);
100 self
101 }
102
103 pub fn persistence(mut self, v: Arc<dyn RuntimeSessionPersistence>) -> Self {
104 self.persistence = Some(v);
105 self
106 }
107
108 pub fn attachment_reader(mut self, v: Arc<dyn AttachmentReader>) -> Self {
109 self.attachment_reader = Some(v);
110 self
111 }
112
113 pub fn skill_manager(mut self, v: Arc<SkillManager>) -> Self {
114 self.skill_manager = Some(v);
115 self
116 }
117
118 pub fn metrics_collector(mut self, v: MetricsCollector) -> Self {
119 self.metrics_collector = Some(v);
120 self
121 }
122
123 pub fn config(mut self, v: Arc<RwLock<Config>>) -> Self {
124 self.config = Some(v);
125 self
126 }
127
128 pub fn provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
129 self.provider = Some(v);
130 self
131 }
132
133 pub fn default_tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
134 self.default_tools = Some(v);
135 self
136 }
137
138 pub fn build(self) -> Result<AgentRuntime, &'static str> {
139 Ok(AgentRuntime {
140 storage: self.storage.ok_or_else(|| format_missing("storage"))?,
141 persistence: self
142 .persistence
143 .ok_or_else(|| format_missing("persistence"))?,
144 attachment_reader: self
145 .attachment_reader
146 .ok_or_else(|| format_missing("attachment_reader"))?,
147 skill_manager: self
148 .skill_manager
149 .ok_or_else(|| format_missing("skill_manager"))?,
150 metrics_collector: self
151 .metrics_collector
152 .ok_or_else(|| format_missing("metrics_collector"))?,
153 config: self.config.ok_or_else(|| format_missing("config"))?,
154 provider: self.provider.ok_or_else(|| format_missing("provider"))?,
155 default_tools: self
156 .default_tools
157 .ok_or_else(|| format_missing("default_tools"))?,
158 })
159 }
160}
161
162fn format_missing(field: &str) -> &'static str {
163 match field {
166 "storage" => "AgentRuntimeBuilder: missing storage",
167 "persistence" => "AgentRuntimeBuilder: missing persistence",
168 "attachment_reader" => "AgentRuntimeBuilder: missing attachment_reader",
169 "skill_manager" => "AgentRuntimeBuilder: missing skill_manager",
170 "metrics_collector" => "AgentRuntimeBuilder: missing metrics_collector",
171 "config" => "AgentRuntimeBuilder: missing config",
172 "provider" => "AgentRuntimeBuilder: missing provider",
173 "default_tools" => "AgentRuntimeBuilder: missing default_tools",
174 _ => "AgentRuntimeBuilder: missing required field",
175 }
176}
177
178impl Default for AgentRuntimeBuilder {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184pub struct ExecuteRequest {
194 pub initial_message: String,
196 pub event_tx: mpsc::Sender<AgentEvent>,
197 pub cancel_token: CancellationToken,
198
199 pub tools: Option<Arc<dyn ToolExecutor>>,
203 pub provider_override: Option<Arc<dyn LLMProvider>>,
206
207 pub model_roster: ModelRoster,
214 pub reasoning_effort: Option<ReasoningEffort>,
215 pub auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
218 pub disabled_filter_resolver:
221 Option<Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>>,
222 pub disabled_tools: Option<BTreeSet<String>>,
224 pub disabled_skill_ids: Option<BTreeSet<String>>,
226 pub selected_skill_ids: Option<Vec<String>>,
227 pub selected_skill_mode: Option<String>,
228 pub image_fallback: Option<ImageFallbackConfig>,
229 pub gold_config: Option<GoldConfig>,
230 pub guardian_config: Option<GuardianConfig>,
232 pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
235 pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
238 pub bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
241 pub app_data_dir: Option<std::path::PathBuf>,
243 pub run_budget: Option<bamboo_config::RunBudgetConfig>,
251}
252
253pub struct ExecuteRequestBuilder {
269 initial_message: String,
270 event_tx: mpsc::Sender<AgentEvent>,
271 cancel_token: CancellationToken,
272
273 tools: Option<Arc<dyn ToolExecutor>>,
274 provider_override: Option<Arc<dyn LLMProvider>>,
275 model: Option<String>,
279 provider_name: Option<String>,
280 provider_type: Option<String>,
281 fast_model: Option<String>,
282 fast_model_provider: Option<Arc<dyn LLMProvider>>,
283 background_model: Option<String>,
284 background_model_provider: Option<Arc<dyn LLMProvider>>,
285 summarization_model: Option<String>,
286 summarization_model_provider: Option<Arc<dyn LLMProvider>>,
287 reasoning_effort: Option<ReasoningEffort>,
288 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
289 disabled_filter_resolver:
290 Option<Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>>,
291 disabled_tools: Option<BTreeSet<String>>,
292 disabled_skill_ids: Option<BTreeSet<String>>,
293 selected_skill_ids: Option<Vec<String>>,
294 selected_skill_mode: Option<String>,
295 image_fallback: Option<ImageFallbackConfig>,
296 gold_config: Option<GoldConfig>,
297 guardian_config: Option<GuardianConfig>,
298 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
299 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
300 bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
301 app_data_dir: Option<std::path::PathBuf>,
302 run_budget: Option<bamboo_config::RunBudgetConfig>,
303}
304
305impl ExecuteRequestBuilder {
306 pub fn new(
309 initial_message: impl Into<String>,
310 event_tx: mpsc::Sender<AgentEvent>,
311 cancel_token: CancellationToken,
312 ) -> Self {
313 Self {
314 initial_message: initial_message.into(),
315 event_tx,
316 cancel_token,
317 tools: None,
318 provider_override: None,
319 model: None,
320 provider_name: None,
321 provider_type: None,
322 fast_model: None,
323 fast_model_provider: None,
324 background_model: None,
325 background_model_provider: None,
326 summarization_model: None,
327 summarization_model_provider: None,
328 reasoning_effort: None,
329 auxiliary_model_resolver: None,
330 disabled_filter_resolver: None,
331 disabled_tools: None,
332 disabled_skill_ids: None,
333 selected_skill_ids: None,
334 selected_skill_mode: None,
335 image_fallback: None,
336 gold_config: None,
337 guardian_config: None,
338 guardian_spawner: None,
339 bash_resume_hook: None,
340 bash_completion_sink: None,
341 app_data_dir: None,
342 run_budget: None,
343 }
344 }
345
346 pub fn tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
348 self.tools = Some(v);
349 self
350 }
351
352 pub fn provider_override(mut self, v: Arc<dyn LLMProvider>) -> Self {
354 self.provider_override = Some(v);
355 self
356 }
357
358 pub fn model_roster(mut self, roster: ModelRoster) -> Self {
364 self.fast_model = roster.fast_model();
365 self.fast_model_provider = roster.fast_model_provider();
366 self.background_model = roster.background_model();
367 self.background_model_provider = roster.background_model_provider();
368 self.summarization_model = roster.summarization_model();
369 self.summarization_model_provider = roster.summarization_model_provider();
370 self.model = roster.model;
371 self.provider_name = roster.provider_name;
372 self.provider_type = roster.provider_type;
373 self
374 }
375
376 pub fn model(mut self, v: impl Into<String>) -> Self {
378 self.model = Some(v.into());
379 self
380 }
381
382 pub fn provider_name(mut self, v: impl Into<String>) -> Self {
384 self.provider_name = Some(v.into());
385 self
386 }
387
388 pub fn provider_type(mut self, v: impl Into<String>) -> Self {
390 self.provider_type = Some(v.into());
391 self
392 }
393
394 pub fn fast_model(mut self, v: impl Into<String>) -> Self {
396 self.fast_model = Some(v.into());
397 self
398 }
399
400 pub fn fast_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
402 self.fast_model_provider = Some(v);
403 self
404 }
405
406 pub fn background_model(mut self, v: impl Into<String>) -> Self {
408 self.background_model = Some(v.into());
409 self
410 }
411
412 pub fn background_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
414 self.background_model_provider = Some(v);
415 self
416 }
417
418 pub fn summarization_model(mut self, v: impl Into<String>) -> Self {
420 self.summarization_model = Some(v.into());
421 self
422 }
423
424 pub fn summarization_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
426 self.summarization_model_provider = Some(v);
427 self
428 }
429
430 pub fn reasoning_effort(mut self, v: ReasoningEffort) -> Self {
432 self.reasoning_effort = Some(v);
433 self
434 }
435
436 pub fn auxiliary_model_resolver(
438 mut self,
439 v: Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>,
440 ) -> Self {
441 self.auxiliary_model_resolver = Some(v);
442 self
443 }
444
445 pub fn disabled_filter_resolver(
447 mut self,
448 v: Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>,
449 ) -> Self {
450 self.disabled_filter_resolver = Some(v);
451 self
452 }
453
454 pub fn disabled_tools(mut self, v: BTreeSet<String>) -> Self {
456 self.disabled_tools = Some(v);
457 self
458 }
459
460 pub fn disabled_skill_ids(mut self, v: BTreeSet<String>) -> Self {
462 self.disabled_skill_ids = Some(v);
463 self
464 }
465
466 pub fn selected_skill_ids(mut self, v: Vec<String>) -> Self {
468 self.selected_skill_ids = Some(v);
469 self
470 }
471
472 pub fn selected_skill_mode(mut self, v: impl Into<String>) -> Self {
474 self.selected_skill_mode = Some(v.into());
475 self
476 }
477
478 pub fn image_fallback(mut self, v: ImageFallbackConfig) -> Self {
480 self.image_fallback = Some(v);
481 self
482 }
483
484 pub(crate) fn gold_config(mut self, v: Option<GoldConfig>) -> Self {
491 self.gold_config = v;
492 self
493 }
494
495 pub(crate) fn guardian_config(mut self, v: Option<GuardianConfig>) -> Self {
498 self.guardian_config = v;
499 self
500 }
501
502 pub(crate) fn guardian_spawner(mut self, v: Option<Arc<dyn GuardianSpawner>>) -> Self {
505 self.guardian_spawner = v;
506 self
507 }
508
509 pub(crate) fn bash_resume_hook(mut self, v: Option<Arc<dyn BashResumeHook>>) -> Self {
512 self.bash_resume_hook = v;
513 self
514 }
515
516 pub(crate) fn bash_completion_sink(mut self, v: Option<Arc<dyn BashCompletionSink>>) -> Self {
519 self.bash_completion_sink = v;
520 self
521 }
522
523 pub fn app_data_dir(mut self, v: std::path::PathBuf) -> Self {
525 self.app_data_dir = Some(v);
526 self
527 }
528
529 pub fn run_budget(mut self, v: bamboo_config::RunBudgetConfig) -> Self {
534 self.run_budget = Some(v);
535 self
536 }
537
538 pub fn build(self) -> ExecuteRequest {
543 let model_roster = ModelRoster {
544 model: self.model,
545 provider_name: self.provider_name,
546 provider_type: self.provider_type,
547 fast: RoleModel::from_parts(self.fast_model, self.fast_model_provider),
548 background: RoleModel::from_parts(
549 self.background_model,
550 self.background_model_provider,
551 ),
552 summarization: RoleModel::from_parts(
553 self.summarization_model,
554 self.summarization_model_provider,
555 ),
556 };
557 ExecuteRequest {
558 initial_message: self.initial_message,
559 event_tx: self.event_tx,
560 cancel_token: self.cancel_token,
561 tools: self.tools,
562 provider_override: self.provider_override,
563 model_roster,
564 reasoning_effort: self.reasoning_effort,
565 auxiliary_model_resolver: self.auxiliary_model_resolver,
566 disabled_filter_resolver: self.disabled_filter_resolver,
567 disabled_tools: self.disabled_tools,
568 disabled_skill_ids: self.disabled_skill_ids,
569 selected_skill_ids: self.selected_skill_ids,
570 selected_skill_mode: self.selected_skill_mode,
571 image_fallback: self.image_fallback,
572 gold_config: self.gold_config,
573 guardian_config: self.guardian_config,
574 guardian_spawner: self.guardian_spawner,
575 bash_resume_hook: self.bash_resume_hook,
576 bash_completion_sink: self.bash_completion_sink,
577 app_data_dir: self.app_data_dir,
578 run_budget: self.run_budget,
579 }
580 }
581}
582
583fn extract_system_prompt(session: &Session) -> Option<String> {
589 session
590 .messages
591 .iter()
592 .find(|m| matches!(m.role, Role::System))
593 .map(|m| m.content.clone())
594}
595
596impl AgentRuntime {
601 pub async fn execute(
606 &self,
607 session: &mut Session,
608 req: ExecuteRequest,
609 ) -> crate::runtime::runner::Result<()> {
610 let system_prompt = extract_system_prompt(session);
611 let config = self.config.read().await;
612 let ExecuteRequest {
613 initial_message,
614 event_tx,
615 cancel_token,
616 tools,
617 provider_override,
618 model_roster,
619 reasoning_effort,
620 auxiliary_model_resolver,
621 disabled_filter_resolver,
622 disabled_tools,
623 disabled_skill_ids,
624 selected_skill_ids,
625 selected_skill_mode,
626 image_fallback,
627 gold_config,
628 guardian_config,
629 guardian_spawner,
630 bash_resume_hook,
631 bash_completion_sink,
632 app_data_dir,
633 run_budget,
634 } = req;
635 let tools = tools.unwrap_or_else(|| self.default_tools.clone());
636 let llm = provider_override.unwrap_or_else(|| self.provider.clone());
637
638 let fast_model = model_roster.fast_model();
643 let fast_model_provider = model_roster.fast_model_provider();
644 let background_model = model_roster.background_model();
645 let background_model_provider = model_roster.background_model_provider();
646 let summarization_model = model_roster.summarization_model();
647 let summarization_model_provider = model_roster.summarization_model_provider();
648 let ModelRoster {
649 model,
650 provider_name,
651 provider_type,
652 ..
653 } = model_roster;
654
655 let loop_config = AgentLoopConfig {
656 max_rounds: 200,
657 system_prompt,
658 legacy_model_limits: config.extra.get("model_limits").cloned(),
661 disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
662 selected_skill_ids,
663 selected_skill_mode,
664 skill_manager: Some(self.skill_manager.clone()),
665 skip_initial_user_message: true,
666 storage: Some(self.storage.clone()),
667 persistence: Some(self.persistence.clone()),
668 attachment_reader: Some(self.attachment_reader.clone()),
669 metrics_collector: Some(self.metrics_collector.clone()),
670 model_name: model,
671 fast_model_name: fast_model.or_else(|| config.get_fast_model()),
672 fast_model_provider,
673 background_model_name: background_model
674 .or_else(|| config.get_memory_background_model()),
675 planning_model_name: config
676 .defaults
677 .as_ref()
678 .and_then(|d| d.planning.as_ref())
679 .map(|r| r.model.clone()),
680 search_model_name: config
681 .defaults
682 .as_ref()
683 .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
684 .map(|r| r.model.clone()),
685 compression_instructions: None,
686 summarization_model_name: summarization_model
687 .or_else(|| config.get_task_summary_model()),
688 background_model_provider,
689 summarization_model_provider,
690 provider_name: Some(provider_name.unwrap_or_else(|| config.provider.clone())),
691 provider_type,
692 reasoning_effort,
693 auxiliary_model_resolver,
694 disabled_filter_resolver,
695 disabled_tools: {
696 let mut merged = config.disabled_tool_names();
697 if let Some(dt) = disabled_tools {
698 merged.extend(dt);
699 }
700 merged
701 },
702 image_fallback,
703 app_data_dir,
704 prompt_memory_flags: config
705 .memory
706 .as_ref()
707 .map(PromptMemoryFlags::from)
708 .unwrap_or_default(),
709 features_dynamic_model_routing: config.features.dynamic_model_routing,
710 permission_mode: session
711 .agent_runtime_state
712 .as_ref()
713 .and_then(|state| state.plan_mode.as_ref())
714 .map(|_| PermissionMode::Plan),
715 gold_config,
716 guardian_config,
717 guardian_spawner,
718 bash_resume_hook,
719 bash_completion_sink,
720 mcp_tool_guidance: tools.tool_guidance(),
724 run_budget: config.run_budget.merged_with_override(run_budget.as_ref()),
730 ..Default::default()
731 };
732
733 drop(config);
734
735 run_agent_loop_with_config(
736 session,
737 initial_message,
738 event_tx,
739 llm,
740 tools,
741 cancel_token,
742 loop_config,
743 )
744 .await
745 }
746}