1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use chrono::Utc;
13use tokio::sync::{broadcast, RwLock};
14
15use bamboo_agent_core::storage::Storage;
16use bamboo_agent_core::AgentEvent;
17use bamboo_engine::Agent;
18use bamboo_llm::Config;
19use bamboo_llm::LLMProvider;
20use bamboo_mcp::manager::McpServerManager;
21use bamboo_skills::{SkillManager, SkillStoreConfig};
22use bamboo_storage::LockedSessionStore;
23use bamboo_storage::SessionStoreV2;
24
25use crate::error::AppError;
26use crate::schedule_app::manager::{build_schedule_context, ScheduleContext};
27use crate::schedule_app::ScheduleManager;
28use crate::schedule_app::ScheduleStore;
29use bamboo_engine::execution::spawn::{SpawnContext, SpawnScheduler};
30use bamboo_metrics::metrics_service::MetricsService;
31
32use super::{AgentRunner, AgentStatus};
33
34pub(super) type PermissionChecker = dyn bamboo_tools::permission::PermissionChecker;
36
37type ProviderHandles = (Arc<RwLock<Arc<dyn LLMProvider>>>, Arc<dyn LLMProvider>);
39
40pub async fn init_storage(
44 data_dir: &Path,
45) -> Result<(Arc<SessionStoreV2>, Arc<dyn Storage>), AppError> {
46 tracing::info!("Initializing session store V2 at: {:?}", data_dir);
47 let session_store = Arc::new(SessionStoreV2::new(data_dir.to_path_buf()).await.map_err(
48 |error| {
49 tracing::error!(
50 "Failed to initialize SessionStoreV2 at {:?}: {}",
51 data_dir,
52 error
53 );
54 AppError::StorageError(error)
55 },
56 )?);
57
58 match session_store.migrate_runtime_sidecars().await {
64 Ok(0) => {}
65 Ok(count) => tracing::info!("Runtime sidecar migration created {count} sidecar(s)"),
66 Err(error) => {
67 tracing::warn!("Runtime sidecar migration failed (continuing): {error}");
69 }
70 }
71
72 let session_store_for_rebuild = session_store.clone();
73 tokio::spawn(async move {
74 let purged_rows = match session_store_for_rebuild
75 .search_index()
76 .prune_stale_sessions()
77 .await
78 {
79 Ok(count) => count,
80 Err(error) => {
81 tracing::warn!("Background session search index prune failed: {}", error);
82 0
83 }
84 };
85
86 if let Err(error) = session_store_for_rebuild.rebuild_search_index().await {
87 tracing::warn!("Background session search index rebuild failed: {}", error);
88 } else {
89 tracing::info!("Background session search index rebuild completed");
90 }
91
92 match session_store_for_rebuild
93 .search_index()
94 .maybe_vacuum_if_needed(purged_rows)
95 .await
96 {
97 Ok(true) => tracing::info!("Background session search index vacuum completed"),
98 Ok(false) => {}
99 Err(error) => {
100 tracing::warn!("Background session search index vacuum failed: {}", error)
101 }
102 }
103 });
104
105 let storage: Arc<dyn Storage> = session_store.clone();
106 tracing::info!(
107 "Session store V2 initialized (index: {:?}, sessions: {:?})",
108 session_store.index_path(),
109 session_store.sessions_root_dir()
110 );
111 Ok((session_store, storage))
112}
113
114pub async fn init_skill_manager(data_dir: &Path) -> Arc<SkillManager> {
116 let project_dir = std::env::var_os("BAMBOO_WORKSPACE_DIR")
117 .map(PathBuf::from)
118 .or_else(|| std::env::current_dir().ok());
119 let active_mode = std::env::var("BAMBOO_SKILL_MODE")
120 .ok()
121 .map(|value| value.trim().to_string())
122 .filter(|value| !value.is_empty());
123
124 let skill_manager = Arc::new(SkillManager::with_config(SkillStoreConfig {
125 skills_dir: data_dir.join("skills"),
126 project_dir,
127 active_mode,
128 }));
129 if let Err(error) = skill_manager.initialize().await {
130 tracing::warn!("Failed to initialize skill manager: {}", error);
131 }
132 skill_manager
133}
134
135pub fn build_provider_handles(provider: Arc<dyn LLMProvider>) -> ProviderHandles {
141 let provider_lock: Arc<RwLock<Arc<dyn LLMProvider>>> = Arc::new(RwLock::new(provider));
142 let provider_handle: Arc<dyn LLMProvider> = Arc::new(
143 crate::reloadable_provider::ReloadableProvider::new(provider_lock.clone()),
144 );
145 (provider_lock, provider_handle)
146}
147
148pub async fn load_permission_checker(bamboo_home_dir: &Path) -> Arc<PermissionChecker> {
152 use bamboo_tools::permission::{PermissionConfig, RiskLevel};
153
154 let storage = bamboo_tools::permission::storage::PermissionStorage::new(bamboo_home_dir);
155 let permission_config = match storage.load().await {
156 Ok(Some(config)) => config,
157 Ok(None) => {
158 let cfg = PermissionConfig::new();
164 cfg.set_confirm_threshold(RiskLevel::High);
165 cfg
166 }
167 Err(error) => {
168 tracing::warn!(
169 "Failed to load permission config; defaulting to ask-on-high-risk: {error}"
170 );
171 let cfg = PermissionConfig::new();
172 cfg.set_confirm_threshold(RiskLevel::High);
173 cfg
174 }
175 };
176 permission_config.cleanup_expired_grants();
177
178 let config = Arc::new(permission_config);
183 let inner: Arc<dyn bamboo_tools::permission::PermissionChecker> = Arc::new(
184 bamboo_tools::permission::ConfigPermissionChecker::new(config.clone()),
185 );
186 Arc::new(bamboo_tools::permission::ModeAwarePermissionChecker::new(
187 inner, config,
188 ))
189}
190
191pub fn init_mcp_manager(config: Arc<RwLock<Config>>) -> Arc<McpServerManager> {
193 let mcp_manager = Arc::new(McpServerManager::new_with_config(config.clone()));
194
195 {
197 let mcp_manager = mcp_manager.clone();
198 let config = config.clone();
199 tokio::spawn(async move {
200 let mcp_config = config.read().await.mcp.clone();
201 mcp_manager.initialize_from_config(&mcp_config).await;
202 });
203 }
204
205 mcp_manager
206}
207
208pub async fn init_metrics_service(data_dir: &Path) -> Result<Arc<MetricsService>, AppError> {
210 let service = MetricsService::new(data_dir.join("metrics.db"))
211 .await
212 .map_err(|error| {
213 tracing::error!("Failed to initialize metrics storage: {}", error);
214 AppError::InternalError(anyhow::anyhow!(
215 "Failed to initialize metrics storage: {error}"
216 ))
217 })?;
218 Ok(Arc::new(service))
219}
220
221pub fn spawn_runner_cleanup_task(
223 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
224 log_prefix: Option<&'static str>,
225) {
226 tokio::spawn(async move {
227 loop {
228 tokio::time::sleep(Duration::from_secs(60)).await;
229
230 let mut runners_guard = runners.write().await;
231 let now = Utc::now();
232
233 runners_guard.retain(|session_id, runner| {
234 let should_keep = match &runner.status {
235 AgentStatus::Running => true,
236 _ => {
237 let age = now.signed_duration_since(
238 runner.completed_at.unwrap_or(runner.started_at),
239 );
240 age.num_seconds() < 300 }
242 };
243
244 if !should_keep {
245 if let Some(prefix) = log_prefix {
246 tracing::debug!("[{}:{}] Cleaning up completed runner", prefix, session_id);
247 } else {
248 tracing::debug!("[{}] Cleaning up completed runner", session_id);
249 }
250 }
251
252 should_keep
253 });
254 }
255 });
256}
257
258pub async fn init_schedule_store(data_dir: &PathBuf) -> Result<Arc<ScheduleStore>, AppError> {
260 let store = ScheduleStore::new(data_dir.clone())
261 .await
262 .map_err(|error| {
263 tracing::error!(
264 "Failed to initialize ScheduleStore at {:?}: {}",
265 data_dir,
266 error
267 );
268 AppError::StorageError(error)
269 })?;
270 Ok(Arc::new(store))
271}
272
273#[allow(clippy::too_many_arguments)]
275pub fn build_spawn_scheduler(
276 agent: Arc<Agent>,
277 child_tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor>,
278 sessions: bamboo_engine::SessionCache,
279 agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
280 session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
281 external_child_runner: Arc<dyn bamboo_engine::runtime::execution::ExternalChildRunner>,
282 provider_router: Option<Arc<bamboo_llm::ProviderModelRouter>>,
283 completion_handler: Option<Arc<dyn bamboo_engine::execution::ChildCompletionHandler>>,
284 app_data_dir: Option<std::path::PathBuf>,
285 account_feed_inbox: Option<bamboo_engine::execution::AccountFeedInbox>,
286) -> Arc<SpawnScheduler> {
287 Arc::new(SpawnScheduler::new(SpawnContext {
288 agent,
289 tools: child_tools,
290 sessions_cache: sessions,
291 agent_runners,
292 session_event_senders,
293 external_child_runner,
294 provider_router,
295 app_data_dir,
296 completion_handler,
297 account_feed_inbox,
298 }))
299}
300
301#[allow(clippy::too_many_arguments)]
303pub fn build_schedule_manager(
304 schedule_store: Arc<ScheduleStore>,
305 agent: Arc<Agent>,
306 tools_for_schedules: Arc<dyn bamboo_agent_core::tools::ToolExecutor>,
307 sessions: bamboo_engine::SessionCache,
308 agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
309 session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
310 persistence: Arc<LockedSessionStore>,
311 config: Arc<RwLock<Config>>,
312 provider_registry: Arc<bamboo_llm::ProviderRegistry>,
313 app_data_dir: Option<std::path::PathBuf>,
314 account_feed_inbox: Option<bamboo_engine::execution::AccountFeedInbox>,
315) -> Arc<ScheduleManager> {
316 let base_ctx = ScheduleContext {
317 schedule_store,
318 agent,
319 tools: tools_for_schedules,
320 sessions_cache: sessions,
321 agent_runners,
322 session_event_senders,
323 account_feed_inbox,
324 persistence,
325 app_data_dir,
326 trigger_engine: crate::schedule_app::default_trigger_engine(),
327 resolve_run_config: Arc::new(|_| unimplemented!("replaced by build_schedule_context")),
328 };
329 Arc::new(ScheduleManager::new(build_schedule_context(
330 base_ctx,
331 config,
332 provider_registry,
333 )))
334}