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(mut self, v: Option<Arc<dyn BashCompletionSink>>) -> Self {
509 self.bash_completion_sink = v;
510 self
511 }
512
513 pub fn app_data_dir(mut self, v: std::path::PathBuf) -> Self {
515 self.app_data_dir = Some(v);
516 self
517 }
518
519 pub fn build(self) -> ExecuteRequest {
524 let model_roster = ModelRoster {
525 model: self.model,
526 provider_name: self.provider_name,
527 provider_type: self.provider_type,
528 fast: RoleModel::from_parts(self.fast_model, self.fast_model_provider),
529 background: RoleModel::from_parts(
530 self.background_model,
531 self.background_model_provider,
532 ),
533 summarization: RoleModel::from_parts(
534 self.summarization_model,
535 self.summarization_model_provider,
536 ),
537 };
538 ExecuteRequest {
539 initial_message: self.initial_message,
540 event_tx: self.event_tx,
541 cancel_token: self.cancel_token,
542 tools: self.tools,
543 provider_override: self.provider_override,
544 model_roster,
545 reasoning_effort: self.reasoning_effort,
546 auxiliary_model_resolver: self.auxiliary_model_resolver,
547 disabled_filter_resolver: self.disabled_filter_resolver,
548 disabled_tools: self.disabled_tools,
549 disabled_skill_ids: self.disabled_skill_ids,
550 selected_skill_ids: self.selected_skill_ids,
551 selected_skill_mode: self.selected_skill_mode,
552 image_fallback: self.image_fallback,
553 gold_config: self.gold_config,
554 guardian_config: self.guardian_config,
555 guardian_spawner: self.guardian_spawner,
556 bash_resume_hook: self.bash_resume_hook,
557 bash_completion_sink: self.bash_completion_sink,
558 app_data_dir: self.app_data_dir,
559 }
560 }
561}
562
563fn extract_system_prompt(session: &Session) -> Option<String> {
569 session
570 .messages
571 .iter()
572 .find(|m| matches!(m.role, Role::System))
573 .map(|m| m.content.clone())
574}
575
576impl AgentRuntime {
581 pub async fn execute(
586 &self,
587 session: &mut Session,
588 req: ExecuteRequest,
589 ) -> crate::runtime::runner::Result<()> {
590 let system_prompt = extract_system_prompt(session);
591 let config = self.config.read().await;
592 let ExecuteRequest {
593 initial_message,
594 event_tx,
595 cancel_token,
596 tools,
597 provider_override,
598 model_roster,
599 reasoning_effort,
600 auxiliary_model_resolver,
601 disabled_filter_resolver,
602 disabled_tools,
603 disabled_skill_ids,
604 selected_skill_ids,
605 selected_skill_mode,
606 image_fallback,
607 gold_config,
608 guardian_config,
609 guardian_spawner,
610 bash_resume_hook,
611 bash_completion_sink,
612 app_data_dir,
613 } = req;
614 let tools = tools.unwrap_or_else(|| self.default_tools.clone());
615 let llm = provider_override.unwrap_or_else(|| self.provider.clone());
616
617 let fast_model = model_roster.fast_model();
622 let fast_model_provider = model_roster.fast_model_provider();
623 let background_model = model_roster.background_model();
624 let background_model_provider = model_roster.background_model_provider();
625 let summarization_model = model_roster.summarization_model();
626 let summarization_model_provider = model_roster.summarization_model_provider();
627 let ModelRoster {
628 model,
629 provider_name,
630 provider_type,
631 ..
632 } = model_roster;
633
634 let loop_config = AgentLoopConfig {
635 max_rounds: 200,
636 system_prompt,
637 legacy_model_limits: config.extra.get("model_limits").cloned(),
640 disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
641 selected_skill_ids,
642 selected_skill_mode,
643 skill_manager: Some(self.skill_manager.clone()),
644 skip_initial_user_message: true,
645 storage: Some(self.storage.clone()),
646 persistence: Some(self.persistence.clone()),
647 attachment_reader: Some(self.attachment_reader.clone()),
648 metrics_collector: Some(self.metrics_collector.clone()),
649 model_name: model,
650 fast_model_name: fast_model.or_else(|| config.get_fast_model()),
651 fast_model_provider,
652 background_model_name: background_model
653 .or_else(|| config.get_memory_background_model()),
654 planning_model_name: config
655 .defaults
656 .as_ref()
657 .and_then(|d| d.planning.as_ref())
658 .map(|r| r.model.clone()),
659 search_model_name: config
660 .defaults
661 .as_ref()
662 .and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
663 .map(|r| r.model.clone()),
664 compression_instructions: None,
665 summarization_model_name: summarization_model
666 .or_else(|| config.get_task_summary_model()),
667 background_model_provider,
668 summarization_model_provider,
669 provider_name: Some(provider_name.unwrap_or_else(|| config.provider.clone())),
670 provider_type,
671 reasoning_effort,
672 auxiliary_model_resolver,
673 disabled_filter_resolver,
674 disabled_tools: {
675 let mut merged = config.disabled_tool_names();
676 if let Some(dt) = disabled_tools {
677 merged.extend(dt);
678 }
679 merged
680 },
681 image_fallback,
682 app_data_dir,
683 prompt_memory_flags: config
684 .memory
685 .as_ref()
686 .map(PromptMemoryFlags::from)
687 .unwrap_or_default(),
688 features_dynamic_model_routing: config.features.dynamic_model_routing,
689 permission_mode: session
690 .agent_runtime_state
691 .as_ref()
692 .and_then(|state| state.plan_mode.as_ref())
693 .map(|_| PermissionMode::Plan),
694 gold_config,
695 guardian_config,
696 guardian_spawner,
697 bash_resume_hook,
698 bash_completion_sink,
699 mcp_tool_guidance: tools.tool_guidance(),
703 ..Default::default()
704 };
705
706 drop(config);
707
708 run_agent_loop_with_config(
709 session,
710 initial_message,
711 event_tx,
712 llm,
713 tools,
714 cancel_token,
715 loop_config,
716 )
717 .await
718 }
719}