use std::collections::BTreeSet;
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tokio_util::sync::CancellationToken;
use crate::metrics::MetricsCollector;
use crate::skills::SkillManager;
use bamboo_agent_core::storage::{AttachmentReader, Storage};
use bamboo_agent_core::tools::ToolExecutor;
use bamboo_agent_core::{AgentEvent, Role, Session};
use bamboo_domain::ReasoningEffort;
use bamboo_infrastructure::config::PermissionMode;
use bamboo_infrastructure::Config;
use bamboo_infrastructure::LLMProvider;
use crate::runtime::config::{AgentLoopConfig, ImageFallbackConfig, PromptMemoryFlags};
use crate::runtime::hooks::HookRunner;
use crate::runtime::managers::{
LifecycleManager, LlmManager, MemoryManager, PromptManager, ToolManager,
};
use crate::runtime::runner::run_agent_loop_with_config;
#[derive(Clone)]
pub struct AgentRuntime {
pub storage: Arc<dyn Storage>,
pub attachment_reader: Arc<dyn AttachmentReader>,
pub skill_manager: Arc<SkillManager>,
pub metrics_collector: MetricsCollector,
pub config: Arc<RwLock<Config>>,
pub provider: Arc<dyn LLMProvider>,
pub default_tools: Arc<dyn ToolExecutor>,
pub prompt_manager: Option<Arc<dyn PromptManager>>,
pub memory_manager: Option<Arc<dyn MemoryManager>>,
pub tool_manager: Option<Arc<dyn ToolManager>>,
pub llm_manager: Option<Arc<dyn LlmManager>>,
pub lifecycle_manager: Option<Arc<dyn LifecycleManager>>,
pub hook_runner: Option<HookRunner>,
}
pub struct AgentRuntimeBuilder {
storage: Option<Arc<dyn Storage>>,
attachment_reader: Option<Arc<dyn AttachmentReader>>,
skill_manager: Option<Arc<SkillManager>>,
metrics_collector: Option<MetricsCollector>,
config: Option<Arc<RwLock<Config>>>,
provider: Option<Arc<dyn LLMProvider>>,
default_tools: Option<Arc<dyn ToolExecutor>>,
prompt_manager: Option<Arc<dyn PromptManager>>,
memory_manager: Option<Arc<dyn MemoryManager>>,
tool_manager: Option<Arc<dyn ToolManager>>,
llm_manager: Option<Arc<dyn LlmManager>>,
lifecycle_manager: Option<Arc<dyn LifecycleManager>>,
hook_runner: Option<HookRunner>,
}
impl AgentRuntimeBuilder {
pub fn new() -> Self {
Self {
storage: None,
attachment_reader: None,
skill_manager: None,
metrics_collector: None,
config: None,
provider: None,
default_tools: None,
prompt_manager: None,
memory_manager: None,
tool_manager: None,
llm_manager: None,
lifecycle_manager: None,
hook_runner: None,
}
}
pub fn storage(mut self, v: Arc<dyn Storage>) -> Self {
self.storage = Some(v);
self
}
pub fn attachment_reader(mut self, v: Arc<dyn AttachmentReader>) -> Self {
self.attachment_reader = Some(v);
self
}
pub fn skill_manager(mut self, v: Arc<SkillManager>) -> Self {
self.skill_manager = Some(v);
self
}
pub fn metrics_collector(mut self, v: MetricsCollector) -> Self {
self.metrics_collector = Some(v);
self
}
pub fn config(mut self, v: Arc<RwLock<Config>>) -> Self {
self.config = Some(v);
self
}
pub fn provider(mut self, v: Arc<dyn LLMProvider>) -> Self {
self.provider = Some(v);
self
}
pub fn default_tools(mut self, v: Arc<dyn ToolExecutor>) -> Self {
self.default_tools = Some(v);
self
}
pub fn prompt_manager(mut self, v: Arc<dyn PromptManager>) -> Self {
self.prompt_manager = Some(v);
self
}
pub fn memory_manager(mut self, v: Arc<dyn MemoryManager>) -> Self {
self.memory_manager = Some(v);
self
}
pub fn tool_manager(mut self, v: Arc<dyn ToolManager>) -> Self {
self.tool_manager = Some(v);
self
}
pub fn llm_manager(mut self, v: Arc<dyn LlmManager>) -> Self {
self.llm_manager = Some(v);
self
}
pub fn lifecycle_manager(mut self, v: Arc<dyn LifecycleManager>) -> Self {
self.lifecycle_manager = Some(v);
self
}
pub fn hook_runner(mut self, v: HookRunner) -> Self {
self.hook_runner = Some(v);
self
}
pub fn build(self) -> Result<AgentRuntime, &'static str> {
Ok(AgentRuntime {
storage: self.storage.ok_or_else(|| format_missing("storage"))?,
attachment_reader: self
.attachment_reader
.ok_or_else(|| format_missing("attachment_reader"))?,
skill_manager: self
.skill_manager
.ok_or_else(|| format_missing("skill_manager"))?,
metrics_collector: self
.metrics_collector
.ok_or_else(|| format_missing("metrics_collector"))?,
config: self.config.ok_or_else(|| format_missing("config"))?,
provider: self.provider.ok_or_else(|| format_missing("provider"))?,
default_tools: self
.default_tools
.ok_or_else(|| format_missing("default_tools"))?,
prompt_manager: None,
memory_manager: None,
tool_manager: None,
llm_manager: None,
lifecycle_manager: None,
hook_runner: None,
})
}
}
fn format_missing(field: &str) -> &'static str {
match field {
"storage" => "AgentRuntimeBuilder: missing storage",
"attachment_reader" => "AgentRuntimeBuilder: missing attachment_reader",
"skill_manager" => "AgentRuntimeBuilder: missing skill_manager",
"metrics_collector" => "AgentRuntimeBuilder: missing metrics_collector",
"config" => "AgentRuntimeBuilder: missing config",
"provider" => "AgentRuntimeBuilder: missing provider",
"default_tools" => "AgentRuntimeBuilder: missing default_tools",
_ => "AgentRuntimeBuilder: missing required field",
}
}
impl Default for AgentRuntimeBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct ExecuteRequest {
pub initial_message: String,
pub event_tx: mpsc::Sender<AgentEvent>,
pub cancel_token: CancellationToken,
pub tools: Option<Arc<dyn ToolExecutor>>,
pub provider_override: Option<Arc<dyn LLMProvider>>,
pub model: Option<String>,
pub provider_name: Option<String>,
pub background_model: Option<String>,
pub background_model_provider: Option<Arc<dyn LLMProvider>>,
pub reasoning_effort: Option<ReasoningEffort>,
pub disabled_tools: Option<BTreeSet<String>>,
pub disabled_skill_ids: Option<BTreeSet<String>>,
pub selected_skill_ids: Option<Vec<String>>,
pub selected_skill_mode: Option<String>,
pub image_fallback: Option<ImageFallbackConfig>,
}
fn extract_system_prompt(session: &Session) -> Option<String> {
session
.messages
.iter()
.find(|m| matches!(m.role, Role::System))
.map(|m| m.content.clone())
}
impl AgentRuntime {
pub async fn execute(
&self,
session: &mut Session,
req: ExecuteRequest,
) -> crate::runtime::runner::Result<()> {
let system_prompt = extract_system_prompt(session);
let config = self.config.read().await;
let ExecuteRequest {
initial_message,
event_tx,
cancel_token,
tools,
provider_override,
model,
provider_name,
background_model,
background_model_provider,
reasoning_effort,
disabled_tools,
disabled_skill_ids,
selected_skill_ids,
selected_skill_mode,
image_fallback,
} = req;
let tools = tools.unwrap_or_else(|| self.default_tools.clone());
let llm = provider_override.unwrap_or_else(|| self.provider.clone());
let loop_config = AgentLoopConfig {
max_rounds: 200,
system_prompt,
disabled_skill_ids: disabled_skill_ids.unwrap_or_else(|| config.disabled_skill_ids()),
selected_skill_ids,
selected_skill_mode,
skill_manager: Some(self.skill_manager.clone()),
skip_initial_user_message: true,
storage: Some(self.storage.clone()),
attachment_reader: Some(self.attachment_reader.clone()),
metrics_collector: Some(self.metrics_collector.clone()),
model_name: model,
fast_model_name: config.get_fast_model(),
background_model_name: background_model
.or_else(|| config.get_memory_background_model()),
background_model_provider,
planning_model_name: config
.defaults
.as_ref()
.and_then(|d| d.planning.as_ref())
.map(|r| r.model.clone()),
search_model_name: config
.defaults
.as_ref()
.and_then(|d| d.search.as_ref().or(d.fast.as_ref()))
.map(|r| r.model.clone()),
provider_name: Some(provider_name.unwrap_or_else(|| config.provider.clone())),
reasoning_effort,
disabled_tools: disabled_tools.unwrap_or_else(|| config.disabled_tool_names()),
image_fallback,
prompt_memory_flags: config
.memory
.as_ref()
.map(PromptMemoryFlags::from)
.unwrap_or_default(),
features_dynamic_model_routing: config.features.dynamic_model_routing,
permission_mode: session
.agent_runtime_state
.as_ref()
.and_then(|state| state.plan_mode.as_ref())
.map(|_| PermissionMode::Plan),
..Default::default()
};
drop(config);
run_agent_loop_with_config(
session,
initial_message,
event_tx,
llm,
tools,
cancel_token,
loop_config,
)
.await
}
}