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}
244
245pub struct ExecuteRequestBuilder {
261 initial_message: String,
262 event_tx: mpsc::Sender<AgentEvent>,
263 cancel_token: CancellationToken,
264
265 tools: Option<Arc<dyn ToolExecutor>>,
266 provider_override: Option<Arc<dyn LLMProvider>>,
267 model: Option<String>,
271 provider_name: Option<String>,
272 provider_type: Option<String>,
273 fast_model: Option<String>,
274 fast_model_provider: Option<Arc<dyn LLMProvider>>,
275 background_model: Option<String>,
276 background_model_provider: Option<Arc<dyn LLMProvider>>,
277 summarization_model: Option<String>,
278 summarization_model_provider: Option<Arc<dyn LLMProvider>>,
279 reasoning_effort: Option<ReasoningEffort>,
280 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
281 disabled_filter_resolver:
282 Option<Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>>,
283 disabled_tools: Option<BTreeSet<String>>,
284 disabled_skill_ids: Option<BTreeSet<String>>,
285 selected_skill_ids: Option<Vec<String>>,
286 selected_skill_mode: Option<String>,
287 image_fallback: Option<ImageFallbackConfig>,
288 gold_config: Option<GoldConfig>,
289 guardian_config: Option<GuardianConfig>,
290 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
291 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
292 bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
293 app_data_dir: Option<std::path::PathBuf>,
294}
295
296impl ExecuteRequestBuilder {
297 pub fn new(
300 initial_message: impl Into<String>,
301 event_tx: mpsc::Sender<AgentEvent>,
302 cancel_token: CancellationToken,
303 ) -> Self {
304 Self {
305 initial_message: initial_message.into(),
306 event_tx,
307 cancel_token,
308 tools: None,
309 provider_override: None,
310 model: None,
311 provider_name: None,
312 provider_type: None,
313 fast_model: None,
314 fast_model_provider: None,
315 background_model: None,
316 background_model_provider: None,
317 summarization_model: None,
318 summarization_model_provider: None,
319 reasoning_effort: None,
320 auxiliary_model_resolver: None,
321 disabled_filter_resolver: None,
322 disabled_tools: None,
323 disabled_skill_ids: None,
324 selected_skill_ids: None,
325 selected_skill_mode: None,
326 image_fallback: None,
327 gold_config: None,
328 guardian_config: None,
329 guardian_spawner: None,
330 bash_resume_hook: None,
331 bash_completion_sink: None,
332 app_data_dir: None,
333 }
334 }
335
336 pub fn tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
338 self.tools = Some(v);
339 self
340 }
341
342 pub fn provider_override(mut self, v: Arc<dyn LLMProvider>) -> Self {
344 self.provider_override = Some(v);
345 self
346 }
347
348 pub fn model_roster(mut self, roster: ModelRoster) -> Self {
354 self.fast_model = roster.fast_model();
355 self.fast_model_provider = roster.fast_model_provider();
356 self.background_model = roster.background_model();
357 self.background_model_provider = roster.background_model_provider();
358 self.summarization_model = roster.summarization_model();
359 self.summarization_model_provider = roster.summarization_model_provider();
360 self.model = roster.model;
361 self.provider_name = roster.provider_name;
362 self.provider_type = roster.provider_type;
363 self
364 }
365
366 pub fn model(mut self, v: impl Into<String>) -> Self {
368 self.model = Some(v.into());
369 self
370 }
371
372 pub fn provider_name(mut self, v: impl Into<String>) -> Self {
374 self.provider_name = Some(v.into());
375 self
376 }
377
378 pub fn provider_type(mut self, v: impl Into<String>) -> Self {
380 self.provider_type = Some(v.into());
381 self
382 }
383
384 pub fn fast_model(mut self, v: impl Into<String>) -> Self {
386 self.fast_model = Some(v.into());
387 self
388 }
389
390 pub fn fast_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
392 self.fast_model_provider = Some(v);
393 self
394 }
395
396 pub fn background_model(mut self, v: impl Into<String>) -> Self {
398 self.background_model = Some(v.into());
399 self
400 }
401
402 pub fn background_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
404 self.background_model_provider = Some(v);
405 self
406 }
407
408 pub fn summarization_model(mut self, v: impl Into<String>) -> Self {
410 self.summarization_model = Some(v.into());
411 self
412 }
413
414 pub fn summarization_model_provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
416 self.summarization_model_provider = Some(v);
417 self
418 }
419
420 pub fn reasoning_effort(mut self, v: ReasoningEffort) -> Self {
422 self.reasoning_effort = Some(v);
423 self
424 }
425
426 pub fn auxiliary_model_resolver(
428 mut self,
429 v: Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>,
430 ) -> Self {
431 self.auxiliary_model_resolver = Some(v);
432 self
433 }
434
435 pub fn disabled_filter_resolver(
437 mut self,
438 v: Arc<dyn Fn() -> (BTreeSet<String>, BTreeSet<String>) + Send + Sync>,
439 ) -> Self {
440 self.disabled_filter_resolver = Some(v);
441 self
442 }
443
444 pub fn disabled_tools(mut self, v: BTreeSet<String>) -> Self {
446 self.disabled_tools = Some(v);
447 self
448 }
449
450 pub fn disabled_skill_ids(mut self, v: BTreeSet<String>) -> Self {
452 self.disabled_skill_ids = Some(v);
453 self
454 }
455
456 pub fn selected_skill_ids(mut self, v: Vec<String>) -> Self {
458 self.selected_skill_ids = Some(v);
459 self
460 }
461
462 pub fn selected_skill_mode(mut self, v: impl Into<String>) -> Self {
464 self.selected_skill_mode = Some(v.into());
465 self
466 }
467
468 pub fn image_fallback(mut self, v: ImageFallbackConfig) -> Self {
470 self.image_fallback = Some(v);
471 self
472 }
473
474 pub(crate) fn gold_config(mut self, v: Option<GoldConfig>) -> Self {
481 self.gold_config = v;
482 self
483 }
484
485 pub(crate) fn guardian_config(mut self, v: Option<GuardianConfig>) -> Self {
488 self.guardian_config = v;
489 self
490 }
491
492 pub(crate) fn guardian_spawner(mut self, v: Option<Arc<dyn GuardianSpawner>>) -> Self {
495 self.guardian_spawner = v;
496 self
497 }
498
499 pub(crate) fn bash_resume_hook(mut self, v: Option<Arc<dyn BashResumeHook>>) -> Self {
502 self.bash_resume_hook = v;
503 self
504 }
505
506 pub(crate) fn bash_completion_sink(
509 mut self,
510 v: Option<Arc<dyn BashCompletionSink>>,
511 ) -> Self {
512 self.bash_completion_sink = v;
513 self
514 }
515
516 pub fn app_data_dir(mut self, v: std::path::PathBuf) -> Self {
518 self.app_data_dir = Some(v);
519 self
520 }
521
522 pub fn build(self) -> ExecuteRequest {
527 let model_roster = ModelRoster {
528 model: self.model,
529 provider_name: self.provider_name,
530 provider_type: self.provider_type,
531 fast: RoleModel::from_parts(self.fast_model, self.fast_model_provider),
532 background: RoleModel::from_parts(
533 self.background_model,
534 self.background_model_provider,
535 ),
536 summarization: RoleModel::from_parts(
537 self.summarization_model,
538 self.summarization_model_provider,
539 ),
540 };
541 ExecuteRequest {
542 initial_message: self.initial_message,
543 event_tx: self.event_tx,
544 cancel_token: self.cancel_token,
545 tools: self.tools,
546 provider_override: self.provider_override,
547 model_roster,
548 reasoning_effort: self.reasoning_effort,
549 auxiliary_model_resolver: self.auxiliary_model_resolver,
550 disabled_filter_resolver: self.disabled_filter_resolver,
551 disabled_tools: self.disabled_tools,
552 disabled_skill_ids: self.disabled_skill_ids,
553 selected_skill_ids: self.selected_skill_ids,
554 selected_skill_mode: self.selected_skill_mode,
555 image_fallback: self.image_fallback,
556 gold_config: self.gold_config,
557 guardian_config: self.guardian_config,
558 guardian_spawner: self.guardian_spawner,
559 bash_resume_hook: self.bash_resume_hook,
560 bash_completion_sink: self.bash_completion_sink,
561 app_data_dir: self.app_data_dir,
562 }
563 }
564}
565
566fn extract_system_prompt(session: &Session) -> Option<String> {
572 session
573 .messages
574 .iter()
575 .find(|m| matches!(m.role, Role::System))
576 .map(|m| m.content.clone())
577}
578
579impl AgentRuntime {
584 pub async fn execute(
589 &self,
590 session: &mut Session,
591 req: ExecuteRequest,
592 ) -> crate::runtime::runner::Result<()> {
593 let system_prompt = extract_system_prompt(session);
594 let config = self.config.read().await;
595 let ExecuteRequest {
596 initial_message,
597 event_tx,
598 cancel_token,
599 tools,
600 provider_override,
601 model_roster,
602 reasoning_effort,
603 auxiliary_model_resolver,
604 disabled_filter_resolver,
605 disabled_tools,
606 disabled_skill_ids,
607 selected_skill_ids,
608 selected_skill_mode,
609 image_fallback,
610 gold_config,
611 guardian_config,
612 guardian_spawner,
613 bash_resume_hook,
614 bash_completion_sink,
615 app_data_dir,
616 } = req;
617 let tools = tools.unwrap_or_else(|| self.default_tools.clone());
618 let llm = provider_override.unwrap_or_else(|| self.provider.clone());
619
620 let fast_model = model_roster.fast_model();
625 let fast_model_provider = model_roster.fast_model_provider();
626 let background_model = model_roster.background_model();
627 let background_model_provider = model_roster.background_model_provider();
628 let summarization_model = model_roster.summarization_model();
629 let summarization_model_provider = model_roster.summarization_model_provider();
630 let ModelRoster {
631 model,
632 provider_name,
633 provider_type,
634 ..
635 } = model_roster;
636
637 let loop_config = AgentLoopConfig {
638 max_rounds: 200,
639 system_prompt,
640 legacy_model_limits: config.extra.get("model_limits").cloned(),
643 disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
644 selected_skill_ids,
645 selected_skill_mode,
646 skill_manager: Some(self.skill_manager.clone()),
647 skip_initial_user_message: true,
648 storage: Some(self.storage.clone()),
649 persistence: Some(self.persistence.clone()),
650 attachment_reader: Some(self.attachment_reader.clone()),
651 metrics_collector: Some(self.metrics_collector.clone()),
652 model_name: model,
653 fast_model_name: fast_model.or_else(|| config.get_fast_model()),
654 fast_model_provider,
655 background_model_name: background_model
656 .or_else(|| config.get_memory_background_model()),
657 planning_model_name: config
658 .defaults
659 .as_ref()
660 .and_then(|d| d.planning.as_ref())
661 .map(|r| r.model.clone()),
662 search_model_name: config
663 .defaults
664 .as_ref()
665 .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
666 .map(|r| r.model.clone()),
667 compression_instructions: None,
668 summarization_model_name: summarization_model
669 .or_else(|| config.get_task_summary_model()),
670 background_model_provider,
671 summarization_model_provider,
672 provider_name: Some(provider_name.unwrap_or_else(|| config.provider.clone())),
673 provider_type,
674 reasoning_effort,
675 auxiliary_model_resolver,
676 disabled_filter_resolver,
677 disabled_tools: {
678 let mut merged = config.disabled_tool_names();
679 if let Some(dt) = disabled_tools {
680 merged.extend(dt);
681 }
682 merged
683 },
684 image_fallback,
685 app_data_dir,
686 prompt_memory_flags: config
687 .memory
688 .as_ref()
689 .map(PromptMemoryFlags::from)
690 .unwrap_or_default(),
691 features_dynamic_model_routing: config.features.dynamic_model_routing,
692 permission_mode: session
693 .agent_runtime_state
694 .as_ref()
695 .and_then(|state| state.plan_mode.as_ref())
696 .map(|_| PermissionMode::Plan),
697 gold_config,
698 guardian_config,
699 guardian_spawner,
700 bash_resume_hook,
701 bash_completion_sink,
702 mcp_tool_guidance: tools.tool_guidance(),
706 ..Default::default()
707 };
708
709 drop(config);
710
711 run_agent_loop_with_config(
712 session,
713 initial_message,
714 event_tx,
715 llm,
716 tools,
717 cancel_token,
718 loop_config,
719 )
720 .await
721 }
722}