Skip to main content

bamboo_server/app_state/
init.rs

1//! Infrastructure initialization helpers.
2//!
3//! These functions create domain-level services (storage, skill manager, provider handles,
4//! MCP servers, metrics, permissions, schedules) that are shared between `AppState`
5//! and `AgentRuntime`.
6
7use 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
34/// Type alias for the permission checker trait object.
35pub(super) type PermissionChecker = dyn bamboo_tools::permission::PermissionChecker;
36
37/// Type alias for the provider lock + handle pair returned by [`build_provider_handles`].
38type ProviderHandles = (Arc<RwLock<Arc<dyn LLMProvider>>>, Arc<dyn LLMProvider>);
39
40/// Initialize storage components (session store + storage trait object).
41///
42/// Spawns a background task to rebuild the search index on startup.
43pub 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    // One-shot, idempotent migration: split each legacy session's runtime
59    // control-plane into a `runtime.json` sidecar so the O(1) runtime-save path
60    // (used heavily during sub-agent spawn) is active immediately. Run inline
61    // before the server starts serving so it cannot race concurrent saves; it is
62    // a no-op on already-migrated stores (marker file).
63    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            // Non-fatal: loading tolerates a missing sidecar. Log and continue.
68            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
114/// Initialize the skill manager with workspace directory and active mode from environment.
115pub 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
135/// Build reloadable provider handles.
136///
137/// Returns `(provider_lock, provider_handle)` where `provider_lock` is the
138/// hot-reloadable `RwLock` and `provider_handle` is a stable `ReloadableProvider`
139/// that always delegates to the latest value in the lock.
140pub 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
148/// Load permission configuration from disk.
149///
150/// Falls back to disabled permissions if no config exists or loading fails.
151pub async fn load_permission_checker(bamboo_home_dir: &Path) -> Arc<PermissionChecker> {
152    let storage = bamboo_tools::permission::storage::PermissionStorage::new(bamboo_home_dir);
153    let permission_config = match storage.load().await {
154        Ok(Some(config)) => config,
155        Ok(None) => {
156            let cfg = bamboo_tools::permission::PermissionConfig::new();
157            cfg.set_enabled(false);
158            cfg
159        }
160        Err(error) => {
161            tracing::warn!("Failed to load permission config; defaulting to disabled: {error}");
162            let cfg = bamboo_tools::permission::PermissionConfig::new();
163            cfg.set_enabled(false);
164            cfg
165        }
166    };
167    permission_config.cleanup_expired_grants();
168    Arc::new(bamboo_tools::permission::ConfigPermissionChecker::new(
169        Arc::new(permission_config),
170    ))
171}
172
173/// Initialize MCP server manager with background server initialization.
174pub fn init_mcp_manager(config: Arc<RwLock<Config>>) -> Arc<McpServerManager> {
175    let mcp_manager = Arc::new(McpServerManager::new_with_config(config.clone()));
176
177    // Initialize MCP servers in the background so the HTTP API is responsive quickly.
178    {
179        let mcp_manager = mcp_manager.clone();
180        let config = config.clone();
181        tokio::spawn(async move {
182            let mcp_config = config.read().await.mcp.clone();
183            mcp_manager.initialize_from_config(&mcp_config).await;
184        });
185    }
186
187    mcp_manager
188}
189
190/// Initialize metrics service with SQLite backend.
191pub async fn init_metrics_service(data_dir: &Path) -> Result<Arc<MetricsService>, AppError> {
192    let service = MetricsService::new(data_dir.join("metrics.db"))
193        .await
194        .map_err(|error| {
195            tracing::error!("Failed to initialize metrics storage: {}", error);
196            AppError::InternalError(anyhow::anyhow!(
197                "Failed to initialize metrics storage: {error}"
198            ))
199        })?;
200    Ok(Arc::new(service))
201}
202
203/// Spawn a background task that cleans up completed agent runners after 5 minutes.
204pub fn spawn_runner_cleanup_task(
205    runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
206    log_prefix: Option<&'static str>,
207) {
208    tokio::spawn(async move {
209        loop {
210            tokio::time::sleep(Duration::from_secs(60)).await;
211
212            let mut runners_guard = runners.write().await;
213            let now = Utc::now();
214
215            runners_guard.retain(|session_id, runner| {
216                let should_keep = match &runner.status {
217                    AgentStatus::Running => true,
218                    _ => {
219                        let age = now.signed_duration_since(
220                            runner.completed_at.unwrap_or(runner.started_at),
221                        );
222                        age.num_seconds() < 300 // 5 minute TTL
223                    }
224                };
225
226                if !should_keep {
227                    if let Some(prefix) = log_prefix {
228                        tracing::debug!("[{}:{}] Cleaning up completed runner", prefix, session_id);
229                    } else {
230                        tracing::debug!("[{}] Cleaning up completed runner", session_id);
231                    }
232                }
233
234                should_keep
235            });
236        }
237    });
238}
239
240/// Initialize schedule store for timed tasks.
241pub async fn init_schedule_store(data_dir: &PathBuf) -> Result<Arc<ScheduleStore>, AppError> {
242    let store = ScheduleStore::new(data_dir.clone())
243        .await
244        .map_err(|error| {
245            tracing::error!(
246                "Failed to initialize ScheduleStore at {:?}: {}",
247                data_dir,
248                error
249            );
250            AppError::StorageError(error)
251        })?;
252    Ok(Arc::new(store))
253}
254
255/// Build sub-session spawn scheduler.
256#[allow(clippy::too_many_arguments)]
257pub fn build_spawn_scheduler(
258    agent: Arc<Agent>,
259    child_tools: Arc<dyn bamboo_agent_core::tools::ToolExecutor>,
260    sessions: bamboo_engine::SessionCache,
261    agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
262    session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
263    external_child_runner: Option<Arc<dyn bamboo_engine::runtime::execution::ExternalChildRunner>>,
264    provider_router: Option<Arc<bamboo_llm::ProviderModelRouter>>,
265    completion_handler: Option<Arc<dyn bamboo_engine::execution::ChildCompletionHandler>>,
266    app_data_dir: Option<std::path::PathBuf>,
267    account_feed_inbox: Option<bamboo_engine::execution::AccountFeedInbox>,
268) -> Arc<SpawnScheduler> {
269    Arc::new(SpawnScheduler::new(SpawnContext {
270        agent,
271        tools: child_tools,
272        sessions_cache: sessions,
273        agent_runners,
274        session_event_senders,
275        external_child_runner,
276        provider_router,
277        app_data_dir,
278        completion_handler,
279        account_feed_inbox,
280    }))
281}
282
283/// Build schedule manager with minimal tool surface for background automation.
284#[allow(clippy::too_many_arguments)]
285pub fn build_schedule_manager(
286    schedule_store: Arc<ScheduleStore>,
287    agent: Arc<Agent>,
288    tools_for_schedules: Arc<dyn bamboo_agent_core::tools::ToolExecutor>,
289    sessions: bamboo_engine::SessionCache,
290    agent_runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
291    session_event_senders: Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
292    persistence: Arc<LockedSessionStore>,
293    config: Arc<RwLock<Config>>,
294    provider_registry: Arc<bamboo_llm::ProviderRegistry>,
295    app_data_dir: Option<std::path::PathBuf>,
296    account_feed_inbox: Option<bamboo_engine::execution::AccountFeedInbox>,
297) -> Arc<ScheduleManager> {
298    let base_ctx = ScheduleContext {
299        schedule_store,
300        agent,
301        tools: tools_for_schedules,
302        sessions_cache: sessions,
303        agent_runners,
304        session_event_senders,
305        account_feed_inbox,
306        persistence,
307        app_data_dir,
308        trigger_engine: crate::schedule_app::default_trigger_engine(),
309        resolve_run_config: Arc::new(|_| unimplemented!("replaced by build_schedule_context")),
310    };
311    Arc::new(ScheduleManager::new(build_schedule_context(
312        base_ctx,
313        config,
314        provider_registry,
315    )))
316}