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(crate) const SESSION_MAP_IDLE_TTL_SECS: i64 = 300;
227
228pub(crate) fn evict_idle_session_entries(
254 runners: &mut HashMap<String, AgentRunner>,
255 senders: &mut HashMap<String, broadcast::Sender<AgentEvent>>,
256 ttl_secs: i64,
257 now: chrono::DateTime<Utc>,
258 log_prefix: Option<&'static str>,
259) -> usize {
260 let mut evicted: Vec<String> = Vec::new();
261
262 runners.retain(|session_id, runner| {
263 let keep = match &runner.status {
264 AgentStatus::Running => true,
265 _ => {
266 let age =
267 now.signed_duration_since(runner.completed_at.unwrap_or(runner.started_at));
268 let expired = age.num_seconds() >= ttl_secs;
269 let has_receivers = runner.event_sender.receiver_count() > 0;
270 !expired || has_receivers
272 }
273 };
274 if !keep {
275 evicted.push(session_id.clone());
276 if let Some(prefix) = log_prefix {
277 tracing::debug!("[{}:{}] Evicting idle terminal runner", prefix, session_id);
278 } else {
279 tracing::debug!("[{}] Evicting idle terminal runner", session_id);
280 }
281 }
282 keep
283 });
284
285 for session_id in &evicted {
291 if senders
292 .get(session_id)
293 .is_some_and(|sender| sender.receiver_count() == 0)
294 {
295 senders.remove(session_id);
296 }
297 }
298
299 evicted.len()
300}
301
302pub fn spawn_session_map_cleanup_task(
309 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
310 senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
311 log_prefix: Option<&'static str>,
312) {
313 tokio::spawn(async move {
314 loop {
315 tokio::time::sleep(Duration::from_secs(60)).await;
316
317 let mut runners_guard = runners.write().await;
331 let mut senders_guard = senders.write().await;
332 let now = Utc::now();
333 let evicted = evict_idle_session_entries(
334 &mut runners_guard,
335 &mut senders_guard,
336 SESSION_MAP_IDLE_TTL_SECS,
337 now,
338 log_prefix,
339 );
340 drop(senders_guard);
341 drop(runners_guard);
342
343 if evicted > 0 {
344 tracing::debug!("Idle-evicted {evicted} completed session runner(s)/sender(s)");
345 }
346 }
347 });
348}
349
350pub async fn init_schedule_store(data_dir: &PathBuf) -> Result<Arc<ScheduleStore>, AppError> {
352 let store = ScheduleStore::new(data_dir.clone())
353 .await
354 .map_err(|error| {
355 tracing::error!(
356 "Failed to initialize ScheduleStore at {:?}: {}",
357 data_dir,
358 error
359 );
360 AppError::StorageError(error)
361 })?;
362 Ok(Arc::new(store))
363}
364
365#[allow(clippy::too_many_arguments)]
367pub fn build_spawn_scheduler(
368 agent: Arc<Agent>,
369 child_tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor>,
370 sessions: bamboo_engine::SessionCache,
371 agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
372 session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
373 external_child_runner: Arc<dyn bamboo_engine::runtime::execution::ExternalChildRunner>,
374 provider_router: Option<Arc<bamboo_llm::ProviderModelRouter>>,
375 completion_handler: Option<Arc<dyn bamboo_engine::execution::ChildCompletionHandler>>,
376 app_data_dir: Option<std::path::PathBuf>,
377 account_feed_inbox: Option<bamboo_engine::execution::AccountFeedInbox>,
378) -> Arc<SpawnScheduler> {
379 Arc::new(SpawnScheduler::new(SpawnContext {
380 agent,
381 tools: child_tools,
382 sessions_cache: sessions,
383 agent_runners,
384 session_event_senders,
385 external_child_runner,
386 provider_router,
387 app_data_dir,
388 completion_handler,
389 account_feed_inbox,
390 }))
391}
392
393#[allow(clippy::too_many_arguments)]
395pub fn build_schedule_manager(
396 schedule_store: Arc<ScheduleStore>,
397 agent: Arc<Agent>,
398 tools_for_schedules: Arc<dyn bamboo_agent_core::tools::ToolExecutor>,
399 sessions: bamboo_engine::SessionCache,
400 agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
401 session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
402 persistence: Arc<LockedSessionStore>,
403 config: Arc<RwLock<Config>>,
404 provider_registry: Arc<bamboo_llm::ProviderRegistry>,
405 app_data_dir: Option<std::path::PathBuf>,
406 account_feed_inbox: Option<bamboo_engine::execution::AccountFeedInbox>,
407 notification_relay: crate::app_state::session_events::NotificationRelayDeps,
408) -> Arc<ScheduleManager> {
409 let base_ctx = ScheduleContext {
410 schedule_store,
411 agent,
412 tools: tools_for_schedules,
413 sessions_cache: sessions,
414 agent_runners,
415 session_event_senders,
416 account_feed_inbox,
417 persistence,
418 app_data_dir,
419 trigger_engine: crate::schedule_app::default_trigger_engine(),
420 notification_relay,
421 resolve_run_config: Arc::new(|_| unimplemented!("replaced by build_schedule_context")),
422 };
423 Arc::new(ScheduleManager::new(build_schedule_context(
424 base_ctx,
425 config,
426 provider_registry,
427 )))
428}
429
430#[cfg(test)]
431mod eviction_tests {
432 use super::*;
433 use chrono::Duration as ChronoDuration;
434
435 const TTL: i64 = SESSION_MAP_IDLE_TTL_SECS;
436
437 fn terminal_runner(
440 tx: &broadcast::Sender<AgentEvent>,
441 completed_secs_ago: i64,
442 now: chrono::DateTime<Utc>,
443 ) -> AgentRunner {
444 let mut runner = AgentRunner::new();
445 runner.status = AgentStatus::Completed;
446 runner.completed_at = Some(now - ChronoDuration::seconds(completed_secs_ago));
447 runner.event_sender = tx.clone();
448 runner
449 }
450
451 fn insert_pair(
452 runners: &mut HashMap<String, AgentRunner>,
453 senders: &mut HashMap<String, broadcast::Sender<AgentEvent>>,
454 id: &str,
455 runner: AgentRunner,
456 tx: broadcast::Sender<AgentEvent>,
457 ) {
458 runners.insert(id.to_string(), runner);
459 senders.insert(id.to_string(), tx);
460 }
461
462 #[test]
463 fn evicts_idle_terminal_pair_past_ttl_with_no_receivers() {
464 let now = Utc::now();
465 let (tx, _) = broadcast::channel::<AgentEvent>(16); let mut runners = HashMap::new();
467 let mut senders = HashMap::new();
468 insert_pair(
469 &mut runners,
470 &mut senders,
471 "s1",
472 terminal_runner(&tx, TTL + 100, now),
473 tx.clone(),
474 );
475
476 let evicted = evict_idle_session_entries(&mut runners, &mut senders, TTL, now, None);
477
478 assert_eq!(evicted, 1);
479 assert!(runners.is_empty(), "terminal idle runner must be dropped");
480 assert!(
481 senders.is_empty(),
482 "paired session sender must be dropped together with the runner"
483 );
484 }
485
486 #[test]
487 fn retains_terminal_runner_with_live_receiver() {
488 let now = Utc::now();
489 let (tx, _) = broadcast::channel::<AgentEvent>(16);
490 let _live_rx = tx.subscribe();
492 assert_eq!(tx.receiver_count(), 1);
493
494 let mut runners = HashMap::new();
495 let mut senders = HashMap::new();
496 insert_pair(
497 &mut runners,
498 &mut senders,
499 "s1",
500 terminal_runner(&tx, TTL + 100, now),
501 tx.clone(),
502 );
503
504 let evicted = evict_idle_session_entries(&mut runners, &mut senders, TTL, now, None);
505
506 assert_eq!(evicted, 0, "must not evict while a receiver is live");
507 assert!(runners.contains_key("s1"));
508 assert!(senders.contains_key("s1"));
509 }
510
511 #[test]
512 fn retains_young_terminal_runner() {
513 let now = Utc::now();
514 let (tx, _) = broadcast::channel::<AgentEvent>(16);
515 let mut runners = HashMap::new();
516 let mut senders = HashMap::new();
517 insert_pair(
519 &mut runners,
520 &mut senders,
521 "s1",
522 terminal_runner(&tx, 60, now),
523 tx.clone(),
524 );
525
526 let evicted = evict_idle_session_entries(&mut runners, &mut senders, TTL, now, None);
527
528 assert_eq!(
529 evicted, 0,
530 "must preserve the post-completion replay window"
531 );
532 assert!(runners.contains_key("s1"));
533 assert!(senders.contains_key("s1"));
534 }
535
536 #[test]
537 fn retains_running_runner_regardless_of_age() {
538 let now = Utc::now();
539 let (tx, _) = broadcast::channel::<AgentEvent>(16);
540 let mut runner = AgentRunner::new();
541 runner.status = AgentStatus::Running;
542 runner.started_at = now - ChronoDuration::seconds(TTL + 100_000);
543 runner.completed_at = None;
544 runner.event_sender = tx.clone();
545
546 let mut runners = HashMap::new();
547 let mut senders = HashMap::new();
548 insert_pair(&mut runners, &mut senders, "s1", runner, tx.clone());
549
550 let evicted = evict_idle_session_entries(&mut runners, &mut senders, TTL, now, None);
551
552 assert_eq!(evicted, 0, "a Running runner is never evicted");
553 assert!(runners.contains_key("s1"));
554 assert!(senders.contains_key("s1"));
555 }
556
557 #[test]
558 fn keeps_sender_if_it_has_receivers_even_when_runner_evicted() {
559 let now = Utc::now();
564 let (runner_tx, _) = broadcast::channel::<AgentEvent>(16);
565 let (sender_tx, _) = broadcast::channel::<AgentEvent>(16);
567 let _live = sender_tx.subscribe();
568
569 let mut runners = HashMap::new();
570 let mut senders = HashMap::new();
571 runners.insert(
572 "s1".to_string(),
573 terminal_runner(&runner_tx, TTL + 100, now),
574 );
575 senders.insert("s1".to_string(), sender_tx.clone());
576
577 let evicted = evict_idle_session_entries(&mut runners, &mut senders, TTL, now, None);
578
579 assert_eq!(evicted, 1, "runner (no receivers) is evicted");
580 assert!(runners.is_empty());
581 assert!(
582 senders.contains_key("s1"),
583 "sender with a live receiver must be retained even when the runner is dropped"
584 );
585 }
586}