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) -> Arc<ScheduleManager> {
408 let base_ctx = ScheduleContext {
409 schedule_store,
410 agent,
411 tools: tools_for_schedules,
412 sessions_cache: sessions,
413 agent_runners,
414 session_event_senders,
415 account_feed_inbox,
416 persistence,
417 app_data_dir,
418 trigger_engine: crate::schedule_app::default_trigger_engine(),
419 resolve_run_config: Arc::new(|_| unimplemented!("replaced by build_schedule_context")),
420 };
421 Arc::new(ScheduleManager::new(build_schedule_context(
422 base_ctx,
423 config,
424 provider_registry,
425 )))
426}
427
428#[cfg(test)]
429mod eviction_tests {
430 use super::*;
431 use chrono::Duration as ChronoDuration;
432
433 const TTL: i64 = SESSION_MAP_IDLE_TTL_SECS;
434
435 fn terminal_runner(
438 tx: &broadcast::Sender<AgentEvent>,
439 completed_secs_ago: i64,
440 now: chrono::DateTime<Utc>,
441 ) -> AgentRunner {
442 let mut runner = AgentRunner::new();
443 runner.status = AgentStatus::Completed;
444 runner.completed_at = Some(now - ChronoDuration::seconds(completed_secs_ago));
445 runner.event_sender = tx.clone();
446 runner
447 }
448
449 fn insert_pair(
450 runners: &mut HashMap<String, AgentRunner>,
451 senders: &mut HashMap<String, broadcast::Sender<AgentEvent>>,
452 id: &str,
453 runner: AgentRunner,
454 tx: broadcast::Sender<AgentEvent>,
455 ) {
456 runners.insert(id.to_string(), runner);
457 senders.insert(id.to_string(), tx);
458 }
459
460 #[test]
461 fn evicts_idle_terminal_pair_past_ttl_with_no_receivers() {
462 let now = Utc::now();
463 let (tx, _) = broadcast::channel::<AgentEvent>(16); let mut runners = HashMap::new();
465 let mut senders = HashMap::new();
466 insert_pair(
467 &mut runners,
468 &mut senders,
469 "s1",
470 terminal_runner(&tx, TTL + 100, now),
471 tx.clone(),
472 );
473
474 let evicted = evict_idle_session_entries(&mut runners, &mut senders, TTL, now, None);
475
476 assert_eq!(evicted, 1);
477 assert!(runners.is_empty(), "terminal idle runner must be dropped");
478 assert!(
479 senders.is_empty(),
480 "paired session sender must be dropped together with the runner"
481 );
482 }
483
484 #[test]
485 fn retains_terminal_runner_with_live_receiver() {
486 let now = Utc::now();
487 let (tx, _) = broadcast::channel::<AgentEvent>(16);
488 let _live_rx = tx.subscribe();
490 assert_eq!(tx.receiver_count(), 1);
491
492 let mut runners = HashMap::new();
493 let mut senders = HashMap::new();
494 insert_pair(
495 &mut runners,
496 &mut senders,
497 "s1",
498 terminal_runner(&tx, TTL + 100, now),
499 tx.clone(),
500 );
501
502 let evicted = evict_idle_session_entries(&mut runners, &mut senders, TTL, now, None);
503
504 assert_eq!(evicted, 0, "must not evict while a receiver is live");
505 assert!(runners.contains_key("s1"));
506 assert!(senders.contains_key("s1"));
507 }
508
509 #[test]
510 fn retains_young_terminal_runner() {
511 let now = Utc::now();
512 let (tx, _) = broadcast::channel::<AgentEvent>(16);
513 let mut runners = HashMap::new();
514 let mut senders = HashMap::new();
515 insert_pair(
517 &mut runners,
518 &mut senders,
519 "s1",
520 terminal_runner(&tx, 60, now),
521 tx.clone(),
522 );
523
524 let evicted = evict_idle_session_entries(&mut runners, &mut senders, TTL, now, None);
525
526 assert_eq!(
527 evicted, 0,
528 "must preserve the post-completion replay window"
529 );
530 assert!(runners.contains_key("s1"));
531 assert!(senders.contains_key("s1"));
532 }
533
534 #[test]
535 fn retains_running_runner_regardless_of_age() {
536 let now = Utc::now();
537 let (tx, _) = broadcast::channel::<AgentEvent>(16);
538 let mut runner = AgentRunner::new();
539 runner.status = AgentStatus::Running;
540 runner.started_at = now - ChronoDuration::seconds(TTL + 100_000);
541 runner.completed_at = None;
542 runner.event_sender = tx.clone();
543
544 let mut runners = HashMap::new();
545 let mut senders = HashMap::new();
546 insert_pair(&mut runners, &mut senders, "s1", runner, tx.clone());
547
548 let evicted = evict_idle_session_entries(&mut runners, &mut senders, TTL, now, None);
549
550 assert_eq!(evicted, 0, "a Running runner is never evicted");
551 assert!(runners.contains_key("s1"));
552 assert!(senders.contains_key("s1"));
553 }
554
555 #[test]
556 fn keeps_sender_if_it_has_receivers_even_when_runner_evicted() {
557 let now = Utc::now();
562 let (runner_tx, _) = broadcast::channel::<AgentEvent>(16);
563 let (sender_tx, _) = broadcast::channel::<AgentEvent>(16);
565 let _live = sender_tx.subscribe();
566
567 let mut runners = HashMap::new();
568 let mut senders = HashMap::new();
569 runners.insert(
570 "s1".to_string(),
571 terminal_runner(&runner_tx, TTL + 100, now),
572 );
573 senders.insert("s1".to_string(), sender_tx.clone());
574
575 let evicted = evict_idle_session_entries(&mut runners, &mut senders, TTL, now, None);
576
577 assert_eq!(evicted, 1, "runner (no receivers) is evicted");
578 assert!(runners.is_empty());
579 assert!(
580 senders.contains_key("s1"),
581 "sender with a live receiver must be retained even when the runner is dropped"
582 );
583 }
584}